From 3d98896b1a3c83ee963c9f5e7e7783defb3c797b Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Tue, 13 May 2025 15:31:21 +0400 Subject: [PATCH] chore: make hostname suffix mutable in views --- App/ViewModels/AgentViewModel.cs | 98 ++++++++++++++++++---- App/ViewModels/TrayWindowViewModel.cs | 24 ++---- App/Views/Pages/TrayWindowMainPage.xaml | 4 +- App/Views/Pages/TrayWindowMainPage.xaml.cs | 14 ++-- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/App/ViewModels/AgentViewModel.cs b/App/ViewModels/AgentViewModel.cs index c44db3e..34b01d7 100644 --- a/App/ViewModels/AgentViewModel.cs +++ b/App/ViewModels/AgentViewModel.cs @@ -23,8 +23,13 @@ namespace Coder.Desktop.App.ViewModels; public interface IAgentViewModelFactory { - public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix, + public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, + string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName); + + public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, + string hostnameSuffix, + AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName); } public class AgentViewModelFactory( @@ -33,14 +38,32 @@ public class AgentViewModelFactory( ICredentialManager credentialManager, IAgentAppViewModelFactory agentAppViewModelFactory) : IAgentViewModelFactory { - public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string hostname, string hostnameSuffix, + public AgentViewModel Create(IAgentExpanderHost expanderHost, Uuid id, string fullyQualifiedDomainName, + string hostnameSuffix, AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string? workspaceName) { return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, expanderHost, id) { - Hostname = hostname, - HostnameSuffix = hostnameSuffix, + ConfiguredFqdn = fullyQualifiedDomainName, + ConfiguredHostname = string.Empty, + ConfiguredHostnameSuffix = hostnameSuffix, + ConnectionStatus = connectionStatus, + DashboardBaseUrl = dashboardBaseUrl, + WorkspaceName = workspaceName, + }; + } + + public AgentViewModel CreateDummy(IAgentExpanderHost expanderHost, Uuid id, + string hostnameSuffix, + AgentConnectionStatus connectionStatus, Uri dashboardBaseUrl, string workspaceName) + { + return new AgentViewModel(childLogger, coderApiClientFactory, credentialManager, agentAppViewModelFactory, + expanderHost, id) + { + ConfiguredFqdn = string.Empty, + ConfiguredHostname = workspaceName, + ConfiguredHostnameSuffix = hostnameSuffix, ConnectionStatus = connectionStatus, DashboardBaseUrl = dashboardBaseUrl, WorkspaceName = workspaceName, @@ -84,15 +107,55 @@ public partial class AgentViewModel : ObservableObject, IModelUpdateable Hostname + HostnameSuffix; + /// + /// ViewableHostname is the hostname portion of the fully qualified domain name (FQDN) specifically for + /// views that render it differently than the suffix. If the ConfiguredHostnameSuffix doesn't actually + /// match the FQDN, then this will be the entire FQDN, and ViewableHostnameSuffix will be empty. + /// + public string ViewableHostname => !FullyQualifiedDomainName.EndsWith(ConfiguredHostnameSuffix) + ? FullyQualifiedDomainName + : FullyQualifiedDomainName[0..^ConfiguredHostnameSuffix.Length]; + + /// + /// ViewableHostnameSuffix is the domain suffix portion (including leading dot) of the fully qualified + /// domain name (FQDN) specifically for views that render it differently from the rest of the FQDN. If + /// the ConfiguredHostnameSuffix doesn't actually match the FQDN, then this will be empty and the + /// ViewableHostname will contain the entire FQDN. + /// + public string ViewableHostnameSuffix => FullyQualifiedDomainName.EndsWith(ConfiguredHostnameSuffix) + ? ConfiguredHostnameSuffix + : string.Empty; [ObservableProperty] [NotifyPropertyChangedFor(nameof(ShowExpandAppsMessage))] @@ -202,10 +265,12 @@ public bool TryApplyChanges(AgentViewModel model) // To avoid spurious UI updates which cause flashing, don't actually // write to values unless they've changed. - if (Hostname != model.Hostname) - Hostname = model.Hostname; - if (HostnameSuffix != model.HostnameSuffix) - HostnameSuffix = model.HostnameSuffix; + if (ConfiguredFqdn != model.ConfiguredFqdn) + ConfiguredFqdn = model.ConfiguredFqdn; + if (ConfiguredHostname != model.ConfiguredHostname) + ConfiguredHostname = model.ConfiguredHostname; + if (ConfiguredHostnameSuffix != model.ConfiguredHostnameSuffix) + ConfiguredHostnameSuffix = model.ConfiguredHostnameSuffix; if (ConnectionStatus != model.ConnectionStatus) ConnectionStatus = model.ConnectionStatus; if (DashboardBaseUrl != model.DashboardBaseUrl) @@ -337,12 +402,13 @@ private void ContinueFetchApps(Task task) { Scheme = scheme, Host = "vscode-remote", - Path = $"/ssh-remote+{FullHostname}/{workspaceAgent.ExpandedDirectory}", + Path = $"/ssh-remote+{FullyQualifiedDomainName}/{workspaceAgent.ExpandedDirectory}", }.Uri; } catch (Exception e) { - _logger.LogWarning(e, "Could not craft app URI for display app {displayApp}, app will not appear in list", + _logger.LogWarning(e, + "Could not craft app URI for display app {displayApp}, app will not appear in list", displayApp); continue; } @@ -365,7 +431,7 @@ private void CopyHostname(object parameter) { RequestedOperation = DataPackageOperation.Copy, }; - dataPackage.SetText(FullHostname); + dataPackage.SetText(FullyQualifiedDomainName); Clipboard.SetContent(dataPackage); if (parameter is not FrameworkElement frameworkElement) return; diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index b0c9a8b..1dccab0 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -29,6 +29,7 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost { private const int MaxAgents = 5; private const string DefaultDashboardUrl = "https://coder.com"; + private const string DefaultHostnameSuffix = ".coder"; private readonly IServiceProvider _services; private readonly IRpcController _rpcController; @@ -90,6 +91,8 @@ public partial class TrayWindowViewModel : ObservableObject, IAgentExpanderHost [ObservableProperty] public partial string DashboardUrl { get; set; } = DefaultDashboardUrl; + private string _hostnameSuffix = DefaultHostnameSuffix; + public TrayWindowViewModel(IServiceProvider services, IRpcController rpcController, ICredentialManager credentialManager, IAgentViewModelFactory agentViewModelFactory) { @@ -181,14 +184,6 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (string.IsNullOrWhiteSpace(fqdn)) continue; - var fqdnPrefix = fqdn; - var fqdnSuffix = ""; - if (fqdn.Contains('.')) - { - fqdnPrefix = fqdn[..fqdn.LastIndexOf('.')]; - fqdnSuffix = fqdn[fqdn.LastIndexOf('.')..]; - } - var lastHandshakeAgo = DateTime.UtcNow.Subtract(agent.LastHandshake.ToDateTime()); var connectionStatus = lastHandshakeAgo < TimeSpan.FromMinutes(5) ? AgentConnectionStatus.Green @@ -199,8 +194,8 @@ private void UpdateFromRpcModel(RpcModel rpcModel) agents.Add(_agentViewModelFactory.Create( this, uuid, - fqdnPrefix, - fqdnSuffix, + fqdn, + _hostnameSuffix, connectionStatus, credentialModel.CoderUrl, workspace?.Name)); @@ -214,15 +209,12 @@ private void UpdateFromRpcModel(RpcModel rpcModel) if (!Uuid.TryFrom(workspace.Id.Span, out var uuid)) continue; - agents.Add(_agentViewModelFactory.Create( + agents.Add(_agentViewModelFactory.CreateDummy( this, // Workspace ID is fine as a stand-in here, it shouldn't // conflict with any agent IDs. uuid, - // We assume that it's a single-agent workspace. - workspace.Name, - // TODO: this needs to get the suffix from the server - ".coder", + _hostnameSuffix, AgentConnectionStatus.Gray, credentialModel.CoderUrl, workspace.Name)); @@ -233,7 +225,7 @@ private void UpdateFromRpcModel(RpcModel rpcModel) { if (a.ConnectionStatus != b.ConnectionStatus) return a.ConnectionStatus.CompareTo(b.ConnectionStatus); - return string.Compare(a.FullHostname, b.FullHostname, StringComparison.Ordinal); + return string.Compare(a.FullyQualifiedDomainName, b.FullyQualifiedDomainName, StringComparison.Ordinal); }); if (Agents.Count < MaxAgents) ShowAllAgents = false; diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index b66aa6e..f3549c2 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -169,9 +169,9 @@ TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"> - - diff --git a/App/Views/Pages/TrayWindowMainPage.xaml.cs b/App/Views/Pages/TrayWindowMainPage.xaml.cs index 5911092..e1cbab3 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml.cs +++ b/App/Views/Pages/TrayWindowMainPage.xaml.cs @@ -18,9 +18,9 @@ public TrayWindowMainPage(TrayWindowViewModel viewModel) } // HACK: using XAML to populate the text Runs results in an additional - // whitespace Run being inserted between the Hostname and the - // HostnameSuffix. You might think, "OK let's populate the entire TextBlock - // content from code then!", but this results in the ItemsRepeater + // whitespace Run being inserted between the ViewableHostname and the + // ViewableHostnameSuffix. You might think, "OK let's populate the entire + // TextBlock content from code then!", but this results in the ItemsRepeater // corrupting it and firing events off to the wrong AgentModel. // // This is the best solution I came up with that worked. @@ -28,12 +28,12 @@ public void AgentHostnameText_OnLoaded(object sender, RoutedEventArgs e) { if (sender is not TextBlock textBlock) return; - var nonEmptyRuns = new List(); + var nonWhitespaceRuns = new List(); foreach (var inline in textBlock.Inlines) - if (inline is Run run && !string.IsNullOrWhiteSpace(run.Text)) - nonEmptyRuns.Add(run); + if (inline is Run run && run.Text != " ") + nonWhitespaceRuns.Add(run); textBlock.Inlines.Clear(); - foreach (var run in nonEmptyRuns) textBlock.Inlines.Add(run); + foreach (var run in nonWhitespaceRuns) textBlock.Inlines.Add(run); } }