diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor index 2a706b5f..c4272905 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Deployment/DebugView.razor @@ -191,9 +191,10 @@ Alarm + Kind State + Sev Level - Priority Timestamp @@ -201,18 +202,44 @@ @foreach (var alarm in FilteredAlarmStates) { + title="@BuildAlarmTooltip(alarm)"> @alarm.AlarmName @if (!string.IsNullOrEmpty(alarm.Message)) { ๐Ÿ’ฌ } + @if (!string.IsNullOrEmpty(alarm.SourceReference)) + { +
@alarm.SourceReference
+ } + + + @FormatKind(alarm.Kind) @alarm.State + @if (alarm.Kind != AlarmKind.Computed) + { + @if (alarm.Condition.Active && !alarm.Condition.Acknowledged) + { + Unacked + } + @if (alarm.Condition.Shelve != AlarmShelveState.Unshelved) + { + Shelved + } + @if (alarm.Condition.Suppressed) + { + Suppressed + } + } + @alarm.Condition.Severity @if (alarm.Level != AlarmLevel.None) { @@ -224,7 +251,6 @@ โ€” } - @alarm.Priority @alarm.Timestamp.LocalDateTime.ToString("HH:mm:ss") @@ -284,7 +310,8 @@ string.IsNullOrWhiteSpace(_alarmFilter) ? _alarmStates.Values.OrderBy(a => a.AlarmName).ToList() : _alarmStates.Values - .Where(a => a.AlarmName.Contains(_alarmFilter, StringComparison.OrdinalIgnoreCase)) + .Where(a => a.AlarmName.Contains(_alarmFilter, StringComparison.OrdinalIgnoreCase) + || a.SourceReference.Contains(_alarmFilter, StringComparison.OrdinalIgnoreCase)) .OrderBy(a => a.AlarmName) .ToList(); @@ -575,6 +602,40 @@ _ => "bg-secondary" }; + /// Badge class distinguishing computed (neutral) from native (info) alarms. + private static string GetKindBadge(AlarmKind kind) => kind switch + { + AlarmKind.Computed => "bg-secondary", + _ => "bg-info text-dark" + }; + + /// Short display label for the alarm kind. + private static string FormatKind(AlarmKind kind) => kind switch + { + AlarmKind.NativeOpcUa => "OPC UA", + AlarmKind.NativeMxAccess => "MxAccess", + _ => "Computed" + }; + + /// + /// Builds the row tooltip from the alarm's operator message plus native + /// metadata (type, category, operator, raise time, current/limit value). + /// Returns null when there is nothing extra to show. + /// + private static string? BuildAlarmTooltip(AlarmStateChanged a) + { + var parts = new List(); + if (!string.IsNullOrEmpty(a.Message)) parts.Add(a.Message); + if (!string.IsNullOrEmpty(a.AlarmTypeName)) parts.Add($"Type: {a.AlarmTypeName}"); + if (!string.IsNullOrEmpty(a.Category)) parts.Add($"Category: {a.Category}"); + if (!string.IsNullOrEmpty(a.OperatorUser)) parts.Add($"By: {a.OperatorUser}"); + if (!string.IsNullOrEmpty(a.OperatorComment)) parts.Add($"Comment: {a.OperatorComment}"); + if (a.OriginalRaiseTime.HasValue) parts.Add($"Raised: {a.OriginalRaiseTime.Value.LocalDateTime:HH:mm:ss}"); + if (!string.IsNullOrEmpty(a.CurrentValue)) parts.Add($"Value: {a.CurrentValue}"); + if (!string.IsNullOrEmpty(a.LimitValue)) parts.Add($"Limit: {a.LimitValue}"); + return parts.Count == 0 ? null : string.Join(" ยท ", parts); + } + private static string FormatLevel(AlarmLevel level) => level switch { AlarmLevel.HighHigh => "HiHi", diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugViewAlarmTableTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugViewAlarmTableTests.cs new file mode 100644 index 00000000..09be8afe --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Deployment/DebugViewAlarmTableTests.cs @@ -0,0 +1,103 @@ +using System.Reflection; +using System.Security.Claims; +using Bunit; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using NSubstitute; +using ZB.MOM.WW.ScadaBridge.CentralUI.Auth; +using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites; +using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; +using ZB.MOM.WW.ScadaBridge.Commons.Messages.DebugView; +using ZB.MOM.WW.ScadaBridge.Commons.Messages.Streaming; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Alarms; +using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums; +using ZB.MOM.WW.ScadaBridge.Communication; +using ZB.MOM.WW.ScadaBridge.Communication.Grpc; +using DebugViewPage = ZB.MOM.WW.ScadaBridge.CentralUI.Components.Pages.Deployment.DebugView; + +namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment; + +/// +/// Task 23: the DebugView alarm table surfaces enriched native alarm state โ€” +/// kind, severity, source reference, and a composite condition badge set +/// (Unacked / Shelved / Suppressed). +/// +public class DebugViewAlarmTableTests : BunitContext +{ + private IRenderedComponent RenderPage() + { + JSInterop.Mode = JSRuntimeMode.Loose; + + var repo = Substitute.For(); + var siteRepo = Substitute.For(); + siteRepo.GetAllSitesAsync().Returns(new List()); + Services.AddSingleton(repo); + Services.AddSingleton(siteRepo); + + var comms = new CommunicationService( + Options.Create(new CommunicationOptions()), + NullLogger.Instance); + Services.AddSingleton(comms); + + var grpcFactory = new SiteStreamGrpcClientFactory(NullLoggerFactory.Instance); + var debugStream = new DebugStreamService( + comms, new ServiceCollection().BuildServiceProvider(), grpcFactory, + NullLogger.Instance); + Services.AddSingleton(debugStream); + + var identity = new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, "deployer") }, "TestCookie"); + var stubAuth = new StubAuthStateProvider(new AuthenticationState(new ClaimsPrincipal(identity))); + Services.AddSingleton(stubAuth); + Services.AddScoped(_ => new SiteScopeService(stubAuth)); + + return Render(); + } + + private sealed class StubAuthStateProvider : AuthenticationStateProvider + { + private readonly AuthenticationState _state; + public StubAuthStateProvider(AuthenticationState state) => _state = state; + public override Task GetAuthenticationStateAsync() => Task.FromResult(_state); + } + + private static void SetField(DebugViewPage c, string name, object? value) => + typeof(DebugViewPage).GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.SetValue(c, value); + + private static Dictionary AlarmStates(DebugViewPage c) => + (Dictionary)typeof(DebugViewPage) + .GetField("_alarmStates", BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(c)!; + + [Fact] + public void AlarmTable_RendersNativeAlarm_WithSeverityKindAndShelvedBadge() + { + var cut = RenderPage(); + + var native = new AlarmStateChanged("inst", "T01.Hi", AlarmState.Active, 800, DateTimeOffset.UtcNow) + { + Kind = AlarmKind.NativeOpcUa, + SourceReference = "ns=2;s=Tank01.Level.HiHi", + Condition = new AlarmConditionState( + Active: true, Acknowledged: false, Confirmed: null, + Shelve: AlarmShelveState.OneShotShelved, Suppressed: false, Severity: 800) + }; + + cut.InvokeAsync(() => + { + SetField(cut.Instance, "_connected", true); + SetField(cut.Instance, "_snapshot", + new DebugViewSnapshot("inst", new List(), + new List { native }, DateTimeOffset.UtcNow)); + AlarmStates(cut.Instance)["T01.Hi"] = native; + }); + cut.Render(); + + var markup = cut.Markup; + Assert.Contains("800", markup); // severity surfaced + Assert.Contains("Shelved", markup); // composite condition badge + Assert.Contains("OPC UA", markup); // native kind badge + Assert.Contains("ns=2;s=Tank01.Level.HiHi", markup); // source reference + Assert.Contains("Unacked", markup); // active + unacknowledged + } +}