workspace: finalize workdir deletion flow
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
<script lang="ts">
|
||||
import { dismissWorkspaceAlert, workspaceAlerts, type WorkspaceAlertLevel } from './store';
|
||||
|
||||
function label(level: WorkspaceAlertLevel): string {
|
||||
switch (level) {
|
||||
case 'success': return 'Success';
|
||||
case 'info': return 'Info';
|
||||
case 'warning': return 'Warning';
|
||||
case 'error': return 'Error';
|
||||
case 'system': return 'System';
|
||||
case 'debug': return 'Debug';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="workspace-alerts" aria-live="polite" aria-atomic="false">
|
||||
{#each $workspaceAlerts as alert (alert.id)}
|
||||
<article class={`workspace-alert ${alert.level}`} role={alert.level === 'error' ? 'alert' : 'status'}>
|
||||
<div class="workspace-alert-icon" aria-hidden="true">
|
||||
{#if alert.level === 'success'}
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="m9 12 2 2 4-4" /></svg>
|
||||
{:else if alert.level === 'info'}
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><path d="M12 16v-4" /><path d="M12 8h.01" /></svg>
|
||||
{:else if alert.level === 'warning' || alert.level === 'error'}
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" /><line x1="12" x2="12" y1="8" y2="12" /><line x1="12" x2="12.01" y1="16" y2="16" /></svg>
|
||||
{:else if alert.level === 'system'}
|
||||
<svg viewBox="0 0 24 24"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" /><circle cx="12" cy="12" r="3" /></svg>
|
||||
{:else}
|
||||
<svg viewBox="0 0 24 24"><path d="m8 2 1.88 1.88" /><path d="M14.12 3.88 16 2" /><path d="M9 7.13v-1a3.003 3.003 0 1 1 6 0v1" /><path d="M12 20c-3.3 0-6-2.7-6-6v-3a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v3c0 3.3-2.7 6-6 6" /><path d="M12 20v-9" /><path d="M6.53 9C4.6 8.8 3 7.1 3 5" /><path d="M6 13H2" /><path d="M3 21c0-2.1 1.7-3.9 3.8-4" /><path d="M20.97 5c0 2.1-1.6 3.8-3.5 4" /><path d="M22 13h-4" /><path d="M17.2 17c2.1.1 3.8 1.9 3.8 4" /></svg>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="workspace-alert-body">
|
||||
<strong>{alert.title ?? label(alert.level)}</strong>
|
||||
<p>{alert.message}</p>
|
||||
</div>
|
||||
<button type="button" class="workspace-alert-dismiss" aria-label="Dismiss alert" onclick={() => dismissWorkspaceAlert(alert.id)}>×</button>
|
||||
</article>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.workspace-alerts {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
width: min(28rem, calc(100vw - 2rem));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.workspace-alert {
|
||||
--alert-color: var(--text);
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
gap: 0.7rem;
|
||||
align-items: start;
|
||||
padding: 0.8rem 0.85rem;
|
||||
border: 1px solid color-mix(in srgb, var(--alert-color) 45%, var(--line));
|
||||
border-radius: 0.85rem;
|
||||
background: color-mix(in srgb, var(--alert-color) 12%, var(--bg-raised) 88%);
|
||||
color: var(--text);
|
||||
box-shadow: 0 18px 50px rgb(0 0 0 / 0.28);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.workspace-alert.success { --alert-color: oklch(70% 0.18 145); }
|
||||
.workspace-alert.info { --alert-color: oklch(68% 0.16 245); }
|
||||
.workspace-alert.warning { --alert-color: oklch(78% 0.18 85); }
|
||||
.workspace-alert.error { --alert-color: oklch(62% 0.2 30); }
|
||||
.workspace-alert.system { --alert-color: oklch(96% 0.01 260); }
|
||||
.workspace-alert.debug { --alert-color: oklch(68% 0.18 305); }
|
||||
|
||||
.workspace-alert-icon {
|
||||
color: var(--alert-color);
|
||||
margin-top: 0.1rem;
|
||||
}
|
||||
|
||||
.workspace-alert-icon svg {
|
||||
display: block;
|
||||
width: 1.15rem;
|
||||
height: 1.15rem;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.workspace-alert-body {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.workspace-alert-body strong {
|
||||
display: block;
|
||||
margin-bottom: 0.15rem;
|
||||
color: var(--alert-color);
|
||||
font-size: 0.82rem;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workspace-alert-body p {
|
||||
margin: 0;
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--text);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.workspace-alert-dismiss {
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.workspace-alert-dismiss:hover,
|
||||
.workspace-alert-dismiss:focus-visible {
|
||||
background: color-mix(in srgb, var(--alert-color) 18%, transparent);
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,61 @@
|
||||
export type WorkspaceAlertLevel = "success" | "info" | "warning" | "error" | "system" | "debug";
|
||||
|
||||
export type WorkspaceAlert = {
|
||||
id: string;
|
||||
level: WorkspaceAlertLevel;
|
||||
title?: string;
|
||||
message: string;
|
||||
createdAt: number;
|
||||
};
|
||||
|
||||
type AlertSubscriber = (alerts: WorkspaceAlert[]) => void;
|
||||
|
||||
let sequence = 0;
|
||||
let alerts: WorkspaceAlert[] = [];
|
||||
const subscribers = new Set<AlertSubscriber>();
|
||||
|
||||
function emit(): void {
|
||||
const snapshot = alerts.slice();
|
||||
for (const subscriber of subscribers) subscriber(snapshot);
|
||||
}
|
||||
|
||||
export const workspaceAlerts = {
|
||||
subscribe(subscriber: AlertSubscriber): () => void {
|
||||
subscribers.add(subscriber);
|
||||
subscriber(alerts.slice());
|
||||
return () => subscribers.delete(subscriber);
|
||||
},
|
||||
};
|
||||
|
||||
export function pushWorkspaceAlert(
|
||||
level: WorkspaceAlertLevel,
|
||||
message: string,
|
||||
options: { title?: string; id?: string } = {},
|
||||
): string {
|
||||
const id = options.id ?? `${Date.now().toString(36)}-${(sequence++).toString(36)}`;
|
||||
alerts = [
|
||||
...alerts.filter((alert) => alert.id !== id),
|
||||
{
|
||||
id,
|
||||
level,
|
||||
title: options.title,
|
||||
message,
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
].slice(-8);
|
||||
emit();
|
||||
return id;
|
||||
}
|
||||
|
||||
export function dismissWorkspaceAlert(id: string): void {
|
||||
const next = alerts.filter((alert) => alert.id !== id);
|
||||
if (next.length === alerts.length) return;
|
||||
alerts = next;
|
||||
emit();
|
||||
}
|
||||
|
||||
export function clearWorkspaceAlerts(): void {
|
||||
if (alerts.length === 0) return;
|
||||
alerts = [];
|
||||
emit();
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
import WorkspaceAlerts from '$lib/workspace-alerts/WorkspaceAlerts.svelte';
|
||||
import WorkspaceSidebar from '$lib/workspace-sidebar/WorkspaceSidebar.svelte';
|
||||
import '../app.css';
|
||||
import type { LayoutProps } from './$types';
|
||||
@@ -8,6 +9,8 @@
|
||||
let sidebarCollapsed = $state(false);
|
||||
</script>
|
||||
|
||||
<WorkspaceAlerts />
|
||||
|
||||
<div class:sidebar-collapsed={sidebarCollapsed} class="workspace-layout">
|
||||
<WorkspaceSidebar
|
||||
workspace={data.workspace}
|
||||
|
||||
+106
-29
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { pushWorkspaceAlert } from '$lib/workspace-alerts/store';
|
||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||
import type {
|
||||
CleanupWorkdirCandidate,
|
||||
@@ -9,9 +10,9 @@
|
||||
import type { PageProps } from './$types';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let cleanupStatus = $state<string | null>(null);
|
||||
let cleanupBusyTarget = $state<string | null>(null);
|
||||
let cleanupPlan = $state<RuntimeCleanupPlanResponse | null>(null);
|
||||
let workdirs = $state<WorkingDirectorySummary[]>([]);
|
||||
let runtimeLabel = $derived(
|
||||
data.runtimes?.items.find((runtime) => runtime.runtime_id === data.runtimeId)?.label ?? data.runtimeId,
|
||||
);
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
$effect(() => {
|
||||
cleanupPlan = data.cleanupPlan ?? null;
|
||||
workdirs = data.workdirs?.items ?? [];
|
||||
});
|
||||
|
||||
function commitLabel(workdir: WorkingDirectorySummary): string {
|
||||
@@ -29,26 +31,35 @@
|
||||
return workdir.requested_selector ?? 'HEAD';
|
||||
}
|
||||
|
||||
function cleanupLabel(candidate: CleanupWorkdirCandidate): string {
|
||||
if (candidate.action === 'workdir_dirty_discard') {
|
||||
return candidate.cleanliness === 'dirty' ? 'Discard' : 'Discard unknown';
|
||||
}
|
||||
if (candidate.action === 'workdir_record_delete') return 'Delete record';
|
||||
return 'Clean up';
|
||||
}
|
||||
|
||||
function cleanupCandidate(workdir: WorkingDirectorySummary): CleanupWorkdirCandidate | undefined {
|
||||
return cleanupCandidates.find((candidate) => candidate.workdir_id === workdir.working_directory_id);
|
||||
}
|
||||
|
||||
async function executeWorkdirCleanup(candidate: CleanupWorkdirCandidate): Promise<void> {
|
||||
if (!cleanupPlan) return;
|
||||
if (candidate.action === 'workdir_dirty_discard') {
|
||||
const confirmed = window.confirm(`${cleanupLabel(candidate)} ${candidate.workdir_id}? This explicitly discards the Workdir contents.`);
|
||||
if (!confirmed) return;
|
||||
function isDeleteDisabled(candidate: CleanupWorkdirCandidate): boolean {
|
||||
return Boolean(candidate.blocking_reason) || candidate.action === 'workdir_dirty_discard' || cleanupBusyTarget !== null;
|
||||
}
|
||||
|
||||
function errorMessage(payload: unknown, fallback: string): string {
|
||||
if (payload && typeof payload === 'object') {
|
||||
if ('message' in payload && typeof payload.message === 'string') return payload.message;
|
||||
if ('error' in payload) {
|
||||
const error = payload.error;
|
||||
if (typeof error === 'string') return error;
|
||||
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') return error.message;
|
||||
}
|
||||
if ('diagnostics' in payload && Array.isArray(payload.diagnostics)) {
|
||||
const diagnostic = payload.diagnostics.find(
|
||||
(entry): entry is { message: string } => Boolean(entry) && typeof entry === 'object' && 'message' in entry && typeof entry.message === 'string',
|
||||
);
|
||||
if (diagnostic) return diagnostic.message;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function deleteWorkdir(workdir: WorkingDirectorySummary, candidate: CleanupWorkdirCandidate): Promise<void> {
|
||||
if (!cleanupPlan || isDeleteDisabled(candidate)) return;
|
||||
cleanupBusyTarget = candidate.target_id;
|
||||
cleanupStatus = null;
|
||||
try {
|
||||
const response = await fetch(
|
||||
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(data.runtimeId)}/cleanup-executions`),
|
||||
@@ -60,16 +71,26 @@
|
||||
expected_plan_digest: cleanupPlan.digest,
|
||||
worker_target_ids: [],
|
||||
workdir_target_ids: [candidate.target_id],
|
||||
confirm_dirty_discard_target_ids: candidate.action === 'workdir_dirty_discard' ? [candidate.target_id] : [],
|
||||
confirm_dirty_discard_target_ids: [],
|
||||
}),
|
||||
},
|
||||
);
|
||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | { message?: string; error?: string } | null;
|
||||
if (!response.ok) throw new Error(payload && 'message' in payload ? (payload.message ?? payload.error) : response.statusText);
|
||||
if (payload && 'plan_after' in payload) cleanupPlan = payload.plan_after;
|
||||
cleanupStatus = `Executed cleanup for ${candidate.workdir_id}. Refresh to see the latest Workdir list.`;
|
||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
|
||||
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
||||
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
|
||||
cleanupPlan = (payload as RuntimeCleanupExecutionResponse).plan_after;
|
||||
}
|
||||
const result = payload && typeof payload === 'object' && 'results' in payload
|
||||
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
|
||||
: undefined;
|
||||
if (!result || result.status !== 'deleted') {
|
||||
throw new Error(result?.message ?? 'Runtime did not delete the selected Workdir');
|
||||
}
|
||||
workdirs = workdirs.filter((item) => item.working_directory_id !== workdir.working_directory_id);
|
||||
} catch (error) {
|
||||
cleanupStatus = error instanceof Error ? error.message : 'Workdir cleanup failed';
|
||||
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Workdir deletion failed', {
|
||||
title: 'Workdir deletion failed',
|
||||
});
|
||||
} finally {
|
||||
cleanupBusyTarget = null;
|
||||
}
|
||||
@@ -88,7 +109,6 @@
|
||||
<h1 id="workdirs-heading">Workdirs</h1>
|
||||
<p>Workdirs owned by <code>{data.runtimeId}</code>.</p>
|
||||
{#if data.cleanupPlanError}<p class="section-state error">{data.cleanupPlanError}</p>{/if}
|
||||
{#if cleanupStatus}<p>{cleanupStatus}</p>{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
@@ -96,7 +116,7 @@
|
||||
<p class="section-state error">{data.workdirsError}</p>
|
||||
{:else if !data.workdirs}
|
||||
<p class="section-state">Loading workdirs…</p>
|
||||
{:else if data.workdirs.items.length === 0}
|
||||
{:else if workdirs.length === 0}
|
||||
<p class="section-state">No workdirs are visible for this Runtime.</p>
|
||||
{:else}
|
||||
<div class="table-wrap">
|
||||
@@ -113,7 +133,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.workdirs.items as workdir}
|
||||
{#each workdirs as workdir}
|
||||
{@const cleanup = cleanupCandidate(workdir)}
|
||||
<tr>
|
||||
<td><code>{workdir.working_directory_id}</code></td>
|
||||
@@ -125,14 +145,19 @@
|
||||
<td>
|
||||
{#if cleanup}
|
||||
<button
|
||||
class="icon-action danger"
|
||||
type="button"
|
||||
disabled={!!cleanup.blocking_reason || cleanupBusyTarget === cleanup.target_id}
|
||||
title={cleanup.blocking_reason ?? cleanup.reason}
|
||||
onclick={() => executeWorkdirCleanup(cleanup)}
|
||||
disabled={isDeleteDisabled(cleanup)}
|
||||
aria-label={`Delete ${workdir.working_directory_id}`}
|
||||
title={cleanup.action === 'workdir_dirty_discard' ? 'Dirty Workdirs must be cleaned before deletion' : (cleanup.blocking_reason ?? cleanup.reason)}
|
||||
onclick={() => deleteWorkdir(workdir, cleanup)}
|
||||
>
|
||||
{cleanupBusyTarget === cleanup.target_id ? 'Executing…' : cleanupLabel(cleanup)}
|
||||
{#if cleanupBusyTarget === cleanup.target_id}
|
||||
<span class="spinner" aria-hidden="true"></span>
|
||||
{:else}
|
||||
<svg class="action-icon" aria-hidden="true" viewBox="0 0 24 24"><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" /><path d="M3 6h18" /><path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" /></svg>
|
||||
{/if}
|
||||
</button>
|
||||
{#if cleanup.blocking_reason}<small class="error">{cleanup.blocking_reason}</small>{/if}
|
||||
{:else}
|
||||
<span class="muted">—</span>
|
||||
{/if}
|
||||
@@ -144,3 +169,55 @@
|
||||
</div>
|
||||
{/if}
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.icon-action {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon-action.danger:hover:not(:disabled),
|
||||
.icon-action.danger:focus-visible:not(:disabled) {
|
||||
border-color: var(--danger, oklch(60% 0.18 30));
|
||||
color: var(--danger, oklch(60% 0.18 30));
|
||||
}
|
||||
|
||||
.icon-action:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
stroke-linejoin: round;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border: 2px solid currentColor;
|
||||
border-right-color: transparent;
|
||||
border-radius: 999px;
|
||||
animation: workdir-action-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes workdir-action-spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { pushWorkspaceAlert } from '$lib/workspace-alerts/store';
|
||||
import { workspaceApiPath } from '$lib/workspace-api/http';
|
||||
import { workerConsoleHref } from '$lib/workspace-console/model';
|
||||
import { canOpenWorkerConsole } from '$lib/workspace-sidebar/workers';
|
||||
@@ -8,12 +9,13 @@
|
||||
type WorkerActionKind = 'pin' | 'delete';
|
||||
|
||||
let { data }: PageProps = $props();
|
||||
let statusMessage = $state<string | null>(null);
|
||||
let cleanupPlans = $state<Record<string, RuntimeCleanupPlanResponse>>({});
|
||||
let workers = $state<Worker[]>([]);
|
||||
let busyAction = $state<{ workerKey: string; kind: WorkerActionKind } | null>(null);
|
||||
|
||||
$effect(() => {
|
||||
cleanupPlans = data.cleanupPlans;
|
||||
workers = data.workers?.items ?? [];
|
||||
});
|
||||
|
||||
function workerKey(worker: Worker): string {
|
||||
@@ -28,6 +30,24 @@
|
||||
return busyAction !== null;
|
||||
}
|
||||
|
||||
function errorMessage(payload: unknown, fallback: string): string {
|
||||
if (payload && typeof payload === 'object') {
|
||||
if ('message' in payload && typeof payload.message === 'string') return payload.message;
|
||||
if ('error' in payload) {
|
||||
const error = payload.error;
|
||||
if (typeof error === 'string') return error;
|
||||
if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') return error.message;
|
||||
}
|
||||
if ('diagnostics' in payload && Array.isArray(payload.diagnostics)) {
|
||||
const diagnostic = payload.diagnostics.find(
|
||||
(entry): entry is { message: string } => Boolean(entry) && typeof entry === 'object' && 'message' in entry && typeof entry.message === 'string',
|
||||
);
|
||||
if (diagnostic) return diagnostic.message;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
async function refreshCleanupPlan(runtimeId: string): Promise<void> {
|
||||
const response = await fetch(
|
||||
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(runtimeId)}/cleanup-plan`),
|
||||
@@ -40,7 +60,6 @@
|
||||
async function setPinned(worker: Worker, pinned: boolean): Promise<void> {
|
||||
if (busyAction) return;
|
||||
busyAction = { workerKey: workerKey(worker), kind: 'pin' };
|
||||
statusMessage = null;
|
||||
try {
|
||||
const response = await fetch(
|
||||
workspaceApiPath(
|
||||
@@ -51,13 +70,16 @@
|
||||
);
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok) {
|
||||
statusMessage = payload?.message ?? payload?.error ?? response.statusText;
|
||||
pushWorkspaceAlert('error', errorMessage(payload, response.statusText), { title: 'Worker pin failed' });
|
||||
return;
|
||||
}
|
||||
worker.pinned = Boolean(payload?.pinned);
|
||||
worker.retention_state = payload?.retention_state ?? (worker.pinned ? 'pinned' : 'normal');
|
||||
await refreshCleanupPlan(worker.runtime_id);
|
||||
statusMessage = `${worker.label} ${worker.pinned ? 'pinned' : 'unpinned'}.`;
|
||||
} catch (error) {
|
||||
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Worker pin failed', {
|
||||
title: 'Worker pin failed',
|
||||
});
|
||||
} finally {
|
||||
busyAction = null;
|
||||
}
|
||||
@@ -71,7 +93,6 @@
|
||||
|
||||
async function deleteWorker(worker: Worker, candidate: CleanupWorkerCandidate): Promise<void> {
|
||||
if (!cleanupPlans?.[worker.runtime_id] || busyAction) return;
|
||||
statusMessage = null;
|
||||
busyAction = { workerKey: workerKey(worker), kind: 'delete' };
|
||||
try {
|
||||
const plan = cleanupPlans[worker.runtime_id];
|
||||
@@ -89,19 +110,25 @@
|
||||
}),
|
||||
},
|
||||
);
|
||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | { message?: string; error?: string } | null;
|
||||
if (!response.ok) throw new Error(payload && 'message' in payload ? (payload.message ?? payload.error) : response.statusText);
|
||||
if (payload && 'plan_after' in payload) {
|
||||
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: payload.plan_after };
|
||||
const payload = (await response.json().catch(() => null)) as RuntimeCleanupExecutionResponse | unknown;
|
||||
if (!response.ok) throw new Error(errorMessage(payload, response.statusText));
|
||||
if (payload && typeof payload === 'object' && 'plan_after' in payload) {
|
||||
const execution = payload as RuntimeCleanupExecutionResponse;
|
||||
cleanupPlans = { ...cleanupPlans, [worker.runtime_id]: execution.plan_after };
|
||||
}
|
||||
if (data.workers) {
|
||||
data.workers.items = data.workers.items.filter(
|
||||
(item) => !(item.runtime_id === worker.runtime_id && item.worker_id === worker.worker_id),
|
||||
);
|
||||
const result = payload && typeof payload === 'object' && 'results' in payload
|
||||
? (payload as RuntimeCleanupExecutionResponse).results.find((entry) => entry.target_id === candidate.target_id)
|
||||
: undefined;
|
||||
if (!result || result.status !== 'deleted') {
|
||||
throw new Error(result?.message ?? 'Runtime did not delete the selected Worker');
|
||||
}
|
||||
statusMessage = `Deleted Worker ${worker.label}.`;
|
||||
workers = workers.filter(
|
||||
(item) => !(item.runtime_id === worker.runtime_id && item.worker_id === worker.worker_id),
|
||||
);
|
||||
} catch (error) {
|
||||
statusMessage = error instanceof Error ? error.message : 'Worker cleanup failed';
|
||||
pushWorkspaceAlert('error', error instanceof Error ? error.message : 'Worker deletion failed', {
|
||||
title: 'Worker deletion failed',
|
||||
});
|
||||
} finally {
|
||||
busyAction = null;
|
||||
}
|
||||
@@ -134,7 +161,6 @@
|
||||
<div>
|
||||
<h1 id="workers-heading">Workers</h1>
|
||||
<p>Workers running or persisted for this workspace. Pinning updates Backend retention.</p>
|
||||
{#if statusMessage}<p>{statusMessage}</p>{/if}
|
||||
</div>
|
||||
<a class="section-action" href={`/w/${data.workspaceId}/workers/new`}>New Worker</a>
|
||||
</header>
|
||||
@@ -143,7 +169,7 @@
|
||||
<p class="section-state error">{data.workersError}</p>
|
||||
{:else if !data.workers}
|
||||
<p class="section-state">Loading Workers…</p>
|
||||
{:else if data.workers.items.length === 0}
|
||||
{:else if workers.length === 0}
|
||||
<p class="section-state">No Workers are visible.</p>
|
||||
{:else}
|
||||
<div class="table-wrap workers-table-wrap">
|
||||
@@ -160,7 +186,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each data.workers.items as worker}
|
||||
{#each workers as worker}
|
||||
{@const cleanup = cleanupCandidate(worker)}
|
||||
{@const canDelete = cleanup && !cleanup.blocking_reason}
|
||||
{@const anyActionDisabled = actionsDisabled()}
|
||||
|
||||
Reference in New Issue
Block a user