Kaynağa Gözat

Merge pull request #345 from Timmoth/fix/breadcrumb-shows-system-type

Show a system's type in the breadcrumb, not its storage kind
Tim Jones 2 gün önce
ebeveyn
işleme
ca5b0dd4a7

+ 2 - 2
Shared.Rcl/Components/CrumbLevel.razor

@@ -9,7 +9,7 @@
                href="@Items[0].Href">
                 <span>@Items[0].Label</span>
                 <span class="text-xxs text-zinc-400">
-                    (@Items[0].Kind.ToLower())
+                    (@Items[0].Description)
                 </span>
             </a>
         </div>
@@ -31,7 +31,7 @@
                        href="@crumb.Href">
                         <span>@crumb.Label</span>
                         <span class="text-xs text-zinc-400">
-                            (@crumb.Kind)
+                            (@crumb.Description)
                         </span>
                     </a>
                 </div>

+ 49 - 11
Shared.Rcl/Components/ResourceBreadCrumbComponent.razor

@@ -1,5 +1,6 @@
 @using RackPeek.Domain.Persistence
 @using RackPeek.Domain.Resources
+@using RackPeek.Domain.Resources.SystemResources
 @inject IResourceCollection Repo
 
 <div class="text-sm text-zinc-300 flex items-stretch gap-3">
@@ -35,7 +36,7 @@
             case ResourceType.Hardware:
                 AddLevel(new Breadcrumb(
                     ResourceName,
-                    Kind,
+                    Humanise(Kind),
                     Resource.GetResourceUrl("hardware", ResourceName)));
                 break;
 
@@ -60,11 +61,45 @@
 
         AddLevel(new Breadcrumb(
             name,
-            kind,
+            DescribeKind(kind, resource),
             Resource.GetResourceUrl(kind, name)));
     }
 
-    private void RenderLevels(Dictionary<int, List<(string Name, string Kind)>> byDistance)
+    /// <summary>
+    ///     What to show in brackets after a crumb. "System" is the storage kind, not a
+    ///     useful description — a chain reading Server / System / System hides the very
+    ///     thing the chain exists to show, so a system reports its own type instead:
+    ///     Server / Hypervisor / VM.
+    /// </summary>
+    private static string DescribeKind(string kind, Resource? resource)
+    {
+        var type = (resource as SystemResource)?.Type;
+
+        return Humanise(string.IsNullOrWhiteSpace(type) ? kind : type);
+    }
+
+    /// <summary>
+    ///     Kinds and types are stored lower-case. Title-case them for display, keeping
+    ///     the initialisms that would look wrong that way.
+    /// </summary>
+    private static string Humanise(string value)
+    {
+        if (string.IsNullOrWhiteSpace(value))
+            return string.Empty;
+
+        var trimmed = value.Trim();
+
+        return trimmed.ToLowerInvariant() switch
+        {
+            "vm" => "VM",
+            "ups" => "UPS",
+            "accesspoint" => "Access Point",
+            "baremetal" => "Bare metal",
+            _ => char.ToUpperInvariant(trimmed[0]) + trimmed[1..].ToLowerInvariant()
+        };
+    }
+
+    private void RenderLevels(Dictionary<int, List<(string Name, string Kind, string Description)>> byDistance)
     {
         foreach (var dist in byDistance.Keys.OrderByDescending(x => x))
         {
@@ -75,7 +110,7 @@
                 .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
                 .Select(x => new Breadcrumb(
                     x.Name,
-                    x.Kind,
+                    x.Description,
                     Resource.GetResourceUrl(x.Kind, x.Name)));
 
             var systems = items
@@ -83,7 +118,7 @@
                 .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
                 .Select(x => new Breadcrumb(
                     x.Name,
-                    x.Kind,
+                    x.Description,
                     Resource.GetResourceUrl(x.Kind, x.Name)));
 
             AddLevel(hardware);
@@ -91,10 +126,10 @@
         }
     }
 
