feat(adminui): F15.3 closes F15 — live alerts/script-log, CSV import, Monaco editor
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been cancelled
v2-ci / build (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been cancelled
v2-ci / integration (push) Has been cancelled
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been cancelled
v2-ci / build (push) Has been cancelled
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been cancelled
v2-ci / integration (push) Has been cancelled
Final F15 batch wires up the SignalR-backed live pages, ports the bulk
equipment importer, and progressively enhances the Script source editor
with Monaco.
Message contracts:
- Commons.Messages.Alerts.AlarmTransitionEvent — fires on every alarm
state transition; published on the `alerts` DPS topic by future
ScriptedAlarmActor (F9) emits.
- Commons.Messages.Logging.ScriptLogEntry — one log line emitted by a
hosted script; published on the `script-logs` DPS topic by future
VirtualTagActor (F8) + ScriptedAlarmActor (F9) emits.
(Folder named "Logging" to dodge .gitignore's "logs/" rule.)
SignalR plumbing:
- AlertHub gains MethodName + bridge actor (AlertSignalRBridge)
- ScriptLogHub introduced; ScriptLogSignalRBridge follows the same
DPS-subscribe → IHubContext fan-out pattern as FleetStatusSignalRBridge
- WithOtOpcUaSignalRBridges now spawns all three bridges
- MapOtOpcUaHubs maps /hubs/script-log alongside the existing hubs
Pages:
- /alerts live alarm tail, 200-row capacity
- /script-log live script-log tail with level + script
filter, 500-row capacity
- /clusters/{id}/equipment/import — CSV bulk Equipment add with preview
(Name/MachineCode/UnsLineId/Driver +
optional ZTag/SAPID/Manufacturer/Model;
skips rows whose MachineCode already
exists in the fleet)
- ScriptEdit progressively enhanced with Monaco editor via JSInterop —
the textarea remains Blazor's source of truth and Monaco syncs into it
on every keystroke so @bind keeps working; falls back gracefully if
the CDN is unreachable.
MainLayout nav gains a "Live" section (Deployments, Alerts, Alarms
historian) and a "Scripts" link under Scripting. ClusterEquipment
surfaces the new Import CSV button.
Tally: F15 ships ~42 razor pages + 3 SignalR hubs + 3 bridge actors.
Microsoft.AspNetCore.SignalR.Client added (was already in central PM).
All 104 v2 tests remain green.
This commit is contained in:
@@ -2,8 +2,14 @@ using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
|
||||
/// <summary>Browser-facing alert / toast push channel. Bridge wiring staged for F16.</summary>
|
||||
/// <summary>
|
||||
/// Browser-facing alert push channel. Subscribers receive
|
||||
/// <see cref="Commons.Messages.Alerts.AlarmTransitionEvent"/> snapshots whenever an alarm fires,
|
||||
/// clears, or is acknowledged on any cluster node. Bridge: <c>AlertSignalRBridge</c> subscribes
|
||||
/// to the <c>alerts</c> DPS topic and forwards to every connected SignalR client.
|
||||
/// </summary>
|
||||
public sealed class AlertHub : Hub
|
||||
{
|
||||
public const string Endpoint = "/hubs/alerts";
|
||||
public const string MethodName = "alarmTransition";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster.Tools.PublishSubscribe;
|
||||
using Akka.Event;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Alerts;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Akka actor that subscribes to the <c>alerts</c> DistributedPubSub topic and forwards each
|
||||
/// <see cref="AlarmTransitionEvent"/> to every SignalR client connected to <see cref="AlertHub"/>.
|
||||
/// Mirrors <c>FleetStatusSignalRBridge</c>'s design — one bridge per admin node, hub fan-out is
|
||||
/// per-node, no cluster-singleton needed.
|
||||
/// </summary>
|
||||
public sealed class AlertSignalRBridge : ReceiveActor
|
||||
{
|
||||
public const string TopicName = "alerts";
|
||||
|
||||
private readonly IHubContext<AlertHub> _hub;
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
|
||||
public static Props Props(IHubContext<AlertHub> hub) =>
|
||||
Akka.Actor.Props.Create(() => new AlertSignalRBridge(hub));
|
||||
|
||||
public AlertSignalRBridge(IHubContext<AlertHub> hub)
|
||||
{
|
||||
_hub = hub;
|
||||
ReceiveAsync<AlarmTransitionEvent>(ForwardAsync);
|
||||
Receive<SubscribeAck>(_ => { /* DPS confirmation */ });
|
||||
}
|
||||
|
||||
protected override void PreStart() =>
|
||||
DistributedPubSub.Get(Context.System).Mediator.Tell(new Subscribe(TopicName, Self));
|
||||
|
||||
private async Task ForwardAsync(AlarmTransitionEvent msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hub.Clients.All.SendAsync(AlertHub.MethodName, msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warning(ex, "AlertSignalRBridge: SignalR push failed for {AlarmId}", msg.AlarmId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ public static class HubRouteBuilderExtensions
|
||||
{
|
||||
app.MapHub<FleetStatusHub>(FleetStatusHub.Endpoint);
|
||||
app.MapHub<AlertHub>(AlertHub.Endpoint);
|
||||
app.MapHub<ScriptLogHub>(ScriptLogHub.Endpoint);
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
public static class HubServiceCollectionExtensions
|
||||
{
|
||||
public const string FleetStatusSignalRBridgeName = "fleet-status-signalr-bridge";
|
||||
public const string AlertSignalRBridgeName = "alert-signalr-bridge";
|
||||
public const string ScriptLogSignalRBridgeName = "script-log-signalr-bridge";
|
||||
|
||||
/// <summary>
|
||||
/// Spawns the SignalR bridge actors that forward DPS messages to browser-facing SignalR
|
||||
/// hubs. Currently: <see cref="FleetStatusSignalRBridge"/> (DPS <c>fleet-status</c> topic →
|
||||
/// <see cref="FleetStatusHub"/> clients).
|
||||
/// hubs: <c>fleet-status</c> → <see cref="FleetStatusHub"/>, <c>alerts</c> →
|
||||
/// <see cref="AlertHub"/>, <c>script-logs</c> → <see cref="ScriptLogHub"/>.
|
||||
///
|
||||
/// Call inside the admin-role configurator on the shared <see cref="AkkaConfigurationBuilder"/>:
|
||||
/// <code>
|
||||
@@ -27,13 +29,23 @@ public static class HubServiceCollectionExtensions
|
||||
{
|
||||
builder.WithActors((system, registry, resolver) =>
|
||||
{
|
||||
var hub = resolver.GetService<IHubContext<FleetStatusHub>>();
|
||||
var actor = system.ActorOf(FleetStatusSignalRBridge.Props(hub), FleetStatusSignalRBridgeName);
|
||||
registry.Register<FleetStatusSignalRBridgeKey>(actor);
|
||||
var fleetHub = resolver.GetService<IHubContext<FleetStatusHub>>();
|
||||
var fleetBridge = system.ActorOf(FleetStatusSignalRBridge.Props(fleetHub), FleetStatusSignalRBridgeName);
|
||||
registry.Register<FleetStatusSignalRBridgeKey>(fleetBridge);
|
||||
|
||||
var alertHub = resolver.GetService<IHubContext<AlertHub>>();
|
||||
var alertBridge = system.ActorOf(AlertSignalRBridge.Props(alertHub), AlertSignalRBridgeName);
|
||||
registry.Register<AlertSignalRBridgeKey>(alertBridge);
|
||||
|
||||
var scriptLogHub = resolver.GetService<IHubContext<ScriptLogHub>>();
|
||||
var scriptLogBridge = system.ActorOf(ScriptLogSignalRBridge.Props(scriptLogHub), ScriptLogSignalRBridgeName);
|
||||
registry.Register<ScriptLogSignalRBridgeKey>(scriptLogBridge);
|
||||
});
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Marker key for <see cref="ActorRegistry"/> lookup of the SignalR bridge actor.</summary>
|
||||
/// <summary>Marker keys for <see cref="ActorRegistry"/> lookup of the SignalR bridge actors.</summary>
|
||||
public sealed class FleetStatusSignalRBridgeKey { }
|
||||
public sealed class AlertSignalRBridgeKey { }
|
||||
public sealed class ScriptLogSignalRBridgeKey { }
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Browser-facing script-log push channel. Subscribers receive
|
||||
/// <see cref="Commons.Messages.Logging.ScriptLogEntry"/> lines emitted by VirtualTagActor +
|
||||
/// ScriptedAlarmActor as their hosted scripts log diagnostic output. Bridge:
|
||||
/// <c>ScriptLogSignalRBridge</c> subscribes to the <c>script-logs</c> DPS topic and forwards
|
||||
/// to every connected SignalR client.
|
||||
/// </summary>
|
||||
public sealed class ScriptLogHub : Hub
|
||||
{
|
||||
public const string Endpoint = "/hubs/script-log";
|
||||
public const string MethodName = "scriptLogEntry";
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using Akka.Actor;
|
||||
using Akka.Cluster.Tools.PublishSubscribe;
|
||||
using Akka.Event;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using ZB.MOM.WW.OtOpcUa.Commons.Messages.Logging;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
|
||||
|
||||
/// <summary>
|
||||
/// Akka actor that subscribes to the <c>script-logs</c> DistributedPubSub topic and forwards each
|
||||
/// <see cref="ScriptLogEntry"/> to every SignalR client connected to <see cref="ScriptLogHub"/>.
|
||||
/// </summary>
|
||||
public sealed class ScriptLogSignalRBridge : ReceiveActor
|
||||
{
|
||||
public const string TopicName = "script-logs";
|
||||
|
||||
private readonly IHubContext<ScriptLogHub> _hub;
|
||||
private readonly ILoggingAdapter _log = Context.GetLogger();
|
||||
|
||||
public static Props Props(IHubContext<ScriptLogHub> hub) =>
|
||||
Akka.Actor.Props.Create(() => new ScriptLogSignalRBridge(hub));
|
||||
|
||||
public ScriptLogSignalRBridge(IHubContext<ScriptLogHub> hub)
|
||||
{
|
||||
_hub = hub;
|
||||
ReceiveAsync<ScriptLogEntry>(ForwardAsync);
|
||||
Receive<SubscribeAck>(_ => { /* DPS confirmation */ });
|
||||
}
|
||||
|
||||
protected override void PreStart() =>
|
||||
DistributedPubSub.Get(Context.System).Mediator.Tell(new Subscribe(TopicName, Self));
|
||||
|
||||
private async Task ForwardAsync(ScriptLogEntry msg)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _hub.Clients.All.SendAsync(ScriptLogHub.MethodName, msg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_log.Warning(ex, "ScriptLogSignalRBridge: SignalR push failed for {ScriptId}", msg.ScriptId);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user