refactor(ui): replace instances table with hierarchical TreeView (Site → Area → Instance)
This commit is contained in:
@@ -75,100 +75,69 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table class="table table-sm table-striped table-hover">
|
||||
<thead class="table-dark">
|
||||
<tr>
|
||||
<th>Instance Name</th>
|
||||
<th>Template</th>
|
||||
<th>Site</th>
|
||||
<th>Area</th>
|
||||
<th>Status</th>
|
||||
<th>Staleness</th>
|
||||
<th style="width: 240px;">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (_filteredInstances.Count == 0)
|
||||
<TreeView @ref="_instanceTree" TItem="InstanceTreeNode" Items="_treeRoots"
|
||||
ChildrenSelector="n => n.Children"
|
||||
HasChildrenSelector="n => n.Children.Count > 0"
|
||||
KeySelector="n => n.Key"
|
||||
StorageKey="instances-tree"
|
||||
Selectable="true"
|
||||
SelectedKey="_selectedKey"
|
||||
SelectedKeyChanged="key => { _selectedKey = key; }">
|
||||
<NodeContent Context="node">
|
||||
@switch (node.Kind)
|
||||
{
|
||||
<tr>
|
||||
<td colspan="7" class="text-muted text-center">No instances match the current filters.</td>
|
||||
</tr>
|
||||
case InstanceNodeKind.Site:
|
||||
<span class="fw-semibold">@node.Label</span>
|
||||
break;
|
||||
case InstanceNodeKind.Area:
|
||||
<span class="text-secondary">@node.Label</span>
|
||||
break;
|
||||
case InstanceNodeKind.Instance:
|
||||
<span>@node.Label</span>
|
||||
<span class="badge @GetStateBadge(node.Instance!.State) ms-1">@node.Instance!.State</span>
|
||||
@if (node.Instance!.State != InstanceState.NotDeployed)
|
||||
{
|
||||
<span class="badge @(node.IsStale ? "bg-warning text-dark" : "bg-light text-dark") ms-1">
|
||||
@(node.IsStale ? "Stale" : "Current")
|
||||
</span>
|
||||
}
|
||||
break;
|
||||
}
|
||||
@foreach (var inst in _pagedInstances)
|
||||
</NodeContent>
|
||||
<ContextMenu Context="node">
|
||||
@if (node.Kind == InstanceNodeKind.Instance)
|
||||
{
|
||||
<tr>
|
||||
<td><strong>@inst.UniqueName</strong></td>
|
||||
<td>@GetTemplateName(inst.TemplateId)</td>
|
||||
<td>@GetSiteName(inst.SiteId)</td>
|
||||
<td>@(inst.AreaId.HasValue ? GetAreaName(inst.AreaId.Value) : "—")</td>
|
||||
<td>
|
||||
<span class="badge @GetStateBadge(inst.State)">@inst.State</span>
|
||||
</td>
|
||||
<td>
|
||||
@{
|
||||
var isStale = _stalenessMap.GetValueOrDefault(inst.Id);
|
||||
}
|
||||
@if (inst.State == InstanceState.NotDeployed)
|
||||
{
|
||||
<span class="text-muted small">—</span>
|
||||
}
|
||||
else if (isStale)
|
||||
{
|
||||
<span class="badge bg-warning text-dark" title="Template changes pending">Stale</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span class="badge bg-light text-dark">Current</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-outline-primary btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => DeployInstance(inst)" disabled="@_actionInProgress"
|
||||
title="Flatten template and send config to site">@(isStale ? "Redeploy" : "Deploy")</button>
|
||||
@if (inst.State == InstanceState.Enabled)
|
||||
{
|
||||
<button class="btn btn-outline-warning btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => DisableInstance(inst)" disabled="@_actionInProgress">Disable</button>
|
||||
}
|
||||
else if (inst.State == InstanceState.Disabled)
|
||||
{
|
||||
<button class="btn btn-outline-success btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => EnableInstance(inst)" disabled="@_actionInProgress">Enable</button>
|
||||
}
|
||||
<button class="btn btn-outline-info btn-sm py-0 px-1 me-1"
|
||||
@onclick='() => NavigationManager.NavigateTo($"/deployment/instances/{inst.Id}/configure")'>Configure</button>
|
||||
<button class="btn btn-outline-info btn-sm py-0 px-1 me-1"
|
||||
@onclick="() => ShowDiff(inst)" disabled="@(_actionInProgress || inst.State == InstanceState.NotDeployed)">Diff</button>
|
||||
<button class="btn btn-outline-danger btn-sm py-0 px-1"
|
||||
@onclick="() => DeleteInstance(inst)" disabled="@_actionInProgress">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@* Pagination *@
|
||||
@if (_totalPages > 1)
|
||||
{
|
||||
<nav>
|
||||
<ul class="pagination pagination-sm justify-content-end">
|
||||
<li class="page-item @(_currentPage <= 1 ? "disabled" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(_currentPage - 1)">Previous</button>
|
||||
</li>
|
||||
@for (int i = 1; i <= _totalPages; i++)
|
||||
var inst = node.Instance!;
|
||||
var isStale = node.IsStale;
|
||||
<button class="dropdown-item" @onclick="() => DeployInstance(inst)"
|
||||
disabled="@_actionInProgress">@(isStale ? "Redeploy" : "Deploy")</button>
|
||||
@if (inst.State == InstanceState.Enabled)
|
||||
{
|
||||
var page = i;
|
||||
<li class="page-item @(page == _currentPage ? "active" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(page)">@(page)</button>
|
||||
</li>
|
||||
<button class="dropdown-item" @onclick="() => DisableInstance(inst)"
|
||||
disabled="@_actionInProgress">Disable</button>
|
||||
}
|
||||
<li class="page-item @(_currentPage >= _totalPages ? "disabled" : "")">
|
||||
<button class="page-link" @onclick="() => GoToPage(_currentPage + 1)">Next</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
}
|
||||
<div class="text-muted small">
|
||||
else if (inst.State == InstanceState.Disabled)
|
||||
{
|
||||
<button class="dropdown-item" @onclick="() => EnableInstance(inst)"
|
||||
disabled="@_actionInProgress">Enable</button>
|
||||
}
|
||||
<button class="dropdown-item"
|
||||
@onclick='() => NavigationManager.NavigateTo($"/deployment/instances/{inst.Id}/configure")'>
|
||||
Configure
|
||||
</button>
|
||||
<button class="dropdown-item" @onclick="() => ShowDiff(inst)"
|
||||
disabled="@(_actionInProgress || inst.State == InstanceState.NotDeployed)">Diff</button>
|
||||
<div class="dropdown-divider"></div>
|
||||
<button class="dropdown-item text-danger" @onclick="() => DeleteInstance(inst)"
|
||||
disabled="@_actionInProgress">Delete</button>
|
||||
}
|
||||
</ContextMenu>
|
||||
<EmptyContent>
|
||||
<span class="text-muted fst-italic">No instances match the current filters.</span>
|
||||
</EmptyContent>
|
||||
</TreeView>
|
||||
|
||||
<div class="text-muted small mt-2">
|
||||
@_filteredInstances.Count instance(s) total
|
||||
</div>
|
||||
|
||||
@@ -230,9 +199,13 @@
|
||||
return authState.User.FindFirst("Username")?.Value ?? "unknown";
|
||||
}
|
||||
|
||||
record InstanceTreeNode(string Key, string Label, InstanceNodeKind Kind,
|
||||
List<InstanceTreeNode> Children, Instance? Instance = null,
|
||||
bool IsStale = false);
|
||||
enum InstanceNodeKind { Site, Area, Instance }
|
||||
|
||||
private List<Instance> _allInstances = new();
|
||||
private List<Instance> _filteredInstances = new();
|
||||
private List<Instance> _pagedInstances = new();
|
||||
private List<Site> _sites = new();
|
||||
private List<Template> _templates = new();
|
||||
private List<Area> _allAreas = new();
|
||||
@@ -246,9 +219,9 @@
|
||||
private string _filterStatus = string.Empty;
|
||||
private string _filterSearch = string.Empty;
|
||||
|
||||
private int _currentPage = 1;
|
||||
private int _totalPages;
|
||||
private const int PageSize = 25;
|
||||
private List<InstanceTreeNode> _treeRoots = new();
|
||||
private TreeView<InstanceTreeNode> _instanceTree = default!;
|
||||
private object? _selectedKey;
|
||||
|
||||
private ToastNotification _toast = default!;
|
||||
private ConfirmDialog _confirmDialog = default!;
|
||||
@@ -312,26 +285,66 @@
|
||||
return true;
|
||||
}).OrderBy(i => i.UniqueName).ToList();
|
||||
|
||||
_totalPages = Math.Max(1, (int)Math.Ceiling(_filteredInstances.Count / (double)PageSize));
|
||||
if (_currentPage > _totalPages) _currentPage = 1;
|
||||
UpdatePage();
|
||||
BuildTree();
|
||||
}
|
||||
|
||||
private void GoToPage(int page)
|
||||
private void BuildTree()
|
||||
{
|
||||
if (page < 1 || page > _totalPages) return;
|
||||
_currentPage = page;
|
||||
UpdatePage();
|
||||
_treeRoots = _sites.Select(site =>
|
||||
{
|
||||
var siteAreas = _allAreas.Where(a => a.SiteId == site.Id).ToList();
|
||||
var siteInstances = _filteredInstances.Where(i => i.SiteId == site.Id).ToList();
|
||||
|
||||
var areaNodes = BuildAreaNodes(siteAreas, siteInstances, parentId: null);
|
||||
|
||||
var unassigned = siteInstances
|
||||
.Where(i => i.AreaId == null)
|
||||
.Select(MakeInstanceNode)
|
||||
.ToList();
|
||||
|
||||
var children = areaNodes.Concat(unassigned).ToList();
|
||||
|
||||
return new InstanceTreeNode(
|
||||
Key: $"site-{site.Id}",
|
||||
Label: site.Name,
|
||||
Kind: InstanceNodeKind.Site,
|
||||
Children: children);
|
||||
})
|
||||
.Where(s => s.Children.Count > 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private void UpdatePage()
|
||||
private List<InstanceTreeNode> BuildAreaNodes(List<Area> allAreas,
|
||||
List<Instance> instances, int? parentId)
|
||||
{
|
||||
_pagedInstances = _filteredInstances
|
||||
.Skip((_currentPage - 1) * PageSize)
|
||||
.Take(PageSize)
|
||||
return allAreas
|
||||
.Where(a => a.ParentAreaId == parentId)
|
||||
.Select(area =>
|
||||
{
|
||||
var childAreas = BuildAreaNodes(allAreas, instances, area.Id);
|
||||
var areaInstances = instances
|
||||
.Where(i => i.AreaId == area.Id)
|
||||
.Select(MakeInstanceNode)
|
||||
.ToList();
|
||||
var children = childAreas.Concat(areaInstances).ToList();
|
||||
return new InstanceTreeNode(
|
||||
Key: $"area-{area.Id}",
|
||||
Label: area.Name,
|
||||
Kind: InstanceNodeKind.Area,
|
||||
Children: children);
|
||||
})
|
||||
.Where(a => a.Children.Count > 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
private InstanceTreeNode MakeInstanceNode(Instance inst) => new(
|
||||
Key: $"inst-{inst.Id}",
|
||||
Label: inst.UniqueName,
|
||||
Kind: InstanceNodeKind.Instance,
|
||||
Children: new(),
|
||||
Instance: inst,
|
||||
IsStale: _stalenessMap.GetValueOrDefault(inst.Id));
|
||||
|
||||
private string GetTemplateName(int templateId) =>
|
||||
_templates.FirstOrDefault(t => t.Id == templateId)?.Name ?? $"#{templateId}";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user