-    private async Task<Dictionary<int, List<(string Name, string Kind)>>>
+    private async Task<Dictionary<int, List<(string Name, string Kind, string Description)>>>
         BuildAncestorGraph(IEnumerable<string> startingNodes)
     {
-        var byDistance = new Dictionary<int, List<(string, string)>>();
+        var byDistance = new Dictionary<int, List<(string, string, string)>>();
         var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
         var queue = new Queue<(string Name, int Dist)>();
 
@@ -119,15 +154,16 @@
 
             if (!byDistance.TryGetValue(dist, out var list))
             {
-                list = new List<(string, string)>();
+                list = new List<(string, string, string)>();
                 byDistance[dist] = list;
             }
 
-            list.Add((name, kind));
+            var res = await Repo.GetByNameAsync(name);
+
+            list.Add((name, kind, DescribeKind(kind, res)));
 
             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))
@@ -152,6 +188,8 @@
             Levels.Add(list);
     }
 
-    public record Breadcrumb(string Label, string Kind, string Href);
+    /// <summary><c>Description</c> is what shows in brackets: a system's type
+    /// (Hypervisor, VM) where it has one, otherwise its kind.</summary>
+    public record Breadcrumb(string Label, string Description, string Href);
 
 }

+ 78 - 0
Tests.E2e/BreadcrumbTypeTests.cs

@@ -0,0 +1,78 @@
+using Microsoft.Playwright;
+using Tests.E2e.Infra;
+using Tests.E2e.PageObjectModels;
+using Xunit.Abstractions;
+
+namespace Tests.E2e;
+
+/// <summary>
+///     The breadcrumb used to label every System with its storage kind, so a guest on a
+///     hypervisor on a server read "nebula (server) / nebula-pve (system) / immich
+///     (system)" — the chain existed to show the nesting and then hid what each layer
+///     actually was. A System now reports its own type instead.
+/// </summary>
+public class BreadcrumbTypeTests(
+    PlaywrightFixture fixture,
+    ITestOutputHelper output) : E2ETestBase(fixture, output) {
+    private readonly PlaywrightFixture _fixture = fixture;
+    private readonly ITestOutputHelper _output = output;
+
+    private static string HypervisorStack(string server, string hypervisor, string guest) =>
+        $"""
+         version: 4
+         resources:
+           - kind: Server
+             name: {server}
+           - kind: System
+             name: {hypervisor}
+             type: hypervisor
+             runsOn:
+               - {server}
+           - kind: System
+             name: {guest}
+             type: vm
+             runsOn:
+               - {hypervisor}
+         connections: []
+         """;
+
+    [Fact]
+    public async Task A_breadcrumb_names_each_layers_type_not_its_storage_kind() {
+        (IBrowserContext context, IPage page) = await CreatePageAsync();
+
+        var server = $"e2e-bcs-{Guid.NewGuid():N}"[..14];
+        var hypervisor = $"e2e-bch-{Guid.NewGuid():N}"[..14];
+        var guest = $"e2e-bcg-{Guid.NewGuid():N}"[..14];
+
+        try {
+            var import = new YamlImportPom(page);
+            await import.GotoAsync(_fixture.BaseUrl);
+            await import.PasteAsync(HypervisorStack(server, hypervisor, guest));
+            await import.AssertNoErrorAsync();
+            await import.ApplyAsync();
+
+            await page.GotoAsync(
+                $"{_fixture.BaseUrl}/resources/systems/{Uri.EscapeDataString(guest)}");
+
+            await Assertions.Expect(page.GetByTestId("circuit-probe"))
+                .ToHaveAttributeAsync("data-circuit-ready", "true");
+
+            var body = await page.InnerTextAsync("body");
+
+            Assert.Contains("(Server)", body);
+            Assert.Contains("(Hypervisor)", body);
+            Assert.Contains("(VM)", body);
+
+            // The kind must no longer stand in for the two systems' types.
+            Assert.DoesNotContain("(system)", body, StringComparison.OrdinalIgnoreCase);
+        }
+        catch (Exception) {
+            _output.WriteLine($"TEST FAILED — URL: {page.Url}");
+            _output.WriteLine(await page.ContentAsync());
+            throw;
+        }
+        finally {
+            await context.CloseAsync();
+        }
+    }
+}