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

#127 system on system (#202)

* Added System<->System RunsOn

* Fixed breadcrumbs / runs on navigation for system to system

* Added resource kind to breadcrumbs
Tim Jones 1 месяц назад
Родитель
Сommit
911b2fbaea

+ 28 - 0
RackPeek.Domain/Resources/Resource.cs

@@ -13,6 +13,34 @@ namespace RackPeek.Domain.Resources;
 
 public abstract class Resource
 {
+    private static readonly string[] HardwareTypes =
+        ["server", "switch", "firewall", "router", "accesspoint", "desktop", "laptop", "ups"];
+
+    public static bool IsHardware(string kind)
+    {
+        kind = kind.Trim().ToLower();
+        return kind == "hardware" || HardwareTypes.Contains(kind);
+    } 
+        
+    public static string GetResourceUrl(string kind, string name)
+    {
+        var encoded = Uri.EscapeDataString(name);
+
+        kind = kind.Trim().ToLower();
+        if (IsHardware(kind))
+        {
+            return $"resources/hardware/{encoded}";
+        }else if (kind == "system")
+        {
+            return $"resources/systems/{encoded}";
+        }else if (kind == "service")
+        {
+            return $"resources/services/{encoded}";
+        }
+
+        return "#";
+    }
+    
     private static readonly Dictionary<string, string> KindToPluralDictionary = new()
     {
         { "hardware", "hardware" },

+ 1 - 1
RackPeek.Domain/RpkConstants.cs

@@ -2,5 +2,5 @@ namespace RackPeek.Domain;
 
 public static class RpkConstants
 {
-    public const string Version = "v1.0.0";
+    public const string Version = "v1.1.0";
 }

+ 1 - 1
RackPeek/RackPeek.csproj

@@ -5,7 +5,7 @@
         <TargetFramework>net10.0</TargetFramework>
         <ImplicitUsings>enable</ImplicitUsings>
         <Nullable>enable</Nullable>
-        <AssemblyVersion>1.0.0</AssemblyVersion>
+        <AssemblyVersion>1.1.0</AssemblyVersion>
     </PropertyGroup>
 
     <ItemGroup>

+ 12 - 7
Shared.Rcl/Components/CrumbLevel.razor

@@ -1,20 +1,21 @@
 <div class="flex flex-col justify-center">
     @if (Items is null || Items.Count == 0)
     {
-        
     }
     else if (Items.Count == 1)
     {
         <div class="flex items-center h-full">
-            <a class="hover:text-white transition-colors"
+            <a class="hover:text-white transition-colors flex items-center gap-1"
                href="@Items[0].Href">
-                @Items[0].Label
+                <span>@Items[0].Label</span>
+                <span class="text-xxs text-zinc-400">
+                    (@Items[0].Kind.ToLower())
+                </span>
             </a>
         </div>
     }
     else
     {
-        <!-- Multi-parent stack -->
         <div class="flex flex-col gap-0.5">
             @for (var i = 0; i < Items.Count; i++)
             {
@@ -26,9 +27,12 @@
                         @(isLast ? "└─" : "├─")
                     </span>
 
-                    <a class="hover:text-white transition-colors"
+                    <a class="hover:text-white transition-colors flex items-center gap-1"
                        href="@crumb.Href">
-                        @crumb.Label
+                        <span>@crumb.Label</span>
+                        <span class="text-xs text-zinc-400">
+                            (@crumb.Kind)
+                        </span>
                     </a>
                 </div>
             }
@@ -37,5 +41,6 @@
 </div>
 
 @code {
-    [Parameter, EditorRequired] public List<ResourceBreadCrumbComponent.Breadcrumb> Items { get; set; } = new();
+    [Parameter, EditorRequired] 
+    public List<ResourceBreadCrumbComponent.Breadcrumb> Items { get; set; } = new();
 }

+ 98 - 47
Shared.Rcl/Components/ResourceBreadCrumbComponent.razor

@@ -1,4 +1,5 @@
 @using RackPeek.Domain.Persistence
+@using RackPeek.Domain.Resources
 @inject IResourceCollection Repo
 
 <div class="text-sm text-zinc-300 flex items-stretch gap-3">
@@ -16,90 +17,140 @@
 </div>
 
 @code {
-    [Parameter] [EditorRequired] public ResourceType ResourceType { get; set; }
-    [Parameter] [EditorRequired] public string ResourceName { get; set; } = default!;
+    [Parameter, EditorRequired] 
+    public ResourceType ResourceType { get; set; }
     
+    public string Kind { get; set; }
+
+    [Parameter, EditorRequired] 
+    public string ResourceName { get; set; } = default!;
+
     private List<List<Breadcrumb>> Levels { get; } = new();
 
     protected override async Task OnParametersSetAsync()
     {
+        Kind = await Repo.GetKind(ResourceName) ?? "";
         Levels.Clear();
 
         switch (ResourceType)
         {
             case ResourceType.Hardware:
-                AddLevel(new Breadcrumb(ResourceName, $"resources/hardware/{Uri.EscapeDataString(ResourceName)}"));
+                AddLevel(new Breadcrumb(
+                    ResourceName,
+                    Kind,
+                    Resource.GetResourceUrl("hardware", ResourceName)));
                 break;
 
             case ResourceType.System:
-                await BuildSystem(ResourceName);
+                await BuildFromNode(ResourceName, "system");
                 break;
 
             case ResourceType.Service:
-                await BuildService(ResourceName);
+                await BuildFromNode(ResourceName, "service");
                 break;
         }
     }
 
-    private void AddLevel(params Breadcrumb[] items)
-        => Levels.Add(items.ToList());
-
-    private void AddLevel(IEnumerable<Breadcrumb> items)
+    private async Task BuildFromNode(string name, string kind)
     {
-        var list = items.ToList();
-        if (list.Count > 0)
-            Levels.Add(list);
-    }
+        var resource = await Repo.GetByNameAsync(name);
+        var parents = resource?.RunsOn ?? Enumerable.Empty<string>();
 
-    private async Task BuildSystem(string systemName)
-    {
-        var system = await Repo.GetByNameAsync(systemName);
+        var byDistance = await BuildAncestorGraph(parents);
 
-        var hwParents = (system?.RunsOn ?? new List<string>())
-            .Where(x => !string.IsNullOrWhiteSpace(x))
-            .Distinct(StringComparer.OrdinalIgnoreCase)
-            .Select(hw => new Breadcrumb(hw, $"resources/hardware/{Uri.EscapeDataString(hw)}"))
-            .OrderBy(b => b.Label, StringComparer.OrdinalIgnoreCase);
+        RenderLevels(byDistance);
 
-        AddLevel(hwParents);
+        AddLevel(new Breadcrumb(
+            name,
+            kind,
+            Resource.GetResourceUrl(kind, name)));
+    }
 
-        AddLevel(new Breadcrumb(systemName, $"resources/systems/{Uri.EscapeDataString(systemName)}"));
+    private void RenderLevels(Dictionary<int, List<(string Name, string Kind)>> byDistance)
+    {
+        foreach (var dist in byDistance.Keys.OrderByDescending(x => x))
+        {
+            var items = byDistance[dist];
+
+            var hardware = items
+                .Where(x => Resource.IsHardware(x.Kind))
+                .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
+                .Select(x => new Breadcrumb(
+                    x.Name,
+                    x.Kind,
+                    Resource.GetResourceUrl(x.Kind, x.Name)));
+
+            var systems = items
+                .Where(x => x.Kind == "system")
+                .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
+                .Select(x => new Breadcrumb(
+                    x.Name,
+                    x.Kind,
+                    Resource.GetResourceUrl(x.Kind, x.Name)));
+
+            AddLevel(hardware);
+            AddLevel(systems);
+        }
     }
 
-    private async Task BuildService(string serviceName)
+    private async Task<Dictionary<int, List<(string Name, string Kind)>>> 
+        BuildAncestorGraph(IEnumerable<string> startingNodes)
     {
-        var service = await Repo.GetByNameAsync(serviceName);
+        var byDistance = new Dictionary<int, List<(string, string)>>();
+        var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+        var queue = new Queue<(string Name, int Dist)>();
 
-        var systemParents = (service?.RunsOn ?? new List<string>())
-            .Where(x => !string.IsNullOrWhiteSpace(x))
-            .Distinct(StringComparer.OrdinalIgnoreCase)
-            .ToList();
-        
-        var hwSet = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+        foreach (var node in startingNodes
+                     .Where(x => !string.IsNullOrWhiteSpace(x))
+                     .Distinct(StringComparer.OrdinalIgnoreCase))
+        {
+            queue.Enqueue((node, 1));
+        }
 
-        foreach (var sysName in systemParents)
+        while (queue.Count > 0)
         {
-            var sys = await Repo.GetByNameAsync(sysName);
-            if (sys?.RunsOn is { Count: > 0 })
+            var (name, dist) = queue.Dequeue();
+            if (!visited.Add(name))
+                continue;
+
+            var kind = await Repo.GetKind(name);
+            if (kind is null)
+                continue;
+
+            kind = kind.Trim().ToLowerInvariant();
+
+            if (!byDistance.TryGetValue(dist, out var list))
             {
-                foreach (var hw in sys.RunsOn.Where(x => !string.IsNullOrWhiteSpace(x)))
-                    hwSet.Add(hw);
+                list = new List<(string, string)>();
+                byDistance[dist] = list;
             }
-        }
 
-        var hwParents = hwSet
-            .Select(hw => new Breadcrumb(hw, $"resources/hardware/{Uri.EscapeDataString(hw)}"))
-            .OrderBy(b => b.Label, StringComparer.OrdinalIgnoreCase);
+            list.Add((name, kind));
 
-        var sysCrumbs = systemParents
-            .Select(sys => new Breadcrumb(sys, $"resources/systems/{Uri.EscapeDataString(sys)}"))
-            .OrderBy(b => b.Label, StringComparer.OrdinalIgnoreCase);
+            if (kind == "system")
+            {
+                var res = await Repo.GetByNameAsync(name);
+                foreach (var parent in (res?.RunsOn ?? Enumerable.Empty<string>())
+                         .Where(x => !string.IsNullOrWhiteSpace(x))
+                         .Distinct(StringComparer.OrdinalIgnoreCase))
+                {
+                    queue.Enqueue((parent, dist + 1));
+                }
+            }
+        }
 
-        AddLevel(hwParents);
-        AddLevel(sysCrumbs);
+        return byDistance;
+    }
 
-        AddLevel(new Breadcrumb(serviceName, $"resources/services/{Uri.EscapeDataString(serviceName)}"));
+    private void AddLevel(params Breadcrumb[] items)
+        => AddLevel((IEnumerable<Breadcrumb>)items);
+
+    private void AddLevel(IEnumerable<Breadcrumb> items)
+    {
+        var list = items.ToList();
+        if (list.Count > 0)
+            Levels.Add(list);
     }
 
-    public record Breadcrumb(string Label, string Href);
+    public record Breadcrumb(string Label, string Kind, string Href);
 }

+ 3 - 5
Shared.Rcl/Hardware/HardwareDetailsPage.razor

@@ -27,7 +27,8 @@
 
 <ResourceBreadCrumbComponent
     ResourceType="ResourceType.Hardware"
-    ResourceName="@HardwareName"/>
+    ResourceName="@HardwareName"
+    />
 
 <div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6">
     @if (_hardware is null && !_loading)
@@ -94,15 +95,12 @@
             </div>
         }
 
-<!---
-TODO: Figure out how lists work
         <div class="m-4">
             <AddResourceComponent TResource="SystemResource"
                                   Placeholder="System name"
                                   OnCreated="NavigateToNewResource"
-                                  RunsOn="@HardwareName"/>
+                                  RunsOn="@(new List<string>(){HardwareName})"/>
         </div>
---->
     }
 </div>
 

