Files
scadalink-design/src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor
Joseph Doherty 2b5dabb336 refactor(ui): redesign Areas page with TreeView and dedicated Add/Edit/Delete pages
Areas page now shows a single TreeView with sites as roots and areas as
children. Context menus: sites get "Add Area", areas get "Add Child Area",
"Edit Area", "Delete Area" — each navigating to a dedicated page.

The Delete Area page shows a TreeView of the area and all recursive children
with assigned instances. Deletion is blocked if any instances are assigned
to the area or its descendants.
2026-03-24 16:19:39 -04:00

134 lines
4.6 KiB
Plaintext

@page "/admin/areas"
@using ScadaLink.Security
@using ScadaLink.Commons.Entities.Instances
@using ScadaLink.Commons.Entities.Sites
@using ScadaLink.Commons.Interfaces.Repositories
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject ISiteRepository SiteRepository
@inject ITemplateEngineRepository TemplateEngineRepository
@inject NavigationManager NavigationManager
<div class="container-fluid mt-3">
<h4 class="mb-3">Area Management</h4>
<ToastNotification @ref="_toast" />
@if (_loading)
{
<LoadingSpinner IsLoading="true" />
}
else if (_errorMessage != null)
{
<div class="alert alert-danger">@_errorMessage</div>
}
else
{
<TreeView TItem="AreaTreeNode" Items="_treeRoots"
ChildrenSelector="n => n.Children"
HasChildrenSelector="n => n.Children.Count > 0"
KeySelector="n => n.Key"
StorageKey="areas-tree">
<NodeContent Context="node">
@if (node.Kind == AreaNodeKind.Site)
{
<span class="fw-semibold">@node.Label</span>
<span class="badge bg-secondary ms-1">@node.AreaCount</span>
}
else
{
<span>@node.Label</span>
}
</NodeContent>
<ContextMenu Context="node">
@if (node.Kind == AreaNodeKind.Site)
{
<button class="dropdown-item"
@onclick='() => NavigationManager.NavigateTo($"/admin/areas/add?siteId={node.SiteId}")'>
Add Area
</button>
}
else
{
<button class="dropdown-item"
@onclick='() => NavigationManager.NavigateTo($"/admin/areas/add?siteId={node.SiteId}&parentAreaId={node.Area!.Id}")'>
Add Child Area
</button>
<button class="dropdown-item"
@onclick='() => NavigationManager.NavigateTo($"/admin/areas/{node.Area!.Id}/edit")'>
Edit Area
</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger"
@onclick='() => NavigationManager.NavigateTo($"/admin/areas/{node.Area!.Id}/delete")'>
Delete Area
</button>
}
</ContextMenu>
<EmptyContent>
<span class="text-muted fst-italic">No sites configured.</span>
</EmptyContent>
</TreeView>
}
</div>
@code {
record AreaTreeNode(string Key, string Label, AreaNodeKind Kind, List<AreaTreeNode> Children,
int SiteId, Area? Area = null, int AreaCount = 0);
enum AreaNodeKind { Site, Area }
private List<Site> _sites = new();
private List<AreaTreeNode> _treeRoots = new();
private bool _loading = true;
private string? _errorMessage;
private ToastNotification _toast = default!;
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
}
private async Task LoadDataAsync()
{
_loading = true;
try
{
_sites = (await SiteRepository.GetAllSitesAsync()).ToList();
_treeRoots = new();
foreach (var site in _sites)
{
var areas = (await TemplateEngineRepository.GetAreasBySiteIdAsync(site.Id)).ToList();
var rootAreas = areas.Where(a => a.ParentAreaId == null).OrderBy(a => a.Name);
var children = rootAreas.Select(a => BuildAreaNode(a, site.Id)).ToList();
_treeRoots.Add(new AreaTreeNode(
Key: $"site-{site.Id}",
Label: site.Name,
Kind: AreaNodeKind.Site,
Children: children,
SiteId: site.Id,
AreaCount: areas.Count));
}
}
catch (Exception ex)
{
_errorMessage = $"Failed to load data: {ex.Message}";
}
_loading = false;
}
private AreaTreeNode BuildAreaNode(Area area, int siteId)
{
var children = area.Children
.OrderBy(c => c.Name)
.Select(c => BuildAreaNode(c, siteId))
.ToList();
return new AreaTreeNode(
Key: $"area-{area.Id}",
Label: area.Name,
Kind: AreaNodeKind.Area,
Children: children,
SiteId: siteId,
Area: area);
}
}