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"> <div class="btn-group btn-group-sm" role="group" aria-label="Session admin actions">
<button type="button" class="btn btn-outline-warning" <button type="button" class="btn btn-outline-warning"
disabled="@IsBusy" disabled="@IsBusy"
@onclick="CloseSessionAsync"> @onclick="RequestClose">
Close session Close session
</button> </button>
<button type="button" class="btn btn-outline-danger" <button type="button" class="btn btn-outline-danger"
disabled="@IsBusy" disabled="@IsBusy"
@onclick="KillWorkerAsync"> @onclick="RequestKill">
Kill worker Kill worker
</button> </button>
</div> </div>
@@ -54,6 +54,18 @@ else
</div> </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"> <section class="dashboard-section">
<div class="section-heading"> <div class="section-heading">
<h2>Session</h2> <h2>Session</h2>
@@ -176,24 +188,55 @@ else
} }
} }
private Task CloseSessionAsync() private PendingConfirm? PendingAction { get; set; }
{
return RunAdminActionAsync(user => SessionAdminService.CloseSessionAsync(user, SessionId, CancellationToken.None));
}
private Task KillWorkerAsync() private void RequestClose()
{
return RunAdminActionAsync(user => SessionAdminService.KillWorkerAsync(user, SessionId, CancellationToken.None));
}
private async Task RunAdminActionAsync(
Func<System.Security.Claims.ClaimsPrincipal, Task<DashboardSessionAdminResult>> action)
{ {
if (IsBusy) if (IsBusy)
{ {
return; 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; IsBusy = true;
try try
{ {
@@ -207,9 +250,17 @@ else
finally finally
{ {
IsBusy = false; 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() private async Task AttachEventsHubAsync()
{ {
if (string.IsNullOrWhiteSpace(SessionId)) if (string.IsNullOrWhiteSpace(SessionId))
@@ -25,6 +25,18 @@ else
</div> </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"> <section class="dashboard-section">
@if (Snapshot.Sessions.Count == 0) @if (Snapshot.Sessions.Count == 0)
{ {
@@ -78,12 +90,12 @@ else
<div class="btn-group btn-group-sm" role="group" aria-label="Session actions"> <div class="btn-group btn-group-sm" role="group" aria-label="Session actions">
<button type="button" class="btn btn-outline-warning" <button type="button" class="btn btn-outline-warning"
disabled="@IsBusy" disabled="@IsBusy"
@onclick="() => CloseSessionAsync(session.SessionId)"> @onclick="() => RequestClose(session.SessionId)">
Close Close
</button> </button>
<button type="button" class="btn btn-outline-danger" <button type="button" class="btn btn-outline-danger"
disabled="@IsBusy" disabled="@IsBusy"
@onclick="() => KillWorkerAsync(session.SessionId)"> @onclick="() => RequestKill(session.SessionId)">
Kill Kill
</button> </button>
</div> </div>
@@ -116,24 +128,55 @@ else
CanManage = SessionAdminService.CanManage(authenticationState.User); CanManage = SessionAdminService.CanManage(authenticationState.User);
} }
private Task CloseSessionAsync(string sessionId) private PendingConfirm? PendingAction { get; set; }
{
return RunActionAsync(user => SessionAdminService.CloseSessionAsync(user, sessionId, CancellationToken.None));
}
private Task KillWorkerAsync(string sessionId) private void RequestClose(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) if (IsBusy)
{ {
return; 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; IsBusy = true;
try try
{ {
@@ -147,6 +190,14 @@ else
finally finally
{ {
IsBusy = false; 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> </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"> <section class="dashboard-section">
@if (Snapshot.Workers.Count == 0) @if (Snapshot.Workers.Count == 0)
{ {
@@ -61,7 +73,7 @@ else
<td> <td>
<button type="button" class="btn btn-sm btn-outline-danger" <button type="button" class="btn btn-sm btn-outline-danger"
disabled="@IsBusy" disabled="@IsBusy"
@onclick="() => KillWorkerAsync(worker.SessionId)"> @onclick="() => RequestKill(worker.SessionId)">
Kill Kill
</button> </button>
</td> </td>
@@ -93,13 +105,34 @@ else
CanManage = SessionAdminService.CanManage(authenticationState.User); CanManage = SessionAdminService.CanManage(authenticationState.User);
} }
private async Task KillWorkerAsync(string sessionId) private string? PendingSessionId { get; set; }
private void RequestKill(string sessionId)
{ {
if (IsBusy) if (IsBusy)
{ {
return; 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; IsBusy = true;
try try
{ {
@@ -115,6 +148,7 @@ else
finally finally
{ {
IsBusy = false; 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; }
}