UTMServerView.swift 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. //
  2. // Copyright © 2023 osy. All rights reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License");
  5. // you may not use this file except in compliance with the License.
  6. // You may obtain a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS,
  12. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. // See the License for the specific language governing permissions and
  14. // limitations under the License.
  15. //
  16. import SwiftUI
  17. @available(macOS 13, *)
  18. struct UTMServerView: View {
  19. @EnvironmentObject private var remoteServer: UTMRemoteServer.State
  20. @State private var isDeletingAll: Bool = false
  21. var body: some View {
  22. VStack(alignment: .leading) {
  23. HStack {
  24. Toggle("Enable UTM Server", isOn: Binding<Bool>(get: {
  25. remoteServer.isServerActive
  26. }, set: { value in
  27. if value {
  28. remoteServer.requestServerAction(.start)
  29. } else {
  30. remoteServer.requestServerAction(.stop)
  31. }
  32. }))
  33. Spacer()
  34. Button {
  35. isDeletingAll = true
  36. } label: {
  37. Text("Reset Identity")
  38. }
  39. .alert("Confirmation", isPresented: $isDeletingAll) {
  40. Button(role: .destructive) {
  41. remoteServer.allClients.removeAll()
  42. remoteServer.requestServerAction(.reset)
  43. } label: {
  44. Text("Reset Identity")
  45. }.keyboardShortcut(.defaultAction)
  46. } message: {
  47. Text("Do you want to forget all clients and generate a new server identity? Any clients that previously paired with this server will be instructed to manually unpair with this server before they can connect again.")
  48. }
  49. }.padding([.top, .leading, .trailing])
  50. ServerOverview()
  51. Divider()
  52. HStack {
  53. if let address = remoteServer.externalIPAddress, let port = remoteServer.externalPort {
  54. Text("Server IP: \(address), Port: \(String(port))")
  55. .textSelection(.enabled)
  56. }
  57. Spacer()
  58. if remoteServer.isServerActive {
  59. Image(systemName: "circle.fill")
  60. .foregroundStyle(.green)
  61. Text("Running")
  62. } else {
  63. Image(systemName: "circle.fill")
  64. .foregroundStyle(.red)
  65. Text("Stopped")
  66. }
  67. }.padding([.bottom, .leading, .trailing])
  68. }.disabled(remoteServer.isBusy)
  69. }
  70. }
  71. @available(macOS 13, *)
  72. fileprivate struct ServerOverview: View {
  73. @EnvironmentObject private var remoteServer: UTMRemoteServer.State
  74. @State private var sortOrder = [KeyPathComparator(\UTMRemoteServer.State.Client.name)]
  75. @State private var selectedFingerprints = Set<UTMRemoteServer.State.ClientFingerprint>()
  76. @State private var isDeleting: Bool = false
  77. var body: some View {
  78. Table(remoteServer.allClients, selection: $selectedFingerprints, sortOrder: $sortOrder) {
  79. TableColumn("") { client in
  80. if remoteServer.isConnected(client.fingerprint) {
  81. Image(systemName: "circle.fill")
  82. .foregroundStyle(.green)
  83. }
  84. }.width(16)
  85. TableColumn("Name", value: \.name)
  86. .width(ideal: 200)
  87. TableColumn("Fingerprint") { client in
  88. Text((client.fingerprint ^ remoteServer.serverFingerprint).hexString())
  89. }.width(ideal: 300)
  90. TableColumn("Last Seen", value: \.lastSeen) { client in
  91. Text(DateFormatter.localizedString(from: client.lastSeen, dateStyle: .short, timeStyle: .short))
  92. }.width(ideal: 150)
  93. TableColumn("Status") { client in
  94. if remoteServer.isConnected(client.fingerprint) {
  95. Text("Connected")
  96. } else if remoteServer.isBlocked(client.fingerprint) {
  97. Text("Blocked")
  98. } else if !remoteServer.isApproved(client.fingerprint) {
  99. HStack {
  100. Button {
  101. remoteServer.approve(client.fingerprint)
  102. } label: {
  103. Text("Approve")
  104. }.buttonStyle(.bordered)
  105. Button {
  106. remoteServer.block(client.fingerprint)
  107. } label: {
  108. Text("Block")
  109. }.buttonStyle(.bordered)
  110. }
  111. }
  112. }.width(ideal: 140)
  113. }
  114. .contextMenu(forSelectionType: UTMRemoteServer.State.ClientFingerprint.self) { items in
  115. if items.count == 1 {
  116. if remoteServer.isConnected(items.first!) {
  117. Button {
  118. remoteServer.disconnect(items.first!)
  119. } label: {
  120. Text("Disconnect")
  121. }
  122. }
  123. if !remoteServer.isApproved(items.first!) {
  124. Button {
  125. remoteServer.approve(items.first!)
  126. } label: {
  127. Text("Approve")
  128. }
  129. }
  130. if !remoteServer.isBlocked(items.first!) {
  131. Button {
  132. remoteServer.block(items.first!)
  133. } label: {
  134. Text("Block")
  135. }
  136. }
  137. }
  138. if items.count > 0 {
  139. Button {
  140. isDeleting = true
  141. selectedFingerprints = items
  142. } label: {
  143. Text("Delete")
  144. }
  145. }
  146. }
  147. .onChange(of: sortOrder) {
  148. remoteServer.allClients.sort(using: $0)
  149. }
  150. .onDeleteCommand {
  151. isDeleting = true
  152. }
  153. .alert("Confirmation", isPresented: $isDeleting) {
  154. Button(role: .destructive) {
  155. remoteServer.allClients.removeAll(where: { selectedFingerprints.contains($0.fingerprint) })
  156. } label: {
  157. Text("Delete")
  158. }.keyboardShortcut(.defaultAction)
  159. } message: {
  160. Text("Do you want to forget the selected client(s)?")
  161. }
  162. }
  163. }
  164. @available(macOS 13, *)
  165. #Preview {
  166. UTMServerView()
  167. }