diff --git a/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte b/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte index 2c10794646..123d8b03d3 100644 --- a/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte +++ b/frontend/src/lib/components/workspaceSettings/CreateWorkspaceInner.svelte @@ -26,16 +26,14 @@ import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte' import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte' import { AI_PROVIDERS } from '$lib/components/copilot/lib' - import { LoaderCircle, Check, X, Loader2 } from 'lucide-svelte' + import { LoaderCircle } from 'lucide-svelte' import PrefixedInput from '../PrefixedInput.svelte' import TextInput from '../text_input/TextInput.svelte' import { jobManager } from '$lib/services/JobManager' import Alert from '../common/alert/Alert.svelte' import { base } from '$lib/base' import Label from '../Label.svelte' - import Select from '../select/Select.svelte' - import { resource } from 'runed' - import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte' + import ForkDatatableSection from './ForkDatatableSection.svelte' interface Props { isFork?: boolean @@ -54,29 +52,7 @@ let codeCompletionEnabled = $state(true) let checking = $state(false) - let allDatatables = resource([], async () => - $workspaceStore - ? WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' }) - : undefined - ) - let datatableBehaviors: Record = - $state({}) - - type ForkStep = { - label: string - status: 'pending' | 'running' | 'done' | 'error' - error?: string - } - type DatatableCloneJob = { - name: string - resourceType: string - behavior: 'schema_only' | 'schema_and_data' - steps: ForkStep[] - } - let cloneModalOpen = $state(false) - let currentCloneJob: DatatableCloneJob | undefined = $state(undefined) - let cloneQueue: DatatableCloneJob[] = $state([]) - let cloneRunning = $state(false) + let forkDatatableSection: ReturnType | undefined = $state(undefined) let workspaceColor: string | undefined = $state(undefined) let colorEnabled = $state(false) @@ -221,60 +197,13 @@ return } - // Build clone queue for datatables that need cloning - const datatablesToClone = (allDatatables.current ?? []) - .filter((dt) => { - const behavior = datatableBehaviors[dt.name] ?? 'keep_original' - return behavior !== 'keep_original' - }) - .map((dt) => { - const behavior = datatableBehaviors[dt.name] as 'schema_only' | 'schema_and_data' - const isInstance = dt.resource_type === 'instance' - const newDbName = isInstance - ? `${prefixed_id.replace(/-/g, '_')}__${dt.name}` - : `${prefixed_id.replace(/-/g, '_')}__${dt.name}` - - const steps: ForkStep[] = isInstance - ? [ - { - label: `CREATE DATABASE "${newDbName}" + grant permissions`, - status: 'pending' - }, - { - label: `pg_dump → pg_import (${behavior === 'schema_only' ? 'schema only' : 'schema + data'})`, - status: 'pending' - } - ] - : [ - { - label: `CREATE DATABASE "${newDbName}" + pg_dump → pg_import (${behavior === 'schema_only' ? 'schema only' : 'schema + data'})`, - status: 'pending' - } - ] - - return { - name: dt.name, - resourceType: dt.resource_type, - behavior, - steps, - _newDbName: newDbName, - _isInstance: isInstance, - _sourceWorkspace: $workspaceStore!, - _targetWorkspace: prefixed_id - } as DatatableCloneJob & { - _newDbName: string - _isInstance: boolean - _sourceWorkspace: string - _targetWorkspace: string - } - }) - - if (datatablesToClone.length > 0) { - cloneQueue = datatablesToClone - currentCloneJob = cloneQueue[0] - cloneModalOpen = true - // Don't finish fork yet — the modal handles remaining steps - return + // Check if any datatables need cloning + if (forkDatatableSection) { + const queue = forkDatatableSection.buildCloneQueue(prefixed_id) + if (queue.length > 0) { + forkDatatableSection.startCloning(queue) + return + } } forkCreationLoading = false @@ -284,103 +213,6 @@ } } - async function executeCloneJob( - job: DatatableCloneJob & { - _newDbName: string - _isInstance: boolean - _sourceWorkspace: string - _targetWorkspace: string - } - ) { - cloneRunning = true - let stepIdx = 0 - - if (job._isInstance) { - // Step 1: CREATE DATABASE + grant permissions via setupCustomInstanceDb - job.steps[stepIdx].status = 'running' - try { - await SettingService.setupCustomInstanceDb({ - name: job._newDbName, - requestBody: { tag: 'datatable' } - }) - job.steps[stepIdx].status = 'done' - } catch (e: any) { - const msg = e?.body ?? e?.message ?? String(e) - if (msg.includes('already exists')) { - job.steps[stepIdx].status = 'done' - } else { - job.steps[stepIdx].status = 'error' - job.steps[stepIdx].error = msg - cloneRunning = false - return - } - } - stepIdx++ - - // Step 2: fork_pg_database (dump + import) - job.steps[stepIdx].status = 'running' - try { - await WorkspaceService.forkPgDatabase({ - workspace: job._targetWorkspace, - requestBody: { - source: `datatable://${job.name}`, - target: `datatable://${job.name}`, - fork_behavior: job.behavior, - target_override_dbname: job._newDbName - } - }) - job.steps[stepIdx].status = 'done' - } catch (e: any) { - job.steps[stepIdx].status = 'error' - job.steps[stepIdx].error = e?.body ?? e?.message ?? String(e) - cloneRunning = false - return - } - } else { - // Resource DB: CREATE DATABASE + dump/import in one call - job.steps[stepIdx].status = 'running' - try { - await WorkspaceService.forkPgDatabase({ - workspace: job._targetWorkspace, - requestBody: { - source: `datatable://${job.name}`, - target: `datatable://${job.name}`, - fork_behavior: job.behavior, - target_override_dbname: job._newDbName, - create_target_db: true - } - }) - job.steps[stepIdx].status = 'done' - } catch (e: any) { - job.steps[stepIdx].status = 'error' - job.steps[stepIdx].error = e?.body ?? e?.message ?? String(e) - cloneRunning = false - return - } - } - - cloneRunning = false - } - - function advanceCloneQueue() { - const idx = cloneQueue.indexOf(currentCloneJob!) - if (idx < cloneQueue.length - 1) { - currentCloneJob = cloneQueue[idx + 1] - } else { - // All done - cloneModalOpen = false - currentCloneJob = undefined - cloneQueue = [] - forkCreationLoading = false - sendUserToast(`Successfully forked workspace ${$workspaceStore} as: wm-fork-${id}`) - usersWorkspaceStore.set(undefined) - WorkspaceService.listUserWorkspaces().then((ws) => { - usersWorkspaceStore.set(ws) - switchWorkspace(`wm-fork-${id}`) - }) - } - } - async function createWorkspace(): Promise { await WorkspaceService.createWorkspace({ requestBody: { @@ -647,34 +479,22 @@ {/if} - {#if isFork && allDatatables.current && allDatatables.current.length > 0} - +{/if} + +{#if cloneModalOpen && currentCloneJob} + s.status === 'done') + ? 'Next' + : 'Start'} + open={cloneModalOpen} + loading={cloneRunning} + onConfirmed={async () => { + if (currentCloneJob!.steps.every((s) => s.status === 'done')) { + advanceCloneQueue() + } else { + await executeCloneJob(currentCloneJob!) + } + }} + onCanceled={() => { + cloneModalOpen = false + currentCloneJob = undefined + cloneQueue = [] + onCanceled?.() + }} + > + {#if currentCloneJob.behavior === 'schema_and_data'} + + This will copy the entire database including all data. The pg_dump output is temporarily + stored on disk and may consume significant server disk space during the operation. + + {:else} + + This will copy the database schema only. All tables will be empty. This is a lightweight + operation. + + {/if} + + {#if currentCloneJob.resourceType === 'instance'} +

+ This will run CREATE DATABASE {currentCloneJob.steps[0]?.label.match(/"([^"]+)"/)?.[1] ?? ''} on the Windmill PostgreSQL instance. +

+ {:else} +

+ This will run CREATE DATABASE on the resource's PostgreSQL server. +

+ {/if} + +
+ {#each currentCloneJob.steps as step} +
+ {#if step.status === 'done'} + + {:else if step.status === 'running'} + + {:else if step.status === 'error'} + + {:else} +
+ {/if} + + {step.label} + +
+ {#if step.error} +

{step.error}

+ {/if} + {/each} +
+
+{/if}