Skip to content

Commit 9e50acd

Browse files
authored
chore: make hostname suffix mutable in views (#102)
Part 1 of #49 Makes the workspace suffix dynamic in the views. A later PR in this stack will fetch the suffix and apply it to any views if it changes. If the suffix doesn't match the FQDN provided by the VPN service, we won't gray out anything when we show the name, but if we later get a suffix that matches, it will update.
1 parent cd845d4 commit 9e50acd

File tree

4 files changed

+99
-41
lines changed

4 files changed

+99
-41
lines changed

App/ViewModels/AgentViewModel.cs

+82-16
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,13 @@ namespace Coder.Desktop.App.ViewModels;
2323

2424
public interface IAgentViewModelFactory
2525
{
26-
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix,
26+
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName,
27+
string hostnameSuffix,
2728
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName);
29+
30+
public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
31+
string hostnameSuffix,
32+
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName);
2833
}
2934

3035
public class AgentViewModelFactory(
@@ -33,14 +38,32 @@ public class AgentViewModelFactory(
3338
ICredentialManager credentialManager,
3439
IAgentAppViewModelFactory agentAppViewModelFactory) : IAgentViewModelFactory
3540
{
36-
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix,
41+
public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName,
42+
string hostnameSuffix,
3743
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName)
3844
{
3945
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory,
4046
expanderHost, id)
4147
{
42-
Hostname = hostname,
43-
HostnameSuffix = hostnameSuffix,
48+
ConfiguredFqdn = fullyQualifiedDomainName,
49+
ConfiguredHostname = string.Empty,
50+
ConfiguredHostnameSuffix = hostnameSuffix,
51+
ConnectionStatus = connectionStatus,
52+
DashboardBaseUrl = dashboardBaseUrl,
53+
WorkspaceName = workspaceName,
54+
};
55+
}
56+
57+
public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id,
58+
string hostnameSuffix,
59+
AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName)
60+
{
61+
return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory,
62+
expanderHost, id)
63+
{
64+
ConfiguredFqdn = string.Empty,
65+
ConfiguredHostname = workspaceName,
66+
ConfiguredHostnameSuffix = hostnameSuffix,
4467
ConnectionStatus = connectionStatus,
4568
DashboardBaseUrl = dashboardBaseUrl,
4669
WorkspaceName = workspaceName,
@@ -84,15 +107,55 @@ public partial class AgentViewModel : ObservableObject, IModelUpdateable<AgentVi
84107

85108
public readonly Uuid Id;
86109

110+
// This is set only for "dummy" agents that represent unstarted workspaces. If set, then ConfiguredFqdn
111+
// should be empty, otherwise it will override this.
112+
[ObservableProperty]
113+
[NotifyPropertyChangedFor(nameof(ViewableHostname))]
114+
[NotifyPropertyChangedFor(nameof(ViewableHostnameSuffix))]
115+
[NotifyPropertyChangedFor(nameof(FullyQualifiedDomainName))]
116+
public required partial string ConfiguredHostname { get; set; }
117+
118+
// This should be set if we have an FQDN from the VPN service, and overrides ConfiguredHostname if set.
87119
[ObservableProperty]
88-
[NotifyPropertyChangedFor(nameof(FullHostname))]
89-
public required partial string Hostname { get; set; }
120+
[NotifyPropertyChangedFor(nameof(ViewableHostname))]
121+
[NotifyPropertyChangedFor(nameof(ViewableHostnameSuffix))]
122+
[NotifyPropertyChangedFor(nameof(FullyQualifiedDomainName))]
123+
public required partial string ConfiguredFqdn { get; set; }
90124

91125
[ObservableProperty]
92-
[NotifyPropertyChangedFor(nameof(FullHostname))]
93-
public required partial string HostnameSuffix { get; set; } // including leading dot
126+
[NotifyPropertyChangedFor(nameof(ViewableHostname))]
127+
[NotifyPropertyChangedFor(nameof(ViewableHostnameSuffix))]
128+
[NotifyPropertyChangedFor(nameof(FullyQualifiedDomainName))]
129+
public required partial string ConfiguredHostnameSuffix { get; set; } // including leading dot
130+
131+
132+
public string FullyQualifiedDomainName
133+
{
134+
get
135+
{
136+
if (!string.IsNullOrEmpty(ConfiguredFqdn)) return ConfiguredFqdn;
137+
return ConfiguredHostname + ConfiguredHostnameSuffix;
138+
}
139+
}
94140

95-
public string FullHostname => Hostname + HostnameSuffix;
141+
/// <summary>
142+
/// ViewableHostname is the hostname portion of the fully qualified domain name (FQDN) specifically for
143+
/// views that render it differently than the suffix. If the ConfiguredHostnameSuffix doesn't actually
144+
/// match the FQDN, then this will be the entire FQDN, and ViewableHostnameSuffix will be empty.
145+
/// </summary>
146+
public string ViewableHostname => !FullyQualifiedDomainName.EndsWith(ConfiguredHostnameSuffix)
147+
? FullyQualifiedDomainName
148+
: FullyQualifiedDomainName[0..^ConfiguredHostnameSuffix.Length];
149+
150+
/// <summary>
151+
/// ViewableHostnameSuffix is the domain suffix portion (including leading dot) of the fully qualified
152+
/// domain name (FQDN) specifically for views that render it differently from the rest of the FQDN. If
153+
/// the ConfiguredHostnameSuffix doesn't actually match the FQDN, then this will be empty and the
154+
/// ViewableHostname will contain the entire FQDN.
155+
/// </summary>
156+
public string ViewableHostnameSuffix => FullyQualifiedDomainName.EndsWith(ConfiguredHostnameSuffix)
157+
? ConfiguredHostnameSuffix
158+
: string.Empty;
96159

97160
[ObservableProperty]
98161
[NotifyPropertyChangedFor(nameof(ShowExpandAppsMessage))]
@@ -202,10 +265,12 @@ public bool TryApplyChanges(AgentViewModel model)
202265

203266
// To avoid spurious UI updates which cause flashing, don't actually
204267
// write to values unless they've changed.
205-
if (Hostname != model.Hostname)
206-
Hostname = model.Hostname;
207-
if (HostnameSuffix != model.HostnameSuffix)
208-
HostnameSuffix = model.HostnameSuffix;
268+
if (ConfiguredFqdn != model.ConfiguredFqdn)
269+
ConfiguredFqdn = model.ConfiguredFqdn;
270+
if (ConfiguredHostname != model.ConfiguredHostname)
271+
ConfiguredHostname = model.ConfiguredHostname;
272+
if (ConfiguredHostnameSuffix != model.ConfiguredHostnameSuffix)
273+
ConfiguredHostnameSuffix = model.ConfiguredHostnameSuffix;
209274
if (ConnectionStatus != model.ConnectionStatus)
210275
ConnectionStatus = model.ConnectionStatus;
211276
if (DashboardBaseUrl != model.DashboardBaseUrl)
@@ -337,12 +402,13 @@ private void ContinueFetchApps(Task<WorkspaceAgent> task)
337402
{
338403
Scheme = scheme,
339404
Host = "vscode-remote",
340-
Path = $"/ssh-remote+{FullHostname}/{workspaceAgent.ExpandedDirectory}",
405+
Path = $"/ssh-remote+{FullyQualifiedDomainName}/{workspaceAgent.ExpandedDirectory}",
341406
}.Uri;
342407
}
343408
catch (Exception e)
344409
{
345-
_logger.LogWarning(e, "Could not craft app URI for display app {displayApp}, app will not appear in list",
410+
_logger.LogWarning(e,
411+
"Could not craft app URI for display app {displayApp}, app will not appear in list",
346412
displayApp);
347413
continue;
348414
}
@@ -365,7 +431,7 @@ private void CopyHostname(object parameter)
365431
{
366432
RequestedOperation = DataPackageOperation.Copy,
367433
};
368-
dataPackage.SetText(FullHostname);
434+
dataPackage.SetText(FullyQualifiedDomainName);
369435
Clipboard.SetContent(dataPackage);
370436

371437
if (parameter is not FrameworkElement frameworkElement) return;

App/ViewModels/TrayWindowViewModel.cs

+8-16
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
2929
{
3030
private const int MaxAgents = 5;
3131
private const string DefaultDashboardUrl = "https://coder.com";
32+
private const string DefaultHostnameSuffix = ".coder";
3233

3334
private readonly IServiceProvider _services;
3435
private readonly IRpcController _rpcController;
@@ -90,6 +91,8 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost
9091

9192
[ObservableProperty] public partial string DashboardUrl { get; set; } = DefaultDashboardUrl;
9293

94+
private string _hostnameSuffix = DefaultHostnameSuffix;
95+
9396
public TrayWindowViewModel(IServiceProvider services, IRpcController rpcController,
9497
ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory)
9598
{
@@ -181,14 +184,6 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
181184
if (string.IsNullOrWhiteSpace(fqdn))
182185
continue;
183186

184-
var fqdnPrefix = fqdn;
185-
var fqdnSuffix = "";
186-
if (fqdn.Contains('.'))
187-
{
188-
fqdnPrefix = fqdn[..fqdn.LastIndexOf('.')];
189-
fqdnSuffix = fqdn[fqdn.LastIndexOf('.')..];
190-
}
191-
192187
var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime());
193188
var connectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5)
194189
? AgentConnectionStatus.Green
@@ -199,8 +194,8 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
199194
agents.Add(_agentViewModelFactory.Create(
200195
this,
201196
uuid,
202-
fqdnPrefix,
203-
fqdnSuffix,
197+
fqdn,
198+
_hostnameSuffix,
204199
connectionStatus,
205200
credentialModel.CoderUrl,
206201
workspace?.Name));
@@ -214,15 +209,12 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
214209
if (!Uuid.TryFrom(workspace.Id.Span, out var uuid))
215210
continue;
216211

217-
agents.Add(_agentViewModelFactory.Create(
212+
agents.Add(_agentViewModelFactory.CreateDummy(
218213
this,
219214
// Workspace ID is fine as a stand-in here, it shouldn't
220215
// conflict with any agent IDs.
221216
uuid,
222-
// We assume that it's a single-agent workspace.
223-
workspace.Name,
224-
// TODO: this needs to get the suffix from the server
225-
".coder",
217+
_hostnameSuffix,
226218
AgentConnectionStatus.Gray,
227219
credentialModel.CoderUrl,
228220
workspace.Name));
@@ -233,7 +225,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel)
233225
{
234226
if (a.ConnectionStatus != b.ConnectionStatus)
235227
return a.ConnectionStatus.CompareTo(b.ConnectionStatus);
236-
return string.Compare(a.FullHostname, b.FullHostname, StringComparison.Ordinal);
228+
return string.Compare(a.FullyQualifiedDomainName, b.FullyQualifiedDomainName, StringComparison.Ordinal);
237229
});
238230

239231
if (Agents.Count < MaxAgents) ShowAllAgents = false;

App/Views/Pages/TrayWindowMainPage.xaml

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,9 @@
169169
TextTrimming="CharacterEllipsis"
170170
TextWrapping="NoWrap">
171171

172-
<Run Text="{x:Bind Hostname, Mode=OneWay}"
172+
<Run Text="{x:Bind ViewableHostname, Mode=OneWay}"
173173
Foreground="{ThemeResource DefaultTextForegroundThemeBrush}" />
174-
<Run Text="{x:Bind HostnameSuffix, Mode=OneWay}"
174+
<Run Text="{x:Bind ViewableHostnameSuffix, Mode=OneWay}"
175175
Foreground="{ThemeResource SystemControlForegroundBaseMediumBrush}" />
176176
</TextBlock>
177177

App/Views/Pages/TrayWindowMainPage.xaml.cs

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,22 +18,22 @@ public TrayWindowMainPage(TrayWindowViewModel viewModel)
1818
}
1919

