dashboard(alarms): provider-status badge (alarmmgr vs degraded subtag)
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
@page "/alarms"
|
||||
@implements IAsyncDisposable
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
|
||||
@inject IDashboardLiveDataService LiveData
|
||||
@inject IOptions<GatewayOptions> GatewayOptions
|
||||
@inject DashboardHubConnectionFactory HubFactory
|
||||
|
||||
<PageTitle>Dashboard Alarms</PageTitle>
|
||||
|
||||
@@ -10,6 +13,12 @@
|
||||
<h1>Alarms</h1>
|
||||
<div class="text-secondary">@HeaderLine()</div>
|
||||
</div>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<span class="badge @_providerStatus.BadgeCssClass"
|
||||
title="@ProviderStatusTitle()">
|
||||
@_providerStatus.Label
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (!GatewayOptions.Value.Alarms.Enabled)
|
||||
@@ -163,10 +172,44 @@
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private Task? _pollTask;
|
||||
|
||||
private DashboardAlarmProviderStatus _providerStatus = DashboardAlarmProviderStatus.Healthy;
|
||||
private HubConnection? _alarmsHub;
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_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()
|
||||
@@ -268,6 +311,19 @@
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
|
||||
if (_alarmsHub is not null)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _alarmsHub.DisposeAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Disposal-time errors are best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
if (_pollTask is not null)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
[Fact]
|
||||
public void FormatValue_AndDataType_RenderArrayElementsAndElementType()
|
||||
|
||||
Reference in New Issue
Block a user