ResourceBreadCrumbComponent.razor 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. @using RackPeek.Domain.Persistence
  2. @using RackPeek.Domain.Resources
  3. @using RackPeek.Domain.Resources.SystemResources
  4. @inject IResourceCollection Repo
  5. <div class="text-sm text-zinc-300 flex items-stretch gap-3">
  6. @for (var i = 0; i < Levels.Count; i++)
  7. {
  8. if (i > 0)
  9. {
  10. <div class="flex items-center text-zinc-500">/</div>
  11. }
  12. <div class="flex">
  13. <CrumbLevel Items="Levels[i]"/>
  14. </div>
  15. }
  16. </div>
  17. @code {
  18. [Parameter] [EditorRequired] public ResourceType ResourceType { get; set; }
  19. public string Kind { get; set; } = string.Empty;
  20. [Parameter] [EditorRequired] public string ResourceName { get; set; } = default!;
  21. private List<List<Breadcrumb>> Levels { get; } = new();
  22. protected override async Task OnParametersSetAsync()
  23. {
  24. Kind = await Repo.GetKind(ResourceName) ?? "";
  25. Levels.Clear();
  26. switch (ResourceType)
  27. {
  28. case ResourceType.Hardware:
  29. AddLevel(new Breadcrumb(
  30. ResourceName,
  31. Humanise(Kind),
  32. Resource.GetResourceUrl("hardware", ResourceName)));
  33. break;
  34. case ResourceType.System:
  35. await BuildFromNode(ResourceName, "system");
  36. break;
  37. case ResourceType.Service:
  38. await BuildFromNode(ResourceName, "service");
  39. break;
  40. }
  41. }
  42. private async Task BuildFromNode(string name, string kind)
  43. {
  44. var resource = await Repo.GetByNameAsync(name);
  45. var parents = resource?.RunsOn ?? Enumerable.Empty<string>();
  46. var byDistance = await BuildAncestorGraph(parents);
  47. RenderLevels(byDistance);
  48. AddLevel(new Breadcrumb(
  49. name,
  50. DescribeKind(kind, resource),
  51. Resource.GetResourceUrl(kind, name)));
  52. }
  53. /// <summary>
  54. /// What to show in brackets after a crumb. "System" is the storage kind, not a
  55. /// useful description — a chain reading Server / System / System hides the very
  56. /// thing the chain exists to show, so a system reports its own type instead:
  57. /// Server / Hypervisor / VM.
  58. /// </summary>
  59. private static string DescribeKind(string kind, Resource? resource)
  60. {
  61. var type = (resource as SystemResource)?.Type;
  62. return Humanise(string.IsNullOrWhiteSpace(type) ? kind : type);
  63. }
  64. /// <summary>
  65. /// Kinds and types are stored lower-case. Title-case them for display, keeping
  66. /// the initialisms that would look wrong that way.
  67. /// </summary>
  68. private static string Humanise(string value)
  69. {
  70. if (string.IsNullOrWhiteSpace(value))
  71. return string.Empty;
  72. var trimmed = value.Trim();
  73. return trimmed.ToLowerInvariant() switch
  74. {
  75. "vm" => "VM",
  76. "ups" => "UPS",
  77. "accesspoint" => "Access Point",
  78. "baremetal" => "Bare metal",
  79. _ => char.ToUpperInvariant(trimmed[0]) + trimmed[1..].ToLowerInvariant()
  80. };
  81. }
  82. private void RenderLevels(Dictionary<int, List<(string Name, string Kind, string Description)>> byDistance)
  83. {
  84. foreach (var dist in byDistance.Keys.OrderByDescending(x => x))
  85. {
  86. var items = byDistance[dist];
  87. var hardware = items
  88. .Where(x => Resource.IsHardware(x.Kind))
  89. .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
  90. .Select(x => new Breadcrumb(
  91. x.Name,
  92. x.Description,
  93. Resource.GetResourceUrl(x.Kind, x.Name)));
  94. var systems = items
  95. .Where(x => x.Kind == "system")
  96. .OrderBy(x => x.Name, StringComparer.OrdinalIgnoreCase)
  97. .Select(x => new Breadcrumb(
  98. x.Name,
  99. x.Description,
  100. Resource.GetResourceUrl(x.Kind, x.Name)));
  101. AddLevel(hardware);
  102. AddLevel(systems);
  103. }
  104. }
  105. private async Task<Dictionary<int, List<(string Name, string Kind, string Description)>>>
  106. BuildAncestorGraph(IEnumerable<string> startingNodes)
  107. {
  108. var byDistance = new Dictionary<int, List<(string, string, string)>>();
  109. var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
  110. var queue = new Queue<(string Name, int Dist)>();
  111. foreach (var node in startingNodes
  112. .Where(x => !string.IsNullOrWhiteSpace(x))
  113. .Distinct(StringComparer.OrdinalIgnoreCase))
  114. {
  115. queue.Enqueue((node, 1));
  116. }
  117. while (queue.Count > 0)
  118. {
  119. var (name, dist) = queue.Dequeue();
  120. if (!visited.Add(name))
  121. continue;
  122. var kind = await Repo.GetKind(name);
  123. if (kind is null)
  124. continue;
  125. kind = kind.Trim().ToLowerInvariant();
  126. if (!byDistance.TryGetValue(dist, out var list))
  127. {
  128. list = new List<(string, string, string)>();
  129. byDistance[dist] = list;
  130. }
  131. var res = await Repo.GetByNameAsync(name);
  132. list.Add((name, kind, DescribeKind(kind, res)));
  133. if (kind == "system")
  134. {
  135. foreach (var parent in (res?.RunsOn ?? Enumerable.Empty<string>())
  136. .Where(x => !string.IsNullOrWhiteSpace(x))
  137. .Distinct(StringComparer.OrdinalIgnoreCase))
  138. {
  139. queue.Enqueue((parent, dist + 1));
  140. }
  141. }
  142. }
  143. return byDistance;
  144. }
  145. private void AddLevel(params Breadcrumb[] items)
  146. {
  147. AddLevel((IEnumerable<Breadcrumb>)items);
  148. }
  149. private void AddLevel(IEnumerable<Breadcrumb> items)
  150. {
  151. var list = items.ToList();
  152. if (list.Count > 0)
  153. Levels.Add(list);
  154. }
  155. /// <summary><c>Description</c> is what shows in brackets: a system's type
  156. /// (Hypervisor, VM) where it has one, otherwise its kind.</summary>
  157. public record Breadcrumb(string Label, string Description, string Href);
  158. }