Files
lmxopcua/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Login.razor
Joseph Doherty e4d0d82f7f feat(adminui): collapsible nav sidebar with cookie state + LoginLayout
Port the ScadaLink CentralUI sidebar pattern into the OtOpcUa AdminUI:

- Drop the top app-bar. Brand moves into the side rail's header — same
  visual rhythm as ScadaLink's NavMenu.
- New NavSection.razor: collapsible eyebrow toggle (rail-eyebrow-toggle CSS)
  with a chevron + label. Mirrors ScadaLink/Components/Layout/NavSection.
- New NavSidebar.razor: interactive island carrying the three section
  groups (Navigation / Scripting / Live) + session block. Marked
  @rendermode InteractiveServer; MainLayout itself stays static-rendered
  because layouts can't take a RenderFragment Body across an interactive
  boundary.
- New wwwroot/js/nav-state.js: window.navState.get/.set persists the
  expanded-section list to the otopcua_nav cookie (one-year lifetime,
  SameSite=Lax). Same shape as ScadaLink's scadabridge_nav.
- New LoginLayout.razor + @layout LoginLayout on Login.razor: the login
  page now renders without the side rail — clean centred card.
- MainLayout.razor: slimmed down to the d-flex shell + hamburger toggle +
  <NavSidebar/> + @Body.
- Login.razor: also drops the trailing "LDAP bind against the configured
  directory..." footer that the user asked to remove.
- site.css: adds .side-rail .brand styles (mirrored from ScadaLink) and
  the .rail-eyebrow-toggle / .rail-eyebrow-chevron / .rail-section-body
  styles for the new collapsible UI.

Auto-expand on page load: NavSidebar seeds the expanded set from the
current URL's first path segment (in OnInitialized so it works even on
the very first server render) and from the cookie (in OnAfterRenderAsync
once JS interop is available). LocationChanged hooks keep the expanded
state in sync as the user navigates between sections.
2026-05-26 13:48:35 -04:00

51 lines
2.2 KiB
Plaintext

@page "/login"
@layout LoginLayout
@* Login MUST stay anonymously reachable — otherwise the fallback authorization policy
would lock operators out of the only way in (Admin-001). Static-rendered on purpose:
the form POSTs to /auth/login while ASP.NET still owns an unstarted HTTP response.
Calling SignInAsync from an interactive circuit would be too late.
Uses LoginLayout (no side rail) so the page renders as a clean centred card. *@
@attribute [Microsoft.AspNetCore.Authorization.AllowAnonymous]
<div class="login-wrap rise" style="animation-delay:.02s">
<section class="panel">
<div class="panel-head">OtOpcUa Admin &mdash; sign in</div>
<div style="padding:1.1rem 1.1rem 1.25rem">
<form method="post" action="/auth/login" data-enhance="false">
@if (ReturnUrl is not null)
{
<input type="hidden" name="returnUrl" value="@ReturnUrl"/>
}
<div class="mb-3">
<label class="form-label" for="username">Username</label>
<input id="username" name="username" type="text"
class="form-control form-control-sm" autocomplete="username"/>
</div>
<div class="mb-3">
<label class="form-label" for="password">Password</label>
<input id="password" name="password" type="password"
class="form-control form-control-sm" autocomplete="current-password"/>
</div>
@if (!string.IsNullOrWhiteSpace(Error))
{
<div class="panel notice" style="margin-bottom:.85rem">@Error</div>
}
<button class="btn btn-primary w-100" type="submit">Sign in</button>
</form>
</div>
</section>
</div>
@code {
/// <summary>Error message surfaced by /auth/login after a failed bind.</summary>
[SupplyParameterFromQuery]
private string? Error { get; set; }
/// <summary>Original protected URL the operator was bounced from; round-tripped to the endpoint.</summary>
[SupplyParameterFromQuery]
private string? ReturnUrl { get; set; }
}