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" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Operation timeout *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">Operation settings</div>
|
||||
|
||||
+5
@@ -37,6 +37,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Operation settings *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">Operation settings</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Connection *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||
<div class="panel-head">Connection</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* mxaccessgw connection *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">mxaccessgw connection</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Connection *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">Connection</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Transport *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">Transport</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Endpoint *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.06s">
|
||||
<div class="panel-head">Endpoint</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Connection *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||
<div class="panel-head">Connection</div>
|
||||
|
||||
+5
@@ -36,6 +36,11 @@ else
|
||||
|
||||
<DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />
|
||||
|
||||
@if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
|
||||
{
|
||||
<DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
|
||||
}
|
||||
|
||||
@* Options *@
|
||||
<section class="panel rise mt-3" style="animation-delay:.05s">
|
||||
<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