Add Browse and Alarms dashboard tabs
Browse renders the Galaxy hierarchy tree from IGalaxyHierarchyCache: expandable areas/objects with attribute name, data type and the alarm/historized flags, plus a name/reference filter. Right-click or double-click an attribute to add it to a subscription panel that polls live value, quality and source timestamp every two seconds. Alarms lists the worker's currently-active alarm set via IAlarmRpcDispatcher, defaulting to unacknowledged Active alarms with filters for acknowledged alarms, area, severity range and text. It is read-only and warns when alarm auto-subscribe is disabled. Both tabs read live MXAccess data through a new singleton DashboardLiveDataService that owns one shared, lazily-opened gateway session (one worker) for the whole dashboard, re-opened transparently if it faults or its lease expires. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
using MxGateway.Contracts.Proto;
|
||||
using MxGateway.Contracts.Proto.Galaxy;
|
||||
using MxGateway.Server.Dashboard;
|
||||
|
||||
namespace MxGateway.Tests.Gateway.Dashboard;
|
||||
|
||||
/// <summary>
|
||||
/// Unit tests for the pure projection/formatting helpers behind the
|
||||
/// dashboard Browse and Alarms tabs.
|
||||
/// </summary>
|
||||
public sealed class DashboardBrowseAndAlarmModelTests
|
||||
{
|
||||
[Fact]
|
||||
public void BuildTree_LinksChildrenToParents_AndPromotesOrphansToRoots()
|
||||
{
|
||||
GalaxyObject area = new() { GobjectId = 1, BrowseName = "AreaA", IsArea = true, ParentGobjectId = 0 };
|
||||
GalaxyObject child = new() { GobjectId = 2, BrowseName = "Pump01", ParentGobjectId = 1 };
|
||||
GalaxyObject orphan = new() { GobjectId = 3, BrowseName = "Lost", ParentGobjectId = 99 };
|
||||
|
||||
IReadOnlyList<DashboardBrowseNode> roots = DashboardBrowseTreeBuilder.Build([area, child, orphan]);
|
||||
|
||||
// The area and the orphan (its parent id is absent) are both roots.
|
||||
Assert.Equal(2, roots.Count);
|
||||
DashboardBrowseNode areaNode = Assert.Single(roots, node => node.Object.GobjectId == 1);
|
||||
Assert.Single(areaNode.Children);
|
||||
Assert.Equal(2, areaNode.Children[0].Object.GobjectId);
|
||||
Assert.Contains(roots, node => node.Object.GobjectId == 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildTree_SortsAreasBeforeObjects()
|
||||
{
|
||||
GalaxyObject instance = new() { GobjectId = 1, BrowseName = "Zeta", IsArea = false };
|
||||
GalaxyObject areaB = new() { GobjectId = 2, BrowseName = "Beta", IsArea = true };
|
||||
|
||||
IReadOnlyList<DashboardBrowseNode> roots = DashboardBrowseTreeBuilder.Build([instance, areaB]);
|
||||
|
||||
Assert.Equal(2, roots.Count);
|
||||
Assert.True(roots[0].IsArea);
|
||||
Assert.Equal("Beta", roots[0].DisplayName);
|
||||
Assert.False(roots[1].IsArea);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(true, "true")]
|
||||
[InlineData(false, "false")]
|
||||
public void FormatValue_FormatsBooleans(bool input, string expected)
|
||||
{
|
||||
MxValue value = new() { DataType = MxDataType.Boolean, BoolValue = input };
|
||||
Assert.Equal(expected, DashboardMxValueFormatter.FormatValue(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatValue_FormatsNumbersAndStrings()
|
||||
{
|
||||
Assert.Equal("42", DashboardMxValueFormatter.FormatValue(new MxValue { Int32Value = 42 }));
|
||||
Assert.Equal("hello", DashboardMxValueFormatter.FormatValue(new MxValue { StringValue = "hello" }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FormatValue_HandlesNullPayloadAndNullReference()
|
||||
{
|
||||
Assert.Equal("-", DashboardMxValueFormatter.FormatValue(null));
|
||||
Assert.Equal("(null)", DashboardMxValueFormatter.FormatValue(new MxValue { IsNull = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagValue_FromSuccessfulReadResult_MarksGoodQuality()
|
||||
{
|
||||
BulkReadResult result = new()
|
||||
{
|
||||
TagAddress = "Galaxy!Area.Tag",
|
||||
WasSuccessful = true,
|
||||
Quality = 192,
|
||||
Value = new MxValue { DataType = MxDataType.Double, DoubleValue = 1.5 },
|
||||
};
|
||||
|
||||
DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result);
|
||||
|
||||
Assert.True(value.Ok);
|
||||
Assert.True(value.QualityGood);
|
||||
Assert.Equal("1.5", value.ValueText);
|
||||
Assert.Null(value.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TagValue_FromFailedReadResult_CarriesError()
|
||||
{
|
||||
BulkReadResult result = new()
|
||||
{
|
||||
TagAddress = "Galaxy!Area.Bad",
|
||||
WasSuccessful = false,
|
||||
Quality = 0,
|
||||
ErrorMessage = "invalid handle",
|
||||
};
|
||||
|
||||
DashboardTagValue value = DashboardTagValue.FromBulkReadResult(result);
|
||||
|
||||
Assert.False(value.Ok);
|
||||
Assert.False(value.QualityGood);
|
||||
Assert.Equal("invalid handle", value.Error);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveAlarm_FromSnapshot_ParsesProviderAndAcknowledgementState()
|
||||
{
|
||||
ActiveAlarmSnapshot unacked = new()
|
||||
{
|
||||
AlarmFullReference = "Galaxy!TestArea.TestMachine_001.TestAlarm001",
|
||||
Category = "TestArea",
|
||||
CurrentState = AlarmConditionState.Active,
|
||||
Severity = 500,
|
||||
};
|
||||
ActiveAlarmSnapshot acked = new()
|
||||
{
|
||||
AlarmFullReference = "Galaxy!TestArea.TestMachine_002.TestAlarm001",
|
||||
CurrentState = AlarmConditionState.ActiveAcked,
|
||||
};
|
||||
|
||||
DashboardActiveAlarm unackedRow = DashboardActiveAlarm.FromSnapshot(unacked);
|
||||
DashboardActiveAlarm ackedRow = DashboardActiveAlarm.FromSnapshot(acked);
|
||||
|
||||
Assert.Equal("Galaxy", unackedRow.Provider);
|
||||
Assert.Equal("TestArea", unackedRow.Area);
|
||||
Assert.Equal(500, unackedRow.Severity);
|
||||
Assert.True(unackedRow.IsUnacknowledged);
|
||||
Assert.False(ackedRow.IsUnacknowledged);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user