Dashboard: confirm before Close session / Kill worker

Add a shared ConfirmDialog component and route Sessions, Workers, and
SessionDetails Close/Kill buttons through it. The dialog shows the
target session id and a color-matched confirm button (yellow Close,
red Kill); Cancel dismisses without invoking the admin service.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-24 07:17:32 -04:00
parent c5e7479ee4
commit 0e56b5befb
4 changed files with 223 additions and 28 deletions
@@ -34,12 +34,12 @@ else
<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">
@onclick="RequestClose">
Close session
</button>
<button type="button" class="btn btn-outline-danger"
disabled="@IsBusy"
@onclick="KillWorkerAsync">
@onclick="RequestKill">
Kill worker
</button>
</div>
@@ -54,6 +54,18 @@ else
</div>
}
@if (CanManage)
{
<ConfirmDialog IsOpen="@(PendingAction is not null)"
Title="@(PendingAction?.Title ?? string.Empty)"
Message="@(PendingAction?.Message ?? string.Empty)"
ConfirmLabel="@(PendingAction?.ConfirmLabel ?? "Confirm")"
ConfirmButtonClass="@(PendingAction?.ConfirmButtonClass ?? "btn-primary")"
IsBusy="IsBusy"
OnConfirm="ConfirmPendingAsync"
OnCancel="CancelPending" />
}
<section class="dashboard-section">
<div class="section-heading">
<h2>Session</h2>
@@ -176,24 +188,55 @@ else
}
}
private Task CloseSessionAsync()
{
return RunAdminActionAsync(user => SessionAdminService.CloseSessionAsync(user, SessionId, CancellationToken.None));
}
private PendingConfirm? PendingAction { get; set; }
private Task KillWorkerAsync()
{
return RunAdminActionAsync(user => SessionAdminService.KillWorkerAsync(user, SessionId, CancellationToken.None));
}
private async Task RunAdminActionAsync(
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action)
private void RequestClose()
{
if (IsBusy)
{
return;
}
PendingAction = new PendingConfirm(
Title: "Close session?",
Message: $"Gracefully close session {SessionId}? The worker will be shut down.",
ConfirmLabel: "Close",
ConfirmButtonClass: "btn-warning",
Action: user => SessionAdminService.CloseSessionAsync(user, SessionId, CancellationToken.None));
}
private void RequestKill()
{
if (IsBusy)
{
return;
}
PendingAction = new PendingConfirm(
Title: "Kill worker?",
Message: $"Forcefully kill the worker for session {SessionId}? This skips graceful shutdown.",
ConfirmLabel: "Kill",
ConfirmButtonClass: "btn-danger",
Action: user => SessionAdminService.KillWorkerAsync(user, SessionId, CancellationToken.None));
}
private void CancelPending()
{
if (!IsBusy)
{
PendingAction = null;
}
}
private async Task ConfirmPendingAsync()
{
if (IsBusy || PendingAction is null)
{
return;
}
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action = PendingAction.Action;
IsBusy = true;
try
{
@@ -207,9 +250,17 @@ else
finally
{
IsBusy = false;
PendingAction = null;
}
}
private sealed record PendingConfirm(
string Title,
string Message,
string ConfirmLabel,
string ConfirmButtonClass,
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> Action);
private async Task AttachEventsHubAsync()
{
if (string.IsNullOrWhiteSpace(SessionId))
@@ -25,6 +25,18 @@ else
</div>
}
@if (CanManage)
{
<ConfirmDialog IsOpen="@(PendingAction is not null)"
Title="@(PendingAction?.Title ?? string.Empty)"
Message="@(PendingAction?.Message ?? string.Empty)"
ConfirmLabel="@(PendingAction?.ConfirmLabel ?? "Confirm")"
ConfirmButtonClass="@(PendingAction?.ConfirmButtonClass ?? "btn-primary")"
IsBusy="IsBusy"
OnConfirm="ConfirmPendingAsync"
OnCancel="CancelPending" />
}
<section class="dashboard-section">
@if (Snapshot.Sessions.Count == 0)
{
@@ -78,12 +90,12 @@ else
<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)">
@onclick="() => RequestClose(session.SessionId)">
Close
</button>
<button type="button" class="btn btn-outline-danger"
disabled="@IsBusy"
@onclick="() => KillWorkerAsync(session.SessionId)">
@onclick="() => RequestKill(session.SessionId)">
Kill
</button>
</div>
@@ -116,24 +128,55 @@ else
CanManage = SessionAdminService.CanManage(authenticationState.User);
}
private Task CloseSessionAsync(string sessionId)
{
return RunActionAsync(user => SessionAdminService.CloseSessionAsync(user, sessionId, CancellationToken.None));
}
private PendingConfirm? PendingAction { get; set; }
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)
private void RequestClose(string sessionId)
{
if (IsBusy)
{
return;
}
PendingAction = new PendingConfirm(
Title: "Close session?",
Message: $"Gracefully close session {sessionId}? The worker will be shut down.",
ConfirmLabel: "Close",
ConfirmButtonClass: "btn-warning",
Action: user => SessionAdminService.CloseSessionAsync(user, sessionId, CancellationToken.None));
}
private void RequestKill(string sessionId)
{
if (IsBusy)
{
return;
}
PendingAction = new PendingConfirm(
Title: "Kill worker?",
Message: $"Forcefully kill the worker for session {sessionId}? This skips graceful shutdown.",
ConfirmLabel: "Kill",
ConfirmButtonClass: "btn-danger",
Action: user => SessionAdminService.KillWorkerAsync(user, sessionId, CancellationToken.None));
}
private void CancelPending()
{
if (!IsBusy)
{
PendingAction = null;
}
}
private async Task ConfirmPendingAsync()
{
if (IsBusy || PendingAction is null)
{
return;
}
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action = PendingAction.Action;
IsBusy = true;
try
{
@@ -147,6 +190,14 @@ else
finally
{
IsBusy = false;
PendingAction = null;
}
}
private sealed record PendingConfirm(
string Title,
string Message,
string ConfirmLabel,
string ConfirmButtonClass,
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> Action);
}
@@ -25,6 +25,18 @@ else
</div>
}
@if (CanManage)
{
<ConfirmDialog IsOpen="@(PendingSessionId is not null)"
Title="Kill worker?"
Message="@($"Forcefully kill the worker for session {PendingSessionId}? This skips graceful shutdown.")"
ConfirmLabel="Kill"
ConfirmButtonClass="btn-danger"
IsBusy="IsBusy"
OnConfirm="ConfirmKillAsync"
OnCancel="CancelPending" />
}
<section class="dashboard-section">
@if (Snapshot.Workers.Count == 0)
{
@@ -61,7 +73,7 @@ else
<td>
<button type="button" class="btn btn-sm btn-outline-danger"
disabled="@IsBusy"
@onclick="() => KillWorkerAsync(worker.SessionId)">
@onclick="() => RequestKill(worker.SessionId)">
Kill
</button>
</td>
@@ -93,13 +105,34 @@ else
CanManage = SessionAdminService.CanManage(authenticationState.User);
}
private async Task KillWorkerAsync(string sessionId)
private string? PendingSessionId { get; set; }
private void RequestKill(string sessionId)
{
if (IsBusy)
{
return;
}
PendingSessionId = sessionId;
}
private void CancelPending()
{
if (!IsBusy)
{
PendingSessionId = null;
}
}
private async Task ConfirmKillAsync()
{
if (IsBusy || PendingSessionId is null)
{
return;
}
string sessionId = PendingSessionId;
IsBusy = true;
try
{
@@ -115,6 +148,7 @@ else
finally
{
IsBusy = false;
PendingSessionId = null;
}
}
}
@@ -0,0 +1,59 @@
@if (IsOpen)
{
<div class="modal-backdrop fade show"></div>
<div class="modal fade show" role="dialog" aria-modal="true" aria-labelledby="@TitleId" tabindex="-1" style="display: block;">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title h5" id="@TitleId">@Title</h2>
<button type="button" class="btn-close" aria-label="Close"
disabled="@IsBusy"
@onclick="OnCancel"></button>
</div>
<div class="modal-body">
<p class="mb-0">@Message</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary"
disabled="@IsBusy"
@onclick="OnCancel">
Cancel
</button>
<button type="button" class="btn @ConfirmButtonClass"
disabled="@IsBusy"
@onclick="OnConfirm">
@ConfirmLabel
</button>
</div>
</div>
</div>
</div>
}
@code {
private readonly string TitleId = $"confirm-dialog-{Guid.NewGuid():N}";
[Parameter]
public bool IsOpen { get; set; }
[Parameter]
public string Title { get; set; } = "Confirm";
[Parameter]
public string Message { get; set; } = string.Empty;
[Parameter]
public string ConfirmLabel { get; set; } = "Confirm";
[Parameter]
public string ConfirmButtonClass { get; set; } = "btn-primary";
[Parameter]
public bool IsBusy { get; set; }
[Parameter]
public EventCallback OnConfirm { get; set; }
[Parameter]
public EventCallback OnCancel { get; set; }
}