2020
// HACK: using XAML to populate the text Runs results in an additional
21-
// whitespace Run being inserted between the Hostname and the
22-
// HostnameSuffix. You might think, "OK let's populate the entire TextBlock
23-
// content from code then!", but this results in the ItemsRepeater
21+
// whitespace Run being inserted between the ViewableHostname and the
22+
// ViewableHostnameSuffix. You might think, "OK let's populate the entire
23+
// TextBlock content from code then!", but this results in the ItemsRepeater
2424
// corrupting it and firing events off to the wrong AgentModel.
2525
//
2626
// This is the best solution I came up with that worked.
2727
public void AgentHostnameText_OnLoaded(object sender, RoutedEventArgs e)
2828
{
2929
if (sender is not TextBlock textBlock) return;
3030

31-
var nonEmptyRuns = new List<Run>();
31+
var nonWhitespaceRuns = new List<Run>();
3232
foreach (var inline in textBlock.Inlines)
33-
if (inline is Run run && !string.IsNullOrWhiteSpace(run.Text))
34-
nonEmptyRuns.Add(run);
33+
if (inline is Run run && run.Text != " ")
34+
nonWhitespaceRuns.Add(run);
3535

3636
textBlock.Inlines.Clear();
37-
foreach (var run in nonEmptyRuns) textBlock.Inlines.Add(run);
37+
foreach (var run in nonWhitespaceRuns) textBlock.Inlines.Add(run);
3838
}
3939
}

0 commit comments

Comments
 (0)