1
0
mirror of https://github.com/nicoverbruggen/phpmon.git synced 2025-11-06 12:50:06 +01: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 { class FakeServicesManager: ServicesManager {
var fixedFormulae: [String] = [] var fixedFormulae: [String] = []
var fixedStatus: ServiceStatus = .loading var fixedStatus: ServiceStatus = .active
override init() {} override init() {}
init( init(
formulae: [String] = ["php", "nginx", "dnsmasq"], formulae: [String] = ["php", "nginx", "dnsmasq"],
status: ServiceStatus = .loading status: ServiceStatus = .active
) { ) {
super.init() super.init()
@@ -27,9 +27,10 @@ class FakeServicesManager: ServicesManager {
self.fixedStatus = status self.fixedStatus = status
self.serviceWrappers = self.formulae.map { self.serviceWrappers = self.formulae.map {
let wrapper = ServiceWrapper(formula: $0) let wrapper = ServiceWrapper(
wrapper.isBusy = (status == .loading) formula: $0,
wrapper.service = HomebrewService.dummy(named: $0.name, enabled: true) service: HomebrewService.dummy(named: $0.name, enabled: true)
)
return wrapper return wrapper
} }
} }
@@ -42,35 +43,9 @@ class FakeServicesManager: ServicesManager {
override func reloadServicesStatus() async { override func reloadServicesStatus() async {
await delay(seconds: 0.3) await delay(seconds: 0.3)
for formula in self.serviceWrappers {
formula.service?.running = true
formula.isBusy = false
}
broadcastServicesUpdated()
} }
override func toggleService(named: String) async { override func toggleService(named: String) async {
guard let wrapper = self[named] else { await delay(seconds: 0.3)
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()
}
} }
} }

View File

@@ -14,7 +14,6 @@ import Foundation
public enum ServiceStatus: String { public enum ServiceStatus: String {
case active case active
case inactive case inactive
case loading
case missing case missing
} }
@@ -22,40 +21,28 @@ public enum ServiceStatus: String {
Service wrapper, that contains the Homebrew JSON output (if determined) and the formula. 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. 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 formula: HomebrewFormula
var service: HomebrewService? var status: ServiceStatus = .missing
var isBusy: Bool = false
public var name: String { public var name: String {
return formula.name return formula.name
} }
public var status: ServiceStatus { init(formula: HomebrewFormula, service: HomebrewService? = nil) {
if isBusy {
return .loading
}
guard let service = self.service else {
return .missing
}
return service.running ? .active : .inactive
}
init(formula: HomebrewFormula) {
self.formula = formula self.formula = formula
self.isBusy = true
if service != nil {
self.status = service!.running ? .active : .inactive
}
} }
public static func == (lhs: ServiceWrapper, rhs: ServiceWrapper) -> Bool { public static func == (lhs: ServiceWrapper, rhs: ServiceWrapper) -> Bool {
return lhs.service == rhs.service return lhs.hashValue == rhs.hashValue
&& lhs.formula == rhs.formula
} }
public func hash(into hasher: inout Hasher) { public func hash(into hasher: inout Hasher) {
hasher.combine(formula) hasher.combine(formula)
hasher.combine(service) hasher.combine(status)
} }
} }

View File

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

View File

@@ -16,6 +16,11 @@ class ValetServicesManager: ServicesManager {
Task { await self.reloadServicesStatus() } 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 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 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) }) .filter({ return userServiceNames.contains($0.name) })
} }
// Ensure both commands complete (but run concurrently) self.homebrewServices = []
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
}
for wrapper in serviceWrappers { for await services in group {
wrapper.isBusy = false homebrewServices.append(contentsOf: services)
}
} }
// Broadcast that all services have been updated Task { @MainActor in
self.broadcastServicesUpdated() // 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.") 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 // 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)" let command = "services \(action) \(wrapper.formula.name)"
// Run the command // Run the command

View File

@@ -57,7 +57,7 @@ struct ServicesView: View {
VStack(alignment: .leading, spacing: CGFloat(self.rowSpacing)) { VStack(alignment: .leading, spacing: CGFloat(self.rowSpacing)) {
ForEach(manager.serviceWrappers.chunked(by: perRow), id: \.self) { chunk in ForEach(manager.serviceWrappers.chunked(by: perRow), id: \.self) { chunk in
HStack { HStack {
ForEach(chunk) { service in ForEach(chunk, id: \.self) { service in
ServiceView(service: service) ServiceView(service: service)
.frame(minWidth: 70) .frame(minWidth: 70)
} }
@@ -86,7 +86,8 @@ struct ServicesView: View {
} }
struct ServiceView: View { struct ServiceView: View {
@ObservedObject var service: ServiceWrapper var service: ServiceWrapper
@State var isBusy: Bool = false
var body: some View { var body: some View {
VStack(alignment: .center, spacing: 0) { VStack(alignment: .center, spacing: 0) {
@@ -95,67 +96,58 @@ struct ServiceView: View {
.frame(minWidth: 70, alignment: .center) .frame(minWidth: 70, alignment: .center)
.padding(.top, 4) .padding(.top, 4)
.padding(.bottom, 2) .padding(.bottom, 2)
if service.status == .loading { if isBusy {
ProgressView() ProgressView()
.scaleEffect(x: 0.4, y: 0.4, anchor: .center) .scaleEffect(x: 0.4, y: 0.4, anchor: .center)
.frame(minWidth: 70, alignment: .center) .frame(minWidth: 70, alignment: .center)
.frame(width: 25, height: 25) .frame(width: 25, height: 25)
} } else {
if service.status == .missing { if service.status == .missing {
Button { Button {
Task { @MainActor in Task { @MainActor in
BetterAlert().withInformation( BetterAlert().withInformation(
title: "alert.warnings.service_missing.title".localized, title: "alert.warnings.service_missing.title".localized,
subtitle: "alert.warnings.service_missing.subtitle".localized, subtitle: "alert.warnings.service_missing.subtitle".localized,
description: "alert.warnings.service_missing.description".localized description: "alert.warnings.service_missing.description".localized
) )
.withPrimary(text: "OK") .withPrimary(text: "OK")
.show() .show()
}
} label: {
Text("?")
} }
} label: { .focusable(false)
Text("?") // .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) }.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 { struct ServicesView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
ServicesView(manager: FakeServicesManager( ServicesView(manager: FakeServicesManager(
formulae: ["php", "nginx", "dnsmasq"], formulae: ["php", "nginx", "dnsmasq"],
status: .loading status: .active
), perRow: 4) ), perRow: 4)
.frame(width: 330.0) .frame(width: 330.0)
.previewDisplayName("Loading") .previewDisplayName("Loading")