Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit7ffa48b

Browse files
fix: display offline workspaces (#39)
1 parente64ea22 commit7ffa48b

16 files changed

+611
-309
lines changed

‎Coder Desktop/Coder Desktop/About.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import SwiftUI
22

33
enumAbout{
4+
publicstaticletrepo:String="https://github.com/coder/coder-desktop-macos"
45
privatestaticvarcredits:NSAttributedString{
56
letcoder=NSMutableAttributedString(
67
string:"Coder.com",
@@ -21,7 +22,7 @@ enum About {
2122
string:"GitHub",
2223
attributes:[
2324
.foregroundColor:NSColor.labelColor,
24-
.link:NSURL(string:"https://github.com/coder/coder-desktop-macos")!,
25+
.link:NSURL(string:About.repo)!,
2526
.font:NSFont.systemFont(ofSize:NSFont.systemFontSize),
2627
]
2728
)

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

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,29 +3,29 @@ import SwiftUI
33

44
@MainActor
55
finalclass PreviewVPN:Coder_Desktop.VPNService{
6-
@Publishedvarstate:Coder_Desktop.VPNServiceState=.disabled
7-
@Publishedvaragents:[UUID:Coder_Desktop.Agent]=[
8-
UUID():Agent(id:UUID(), name:"dev", status:.error,copyableDNS:"asdf.coder", wsName:"dogfood2",
6+
@Publishedvarstate:Coder_Desktop.VPNServiceState=.connected
7+
@PublishedvarmenuState:VPNMenuState=.init(agents:[
8+
UUID():Agent(id:UUID(), name:"dev", status:.error,hosts:["asdf.coder"], wsName:"dogfood2",
99
wsID:UUID()),
10-
UUID():Agent(id:UUID(), name:"dev", status:.okay,copyableDNS:"asdf.coder",
10+
UUID():Agent(id:UUID(), name:"dev", status:.okay,hosts:["asdf.coder"],
1111
wsName:"testing-a-very-long-name", wsID:UUID()),
12-
UUID():Agent(id:UUID(), name:"dev", status:.warn,copyableDNS:"asdf.coder", wsName:"opensrc",
12+
UUID():Agent(id:UUID(), name:"dev", status:.warn,hosts:["asdf.coder"], wsName:"opensrc",
1313
wsID:UUID()),
14-
UUID():Agent(id:UUID(), name:"dev", status:.off,copyableDNS:"asdf.coder", wsName:"gvisor",
14+
UUID():Agent(id:UUID(), name:"dev", status:.off,hosts:["asdf.coder"], wsName:"gvisor",
1515
wsID:UUID()),
16-
UUID():Agent(id:UUID(), name:"dev", status:.off,copyableDNS:"asdf.coder", wsName:"example",
16+
UUID():Agent(id:UUID(), name:"dev", status:.off,hosts:["asdf.coder"], wsName:"example",
1717
wsID:UUID()),
18-
UUID():Agent(id:UUID(), name:"dev", status:.error,copyableDNS:"asdf.coder", wsName:"dogfood2",
18+
UUID():Agent(id:UUID(), name:"dev", status:.error,hosts:["asdf.coder"], wsName:"dogfood2",
1919
wsID:UUID()),
20-
UUID():Agent(id:UUID(), name:"dev", status:.okay,copyableDNS:"asdf.coder",
20+
UUID():Agent(id:UUID(), name:"dev", status:.okay,hosts:["asdf.coder"],
2121
wsName:"testing-a-very-long-name", wsID:UUID()),
22-
UUID():Agent(id:UUID(), name:"dev", status:.warn,copyableDNS:"asdf.coder", wsName:"opensrc",
22+
UUID():Agent(id:UUID(), name:"dev", status:.warn,hosts:["asdf.coder"], wsName:"opensrc",
2323
wsID:UUID()),
24-
UUID():Agent(id:UUID(), name:"dev", status:.off,copyableDNS:"asdf.coder", wsName:"gvisor",
24+
UUID():Agent(id:UUID(), name:"dev", status:.off,hosts:["asdf.coder"], wsName:"gvisor",
2525
wsID:UUID()),
26-
UUID():Agent(id:UUID(), name:"dev", status:.off,copyableDNS:"asdf.coder", wsName:"example",
26+
UUID():Agent(id:UUID(), name:"dev", status:.off,hosts:["asdf.coder"], wsName:"example",
2727
wsID:UUID()),
28-
]
28+
], workspaces:[:])
2929
letshouldFail:Bool
3030
letlongError="This is a long error to test the UI with long error messages"
3131

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import Foundation
2+
import SwiftUI
3+
import VPNLib
4+
5+
structAgent:Identifiable,Equatable,Comparable{
6+
letid:UUID
7+
letname:String
8+
letstatus:AgentStatus
9+
lethosts:[String]
10+
letwsName:String
11+
letwsID:UUID
12+
13+
// Agents are sorted by status, and then by name
14+
staticfunc<(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+
// Hosts arrive sorted by length, the shortest looks best in the UI.
22+
varprimaryHost:String?{ hosts.first}
23+
}
24+
25+
enumAgentStatus:Int,Equatable,Comparable{
26+
case okay=0
27+
case warn=1
28+
case error=2
29+
case off=3
30+
31+
publicvarcolor:Color{
32+
switchself{
33+
case.okay:.green
34+
case.warn:.yellow
35+
case.error:.red
36+
case.off:.gray
37+
}
38+
}
39+
40+
staticfunc<(lhs:AgentStatus, rhs:AgentStatus)->Bool{
41+
lhs.rawValue< rhs.rawValue
42+
}
43+
}
44+
45+
structWorkspace:Identifiable,Equatable,Comparable{
46+
letid:UUID
47+
letname:String
48+
varagents:Set<UUID>
49+
50+
staticfunc<(lhs:Workspace, rhs:Workspace)->Bool{
51+
lhs.name.localizedCompare(rhs.name)==.orderedAscending
52+
}
53+
}
54+
55+
structVPNMenuState{
56+
varagents:[UUID:Agent]=[:]
57+
varworkspaces:[UUID:Workspace]=[:]
58+
// Upserted agents that don't belong to any known workspace, have no FQDNs,
59+
// or have any invalid UUIDs.
60+
varinvalidAgents:[Vpn_Agent]=[]
61+
62+
mutatingfunc upsertAgent(_ agent:Vpn_Agent){
63+
guard
64+
let id=UUID(uuidData: agent.id),
65+
let wsID=UUID(uuidData: agent.workspaceID),
66+
var workspace=workspaces[wsID],
67+
!agent.fqdn.isEmpty
68+
else{
69+
invalidAgents.append(agent)
70+
return
71+
}
72+
// An existing agent with the same name, belonging to the same workspace
73+
// is from a previous workspace build, and should be removed.
74+
agents.filter{ $0.value.name== agent.name && $0.value.wsID== wsID}
75+
.forEach{agents[$0.key]=nil}
76+
workspace.agents.insert(id)
77+
workspaces[wsID]= workspace
78+
79+
agents[id]=Agent(
80+
id: id,
81+
name: agent.name,
82+
// If last handshake was not within last five minutes, the agent is unhealthy
83+
status: agent.lastHandshake.date>Date.now.addingTimeInterval(-300)?.okay:.warn,
84+
// Remove trailing dot if present
85+
hosts: agent.fqdn.map{ $0.hasSuffix(".")?String($0.dropLast()): $0},
86+
wsName: workspace.name,
87+
wsID: wsID
88+
)
89+
}
90+
91+
mutatingfunc deleteAgent(withId id:Data){
92+
guardlet agentUUID=UUID(uuidData: id)else{return}
93+
// Update Workspaces
94+
iflet agent=agents[agentUUID],var ws=workspaces[agent.wsID]{
95+
ws.agents.remove(agentUUID)
96+
workspaces[agent.wsID]= ws
97+
}
98+
agents[agentUUID]=nil
99+
// Remove from invalid agents if present
100+
invalidAgents.removeAll{ invalidAgentin
101+
invalidAgent.id== id
102+
}
103+
}
104+
105+
mutatingfunc upsertWorkspace(_ workspace:Vpn_Workspace){
106+
guardlet wsID=UUID(uuidData: workspace.id)else{return}
107+
workspaces[wsID]=Workspace(id: wsID, name: workspace.name, agents:[])
108+
// Check if we can associate any invalid agents with this workspace
109+
invalidAgents.filter{ agentin
110+
agent.workspaceID== workspace.id
111+
}.forEach{ agentin
112+
invalidAgents.removeAll{ $0== agent}
113+
upsertAgent(agent)
114+
}
115+
}
116+
117+
mutatingfunc deleteWorkspace(withId id:Data){
118+
guardlet wsID=UUID(uuidData: id)else{return}
119+
agents.filter{ _, valuein
120+
value.wsID== wsID
121+
}.forEach{ key, _in
122+
agents[key]=nil
123+
}
124+
workspaces[wsID]=nil
125+
}
126+
127+
varsorted:[VPNMenuItem]{
128+
varitems= agents.values.map{VPNMenuItem.agent($0)}
129+
// Workspaces with no agents are shown as offline
130+
items+= workspaces.filter{ _, valuein
131+
value.agents.isEmpty
132+
}.map{VPNMenuItem.offlineWorkspace(Workspace(id: $0.key, name: $0.value.name, agents: $0.value.agents))}
133+
return items.sorted()
134+
}
135+
136+
mutatingfunc clear(){
137+
agents.removeAll()
138+
workspaces.removeAll()
139+
}
140+
}

‎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
protocolVPNService:ObservableObject{
88
varstate:VPNServiceState{get}
9-
varagents:[UUID:Agent]{get}
9+
varmenuState:VPNMenuState{get}
1010
func start()async
1111
func stop()async
1212
func configureTunnelProviderProtocol(proto:NETunnelProviderProtocol?)
@@ -41,7 +41,6 @@ enum VPNServiceError: Error, Equatable {
4141
finalclassCoderVPNService:NSObject,VPNService{
4242
varlogger=Logger(subsystem:Bundle.main.bundleIdentifier!, category:"vpn")
4343
lazyvarxpc:VPNXPCInterface=.init(vpn:self)
44-
varworkspaces:[UUID:String]=[:]
4544

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

59-
@Publishedvaragents:[UUID:Agent]=[:]
58+
@PublishedvarmenuState: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
letmsg=tryVpn_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{ agentIDin
165-
agents[agentID]=nil
166-
}
167-
update.deletedWorkspaces
168-
.compactMap{UUID(uuidData: $0.id)}
169-
.forEach{ workspaceIDin
170-
workspaces[workspaceID]=nil
171-
for(id, agent)in agentswhere agent.wsID== workspaceID{
172-
agents[id]=nil
173-
}
174-
}
175-
176-
// Update workspaces
177-
forworkspaceProtoin update.upsertedWorkspaces{
178-
iflet workspaceID=UUID(uuidData: workspaceProto.id){
179-
workspaces[workspaceID]= workspaceProto.name
180-
}
181-
}
182-
183-
foragentProtoin update.upsertedAgents{
184-
guardlet agentID=UUID(uuidData: agentProto.id)else{
185-
continue
186-
}
187-
guardlet workspaceID=UUID(uuidData: agentProto.workspaceID)else{
188-
continue
189-
}
190-
letworkspaceName=workspaces[workspaceID]??"Unknown Workspace"
191-
letnewAgent=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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp