diff --git a/App/Converters/AgentStatusToColorConverter.cs b/App/Converters/AgentStatusToColorConverter.cs index 25f1f66..ebcabdd 100644 --- a/App/Converters/AgentStatusToColorConverter.cs +++ b/App/Converters/AgentStatusToColorConverter.cs @@ -9,7 +9,7 @@ namespace Coder.Desktop.App.Converters; public class AgentStatusToColorConverter : IValueConverter { private static readonly SolidColorBrush Green = new(Color.FromArgb(255, 52, 199, 89)); - private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 204, 1, 0)); + private static readonly SolidColorBrush Yellow = new(Color.FromArgb(255, 255, 204, 1)); private static readonly SolidColorBrush Red = new(Color.FromArgb(255, 255, 59, 48)); private static readonly SolidColorBrush Gray = new(Color.FromArgb(255, 142, 142, 147)); diff --git a/App/Converters/InverseBoolToVisibilityConverter.cs b/App/Converters/InverseBoolToVisibilityConverter.cs deleted file mode 100644 index dd9c864..0000000 --- a/App/Converters/InverseBoolToVisibilityConverter.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Microsoft.UI.Xaml; - -namespace Coder.Desktop.App.Converters; - -public partial class InverseBoolToVisibilityConverter : BoolToObjectConverter -{ - public InverseBoolToVisibilityConverter() - { - TrueValue = Visibility.Collapsed; - FalseValue = Visibility.Visible; - } -} diff --git a/App/Converters/VpnLifecycleToVisibilityConverter.cs b/App/Converters/VpnLifecycleToVisibilityConverter.cs deleted file mode 100644 index bf83bea..0000000 --- a/App/Converters/VpnLifecycleToVisibilityConverter.cs +++ /dev/null @@ -1,14 +0,0 @@ -using System; -using Microsoft.UI.Xaml; -using Microsoft.UI.Xaml.Data; - -namespace Coder.Desktop.App.Converters; - -public partial class VpnLifecycleToVisibilityConverter : VpnLifecycleToBoolConverter, IValueConverter -{ - public new object Convert(object value, Type targetType, object parameter, string language) - { - var boolValue = base.Convert(value, targetType, parameter, language); - return boolValue is true ? Visibility.Visible : Visibility.Collapsed; - } -} diff --git a/App/Services/RpcController.cs b/App/Services/RpcController.cs index a02347f..1b3dac6 100644 --- a/App/Services/RpcController.cs +++ b/App/Services/RpcController.cs @@ -146,7 +146,8 @@ public async Task Reconnect(CancellationToken ct = default) Status = new StatusRequest(), }, ct); if (statusReply.MsgCase != ServiceMessage.MsgOneofCase.Status) - throw new InvalidOperationException($"Unexpected reply message type: {statusReply.MsgCase}"); + throw new VpnLifecycleException( + $"Failed to get VPN status. Unexpected reply message type: {statusReply.MsgCase}"); ApplyStatusUpdate(statusReply.Status); } @@ -172,8 +173,6 @@ public async Task StartVpn(CancellationToken ct = default) ApiToken = credentials.ApiToken, }, }, ct); - if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start) - throw new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}"); } catch (Exception e) { @@ -181,11 +180,19 @@ public async Task StartVpn(CancellationToken ct = default) throw new RpcOperationException("Failed to send start command to service", e); } + if (reply.MsgCase != ServiceMessage.MsgOneofCase.Start) + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to start VPN. Unexpected reply message type: {reply.MsgCase}"); + } + if (!reply.Start.Success) { + // We use Stopped instead of Unknown here as it's usually the case + // that a failed start got cleaned up successfully. MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); - throw new VpnLifecycleException("Failed to start VPN", - new InvalidOperationException($"Service reported failure: {reply.Start.ErrorMessage}")); + throw new VpnLifecycleException( + $"Failed to start VPN. Service reported failure: {reply.Start.ErrorMessage}"); } MutateState(state => { state.VpnLifecycle = VpnLifecycle.Started; }); @@ -212,16 +219,20 @@ public async Task StopVpn(CancellationToken ct = default) } finally { - // Technically the state is unknown now. - MutateState(state => { state.VpnLifecycle = VpnLifecycle.Stopped; }); + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); } if (reply.MsgCase != ServiceMessage.MsgOneofCase.Stop) - throw new VpnLifecycleException("Failed to stop VPN", - new InvalidOperationException($"Unexpected reply message type: {reply.MsgCase}")); + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to stop VPN. Unexpected reply message type: {reply.MsgCase}"); + } + if (!reply.Stop.Success) - throw new VpnLifecycleException("Failed to stop VPN", - new InvalidOperationException($"Service reported failure: {reply.Stop.ErrorMessage}")); + { + MutateState(state => { state.VpnLifecycle = VpnLifecycle.Unknown; }); + throw new VpnLifecycleException($"Failed to stop VPN. Service reported failure: {reply.Stop.ErrorMessage}"); + } } public async ValueTask DisposeAsync() diff --git a/App/ViewModels/TrayWindowViewModel.cs b/App/ViewModels/TrayWindowViewModel.cs index 1fccb7e..204d9f0 100644 --- a/App/ViewModels/TrayWindowViewModel.cs +++ b/App/ViewModels/TrayWindowViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Coder.Desktop.App.Models; using Coder.Desktop.App.Services; using Coder.Desktop.Vpn.Proto; @@ -10,6 +11,7 @@ using Microsoft.UI.Dispatching; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Exception = System.Exception; namespace Coder.Desktop.App.ViewModels; @@ -23,22 +25,45 @@ public partial class TrayWindowViewModel : ObservableObject private DispatcherQueue? _dispatcherQueue; - [ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowEnableSection))] + [NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown; // This is a separate property because we need the switch to be 2-way. [ObservableProperty] public partial bool VpnSwitchActive { get; set; } = false; - [ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ShowEnableSection))] + [NotifyPropertyChangedFor(nameof(ShowWorkspacesHeader))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))] + [NotifyPropertyChangedFor(nameof(ShowFailedSection))] + public partial string? VpnFailedMessage { get; set; } = null; [ObservableProperty] - [NotifyPropertyChangedFor(nameof(NoAgents))] - [NotifyPropertyChangedFor(nameof(AgentOverflow))] [NotifyPropertyChangedFor(nameof(VisibleAgents))] + [NotifyPropertyChangedFor(nameof(ShowNoAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentsSection))] + [NotifyPropertyChangedFor(nameof(ShowAgentOverflowButton))] public partial List Agents { get; set; } = []; - public bool NoAgents => Agents.Count == 0; + public bool ShowEnableSection => VpnFailedMessage is null && VpnLifecycle is not VpnLifecycle.Started; + + public bool ShowWorkspacesHeader => VpnFailedMessage is null && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowNoAgentsSection => + VpnFailedMessage is null && Agents.Count == 0 && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowAgentsSection => + VpnFailedMessage is null && Agents.Count > 0 && VpnLifecycle is VpnLifecycle.Started; + + public bool ShowFailedSection => VpnFailedMessage is not null; - public bool AgentOverflow => Agents.Count > MaxAgents; + public bool ShowAgentOverflowButton => VpnFailedMessage is null && Agents.Count > MaxAgents; [ObservableProperty] [NotifyPropertyChangedFor(nameof(VisibleAgents))] @@ -190,24 +215,47 @@ public void VpnSwitch_Toggled(object sender, RoutedEventArgs e) { if (sender is not ToggleSwitch toggleSwitch) return; - VpnFailedMessage = ""; + VpnFailedMessage = null; + + // The start/stop methods will call back to update the state. + if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped) + _ = StartVpn(); // in the background + else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started) + _ = StopVpn(); // in the background + else + toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; + } + + private async Task StartVpn() + { try { - // The start/stop methods will call back to update the state. - if (toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Stopped) - _rpcController.StartVpn(); - else if (!toggleSwitch.IsOn && VpnLifecycle is VpnLifecycle.Started) - _rpcController.StopVpn(); - else - toggleSwitch.IsOn = VpnLifecycle is VpnLifecycle.Starting or VpnLifecycle.Started; + await _rpcController.StartVpn(); } - catch + catch (Exception e) { - // TODO: display error - VpnFailedMessage = e.ToString(); + VpnFailedMessage = "Failed to start CoderVPN: " + MaybeUnwrapTunnelError(e); } } + private async Task StopVpn() + { + try + { + await _rpcController.StopVpn(); + } + catch (Exception e) + { + VpnFailedMessage = "Failed to stop CoderVPN: " + MaybeUnwrapTunnelError(e); + } + } + + private static string MaybeUnwrapTunnelError(Exception e) + { + if (e is VpnLifecycleException vpnError) return vpnError.Message; + return e.ToString(); + } + [RelayCommand] public void ToggleShowAllAgents() { diff --git a/App/Views/Pages/TrayWindowMainPage.xaml b/App/Views/Pages/TrayWindowMainPage.xaml index 66ec273..e466826 100644 --- a/App/Views/Pages/TrayWindowMainPage.xaml +++ b/App/Views/Pages/TrayWindowMainPage.xaml @@ -13,15 +13,11 @@ - - - + + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + - - + + + + + + + + + + + + HorizontalTextAlignment="Left" + TextTrimming="CharacterEllipsis" + TextWrapping="NoWrap" + Width="180"> + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy