@page "/sessions/{SessionId}"
@inherits DashboardPageBase
@implements IAsyncDisposable
@using Microsoft.AspNetCore.SignalR.Client
@using ZB.MOM.WW.MxGateway.Contracts.Proto
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
Dashboard Session
@if (Snapshot is null)
{
Loading session.
}
else if (CurrentSession is null)
{
Session Not Found
The session is not present in the current snapshot.
}
else
{
Session
| Backend | @CurrentSession.BackendName |
| Client identity | @DashboardDisplay.Text(CurrentSession.ClientIdentity) |
| Client session | @DashboardDisplay.Text(CurrentSession.ClientSessionName) |
| Client correlation | @DashboardDisplay.Text(CurrentSession.ClientCorrelationId) |
| Opened | @DashboardDisplay.DateTime(CurrentSession.OpenedAt) |
| Last activity | @DashboardDisplay.DateTime(CurrentSession.LastClientActivityAt) |
| Lease expires | @DashboardDisplay.DateTime(CurrentSession.LeaseExpiresAt) |
| Events received | @DashboardDisplay.Count(CurrentSession.EventsReceived) |
| Last fault | @DashboardDisplay.Text(CurrentSession.LastFault) |
Worker
| Process id | @(CurrentSession.WorkerProcessId?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "-") |
| State | |
| Last heartbeat | @DashboardDisplay.DateTime(CurrentSession.LastWorkerHeartbeatAt) |
Recent events
@(_eventsConnected ? "live" : "offline")
@if (_recentEvents.Count == 0)
{
Waiting for events. The dashboard mirrors the session's gRPC event stream — events
appear here only while a gRPC client is also consuming this session's events.
}
else
{
| Worker seq |
Family |
Server |
Item |
Status |
@foreach (MxEvent evt in _recentEvents)
{
| @evt.WorkerSequence |
@evt.Family |
@evt.ServerHandle |
@evt.ItemHandle |
@DashboardDisplay.Text(EventStatusLabel(evt)) |
}
}
}
@code {
private const int MaxRecentEvents = 50;
[Parameter]
public string SessionId { get; set; } = string.Empty;
private DashboardSessionSummary? CurrentSession => Snapshot?.Sessions.FirstOrDefault(session =>
string.Equals(session.SessionId, SessionId, StringComparison.Ordinal));
private HubConnection? _eventsHub;
private bool _eventsConnected;
private string? _subscribedSessionId;
private readonly LinkedList _recentEvents = new();
protected override async Task OnParametersSetAsync()
{
if (!string.Equals(_subscribedSessionId, SessionId, StringComparison.Ordinal))
{
await DetachEventsHubAsync().ConfigureAwait(false);
await AttachEventsHubAsync().ConfigureAwait(false);
}
}
private async Task AttachEventsHubAsync()
{
if (string.IsNullOrWhiteSpace(SessionId))
{
return;
}
_eventsHub = HubFactory.Create("/hubs/events");
_eventsHub.On(EventsHub.EventMessage, async mxEvent =>
{
_recentEvents.AddFirst(mxEvent);
while (_recentEvents.Count > MaxRecentEvents)
{
_recentEvents.RemoveLast();
}
await InvokeAsync(StateHasChanged).ConfigureAwait(false);
});
_eventsHub.Closed += _ =>
{
_eventsConnected = false;
return InvokeAsync(StateHasChanged);
};
_eventsHub.Reconnected += _ =>
{
_eventsConnected = true;
return InvokeAsync(StateHasChanged);
};
try
{
await _eventsHub.StartAsync().ConfigureAwait(false);
await _eventsHub.SendAsync("SubscribeSession", SessionId).ConfigureAwait(false);
_eventsConnected = true;
_subscribedSessionId = SessionId;
}
catch
{
_eventsConnected = false;
}
}
private async Task DetachEventsHubAsync()
{
HubConnection? hub = _eventsHub;
_eventsHub = null;
_eventsConnected = false;
_subscribedSessionId = null;
_recentEvents.Clear();
if (hub is not null)
{
try
{
await hub.DisposeAsync().ConfigureAwait(false);
}
catch
{
// Disposal-time errors are best-effort.
}
}
}
private static string EventStatusLabel(MxEvent evt)
{
return evt.Family == MxEventFamily.OnAlarmTransition
? evt.OnAlarmTransition?.AlarmFullReference ?? string.Empty
: string.Empty;
}
public new async ValueTask DisposeAsync()
{
await DetachEventsHubAsync().ConfigureAwait(false);
await base.DisposeAsync().ConfigureAwait(false);
}
}