SubnetBrowser.razor 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. @page "/subnets"
  2. @using RackPeek.Domain.Persistence
  3. @using RackPeek.Domain.Resources
  4. @inject IResourceCollection ResourceCollection
  5. <div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6"
  6. data-testid="subnet-browser-root">
  7. <h1 class="text-lg text-zinc-100"
  8. data-testid="subnet-browser-title">
  9. Subnet Browser
  10. </h1>
  11. <!-- Filter -->
  12. <div>
  13. <input
  14. data-testid="subnet-browser-filter"
  15. placeholder="Filter by IP (e.g. 10.0.99)"
  16. class="w-full px-3 py-2 rounded-md
  17. bg-zinc-800 text-zinc-100
  18. border border-zinc-700
  19. focus:outline-none focus:ring-2 focus:ring-emerald-500"
  20. @bind="Filter"
  21. @bind:event="oninput" />
  22. </div>
  23. @if (_grouped is null)
  24. {
  25. <div class="text-zinc-500"
  26. data-testid="subnet-browser-loading">
  27. loading…
  28. </div>
  29. }
  30. else if (!_grouped.Any())
  31. {
  32. <div class="text-zinc-500"
  33. data-testid="subnet-browser-empty">
  34. no matching IPs found
  35. </div>
  36. }
  37. else
  38. {
  39. <div class="space-y-6"
  40. data-testid="subnet-browser-list">
  41. @foreach (var subnetGroup in _grouped.OrderBy(x => x.Key))
  42. {
  43. <div data-testid=@($"subnet-group-{subnetGroup.Key.Replace('.', '-')}")>
  44. <!-- Subnet Header -->
  45. <div class="text-xs text-zinc-500 uppercase tracking-wider mb-3">
  46. @subnetGroup.Key
  47. </div>
  48. <ul class="ml-2 border-l border-zinc-800 pl-4 space-y-3">
  49. @foreach (var ipGroup in subnetGroup.Value.OrderBy(x => x.Key))
  50. {
  51. <li>
  52. <!-- IP -->
  53. <div class="text-zinc-100">
  54. ├─ @ipGroup.Key
  55. </div>
  56. <!-- Resources on this IP -->
  57. <ul class="ml-4 mt-2 border-l border-zinc-800 pl-4 space-y-1">
  58. @foreach (var (resource, _) in ipGroup.Value)
  59. {
  60. var url = GetResourceUrl(resource);
  61. <li class="text-zinc-500 hover:text-emerald-300">
  62. <NavLink href="@url" class="block">
  63. └─ @resource.Name (@resource.GetType().Name.Replace("Resource",""))
  64. </NavLink>
  65. </li>
  66. }
  67. </ul>
  68. </li>
  69. }
  70. </ul>
  71. </div>
  72. }
  73. </div>
  74. }
  75. </div>
  76. @code {
  77. private string _filter = string.Empty;
  78. private IReadOnlyList<(Resource resource, string ip)> _all = [];
  79. private Dictionary<string, Dictionary<string, List<(Resource, string)>>>? _grouped;
  80. protected override async Task OnInitializedAsync()
  81. {
  82. _all = await ResourceCollection.GetResourceIpsAsync();
  83. ApplyFilter();
  84. }
  85. private string Filter
  86. {
  87. get => _filter;
  88. set
  89. {
  90. if (_filter == value)
  91. return;
  92. _filter = value;
  93. ApplyFilter();
  94. }
  95. }
  96. private void ApplyFilter()
  97. {
  98. var filtered = string.IsNullOrWhiteSpace(_filter)
  99. ? _all
  100. : _all.Where(x =>
  101. x.ip.Contains(_filter, StringComparison.OrdinalIgnoreCase))
  102. .ToList();
  103. _grouped = filtered
  104. .Where(x => !string.IsNullOrWhiteSpace(x.ip))
  105. .GroupBy(x => GetSubnet(x.ip))
  106. .ToDictionary(
  107. g => g.Key,
  108. g => g.GroupBy(x => x.ip)
  109. .ToDictionary(
  110. ip => ip.Key,
  111. ip => ip.ToList()
  112. )
  113. );
  114. StateHasChanged();
  115. }
  116. private string GetSubnet(string ip)
  117. {
  118. var parts = ip.Split('.');
  119. return parts.Length == 4
  120. ? $"{parts[0]}.{parts[1]}.{parts[2]}.x"
  121. : "unknown";
  122. }
  123. private string GetResourceUrl(Resource resource)
  124. {
  125. return resource switch
  126. {
  127. RackPeek.Domain.Resources.SystemResources.SystemResource
  128. => $"resources/systems/{Uri.EscapeDataString(resource.Name)}",
  129. RackPeek.Domain.Resources.Services.Service
  130. => $"resources/services/{Uri.EscapeDataString(resource.Name)}",
  131. _ => "#"
  132. };
  133. }
  134. }