From 286d47636cea52c1980b33e8cb5b0b6c06b85d3c Mon Sep 17 00:00:00 2001 From: Hublack <71221641+HublackBE@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:47:48 +0100 Subject: [PATCH 1/4] Show service state in UI and improve unit data merging Added a new TextBlock to display each service's state in the UI. Updated backend logic to fetch and merge both all units and loaded units, ensuring LoadState is accurate. Extended ServiceInfo with a State property and improved Description to use the unit's description when available. Updated service list population to reflect these changes. --- .../srvMgnt/Views/ServicesPage.xaml | 6 +++ .../srvMgnt/Views/ServicesPage.xaml.cs | 46 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml index 7dbffea..ad1c121 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml @@ -90,6 +90,12 @@ x:Phase="2" Style="{ThemeResource BodyTextBlockStyle}" Margin="12,0,0,8"/> + diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs index bfc727a..4cd775c 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.UI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; @@ -357,13 +358,32 @@ private async Task LoadAllServices(CancellationToken cancellationToken = default } GetUnitsReply? response = await client.Service.GetAllUnitsAsync(request: new GetUnitsRequest(), cancellationToken: cancellationToken); - if (response is null) + GetUnitsReply? loadedResponse = await client.Service.GetLoadedUnitsAsync(request: new GetUnitsRequest(), cancellationToken: cancellationToken); + if (response is null || loadedResponse is null) { await ShowErrorInfoBarAsync("Failed to retrieve services: received null response from agent."); return; } - foreach (LoadedUnit? unit in response.Units) + IEnumerable units = response.Units; + IEnumerable loadedUnits = loadedResponse.Units; + + // Returns the union of both, updating LoadState where possible + units = units.Join( + loadedUnits, + unit => ExtractShortUnitName(unit.Name), + loadedUnit => loadedUnit.Name, + (unit, loadedUnit) => + { + if (loadedUnit is not null) + { + loadedUnit.LoadState = unit.LoadState; + return loadedUnit; + } + return unit; + }); + + foreach (LoadedUnit? unit in units) { string unitName = ExtractShortUnitName(unit.Name); // Choose a color/brush based on unit state @@ -380,12 +400,12 @@ private async Task LoadAllServices(CancellationToken cancellationToken = default ServiceInfo serviceInfo = new ServiceInfo { Name = unitName, - Description = $"State: {unit.LoadState}", + Description = unit.Description ?? unitName, + State = $"State: {unit.LoadState ?? "Unknown"}", Fill = brush }; - await DispatcherQueue.EnqueueAsync(() => - { + await DispatcherQueue.EnqueueAsync(() => { allServices.Add(serviceInfo); services.Add(serviceInfo); }); @@ -612,7 +632,21 @@ public string Description } } - private SolidColorBrush _fill = new SolidColorBrush(Colors.Transparent); + private string _state = string.Empty; + public string State + { + get => _state; + set + { + if (_state != value) + { + _state = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); + } + } + } + + private SolidColorBrush _fill = new SolidColorBrush(Colors.Transparent); public SolidColorBrush Fill { get => _fill; From d33ed9cc2fce5852eee62dff695b1ec9170d1e9b Mon Sep 17 00:00:00 2001 From: Hublack <71221641+HublackBE@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:58:04 +0100 Subject: [PATCH 2/4] Refactor Services UI: add state chips, improve view model Redesign service list to show name, description, and colored state/active-state chips. Refactor ServiceInfo with separate State, ActiveState, StateFill, and ActiveStateFill properties. Update sorting, error handling, and property change notifications. Improve layout and visual clarity. --- .../srvMgnt/Views/ServicesPage.xaml | 68 ++++---- .../srvMgnt/Views/ServicesPage.xaml.cs | 158 ++++++++++-------- 2 files changed, 132 insertions(+), 94 deletions(-) diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml index ad1c121..df0e394 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml @@ -50,9 +50,19 @@ VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="1"> + + + + + - + @@ -64,41 +74,43 @@ - - - - - - - + + + + + + + + + + + + + + + - - + + + + + + + + + - - - + + + - + + diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs index 4cd775c..2072c40 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs @@ -143,7 +143,8 @@ private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) { await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); } - } else + } + else { // Cancel, do nothing } @@ -321,7 +322,7 @@ private async Task InitializeAsync(NavigationEventArgs e, System.Threading.Cance await DispatcherQueue.EnqueueAsync(() => { services.Clear(); - services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, Fill = new SolidColorBrush(Colors.Red) }); + services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, StateFill = new SolidColorBrush(Colors.Red) }); }).ConfigureAwait(false); await ShowErrorInfoBarAsync($"Initialization failed: {ex.Message}"); @@ -397,15 +398,27 @@ private async Task LoadAllServices(CancellationToken cancellationToken = default _ => new SolidColorBrush(Colors.Goldenrod) }; + SolidColorBrush activeStateFill = unit.ActiveState switch + { + "running" => (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"], + "exited" => (SolidColorBrush)Application.Current.Resources["SystemFillColorNeutralBrush"], + "dead" => (SolidColorBrush)Application.Current.Resources["SystemFillColorNeutralBrush"], + "failed" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBackgroundBrush"], + _ => new SolidColorBrush(Colors.Goldenrod) + }; + ServiceInfo serviceInfo = new ServiceInfo { Name = unitName, Description = unit.Description ?? unitName, - State = $"State: {unit.LoadState ?? "Unknown"}", - Fill = brush + State = unit.LoadState ?? "Unknown", + ActiveState = unit.ActiveState ?? "", + StateFill = brush, + ActiveStateFill = activeStateFill }; - await DispatcherQueue.EnqueueAsync(() => { + await DispatcherQueue.EnqueueAsync(() => + { allServices.Add(serviceInfo); services.Add(serviceInfo); }); @@ -469,28 +482,13 @@ private async Task UpdateServiceVisualStateAsync(ServiceInfo serviceInfo, string { await DispatcherQueue.EnqueueAsync(() => { - string desc = action switch - { - "started" => "State: running", - "stopped" => "State: stopped", - "restarted" => "State: running", - "enabled" => "State: enabled", - "disabled" => "State: disabled", - _ => serviceInfo.Description - }; - SolidColorBrush brush = action switch { - "started" => new SolidColorBrush(Colors.Green), - "stopped" => new SolidColorBrush(Colors.Gray), - "restarted" => new SolidColorBrush(Colors.Green), "enabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], "disabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], - _ => serviceInfo.Fill + _ => serviceInfo.StateFill }; - - serviceInfo.Description = desc; - serviceInfo.Fill = brush; + serviceInfo.StateFill = brush; }); } @@ -506,17 +504,17 @@ private void Order_Services() List ordered = order switch { "Name" => services.OrderBy(s => s.Name).ThenBy(s => s.Description).ToList(), - "State" => services.OrderBy(p => p.Description).ThenBy(s => s.Name).ToList(), + "State" => services.OrderBy(p => p.State).ThenBy(s => s.Name).ToList(), _ => services .OrderBy(s => { - if (!string.IsNullOrEmpty(s.Description) && s.Description.Contains("State: enabled", StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty(s.State) && s.State.Contains("enabled", StringComparison.InvariantCultureIgnoreCase)) return 0; - if (!string.IsNullOrEmpty(s.Description) && s.Description.Contains("State: disabled", StringComparison.InvariantCultureIgnoreCase)) + if (!string.IsNullOrEmpty(s.State) && s.State.Contains("disabled", StringComparison.InvariantCultureIgnoreCase)) return 1; return 2; }) - .ThenBy(s => s.Description, StringComparer.InvariantCultureIgnoreCase) + .ThenBy(s => s.State, StringComparer.InvariantCultureIgnoreCase) .ThenBy(s => s.Name, StringComparer.InvariantCultureIgnoreCase) .ToList() }; @@ -600,66 +598,94 @@ await DispatcherQueue.EnqueueAsync(() => } } } - - // Small view-model used by the DataTemplate in XAML. - public sealed class ServiceInfo : INotifyPropertyChanged + + // Small view-model used by the DataTemplate in XAML. + public sealed class ServiceInfo : INotifyPropertyChanged + { + private string _name = string.Empty; + public string Name { - private string _name = string.Empty; - public string Name + get => _name; + set { - get => _name; - set + if (_name != value) { - if (_name != value) - { - _name = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); - } + _name = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } + } - private string _description = string.Empty; - public string Description + private string _description = string.Empty; + public string Description + { + get => _description; + set { - get => _description; - set + if (_description != value) { - if (_description != value) - { - _description = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description))); - } + _description = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Description))); + } + } + } + + private string _state = string.Empty; + public string State + { + get => _state; + set + { + if (_state != value) + { + _state = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); } } + } - private string _state = string.Empty; - public string State + private string _activeState = string.Empty; + public string ActiveState + { + get => _activeState; + set { - get => _state; - set + if (_activeState != value) { - if (_state != value) - { - _state = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); - } + _activeState = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ActiveState))); } + } } - private SolidColorBrush _fill = new SolidColorBrush(Colors.Transparent); - public SolidColorBrush Fill + private SolidColorBrush _stateFill = new SolidColorBrush(Colors.Transparent); + public SolidColorBrush StateFill + { + get => _stateFill; + set { - get => _fill; - set + if (_stateFill != value) { - if (_fill != value) - { - _fill = value; - PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Fill))); - } + _stateFill = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateFill))); } } + } - public event PropertyChangedEventHandler? PropertyChanged; + private SolidColorBrush _activeStateFill = new SolidColorBrush(Colors.Transparent); + public SolidColorBrush ActiveStateFill + { + get => _activeStateFill; + set + { + if (_activeStateFill != value) + { + _activeStateFill = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ActiveStateFill))); + } + } } + + public event PropertyChangedEventHandler? PropertyChanged; } +} From 6d673cf9545e5f3ca23ae97ffd2f8106d3ee502a Mon Sep 17 00:00:00 2001 From: Hublack <71221641+HublackBE@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:24:00 +0100 Subject: [PATCH 3/4] Refactor service list UI and improve action dialogs Refactored the ListView item template in ServicesPage.xaml for a clearer, more modern layout, replacing the custom ItemContainerStyle and adding visual state indicators with colored ellipses. Moved the "Actions" button for better alignment. Updated confirmation dialogs for stopping and restarting services with improved button text and titles. Rewrote UpdateServiceVisualStateAsync to update both state text and color for various actions, and enhanced error handling and info bar messaging for a better user experience. --- .../srvMgnt/Views/ServicesPage.xaml | 77 ++++++++-------- .../srvMgnt/Views/ServicesPage.xaml.cs | 90 ++++++++++++------- 2 files changed, 100 insertions(+), 67 deletions(-) diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml index df0e394..9003741 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml @@ -50,19 +50,9 @@ VerticalAlignment="Stretch" HorizontalAlignment="Stretch" Grid.Row="1"> - - - - - - + @@ -74,43 +64,56 @@ + + + + + - - - - - - - - - + + - - + + + - - - - - - - - - + + + + + + + + + + + + + + - - - - - - + diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs index 33e23ad..c68ca42 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml.cs @@ -120,9 +120,9 @@ private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) ContentDialog dialog = new() { XamlRoot = this.XamlRoot, - Title = $"Are you sure you want to Stop {serviceInfo.Name} ?", + Title = $"Are you sure you want to Stop {serviceInfo.Name}?", CloseButtonText = "Cancel", - PrimaryButtonText = "Kill", + PrimaryButtonText = "Stop", }; ContentDialogResult result = await dialog.ShowAsync(); @@ -158,33 +158,46 @@ private async void ServiceRestartMenuItem_Click(object sender, RoutedEventArgs e ServiceInfo serviceInfo = (ServiceInfo)menuFlyoutItem.DataContext; - try + ContentDialog dialog = new() { - UnitActionReply response = await client!.Service.PerformUnitActionAsync(new UnitActionRequest - { - UnitName = serviceInfo.Name, - Action = UnitActionRequest.Types.UnitAction.Restart - }); + XamlRoot = this.XamlRoot, + Title = $"Are you sure you want to Restart {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Restart", + }; - if (response.Success) - { - await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); - await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); - } - else - { - await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); - } - } - catch (Exception ex) + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) { - if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + try { - await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + UnitActionReply response = await client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Restart + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } } - else + catch (Exception ex) { - await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + } + else + { + await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + } } } } @@ -325,7 +338,6 @@ private async Task InitializeAsync(NavigationEventArgs e, System.Threading.Cance await DispatcherQueue.EnqueueAsync(() => { services.Clear(); - services.Add(new ServiceInfo { Name = "(error)", Description = ex.Message, Fill = new SolidColorBrush(Colors.Red) }); }).ConfigureAwait(false); await ShowErrorInfoBarAsync($"Initialization failed: {ex.Message}"); } @@ -494,13 +506,31 @@ private async Task UpdateServiceVisualStateAsync(ServiceInfo serviceInfo, string { await DispatcherQueue.EnqueueAsync(() => { - SolidColorBrush brush = action switch + switch(action) { - "enabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"], - "disabled" => (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"], - _ => serviceInfo.StateFill - }; - serviceInfo.StateFill = brush; + case "enabled": + serviceInfo.State = "enabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"]; + break; + case "disabled": + serviceInfo.State = "disabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + case "started": + serviceInfo.ActiveState = "started"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"]; + break; + case "restarted": + serviceInfo.ActiveState = "restarted"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"]; + break; + case "stopped": + serviceInfo.ActiveState = "stopped"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + default: + break; + } }); } From ca9eb61e913ad7526c37b1e83c065c1cd6459cf4 Mon Sep 17 00:00:00 2001 From: Hublack <71221641+HublackBE@users.noreply.github.com> Date: Mon, 12 Jan 2026 15:12:26 +0100 Subject: [PATCH 4/4] Add service actions & InfoBar feedback to details view ServiceDetailsWindow now features an "Actions" menu for starting, stopping, restarting, enabling, and disabling services, with async handlers and InfoBar notifications for user feedback. Refactored Agent to Client for clarity. Improved text styles and visual state updates. ServicesPage item layout and interaction feedback enhanced for better usability. --- .../srvMgnt/Views/ServiceDetailsWindow.xaml | 19 +- .../Views/ServiceDetailsWindow.xaml.cs | 254 +++++++++++++++++- .../srvMgnt/Views/ServicesPage.xaml | 12 +- 3 files changed, 271 insertions(+), 14 deletions(-) diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml index a7da255..1f056bf 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServiceDetailsWindow.xaml @@ -14,15 +14,26 @@ - + - - - + + + + + + + + + + + + + + public sealed partial class ServiceDetailsWindow : Window { - public AgentClient Agent { get; } + public AgentClient Client { get; } public ServiceInfo Service { get; } private CancellationTokenSource? _cts; @@ -37,7 +40,7 @@ public sealed partial class ServiceDetailsWindow : Window public ServiceDetailsWindow(AgentClient agentClient, ServiceInfo serviceInfo) { InitializeComponent(); - Agent = agentClient; + Client = agentClient; Service = serviceInfo; ExtendsContentIntoTitleBar = true; @@ -50,15 +53,182 @@ public ServiceDetailsWindow(AgentClient agentClient, ServiceInfo serviceInfo) this.Closed += (_, _) => _cts?.Cancel(); } + private async void ServiceStartMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Start + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully started the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "started"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + + private async void ServiceStopMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + + ServiceInfo serviceInfo = (ServiceInfo)menuFlyoutItem.DataContext; + + ContentDialog dialog = new() + { + XamlRoot = this.Content.XamlRoot, + Title = $"Are you sure you want to Stop {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Stop", + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(request: new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Stop + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully stopped the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "stopped"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + else + { + // Cancel, do nothing + } + + } + + private async void ServiceRestartMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + + ServiceInfo serviceInfo = Service; + + ContentDialog dialog = new() + { + XamlRoot = this.Content.XamlRoot, + Title = $"Are you sure you want to Restart {serviceInfo.Name}?", + CloseButtonText = "Cancel", + PrimaryButtonText = "Restart", + }; + + ContentDialogResult result = await dialog.ShowAsync(); + + if (result == ContentDialogResult.Primary) + { + try + { + UnitActionReply response = await Client!.Service.PerformUnitActionAsync(new UnitActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitActionRequest.Types.UnitAction.Restart + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "restarted"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + catch (Exception ex) + { + if (ex is Grpc.Core.RpcException && serviceInfo.Name == "agent.service") + { + await ShowInfoBarAsync("Service Action Result", $"Successfully restarted the Agent!", InfoBarSeverity.Success); + } + else + { + await ShowErrorInfoBarAsync($"Exception during restart: {ex.Message}"); + } + } + } + } + + private async void ServiceEnableMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitFileActionReply response = await Client!.Service.PerformUnitFileActionAsync(new UnitFileActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitFileActionRequest.Types.UnitFileAction.Enable + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully enabled the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "enabled"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + + private async void ServiceDisableMenuItem_Click(object sender, RoutedEventArgs e) + { + MenuFlyoutItem menuFlyoutItem = (MenuFlyoutItem)sender; + ServiceInfo serviceInfo = Service; + + UnitFileActionReply response = await Client!.Service.PerformUnitFileActionAsync(new UnitFileActionRequest + { + UnitName = serviceInfo.Name, + Action = UnitFileActionRequest.Types.UnitFileAction.Disable + }); + + if (response.Success) + { + await ShowInfoBarAsync("Service Action Result", $"Successfully disabled the service!", InfoBarSeverity.Success); + await UpdateServiceVisualStateAsync(serviceInfo, "disabled"); + await Task.Delay(500); // brief delay to allow logs to populate + await GetLogsAsync(CancellationToken.None); + } + else + { + await ShowErrorInfoBarAsync(response.ErrorMessage ?? "Unknown error"); + } + } + private async Task GetLogsAsync(CancellationToken cancellationToken) { - if (Agent == null) return; + if (Client == null) return; AsyncServerStreamingCall? call = null; try { - call = Agent.Journal.Action(new Journal.V1.JournalRequest() + call = Client.Journal.Action(new Journal.V1.JournalRequest() { NumFromTail = 50, Field = Journal.V1.JournalRequest.Types.Field.Systemd, @@ -152,5 +322,81 @@ private async Task GetLogsAsync(CancellationToken cancellationToken) } } } + + private async Task UpdateServiceVisualStateAsync(ServiceInfo serviceInfo, string action) + { + await DispatcherQueue.EnqueueAsync(() => + { + switch (action) + { + case "enabled": + serviceInfo.State = "enabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorAttentionBrush"]; + break; + case "disabled": + serviceInfo.State = "disabled"; + serviceInfo.StateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + case "started": + serviceInfo.ActiveState = "started"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorSuccessBrush"]; + break; + case "restarted": + serviceInfo.ActiveState = "restarted"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCautionBrush"]; + break; + case "stopped": + serviceInfo.ActiveState = "stopped"; + serviceInfo.ActiveStateFill = (SolidColorBrush)Application.Current.Resources["SystemFillColorCriticalBrush"]; + break; + default: + break; + } + }); + } + + private async Task ShowInfoBarAsync(string title, string message, InfoBarSeverity severity) + { + // Ensure UI thread + await DispatcherQueue.EnqueueAsync(() => + { + if (GridRoot == null) + { + // fallback to page XamlRoot; but InfoBar must be in visual tree to be visible + // if GridRoot is not available the InfoBar won't be shown persistently. + return; + } + + InfoBar infoBar = new() + { + Title = title, + Message = message, + Severity = severity, + IsOpen = true, + VerticalAlignment = VerticalAlignment.Top, + Margin = new Thickness(0, 0, 0, 0) + }; + + // Add to visual tree so it is visible + GridRoot.Children.Add(infoBar); + + // Remove from visual tree when closed + void OnClosed(object? s, InfoBarClosedEventArgs args) + { + infoBar.Closed -= OnClosed; + if (GridRoot.Children.Contains(infoBar)) + { + GridRoot.Children.Remove(infoBar); + } + } + + infoBar.Closed += OnClosed; + }); + } + + private async Task ShowErrorInfoBarAsync(string message) + { + await ShowInfoBarAsync("Service Action Result", $"Error: {message}", InfoBarSeverity.Error); + } } } diff --git a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml index 9003741..a225039 100644 --- a/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml +++ b/src/paradigm-ehb.CommandCenter.WinUI/srvMgnt/Views/ServicesPage.xaml @@ -49,10 +49,11 @@ ShowsScrollingPlaceholders="True" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" - Grid.Row="1"> + Grid.Row="1" + > - + @@ -66,15 +67,14 @@ - + - - + @@ -113,7 +113,7 @@ - +