1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-08-07 03:50:08 +02:00

🏗 WIP: Functional service toggling

This commit is contained in:
2023-01-06 20:30:25 +01:00
parent 456948ffd9
commit 684a53fc4a
5 changed files with 77 additions and 129 deletions

View File

@ -10,13 +10,13 @@ import Foundation
class FakeServicesManager: ServicesManager {
var fixedFormulae: [String] = []
var fixedStatus: ServiceStatus = .loading
var fixedStatus: ServiceStatus = .active
override init() {}
init(
formulae: [String] = ["php", "nginx", "dnsmasq"],
status: ServiceStatus = .loading
status: ServiceStatus = .active
) {
super.init()
@ -27,9 +27,10 @@ class FakeServicesManager: ServicesManager {
self.fixedStatus = status
self.serviceWrappers = self.formulae.map {
let wrapper = ServiceWrapper(formula: $0)
wrapper.isBusy = (status == .loading)
wrapper.service = HomebrewService.dummy(named: $0.name, enabled: true)
let wrapper = ServiceWrapper(
formula: $0,
service: HomebrewService.dummy(named: $0.name, enabled: true)
)
return wrapper
}
}
@ -42,35 +43,9 @@ class FakeServicesManager: ServicesManager {
override func reloadServicesStatus() async {
await delay(seconds: 0.3)
for formula in self.serviceWrappers {
formula.service?.running = true
formula.isBusy = false
}
broadcastServicesUpdated()
}
override func toggleService(named: String) async {
guard let wrapper = self[named] else {
return Log.err("The wrapper for service \(named) does not exist.")
}
wrapper.isBusy = true
Task { @MainActor in
wrapper.objectWillChange.send()
}
guard let service = wrapper.service else {
return Log.err("The actual service for wrapper \(named) is nil.")
}
await delay(seconds: 2)
service.running = !service.running
wrapper.isBusy = false
Task { @MainActor in
wrapper.objectWillChange.send()
self.objectWillChange.send()
}
await delay(seconds: 0.3)
}
}

View File

@ -14,7 +14,6 @@ import Foundation
public enum ServiceStatus: String {
case active
case inactive
case loading
case missing
}
@ -22,40 +21,28 @@ public enum ServiceStatus: String {
Service wrapper, that contains the Homebrew JSON output (if determined) and the formula.
This helps the app determine whether a service should run as an administrator or not.
*/
public class ServiceWrapper: ObservableObject, Identifiable, Hashable {
public struct ServiceWrapper: Hashable {
var formula: HomebrewFormula
var service: HomebrewService?
var isBusy: Bool = false
var status: ServiceStatus = .missing
public var name: String {
return formula.name
}
public var status: ServiceStatus {
if isBusy {
return .loading
}
guard let service = self.service else {
return .missing
}
return service.running ? .active : .inactive
}
init(formula: HomebrewFormula) {
init(formula: HomebrewFormula, service: HomebrewService? = nil) {
self.formula = formula
self.isBusy = true
if service != nil {
self.status = service!.running ? .active : .inactive
}
}
public static func == (lhs: ServiceWrapper, rhs: ServiceWrapper) -> Bool {
return lhs.service == rhs.service
&& lhs.formula == rhs.formula
return lhs.hashValue == rhs.hashValue
}
public func hash(into hasher: inout Hasher) {
hasher.combine(formula)
hasher.combine(service)
hasher.combine(status)
}
}

View File

@ -18,7 +18,7 @@ class ServicesManager: ObservableObject {
public static func useFake() {
ServicesManager.shared = FakeServicesManager.init(
formulae: ["php", "nginx", "dnsmasq", "mysql"],
status: .loading
status: .active
)
}
@ -39,9 +39,6 @@ class ServicesManager: ObservableObject {
let statuses = self.serviceWrappers[0...2].map { $0.status }
if statuses.contains(.loading) {
return "Determining Valet status..."
}
if statuses.contains(.missing) {
return "A key service is not installed."
}
@ -58,9 +55,6 @@ class ServicesManager: ObservableObject {
}
let statuses = self.serviceWrappers[0...2].map { $0.status }
if statuses.contains(.loading) {
return .orange
}
if statuses.contains(.missing) {
return .red
}
@ -92,10 +86,6 @@ class ServicesManager: ObservableObject {
*/
public func broadcastServicesUpdated() {
Task { @MainActor in
self.serviceWrappers.forEach { wrapper in
wrapper.objectWillChange.send()
}
self.objectWillChange.send()
}
}

View File

@ -16,6 +16,11 @@ class ValetServicesManager: ServicesManager {
Task { await self.reloadServicesStatus() }
}
/**
The last known state of all Homebrew services.
*/
var homebrewServices: [HomebrewService] = []
/**
This method allows us to reload the Homebrew services, but we run this command
twice (once for user services, and once for root services). Please note that
@ -53,20 +58,23 @@ class ValetServicesManager: ServicesManager {
.filter({ return userServiceNames.contains($0.name) })
}
// Ensure both commands complete (but run concurrently)
for await services in group {
// For both groups (user and root services), set the service to the wrapper object
for service in services {
self[service.name]?.service = service
}
self.homebrewServices = []
for wrapper in serviceWrappers {
wrapper.isBusy = false
}
for await services in group {
homebrewServices.append(contentsOf: services)
}
// Broadcast that all services have been updated
self.broadcastServicesUpdated()
Task { @MainActor in
// Ensure both commands complete (but run concurrently)
serviceWrappers = formulae.map { formula in
ServiceWrapper(formula: formula, service: homebrewServices.first(where: { service in
service.name == formula.name
}))
}
// Broadcast that all services have been updated
self.broadcastServicesUpdated()
}
})
}
@ -75,12 +83,8 @@ class ValetServicesManager: ServicesManager {
return Log.err("The wrapper for '\(named)' is missing.")
}
if wrapper.service == nil {
return Log.err("The Homebrew service for \(named) is missing.")
}
// Prepare the appropriate command to stop or start a service
let action = wrapper.service!.running ? "stop" : "start"
let action = wrapper.status == .active ? "stop" : "start"
let command = "services \(action) \(wrapper.formula.name)"
// Run the command

View File

@ -57,7 +57,7 @@ struct ServicesView: View {
VStack(alignment: .leading, spacing: CGFloat(self.rowSpacing)) {
ForEach(manager.serviceWrappers.chunked(by: perRow), id: \.self) { chunk in
HStack {
ForEach(chunk) { service in
ForEach(chunk, id: \.self) { service in
ServiceView(service: service)
.frame(minWidth: 70)
}
@ -86,7 +86,8 @@ struct ServicesView: View {
}
struct ServiceView: View {
@ObservedObject var service: ServiceWrapper
var service: ServiceWrapper
@State var isBusy: Bool = false
var body: some View {
VStack(alignment: .center, spacing: 0) {
@ -95,67 +96,58 @@ struct ServiceView: View {
.frame(minWidth: 70, alignment: .center)
.padding(.top, 4)
.padding(.bottom, 2)
if service.status == .loading {
if isBusy {
ProgressView()
.scaleEffect(x: 0.4, y: 0.4, anchor: .center)
.frame(minWidth: 70, alignment: .center)
.frame(width: 25, height: 25)
}
if service.status == .missing {
Button {
Task { @MainActor in
BetterAlert().withInformation(
title: "alert.warnings.service_missing.title".localized,
subtitle: "alert.warnings.service_missing.subtitle".localized,
description: "alert.warnings.service_missing.description".localized
)
.withPrimary(text: "OK")
.show()
} else {
if service.status == .missing {
Button {
Task { @MainActor in
BetterAlert().withInformation(
title: "alert.warnings.service_missing.title".localized,
subtitle: "alert.warnings.service_missing.subtitle".localized,
description: "alert.warnings.service_missing.description".localized
)
.withPrimary(text: "OK")
.show()
}
} label: {
Text("?")
}
} label: {
Text("?")
.focusable(false)
// .buttonStyle(BlueButton())
.frame(minWidth: 70, alignment: .center)
}
if service.status == .active || service.status == .inactive {
Button {
Task {
isBusy = true
await ServicesManager.shared.toggleService(named: service.name)
isBusy = false
}
} label: {
Image(
systemName: service.status == .active ? "checkmark" : "xmark"
)
.resizable()
.frame(width: 12.0, height: 12.0)
.foregroundColor(
service.status == .active ? Color("IconColorGreen") : Color("IconColorRed")
)
}.frame(width: 25, height: 25)
}
.focusable(false)
// .buttonStyle(BlueButton())
.frame(minWidth: 70, alignment: .center)
}
if service.status == .active || service.status == .inactive {
Button {
Task { await ServicesManager.shared.toggleService(named: service.name) }
} label: {
Image(
systemName: service.status == .active ? "checkmark" : "xmark"
)
.resizable()
.frame(width: 12.0, height: 12.0)
.foregroundColor(
service.status == .active ? Color("IconColorGreen") : Color("IconColorRed")
)
}.frame(width: 25, height: 25)
}
}.frame(minWidth: 70)
}
}
public struct BlueButton: ButtonStyle {
public func makeBody(configuration: Configuration) -> some View {
configuration.label
.frame(width: 25, height: 25)
.background(configuration.isPressed
? Color(red: 0, green: 0.5, blue: 0.9)
: Color(red: 0, green: 0, blue: 0.5)
)
.foregroundColor(.white)
.clipShape(Capsule())
.contentShape(Rectangle())
}
}
struct ServicesView_Previews: PreviewProvider {
static var previews: some View {
ServicesView(manager: FakeServicesManager(
formulae: ["php", "nginx", "dnsmasq"],
status: .loading
status: .active
), perRow: 4)
.frame(width: 330.0)
.previewDisplayName("Loading")