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:
+64
-13
@@ -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; }
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user