Add fleet action dialog with server selection
This commit is contained in:
@@ -174,7 +174,7 @@
|
||||
<div class="server-content">
|
||||
<div class="server-heading"><strong><HistoricalServerIdentity name={server.server} deletedAt={server.server_deleted_at} /></strong><Badge variant={needsAttention(server) ? 'danger' : server.state === 'done' ? 'online' : launching(server) ? 'info' : 'neutral'}>{stateLabel(server)}</Badge></div>
|
||||
<p class:error={!!server.error}>{server.error ?? (server.state === 'queued' ? queueHint(server) : server.state === 'done' ? server.action === 'destroy' ? server.stage : 'Startup checks passed.' : server.stage)}</p>
|
||||
<div class="server-meta"><span>{server.action === 'destroy' ? 'Remove server' : server.port === null ? 'Create server · Port not assigned yet' : `Create server · Port ${server.port}`}</span><span>{needsAttention(server) ? 'Review & recover' : 'View operation'} <span aria-hidden="true">→</span></span></div>
|
||||
<div class="server-meta"><span>{server.action === 'destroy' ? 'Destroy server' : server.port === null ? 'Create server · Port not assigned yet' : `Create server · Port ${server.port}`}</span><span>{needsAttention(server) ? 'Review & recover' : 'View operation'} <span aria-hidden="true">→</span></span></div>
|
||||
</div>
|
||||
</a>
|
||||
{#if server.cancellation}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import Badge from '$ui/components/primitives/Badge.svelte';
|
||||
import Button from '$ui/components/primitives/Button.svelte';
|
||||
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
|
||||
import HousePartyDialog from '$ui/components/HousePartyDialog.svelte';
|
||||
import FleetActionsDialog from '$ui/components/FleetActionsDialog.svelte';
|
||||
import Freshness from '$ui/components/Freshness.svelte';
|
||||
import RelativeTime from '$ui/components/RelativeTime.svelte';
|
||||
|
||||
@@ -35,8 +35,7 @@
|
||||
let historyRefresh = $state(0);
|
||||
let loading = $state(true);
|
||||
let error = $state('');
|
||||
let housePartyOpen = $state(false);
|
||||
let housePartyNotice = $state('');
|
||||
let fleetNotice = $state('');
|
||||
let tab = $state('active');
|
||||
let limit = $state(25);
|
||||
let offset = $state(0);
|
||||
@@ -75,7 +74,6 @@
|
||||
if (!await confirmAction({ title: bulk ? `Cancel all ${ids.length} active operations?` : 'Cancel operation?',
|
||||
message: `${bulk ? `This affects all ${ids.length} cancellable active operations across the fleet, regardless of the current filters.` : `Cancel ${actionLabels[rows[0].action].toLowerCase()} on ${serverTitle(rows[0].server)}.`} Cancelling stops remaining work and health checks. Changes already made are not undone; commands already delivered to a host may still finish.`,
|
||||
confirmLabel: bulk ? 'Cancel all operations' : 'Cancel operation', danger: true })) return;
|
||||
fleetActionsOpen = false;
|
||||
cancelling = true; cancellationNotice = ''; let cancelled = 0; const unavailable: string[] = [];
|
||||
for (const id of ids) {
|
||||
try { const response = await fetch(`/api/provisioning/${encodeURIComponent(id)}/cancel`, { method: 'POST' }); if (response.ok) cancelled++; else unavailable.push(id); }
|
||||
@@ -168,10 +166,7 @@
|
||||
<SectionHeader title="Provisioning">
|
||||
{#snippet actions()}
|
||||
<Button onclick={() => lifecycleOpen = true}>Server actions</Button>
|
||||
<details class="fleet-actions" bind:open={fleetActionsOpen}><summary>Fleet actions</summary><div class="fleet-menu">
|
||||
{#if active.some(row => row.canCancel)}<Button disabled={cancelling} onclick={() => cancelRequests(active.filter(row => row.canCancel && !row.server_deleted_at), true)}>Cancel all active operations</Button>{/if}
|
||||
<Button variant="danger" onclick={() => { fleetActionsOpen = false; housePartyOpen = true; }}>Remove fleet servers…</Button>
|
||||
</div></details>
|
||||
<Button onclick={() => fleetActionsOpen = true}>Fleet actions</Button>
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
{#if $page.url.searchParams.has('returnTo')}<BackLink fallback="/" />{/if}
|
||||
@@ -184,8 +179,8 @@
|
||||
{#if filtered}<Button variant="ghost" onclick={clearFilters}>Clear filters</Button>{/if}
|
||||
</div>
|
||||
<LifecycleDialog bind:open={lifecycleOpen} />
|
||||
<HousePartyDialog bind:open={housePartyOpen} onqueued={count => { housePartyNotice = `Removal queued for ${count} server${count === 1 ? '' : 's'}.`; tab = 'active'; clearFilters(); void load(); }} />
|
||||
{#if housePartyNotice}<p role="status">{housePartyNotice}</p>{/if}
|
||||
<FleetActionsDialog bind:open={fleetActionsOpen} onqueued={(count, action) => { fleetNotice = `${actionLabels[action]} queued for ${count} server${count === 1 ? '' : 's'}.`; tab = 'active'; clearFilters(); void load(); }} />
|
||||
{#if fleetNotice}<p role="status">{fleetNotice}</p>{/if}
|
||||
{#if cancellationNotice}<p role="status">{cancellationNotice}</p>{/if}
|
||||
<div id="deployment-panel" role="tabpanel" aria-labelledby={"deployment-tabs-" + tab} class="panels">
|
||||
{#if error && tab === 'active'}<p role="alert" class="error">{error} {#if updatedAt}Showing the last loaded operations.{/if}</p>{/if}
|
||||
@@ -193,6 +188,7 @@
|
||||
{#if tab === 'active'}<p class="queue-summary"><strong>{filtered ? `${visibleActive.length} of ${active.length}` : active.length} active</strong>{#if queueSummary}<span> · {queueSummary}</span>{/if}</p>
|
||||
{:else}<h2>History <span>{historyTotal}</span></h2>{/if}
|
||||
<div class="queue-tools">
|
||||
{#if tab === 'active' && active.some(row => row.canCancel)}<Button size="sm" disabled={cancelling} onclick={() => cancelRequests(active.filter(row => row.canCancel && !row.server_deleted_at), true)}>Cancel all active operations</Button>{/if}
|
||||
{#if needsReview}<Button size="sm" variant="ghost" onclick={reviewAttention}>Needs attention ({needsReview})</Button>{/if}
|
||||
{#if tab === 'active'}<Freshness {updatedAt} error={error ? 'Operation updates interrupted.' : ''} intervalLabel="Updates every 5 seconds" onRetry={() => void load()} retryDisabled={refreshing} />
|
||||
{:else}<Freshness updatedAt={historyUpdatedAt} error={historyError ? 'History updates interrupted.' : ''} intervalLabel="Updates every 15 seconds" onRetry={() => historyRefresh++} retryDisabled={historyLoading} />{/if}
|
||||
@@ -263,6 +259,5 @@
|
||||
.detail-row { background:var(--surface); }.operation-details { display:grid; gap:16px; line-height:1.6; overflow-wrap:anywhere; }
|
||||
dl { display:flex; flex-wrap:wrap; gap:12px 32px; margin:0; }dt { color:var(--text-tertiary); font-size:var(--text-xs); }dd { margin:0; }.mono { font-family:var(--font-mono); }
|
||||
.detail-links { display:flex; align-items:center; flex-wrap:wrap; gap:16px; }.error { color:var(--state-danger); }.warning { color:var(--state-warning); }
|
||||
.fleet-actions { position:relative; font-size:var(--text-sm); }.fleet-actions summary { padding:10px; cursor:pointer; }.fleet-menu { position:absolute; right:0; top:100%; z-index:10; display:grid; gap:8px; padding:12px; background:var(--surface-overlay); border:1px solid var(--border-strong); border-radius:var(--radius-md); min-width:250px; box-shadow:var(--shadow-raised); }
|
||||
@media(max-width:720px) { table { min-width:820px; }.search { flex-basis:100%; }th,td { padding:12px 8px; }.fleet-menu { right:auto; left:0; } }
|
||||
@media(max-width:720px) { table { min-width:820px; }.search { flex-basis:100%; }th,td { padding:12px 8px; }}
|
||||
</style>
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
{#snippet actions()}
|
||||
{#if canOperate}
|
||||
{#if primaryAction}<Button onclick={() => openAction(primaryAction)}>{actionLabels[primaryAction]} server</Button>{/if}
|
||||
<details class="server-actions"><summary>More actions</summary><div class="action-menu">{#each availableActions.filter(action => action !== primaryAction) as action}<Button onclick={() => openAction(action)}>{actionLabels[action]} server</Button>{/each}<Button variant="danger" onclick={() => destroyOpen = true}>Remove server</Button></div></details>
|
||||
<details class="server-actions"><summary>More actions</summary><div class="action-menu">{#each availableActions.filter(action => action !== primaryAction) as action}<Button onclick={() => openAction(action)}>{actionLabels[action]} server</Button>{/each}<Button variant="danger" onclick={() => destroyOpen = true}>Destroy server</Button></div></details>
|
||||
{/if}
|
||||
{/snippet}
|
||||
</SectionHeader>
|
||||
|
||||
Reference in New Issue
Block a user