Przeglądaj źródła

Abstracted list pages

Tim Jones 1 miesiąc temu
rodzic
commit
e5d6eb6ff2

+ 1 - 1
RackPeek.Domain/UseCases/AddResourceUseCase.cs

@@ -19,7 +19,7 @@ public class AddResourceUseCase<T>(IResourceCollection repo) : IAddResourceUseCa
 
         var existingResource = await repo.GetByNameAsync(name);
         if (existingResource != null)
-            throw new ConflictException($"{existingResource.Kind} resource '{name}' already exists.");
+            throw new ConflictException($"Resource '{name}' ({existingResource.Kind}) already exists.");
 
         if (runsOn != null)
         {

+ 0 - 51
Shared.Rcl/AccessPoints/AccessPointsListComponent.razor

@@ -1,51 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.AccessPoints
-@inject IGetAllResourcesByKindUseCase<AccessPoint> GetAllUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Access Points</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-    <AddResourceComponent TResource="AccessPoint"
-                        Placeholder="AP name"
-                        OnCreated="NavigateToNewResource" />
-
-
-    @if (_AccessPoints is null)
-    {
-        <div class="text-zinc-500">loading AccessPoints…</div>
-    }
-    else if (_AccessPoints.Count == 0)
-    {
-        <div class="text-zinc-500">no AccessPoints found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var accessPoint in _AccessPoints.OrderBy(s => s.Name))
-            {
-                <AccessPointCardComponent AccessPoint="accessPoint" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<AccessPoint>? _AccessPoints;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _AccessPoints = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _AccessPoints = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 27 - 5
Shared.Rcl/AccessPoints/AccessPointsListPage.razor

@@ -1,9 +1,31 @@
 @page "/accesspoints/list"
+@using RackPeek.Domain.Resources.Hardware.AccessPoints
+@inject NavigationManager Nav
 
-<PageTitle>AccessPoints</PageTitle>
+<ResourcesListComponent TResource="AccessPoint"
+                  Title="AccessPoints"
+                  TestId="accesspoints"
+                  OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    AccessPoints
-</h1>
+    <ItemTemplate Context="accessPoint">
+        <AccessPointCardComponent AccessPoint="accessPoint"
+                                  OnDeleted="Reload" />
+    </ItemTemplate>
 
-<AccessPointsListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<AccessPoint> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+}

+ 106 - 0
Shared.Rcl/Components/ResourcesListComponent.razor

@@ -0,0 +1,106 @@
+@using RackPeek.Domain.Persistence
+@typeparam TResource where TResource : RackPeek.Domain.Resources.Resource
+
+@inject IResourceCollection Repo
+@inject NavigationManager Nav
+
+<PageTitle>@Title</PageTitle>
+
+<h1 class="text-lg text-zinc-100" data-testid="@($"{TestId}-page-title")">
+    @Title
+</h1>
+
+<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6"
+     data-testid="@($"{TestId}-page-root")">
+
+    <!-- Add Resource Section -->
+    <div data-testid="@($"{TestId}-add-section")">
+        <AddResourceComponent TResource="TResource"
+                              Placeholder=@($"{@Title} name")
+                              OnCreated="OnCreated" />
+    </div>
+
+    @if (_resources is null)
+    {
+        <div class="text-zinc-500"
+             data-testid="@($"{TestId}-loading")">
+            loading @Title.ToLower()…
+        </div>
+    }
+    else if (!_resources.Any())
+    {
+        <div class="text-zinc-500"
+             data-testid="@($"{TestId}-empty")">
+            no @Title.ToLower() found
+        </div>
+    }
+    else
+    {
+        <div class="space-y-4"
+             data-testid="@($"{TestId}-list")">
+
+            @foreach (var group in GroupResources(_resources))
+            {
+                var groupKey = FormatGroupKey(group.Key);
+
+                <div data-testid="@($"{TestId}-group-{groupKey}")">
+
+                    @if (ShouldGroup)
+                    {
+                        <div class="text-xs text-zinc-500 uppercase tracking-wider mb-2"
+                             data-testid="@($"{TestId}-group-title-{groupKey}")">
+                            @DisplayGroupKey(group.Key)
+                        </div>
+                    }
+
+                    <div class="space-y-4"
+                         data-testid="@($"{TestId}-group-list-{groupKey}")">
+
+                        @foreach (var item in group)
+                        {
+                            @ItemTemplate(item)
+                        }
+
+                    </div>
+                </div>
+            }
+
+        </div>
+    }
+</div>
+
+@code {
+    [Parameter] public string Title { get; set; } = default!;
+    [Parameter] public string TestId { get; set; } = default!;
+
+    [Parameter] public RenderFragment AddSection { get; set; } = default!;
+    [Parameter] public RenderFragment<TResource> ItemTemplate { get; set; } = default!;
+
+    [Parameter] public bool ShouldGroup { get; set; } = false;
+
+    [Parameter] public Func<TResource, string?> GroupBy { get; set; } = (s => (string?)null);
+    [Parameter] public EventCallback<string> OnCreated { get; set; }
+
+    private IReadOnlyList<TResource>? _resources;
+
+    protected override async Task OnInitializedAsync()
+    {
+        _resources = await Repo.GetAllOfTypeAsync<TResource>();
+    }
+
+    private IEnumerable<IGrouping<string?, TResource>> GroupResources(
+        IEnumerable<TResource> resources)
+        => resources
+            .GroupBy(GroupBy)
+            .OrderByDescending(g => g.Count());
+
+    private static string FormatGroupKey(string? key)
+        => string.IsNullOrWhiteSpace(key)
+            ? "unassigned"
+            : key.Replace(" ", "-");
+
+    private static string DisplayGroupKey(string? key)
+        => string.IsNullOrWhiteSpace(key)
+            ? "Unassigned"
+            : key;
+}

+ 0 - 52
Shared.Rcl/Desktops/DesktopsListComponent.razor

@@ -1,52 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.Desktops
-@inject IGetAllResourcesByKindUseCase<Desktop> GetAllUseCase
-
-@inject NavigationManager Nav
-
-<PageTitle>Desktops</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-
-    <AddResourceComponent TResource="Desktop"
-                          Placeholder="Desktop name"
-                          OnCreated="NavigateToNewResource" />
-    
-    @if (_desktops is null)
-    {
-        <div class="text-zinc-500">loading desktops…</div>
-    }
-    else if (_desktops.Count == 0)
-    {
-        <div class="text-zinc-500">no desktops found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var desktop in _desktops.OrderBy(s => s.Name))
-            {
-                <DesktopCardComponent Desktop="desktop" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Desktop>? _desktops;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _desktops = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _desktops = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 27 - 5
Shared.Rcl/Desktops/DesktopsListPage.razor

@@ -1,9 +1,31 @@
 @page "/desktops/list"
+@using RackPeek.Domain.Resources.Hardware.Desktops
+@inject NavigationManager Nav
 
-<PageTitle>Desktops</PageTitle>
+<ResourcesListComponent TResource="Desktop"
+                  Title="Desktops"
+                  TestId="desktops"
+                  OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Desktops
-</h1>
+    <ItemTemplate Context="desktop">
+        <DesktopCardComponent Desktop="desktop"
+                              OnDeleted="Reload" />
+    </ItemTemplate>
 
-<DesktopsListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Desktop> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+}

+ 0 - 50
Shared.Rcl/Firewalls/FirewallListComponent.razor

@@ -1,50 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.Firewalls
-@inject IGetAllResourcesByKindUseCase<Firewall> GetAllUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Firewall</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-    <AddResourceComponent TResource="Firewall"
-                          Placeholder="Firewall name"
-                          OnCreated="NavigateToNewResource" />
-
-    @if (_Firewall is null)
-    {
-        <div class="text-zinc-500">loading Firewall…</div>
-    }
-    else if (_Firewall.Count == 0)
-    {
-        <div class="text-zinc-500">no Firewall found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var _switch in _Firewall.OrderBy(s => s.Name))
-            {
-                <FirewallCardComponent Firewall="_switch" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Firewall>? _Firewall;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _Firewall = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _Firewall = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 27 - 5
Shared.Rcl/Firewalls/FirewallListPage.razor

@@ -1,9 +1,31 @@
 @page "/firewalls/list"
+@using RackPeek.Domain.Resources.Hardware.Firewalls
+@inject NavigationManager Nav
 
-<PageTitle>Firewalls</PageTitle>
+<ResourcesListComponent TResource="Firewall"
+                        Title="Firewalls"
+                        TestId="firewalls"
+                        OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Firewalls
-</h1>
+    <ItemTemplate Context="firewall">
+        <FirewallCardComponent Firewall="firewall"
+                               OnDeleted="Reload" />
+    </ItemTemplate>
 
-<FirewallListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Firewall> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+}

+ 0 - 49
Shared.Rcl/Laptops/LaptopsListComponent.razor

@@ -1,49 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.Laptops
-@inject IGetAllResourcesByKindUseCase<Laptop> GetAllUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Laptops</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-    <AddResourceComponent TResource="Laptop"
-                          Placeholder="Laptop name"
-                          OnCreated="NavigateToNewResource" />
-    @if (_Laptops is null)
-    {
-        <div class="text-zinc-500">loading Laptops…</div>
-    }
-    else if (_Laptops.Count == 0)
-    {
-        <div class="text-zinc-500">no Laptops found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var Laptop in _Laptops.OrderBy(s => s.Name))
-            {
-                <LaptopCardComponent Laptop="Laptop" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Laptop>? _Laptops;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _Laptops = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _Laptops = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 28 - 5
Shared.Rcl/Laptops/LaptopsListPage.razor

@@ -1,9 +1,32 @@
 @page "/laptops/list"
+@using RackPeek.Domain.Resources.Hardware.Laptops
+@inject NavigationManager Nav
 
-<PageTitle>Laptops</PageTitle>
+<ResourcesListComponent TResource="Laptop"
+                        Title="Laptops"
+                        TestId="laptops"
+                        OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Laptops
-</h1>
+    <ItemTemplate Context="laptop">
+        <LaptopCardComponent Laptop="laptop"
+                             OnDeleted="Reload" />
+    </ItemTemplate>
 
-<LaptopsListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Laptop> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+
+}

+ 0 - 51
Shared.Rcl/Routers/RouterListComponent.razor

@@ -1,51 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.Routers
-@using Router = RackPeek.Domain.Resources.Hardware.Routers.Router
-@inject IGetAllResourcesByKindUseCase<Router> GetAllUseCase
-@inject IGetResourceByNameUseCase<Router> GetByNameUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Routers</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-    <AddRouterComponent OnCreated="NavigateToNewResource"/>
-
-
-    @if (_Routeres is null)
-    {
-        <div class="text-zinc-500">loading Routers…</div>
-    }
-    else if (_Routeres.Count == 0)
-    {
-        <div class="text-zinc-500">no Routers found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var _Router in _Routeres.OrderBy(s => s.Name))
-            {
-                <RouterCardComponent Router="_Router" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Router>? _Routeres;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _Routeres = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _Routeres = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 28 - 5
Shared.Rcl/Routers/RouterListPage.razor

@@ -1,9 +1,32 @@
 @page "/routers/list"
+@using Router = RackPeek.Domain.Resources.Hardware.Routers.Router
+@inject NavigationManager Nav
 
-<PageTitle>Routers</PageTitle>
+<ResourcesListComponent TResource="Router"
+                        Title="Routers"
+                        TestId="routers"
+                        OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Routers
-</h1>
+    <ItemTemplate Context="router">
+        <RouterCardComponent Router="router"
+                             OnDeleted="Reload" />
+    </ItemTemplate>
 
-<RouterListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Router> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+
+}

+ 14 - 55
Shared.Rcl/Servers/ServersListPage.razor

@@ -1,72 +1,31 @@
 @page "/servers/list"
 @using RackPeek.Domain.Resources.Hardware.Servers
-@inject IGetAllResourcesByKindUseCase<Server> GetAllUseCase
-@inject IGetResourceByNameUseCase<Server> GetByNameUseCase
 @inject NavigationManager Nav
 
-<PageTitle>Servers</PageTitle>
+<ResourcesListComponent TResource="Server"
+                  Title="Servers"
+                  TestId="servers"
+                  OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100"
-    data-testid="servers-page-title">
-    Servers
-</h1>
+    <ItemTemplate Context="server">
+        <ServerCardComponent Server="server"
+                             OnDeleted="Reload" />
+    </ItemTemplate>
 
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6"
-     data-testid="servers-page-root">
-
-    <!-- Add Server -->
-    <div data-testid="add-server-section">
-        <AddResourceComponent TResource="Server"
-                              Placeholder="name"
-                              OnCreated="NavigateToNewResource"
-                              />
-    </div>
-
-    @if (_servers is null)
-    {
-        <div class="text-zinc-500"
-             data-testid="servers-loading">
-            loading servers…
-        </div>
-    }
-    else if (_servers.Count == 0)
-    {
-        <div class="text-zinc-500"
-             data-testid="servers-empty">
-            no servers found
-        </div>
-    }
-    else
-    {
-        <div class="space-y-4"
-             data-testid="servers-list">
-
-            @foreach (var server in _servers.OrderBy(s => s.Name))
-            {
-                    <ServerCardComponent Server="server"
-                                         OnDeleted="Callback" />
-            }
-
-        </div>
-    }
-</div>
+</ResourcesListComponent>
 
 @code {
-    private IReadOnlyList<Server>? _servers;
 
-    protected override async Task OnInitializedAsync()
-    {
-        _servers = await GetAllUseCase.ExecuteAsync();
-    }
+    [Inject] IGetAllResourcesByKindUseCase<Server> GetAllUseCase { get; set; } = default!;
 
-    private Task NavigateToNewResource(string serverName)
+    private Task NavigateToNewResource(string name)
     {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
+        Nav.NavigateTo($"resources/hardware/{name}");
         return Task.CompletedTask;
     }
 
-    private async Task Callback(string obj)
+    private async Task Reload(string _)
     {
-        _servers = await GetAllUseCase.ExecuteAsync();
+        await GetAllUseCase.ExecuteAsync();
     }
 }

+ 13 - 3
Shared.Rcl/Services/ServiceCardComponent.razor

@@ -264,7 +264,7 @@
 @code {
     [Parameter] [EditorRequired] public Service Service { get; set; } = default!;
 
-    [Parameter] public EventCallback<ServiceEditModel> OnSave { get; set; }
+    [Parameter] public EventCallback<string> OnSave { get; set; }
 
     private bool _isEditing;
     private ServiceEditModel _edit = new();
@@ -278,9 +278,19 @@
     async Task Save()
     {
         _isEditing = false;
-        await OnSave.InvokeAsync(_edit);
+        await UpdateServiceUseCase.ExecuteAsync(
+            _edit.Name,
+            _edit.Ip,
+            _edit.Port,
+            _edit.Protocol,
+            _edit.Url,
+            _edit.RunsOn,
+            _edit.Notes
+        );
+        
+        await OnSave.InvokeAsync(Service.Name);
     }
-
+    
     void Cancel()
     {
         _isEditing = false;

+ 1 - 15
Shared.Rcl/Services/ServiceDetailsPage.razor

@@ -1,7 +1,6 @@
 @page "/resources/services/{ServiceName}"
 @using RackPeek.Domain.Persistence
 @inject IResourceCollection Repo
-@inject UpdateServiceUseCase UpdateServiceUseCase
 @inject NavigationManager NavigationManager
 
 <PageTitle>Service Details</PageTitle>
@@ -25,7 +24,7 @@
             @_service.Name (@_service.Kind)
         </h1>
 
-        <ServiceCardComponent Service="_service" OnSave="UpdateService" OnDeleted="OnDeleted"/>
+        <ServiceCardComponent Service="_service" OnDeleted="OnDeleted"/>
     }
 </div>
 
@@ -41,19 +40,6 @@
         _loading = false;
     }
 
-    async Task UpdateService(ServiceEditModel edit)
-    {
-        await UpdateServiceUseCase.ExecuteAsync(
-            edit.Name,
-            edit.Ip,
-            edit.Port,
-            edit.Protocol,
-            edit.Url,
-            edit.RunsOn,
-                edit.Notes
-        );
-    }
-
     private void OnDeleted(string obj)
     {
         NavigationManager.NavigateTo("/services/list");

+ 19 - 94
Shared.Rcl/Services/ServicesListPage.razor

@@ -1,110 +1,35 @@
 @page "/services/list"
 @using RackPeek.Domain.Persistence
-@inject IResourceCollection Repo
-@inject UpdateServiceUseCase UpdateServiceUseCase
 @inject NavigationManager Nav
 
-<PageTitle>Services</PageTitle>
-
-<h1 class="text-lg text-zinc-100"
-    data-testid="services-page-title">
-    Services
-</h1>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6"
-     data-testid="services-page-root">
-
-    <!-- Add Service Section -->
-    <div data-testid="add-service-section">
-        <AddResourceComponent TResource="Service"
-                              Placeholder="Service name"
-                              OnCreated="NavigateToNewResource" />
-    </div>
-
-    @if (_services is null)
-    {
-        <div class="text-zinc-500"
-             data-testid="services-loading">
-            loading services…
-        </div>
-    }
-    else if (_services.Count == 0)
-    {
-        <div class="text-zinc-500"
-             data-testid="services-empty">
-            no services found
+<ResourcesListComponent TResource="Service"
+                  Title="Services"
+                  TestId="services"
+                  GroupBy="@(s => s.RunsOn)"
+                  ShouldGroup="true"
+                  OnCreated="NavigateToNewResource">
+
+    <ItemTemplate Context="svc">
+        <div data-testid=@($"services-list-item-{svc.Name.Replace(" ", "-")}")>
+            <ServiceCardComponent Service="svc"
+                                  OnDeleted="Reload" />
         </div>
-    }
-    else
-    {
-        <div class="space-y-4"
-             data-testid="services-list">
-
-            @foreach (var group in _services
-                          .OrderBy(s => s.Name)
-                          .GroupBy(s => s.RunsOn)
-                          .OrderByDescending(g => g.Count()))
-            {
-                var groupKey = string.IsNullOrWhiteSpace(group.Key)
-                    ? "unassigned"
-                    : group.Key.Replace(" ", "-");
-
-                <div data-testid=@($"services-group-{groupKey}")>
+    </ItemTemplate>
 
-                    <div class="text-xs text-zinc-500 uppercase tracking-wider mb-2"
-                         data-testid=@($"services-group-title-{groupKey}")>
-                        @(string.IsNullOrWhiteSpace(group.Key) ? "Unassigned" : group.Key)
-                    </div>
-
-                    <div class="space-y-4"
-                         data-testid=@($"services-group-list-{groupKey}")>
-
-                        @foreach (var svc in group)
-                        {
-                            <div data-testid=@($"services-list-item-{svc.Name.Replace(" ", "-")}")>
-                                <ServiceCardComponent Service="svc"
-                                                      OnSave="UpdateService"
-                                                      OnDeleted="Callback"/>
-                            </div>
-                        }
-
-                    </div>
-                </div>
-            }
-
-        </div>
-    }
-</div>
+</ResourcesListComponent>
 
 @code {
-    private IReadOnlyList<Service>? _services;
 
-    protected override async Task OnInitializedAsync()
-    {
-        _services = await Repo.GetAllOfTypeAsync<Service>();
-    }
-
-    async Task UpdateService(ServiceEditModel edit)
-    {
-        await UpdateServiceUseCase.ExecuteAsync(
-            edit.Name,
-            edit.Ip,
-            edit.Port,
-            edit.Protocol,
-            edit.Url,
-            edit.RunsOn,
-            edit.Notes
-        );
-    }
+    [Inject] IResourceCollection Repo { get; set; } = default!;
 
-    private Task NavigateToNewResource(string serverName)
+    private Task NavigateToNewResource(string name)
     {
-        Nav.NavigateTo($"resources/services/{serverName}");
+        Nav.NavigateTo($"resources/services/{name}");
         return Task.CompletedTask;
     }
 
-    private async Task Callback(string obj)
+    private async Task Reload(string _)
     {
-        _services = await Repo.GetAllOfTypeAsync<Service>();
+        await Repo.GetAllOfTypeAsync<Service>();
     }
-}
+}

+ 0 - 8
Shared.Rcl/Shared.Rcl.csproj

@@ -24,7 +24,6 @@
       <AdditionalFiles Include="AccessPoints\AccessPointCardComponent.razor" />
       <AdditionalFiles Include="AccessPoints\AccessPointsListComponent.razor" />
       <AdditionalFiles Include="AccessPoints\AccessPointsListPage.razor" />
-      <AdditionalFiles Include="AccessPoints\AddAccessPointComponent.razor" />
       <AdditionalFiles Include="Components\ResourceBreadCrumbComponent.razor" />
       <AdditionalFiles Include="Desktops\AddDesktopComponent.razor" />
       <AdditionalFiles Include="Desktops\DesktopCardComponent.razor" />
@@ -49,9 +48,6 @@
       <AdditionalFiles Include="Modals\RamModal.razor" />
       <AdditionalFiles Include="Modals\StringValueModal.razor" />
       <AdditionalFiles Include="Modals\SystemSelectionModal.razor" />
-      <AdditionalFiles Include="Pages\Error.razor" />
-      <AdditionalFiles Include="Pages\Home.razor" />
-      <AdditionalFiles Include="Pages\NotFound.razor" />
       <AdditionalFiles Include="Routers\AddRouterComponent.razor" />
       <AdditionalFiles Include="Routers\RouterCardComponent.razor" />
       <AdditionalFiles Include="Routers\RouterListComponent.razor" />
@@ -81,10 +77,6 @@
     <ItemGroup>
       <ProjectReference Include="..\RackPeek.Domain\RackPeek.Domain.csproj" />
     </ItemGroup>
-
-    <ItemGroup>
-      <Folder Include="Pages\" />
-    </ItemGroup>
     
 
 

+ 0 - 52
Shared.Rcl/Switches/SwitchListComponent.razor

@@ -1,52 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.Switches
-@inject IGetAllResourcesByKindUseCase<Switch> GetAllUseCase
-@inject IGetResourceByNameUseCase<Switch> GetByNameUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Switches</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-
-    <AddResourceComponent TResource="Switch"
-                          Placeholder="Switch name"
-                          OnCreated="NavigateToNewResource" />
-
-    @if (_Switches is null)
-    {
-        <div class="text-zinc-500">loading Switches…</div>
-    }
-    else if (_Switches.Count == 0)
-    {
-        <div class="text-zinc-500">no Switches found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var _switch in _Switches.OrderBy(s => s.Name))
-            {
-                <SwitchCardComponent Switch="_switch" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Switch>? _Switches;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _Switches = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _Switches = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 28 - 5
Shared.Rcl/Switches/SwitchListPage.razor

@@ -1,9 +1,32 @@
 @page "/switches/list"
+@using RackPeek.Domain.Resources.Hardware.Switches
+@inject NavigationManager Nav
 
-<PageTitle>Switches</PageTitle>
+<ResourcesListComponent TResource="Switch"
+                        Title="Switches"
+                        TestId="switches"
+                        OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Switches
-</h1>
+    <ItemTemplate Context="switchItem">
+        <SwitchCardComponent Switch="switchItem"
+                             OnDeleted="Reload" />
+    </ItemTemplate>
 
-<SwitchListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Switch> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+
+}

+ 1 - 1
Shared.Rcl/Systems/SystemsDetailsPage.razor

@@ -92,7 +92,7 @@
 
     private Task NavigateToNewResource(string serverName)
     {
-        Nav.NavigateTo($"resources/systems/{serverName}");
+        Nav.NavigateTo($"resources/services/{serverName}");
         return Task.CompletedTask;
     }
     

+ 26 - 99
Shared.Rcl/Systems/SystemsListPage.razor

@@ -3,126 +3,58 @@
 @using RackPeek.Domain.Persistence
 @using RackPeek.Domain.Resources.SystemResources
 @using RackPeek.Domain.Resources.SystemResources.UseCases
-@inject IResourceCollection Repo
-@inject ISystemRepository SystemRepo
-
-@inject UpdateSystemUseCase UpdateSystemUseCase
 @inject NavigationManager Nav
 
-<PageTitle>Systems</PageTitle>
-
-<h1 class="text-lg text-zinc-100"
-    data-testid="systems-page-title">
-    Systems
-</h1>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6"
-     data-testid="systems-page-root">
-
-    <!-- Add System -->
-    <div data-testid="add-system-section">
-        <AddResourceComponent TResource="SystemResource"
-                              Placeholder="System name"
-                              OnCreated="NavigateToNewResource" />
-    </div>
-
-    @if (_systems is null)
-    {
-        <div class="text-zinc-500"
-             data-testid="systems-loading">
-            loading systems…
-        </div>
-    }
-    else if (_systems.Count == 0)
-    {
-        <div class="text-zinc-500"
-             data-testid="systems-empty">
-            no systems found
+<ResourcesListComponent TResource="SystemResource"
+                        Title="Systems"
+                        TestId="systems"
+                        ShouldGroup="true"
+                        GroupBy="@(s => s.RunsOn)"
+                        OnCreated="NavigateToNewResource">
+
+    <ItemTemplate Context="systemResource">
+        <div data-testid=@($"systems-list-item-{systemResource.Name.Replace(" ", "-")}")>
+            <SystemCardComponent System="systemResource"
+                                 OnSave="UpdateSystem"
+                                 OnDeleted="Reload" />
         </div>
-    }
-    else
-    {
-        <div class="space-y-4"
-             data-testid="systems-list">
-
-            @foreach (var group in _systems
-                          .OrderBy(s => s.Name)
-                          .GroupBy(s => s.RunsOn)
-                          .OrderByDescending(g => g.Count()))
-            {
-                var groupKey = string.IsNullOrWhiteSpace(group.Key)
-                    ? "unassigned"
-                    : group.Key.Replace(" ", "-");
-
-                <div data-testid=@($"systems-group-{groupKey}")>
-
-                    <div class="text-xs text-zinc-500 uppercase tracking-wider mb-2"
-                         data-testid=@($"systems-group-title-{groupKey}")>
-                        @(string.IsNullOrWhiteSpace(group.Key) ? "Unassigned" : group.Key)
-                    </div>
-
-                    <div class="space-y-4"
-                         data-testid=@($"systems-group-list-{groupKey}")>
-
-                        @foreach (var systemResource in group)
-                        {
-                            <div data-testid=@($"systems-list-item-{systemResource.Name.Replace(" ", "-")}")>
-                                <SystemCardComponent System="systemResource"
-                                                     OnSave="UpdateSystem"
-                                                     OnDeleted="Callback"/>
-                            </div>
-                        }
-
-                    </div>
-                </div>
-            }
-        </div>
-    }
-</div>
-
+    </ItemTemplate>
 
+</ResourcesListComponent>
 
 @code {
 
+    [Inject] IResourceCollection Repo { get; set; } = default!;
+    [Inject] ISystemRepository SystemRepo { get; set; } = default!;
+    [Inject] UpdateSystemUseCase UpdateSystemUseCase { get; set; } = default!;
+
     [Parameter]
     [SupplyParameterFromQuery(Name = "type")]
-    public string? Type { get; set; } = null;
+    public string? Type { get; set; }
 
     [Parameter]
     [SupplyParameterFromQuery(Name = "os")]
-    public string? Os { get; set; }= null;
-}
-
-@code {
-    private IReadOnlyList<SystemResource>? _systems;
+    public string? Os { get; set; }
 
-    protected override async Task OnParametersSetAsync()
-    {
-        await Reload();
-    }
-
-    private async Task Reload()
+    private async Task Reload(string _ = "")
     {
         var type = Normalize(Type);
         var os = Normalize(Os);
 
         if (string.IsNullOrEmpty(type) && string.IsNullOrEmpty(os))
         {
-            _systems = await Repo.GetAllOfTypeAsync<SystemResource>();
+            await Repo.GetAllOfTypeAsync<SystemResource>();
         }
         else
         {
-            _systems = await SystemRepo.GetFilteredAsync(
-                type,
-                os
-            );
+            await SystemRepo.GetFilteredAsync(type, os);
         }
     }
 
     private static string? Normalize(string? s)
         => string.IsNullOrWhiteSpace(s) ? null : s.Trim();
 
-    async Task UpdateSystem(SystemEditModel edit)
+    private async Task UpdateSystem(SystemEditModel edit)
     {
         await UpdateSystemUseCase.ExecuteAsync(
             edit.Name,
@@ -137,14 +69,9 @@
         await Reload();
     }
 
-    private Task NavigateToNewResource(string serverName)
+    private Task NavigateToNewResource(string name)
     {
-        Nav.NavigateTo($"resources/systems/{serverName}");
+        Nav.NavigateTo($"resources/systems/{name}");
         return Task.CompletedTask;
     }
-
-    private async Task Callback(string obj)
-    {
-        await Reload();
-    }
 }

+ 0 - 50
Shared.Rcl/Ups/UpsListComponent.razor

@@ -1,50 +0,0 @@
-@using RackPeek.Domain.Resources.Hardware.UpsUnits
-@inject IGetAllResourcesByKindUseCase<Ups> GetAllUseCase
-@inject NavigationManager Nav
-
-<PageTitle>Ups</PageTitle>
-
-<div class="min-h-screen bg-zinc-950 text-zinc-200 font-mono p-6 space-y-6">
-
-    <AddResourceComponent TResource="Ups"
-                          Placeholder="Ups name"
-                          OnCreated="NavigateToNewResource" />
-    @if (_upss is null)
-    {
-        <div class="text-zinc-500">loading Ups…</div>
-    }
-    else if (_upss.Count == 0)
-    {
-        <div class="text-zinc-500">no Ups found</div>
-    }
-    else
-    {
-        <div class="space-y-4">
-            @foreach (var ups in _upss.OrderBy(s => s.Name))
-            {
-                <UpsCardComponent Ups="ups" OnDeleted="Callback"/>
-            }
-        </div>
-    }
-</div>
-
-@code {
-    private IReadOnlyList<Ups>? _upss;
-
-    protected override async Task OnInitializedAsync()
-    {
-        _upss = await GetAllUseCase.ExecuteAsync();
-    }
-
-    private Task NavigateToNewResource(string serverName)
-    {
-        Nav.NavigateTo($"resources/hardware/{serverName}");
-        return Task.CompletedTask;
-    }
-
-    private async Task Callback(string obj)
-    {
-        _upss = await GetAllUseCase.ExecuteAsync();
-    }
-
-}

+ 28 - 5
Shared.Rcl/Ups/UpsListPage.razor

@@ -1,9 +1,32 @@
 @page "/ups/list"
+@using RackPeek.Domain.Resources.Hardware.UpsUnits
+@inject NavigationManager Nav
 
-<PageTitle>Ups</PageTitle>
+<ResourcesListComponent TResource="Ups"
+                        Title="Ups"
+                        TestId="ups"
+                        OnCreated="NavigateToNewResource">
 
-<h1 class="text-lg text-zinc-100">
-    Ups
-</h1>
+    <ItemTemplate Context="ups">
+        <UpsCardComponent Ups="ups"
+                          OnDeleted="Reload" />
+    </ItemTemplate>
 
-<UpsListComponent/>
+</ResourcesListComponent>
+
+@code {
+
+    [Inject] IGetAllResourcesByKindUseCase<Ups> GetAllUseCase { get; set; } = default!;
+
+    private Task NavigateToNewResource(string name)
+    {
+        Nav.NavigateTo($"resources/hardware/{name}");
+        return Task.CompletedTask;
+    }
+
+    private async Task Reload(string _)
+    {
+        await GetAllUseCase.ExecuteAsync();
+    }
+
+}

+ 3 - 3
Tests.E2e/Infra/PlaywrightFixture.cs

@@ -52,9 +52,9 @@ public class PlaywrightFixture : IAsyncLifetime
 
         Browser = await _playwright.Chromium.LaunchAsync(new()
         {
-            Headless = true
-            //Headless = false,
-            //SlowMo = 1000
+            //Headless = true
+            Headless = false, 
+            SlowMo = 200
         });
     }