+ 11 - 11
Shared.Rcl/Systems/SystemCardComponent.razor

@@ -1,4 +1,5 @@
 @using RackPeek.Domain.Persistence
+@using RackPeek.Domain.Resources
 @using RackPeek.Domain.Resources.SubResources
 @using RackPeek.Domain.Resources.SystemResources
 @using RackPeek.Domain.Resources.SystemResources.UseCases
@@ -179,14 +180,16 @@
             {
                 @foreach (var parent in System.RunsOn)
                 {
-                    var kind = _parentKinds.TryGetValue(parent, out var k) ? k : null;
+                    var kind = (_parentKinds.GetValueOrDefault(parent) ?? "").Trim().ToLower();
 
-                    var url = kind switch
+                    string url = "#";
+                    if (Resource.IsHardware(kind))
                     {
-                        "Hardware" => $"resources/hardware/{Uri.EscapeDataString(parent)}",
-                        "System" => $"resources/systems/{Uri.EscapeDataString(parent)}",
-                        _ => "#"
-                    };
+                        url = $"resources/hardware/{Uri.EscapeDataString(parent)}";
+                    }else if (kind == "system")
+                    {
+                        url = $"resources/systems/{Uri.EscapeDataString(parent)}";
+                    }
 
                     <NavLink href="@url"
                              class="text-emerald-400 text-sm pr-4">
@@ -344,13 +347,10 @@
     {
         _parentKinds.Clear();
 
-        if (System.RunsOn == null)
-            return;
-
         foreach (var parent in System.RunsOn)
         {
-            var kind = await ResourceCollection.GetKind(parent);
-            _parentKinds[parent] = kind;
+            var kind = (await ResourceCollection.GetKind(parent))?.ToLower();
+            _parentKinds[parent] = kind ?? "";
         }
     }
     

+ 13 - 0
Shared.Rcl/Systems/SystemsDetailsPage.razor

@@ -47,6 +47,13 @@
                                   Placeholder="Service name"
                                   OnCreated="NavigateToNewResource"
                                   RunsOn="@(new List<string>{SystemName})"/>
+            
+        </div>
+        <div class="m-4">
+            <AddResourceComponent TResource="SystemResource"
+                                  Placeholder="System name"
+                                  OnCreated="NavigateToNewSystemResource"
+                                  RunsOn="@(new List<string>{SystemName})"/>
         </div>
     }
 </div>
@@ -94,5 +101,11 @@
         Nav.NavigateTo($"resources/services/{Uri.EscapeDataString(serverName)}");
         return Task.CompletedTask;
     }
+    
+    private Task NavigateToNewSystemResource(string serverName)
+    {
+        Nav.NavigateTo($"resources/systems/{Uri.EscapeDataString(serverName)}");
+        return Task.CompletedTask;
+    }
 
 }