style(ui): align admin styling with ScadaLink master conventions
- Move CSS into wwwroot/css/ (theme.css, site.css); sidebar 218 -> 220px - Add hamburger + Bootstrap collapse for <lg viewports - Add Components/Shared/ with LoadingSpinner, ToastNotification, StatusBadge - Replace .page-title with flex + <h4 class="mb-0"> across 20 pages - Convert NewCluster + IdentificationFields forms to card + h6 subsection pattern
This commit is contained in:
@@ -9,8 +9,8 @@
|
|||||||
@* Admin-010: Bootstrap 5 is vendored under wwwroot/lib/bootstrap/ per admin-ui.md
|
@* Admin-010: Bootstrap 5 is vendored under wwwroot/lib/bootstrap/ per admin-ui.md
|
||||||
"Tech Stack" — no public-CDN dependency so air-gapped fleet deployments work. *@
|
"Tech Stack" — no public-CDN dependency so air-gapped fleet deployments work. *@
|
||||||
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css"/>
|
<link rel="stylesheet" href="lib/bootstrap/css/bootstrap.min.css"/>
|
||||||
<link rel="stylesheet" href="theme.css"/>
|
<link rel="stylesheet" href="css/theme.css"/>
|
||||||
<link rel="stylesheet" href="app.css"/>
|
<link rel="stylesheet" href="css/site.css"/>
|
||||||
<HeadOutlet/>
|
<HeadOutlet/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -20,42 +20,56 @@
|
|||||||
</AuthorizeView>
|
</AuthorizeView>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="app-shell">
|
<div class="app-shell d-flex flex-column flex-lg-row">
|
||||||
<nav class="side-rail">
|
@* Hamburger toggle: visible only on viewports <lg.
|
||||||
<div class="rail-eyebrow">Navigation</div>
|
Bootstrap collapse JS lives in bootstrap.bundle.min.js (loaded in App.razor). *@
|
||||||
<NavLink class="rail-link" href="/" Match="NavLinkMatch.All">Overview</NavLink>
|
<button class="btn btn-outline-secondary btn-sm d-lg-none m-2 align-self-start"
|
||||||
<NavLink class="rail-link" href="/fleet" Match="NavLinkMatch.Prefix">Fleet status</NavLink>
|
type="button"
|
||||||
<NavLink class="rail-link" href="/hosts" Match="NavLinkMatch.Prefix">Host status</NavLink>
|
data-bs-toggle="collapse"
|
||||||
<NavLink class="rail-link" href="/clusters" Match="NavLinkMatch.Prefix">Clusters</NavLink>
|
data-bs-target="#sidebar-collapse"
|
||||||
<NavLink class="rail-link" href="/reservations" Match="NavLinkMatch.Prefix">Reservations</NavLink>
|
aria-controls="sidebar-collapse"
|
||||||
<NavLink class="rail-link" href="/certificates" Match="NavLinkMatch.Prefix">Certificates</NavLink>
|
aria-expanded="false"
|
||||||
<NavLink class="rail-link" href="/role-grants" Match="NavLinkMatch.Prefix">Role grants</NavLink>
|
aria-label="Toggle navigation">
|
||||||
<div class="rail-eyebrow">Scripting</div>
|
☰
|
||||||
<NavLink class="rail-link" href="/virtual-tags" Match="NavLinkMatch.Prefix">Virtual tags</NavLink>
|
</button>
|
||||||
<NavLink class="rail-link" href="/scripted-alarms" Match="NavLinkMatch.Prefix">Scripted alarms</NavLink>
|
|
||||||
<NavLink class="rail-link" href="/script-log" Match="NavLinkMatch.Prefix">Script log</NavLink>
|
|
||||||
|
|
||||||
<div class="rail-foot">
|
<div class="collapse d-lg-block" id="sidebar-collapse">
|
||||||
<AuthorizeView>
|
<nav class="side-rail">
|
||||||
<Authorized>
|
<div class="rail-eyebrow">Navigation</div>
|
||||||
<div class="rail-eyebrow">Session</div>
|
<NavLink class="rail-link" href="/" Match="NavLinkMatch.All">Overview</NavLink>
|
||||||
<a class="rail-user" href="/account">@context.User.Identity?.Name</a>
|
<NavLink class="rail-link" href="/fleet" Match="NavLinkMatch.Prefix">Fleet status</NavLink>
|
||||||
<div class="rail-roles">
|
<NavLink class="rail-link" href="/hosts" Match="NavLinkMatch.Prefix">Host status</NavLink>
|
||||||
@string.Join(", ", context.User.Claims
|
<NavLink class="rail-link" href="/clusters" Match="NavLinkMatch.Prefix">Clusters</NavLink>
|
||||||
.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value))
|
<NavLink class="rail-link" href="/reservations" Match="NavLinkMatch.Prefix">Reservations</NavLink>
|
||||||
</div>
|
<NavLink class="rail-link" href="/certificates" Match="NavLinkMatch.Prefix">Certificates</NavLink>
|
||||||
<form method="post" action="/auth/logout">
|
<NavLink class="rail-link" href="/role-grants" Match="NavLinkMatch.Prefix">Role grants</NavLink>
|
||||||
<AntiforgeryToken />
|
<div class="rail-eyebrow">Scripting</div>
|
||||||
<button class="rail-btn" type="submit">Sign out</button>
|
<NavLink class="rail-link" href="/virtual-tags" Match="NavLinkMatch.Prefix">Virtual tags</NavLink>
|
||||||
</form>
|
<NavLink class="rail-link" href="/scripted-alarms" Match="NavLinkMatch.Prefix">Scripted alarms</NavLink>
|
||||||
</Authorized>
|
<NavLink class="rail-link" href="/script-log" Match="NavLinkMatch.Prefix">Script log</NavLink>
|
||||||
<NotAuthorized>
|
|
||||||
<div class="rail-eyebrow">Session</div>
|
<div class="rail-foot">
|
||||||
<a class="rail-btn" href="/login">Sign in</a>
|
<AuthorizeView>
|
||||||
</NotAuthorized>
|
<Authorized>
|
||||||
</AuthorizeView>
|
<div class="rail-eyebrow">Session</div>
|
||||||
</div>
|
<a class="rail-user" href="/account">@context.User.Identity?.Name</a>
|
||||||
</nav>
|
<div class="rail-roles">
|
||||||
|
@string.Join(", ", context.User.Claims
|
||||||
|
.Where(c => c.Type.EndsWith("/role")).Select(c => c.Value))
|
||||||
|
</div>
|
||||||
|
<form method="post" action="/auth/logout">
|
||||||
|
<AntiforgeryToken />
|
||||||
|
<button class="rail-btn" type="submit">Sign out</button>
|
||||||
|
</form>
|
||||||
|
</Authorized>
|
||||||
|
<NotAuthorized>
|
||||||
|
<div class="rail-eyebrow">Session</div>
|
||||||
|
<a class="rail-btn" href="/login">Sign in</a>
|
||||||
|
</NotAuthorized>
|
||||||
|
</AuthorizeView>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
<main class="page">
|
<main class="page">
|
||||||
@Body
|
@Body
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Security
|
@using ZB.MOM.WW.OtOpcUa.Admin.Security
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||||
|
|
||||||
<h1 class="page-title">My account</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">My account</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<AuthorizeView>
|
<AuthorizeView>
|
||||||
<Authorized>
|
<Authorized>
|
||||||
|
|||||||
@@ -6,7 +6,9 @@
|
|||||||
@rendermode RenderMode.InteractiveServer
|
@rendermode RenderMode.InteractiveServer
|
||||||
@inject HistorianDiagnosticsService Diag
|
@inject HistorianDiagnosticsService Diag
|
||||||
|
|
||||||
<h1 class="page-title">Alarm historian</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Alarm historian</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">Local store-and-forward queue that ships alarm events to Aveva Historian via Galaxy.Host.</p>
|
<p class="text-muted">Local store-and-forward queue that ships alarm events to Aveva Historian via Galaxy.Host.</p>
|
||||||
|
|
||||||
<section class="agg-grid rise" style="animation-delay:.02s">
|
<section class="agg-grid rise" style="animation-delay:.02s">
|
||||||
|
|||||||
@@ -7,7 +7,9 @@
|
|||||||
@inject AuthenticationStateProvider AuthState
|
@inject AuthenticationStateProvider AuthState
|
||||||
@inject ILogger<Certificates> Log
|
@inject ILogger<Certificates> Log
|
||||||
|
|
||||||
<h1 class="page-title">Certificate trust</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Certificate trust</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<section class="panel notice rise" style="animation-delay:.02s">
|
<section class="panel notice rise" style="animation-delay:.02s">
|
||||||
PKI store root <span class="mono">@Certs.PkiStoreRoot</span>. Trusting a rejected cert moves the file into the trusted store — the OPC UA server picks up the change on the next client handshake.
|
PKI store root <span class="mono">@Certs.PkiStoreRoot</span>. Trusting a rejected cert moves the file into the trusted store — the OPC UA server picks up the change on the next client handshake.
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ else
|
|||||||
}
|
}
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title mb-0">@_cluster.Name</h1>
|
<h4 class="mb-0">@_cluster.Name</h4>
|
||||||
<span class="mono text-muted">@_cluster.ClusterId</span>
|
<span class="mono text-muted">@_cluster.ClusterId</span>
|
||||||
@if (!_cluster.Enabled) { <span class="chip chip-idle ms-2">Disabled</span> }
|
@if (!_cluster.Enabled) { <span class="chip chip-idle ms-2">Disabled</span> }
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
|
||||||
@inject ClusterService ClusterSvc
|
@inject ClusterService ClusterSvc
|
||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<h1 class="page-title">Clusters</h1>
|
<h4 class="mb-0">Clusters</h4>
|
||||||
<a href="/clusters/new" class="btn btn-primary">New cluster</a>
|
<a href="/clusters/new" class="btn btn-primary">New cluster</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title mb-0">Draft diff</h1>
|
<h4 class="mb-0">Draft diff</h4>
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
Cluster <span class="mono">@ClusterId</span> — from last published (@(_fromLabel)) → to draft @GenerationId
|
Cluster <span class="mono">@ClusterId</span> — from last published (@(_fromLabel)) → to draft @GenerationId
|
||||||
</small>
|
</small>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title mb-0">Draft editor</h1>
|
<h4 class="mb-0">Draft editor</h4>
|
||||||
<small class="text-muted">Cluster <span class="mono">@ClusterId</span> · generation @GenerationId</small>
|
<small class="text-muted">Cluster <span class="mono">@ClusterId</span> · generation @GenerationId</small>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -4,43 +4,45 @@
|
|||||||
nine decision #139 fields in a consistent 3-column Bootstrap grid. Used by EquipmentTab's
|
nine decision #139 fields in a consistent 3-column Bootstrap grid. Used by EquipmentTab's
|
||||||
create + edit forms so the same UI renders regardless of which flow opened it. *@
|
create + edit forms so the same UI renders regardless of which flow opened it. *@
|
||||||
|
|
||||||
<div class="panel-head mt-4">OPC 40010 Identification</div>
|
<div class="card mb-3">
|
||||||
<div class="row g-3">
|
<div class="card-body">
|
||||||
<div class="col-md-4">
|
<h6 class="text-muted border-bottom pb-1">OPC 40010 Identification</h6>
|
||||||
<label class="form-label">Manufacturer</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.Manufacturer" class="form-control"/>
|
<label class="form-label small">Manufacturer</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.Manufacturer" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Model</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.Model" class="form-control"/>
|
<label class="form-label small">Model</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.Model" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Serial number</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.SerialNumber" class="form-control"/>
|
<label class="form-label small">Serial number</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.SerialNumber" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Hardware rev</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.HardwareRevision" class="form-control"/>
|
<label class="form-label small">Hardware rev</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.HardwareRevision" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Software rev</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.SoftwareRevision" class="form-control"/>
|
<label class="form-label small">Software rev</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.SoftwareRevision" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Year of construction</label>
|
<div class="mb-2">
|
||||||
<InputNumber @bind-Value="Equipment!.YearOfConstruction" class="form-control"/>
|
<label class="form-label small">Year of construction</label>
|
||||||
</div>
|
<InputNumber @bind-Value="Equipment!.YearOfConstruction" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Asset location</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.AssetLocation" class="form-control"/>
|
<label class="form-label small">Asset location</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.AssetLocation" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Manufacturer URI</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.ManufacturerUri" class="form-control" placeholder="https://…"/>
|
<label class="form-label small">Manufacturer URI</label>
|
||||||
</div>
|
<InputText @bind-Value="Equipment!.ManufacturerUri" class="form-control form-control-sm" placeholder="https://…"/>
|
||||||
<div class="col-md-4">
|
</div>
|
||||||
<label class="form-label">Device manual URI</label>
|
<div class="mb-2">
|
||||||
<InputText @bind-Value="Equipment!.DeviceManualUri" class="form-control" placeholder="https://…"/>
|
<label class="form-label small">Device manual URI</label>
|
||||||
|
<InputText @bind-Value="Equipment!.DeviceManualUri" class="form-control form-control-sm" placeholder="https://…"/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div>
|
<div>
|
||||||
<h1 class="page-title mb-0">Equipment CSV import</h1>
|
<h4 class="mb-0">Equipment CSV import</h4>
|
||||||
<small class="text-muted">Cluster <span class="mono">@ClusterId</span> · draft generation @GenerationId</small>
|
<small class="text-muted">Cluster <span class="mono">@ClusterId</span> · draft generation @GenerationId</small>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn btn-outline-secondary" href="/clusters/@ClusterId/draft/@GenerationId">Back to draft</a>
|
<a class="btn btn-outline-secondary" href="/clusters/@ClusterId/draft/@GenerationId">Back to draft</a>
|
||||||
|
|||||||
@@ -15,49 +15,58 @@
|
|||||||
@inject GenerationService GenerationSvc
|
@inject GenerationService GenerationSvc
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h1 class="page-title mb-4">New cluster</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">New cluster</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<EditForm Model="_input" OnValidSubmit="CreateAsync" FormName="new-cluster">
|
<EditForm Model="_input" OnValidSubmit="CreateAsync" FormName="new-cluster">
|
||||||
<DataAnnotationsValidator/>
|
<DataAnnotationsValidator/>
|
||||||
|
|
||||||
<div class="row g-3">
|
<div class="card mb-3">
|
||||||
<div class="col-md-6">
|
<div class="card-body">
|
||||||
<label class="form-label">ClusterId <span class="text-danger">*</span></label>
|
<h6 class="text-muted border-bottom pb-1">Identity</h6>
|
||||||
<InputText @bind-Value="_input.ClusterId" class="form-control"/>
|
<div class="mb-2">
|
||||||
<div class="form-text">Stable internal ID. Lowercase alphanumeric + hyphens; ≤ 64 chars.</div>
|
<label class="form-label small">ClusterId <span class="text-danger">*</span></label>
|
||||||
<ValidationMessage For="() => _input.ClusterId"/>
|
<InputText @bind-Value="_input.ClusterId" class="form-control form-control-sm"/>
|
||||||
</div>
|
<div class="form-text">Stable internal ID. Lowercase alphanumeric + hyphens; ≤ 64 chars.</div>
|
||||||
<div class="col-md-6">
|
<ValidationMessage For="() => _input.ClusterId"/>
|
||||||
<label class="form-label">Display name <span class="text-danger">*</span></label>
|
</div>
|
||||||
<InputText @bind-Value="_input.Name" class="form-control"/>
|
<div class="mb-3">
|
||||||
<ValidationMessage For="() => _input.Name"/>
|
<label class="form-label small">Display name <span class="text-danger">*</span></label>
|
||||||
</div>
|
<InputText @bind-Value="_input.Name" class="form-control form-control-sm"/>
|
||||||
<div class="col-md-4">
|
<ValidationMessage For="() => _input.Name"/>
|
||||||
<label class="form-label">Enterprise</label>
|
</div>
|
||||||
<InputText @bind-Value="_input.Enterprise" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Site</label>
|
|
||||||
<InputText @bind-Value="_input.Site" class="form-control"/>
|
|
||||||
</div>
|
|
||||||
<div class="col-md-4">
|
|
||||||
<label class="form-label">Redundancy</label>
|
|
||||||
<InputSelect @bind-Value="_input.RedundancyMode" class="form-select">
|
|
||||||
<option value="@RedundancyMode.None">None (single node)</option>
|
|
||||||
<option value="@RedundancyMode.Warm">Warm (2 nodes)</option>
|
|
||||||
<option value="@RedundancyMode.Hot">Hot (2 nodes)</option>
|
|
||||||
</InputSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrEmpty(_error))
|
<h6 class="text-muted border-bottom pb-1">Placement</h6>
|
||||||
{
|
<div class="mb-2">
|
||||||
<section class="panel notice mt-3">@_error</section>
|
<label class="form-label small">Enterprise</label>
|
||||||
}
|
<InputText @bind-Value="_input.Enterprise" class="form-control form-control-sm"/>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small">Site</label>
|
||||||
|
<InputText @bind-Value="_input.Site" class="form-control form-control-sm"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<h6 class="text-muted border-bottom pb-1">Topology</h6>
|
||||||
<button type="submit" class="btn btn-primary" disabled="@_submitting">Create cluster</button>
|
<div class="mb-2">
|
||||||
<a href="/clusters" class="btn btn-secondary ms-2">Cancel</a>
|
<label class="form-label small">Redundancy</label>
|
||||||
|
<InputSelect @bind-Value="_input.RedundancyMode" class="form-select form-select-sm">
|
||||||
|
<option value="@RedundancyMode.None">None (single node)</option>
|
||||||
|
<option value="@RedundancyMode.Warm">Warm (2 nodes)</option>
|
||||||
|
<option value="@RedundancyMode.Hot">Hot (2 nodes)</option>
|
||||||
|
</InputSelect>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (!string.IsNullOrEmpty(_error))
|
||||||
|
{
|
||||||
|
<div class="text-danger small mt-2">@_error</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<button type="submit" class="btn btn-success btn-sm me-1" disabled="@_submitting">Save</button>
|
||||||
|
<a href="/clusters" class="btn btn-outline-secondary btn-sm">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</EditForm>
|
</EditForm>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,9 @@
|
|||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||||
@inject FocasDriverDetailService DetailSvc
|
@inject FocasDriverDetailService DetailSvc
|
||||||
|
|
||||||
<h1 class="page-title">FOCAS driver <span class="mono">@InstanceId</span></h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">FOCAS driver <span class="mono">@InstanceId</span></h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (_loading)
|
@if (_loading)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
@inject IServiceScopeFactory ScopeFactory
|
@inject IServiceScopeFactory ScopeFactory
|
||||||
@implements IDisposable
|
@implements IDisposable
|
||||||
|
|
||||||
<h1 class="page-title">Fleet status</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Fleet status</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center mb-3 gap-2">
|
<div class="d-flex align-items-center mb-3 gap-2">
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="RefreshAsync" disabled="@_refreshing">
|
<button class="btn btn-sm btn-outline-primary" @onclick="RefreshAsync" disabled="@_refreshing">
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
@inject GenerationService GenerationSvc
|
@inject GenerationService GenerationSvc
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h1 class="page-title">Fleet overview</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Fleet overview</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
@if (_clusters is null)
|
@if (_clusters is null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
@inject AdminHubConnectionFactory HubFactory
|
@inject AdminHubConnectionFactory HubFactory
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<h1 class="page-title">Driver host status</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Driver host status</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="d-flex align-items-center mb-3 gap-2">
|
<div class="d-flex align-items-center mb-3 gap-2">
|
||||||
<button class="btn btn-sm btn-outline-primary" @onclick="RefreshAsync" disabled="@_refreshing">
|
<button class="btn btn-sm btn-outline-primary" @onclick="RefreshAsync" disabled="@_refreshing">
|
||||||
|
|||||||
@@ -17,7 +17,9 @@
|
|||||||
|
|
||||||
<PageTitle>Modbus address preview</PageTitle>
|
<PageTitle>Modbus address preview</PageTitle>
|
||||||
|
|
||||||
<h1 class="page-title">Modbus address preview</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Modbus address preview</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Paste an address string and watch the parser break it down field by field. Useful for
|
Paste an address string and watch the parser break it down field by field. Useful for
|
||||||
sanity-checking a tag spreadsheet row before adding it to a driver's <span class="mono">DriverConfig</span>.
|
sanity-checking a tag spreadsheet row before adding it to a driver's <span class="mono">DriverConfig</span>.
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
|
|
||||||
<PageTitle>Modbus diagnostics — @DriverInstanceId</PageTitle>
|
<PageTitle>Modbus diagnostics — @DriverInstanceId</PageTitle>
|
||||||
|
|
||||||
<h1 class="page-title">Modbus auto-prohibitions</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Modbus auto-prohibitions</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Driver instance <span class="mono">@DriverInstanceId</span>. Live snapshot of coalesced ranges
|
Driver instance <span class="mono">@DriverInstanceId</span>. Live snapshot of coalesced ranges
|
||||||
the planner has learned to read individually (#148 / #150 / #151 / #152).
|
the planner has learned to read individually (#148 / #150 / #151 / #152).
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
@rendermode RenderMode.InteractiveServer
|
@rendermode RenderMode.InteractiveServer
|
||||||
@inject ReservationService ReservationSvc
|
@inject ReservationService ReservationSvc
|
||||||
|
|
||||||
<h1 class="page-title">External-ID reservations</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">External-ID reservations</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Fleet-wide ZTag + SAPID reservation state (decision #124). Releasing a reservation is a
|
Fleet-wide ZTag + SAPID reservation state (decision #124). Releasing a reservation is a
|
||||||
FleetAdmin-only audit-logged action — only release when the physical asset is permanently
|
FleetAdmin-only audit-logged action — only release when the physical asset is permanently
|
||||||
|
|||||||
@@ -15,9 +15,8 @@
|
|||||||
@inject AdminHubConnectionFactory HubFactory
|
@inject AdminHubConnectionFactory HubFactory
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<h1 class="page-title">LDAP group → Admin role grants</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">LDAP group → Admin role grants</h4>
|
||||||
<div class="d-flex justify-content-end mb-3">
|
|
||||||
<button class="btn btn-primary btn-sm" @onclick="StartAdd">Add grant</button>
|
<button class="btn btn-primary btn-sm" @onclick="StartAdd">Add grant</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
@inject AdminHubConnectionFactory HubFactory
|
@inject AdminHubConnectionFactory HubFactory
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
|
||||||
<h1 class="page-title">Script log viewer</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Script log viewer</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Live tail of the <code class="mono">scripts-*.log</code> file produced by the OPC UA Server's
|
Live tail of the <code class="mono">scripts-*.log</code> file produced by the OPC UA Server's
|
||||||
Roslyn script runtime. Useful for diagnosing virtual-tag and scripted-alarm script errors in production.
|
Roslyn script runtime. Useful for diagnosing virtual-tag and scripted-alarm script errors in production.
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
@inject ClusterService ClusterSvc
|
@inject ClusterService ClusterSvc
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h1 class="page-title">Scripted Alarms</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Scripted Alarms</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
OPC UA Part 9 alarms raised by C# predicate scripts. To author scripted alarms, open a cluster
|
OPC UA Part 9 alarms raised by C# predicate scripts. To author scripted alarms, open a cluster
|
||||||
draft and use the <strong>Scripted Alarms</strong> tab in the draft editor.
|
draft and use the <strong>Scripted Alarms</strong> tab in the draft editor.
|
||||||
|
|||||||
@@ -9,7 +9,9 @@
|
|||||||
@inject ClusterService ClusterSvc
|
@inject ClusterService ClusterSvc
|
||||||
@inject NavigationManager Nav
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
<h1 class="page-title">Virtual Tags</h1>
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
|
<h4 class="mb-0">Virtual Tags</h4>
|
||||||
|
</div>
|
||||||
<p class="text-muted">
|
<p class="text-muted">
|
||||||
Computed tags driven by C# scripts. To author virtual tags, open a cluster draft and use the
|
Computed tags driven by C# scripts. To author virtual tags, open a cluster draft and use the
|
||||||
<strong>Virtual Tags</strong> tab in the draft editor.
|
<strong>Virtual Tags</strong> tab in the draft editor.
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
@* Reusable loading spinner *@
|
||||||
|
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<div class="d-flex align-items-center text-secondary @CssClass">
|
||||||
|
<div class="spinner-border spinner-border-sm me-2" role="status">
|
||||||
|
<span class="visually-hidden">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<span>@Message</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public bool IsLoading { get; set; }
|
||||||
|
[Parameter] public string Message { get; set; } = "Loading...";
|
||||||
|
[Parameter] public string CssClass { get; set; } = "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
@* Status chip — wraps the theme.css .chip / .chip-ok / .chip-warn / .chip-bad / .chip-idle classes. *@
|
||||||
|
<span class="chip @CssClass">@Text</span>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter] public string Text { get; set; } = "";
|
||||||
|
[Parameter] public string CssClass { get; set; } = "chip-idle";
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
@*
|
||||||
|
Reusable toast notification component.
|
||||||
|
|
||||||
|
Toasts intentionally float above modal dialogs so confirmation feedback
|
||||||
|
(Success/Error) is visible even while a dialog is open.
|
||||||
|
*@
|
||||||
|
@implements IDisposable
|
||||||
|
|
||||||
|
<div class="position-fixed bottom-0 end-0 p-3" style="z-index: 1090;" aria-live="polite" aria-atomic="true">
|
||||||
|
@foreach (var toast in _toasts)
|
||||||
|
{
|
||||||
|
<div class="toast show mb-2" role="alert">
|
||||||
|
<div class="toast-header @GetHeaderClass(toast.Type)">
|
||||||
|
<strong class="me-auto">@toast.Title</strong>
|
||||||
|
<button type="button" class="btn-close btn-close-white" @onclick="() => Dismiss(toast)"></button>
|
||||||
|
</div>
|
||||||
|
<div class="toast-body">@toast.Message</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private const int DefaultAutoDismissMs = 5000;
|
||||||
|
|
||||||
|
private readonly List<ToastItem> _toasts = new();
|
||||||
|
private readonly object _lock = new();
|
||||||
|
|
||||||
|
// Cancels all pending auto-dismiss delays when the component is disposed
|
||||||
|
// so their continuations never touch a disposed component.
|
||||||
|
private readonly CancellationTokenSource _disposalCts = new();
|
||||||
|
|
||||||
|
/// <summary>Number of toasts currently displayed.</summary>
|
||||||
|
public int ToastCount
|
||||||
|
{
|
||||||
|
get { lock (_lock) { return _toasts.Count; } }
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowSuccess(string message, string title = "Success", int? autoDismissMs = null)
|
||||||
|
{
|
||||||
|
AddToast(title, message, ToastType.Success, autoDismissMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowError(string message, string title = "Error", int? autoDismissMs = null)
|
||||||
|
{
|
||||||
|
AddToast(title, message, ToastType.Error, autoDismissMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowWarning(string message, string title = "Warning", int? autoDismissMs = null)
|
||||||
|
{
|
||||||
|
AddToast(title, message, ToastType.Warning, autoDismissMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ShowInfo(string message, string title = "Info", int? autoDismissMs = null)
|
||||||
|
{
|
||||||
|
AddToast(title, message, ToastType.Info, autoDismissMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToast(string title, string message, ToastType type, int? autoDismissMs)
|
||||||
|
{
|
||||||
|
// If the component is already disposed, do not add or schedule anything.
|
||||||
|
if (_disposalCts.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
var toast = new ToastItem { Title = title, Message = message, Type = type };
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_toasts.Add(toast);
|
||||||
|
}
|
||||||
|
StateHasChanged();
|
||||||
|
|
||||||
|
var dismissMs = autoDismissMs ?? DefaultAutoDismissMs;
|
||||||
|
_ = AutoDismissAsync(toast, dismissMs, _disposalCts.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a toast after its dismiss delay. The delay is bound to the
|
||||||
|
/// component's disposal token: if the host page is disposed first, the
|
||||||
|
/// delay is cancelled and the continuation never touches the disposed
|
||||||
|
/// component — no <see cref="ObjectDisposedException"/> escapes.
|
||||||
|
/// </summary>
|
||||||
|
private async Task AutoDismissAsync(ToastItem toast, int dismissMs, CancellationToken token)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await Task.Delay(dismissMs, token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (token.IsCancellationRequested) return;
|
||||||
|
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_toasts.Remove(toast);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
catch (ObjectDisposedException)
|
||||||
|
{
|
||||||
|
// Component disposed between the token check and the render — ignore.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Dismiss(ToastItem toast)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_toasts.Remove(toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetHeaderClass(ToastType type) => type switch
|
||||||
|
{
|
||||||
|
ToastType.Success => "bg-success text-white",
|
||||||
|
ToastType.Error => "bg-danger text-white",
|
||||||
|
ToastType.Warning => "bg-warning text-dark",
|
||||||
|
ToastType.Info => "bg-info text-dark",
|
||||||
|
_ => "bg-secondary text-white"
|
||||||
|
};
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_disposalCts.Cancel();
|
||||||
|
_disposalCts.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum ToastType { Success, Error, Warning, Info }
|
||||||
|
|
||||||
|
private class ToastItem
|
||||||
|
{
|
||||||
|
public string Title { get; init; } = "";
|
||||||
|
public string Message { get; init; } = "";
|
||||||
|
public ToastType Type { get; init; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,4 +12,5 @@
|
|||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Layout
|
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Layout
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages
|
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Clusters
|
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Pages.Clusters
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.Admin.Components.Shared
|
||||||
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
@using ZB.MOM.WW.OtOpcUa.Admin.Services
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
Tokens live in theme.css; this sheet only carries layout + the side rail. */
|
Tokens live in theme.css; this sheet only carries layout + the side rail. */
|
||||||
|
|
||||||
/* ── App shell: side rail + page ─────────────────────────────────────────── */
|
/* ── App shell: side rail + page ─────────────────────────────────────────── */
|
||||||
|
/* The outer flex direction is supplied by Bootstrap utilities on the wrapper
|
||||||
|
(`d-flex flex-column flex-lg-row`) so the mobile hamburger row stacks above
|
||||||
|
the rail on <lg viewports and the rail sits beside the page on lg+. */
|
||||||
.app-shell {
|
.app-shell {
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
min-height: calc(100vh - 3.3rem);
|
min-height: calc(100vh - 3.3rem);
|
||||||
}
|
}
|
||||||
@@ -15,8 +17,8 @@
|
|||||||
|
|
||||||
/* ── Side rail ───────────────────────────────────────────────────────────── */
|
/* ── Side rail ───────────────────────────────────────────────────────────── */
|
||||||
.side-rail {
|
.side-rail {
|
||||||
width: 218px;
|
width: 220px;
|
||||||
flex: 0 0 218px;
|
flex: 0 0 220px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.15rem;
|
gap: 0.15rem;
|
||||||
@@ -25,6 +27,28 @@
|
|||||||
border-right: 1px solid var(--rule-strong);
|
border-right: 1px solid var(--rule-strong);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* On lg+ keep the side rail pinned so it stays visible when content scrolls. */
|
||||||
|
@media (min-width: 992px) {
|
||||||
|
#sidebar-collapse {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
height: 100vh;
|
||||||
|
align-self: flex-start;
|
||||||
|
z-index: 1020;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the side rail is collapsed under <lg viewports the Bootstrap collapse
|
||||||
|
container removes the fixed width; restore full width on mobile. */
|
||||||
|
@media (max-width: 991.98px) {
|
||||||
|
.side-rail {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.rail-eyebrow {
|
.rail-eyebrow {
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -90,14 +114,6 @@
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Page headings — uppercase eyebrow, calm spacing ─────────────────────── */
|
|
||||||
.page-title {
|
|
||||||
font-size: 1.15rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin: 0 0 1rem;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Login card centring ─────────────────────────────────────────────────── */
|
/* ── Login card centring ─────────────────────────────────────────────────── */
|
||||||
.login-wrap {
|
.login-wrap {
|
||||||
max-width: 380px;
|
max-width: 380px;
|
||||||
Reference in New Issue
Block a user