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

Commitf8a5ca5

Browse files
feat: include ping and network stats on status tooltip (#181)
Closes#64.![Screenshot 2025-06-06 at 4 03 59 pm](https://github.com/user-attachments/assets/0b844e2f-4f09-4137-b937-a16a5db3b6ac)![Screenshot 2025-06-06 at 4 03 51 pm](https://github.com/user-attachments/assets/1ac021aa-7761-49a3-abad-a286271a794a)
1 parent170b399 commitf8a5ca5

File tree

11 files changed

+371
-17
lines changed

11 files changed

+371
-17
lines changed

‎Coder-Desktop/Coder-Desktop/Coder_DesktopApp.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,9 @@ class AppDelegate: NSObject, NSApplicationDelegate {
8484
}
8585

8686
func applicationDidFinishLaunching(_:Notification){
87+
// We have important file sync and network info behind tooltips,
88+
// so the default delay is too long.
89+
UserDefaults.standard.setValue(Theme.Animation.tooltipDelay, forKey:"NSInitialToolTipDelay")
8790
// Init SVG loader
8891
SDImageCodersManager.shared.addCoder(SDImageSVGCoder.shared)
8992

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@ import SwiftUI
55
finalclass PreviewVPN:Coder_Desktop.VPNService{
66
@Publishedvarstate:Coder_Desktop.VPNServiceState=.connected
77
@PublishedvarmenuState:VPNMenuState=.init(agents:[
8-
UUID():Agent(id:UUID(), name:"dev", status:.error, hosts:["asdf.coder"], wsName:"dogfood2",
8+
UUID():Agent(id:UUID(), name:"dev", status:.no_recent_handshake, hosts:["asdf.coder"], wsName:"dogfood2",
99
wsID:UUID(), primaryHost:"asdf.coder"),
1010
UUID():Agent(id:UUID(), name:"dev", status:.okay, hosts:["asdf.coder"],
1111
wsName:"testing-a-very-long-name", wsID:UUID(), primaryHost:"asdf.coder"),
12-
UUID():Agent(id:UUID(), name:"dev", status:.warn, hosts:["asdf.coder"], wsName:"opensrc",
12+
UUID():Agent(id:UUID(), name:"dev", status:.high_latency, hosts:["asdf.coder"], wsName:"opensrc",
1313
wsID:UUID(), primaryHost:"asdf.coder"),
1414
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"gvisor",
1515
wsID:UUID(), primaryHost:"asdf.coder"),
1616
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"example",
1717
wsID:UUID(), primaryHost:"asdf.coder"),
18-
UUID():Agent(id:UUID(), name:"dev", status:.error, hosts:["asdf.coder"], wsName:"dogfood2",
18+
UUID():Agent(id:UUID(), name:"dev", status:.no_recent_handshake, hosts:["asdf.coder"], wsName:"dogfood2",
1919
wsID:UUID(), primaryHost:"asdf.coder"),
2020
UUID():Agent(id:UUID(), name:"dev", status:.okay, hosts:["asdf.coder"],
2121
wsName:"testing-a-very-long-name", wsID:UUID(), primaryHost:"asdf.coder"),
22-
UUID():Agent(id:UUID(), name:"dev", status:.warn, hosts:["asdf.coder"], wsName:"opensrc",
22+
UUID():Agent(id:UUID(), name:"dev", status:.high_latency, hosts:["asdf.coder"], wsName:"opensrc",
2323
wsID:UUID(), primaryHost:"asdf.coder"),
2424
UUID():Agent(id:UUID(), name:"dev", status:.off, hosts:["asdf.coder"], wsName:"gvisor",
2525
wsID:UUID(), primaryHost:"asdf.coder"),

‎Coder-Desktop/Coder-Desktop/Theme.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ enum Theme {
1515

1616
enumAnimation{
1717
staticletcollapsibleDuration=0.2
18+
staticlettooltipDelay:Int=250 // milliseconds
1819
}
1920

2021
staticletdefaultVisibleAgents=5

‎Coder-Desktop/Coder-Desktop/VPN/MenuState.swift

Lines changed: 163 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Foundation
2+
import SwiftProtobuf
23
import SwiftUI
34
import VPNLib
45

@@ -9,6 +10,29 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
910
lethosts:[String]
1011
letwsName:String
1112
letwsID:UUID
13+
letlastPing:LastPing?
14+
letlastHandshake:Date?
15+
16+
init(id:UUID,
17+
name:String,
18+
status:AgentStatus,
19+
hosts:[String],
20+
wsName:String,
21+
wsID:UUID,
22+
lastPing:LastPing?=nil,
23+
lastHandshake:Date?=nil,
24+
primaryHost:String)
25+
{
26+
self.id= id
27+
self.name= name
28+
self.status= status
29+
self.hosts= hosts
30+
self.wsName= wsName
31+
self.wsID= wsID
32+
self.lastPing= lastPing
33+
self.lastHandshake= lastHandshake
34+
self.primaryHost= primaryHost
35+
}
1236

1337
// Agents are sorted by status, and then by name
1438
staticfunc<(lhs:Agent, rhs:Agent)->Bool{
@@ -18,21 +42,94 @@ struct Agent: Identifiable, Equatable, Comparable, Hashable {
1842
return lhs.wsName.localizedCompare(rhs.wsName)==.orderedAscending
1943
}
2044

45+
varstatusString:String{
46+
switch status{
47+
case.okay,.high_latency:
48+
break
49+
default:
50+
return status.description
51+
}
52+
53+
guardlet lastPingelse{
54+
// Either:
55+
// - Old coder deployment
56+
// - We haven't received any pings yet
57+
return status.description
58+
}
59+
60+
lethighLatencyWarning= status==.high_latency?"(High latency)":""
61+
62+
varstr:String
63+
if lastPing.didP2p{
64+
str="""
65+
You're connected peer-to-peer.\(highLatencyWarning)
66+
67+
You ↔\(lastPing.latency.prettyPrintMs)\(wsName)
68+
"""
69+
}else{
70+
str="""
71+
You're connected through a DERP relay.\(highLatencyWarning)
72+
We'll switch over to peer-to-peer when available.
73+
74+
Total latency:\(lastPing.latency.prettyPrintMs)
75+
"""
76+
// We're not guranteed to have the preferred DERP latency
77+
iflet preferredDerpLatency= lastPing.preferredDerpLatency{
78+
str+="\nYou ↔\(lastPing.preferredDerp):\(preferredDerpLatency.prettyPrintMs)"
79+
letderpToWorkspaceEstLatency= lastPing.latency- preferredDerpLatency
80+
// We're not guaranteed the preferred derp latency is less than
81+
// the total, as they might have been recorded at slightly
82+
// different times, and we don't want to show a negative value.
83+
if derpToWorkspaceEstLatency>0{
84+
str+="\n\(lastPing.preferredDerp)\(wsName):\(derpToWorkspaceEstLatency.prettyPrintMs)"
85+
}
86+
}
87+
}
88+
str+="\n\nLast handshake:\(lastHandshake?.relativeTimeString??"Unknown")"
89+
return str
90+
}
91+
2192
letprimaryHost:String
2293
}
2394

95+
extensionTimeInterval{
96+
varprettyPrintMs:String{
97+
letmilliseconds=self*1000
98+
return"\(milliseconds.formatted(.number.precision(.fractionLength(2)))) ms"
99+
}
100+
}
101+
102+
structLastPing:Equatable,Hashable{
103+
letlatency:TimeInterval
104+
letdidP2p:Bool
105+
letpreferredDerp:String
106+
letpreferredDerpLatency:TimeInterval?
107+
}
108+
24109
enumAgentStatus:Int,Equatable,Comparable{
25110
case okay=0
26-
case warn=1
27-
case error=2
28-
case off=3
111+
case connecting=1
112+
case high_latency=2
113+
case no_recent_handshake=3
114+
case off=4
115+
116+
publicvardescription:String{
117+
switchself{
118+
case.okay:"Connected"
119+
case.connecting:"Connecting..."
120+
case.high_latency:"Connected, but with high latency" // Message currently unused
121+
case.no_recent_handshake:"Could not establish a connection to the agent. Retrying..."
122+
case.off:"Offline"
123+
}
124+
}
29125

30126
publicvarcolor:Color{
31127
switchself{
32128
case.okay:.green
33-
case.warn:.yellow
34-
case.error:.red
129+
case.high_latency:.yellow
130+
case.no_recent_handshake:.red
35131
case.off:.secondary
132+
case.connecting:.yellow
36133
}
37134
}
38135

@@ -87,14 +184,27 @@ struct VPNMenuState {
87184
workspace.agents.insert(id)
88185
workspaces[wsID]= workspace
89186

187+
varlastPing:LastPing?
188+
if agent.hasLastPing{
189+
lastPing=LastPing(
190+
latency: agent.lastPing.latency.timeInterval,
191+
didP2p: agent.lastPing.didP2P,
192+
preferredDerp: agent.lastPing.preferredDerp,
193+
preferredDerpLatency:
194+
agent.lastPing.hasPreferredDerpLatency
195+
? agent.lastPing.preferredDerpLatency.timeInterval
196+
:nil
197+
)
198+
}
90199
agents[id]=Agent(
91200
id: id,
92201
name: agent.name,
93-
// If last handshake was not within last five minutes, the agent is unhealthy
94-
status: agent.lastHandshake.date>Date.now.addingTimeInterval(-300)?.okay:.warn,
202+
status: agent.status,
95203
hosts: nonEmptyHosts,
96204
wsName: workspace.name,
97205
wsID: wsID,
206+
lastPing: lastPing,
207+
lastHandshake: agent.lastHandshake.maybeDate,
98208
// Hosts arrive sorted by length, the shortest looks best in the UI.
99209
primaryHost: nonEmptyHosts.first!
100210
)
@@ -154,3 +264,49 @@ struct VPNMenuState {
154264
workspaces.removeAll()
155265
}
156266
}
267+
268+
extensionDate{
269+
varrelativeTimeString:String{
270+
letformatter=RelativeDateTimeFormatter()
271+
formatter.unitsStyle=.full
272+
ifDate.now.timeIntervalSince(self)<1.0{
273+
// Instead of showing "in 0 seconds"
274+
return"Just now"
275+
}
276+
return formatter.localizedString(for:self, relativeTo:Date.now)
277+
}
278+
}
279+
280+
extensionSwiftProtobuf.Google_Protobuf_Timestamp{
281+
varmaybeDate:Date?{
282+
guard seconds>0else{returnnil}
283+
return date
284+
}
285+
}
286+
287+
extensionVpn_Agent{
288+
varhealthyLastHandshakeMin:Date{
289+
Date.now.addingTimeInterval(-300) // 5 minutes ago
290+
}
291+
292+
varhealthyPingMax:TimeInterval{0.15} // 150ms
293+
294+
varstatus:AgentStatus{
295+
// Initially the handshake is missing
296+
guardlet lastHandshake= lastHandshake.maybeDateelse{
297+
return.connecting
298+
}
299+
// If last handshake was not within the last five minutes, the agent
300+
// is potentially unhealthy.
301+
guard lastHandshake>= healthyLastHandshakeMinelse{
302+
return.no_recent_handshake
303+
}
304+
// No ping data, but we have a recent handshake.
305+
// We show green for backwards compatibility with old Coder
306+
// deployments.
307+
guard hasLastPingelse{
308+
return.okay
309+
}
310+
return lastPing.latency.timeInterval< healthyPingMax?.okay:.high_latency
311+
}
312+
}

‎Coder-Desktop/Coder-Desktop/Views/VPN/VPNMenuItem.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ enum VPNMenuItem: Equatable, Comparable, Identifiable {
2121
}
2222
}
2323

24+
varstatusString:String{
25+
switchself{
26+
caselet.agent(agent): agent.statusString
27+
case.offlineWorkspace: status.description
28+
}
29+
}
30+
2431
varid:UUID{
2532
switchself{
2633
caselet.agent(agent): agent.id
@@ -224,6 +231,7 @@ struct MenuItemIcons: View {
224231
StatusDot(color: item.status.color)
225232
.padding(.trailing,3)
226233
.padding(.top,1)
234+
.help(item.statusString)
227235
MenuItemIconButton(systemName:"doc.on.doc", action: copyToClipboard)
228236
.font(.system(size:9))
229237
.symbolVariant(.fill)

‎Coder-Desktop/Coder-DesktopTests/AgentsTests.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ struct AgentsTests {
2828
hosts:["a\($0).coder"],
2929
wsName:"ws\($0)",
3030
wsID:UUID(),
31+
lastPing:nil,
3132
primaryHost:"a\($0).coder"
3233
)
3334
return(agent.id, agent)

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp