feat: restyle Admin UI with the technical-light design system

Adopt the technical-light design system across the Admin web UI:

- Vendor theme.css + IBM Plex woff2 fonts into wwwroot; include
  theme.css globally after Bootstrap.
- Rebuild MainLayout: top app-bar (brand mark, breadcrumb, connection
  pill) + hairline-ruled side rail with accent-bordered active link.
- Convert all 33 pages to the component catalog — tables to
  panel + data-table (num/mono columns), KPI cards to agg-grid,
  detail blocks to metric-card/kv rows, badges to chips, alerts to
  panel notice, headings to page-title/panel-head, .rise reveals.
- Buttons/forms stay on Bootstrap; theme.css restyles them via
  --bs-* overrides. View-specific layout lives in app.css; all
  colour/type comes from theme.css tokens.

Also fix a pre-existing /fleet 500: the node-state query ordered on
a property of a constructed FleetNodeRow record, which EF Core
cannot translate. Order the join's columns before projecting.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-18 02:20:09 -04:00
parent 31b9468102
commit 482d5f5637
40 changed files with 1837 additions and 1206 deletions

View File

@@ -10,7 +10,7 @@
@implements IAsyncDisposable
<div class="d-flex justify-content-between mb-3">
<h4>Access-control grants</h4>
<h4 class="panel-head">Access-control grants</h4>
<button class="btn btn-sm btn-primary" @onclick="() => _showForm = true">Add grant</button>
</div>
@@ -18,27 +18,32 @@
else if (_acls.Count == 0) { <p class="text-muted">No ACL grants in this draft. Publish will result in a cluster with no external access.</p> }
else
{
<table class="table table-sm">
<thead><tr><th>LDAP group</th><th>Scope</th><th>Scope ID</th><th>Permissions</th><th></th></tr></thead>
<tbody>
@foreach (var a in _acls)
{
<tr>
<td>@a.LdapGroup</td>
<td>@a.ScopeKind</td>
<td><code>@(a.ScopeId ?? "-")</code></td>
<td><code>@a.PermissionFlags</code></td>
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => RevokeAsync(a.NodeAclRowId)">Revoke</button></td>
</tr>
}
</tbody>
</table>
<section class="panel rise" style="animation-delay:.02s">
<div class="panel-head">Grants</div>
<div class="table-wrap">
<table class="data-table">
<thead><tr><th>LDAP group</th><th>Scope</th><th>Scope ID</th><th>Permissions</th><th></th></tr></thead>
<tbody>
@foreach (var a in _acls)
{
<tr>
<td>@a.LdapGroup</td>
<td>@a.ScopeKind</td>
<td><span class="mono">@(a.ScopeId ?? "-")</span></td>
<td><span class="mono">@a.PermissionFlags</span></td>
<td><button class="btn btn-sm btn-outline-danger" @onclick="() => RevokeAsync(a.NodeAclRowId)">Revoke</button></td>
</tr>
}
</tbody>
</table>
</div>
</section>
}
@* Probe-this-permission — task #196 slice 1 *@
<div class="card mt-4 mb-3">
<div class="card-header">
<strong>Probe this permission</strong>
<section class="panel rise" style="animation-delay:.08s">
<div class="panel-head">
Probe this permission
<span class="small text-muted ms-2">
Ask the trie "if LDAP group X asks for permission Y on node Z, would it be granted?" —
answers the same way the live server does at request time.
@@ -88,64 +93,67 @@ else
<span class="ms-3">
@if (_probeResult.Granted)
{
<span class="badge bg-success">Granted</span>
<span class="chip chip-ok">Granted</span>
}
else
{
<span class="badge bg-danger">Denied</span>
<span class="chip chip-bad">Denied</span>
}
<span class="small ms-2">
Required <code>@_probeResult.Required</code>,
Effective <code>@_probeResult.Effective</code>
Required <span class="mono">@_probeResult.Required</span>,
Effective <span class="mono">@_probeResult.Effective</span>
</span>
</span>
}
</div>
@if (_probeResult is not null && _probeResult.Matches.Count > 0)
{
<table class="table table-sm mt-3 mb-0">
<thead><tr><th>LDAP group matched</th><th>Level</th><th>Flags contributed</th></tr></thead>
<tbody>
@foreach (var m in _probeResult.Matches)
{
<tr>
<td><code>@m.LdapGroup</code></td>
<td>@m.Scope</td>
<td><code>@m.PermissionFlags</code></td>
</tr>
}
</tbody>
</table>
<div class="table-wrap mt-3">
<table class="data-table">
<thead><tr><th>LDAP group matched</th><th>Level</th><th>Flags contributed</th></tr></thead>
<tbody>
@foreach (var m in _probeResult.Matches)
{
<tr>
<td><span class="mono">@m.LdapGroup</span></td>
<td>@m.Scope</td>
<td><span class="mono">@m.PermissionFlags</span></td>
</tr>
}
</tbody>
</table>
</div>
}
else if (_probeResult is not null)
{
<div class="mt-2 small text-muted">No matching grants for this (group, scope) — effective permission is <code>None</code>.</div>
<div class="mt-2 small text-muted">No matching grants for this (group, scope) — effective permission is <span class="mono">None</span>.</div>
}
</div>
</div>
</section>
@if (_showForm)
{
<div class="card">
<section class="panel rise" style="animation-delay:.14s">
<div class="panel-head">Add grant</div>
<div class="card-body">
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">LDAP group</label>
<input class="form-control" @bind="_group"/>
<input class="form-control form-control-sm" @bind="_group"/>
</div>
<div class="col-md-4">
<label class="form-label">Scope kind</label>
<select class="form-select" @bind="_scopeKind">
<select class="form-select form-select-sm" @bind="_scopeKind">
@foreach (var k in Enum.GetValues<NodeAclScopeKind>()) { <option value="@k">@k</option> }
</select>
</div>
<div class="col-md-4">
<label class="form-label">Scope ID (empty for Cluster-wide)</label>
<input class="form-control" @bind="_scopeId"/>
<input class="form-control form-control-sm" @bind="_scopeId"/>
</div>
<div class="col-12">
<label class="form-label">Permissions (bundled presets — per-flag editor in v2.1)</label>
<select class="form-select" @bind="_preset">
<select class="form-select form-select-sm" @bind="_preset">
<option value="Read">Read (Browse + Read)</option>
<option value="WriteOperate">Read + Write Operate</option>
<option value="Engineer">Read + Write Tune + Write Configure</option>
@@ -154,13 +162,13 @@ else
</select>
</div>
</div>
@if (_error is not null) { <div class="alert alert-danger mt-3">@_error</div> }
@if (_error is not null) { <section class="panel notice mt-3">@_error</section> }
<div class="mt-3">
<button class="btn btn-sm btn-primary" @onclick="SaveAsync">Save</button>
<button class="btn btn-sm btn-secondary ms-2" @onclick="() => _showForm = false">Cancel</button>
</div>
</div>
</div>
</section>
}
@code {