feat(dashboard): split MainLayout into ZB.MOM.WW.Theme ThemeShell + kit nav
This commit is contained in:
@@ -1,210 +1,40 @@
|
|||||||
@using System.Linq
|
|
||||||
@using Microsoft.AspNetCore.Components.Routing
|
|
||||||
@using Microsoft.JSInterop
|
|
||||||
@implements IDisposable
|
|
||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@inject NavigationManager Navigation
|
|
||||||
@inject IJSRuntime JS
|
|
||||||
|
|
||||||
<div class="d-flex flex-column flex-lg-row" style="min-height: 100vh;">
|
@* Thin layout: delegates the side-rail chassis (hamburger, brand, responsive
|
||||||
@* Hamburger toggle: visible only on viewports <lg. Bootstrap collapse JS
|
collapse) to the shared ZB.MOM.WW.Theme <ThemeShell>. The nav is reproduced
|
||||||
lives in bootstrap.bundle.min.js (loaded in App.razor). *@
|
with the kit's NavRailSection / NavRailItem; section expand-state persistence
|
||||||
<button class="btn btn-outline-secondary btn-sm d-lg-none m-2 align-self-start"
|
is owned by the kit's <details> + ThemeScripts (no JS interop here). *@
|
||||||
type="button"
|
<ThemeShell Product="MXAccess Gateway" Accent="#2f5fd0">
|
||||||
data-bs-toggle="collapse"
|
<Nav>
|
||||||
data-bs-target="#sidebar-collapse"
|
<NavRailItem Href="/" Text="Dashboard" Match="NavLinkMatch.All" />
|
||||||
aria-controls="sidebar-collapse"
|
<NavRailSection Title="Runtime" Key="runtime">
|
||||||
aria-expanded="false"
|
<NavRailItem Href="/sessions" Text="Sessions" />
|
||||||
aria-label="Toggle navigation">
|
<NavRailItem Href="/workers" Text="Workers" />
|
||||||
☰
|
<NavRailItem Href="/events" Text="Events" />
|
||||||
</button>
|
<NavRailItem Href="/alarms" Text="Alarms" />
|
||||||
|
</NavRailSection>
|
||||||
<div class="collapse d-lg-block" id="sidebar-collapse">
|
<NavRailSection Title="Galaxy" Key="galaxy">
|
||||||
<nav class="sidebar d-flex flex-column">
|
<NavRailItem Href="/galaxy" Text="Repository" />
|
||||||
<a class="brand" href="/"><span class="mark">▮</span> MXAccess Gateway</a>
|
<NavRailItem Href="/browse" Text="Browse" />
|
||||||
|
</NavRailSection>
|
||||||
<div style="overflow-y:auto; flex:1 1 auto; min-height:0;">
|
<NavRailSection Title="Admin" Key="admin">
|
||||||
<ul class="nav flex-column">
|
<NavRailItem Href="/apikeys" Text="API Keys" />
|
||||||
<li class="nav-item">
|
<NavRailItem Href="/settings" Text="Settings" />
|
||||||
<NavLink class="nav-link" href="/" Match="NavLinkMatch.All">Dashboard</NavLink>
|
</NavRailSection>
|
||||||
</li>
|
</Nav>
|
||||||
|
<RailFooter>
|
||||||
<NavSection Title="Runtime"
|
<AuthorizeView>
|
||||||
Expanded="@_expanded.Contains("runtime")"
|
<Authorized Context="authState">
|
||||||
OnToggle="@(() => ToggleAsync("runtime"))">
|
<span class="rail-user">@authState.User.Identity?.Name</span>
|
||||||
<li class="nav-item">
|
<form method="post" action="/logout" data-enhance="false">
|
||||||
<NavLink class="nav-link" href="/sessions" Match="NavLinkMatch.Prefix">Sessions</NavLink>
|
<AntiforgeryToken />
|
||||||
</li>
|
<button class="rail-btn" type="submit">Sign Out</button>
|
||||||
<li class="nav-item">
|
</form>
|
||||||
<NavLink class="nav-link" href="/workers" Match="NavLinkMatch.Prefix">Workers</NavLink>
|
</Authorized>
|
||||||
</li>
|
<NotAuthorized>
|
||||||
<li class="nav-item">
|
<a class="rail-btn" href="/login">Sign In</a>
|
||||||
<NavLink class="nav-link" href="/events" Match="NavLinkMatch.Prefix">Events</NavLink>
|
</NotAuthorized>
|
||||||
</li>
|
</AuthorizeView>
|
||||||
<li class="nav-item">
|
</RailFooter>
|
||||||
<NavLink class="nav-link" href="/alarms" Match="NavLinkMatch.Prefix">Alarms</NavLink>
|
<ChildContent>@Body</ChildContent>
|
||||||
</li>
|
</ThemeShell>
|
||||||
</NavSection>
|
|
||||||
|
|
||||||
<NavSection Title="Galaxy"
|
|
||||||
Expanded="@_expanded.Contains("galaxy")"
|
|
||||||
OnToggle="@(() => ToggleAsync("galaxy"))">
|
|
||||||
<li class="nav-item">
|
|
||||||
<NavLink class="nav-link" href="/galaxy" Match="NavLinkMatch.Prefix">Repository</NavLink>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<NavLink class="nav-link" href="/browse" Match="NavLinkMatch.Prefix">Browse</NavLink>
|
|
||||||
</li>
|
|
||||||
</NavSection>
|
|
||||||
|
|
||||||
<NavSection Title="Admin"
|
|
||||||
Expanded="@_expanded.Contains("admin")"
|
|
||||||
OnToggle="@(() => ToggleAsync("admin"))">
|
|
||||||
<li class="nav-item">
|
|
||||||
<NavLink class="nav-link" href="/apikeys" Match="NavLinkMatch.Prefix">API Keys</NavLink>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<NavLink class="nav-link" href="/settings" Match="NavLinkMatch.Prefix">Settings</NavLink>
|
|
||||||
</li>
|
|
||||||
</NavSection>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<AuthorizeView>
|
|
||||||
<Authorized Context="authState">
|
|
||||||
<div class="border-top px-3 py-2">
|
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
|
||||||
<span class="text-body-secondary small">@authState.User.Identity?.Name</span>
|
|
||||||
<form method="post" action="/logout" data-enhance="false">
|
|
||||||
<AntiforgeryToken />
|
|
||||||
<button type="submit" class="btn btn-outline-secondary btn-sm py-0 px-2">Sign Out</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Authorized>
|
|
||||||
<NotAuthorized>
|
|
||||||
<div class="border-top px-3 py-2">
|
|
||||||
<a href="/login" class="btn btn-outline-secondary btn-sm py-0 px-2 w-100">Sign In</a>
|
|
||||||
</div>
|
|
||||||
</NotAuthorized>
|
|
||||||
</AuthorizeView>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<main class="page flex-grow-1">
|
|
||||||
@Body
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@code {
|
|
||||||
// Sections whose collapsed/expanded state we persist. Acts as the allow-list
|
|
||||||
// when parsing the cookie so stale or attacker-supplied ids are ignored.
|
|
||||||
private static readonly string[] SectionIds = { "runtime", "galaxy", "admin" };
|
|
||||||
|
|
||||||
// The currently-expanded sections. Populated from the cookie on first
|
|
||||||
// render; mutated by ToggleAsync and by navigating into a section.
|
|
||||||
private readonly HashSet<string> _expanded = new(StringComparer.Ordinal);
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
|
||||||
{
|
|
||||||
Navigation.LocationChanged += OnLocationChanged;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!firstRender)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hydrate from the cookie. Until this completes the sidebar paints
|
|
||||||
// collapsed, matching the CentralUI behaviour.
|
|
||||||
string saved;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
saved = await JS.InvokeAsync<string>("navState.get") ?? string.Empty;
|
|
||||||
}
|
|
||||||
catch (JSDisconnectedException)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var id in saved.Split(
|
|
||||||
',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
|
||||||
{
|
|
||||||
if (Array.IndexOf(SectionIds, id) >= 0)
|
|
||||||
{
|
|
||||||
_expanded.Add(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The section of the page we loaded on is always expanded.
|
|
||||||
if (EnsureCurrentSectionExpanded())
|
|
||||||
{
|
|
||||||
await PersistAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
|
|
||||||
{
|
|
||||||
if (EnsureCurrentSectionExpanded())
|
|
||||||
{
|
|
||||||
_ = PersistAsync();
|
|
||||||
_ = InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ToggleAsync(string id)
|
|
||||||
{
|
|
||||||
if (!_expanded.Remove(id))
|
|
||||||
{
|
|
||||||
_expanded.Add(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await PersistAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adds the current page's section to _expanded; returns true if it changed.
|
|
||||||
private bool EnsureCurrentSectionExpanded()
|
|
||||||
{
|
|
||||||
var section = CurrentSection();
|
|
||||||
return section is not null && _expanded.Add(section);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Maps the current URL's first path segment to a section id, or null for
|
|
||||||
// sectionless pages (Dashboard, Login).
|
|
||||||
private string? CurrentSection()
|
|
||||||
{
|
|
||||||
var relative = Navigation.ToBaseRelativePath(Navigation.Uri);
|
|
||||||
var firstSegment = relative.Split('?', '#')[0]
|
|
||||||
.Split('/', StringSplitOptions.RemoveEmptyEntries)
|
|
||||||
.FirstOrDefault();
|
|
||||||
|
|
||||||
return firstSegment switch
|
|
||||||
{
|
|
||||||
"sessions" or "workers" or "events" or "alarms" => "runtime",
|
|
||||||
"galaxy" or "browse" => "galaxy",
|
|
||||||
"apikeys" or "settings" => "admin",
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task PersistAsync()
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await JS.InvokeVoidAsync("navState.set", string.Join(',', _expanded));
|
|
||||||
}
|
|
||||||
catch (JSDisconnectedException)
|
|
||||||
{
|
|
||||||
// The circuit is gone — nothing to persist to.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
Navigation.LocationChanged -= OnLocationChanged;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
@* A collapsible sidebar nav section. The header is a full-width button that
|
|
||||||
toggles ChildContent visibility. Pattern lifted from ScadaLink CentralUI
|
|
||||||
(Components/Layout/NavSection.razor) — see [[project-deployed-service]]. *@
|
|
||||||
|
|
||||||
<li class="nav-item">
|
|
||||||
<button type="button"
|
|
||||||
class="nav-section-toggle"
|
|
||||||
@onclick="OnToggle"
|
|
||||||
aria-expanded="@(Expanded ? "true" : "false")">
|
|
||||||
<span class="chevron" aria-hidden="true">@(Expanded ? "▾" : "▸")</span>
|
|
||||||
<span>@Title</span>
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
@if (Expanded)
|
|
||||||
{
|
|
||||||
@ChildContent
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
|
||||||
/// <summary>Section label shown in the header (e.g. "Runtime").</summary>
|
|
||||||
[Parameter, EditorRequired]
|
|
||||||
public string Title { get; set; } = string.Empty;
|
|
||||||
|
|
||||||
/// <summary>Whether the section is expanded — its items rendered.</summary>
|
|
||||||
[Parameter]
|
|
||||||
public bool Expanded { get; set; }
|
|
||||||
|
|
||||||
/// <summary>Raised when the header button is clicked.</summary>
|
|
||||||
[Parameter]
|
|
||||||
public EventCallback OnToggle { get; set; }
|
|
||||||
|
|
||||||
/// <summary>The section's nav items, rendered only while expanded.</summary>
|
|
||||||
[Parameter]
|
|
||||||
public RenderFragment? ChildContent { get; set; }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user