Просмотр исходного кода

Merge pull request #49 from Timmoth/Summary-Command

Summary command
Tim Jones 2 месяцев назад
Родитель
Сommit
002da73f3b

+ 31 - 0
RackPeek.Domain/Resources/Hardware/GetHardwareUseCaseSummary.cs

@@ -0,0 +1,31 @@
+namespace RackPeek.Domain.Resources.Hardware;
+
+public sealed class HardwareSummary
+{
+    public int TotalHardware { get; }
+    public IReadOnlyDictionary<string, int> HardwareByKind { get; }
+
+    public HardwareSummary(
+        int totalHardware,
+        IReadOnlyDictionary<string, int> hardwareByKind)
+    {
+        TotalHardware = totalHardware;
+        HardwareByKind = hardwareByKind;
+    }
+}
+
+public class GetHardwareUseCaseSummary(IHardwareRepository repository) : IUseCase
+{
+    public async Task<HardwareSummary> ExecuteAsync()
+    {
+        var totalCountTask = repository.GetCountAsync();
+        var kindCountTask = repository.GetKindCountAsync();
+
+        await Task.WhenAll(totalCountTask, kindCountTask);
+
+        return new HardwareSummary(
+            totalCountTask.Result,
+            kindCountTask.Result
+        );
+    }
+}

+ 3 - 0
RackPeek.Domain/Resources/Hardware/IHardwareRepository.cs

