fix: enforce workspace deletion blockers

This commit is contained in:
2026-09-06 07:54:52 +09:00
parent e3e9e83bc1
commit ab4fb4c1ee
9 changed files with 261 additions and 186 deletions
@@ -91,7 +91,6 @@ export type WorkspaceDeletionPreflightResponse = {
*/
expected_revision: string;
can_delete: boolean;
force_delete_dirty_workdirs_available: boolean;
resources: WorkspaceDeletionResourceCounts;
blockers: Array<WorkspaceDeletionBlocker>;
};
@@ -100,7 +99,6 @@ export type WorkspaceDeletionRequest = {
operation_id: string;
expected_revision: string;
confirmation: string;
force_delete_dirty_workdirs: boolean;
};
export type WorkspaceDeletionOperationResponse = {
@@ -108,7 +106,6 @@ export type WorkspaceDeletionOperationResponse = {
workspace_id: string;
display_name: string;
state: WorkspaceDeletionState;
force_delete_dirty_workdirs: boolean;
resources: WorkspaceDeletionResourceCounts;
child_operation_ids: Array<string>;
blockers: Array<WorkspaceDeletionBlocker>;
@@ -665,7 +665,6 @@ export function parseWorkspaceDeletionPreflightResponse(
"display_name",
"expected_revision",
"can_delete",
"force_delete_dirty_workdirs_available",
"resources",
"blockers",
], "Workspace deletion preflight");
@@ -686,10 +685,6 @@ export function parseWorkspaceDeletionPreflightResponse(
item.can_delete,
"Workspace deletion preflight.can_delete",
),
force_delete_dirty_workdirs_available: boolean(
item.force_delete_dirty_workdirs_available,
"Workspace deletion preflight.force_delete_dirty_workdirs_available",
),
resources: deletionResourceCounts(
item.resources,
"Workspace deletion preflight.resources",
@@ -713,7 +708,6 @@ export function parseWorkspaceDeletionOperationResponse(
"workspace_id",
"display_name",
"state",
"force_delete_dirty_workdirs",
"resources",
"child_operation_ids",
"blockers",
@@ -736,10 +730,6 @@ export function parseWorkspaceDeletionOperationResponse(
"Workspace deletion operation.display_name",
),
state: deletionState(item.state, "Workspace deletion operation.state"),
force_delete_dirty_workdirs: boolean(
item.force_delete_dirty_workdirs,
"Workspace deletion operation.force_delete_dirty_workdirs",
),
resources: deletionResourceCounts(
item.resources,
"Workspace deletion operation.resources",
@@ -32,7 +32,6 @@
let deletionLoading = $state(false);
let deletionSubmitting = $state(false);
let deletionConfirmation = $state('');
let forceDeleteDirtyWorkdirs = $state(false);
let deletionPreflight = $state<WorkspaceDeletionPreflightResponse | null>(null);
let deletionOperation = $state<WorkspaceDeletionOperationResponse | null>(null);
let deletionOperationId = $state('');
@@ -95,7 +94,6 @@
deletionOperation = null;
deletionOperationId = crypto.randomUUID();
deletionConfirmation = '';
forceDeleteDirtyWorkdirs = false;
try {
deletionPreflight = await preflightWorkspaceDeletion(workspaceId);
} catch (err) {
@@ -114,7 +112,6 @@
operation_id: deletionOperationId,
expected_revision: deletionPreflight.expected_revision,
confirmation: deletionConfirmation,
force_delete_dirty_workdirs: forceDeleteDirtyWorkdirs,
});
deletionOperation = operation;
while (operation.state === 'queued' || operation.state === 'running') {
@@ -207,15 +204,9 @@
<p class="status-message error">{blocker.message}</p>
{/each}
<label>
<span>Type <strong>delete {deletionPreflight.display_name}</strong> to confirm</span>
<span>Type <strong>{deletionPreflight.display_name}</strong> to confirm</span>
<input bind:value={deletionConfirmation} autocomplete="off" />
</label>
{#if deletionPreflight.force_delete_dirty_workdirs_available}
<label class="force-delete-option">
<input type="checkbox" bind:checked={forceDeleteDirtyWorkdirs} disabled={deletionOperation !== null} />
<span>Force-delete dirty Workdirs after reviewing the impact</span>
</label>
{/if}
{/if}
{#if deletionOperation}
<p class="status-message">Deletion state: {deletionOperation.state}</p>
@@ -230,7 +221,7 @@
class="danger-button"
type="button"
onclick={() => void deleteWorkspace()}
disabled={deletionSubmitting || !deletionPreflight?.can_delete || deletionConfirmation !== `delete ${deletionPreflight?.display_name ?? ''}`}
disabled={deletionSubmitting || !deletionPreflight?.can_delete || deletionConfirmation !== (deletionPreflight?.display_name ?? '')}
>{deletionSubmitting ? 'Deleting…' : 'Delete Workspace'}</button>
</div>
</div>
@@ -244,6 +235,5 @@
.modal-backdrop { position: fixed; inset: 0; z-index: 100; display: grid; place-items: center; padding: var(--space-4); background: rgb(0 0 0 / 0.55); }
.deletion-dialog { width: min(34rem, 100%); max-height: calc(100vh - 2rem); overflow: auto; padding: var(--space-5); background: var(--color-surface, white); border: 1px solid var(--color-border); }
.deletion-dialog label { display: grid; gap: var(--space-2); margin-block: var(--space-4); }
.force-delete-option { grid-template-columns: auto 1fr !important; align-items: start; }
.dialog-actions { display: flex; justify-content: flex-end; gap: var(--space-2); margin-top: var(--space-5); }
</style>
+1 -4
View File
@@ -117,7 +117,6 @@ Deno.test("Workspace deletion DTOs fail closed and preserve durable operation st
display_name: "Alpha",
expected_revision: "2026-01-01T00:00:00Z",
can_delete: true,
force_delete_dirty_workdirs_available: true,
resources: {
workers: 2,
workdirs: 1,
@@ -137,7 +136,6 @@ Deno.test("Workspace deletion DTOs fail closed and preserve durable operation st
workspace_id: "workspace-a",
display_name: "Alpha",
state: "blocked",
force_delete_dirty_workdirs: false,
resources: preflight.resources,
child_operation_ids: ["worker-remove:arcadia/7"],
blockers: [{
@@ -185,8 +183,7 @@ Deno.test("Workspace settings exposes owner-gated typed destructive confirmation
"permissions.delete_workspace",
"preflightWorkspaceDeletion",
"startWorkspaceDeletion",
"delete ${",
"force_delete_dirty_workdirs",
"deletionConfirmation",
"disposeWorkspaceMultiplexer(workspaceId)",
]
) {