Skip to content

Commit ce1883e

Browse files
committed
fix: display offline workspaces
1 parent e64ea22 commit ce1883e

File tree

11 files changed

+404
-287
lines changed

11 files changed

+404
-287
lines changed

Coder Desktop/Coder Desktop/Preview Content/PreviewVPN.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import SwiftUI
44
@MainActor
55
final class PreviewVPN: Coder_Desktop.VPNService {
66
@Published var state: Coder_Desktop.VPNServiceState = .disabled
7-
@Published var agents: [UUID: Coder_Desktop.Agent] = [
7+
@Published var menuState: VPNMenuState = .init(agents: [
88
UUID(): Agent(id: UUID(), name: "dev", status: .error, copyableDNS: "asdf.coder", wsName: "dogfood2",
99
wsID: UUID()),
1010
UUID(): Agent(id: UUID(), name: "dev", status: .okay, copyableDNS: "asdf.coder",
@@ -25,7 +25,7 @@ final class PreviewVPN: Coder_Desktop.VPNService {
2525
wsID: UUID()),
2626
UUID(): Agent(id: UUID(), name: "dev", status: .off, copyableDNS: "asdf.coder", wsName: "example",
2727
wsID: UUID()),
28-
]
28+
], workspaces: [:])
2929
let shouldFail: Bool
3030
let longError = "This is a long error to test the UI with long error messages"
3131

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import Foundation
2+
import SwiftUI
3+
import VPNLib
4+
5+
struct Agent: Identifiable, Equatable, Comparable {
6+
let id: UUID
7+
let name: String
8+
let status: AgentStatus
9+
let copyableDNS: String
10+
let wsName: String
11+
let wsID: UUID
12+
13+
// Agents are sorted by status, and then by name
14+
static func < (lhs: Agent, rhs: Agent) -> Bool {
15+
if lhs.status != rhs.status {
16+
return lhs.status < rhs.status
17+
}
18+
return lhs.wsName.localizedCompare(rhs.wsName) == .orderedAscending
19+
}
20+
}
21+
22+
enum AgentStatus: Int, Equatable, Comparable {
23+
case okay = 0
24+
case warn = 1
25+
case error = 2
26+
case off = 3
27+
28+
public var color: Color {
29+
switch self {
30+
case .okay: .green
31+
case .warn: .yellow
32+
case .error: .red
33+
case .off: .gray
34+
}
35+
}
36+
37+
static func < (lhs: AgentStatus, rhs: AgentStatus) -> Bool {
38+
lhs.rawValue < rhs.rawValue
39+
}
40+
}
41+
42+
struct Workspace: Identifiable, Equatable, Comparable {
43+
let id: UUID
44+
let name: String
45+
var agents: [UUID]
46+
47+
static func < (lhs: Workspace, rhs: Workspace) -> Bool {
48+
lhs.name.localizedCompare(rhs.name) == .orderedAscending
49+
}
50+
}
51+
52+
struct VPNMenuState {
53+
var agents: [UUID: Agent] = [:]
54+
var workspaces: [UUID: Workspace] = [:]
55+
56+
mutating func upsertAgent(_ agent: Vpn_Agent) {
57+
guard let id = UUID(uuidData: agent.id) else { return }
58+
guard let wsID = UUID(uuidData: agent.workspaceID) else { return }
59+
// An existing agent with the same name, belonging to the same workspace
60+
// is from a previous workspace build, and should be removed.
61+
agents.filter { $0.value.name == agent.name && $0.value.wsID == wsID }
62+
.forEach { agents[$0.key] = nil }
63+
workspaces[wsID]?.agents.append(id)
64+
let wsName = workspaces[wsID]?.name ?? "Unknown Workspace"
65+
agents[id] = Agent(
66+
id: id,
67+
name: agent.name,
68+
// If last handshake was not within last five minutes, the agent is unhealthy
69+
status: agent.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .warn,
70+
// Choose the shortest hostname, and remove trailing dot if present
71+
copyableDNS: agent.fqdn.min(by: { $0.count < $1.count })
72+
.map { $0.hasSuffix(".") ? String($0.dropLast()) : $0 } ?? "UNKNOWN",
73+
wsName: wsName,
74+
wsID: wsID
75+
)
76+
}
77+
78+
mutating func deleteAgent(withId id: Data) {
79+
guard let id = UUID(uuidData: id) else { return }
80+
// Update Workspaces
81+
if let agent = agents[id], var ws = workspaces[agent.wsID] {
82+
ws.agents.removeAll { $0 == id }
83+
workspaces[agent.wsID] = ws
84+
}
85+
agents[id] = nil
86+
}
87+
88+
mutating func upsertWorkspace(_ workspace: Vpn_Workspace) {
89+
guard let id = UUID(uuidData: workspace.id) else { return }
90+
workspaces[id] = Workspace(id: id, name: workspace.name, agents: [])
91+
}
92+
93+
mutating func deleteWorkspace(withId id: Data) {
94+
guard let wsID = UUID(uuidData: id) else { return }
95+
agents.filter { _, value in
96+
value.wsID == wsID
97+
}.forEach { key, _ in
98+
agents[key] = nil
99+
}
100+
workspaces[wsID] = nil
101+
}
102+
103+
func sorted() -> [VPNMenuItem] {
104+
var items = agents.values.map { VPNMenuItem.agent($0) }
105+
// Workspaces with no agents are shown as offline
106+
items += workspaces.filter { _, value in
107+
value.agents.isEmpty
108+
}.map { VPNMenuItem.offlineWorkspace(Workspace(id: $0.key, name: $0.value.name, agents: $0.value.agents)) }
109+
return items.sorted()
110+
}
111+
112+
mutating func clear() {
113+
agents.removeAll()
114+
workspaces.removeAll()
115+
}
116+
}

Coder Desktop/Coder Desktop/VPNService.swift

Lines changed: 8 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import VPNLib
66
@MainActor
77
protocol VPNService: ObservableObject {
88
var state: VPNServiceState { get }
9-
var agents: [UUID: Agent] { get }
9+
var menuState: VPNMenuState { get }
1010
func start() async
1111
func stop() async
1212
func configureTunnelProviderProtocol(proto: NETunnelProviderProtocol?)
@@ -41,7 +41,6 @@ enum VPNServiceError: Error, Equatable {
4141
final class CoderVPNService: NSObject, VPNService {
4242
var logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "vpn")
4343
lazy var xpc: VPNXPCInterface = .init(vpn: self)
44-
var workspaces: [UUID: String] = [:]
4544

4645
@Published var tunnelState: VPNServiceState = .disabled
4746
@Published var sysExtnState: SystemExtensionState = .uninstalled
@@ -56,7 +55,7 @@ final class CoderVPNService: NSObject, VPNService {
5655
return tunnelState
5756
}
5857

59-
@Published var agents: [UUID: Agent] = [:]
58+
@Published var menuState: VPNMenuState = .init()
6059

6160
// systemExtnDelegate holds a reference to the SystemExtensionDelegate so that it doesn't get
6261
// garbage collected while the OSSystemExtensionRequest is in flight, since the OS framework
@@ -85,11 +84,6 @@ final class CoderVPNService: NSObject, VPNService {
8584
NotificationCenter.default.removeObserver(self)
8685
}
8786

88-
func clearPeers() {
89-
agents = [:]
90-
workspaces = [:]
91-
}
92-
9387
func start() async {
9488
switch tunnelState {
9589
case .disabled, .failed:
@@ -150,7 +144,7 @@ final class CoderVPNService: NSObject, VPNService {
150144
do {
151145
let msg = try Vpn_PeerUpdate(serializedBytes: data)
152146
debugPrint(msg)
153-
clearPeers()
147+
menuState.clear()
154148
applyPeerUpdate(with: msg)
155149
} catch {
156150
logger.error("failed to decode peer update \(error)")
@@ -159,53 +153,11 @@ final class CoderVPNService: NSObject, VPNService {
159153

160154
func applyPeerUpdate(with update: Vpn_PeerUpdate) {
161155
// Delete agents
162-
update.deletedAgents
163-
.compactMap { UUID(uuidData: $0.id) }
164-
.forEach { agentID in
165-
agents[agentID] = nil
166-
}
167-
update.deletedWorkspaces
168-
.compactMap { UUID(uuidData: $0.id) }
169-
.forEach { workspaceID in
170-
workspaces[workspaceID] = nil
171-
for (id, agent) in agents where agent.wsID == workspaceID {
172-
agents[id] = nil
173-
}
174-
}
175-
176-
// Update workspaces
177-
for workspaceProto in update.upsertedWorkspaces {
178-
if let workspaceID = UUID(uuidData: workspaceProto.id) {
179-
workspaces[workspaceID] = workspaceProto.name
180-
}
181-
}
182-
183-
for agentProto in update.upsertedAgents {
184-
guard let agentID = UUID(uuidData: agentProto.id) else {
185-
continue
186-
}
187-
guard let workspaceID = UUID(uuidData: agentProto.workspaceID) else {
188-
continue
189-
}
190-
let workspaceName = workspaces[workspaceID] ?? "Unknown Workspace"
191-
let newAgent = Agent(
192-
id: agentID,
193-
name: agentProto.name,
194-
// If last handshake was not within last five minutes, the agent is unhealthy
195-
status: agentProto.lastHandshake.date > Date.now.addingTimeInterval(-300) ? .okay : .off,
196-
copyableDNS: agentProto.fqdn.first ?? "UNKNOWN",
197-
wsName: workspaceName,
198-
wsID: workspaceID
199-
)
200-
201-
// An existing agent with the same name, belonging to the same workspace
202-
// is from a previous workspace build, and should be removed.
203-
agents
204-
.filter { $0.value.name == agentProto.name && $0.value.wsID == workspaceID }
205-
.forEach { agents[$0.key] = nil }
206-
207-
agents[agentID] = newAgent
208-
}
156+
update.deletedAgents.forEach { menuState.deleteAgent(withId: $0.id) }
157+
update.deletedWorkspaces.forEach { menuState.deleteWorkspace(withId: $0.id) }
158+
// Upsert workspaces before agents to populate agent workspace names
159+
update.upsertedWorkspaces.forEach { menuState.upsertWorkspace($0) }
160+
update.upsertedAgents.forEach { menuState.upsertAgent($0) }
209161
}
210162
}
211163

Coder Desktop/Coder Desktop/Views/Agent.swift

Lines changed: 0 additions & 99 deletions
This file was deleted.

Coder Desktop/Coder Desktop/Views/Agents.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ struct Agents<VPN: VPNService, S: Session>: View {
1212
Group {
1313
// Agents List
1414
if vpn.state == .connected {
15-
let sortedAgents = vpn.agents.values.sorted()
16-
let visibleData = viewAll ? sortedAgents[...] : sortedAgents.prefix(defaultVisibleRows)
17-
ForEach(visibleData, id: \.id) { agent in
18-
AgentRowView(agent: agent, baseAccessURL: session.baseAccessURL!)
15+
let items = vpn.menuState.sorted()
16+
let visibleItems = viewAll ? items[...] : items.prefix(defaultVisibleRows)
17+
ForEach(visibleItems, id: \.id) { agent in
18+
MenuItemView(item: agent, baseAccessURL: session.baseAccessURL!)
1919
.padding(.horizontal, Theme.Size.trayMargin)
2020
}
21-
if vpn.agents.count > defaultVisibleRows {
21+
if items.count > defaultVisibleRows {
2222
Toggle(isOn: $viewAll) {
23-
Text(viewAll ? "Show Less" : "Show All")
23+
Text(viewAll ? "Show less" : "Show all")
2424
.font(.headline)
2525
.foregroundColor(.gray)
2626
.padding(.horizontal, Theme.Size.trayInset)

Coder Desktop/Coder Desktop/Views/VPNMenu.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ struct VPNMenu<VPN: VPNService, S: Session>: View {
2828
.disabled(vpnDisabled)
2929
}
3030
Divider()
31-
Text("Workspace Agents")
31+
Text("Workspaces")
3232
.font(.headline)
3333
.foregroundColor(.gray)
3434
VPNState<VPN, S>()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy