feat(ui/deployment): consolidate sites/areas/instances into Topology page

Single /deployment/topology page replaces /deployment/instances (legacy URL
preserved as a secondary @page directive) and the /admin/areas* CRUD pages.
TreeView with Site → Area → Instance, V1–V7 visual guide (bi-building /
bi-diagram-3 / bi-box), always-visible empty containers, search dim, F2
inline area rename, and right-click context menus per node kind (Add Area,
Move to Area…, lifecycle actions, etc.).

Adds AreaService.MoveAreaAsync with cycle prevention, same-site enforcement,
and name-collision check at the new parent. Instance rename intentionally
out of scope — UniqueName is the site-side actor identity, requires its own
design pass.
This commit is contained in:
Joseph Doherty
2026-05-11 22:03:55 -04:00
parent b2eddd9713
commit f3386d0278
18 changed files with 1857 additions and 1122 deletions

View File

@@ -704,34 +704,52 @@ record TestNode(string Key, string Label, List<TestNode> Children);
## Page Integration Notes
### 1. Instances Page (`/deployment/instances` — Instances.razor)
### 1. Topology Page (`/deployment/topology` — Topology.razor)
**Current state:** Flat table with filters (Site, Template, Status, Search), pagination, and 6 inline action buttons per row (Deploy, Disable/Enable, Configure, Diff, Delete). ~490 lines.
The Topology page is the single home for Site → Area → Instance hierarchy management. It replaces the former `/deployment/instances` page (the legacy URL is retained as a secondary `@page` directive on `Topology.razor` so existing bookmarks resolve) and the former `/admin/areas*` admin pages.
**Change to:**
- Replace the `<table>` with a `<TreeView>` showing Site → Area → Sub Area → Instance hierarchy.
- **Keep the existing filter bar** (Site, Template, Status, Search). Filters control which tree roots and leaves are shown:
- Site filter: pass only the matching site root to `Items`.
- Template/Status/Search filters: filter at the instance (leaf) level. Branch nodes with no matching descendants should be pruned from the tree. Build a helper method (`BuildFilteredTree()`) that walks the hierarchy bottom-up, keeping only branches that contain at least one matching instance.
- **Remove the table, pagination, and Actions column.** The tree replaces all of this.
- **Move all 6 action buttons into the `ContextMenu` fragment**, shown only for instance nodes:
- Deploy/Redeploy, Disable/Enable (conditional on state), Configure, Diff, Delete (with divider).
- Site and Area nodes get no context menu (browser default).
- **Node content per type:**
- Site nodes: `<span class="fw-semibold">SiteName</span>`
- Area nodes: `<span class="text-secondary">AreaName</span>`
- Instance nodes: `<span>UniqueName</span>` + status badge + staleness badge
- **Tree model:** Build in `LoadDataAsync` — load sites, areas (recursive via `ParentAreaId`), instances. Group instances by `SiteId` + `AreaId`. Instances with `AreaId == null` attach directly under their site. Wrap in a uniform `TreeNode` record.
- **StorageKey:** `"instances-tree"`
- **Selection:** Enable selection. Clicking an instance could show a detail panel or simply highlight it for context menu use.
**Scope:**
- Structural management of areas (create, rename inline, move, delete) and instance placement (move to area).
- Instance lifecycle: Deploy/Redeploy, Enable/Disable, Configure, Diff, Delete via per-node context menu.
- Search-only filter row (single text input) — dims non-matching rows, preserves tree shape, no collapse.
**Files to modify:**
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor` — replace table with TreeView, add tree model building, move actions to context menu, keep filter bar.
**TreeView wiring:**
- `Items` = list of Site root nodes built from `_sites`, `_allAreas`, and `_allInstances`.
- `KeySelector` returns prefixed keys (`s:{id}`, `a:{id}`, `i:{id}`).
- `StorageKey` = `"topology-tree"` for expansion state.
- A separate `topology-tree-selected` sessionStorage key persists the selected node across navigation.
- `Selectable` = true; selection does not navigate (instance configure goes through the context menu).
- Empty containers always rendered (so they can be drop/move targets).
**Removed code:**
- Pagination logic (`_currentPage`, `_totalPages`, `_pagedInstances`, `GoToPage`)
- Actions column markup
- `<table>` / `<thead>` / `<tbody>` structure
**Glyphs (V1V7 visual guide):**
- Site: `bi-building`
- Area: `bi-diagram-3`
- Instance: `bi-box` + state badge + Stale/Current badge when deployed.
**Context menus:**
- **Site:** Add Area, Create Instance here.
- **Area:** Add Sub-area, Create Instance here, Move to Area…, Rename… (also F2 / double-click inline), Delete.
- **Instance:** Deploy/Redeploy, Enable/Disable (state-dependent), Configure, Diff, Move to Area…, Delete. Instance rename is intentionally absent (see "Instance rename" below).
**Inline rename:** Area rows only. F2 or double-click swaps the label for an input bound to a local buffer. Enter commits via `AreaService.UpdateAreaAsync`; Escape cancels; server validation errors stay surfaced inline.
**Search behavior:** Single text input above the tree. While text is present, any row whose label does not match (case-insensitive substring) and whose subtree contains no match is rendered at `opacity: 0.4`. The tree shape stays intact.
**Top-of-page buttons:** `+ Area` (opens `CreateAreaDialog` with site picker), `+ Instance` (navigates to `/deployment/instances/create` with no preselection), `Refresh`, `Expand`, `Collapse`.
**Files added:**
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/Topology.razor`
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/MoveAreaDialog.razor`
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/MoveInstanceDialog.razor`
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/CreateAreaDialog.razor`
**Files removed:**
- `src/ScadaLink.CentralUI/Components/Pages/Deployment/Instances.razor`
- `src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor` (and AreaAdd / AreaEdit / AreaDelete)
**Backend addition:** `AreaService.MoveAreaAsync(int areaId, int? newParentAreaId, string user)` adds area re-parenting (cycle prevention, same-site, name collision at new parent). Pairs with the existing `InstanceService.AssignToAreaAsync`.
**Instance rename:** Out of scope for this page. `InstanceService` does not currently support renaming an instance (`UniqueName` is also the site-side `InstanceActor` identity and appears in deployment records). A separate design pass is required if rename is wanted.
---
@@ -761,35 +779,7 @@ record TestNode(string Key, string Label, List<TestNode> Children);
---
### 3. Areas Page (`/admin/areas` — Areas.razor)
**Current state:** Two-panel layout. Left panel: site list (`list-group`). Right panel: manually indented flat tree of areas for the selected site, with `[+]`/`-` indicators, inline Edit/Delete buttons, and an add/edit form. Custom `BuildFlatTree()` / `AddChildren()` methods, `AreaTreeNode` record, manual `padding-left` indentation. ~293 lines.
**Change to:**
- **Keep the two-panel layout** (site list on left, area tree on right).
- Replace the custom flat-tree rendering in the right panel with a `<TreeView>` component.
- **Site selection stays as-is** (left panel `list-group` click sets `_selectedSiteId`). This acts as the external filter — the TreeView receives only the selected site's areas as `Items`.
- **Move Edit and Delete into the `ContextMenu` fragment** for area nodes:
- Edit → loads area into the add/edit form (same as current behavior)
- Delete → shows confirm dialog (with child check, same as current)
- **Node content:** `<span>AreaName</span>` — optionally show instance count if available.
- **Tree model:** For the selected site, load root areas (`ParentAreaId == null`), use `ChildrenSelector` to return child areas. The `Area` entity already has `Children` collection, so it can be used directly as `TItem` without a wrapper record — `ChildrenSelector = a => a.Children.ToList()`, `HasChildrenSelector = a => a.Children.Any()`, `KeySelector = a => a.Id`.
- **Keep the add/edit form** at the top of the right panel (above the tree). The "Parent Area" dropdown stays.
- **StorageKey:** `"areas-tree"`
**Files to modify:**
- `src/ScadaLink.CentralUI/Components/Pages/Admin/Areas.razor` — replace custom flat-tree rendering with TreeView, remove `BuildFlatTree()`, `AddChildren()`, `AreaTreeNode` record, manual indentation CSS.
**Removed code:**
- `BuildFlatTree()` method
- `AddChildren()` recursive helper
- `AreaTreeNode` record
- Manual `padding-left` indentation
- Custom `[+]`/`-` toggle rendering
- Inline Edit/Delete buttons in the tree rows
## Interactions
- **DataTable**: The tree replaces flat tables on the three pages listed above. Other pages that don't need hierarchy continue using DataTable.
- **InstanceConfigure.razor**: Right-click → Configure on an instance node navigates to `/deployment/instances/{Id}/configure`.
- **Areas.razor**: The simplest integration — `Area` entity used directly as `TItem`, no wrapper needed.
- **DataTable**: The tree replaces flat tables on the Topology and Data Connections pages. Other pages that don't need hierarchy continue using DataTable.
- **InstanceConfigure.razor**: Right-click → Configure on an instance node navigates to `/deployment/instances/{Id}/configure`. Back-nav returns to `/deployment/topology`.