diff --git a/canvas/src/components/tabs/ContainerConfigTab.tsx b/canvas/src/components/tabs/ContainerConfigTab.tsx index f8a3259c0..24813e1a8 100644 --- a/canvas/src/components/tabs/ContainerConfigTab.tsx +++ b/canvas/src/components/tabs/ContainerConfigTab.tsx @@ -29,8 +29,15 @@ type FormState = { displayMode: string; displayProtocol: string; resolution: string; + dataPersistence: string; // "" (auto) | "persist" | "ephemeral" — internal#734 }; +// internal#734: per-workspace durable-data choice. "" = auto (desktop-control +// keeps data, others follow the org default). Human labels for the selector. +const DATA_PERSISTENCE_OPTIONS = ["", "persist", "ephemeral"]; +const dataPersistenceLabel = (v: string): string => + v === "persist" ? "Always keep (persist)" : v === "ephemeral" ? "Don't keep (ephemeral)" : "Auto"; + export function ContainerConfigTab({ workspaceId, data }: Props) { const runtime = data.runtime; const instanceType = data.compute?.instance_type; @@ -39,9 +46,10 @@ export function ContainerConfigTab({ workspaceId, data }: Props) { const displayProtocol = data.compute?.display?.protocol; const displayWidth = data.compute?.display?.width; const displayHeight = data.compute?.display?.height; + const dataPersistence = data.compute?.data_persistence; const initial = useMemo( - () => formFromData({ runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight }), - [runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight], + () => formFromData({ runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence }), + [runtime, instanceType, rootGB, displayMode, displayProtocol, displayWidth, displayHeight, dataPersistence], ); const [form, setForm] = useState(initial); const [saving, setSaving] = useState(false); @@ -84,6 +92,8 @@ export function ContainerConfigTab({ workspaceId, data }: Props) { display: form.displayEnabled ? { mode: form.displayMode, protocol: form.displayProtocol, width, height } : { mode: "none" }, + // internal#734: omit when "auto" so the wire/default behavior is unchanged. + ...(form.dataPersistence ? { data_persistence: form.dataPersistence } : {}), }; const resp = await api.patch<{ needs_restart?: boolean }>(`/workspaces/${workspaceId}`, { @@ -176,6 +186,18 @@ export function ContainerConfigTab({ workspaceId, data }: Props) { onChange={(resolution) => setForm((s) => ({ ...s, resolution }))} /> )} + setForm((s) => ({ ...s, dataPersistence }))} + /> +

+ Whether this workspace's data survives a restart/recreate. Auto keeps it for + browser (desktop) workspaces; Ephemeral never keeps it (privacy). +

@@ -231,6 +253,7 @@ function formFromData(data: { displayProtocol?: string; displayWidth?: number; displayHeight?: number; + dataPersistence?: string; }): FormState { const width = data.displayWidth ?? 1920; const height = data.displayHeight ?? 1080; @@ -243,6 +266,7 @@ function formFromData(data: { displayMode: data.displayMode && data.displayMode !== "none" ? data.displayMode : "desktop-control", displayProtocol: data.displayProtocol || "novnc", resolution, + dataPersistence: data.dataPersistence || "", }; } diff --git a/canvas/src/components/tabs/DetailsTab.tsx b/canvas/src/components/tabs/DetailsTab.tsx index 197015ecb..d39cb554d 100644 --- a/canvas/src/components/tabs/DetailsTab.tsx +++ b/canvas/src/components/tabs/DetailsTab.tsx @@ -29,6 +29,7 @@ export function DetailsTab({ workspaceId, data }: Props) { const [peers, setPeers] = useState([]); const [saving, setSaving] = useState(false); const [confirmDelete, setConfirmDelete] = useState(false); + const [eraseData, setEraseData] = useState(false); // internal#734: erase saved data on delete const [peersError, setPeersError] = useState(null); const [saveError, setSaveError] = useState(null); const [deleteError, setDeleteError] = useState(null); @@ -93,7 +94,10 @@ export function DetailsTab({ workspaceId, data }: Props) { const handleDelete = async () => { setDeleteError(null); try { - await api.del(`/workspaces/${workspaceId}?confirm=true`, { + // internal#734: erase_data=true asks the server to prune this workspace's + // durable data volume (cookies / downloads / memory). Default off keeps it + // for the orphan-sweeper grace. + await api.del(`/workspaces/${workspaceId}?confirm=true${eraseData ? "&erase_data=true" : ""}`, { headers: { "X-Confirm-Name": name }, }); // Mirror the server-side cascade — drop the row + every @@ -323,6 +327,19 @@ export function DetailsTab({ workspaceId, data }: Props) {

Confirm deletion

+