feat(adminui): DriverStatusPanel + wire into 9 typed pages
Live panel subscribed to the /hubs/driverstatus SignalR feed — renders state chip, last-success age, 5-min error count, last error message. Auto-reconnect; dimmed when no push arrives for 30s. Hidden for new instances (nothing deployed yet); shown read-only on every edit-mode page. Reconnect/Restart buttons land in Phase 8.
This commit is contained in:
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Operation timeout *@
|
@* Operation timeout *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">Operation settings</div>
|
<div class="panel-head">Operation settings</div>
|
||||||
|
|||||||
+5
@@ -37,6 +37,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Operation settings *@
|
@* Operation settings *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">Operation settings</div>
|
<div class="panel-head">Operation settings</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Connection *@
|
@* Connection *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||||
<div class="panel-head">Connection</div>
|
<div class="panel-head">Connection</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* mxaccessgw connection *@
|
@* mxaccessgw connection *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">mxaccessgw connection</div>
|
<div class="panel-head">mxaccessgw connection</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Connection *@
|
@* Connection *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">Connection</div>
|
<div class="panel-head">Connection</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Transport *@
|
@* Transport *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">Transport</div>
|
<div class="panel-head">Transport</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Endpoint *@
|
@* Endpoint *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||||
<div class="panel-head">Endpoint</div>
|
<div class="panel-head">Endpoint</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Connection *@
|
@* Connection *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||||
<div class="panel-head">Connection</div>
|
<div class="panel-head">Connection</div>
|
||||||
|
|||||||
+5
@@ -36,6 +36,11 @@ else
|
|||||||
|
|
||||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||||
|
|
||||||
|
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||||
|
{
|
||||||
|
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||||
|
}
|
||||||
|
|
||||||
@* Options *@
|
@* Options *@
|
||||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||||
<div class="panel-head">Options</div>
|
<div class="panel-head">Options</div>
|
||||||
|
|||||||
+152
@@ -0,0 +1,152 @@
|
|||||||
|
@* Live driver-status panel — subscribes to /hubs/driverstatus and shows state chip,
|
||||||
|
last-success age, 5-min error count, and last error message.
|
||||||
|
Enabled=false renders a static "Disabled" notice and never opens the hub.
|
||||||
|
Reconnect/Restart buttons are Phase 8 (Task 8.3). *@
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
|
@using ZB.MOM.WW.OtOpcUa.Commons.Messages.Drivers
|
||||||
|
@inject NavigationManager Nav
|
||||||
|
|
||||||
|
<section class="panel rise mt-3" style="animation-delay:.04s; @(_stale ? "opacity:0.5;" : "")">
|
||||||
|
<div class="panel-head d-flex align-items-center gap-2">
|
||||||
|
<span>Driver status</span>
|
||||||
|
@if (_snapshot is not null)
|
||||||
|
{
|
||||||
|
<span class="chip @ChipClass(_snapshot.State)">@_snapshot.State</span>
|
||||||
|
}
|
||||||
|
else if (!Enabled)
|
||||||
|
{
|
||||||
|
<span class="chip chip-idle">Disabled</span>
|
||||||
|
}
|
||||||
|
else if (_connecting)
|
||||||
|
{
|
||||||
|
<span class="chip chip-idle">Connecting…</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="padding:1rem">
|
||||||
|
@if (!Enabled)
|
||||||
|
{
|
||||||
|
<p class="mb-0" style="color:var(--ink-soft)">Disabled — not deployed. Enable the driver and save to start receiving live status.</p>
|
||||||
|
}
|
||||||
|
else if (_error is not null)
|
||||||
|
{
|
||||||
|
<p class="mb-0" style="color:var(--bad)">SignalR error: @_error</p>
|
||||||
|
}
|
||||||
|
else if (_snapshot is null)
|
||||||
|
{
|
||||||
|
<p class="mb-0" style="color:var(--ink-faint)">Awaiting first snapshot…</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="d-flex flex-wrap gap-3 align-items-baseline">
|
||||||
|
<span style="color:var(--ink-soft)">
|
||||||
|
Last success:
|
||||||
|
@if (_snapshot.LastSuccessfulReadUtc is { } t)
|
||||||
|
{
|
||||||
|
<strong>@HumanizeAge(t) ago</strong>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<strong>never</strong>
|
||||||
|
}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
@if (_snapshot.ErrorCount5Min > 0)
|
||||||
|
{
|
||||||
|
<span class="chip chip-bad">@_snapshot.ErrorCount5Min error@(_snapshot.ErrorCount5Min == 1 ? "" : "s") / 5 min</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (_snapshot.LastError is { Length: > 0 } lastError)
|
||||||
|
{
|
||||||
|
<details class="mt-2" style="font-size:0.85rem">
|
||||||
|
<summary style="cursor:pointer; color:var(--ink-soft)">Last error</summary>
|
||||||
|
<pre class="mt-1 mb-0" style="white-space:pre-wrap; word-break:break-word; color:var(--bad); font-size:0.8rem">@lastError</pre>
|
||||||
|
</details>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired] public string DriverInstanceId { get; set; } = "";
|
||||||
|
[Parameter] public bool Enabled { get; set; } = true;
|
||||||
|
|
||||||
|
private HubConnection? _hub;
|
||||||
|
private DriverHealthChanged? _snapshot;
|
||||||
|
private DateTime _lastUpdateUtc = DateTime.MinValue;
|
||||||
|
private bool _stale;
|
||||||
|
private bool _connecting;
|
||||||
|
private string? _error;
|
||||||
|
private System.Threading.Timer? _timer;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
if (!Enabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_connecting = true;
|
||||||
|
|
||||||
|
// Tick every 5 s to refresh the stale-dimming check and humanized ages.
|
||||||
|
_timer = new System.Threading.Timer(_ =>
|
||||||
|
{
|
||||||
|
_stale = _snapshot is not null &&
|
||||||
|
(DateTime.UtcNow - _lastUpdateUtc).TotalSeconds > 30;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
}, null, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
|
_hub = new HubConnectionBuilder()
|
||||||
|
.WithUrl(Nav.ToAbsoluteUri("/hubs/driverstatus"))
|
||||||
|
.WithAutomaticReconnect()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
_hub.On<DriverHealthChanged>("status", snap =>
|
||||||
|
{
|
||||||
|
_snapshot = snap;
|
||||||
|
_lastUpdateUtc = DateTime.UtcNow;
|
||||||
|
_stale = false;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _hub.StartAsync();
|
||||||
|
_connecting = false;
|
||||||
|
await _hub.InvokeAsync("JoinDriver", DriverInstanceId);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_connecting = false;
|
||||||
|
_error = ex.Message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_timer?.Dispose();
|
||||||
|
if (_hub is not null)
|
||||||
|
await _hub.DisposeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map DriverState string → chip CSS class using the 4 defined theme variants.
|
||||||
|
private static string ChipClass(string? state) => state switch
|
||||||
|
{
|
||||||
|
"Healthy" => "chip-ok",
|
||||||
|
"Degraded" => "chip-warn",
|
||||||
|
"Connecting" => "chip-warn",
|
||||||
|
"Reconnecting" => "chip-warn",
|
||||||
|
"Faulted" => "chip-bad",
|
||||||
|
_ => "chip-idle", // Unknown, Initializing, null
|
||||||
|
};
|
||||||
|
|
||||||
|
private static string HumanizeAge(DateTime utc)
|
||||||
|
{
|
||||||
|
var age = DateTime.UtcNow - utc;
|
||||||
|
if (age.TotalSeconds < 2) return "just now";
|
||||||
|
if (age.TotalSeconds < 60) return $"{(int)age.TotalSeconds}s";
|
||||||
|
if (age.TotalMinutes < 60) return $"{(int)age.TotalMinutes}m {age.Seconds}s";
|
||||||
|
if (age.TotalHours < 24) return $"{(int)age.TotalHours}h {age.Minutes}m";
|
||||||
|
return $"{(int)age.TotalDays}d {age.Hours}h";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user