From 419316efefe4d003a8db650f0709d19725ee2c8f Mon Sep 17 00:00:00 2001 From: DualFroz Date: Wed, 26 Aug 2026 16:30:39 +0200 Subject: [PATCH] feat(app): add host, docker, and health check view models --- .../ViewModels/DockerContainerViewModel.cs | 30 +++++++ src/Vantage.App/ViewModels/DockerViewModel.cs | 87 +++++++++++++++++++ .../ViewModels/HealthCheckViewModel.cs | 41 +++++++++ src/Vantage.App/ViewModels/HostViewModel.cs | 64 ++++++++++++++ src/Vantage.App/ViewModels/ViewModelBase.cs | 7 ++ 5 files changed, 229 insertions(+) create mode 100644 src/Vantage.App/ViewModels/DockerContainerViewModel.cs create mode 100644 src/Vantage.App/ViewModels/DockerViewModel.cs create mode 100644 src/Vantage.App/ViewModels/HealthCheckViewModel.cs create mode 100644 src/Vantage.App/ViewModels/HostViewModel.cs create mode 100644 src/Vantage.App/ViewModels/ViewModelBase.cs diff --git a/src/Vantage.App/ViewModels/DockerContainerViewModel.cs b/src/Vantage.App/ViewModels/DockerContainerViewModel.cs new file mode 100644 index 0000000..f32d02c --- /dev/null +++ b/src/Vantage.App/ViewModels/DockerContainerViewModel.cs @@ -0,0 +1,30 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Vantage.Core.Models; + +namespace Vantage.App.ViewModels; + +public partial class DockerContainerViewModel : ViewModelBase +{ + [ObservableProperty] private DockerContainer container; + + public DockerContainerViewModel(DockerContainer container) + { + this.container = container; + } + + public string Id => Container.Id; + public string Name => Container.Name; + public string Image => Container.Image; + public string StatusText => Container.StatusText; + public bool IsRunning => Container.IsRunning; + public ContainerState State => Container.State; + + partial void OnContainerChanged(DockerContainer value) + { + OnPropertyChanged(nameof(Name)); + OnPropertyChanged(nameof(Image)); + OnPropertyChanged(nameof(StatusText)); + OnPropertyChanged(nameof(IsRunning)); + OnPropertyChanged(nameof(State)); + } +} diff --git a/src/Vantage.App/ViewModels/DockerViewModel.cs b/src/Vantage.App/ViewModels/DockerViewModel.cs new file mode 100644 index 0000000..08b2d70 --- /dev/null +++ b/src/Vantage.App/ViewModels/DockerViewModel.cs @@ -0,0 +1,87 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Vantage.Core.Docker; +using Vantage.Core.Models; + +namespace Vantage.App.ViewModels; + +public partial class DockerViewModel : ViewModelBase +{ + private readonly IDockerProvider dockerProvider; + + [ObservableProperty] private bool isBusy; + [ObservableProperty] private string? lastError; + [ObservableProperty] private DockerContainerViewModel? selectedContainer; + + public DockerViewModel(IDockerProvider dockerProvider) + { + this.dockerProvider = dockerProvider; + } + + public ObservableCollection Containers { get; } = new(); + + public HostConfig? Host { get; private set; } + + public void SetHost(HostConfig? host) + { + Host = host; + Containers.Clear(); + } + + [RelayCommand] + public async Task RefreshAsync(CancellationToken cancellationToken) + { + if (Host is null) + return; + + IsBusy = true; + LastError = null; + try + { + var containers = await dockerProvider.ListContainersAsync(Host, cancellationToken).ConfigureAwait(false); + Containers.Clear(); + foreach (var container in containers) + Containers.Add(new DockerContainerViewModel(container)); + } + catch (Exception ex) + { + LastError = ex.Message; + } + finally + { + IsBusy = false; + } + } + + [RelayCommand] + private Task StartAsync(DockerContainerViewModel? container) => ExecuteAsync(DockerAction.Start, container); + + [RelayCommand] + private Task StopAsync(DockerContainerViewModel? container) => ExecuteAsync(DockerAction.Stop, container); + + [RelayCommand] + private Task RestartAsync(DockerContainerViewModel? container) => ExecuteAsync(DockerAction.Restart, container); + + private async Task ExecuteAsync(DockerAction action, DockerContainerViewModel? container) + { + if (Host is null || container is null) + return; + + IsBusy = true; + LastError = null; + try + { + await dockerProvider.ExecuteAsync(Host, action, container.Id, CancellationToken.None).ConfigureAwait(false); + await RefreshAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + LastError = ex.Message; + } + finally + { + IsBusy = false; + } + } +} diff --git a/src/Vantage.App/ViewModels/HealthCheckViewModel.cs b/src/Vantage.App/ViewModels/HealthCheckViewModel.cs new file mode 100644 index 0000000..99ded79 --- /dev/null +++ b/src/Vantage.App/ViewModels/HealthCheckViewModel.cs @@ -0,0 +1,41 @@ +using System.Globalization; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Vantage.Core.Health; +using Vantage.Core.Models; + +namespace Vantage.App.ViewModels; + +public partial class HealthCheckViewModel : ViewModelBase +{ + private readonly IHealthChecker checker; + + [ObservableProperty] private HealthStatus status = HealthStatus.Unknown; + [ObservableProperty] private string statusText = "Not checked"; + [ObservableProperty] private string latencyText = "-"; + [ObservableProperty] private string lastCheckedText = "never"; + + public HealthCheckViewModel(HealthCheckConfig config, IHealthChecker checker) + { + Config = config; + this.checker = checker; + } + + public HealthCheckConfig Config { get; } + + public string Name => string.IsNullOrWhiteSpace(Config.Name) ? Config.Target : Config.Name; + public string Target => Config.Target; + public string KindText => Config.Kind.ToString().ToUpperInvariant(); + + [RelayCommand] + public async Task RunAsync(CancellationToken cancellationToken) + { + var checkResult = await checker.CheckAsync(Config, cancellationToken).ConfigureAwait(false); + Status = checkResult.Status; + StatusText = checkResult.Status + (string.IsNullOrEmpty(checkResult.Message) ? "" : $" - {checkResult.Message}"); + LatencyText = checkResult.LatencyMs is { } ms + ? ms.ToString("0", CultureInfo.InvariantCulture) + " ms" + : "-"; + LastCheckedText = checkResult.CheckedAt.ToLocalTime().ToString("HH:mm:ss", CultureInfo.InvariantCulture); + } +} diff --git a/src/Vantage.App/ViewModels/HostViewModel.cs b/src/Vantage.App/ViewModels/HostViewModel.cs new file mode 100644 index 0000000..8355c53 --- /dev/null +++ b/src/Vantage.App/ViewModels/HostViewModel.cs @@ -0,0 +1,64 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using Vantage.Core.Metrics; +using Vantage.Core.Models; + +namespace Vantage.App.ViewModels; + +public partial class HostViewModel : ViewModelBase +{ + private readonly MetricsHistory cpuHistory; + private readonly MetricsHistory memoryHistory; + + [ObservableProperty] private double cpuUsagePercent; + [ObservableProperty] private double memoryUsedPercent; + [ObservableProperty] private double diskUsedPercent; + [ObservableProperty] private string memoryText = "-"; + [ObservableProperty] private string diskText = "-"; + [ObservableProperty] private string loadText = "-"; + [ObservableProperty] private string uptimeText = "-"; + [ObservableProperty] private string statusText = "Not polled yet"; + [ObservableProperty] private bool isOnline; + [ObservableProperty] private string? lastError; + + public HostViewModel(HostConfig config, int historyLength) + { + Config = config; + cpuHistory = new MetricsHistory(historyLength); + memoryHistory = new MetricsHistory(historyLength); + } + + public HostConfig Config { get; } + + public string Name => string.IsNullOrWhiteSpace(Config.Name) ? Config.Address : Config.Name; + public string Endpoint => $"{Config.Username}@{Config.Address}:{Config.Port}"; + + public IReadOnlyList CpuPoints => cpuHistory.Snapshot(); + public IReadOnlyList MemoryPoints => memoryHistory.Snapshot(); + + public void ApplyMetrics(HostMetrics metrics) + { + CpuUsagePercent = metrics.CpuUsagePercent; + MemoryUsedPercent = metrics.MemoryUsedPercent; + DiskUsedPercent = metrics.DiskUsedPercent; + MemoryText = ValueFormatting.BytesPair(metrics.MemoryUsedBytes, metrics.MemoryTotalBytes); + DiskText = ValueFormatting.BytesPair(metrics.DiskUsedBytes, metrics.DiskTotalBytes); + LoadText = $"{metrics.Load1:0.00} {metrics.Load5:0.00} {metrics.Load15:0.00}"; + UptimeText = ValueFormatting.Uptime(metrics.Uptime); + + cpuHistory.Add(metrics.CpuUsagePercent); + memoryHistory.Add(metrics.MemoryUsedPercent); + OnPropertyChanged(nameof(CpuPoints)); + OnPropertyChanged(nameof(MemoryPoints)); + + IsOnline = true; + LastError = null; + StatusText = "Online"; + } + + public void ApplyError(string message) + { + IsOnline = false; + LastError = message; + StatusText = "Unreachable"; + } +} diff --git a/src/Vantage.App/ViewModels/ViewModelBase.cs b/src/Vantage.App/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..4c11e8c --- /dev/null +++ b/src/Vantage.App/ViewModels/ViewModelBase.cs @@ -0,0 +1,7 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Vantage.App.ViewModels; + +public abstract class ViewModelBase : ObservableObject +{ +}