@@ -2,6 +2,9 @@ namespace RackPeek.Domain.Resources.Hardware;
 
 public interface IHardwareRepository
 {
+    Task<int> GetCountAsync();
+    Task<Dictionary<string, int>> GetKindCountAsync();
+
     Task<IReadOnlyList<Models.Hardware>> GetAllAsync();
     Task AddAsync(Models.Hardware hardware);
     Task UpdateAsync(Models.Hardware hardware);

+ 3 - 0
RackPeek.Domain/Resources/Services/IServiceRepository.cs

@@ -2,6 +2,9 @@ namespace RackPeek.Domain.Resources.Services;
 
 public interface IServiceRepository
 {
+    Task<int> GetCountAsync();
+    Task<int> GetIpAddressCountAsync();
+
     Task<IReadOnlyList<Service>> GetAllAsync();
     Task AddAsync(Service service);
     Task UpdateAsync(Service service);

+ 28 - 0
RackPeek.Domain/Resources/Services/UseCases/GetServiceSummaryUseCase.cs

@@ -0,0 +1,28 @@
+namespace RackPeek.Domain.Resources.Services.UseCases;
+public sealed class AllServicesSummary
+{
+    public int TotalServices { get; }
+    public int TotalIpAddresses { get; }
+
+    public AllServicesSummary(int totalServices, int totalIpAddresses)
+    {
+        TotalServices = totalServices;
+        TotalIpAddresses = totalIpAddresses;
+    }
+}
+
+public class GetServiceSummaryUseCase(IServiceRepository repository) : IUseCase
+{
+    public async Task<AllServicesSummary> ExecuteAsync()
+    {
+        var serviceCountTask = repository.GetCountAsync();
+        var ipAddressCountTask = repository.GetIpAddressCountAsync();
+
+        await Task.WhenAll(serviceCountTask, ipAddressCountTask);
+
+        return new AllServicesSummary(
+            serviceCountTask.Result,
+            ipAddressCountTask.Result
+        );
+    }
+}

+ 4 - 0
RackPeek.Domain/Resources/SystemResources/ISystemRepository.cs

@@ -2,6 +2,10 @@ namespace RackPeek.Domain.Resources.SystemResources;
 
 public interface ISystemRepository
 {
+    Task<int> GetSystemCountAsync();
+    Task<Dictionary<string, int>> GetSystemTypeCountAsync();
+    Task<Dictionary<string, int>> GetSystemOsCountAsync();
+
     Task<IReadOnlyList<SystemResource>> GetAllAsync();
     Task AddAsync(SystemResource systemResource);
     Task UpdateAsync(SystemResource systemResource);

+ 40 - 0
RackPeek.Domain/Resources/SystemResources/UseCases/GetSystemSummaryUseCase.cs

@@ -0,0 +1,40 @@
+namespace RackPeek.Domain.Resources.SystemResources.UseCases;
+
+public sealed class SystemSummary
+{
+    public int TotalSystems { get; }
+    public IReadOnlyDictionary<string, int> SystemsByType { get; }
+    public IReadOnlyDictionary<string, int> SystemsByOs { get; }
+
+    public SystemSummary(
+        int totalSystems,
+        IReadOnlyDictionary<string, int> systemsByType,
+        IReadOnlyDictionary<string, int> systemsByOs)
+    {
+        TotalSystems = totalSystems;
+        SystemsByType = systemsByType;
+        SystemsByOs = systemsByOs;
+    }
+}
+
+public class GetSystemSummaryUseCase(ISystemRepository repository) : IUseCase
+{
+    public async Task<SystemSummary> ExecuteAsync()
+    {
+        var totalSystemsTask = repository.GetSystemCountAsync();
+        var systemsByTypeTask = repository.GetSystemTypeCountAsync();
+        var systemsByOsTask = repository.GetSystemOsCountAsync();
+
+        await Task.WhenAll(
+            totalSystemsTask,
+            systemsByTypeTask,
+            systemsByOsTask
+        );
+
+        return new SystemSummary(
+            totalSystemsTask.Result,
+            systemsByTypeTask.Result,
+            systemsByOsTask.Result
+        );
+    }
+}

+ 4 - 1
RackPeek.Web/Components/Components/ServiceCardComponent.razor

@@ -49,7 +49,10 @@
             {
                 <div>
                     <div class="text-zinc-400 mb-1">URL</div>
-                    <div class="text-zinc-300 break-all">@Service.Network.Url</div>
+                    <a href="@Service.Network.Url" target="_blank" rel="noopener noreferrer"
+                       class="text-emerald-400 hover:underline break-all">
+                        @Service.Network.Url
+                    </a>
                 </div>
             }
         }

+ 6 - 3
RackPeek.Web/Components/Layout/MainLayout.razor

@@ -4,9 +4,12 @@
 
     <!-- Header / Branding -->
     <header class="flex items-center justify-between p-4 border-b border-zinc-800 bg-zinc-900">
-        <div class="text-xl font-bold text-emerald-400 tracking-wider">
-            rackpeek
-        </div>
+        <NavLink href="/" class="hover:text-emerald-400" activeClass="text-emerald-400 font-semibold">
+            <div class="text-xl font-bold text-emerald-400 tracking-wider">
+                rackpeek
+            </div>
+        </NavLink>
+
 
         <!-- Navigation -->
         <nav class="space-x-6 text-sm">

+ 147 - 1
RackPeek.Web/Components/Pages/Home.razor

@@ -1,5 +1,151 @@
 @page "/"
-@using RackPeek.Web.Components.Components
+
+@using RackPeek.Domain.Resources.Hardware
+@using RackPeek.Domain.Resources.SystemResources.UseCases
+@using RackPeek.Domain.Resources.Services.UseCases
+@inject GetSystemSummaryUseCase SystemSummaryUseCase
+@inject GetServiceSummaryUseCase ServiceSummaryUseCase
+@inject GetHardwareUseCaseSummary HardwareSummaryUseCase
 
 <PageTitle>rackpeek</PageTitle>
 
+<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
+
+    @if (_loading)
+    {
+        <div class="text-zinc-500">loading summary…</div>
+    }
+    else
+    {
+        <!-- Totals -->
+        <div class="mb-10 max-w-md">
+            <div class="border border-zinc-800 rounded-md p-4">
+                <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
+                    Totals
+                </div>
+
+                <div class="grid grid-cols-2 gap-y-2">
+                    
+                        <div><NavLink href="@($"/hardware/tree")">Hardware </NavLink></div>
+                        <div class="text-right">@_hardware!.TotalHardware</div>
+                        
+                        <div><NavLink href="@($"/systems/list")">Systems </NavLink></div>
+                        <div class="text-right">@_system!.TotalSystems</div>
+                        
+                        <div> <NavLink href="@($"/servers/list")">Services  </NavLink></div>
+                        <div class="text-right">@_service!.TotalServices</div>
+                </div>
+            </div>
+        </div>
+
+        <!-- Tree -->
+        <div class="space-y-10">
+
+            <!-- Hardware -->
+            <div>
+                <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
+                    Hardware
+                </div>
+
+                <ul class="space-y-2">
+                    <li class="text-zinc-100">
+                        ├─ Total (@_hardware!.TotalHardware)
+                    </li>
+
+                    @if (_hardware.HardwareByKind.Any())
+                    {
+                        <ul class="ml-4 mt-2 border-l border-zinc-800 pl-4 space-y-1">
+                            @foreach (var (kind, count) in _hardware.HardwareByKind.OrderByDescending(x => x.Value))
+                            {
+                                <li class="text-zinc-500">
+                                    └─ @kind (@count)
+                                </li>
+                            }
+                        </ul>
+                    }
+                </ul>
+            </div>
+
+            
+            <!-- Systems -->
+            <div>
+                <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
+                    Systems
+                </div>
+
+                <ul class="space-y-3">
+                    <li>
+                        <div class="text-zinc-100">
+                            ├─ Total (@_system!.TotalSystems)
+                        </div>
+
+                        @if (_system.SystemsByType.Any())
+                        {
+                            <ul class="ml-4 mt-2 border-l border-zinc-800 pl-4 space-y-1">
+                                <li class="text-zinc-400">Types</li>
+                                @foreach (var (type, count) in _system.SystemsByType.OrderByDescending(x => x.Value))
+                                {
+                                    <li class="text-zinc-500">
+                                        └─ @type (@count)
+                                    </li>
+                                }
+                            </ul>
+                        }
+
+                        @if (_system.SystemsByOs.Any())
+                        {
+                            <ul class="ml-4 mt-2 border-l border-zinc-800 pl-4 space-y-1">
+                                <li class="text-zinc-400">Operating Systems</li>
+                                @foreach (var (os, count) in _system.SystemsByOs.OrderByDescending(x => x.Value))
+                                {
+                                    <li class="text-zinc-500">
+                                        └─ @os (@count)
+                                    </li>
+                                }
+                            </ul>
+                        }
+                    </li>
+                </ul>
+            </div>
+
+            <!-- Services -->
+            <div>
+                <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
+                    Services
+                </div>
+
+                <ul>
+                    <li class="text-zinc-100">
+                        └─ Total (@_service!.TotalServices)
+                    </li>
+                    <li class="ml-4 text-zinc-500">
+                        └─ IP Addresses (@_service!.TotalIpAddresses)
+                    </li>
+                </ul>
+            </div>
+        </div>
+    }
+</div>
+
+@code {
+    private bool _loading = true;
+
+    private SystemSummary? _system;
+    private AllServicesSummary? _service;
+    private HardwareSummary? _hardware;
+
+    protected override async Task OnInitializedAsync()
+    {
+        var systemTask = SystemSummaryUseCase.ExecuteAsync();
+        var serviceTask = ServiceSummaryUseCase.ExecuteAsync();
+        var hardwareTask = HardwareSummaryUseCase.ExecuteAsync();
+
+        await Task.WhenAll(systemTask, serviceTask, hardwareTask);
+
+        _system = systemTask.Result;
+        _service = serviceTask.Result;
+        _hardware = hardwareTask.Result;
+
+        _loading = false;
+    }
+}

+ 4 - 0
RackPeek/CliBootstrap.cs

@@ -1,5 +1,6 @@
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Commands;
 using RackPeek.Commands.AccessPoints;
 using RackPeek.Commands.Desktops;
 using RackPeek.Commands.Desktops.Cpus;
@@ -70,6 +71,9 @@ public static class CliBootstrap
             config.SetApplicationName("rpk");
             config.ValidateExamples();
 
+
+            config.AddCommand<GetTotalSummaryCommand>("summary")
+                .WithDescription("Show a summarized report for all resources");
             // ----------------------------
             // Server commands (CRUD-style)
             // ----------------------------

+ 138 - 0
RackPeek/Commands/GetTotalSummaryCommand.cs

@@ -0,0 +1,138 @@
+using Microsoft.Extensions.DependencyInjection;
+using RackPeek.Domain.Resources.Hardware;
+using RackPeek.Domain.Resources.Services.UseCases;
+using RackPeek.Domain.Resources.SystemResources.UseCases;
+using Spectre.Console;
+using Spectre.Console.Cli;
+
+namespace RackPeek.Commands;
+
+public class GetTotalSummaryCommand(IServiceProvider provider) : AsyncCommand
+{
+    public override async Task<int> ExecuteAsync(
+        CommandContext context,
+        CancellationToken cancellationToken)
+    {
+        using var scope = provider.CreateScope();
+
+        var systemUseCase =
+            scope.ServiceProvider.GetRequiredService<GetSystemSummaryUseCase>();
+        var serviceUseCase =
+            scope.ServiceProvider.GetRequiredService<GetServiceSummaryUseCase>();
+        var hardwareUseCase =
+            scope.ServiceProvider.GetRequiredService<GetHardwareUseCaseSummary>();
+
+        // Execute all summaries in parallel
+        var systemTask = systemUseCase.ExecuteAsync();
+        var serviceTask = serviceUseCase.ExecuteAsync();
+        var hardwareTask = hardwareUseCase.ExecuteAsync();
+
+        await Task.WhenAll(systemTask, serviceTask, hardwareTask);
+
+        var systemSummary = systemTask.Result;
+        var serviceSummary = serviceTask.Result;
+        var hardwareSummary = hardwareTask.Result;
+
+        RenderSummaryTree(systemSummary, serviceSummary, hardwareSummary);
+
+        return 0;
+    }
+
+    private static void RenderSummaryTree(
+        SystemSummary systemSummary,
+        AllServicesSummary serviceSummary,
+        HardwareSummary hardwareSummary)
+    {
+        var tree = new Tree("[bold]Breakdown[/]");
+
+        var hardwareNode = tree.AddNode(
+            $"[bold]Hardware[/] ({hardwareSummary.TotalHardware})");
+
+        foreach (var (kind, count) in hardwareSummary.HardwareByKind.OrderByDescending(h => h.Value).ThenBy(h => h.Key))
+            hardwareNode.AddNode($"{kind}: {count}");
+        
+        var systemsNode = tree.AddNode(
+            $"[bold]Systems[/] ({systemSummary.TotalSystems})");
+
+        if (systemSummary.SystemsByType.Count > 0)
+        {
+            var typesNode = systemsNode.AddNode("[bold]Types[/]");
+            foreach (var (type, count) in systemSummary.SystemsByType.OrderByDescending(h => h.Value).ThenBy(h => h.Key))
+                typesNode.AddNode($"{type}: {count}");
+        }
+
+        if (systemSummary.SystemsByOs.Count > 0)
+        {
+            var osNode = systemsNode.AddNode("[bold]Operating Systems[/]");
+            foreach (var (os, count) in systemSummary.SystemsByOs.OrderByDescending(h => h.Value).ThenBy(h => h.Key))
+                osNode.AddNode($"{os}: {count}");
+        }
+        
+        var servicesNode = tree.AddNode(
+            $"[bold]Services[/] ({serviceSummary.TotalServices})");
+
+        servicesNode.AddNode(
+            $"IP Addresses: {serviceSummary.TotalIpAddresses}");
+
+        AnsiConsole.Write(tree);
+    }
+    
+    private static void RenderTotals(
+        SystemSummary systemSummary,
+        AllServicesSummary serviceSummary,
+        HardwareSummary hardwareSummary)
+    {
+        var grid = new Grid()
+            .AddColumn()
+            .AddColumn();
+
+        grid.AddRow("[bold]Systems[/]", systemSummary.TotalSystems.ToString());
+        grid.AddRow("[bold]Services[/]", serviceSummary.TotalServices.ToString());
+        grid.AddRow("[bold]Service IPs[/]", serviceSummary.TotalIpAddresses.ToString());
+        grid.AddRow("[bold]Hardware[/]", hardwareSummary.TotalHardware.ToString());
+
+        AnsiConsole.Write(
+            new Panel(grid)
+                .Header("[bold]Totals[/]")
+                .Border(BoxBorder.Rounded));
+    }
+
+    private static void RenderSystemBreakdown(SystemSummary systemSummary)
+    {
+        if (systemSummary.SystemsByType.Count == 0 &&
+            systemSummary.SystemsByOs.Count == 0)
+            return;
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .Title("[bold]Systems Breakdown[/]")
+            .AddColumn("Category")
+            .AddColumn("Name")
+            .AddColumn("Count");
+
+        foreach (var (type, count) in systemSummary.SystemsByType)
+            table.AddRow("Type", type, count.ToString());
+
+        foreach (var (os, count) in systemSummary.SystemsByOs)
+            table.AddRow("OS", os, count.ToString());
+
+        AnsiConsole.Write(table);
+    }
+
+    private static void RenderHardwareBreakdown(HardwareSummary hardwareSummary)
+    {
+        if (hardwareSummary.HardwareByKind.Count == 0)
+            return;
+
+        var table = new Table()
+            .Border(TableBorder.Rounded)
+            .Title("[bold]Hardware Breakdown[/]")
+            .AddColumn("Kind")
+            .AddColumn("Count");
+
+        foreach (var (kind, count) in hardwareSummary.HardwareByKind)
+            table.AddRow(kind, count.ToString());
+
+        AnsiConsole.Write(table);
+    }
+}

+ 12 - 0
RackPeek/Yaml/YamlHardwareRepository.cs

@@ -6,6 +6,18 @@ namespace RackPeek.Yaml;
 
 public class YamlHardwareRepository(YamlResourceCollection resources) : IHardwareRepository
 {
+    public Task<int> GetCountAsync()
+    {
+        return Task.FromResult(resources.HardwareResources.Count);
+    }
+
+    public Task<Dictionary<string, int>> GetKindCountAsync()
+    {
+        return Task.FromResult(resources.HardwareResources
+            .GroupBy(h => h.Kind)
+            .ToDictionary(k => k.Key, v => v.Count()));
+    }
+
     public Task<IReadOnlyList<Hardware>> GetAllAsync()
     {
         return Task.FromResult(resources.HardwareResources);

+ 14 - 0
RackPeek/Yaml/YamlServiceRepository.cs

@@ -4,6 +4,20 @@ namespace RackPeek.Yaml;
 
 public class YamlServiceRepository(YamlResourceCollection resources) : IServiceRepository
 {
+    public Task<int> GetCountAsync()
+    {
+        return Task.FromResult(resources.ServiceResources.Count);
+    }
+
+    public Task<int> GetIpAddressCountAsync()
+    {
+        return Task.FromResult(resources.ServiceResources
+            .Where(i => i.Network?.Ip != null)
+            .Select(i => i.Network!.Ip)
+            .Distinct()
+            .Count());
+    }
+    
     public Task<IReadOnlyList<Service>> GetAllAsync()
     {
         return Task.FromResult(resources.ServiceResources);

+ 20 - 0
RackPeek/Yaml/YamlSystemRepository.cs

@@ -4,6 +4,26 @@ namespace RackPeek.Yaml;
 
 public class YamlSystemRepository(YamlResourceCollection resources) : ISystemRepository
 {
+    public Task<int> GetSystemCountAsync()
+    {
+        return Task.FromResult(resources.SystemResources.Count);    }
+
+    public Task<Dictionary<string, int>> GetSystemTypeCountAsync()
+    {
+        return Task.FromResult(resources.SystemResources
+            .Where(s => !string.IsNullOrEmpty(s.Type))
+            .GroupBy(h => h.Type!)
+            .ToDictionary(k => k.Key, v => v.Count()));
+    }
+
+    public Task<Dictionary<string, int>> GetSystemOsCountAsync()
+    {
+        return Task.FromResult(resources.SystemResources
+            .Where(s => !string.IsNullOrEmpty(s.Os))
+            .GroupBy(h => h.Os!)
+            .ToDictionary(k => k.Key, v => v.Count()));
+    }
+    
     public Task<IReadOnlyList<SystemResource>> GetAllAsync()
     {
         return Task.FromResult(resources.SystemResources);