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:
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user