1
- using System ;
2
- using System . Collections . Generic ;
3
- using System . Collections . ObjectModel ;
4
- using System . ComponentModel ;
5
- using System . Linq ;
6
- using System . Threading ;
7
- using System . Threading . Tasks ;
8
- using Windows . ApplicationModel . DataTransfer ;
9
1
using Coder . Desktop . App . Services ;
10
2
using Coder . Desktop . App . Utils ;
11
3
using Coder . Desktop . CoderSdk ;
18
10
using Microsoft . UI . Xaml ;
19
11
using Microsoft . UI . Xaml . Controls ;
20
12
using Microsoft . UI . Xaml . Controls . Primitives ;
13
+ using System ;
14
+ using System . Collections . Generic ;
15
+ using System . Collections . ObjectModel ;
16
+ using System . ComponentModel ;
17
+ using System . Linq ;
18
+ using System . Text ;
19
+ using System . Threading ;
20
+ using System . Threading . Tasks ;
21
+ using System . Xml . Linq ;
22
+ using Windows . ApplicationModel . DataTransfer ;
21
23
22
24
namespace Coder . Desktop . App . ViewModels ;
23
25
24
26
public interface IAgentViewModelFactory
25
27
{
26
28
public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string fullyQualifiedDomainName ,
27
- string hostnameSuffix ,
28
- AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName ) ;
29
-
29
+ string hostnameSuffix , AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl ,
30
+ string ? workspaceName , bool ? didP2p , string ? preferredDerp , TimeSpan ? latency , TimeSpan ? preferredDerpLatency , DateTime ? lastHandshake ) ;
30
31
public AgentViewModel CreateDummy ( IAgentExpanderHost expanderHost , Uuid id ,
31
32
string hostnameSuffix ,
32
33
AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string workspaceName ) ;
@@ -40,7 +41,9 @@ public class AgentViewModelFactory(
40
41
{
41
42
public AgentViewModel Create ( IAgentExpanderHost expanderHost , Uuid id , string fullyQualifiedDomainName ,
42
43
string hostnameSuffix ,
43
- AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl , string ? workspaceName )
44
+ AgentConnectionStatus connectionStatus , Uri dashboardBaseUrl ,
45
+ string ? workspaceName , bool ? didP2p , string ? preferredDerp , TimeSpan ? latency , TimeSpan ? preferredDerpLatency ,
46
+ DateTime ? lastHandshake )
44
47
{
45
48
return new AgentViewModel ( childLogger , coderApiClientFactory , credentialManager , agentAppViewModelFactory ,
46
49
expanderHost , id )
@@ -51,6 +54,11 @@ public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fu
51
54
ConnectionStatus = connectionStatus ,
52
55
DashboardBaseUrl = dashboardBaseUrl ,
53
56
WorkspaceName = workspaceName ,
57
+ DidP2p = didP2p ,
58
+ PreferredDerp = preferredDerp ,
59
+ Latency = latency ,
60
+ PreferredDerpLatency = preferredDerpLatency ,
61
+ LastHandshake = lastHandshake ,
54
62
} ;
55
63
}
56
64
@@ -73,10 +81,25 @@ public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
73
81
74
82
public enum AgentConnectionStatus
75
83
{
76
- Green ,
77
- Yellow ,
78
- Red ,
79
- Gray ,
84
+ Healthy ,
85
+ Connecting ,
86
+ Unhealthy ,
87
+ NoRecentHandshake ,
88
+ Offline
89
+ }
90
+
91
+ public static class AgentConnectionStatusExtensions
92
+ {
93
+ public static string ToDisplayString ( this AgentConnectionStatus status ) =>
94
+ status switch
95
+ {
96
+ AgentConnectionStatus . Healthy => "Healthy" ,
97
+ AgentConnectionStatus . Connecting => "Connecting" ,
98
+ AgentConnectionStatus . Unhealthy => "High latency" ,
99
+ AgentConnectionStatus . NoRecentHandshake => "No recent handshake" ,
100
+ AgentConnectionStatus . Offline => "Offline" ,
101
+ _ => status . ToString ( )
102
+ } ;
80
103
}
81
104
82
105
public partial class AgentViewModel : ObservableObject , IModelUpdateable < AgentViewModel>
@@ -160,6 +183,7 @@ public string FullyQualifiedDomainName
160
183
[ ObservableProperty ]
161
184
[ NotifyPropertyChangedFor ( nameof ( ShowExpandAppsMessage ) ) ]
162
185
[ NotifyPropertyChangedFor ( nameof ( ExpandAppsMessage ) ) ]
186
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
163
187
public required partial AgentConnectionStatus ConnectionStatus { get ; set ; }
164
188
165
189
[ ObservableProperty ]
@@ -182,6 +206,77 @@ public string FullyQualifiedDomainName
182
206
[ NotifyPropertyChangedFor ( nameof ( ExpandAppsMessage ) ) ]
183
207
public partial bool AppFetchErrored { get ; set ; } = false ;
184
208
209
+ [ ObservableProperty ]
210
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
211
+ public partial bool ? DidP2p { get ; set ; } = false ;
212
+
213
+ [ ObservableProperty ]
214
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
215
+ public partial string ? PreferredDerp { get ; set ; } = null ;
216
+
217
+ [ ObservableProperty ]
218
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
219
+ public partial TimeSpan ? Latency { get ; set ; } = null ;
220
+
221
+ [ ObservableProperty ]
222
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
223
+ public partial TimeSpan ? PreferredDerpLatency { get ; set ; } = null ;
224
+
225
+ [ ObservableProperty ]
226
+ [ NotifyPropertyChangedFor ( nameof ( ConnectionTooltip ) ) ]
227
+ public partial DateTime ? LastHandshake { get ; set ; } = null ;
228
+
229
+ public string ConnectionTooltip
230
+ {
231
+ get
232
+ {
233
+ var description = new StringBuilder ( ) ;
234
+ var highLatencyWarning = ConnectionStatus == AgentConnectionStatus . Unhealthy ? $ "({ AgentConnectionStatus . Unhealthy . ToDisplayString ( ) } )" : "" ;
235
+
236
+ if ( DidP2p != null && DidP2p . Value && Latency != null )
237
+ {
238
+ description . Append ( $ """
239
+ You're connected peer-to-peer. { highLatencyWarning }
240
+
241
+ You ↔ { Latency . Value . Milliseconds } ms ↔ { WorkspaceName }
242
+ """
243
+ ) ;
244
+ }
245
+ else if ( Latency != null )
246
+ {
247
+ description . Append ( $ """
248
+ You're connected through a DERP relay. { highLatencyWarning }
249
+ We'll switch over to peer-to-peer when available.
250
+
251
+ Total latency: { Latency . Value . Milliseconds } ms
252
+ """
253
+ ) ;
254
+
255
+ if ( PreferredDerpLatency != null )
256
+ {
257
+ description . Append ( $ "\n You ↔ { PreferredDerp } : { PreferredDerpLatency . Value . Milliseconds } ms") ;
258
+
259
+ var derpToWorkspaceEstimatedLatency = Latency - PreferredDerpLatency ;
260
+
261
+ // Guard against negative values if the two readings were taken at different times
262
+ if ( derpToWorkspaceEstimatedLatency > TimeSpan . Zero )
263
+ {
264
+ description . Append ( $ "\n { PreferredDerp } ms ↔ { WorkspaceName } : { derpToWorkspaceEstimatedLatency . Value . Milliseconds } ms") ;
265
+ }
266
+ }
267
+ }
268
+ else
269
+ {
270
+ description . Append ( ConnectionStatus . ToDisplayString ( ) ) ;
271
+ }
272
+ if ( LastHandshake != null )
273
+ description . Append ( $ "\n \n Last handshake: { LastHandshake ? . ToString ( ) } ") ;
274
+
275
+ return description . ToString (
2851
) . TrimEnd ( '\n ' , ' ' ) ; ;
276
+ }
277
+ }
278
+
279
+
185
280
// We only show 6 apps max, which fills the entire width of the tray
186
281
// window.
187
282
public IEnumerable < AgentAppViewModel > VisibleApps => Apps . Count > MaxAppsPerRow ? Apps . Take ( MaxAppsPerRow ) : Apps ;
@@ -192,7 +287,7 @@ public string? ExpandAppsMessage
192
287
{
193
288
get
194
289
{
195
- if ( ConnectionStatus == AgentConnectionStatus . Gray )
290
+ if ( ConnectionStatus == AgentConnectionStatus . Offline )
196
291
return "Your workspace is offline." ;
197
292
if ( FetchingApps && Apps . Count == 0 )
198
293
// Don't show this message if we have any apps already. When
@@ -285,6 +380,16 @@ public bool TryApplyChanges(AgentViewModel model)
285
380
DashboardBaseUrl = model . DashboardBaseUrl ;
286
381
if ( WorkspaceName != model . WorkspaceName )
287
382
WorkspaceName = model . WorkspaceName ;
383
+ if ( DidP2p != model . DidP2p )
384
+ DidP2p = model . DidP2p ;
385
+ if ( PreferredDerp != model . PreferredDerp )
386
+ PreferredDerp = model . PreferredDerp ;
387
+ if ( Latency != model . Latency )
388
+ Latency = model . Latency ;
389
+ if ( PreferredDerpLatency != model . PreferredDerpLatency )
390
+ PreferredDerpLatency = model . PreferredDerpLatency ;
391
+ if ( LastHandshake != model . LastHandshake )
392
+ LastHandshake = model . LastHandshake ;
288
393
289
394
// Apps are not set externally.
290
395
@@ -307,7 +412,7 @@ public void SetExpanded(bool expanded)
307
412
308
413
partial void OnConnectionStatusChanged ( AgentConnectionStatus oldValue , AgentConnectionStatus newValue )
309
414
{
310
- if ( IsExpanded && newValue is not AgentConnectionStatus . Gray ) FetchApps ( ) ;
415
+ if ( IsExpanded && newValue is not AgentConnectionStatus . Offline ) FetchApps ( ) ;
311
416
}
312
417
313
418
private void FetchApps ( )
@@ -316,7 +421,7 @@ private void FetchApps()
316
421
FetchingApps = true ;
317
422
318
423
// If the workspace is off, then there's no agent and there's no apps.
319
- if ( ConnectionStatus == AgentConnectionStatus . Gray )
424
+ if ( ConnectionStatus == AgentConnectionSta
10000
tus . Offline )
320
425
{
321
426
FetchingApps = false ;
322
427
Apps . Clear ( ) ;
0 commit comments