dashboard(alarms): provider-status badge (alarmmgr vs degraded subtag)
This commit is contained in:
@@ -1,7 +1,10 @@
|
|||||||
@page "/alarms"
|
@page "/alarms"
|
||||||
@implements IAsyncDisposable
|
@implements IAsyncDisposable
|
||||||
|
@using Microsoft.AspNetCore.SignalR.Client
|
||||||
|
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
|
||||||
@inject IDashboardLiveDataService LiveData
|
@inject IDashboardLiveDataService LiveData
|
||||||
@inject IOptions<GatewayOptions> GatewayOptions
|
@inject IOptions<GatewayOptions> GatewayOptions
|
||||||
|
@inject DashboardHubConnectionFactory HubFactory
|
||||||
|
|
||||||
<PageTitle>Dashboard Alarms</PageTitle>
|
<PageTitle>Dashboard Alarms</PageTitle>
|
||||||
|
|
||||||
@@ -10,6 +13,12 @@
|
|||||||
<h1>Alarms</h1>
|
<h1>Alarms</h1>
|
||||||
<div class="text-secondary">@HeaderLine()</div>
|
<div class="text-secondary">@HeaderLine()</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<span class="badge @_providerStatus.BadgeCssClass"
|
||||||
|
title="@ProviderStatusTitle()">
|
||||||
|
@_providerStatus.Label
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!GatewayOptions.Value.Alarms.Enabled)
|
@if (!GatewayOptions.Value.Alarms.Enabled)
|
||||||
@@ -163,10 +172,44 @@
|
|||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private Task? _pollTask;
|
private Task? _pollTask;
|
||||||
|
|
||||||
|
private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
|
||||||
|
private HubConnection? _alarmsHub;
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_pollTask = PollLoopAsync();
|
_pollTask = PollLoopAsync();
|
||||||
|
_ = AttachAlarmsHubAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private string? ProviderStatusTitle()
|
||||||
|
{
|
||||||
|
return _providerStatus.IsDegraded && !string.IsNullOrWhiteSpace(_providerStatus.Reason)
|
||||||
|
? _providerStatus.Reason
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task AttachAlarmsHubAsync()
|
||||||
|
{
|
||||||
|
_alarmsHub = HubFactory.Create("/hubs/alarms");
|
||||||
|
_alarmsHub.On<AlarmFeedMessage>(AlarmsHub.AlarmMessage, async message =>
|
||||||
|
{
|
||||||
|
if (message.PayloadCase == AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
|
||||||
|
{
|
||||||
|
_providerStatus = DashboardAlarmProviderStatus.FromFeed(message);
|
||||||
|
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _alarmsHub.StartAsync(_cts.Token).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// The badge is best-effort; it stays at the healthy default until
|
||||||
|
// the hub reconnects and delivers a fresh provider-status message.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private string HeaderLine()
|
private string HeaderLine()
|
||||||
@@ -268,6 +311,19 @@
|
|||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
await _cts.CancelAsync();
|
await _cts.CancelAsync();
|
||||||
|
|
||||||
|
if (_alarmsHub is not null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _alarmsHub.DisposeAsync();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Disposal-time errors are best-effort.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_pollTask is not null)
|
if (_pollTask is not null)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
using ZB.MOM.WW.MxGateway.Contracts.Proto;
|
||||||
|
|
||||||
|
namespace ZB.MOM.WW.MxGateway.Server.Dashboard;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dashboard projection of an <see cref="AlarmProviderStatus" /> message
|
||||||
|
/// carried on the alarm feed. Maps the protobuf provider mode / degraded
|
||||||
|
/// flag into Bootstrap-only display fields so the Alarms page can render a
|
||||||
|
/// status badge without touching protobuf types.
|
||||||
|
/// </summary>
|
||||||
|
public sealed record DashboardAlarmProviderStatus(
|
||||||
|
AlarmProviderMode Mode,
|
||||||
|
bool IsDegraded,
|
||||||
|
string Label,
|
||||||
|
string BadgeCssClass,
|
||||||
|
string Reason,
|
||||||
|
DateTimeOffset? SinceUtc)
|
||||||
|
{
|
||||||
|
/// <summary>Badge label shown when the alarm-manager provider is healthy.</summary>
|
||||||
|
public const string AlarmManagerLabel = "Alarm Manager";
|
||||||
|
|
||||||
|
/// <summary>Badge label shown when the feed has fallen back to subtag monitoring.</summary>
|
||||||
|
public const string DegradedLabel = "Subtag monitoring (degraded)";
|
||||||
|
|
||||||
|
private const string HealthyBadge = "bg-success";
|
||||||
|
private const string DegradedBadge = "bg-warning text-dark";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The default status assumed before the first provider-status message
|
||||||
|
/// arrives: healthy alarm-manager mode.
|
||||||
|
/// </summary>
|
||||||
|
public static DashboardAlarmProviderStatus Healthy { get; } = new(
|
||||||
|
Mode: AlarmProviderMode.Alarmmgr,
|
||||||
|
IsDegraded: false,
|
||||||
|
Label: AlarmManagerLabel,
|
||||||
|
BadgeCssClass: HealthyBadge,
|
||||||
|
Reason: string.Empty,
|
||||||
|
SinceUtc: null);
|
||||||
|
|
||||||
|
/// <summary>Projects an alarm-feed provider-status payload into a dashboard badge model.</summary>
|
||||||
|
/// <param name="status">The provider-status payload from an <see cref="AlarmFeedMessage" />.</param>
|
||||||
|
/// <returns>The projected dashboard status.</returns>
|
||||||
|
public static DashboardAlarmProviderStatus FromProviderStatus(AlarmProviderStatus status)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(status);
|
||||||
|
|
||||||
|
// Treat the explicit degraded flag and the SUBTAG mode as equivalent;
|
||||||
|
// the contract sets degraded=true whenever mode == SUBTAG, but guard
|
||||||
|
// against either being set independently.
|
||||||
|
bool degraded = status.Degraded || status.Mode == AlarmProviderMode.Subtag;
|
||||||
|
|
||||||
|
return new DashboardAlarmProviderStatus(
|
||||||
|
Mode: status.Mode,
|
||||||
|
IsDegraded: degraded,
|
||||||
|
Label: degraded ? DegradedLabel : AlarmManagerLabel,
|
||||||
|
BadgeCssClass: degraded ? DegradedBadge : HealthyBadge,
|
||||||
|
Reason: status.Reason ?? string.Empty,
|
||||||
|
SinceUtc: status.Since?.ToDateTimeOffset());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Projects an alarm-feed message into a dashboard badge model.</summary>
|
||||||
|
/// <param name="message">An alarm-feed message whose payload is a provider status.</param>
|
||||||
|
/// <returns>The projected dashboard status.</returns>
|
||||||
|
/// <exception cref="ArgumentException">The message does not carry a provider-status payload.</exception>
|
||||||
|
public static DashboardAlarmProviderStatus FromFeed(AlarmFeedMessage message)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(message);
|
||||||
|
|
||||||
|
if (message.PayloadCase != AlarmFeedMessage.PayloadOneofCase.ProviderStatus)
|
||||||
|
{
|
||||||
|
throw new ArgumentException(
|
||||||
|
"Alarm-feed message does not carry a provider-status payload.",
|
||||||
|
nameof(message));
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromProviderStatus(message.ProviderStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,6 +137,41 @@ public sealed class DashboardBrowseAndAlarmModelTests
|
|||||||
Assert.False(ackedRow.IsUnacknowledged);
|
Assert.False(ackedRow.IsUnacknowledged);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a healthy alarmmgr provider status maps to a green badge.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void FromProviderStatus_Alarmmgr_NotDegraded_GreenBadge()
|
||||||
|
{
|
||||||
|
AlarmProviderStatus status = new()
|
||||||
|
{
|
||||||
|
Mode = AlarmProviderMode.Alarmmgr,
|
||||||
|
Degraded = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);
|
||||||
|
|
||||||
|
Assert.False(model.IsDegraded);
|
||||||
|
Assert.Contains("bg-success", model.BadgeCssClass, StringComparison.Ordinal);
|
||||||
|
Assert.Equal(DashboardAlarmProviderStatus.AlarmManagerLabel, model.Label);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Verifies that a degraded subtag provider status maps to an amber warning badge.</summary>
|
||||||
|
[Fact]
|
||||||
|
public void FromProviderStatus_Subtag_Degraded_WarningBadge()
|
||||||
|
{
|
||||||
|
AlarmProviderStatus status = new()
|
||||||
|
{
|
||||||
|
Mode = AlarmProviderMode.Subtag,
|
||||||
|
Degraded = true,
|
||||||
|
Reason = "x",
|
||||||
|
};
|
||||||
|
|
||||||
|
DashboardAlarmProviderStatus model = DashboardAlarmProviderStatus.FromProviderStatus(status);
|
||||||
|
|
||||||
|
Assert.True(model.IsDegraded);
|
||||||
|
Assert.Contains("bg-warning", model.BadgeCssClass, StringComparison.Ordinal);
|
||||||
|
Assert.Equal("x", model.Reason);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Verifies that the formatter renders array elements and element type correctly.</summary>
|
/// <summary>Verifies that the formatter renders array elements and element type correctly.</summary>
|
||||||
[Fact]
|
[Fact]
|
||||||
public void FormatValue_AndDataType_RenderArrayElementsAndElementType()
|
public void FormatValue_AndDataType_RenderArrayElementsAndElementType()
|
||||||
|
|||||||
Reference in New Issue
Block a user