| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195 |
- @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">
- @for (var i = 0; i < Levels.Count; i++)
- {
- if (i > 0)
- {
- <div class="flex items-center text-zinc-500">/</div>
- }
- <div class="flex">
- <CrumbLevel Items="Levels[i]"/>
- </div>
- }
- </div>
- @code {
- [Parameter] [EditorRequired] public ResourceType ResourceType { get; set; }
- public string Kind { get; set; } = string.Empty;
- [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,
- Humanise(Kind),
- Resource.GetResourceUrl("hardware", ResourceName)));
- break;
- case ResourceType.System:
- await BuildFromNode(ResourceName, "system");
- break;
- case ResourceType.Service:
- await BuildFromNode(ResourceName, "service");
- break;
- }
- }
- private async Task BuildFromNode(string name, string kind)
- {
- var resource = await Repo.GetByNameAsync(name);
- var parents = resource?.RunsOn ?? Enumerable.Empty<string>();
- var byDistance = await BuildAncestorGraph(parents);
- RenderLevels(byDistance);
- AddLevel(new Breadcrumb(
- name,
- DescribeKind(kind, resource),
- Resource.GetResourceUrl(kind, name)));
- }
- /// <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))
- {
- 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.Description,
- 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.Description,
- Resource.GetResourceUrl(x.Kind, x.Name)));
- AddLevel(hardware);
- AddLevel(systems);
- }
- }
- private async Task<Dictionary<int, List<(string Name, string Kind, string Description)>>>
- BuildAncestorGraph(IEnumerable<string> startingNodes)
- {
- var byDistance = new Dictionary<int, List<(string, string, string)>>();
- var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
- var queue = new Queue<(string Name, int Dist)>();
- foreach (var node in startingNodes
- .Where(x => !string.IsNullOrWhiteSpace(x))
- .Distinct(StringComparer.OrdinalIgnoreCase))
- {
- queue.Enqueue((node, 1));
- }
- while (queue.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))
- {
- list = new List<(string, string, string)>();
- byDistance[dist] = list;
- }
- var res = await Repo.GetByNameAsync(name);
- list.Add((name, kind, DescribeKind(kind, res)));
- if (kind == "system")
- {
- foreach (var parent in (res?.RunsOn ?? Enumerable.Empty<string>())
- .Where(x => !string.IsNullOrWhiteSpace(x))
- .Distinct(StringComparer.OrdinalIgnoreCase))
- {
- queue.Enqueue((parent, dist + 1));
- }
- }
- }
- return byDistance;
- }
- 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);
- }
- /// <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);
- }
|