Dashboard: admin-only Close session / Kill worker
Add IDashboardSessionAdminService (Admin-role gate, friendly errors, audit logging) wrapping a new ISessionManager.KillWorkerAsync that skips graceful shutdown and cleans up registry/metrics. Sessions, Workers, and SessionDetails pages render Close / Kill buttons only when CanManage; the service re-checks the role on every call so forged clicks return Unauthenticated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@
|
||||
@using Microsoft.AspNetCore.SignalR.Client
|
||||
@using ZB.MOM.WW.MxGateway.Contracts.Proto
|
||||
@using ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject IDashboardSessionAdminService SessionAdminService
|
||||
|
||||
<PageTitle>Dashboard Session</PageTitle>
|
||||
|
||||
@@ -25,9 +27,33 @@ else
|
||||
<h1>Session Details</h1>
|
||||
<div class="text-secondary"><code>@CurrentSession.SessionId</code></div>
|
||||
</div>
|
||||
<StatusBadge Text="@CurrentSession.State.ToString()" />
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<StatusBadge Text="@CurrentSession.State.ToString()" />
|
||||
@if (CanManage)
|
||||
{
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Session admin actions">
|
||||
<button type="button" class="btn btn-outline-warning"
|
||||
disabled="@IsBusy"
|
||||
@onclick="CloseSessionAsync">
|
||||
Close session
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
disabled="@IsBusy"
|
||||
@onclick="KillWorkerAsync">
|
||||
Kill worker
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (CanManage && !string.IsNullOrWhiteSpace(ResultMessage))
|
||||
{
|
||||
<div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
|
||||
@ResultMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="dashboard-section">
|
||||
<div class="section-heading">
|
||||
<h2>Session</h2>
|
||||
@@ -124,6 +150,23 @@ else
|
||||
private string? _subscribedSessionId;
|
||||
private readonly LinkedList<MxEvent> _recentEvents = new();
|
||||
|
||||
private bool CanManage { get; set; }
|
||||
|
||||
private bool IsBusy { get; set; }
|
||||
|
||||
private string? ResultMessage { get; set; }
|
||||
|
||||
private bool LastOperationSucceeded { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync().ConfigureAwait(false);
|
||||
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
}
|
||||
|
||||
protected override async Task OnParametersSetAsync()
|
||||
{
|
||||
if (!string.Equals(_subscribedSessionId, SessionId, StringComparison.Ordinal))
|
||||
@@ -133,6 +176,40 @@ else
|
||||
}
|
||||
}
|
||||
|
||||
private Task CloseSessionAsync()
|
||||
{
|
||||
return RunAdminActionAsync(user => SessionAdminService.CloseSessionAsync(user, SessionId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private Task KillWorkerAsync()
|
||||
{
|
||||
return RunAdminActionAsync(user => SessionAdminService.KillWorkerAsync(user, SessionId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task RunAdminActionAsync(
|
||||
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
DashboardSessionAdminResult result = await action(authenticationState.User).ConfigureAwait(false);
|
||||
ResultMessage = result.Message;
|
||||
LastOperationSucceeded = result.Succeeded;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AttachEventsHubAsync()
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(SessionId))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@page "/sessions"
|
||||
@inherits DashboardPageBase
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject IDashboardSessionAdminService SessionAdminService
|
||||
|
||||
<PageTitle>Dashboard Sessions</PageTitle>
|
||||
|
||||
@@ -16,6 +18,13 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (CanManage && !string.IsNullOrWhiteSpace(ResultMessage))
|
||||
{
|
||||
<div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
|
||||
@ResultMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="dashboard-section">
|
||||
@if (Snapshot.Sessions.Count == 0)
|
||||
{
|
||||
@@ -37,6 +46,10 @@ else
|
||||
<th scope="col">Activity</th>
|
||||
<th scope="col">Heartbeat</th>
|
||||
<th scope="col">Fault</th>
|
||||
@if (CanManage)
|
||||
{
|
||||
<th scope="col">Actions</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -59,6 +72,23 @@ else
|
||||
<td>@DashboardDisplay.DateTime(session.LastClientActivityAt)</td>
|
||||
<td>@DashboardDisplay.DateTime(session.LastWorkerHeartbeatAt)</td>
|
||||
<td>@DashboardDisplay.Text(session.LastFault)</td>
|
||||
@if (CanManage)
|
||||
{
|
||||
<td>
|
||||
<div class="btn-group btn-group-sm" role="group" aria-label="Session actions">
|
||||
<button type="button" class="btn btn-outline-warning"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => CloseSessionAsync(session.SessionId)">
|
||||
Close
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => KillWorkerAsync(session.SessionId)">
|
||||
Kill
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -67,3 +97,56 @@ else
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool CanManage { get; set; }
|
||||
|
||||
private bool IsBusy { get; set; }
|
||||
|
||||
private string? ResultMessage { get; set; }
|
||||
|
||||
private bool LastOperationSucceeded { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync().ConfigureAwait(false);
|
||||
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
}
|
||||
|
||||
private Task CloseSessionAsync(string sessionId)
|
||||
{
|
||||
return RunActionAsync(user => SessionAdminService.CloseSessionAsync(user, sessionId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private Task KillWorkerAsync(string sessionId)
|
||||
{
|
||||
return RunActionAsync(user => SessionAdminService.KillWorkerAsync(user, sessionId, CancellationToken.None));
|
||||
}
|
||||
|
||||
private async Task RunActionAsync(
|
||||
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
DashboardSessionAdminResult result = await action(authenticationState.User).ConfigureAwait(false);
|
||||
ResultMessage = result.Message;
|
||||
LastOperationSucceeded = result.Succeeded;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@page "/workers"
|
||||
@inherits DashboardPageBase
|
||||
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||||
@inject IDashboardSessionAdminService SessionAdminService
|
||||
|
||||
<PageTitle>Dashboard Workers</PageTitle>
|
||||
|
||||
@@ -16,6 +18,13 @@ else
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (CanManage && !string.IsNullOrWhiteSpace(ResultMessage))
|
||||
{
|
||||
<div class="alert @(LastOperationSucceeded ? "alert-success" : "alert-danger")" role="alert">
|
||||
@ResultMessage
|
||||
</div>
|
||||
}
|
||||
|
||||
<section class="dashboard-section">
|
||||
@if (Snapshot.Workers.Count == 0)
|
||||
{
|
||||
@@ -32,6 +41,10 @@ else
|
||||
<th scope="col">Session</th>
|
||||
<th scope="col">Heartbeat</th>
|
||||
<th scope="col">Fault</th>
|
||||
@if (CanManage)
|
||||
{
|
||||
<th scope="col">Actions</th>
|
||||
}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -43,6 +56,16 @@ else
|
||||
<td><NavLink href="@($"sessions/{Uri.EscapeDataString(worker.SessionId)}")"><code>@worker.SessionId</code></NavLink></td>
|
||||
<td>@DashboardDisplay.DateTime(worker.LastHeartbeatAt)</td>
|
||||
<td>@DashboardDisplay.Text(worker.LastFault)</td>
|
||||
@if (CanManage)
|
||||
{
|
||||
<td>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger"
|
||||
disabled="@IsBusy"
|
||||
@onclick="() => KillWorkerAsync(worker.SessionId)">
|
||||
Kill
|
||||
</button>
|
||||
</td>
|
||||
}
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
@@ -51,3 +74,47 @@ else
|
||||
}
|
||||
</section>
|
||||
}
|
||||
|
||||
@code {
|
||||
private bool CanManage { get; set; }
|
||||
|
||||
private bool IsBusy { get; set; }
|
||||
|
||||
private string? ResultMessage { get; set; }
|
||||
|
||||
private bool LastOperationSucceeded { get; set; }
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await base.OnInitializedAsync().ConfigureAwait(false);
|
||||
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
}
|
||||
|
||||
private async Task KillWorkerAsync(string sessionId)
|
||||
{
|
||||
if (IsBusy)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IsBusy = true;
|
||||
try
|
||||
{
|
||||
AuthenticationState authenticationState = await AuthenticationStateProvider.GetAuthenticationStateAsync()
|
||||
.ConfigureAwait(false);
|
||||
CanManage = SessionAdminService.CanManage(authenticationState.User);
|
||||
DashboardSessionAdminResult result = await SessionAdminService
|
||||
.KillWorkerAsync(authenticationState.User, sessionId, CancellationToken.None)
|
||||
.ConfigureAwait(false);
|
||||
ResultMessage = result.Message;
|
||||
LastOperationSucceeded = result.Succeeded;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsBusy = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user