mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-29 08:12:56 +00:00
* export_datatable_schema * Propose to fork the datatable on ws fork * dump datatable * Dockerfile * Fix import_datatable_dump * datatable schema fork works! * Option to copy both schema and data * Datatable fork behavior * nit ui * use psql instead * remove fork_datatable route * feat: add fork_pg_database and export_pg_schema routes with DB Manager UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: pluralize "schema" to "schemas" in DB Manager export/import UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add import mode select (schema only vs schema + data) to DB Manager import Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Select schema or schema+data when important database * fix: prepend $res: prefix to resource paths in DB Manager import/export Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: dynamic import button label based on selected mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nits * feat: add warning alert when schema+data import mode is selected Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit hide on cloud hosted * refactor: remove fork_behavior from datatable settings Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: split CreateWorkspace into layout wrapper and CreateWorkspaceInner Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: instantiate CreateWorkspaceInner in globalForkModal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * nit icons * Data table fork UI * feat: pass per-datatable fork behaviors from UI to backend during workspace fork Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Fix fork overwriting all datatables * UI nits * custom instance db refactor * custom instance db wizard btn for all in dropdown * nit * Delete custom instance database button * Disable forking for resource datatables * Big import buttons when db empty * Revert "Disable forking for resource datatables" This reverts commit9561cc8fd4. * feat: add non_diffable flag to resource table Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add resource-type datatable fork with CREATE DATABASE Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: tag forked datatables with nonDiffable and forkedFrom Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: diff datatable and ducklake settings individually on workspace merge Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: skip non_diffable resources and datatables in workspace diff Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: default datatable fork behavior to keep_original Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: make grant permissions non-fatal in instance datatable fork Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: make datatable and ducklake diffs visible in workspace comparison Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: remove datatable fork logic from workspace fork route Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: correct ahead/behind logic for datatable and ducklake diffs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "fix: correct ahead/behind logic for datatable and ducklake diffs" This reverts commit6b50884dc6. * revert: remove datatable and ducklake settings diffing logic Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add datatable clone UI with step-by-step confirmation modal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract datatable fork UI into ForkDatatableSection component Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit * fix: run datatable cloning before workspace fork creation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit disable fork admins * nit fix switching workspace prematurely * fix: use source workspace for forkPgDatabase calls during fork Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: update forked workspace datatable settings after fork creation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add forked_from field to DataTable and set it for instance forks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit onFinish * fix: add forked_from to DataTableSettings OpenAPI schema Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: track datatable table DDL changes in workspace_diff Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "feat: track datatable table DDL changes in workspace_diff" This reverts commit7526dd68b9. * feat: add get_datatable_full_schema endpoint and snapshot schema on fork Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix duplicate migration key * fix: set forked_from on datatable config for both instance and resource types Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nits * feat: drop forked databases on workspace deletion with confirmation UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract drop_forked_datatable_databases from delete_workspace Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: cast pg char columns to text in FK schema query Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show dbname instead of resource type in fork deletion modal Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ui nit * refactor: extract drop_custom_instance_database into windmill-common Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add datatable schema diff section to merge UI Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * UI * feat: add review drawer with YAML diff and SQL migration runner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: use Monaco DiffEditor for YAML diff in review drawer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit * Revert "refactor: use Monaco DiffEditor for YAML diff in review drawer" This reverts commita86008ba4c. * Revert "feat: add review drawer with YAML diff and SQL migration runner" This reverts commit0a0deb5ddb. * feat: add review drawer with DiffEditor and SQL migration runner Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * ui nits * fix: show diff between forked_from schema and changed side Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: re-fetch target live schema after migration for correct baseline Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * revert * nit auto next * feat: add confirmation modal before deploying migration to parent Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle missing columns/foreignKeys in schema conversion Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nits * refactor: use temp file on disk for pg_dump instead of in-memory string Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Don't replace postgres dbname * fix: add validation to drop_custom_instance_database and use source db for CREATE/DROP Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: type DataTable.forked_from as DataTableForkedFrom struct Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: simplify fork_pg_database to take source + target_dbname Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * dead code * feat: enforce schema_and_data admin-only and extract create_custom_instance_database Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: rename fork_pg_database to import_pg_database with source/target/override params Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit * refactor: remove original_dbname/original_resource from forked_from, resolve from parent Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit * fix: resolve forked dbname from fork workspace when dropping resource databases Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nits * fix: always clean up global_settings even if database doesn't exist Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: check datatable resource_type from config instead of URL prefix Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: wrap PG default value expressions in braces to prevent CAST quoting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "fix: wrap PG default value expressions in braces to prevent CAST quoting" This reverts commit77f5a2c4e8. * refactor: reuse columnDefToTableEditorValuesColumn for default value handling Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: store raw API schema in forked_from to avoid double transformation Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "fix: store raw API schema in forked_from to avoid double transformation" This reverts commite326197a20. * Revert "refactor: reuse columnDefToTableEditorValuesColumn for default value handling" This reverts commitbd8f071d9f. * fix: validate dbname with strict regex to prevent SQL injection Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix default value * always validate dbname * refactor: move get_datatable_full_schema structs and logic to query_builders.rs Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: split import_pg_database into create_pg_database + import_pg_database Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract drop_forked_datatable_databases into its own route Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: require admin when using $res: resource paths in import_pg_database Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use UserDB for $res: resource access and restrict dbname creation - resolve_pg_source_checked uses UserDB (row-level security) for $res: paths - transform_json_unchecked is now pub(crate) to prevent misuse - Non-superadmins can only create databases with wm_fork_ prefix - datatable:// remains accessible to everyone Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: refuse to drop forked databases unless name starts with wm_fork_ Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: remove resolve_pg_source, use resolve_pg_source_checked everywhere Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix permissions * sqlx prepare * compilation nits * sqlx prepare * sqlx prepare * wrong route syntax * fix: allow workspace owner to edit datatable config for fork setup Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Revert "fix: allow workspace owner to edit datatable config for fork setup" This reverts commitab683e637b. * refactor: move datatable fork setup into create_workspace_fork backend Instead of updating datatable settings from the frontend after fork creation (which required admin/owner access), pass forked_datatables info to create_workspace_fork and handle it atomically in the same transaction. Removes applyPostForkDatatableUpdates from frontend. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: snapshot schema in backend during fork instead of frontend The schema snapshot is now taken by the backend in apply_forked_datatable via snapshot_datatable_schema, which connects to the parent workspace's datatable and runs pg_get_full_schema. This removes the need for the frontend to call getDatatableFullSchema and pass the schema through. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: use get_resource_value_interpolated_internal for $res: to resolve $var: references Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit * sqlx prepare * fix: add permission check to drop_forked_datatable_databases, validate dbnames, restrict temp file perms - drop_forked_datatable_databases: same permission as delete_workspace (fork owner or super admin) - validate_dbname on target_dbname_override and ForkedDatatableInfo.new_dbname - Enforce wm_fork_ prefix on forked datatable new_dbname - DumpFile: set /tmp/windmill/ to 0700 and create files with 0600 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * nit CLI * Rename to ws_specific * sqlx prepare * nit always validate dbname * fix: include foreign keys in CREATE TABLE migration for added tables Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: detect nextval defaults and use SERIAL/BIGSERIAL types in CREATE TABLE Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Update frontend/src/lib/components/DBManagerDrawer.svelte Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update backend/windmill-common/src/lib.rs Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * Update backend/windmill-common/src/lib.rs Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com> * fix: sort foreign keys by constraint name for deterministic schema output Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * sqlx prepare * rename migration to update timestamp --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
268 lines
7.5 KiB
Svelte
268 lines
7.5 KiB
Svelte
<script module lang="ts">
|
|
export type ForkStep = {
|
|
label: string
|
|
status: 'pending' | 'running' | 'done' | 'error'
|
|
error?: string
|
|
}
|
|
|
|
export type DatatableCloneJob = {
|
|
name: string
|
|
resourceType: string
|
|
behavior: 'schema_only' | 'schema_and_data'
|
|
steps: ForkStep[]
|
|
_newDbName: string
|
|
_isInstance: boolean
|
|
_sourceWorkspace: string
|
|
_targetWorkspace: string
|
|
_resourcePath: string
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import { WorkspaceService } from '$lib/gen'
|
|
import { workspaceStore, userStore } from '$lib/stores'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
import { resource } from 'runed'
|
|
import Select from '../select/Select.svelte'
|
|
import Label from '../Label.svelte'
|
|
import Alert from '../common/alert/Alert.svelte'
|
|
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
|
import { Check, X, Loader2 } from 'lucide-svelte'
|
|
|
|
interface Props {
|
|
onAllDone?: () => void
|
|
onCanceled?: () => void
|
|
}
|
|
|
|
let { onAllDone, onCanceled }: Props = $props()
|
|
|
|
let allDatatables = resource([], async () =>
|
|
$workspaceStore
|
|
? WorkspaceService.listDataTables({ workspace: $workspaceStore ?? '' })
|
|
: undefined
|
|
)
|
|
|
|
let datatableBehaviors: Record<string, 'schema_only' | 'schema_and_data' | 'keep_original'> =
|
|
$state({})
|
|
|
|
let cloneModalOpen = $state(false)
|
|
let currentCloneJob: DatatableCloneJob | undefined = $state(undefined)
|
|
let cloneQueue: DatatableCloneJob[] = $state([])
|
|
let cloneRunning = $state(false)
|
|
|
|
export function hasDatatables(): boolean {
|
|
return (allDatatables.current?.length ?? 0) > 0
|
|
}
|
|
|
|
export function buildCloneQueue(targetWorkspaceId: string): DatatableCloneJob[] {
|
|
return (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 = `${targetWorkspaceId.replace(/-/g, '_')}__${dt.name}`
|
|
|
|
const steps: ForkStep[] = [
|
|
{
|
|
label: `CREATE DATABASE "${newDbName}"`,
|
|
status: 'pending'
|
|
},
|
|
{
|
|
label: `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: targetWorkspaceId,
|
|
_resourcePath: dt.resource_path
|
|
}
|
|
})
|
|
}
|
|
|
|
let completedJobs: DatatableCloneJob[] = $state([])
|
|
|
|
export function startCloning(queue: DatatableCloneJob[]) {
|
|
completedJobs = []
|
|
cloneQueue = queue
|
|
currentCloneJob = cloneQueue[0]
|
|
cloneModalOpen = true
|
|
}
|
|
|
|
export function getCompletedCloneJobs(): DatatableCloneJob[] {
|
|
return completedJobs
|
|
}
|
|
|
|
async function executeCloneJob(job: DatatableCloneJob) {
|
|
cloneRunning = true
|
|
let stepIdx = 0
|
|
|
|
// Step 1: Create the database
|
|
job.steps[stepIdx].status = 'running'
|
|
try {
|
|
await WorkspaceService.createPgDatabase({
|
|
workspace: job._sourceWorkspace,
|
|
requestBody: {
|
|
source: `datatable://${job.name}`,
|
|
target_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
|
|
}
|
|
stepIdx++
|
|
|
|
// Step 2: Import data
|
|
job.steps[stepIdx].status = 'running'
|
|
try {
|
|
await WorkspaceService.importPgDatabase({
|
|
workspace: job._sourceWorkspace,
|
|
requestBody: {
|
|
source: `datatable://${job.name}`,
|
|
target: `datatable://${job.name}`,
|
|
target_dbname_override: job._newDbName,
|
|
fork_behavior: job.behavior
|
|
}
|
|
})
|
|
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
|
|
}
|
|
stepIdx++
|
|
|
|
cloneRunning = false
|
|
}
|
|
|
|
function advanceCloneQueue() {
|
|
if (currentCloneJob) {
|
|
completedJobs.push(currentCloneJob)
|
|
}
|
|
const idx = cloneQueue.indexOf(currentCloneJob!)
|
|
if (idx < cloneQueue.length - 1) {
|
|
currentCloneJob = cloneQueue[idx + 1]
|
|
} else {
|
|
cloneModalOpen = false
|
|
currentCloneJob = undefined
|
|
cloneQueue = []
|
|
onAllDone?.()
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#if allDatatables.current && allDatatables.current.length > 0}
|
|
<Label label="Data table behavior">
|
|
<span class="text-xs text-secondary"> Choose how to handle each datatable when forking </span>
|
|
<div class="border rounded-md divide-y">
|
|
{#each allDatatables.current as dt}
|
|
<div class="flex items-center gap-2 justify-between px-4 py-1.5">
|
|
<div class="flex flex-col">
|
|
<span class="text-xs font-medium">{dt.name}</span>
|
|
<span class="text-2xs text-tertiary"
|
|
>{dt.resource_type === 'instance' ? 'Instance DB' : 'Resource DB'}</span
|
|
>
|
|
</div>
|
|
<Select
|
|
dropdownClass="max-w-96"
|
|
bind:value={
|
|
() => datatableBehaviors[dt.name] ?? 'keep_original',
|
|
(v) => (datatableBehaviors[dt.name] = v)
|
|
}
|
|
items={[
|
|
{ value: 'keep_original', label: 'Keep original' },
|
|
{ value: 'schema_only', label: 'Clone schema only' },
|
|
...(!isCloudHosted() && $userStore?.is_admin
|
|
? [{ value: 'schema_and_data', label: 'Clone schema and data' }]
|
|
: [])
|
|
]}
|
|
/>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</Label>
|
|
{/if}
|
|
|
|
{#if cloneModalOpen && currentCloneJob}
|
|
<ConfirmationModal
|
|
title="Clone datatable: {currentCloneJob.name}"
|
|
confirmationText={cloneRunning ? 'Running...' : 'Start'}
|
|
open={cloneModalOpen}
|
|
loading={cloneRunning}
|
|
onConfirmed={async () => {
|
|
await executeCloneJob(currentCloneJob!)
|
|
advanceCloneQueue()
|
|
}}
|
|
onCanceled={() => {
|
|
cloneModalOpen = false
|
|
currentCloneJob = undefined
|
|
cloneQueue = []
|
|
onCanceled?.()
|
|
}}
|
|
>
|
|
{#if currentCloneJob.behavior === 'schema_and_data'}
|
|
<Alert type="error" title="Heavy operation">
|
|
This will copy the <b>entire database</b> including all data. The pg_dump output is temporarily
|
|
stored on disk and may consume significant server disk space during the operation.
|
|
</Alert>
|
|
{:else}
|
|
<Alert type="info" title="Schema only">
|
|
This will copy the database schema only. All tables will be empty. This is a lightweight
|
|
operation.
|
|
</Alert>
|
|
{/if}
|
|
|
|
{#if currentCloneJob.resourceType === 'instance'}
|
|
<p class="text-xs text-secondary mt-2">
|
|
This will run <code
|
|
>CREATE DATABASE {currentCloneJob.steps[0]?.label.match(/"([^"]+)"/)?.[1] ?? ''}</code
|
|
> on the Windmill PostgreSQL instance.
|
|
</p>
|
|
{:else}
|
|
<p class="text-xs text-secondary mt-2">
|
|
This will run <code>CREATE DATABASE</code> on the resource's PostgreSQL server.
|
|
</p>
|
|
{/if}
|
|
|
|
<div class="mt-4 flex flex-col gap-2">
|
|
{#each currentCloneJob.steps as step}
|
|
<div class="flex items-center gap-2 text-xs">
|
|
{#if step.status === 'done'}
|
|
<Check class="w-4 h-4 shrink-0 text-green-500" />
|
|
{:else if step.status === 'running'}
|
|
<Loader2 class="w-4 h-4 shrink-0 animate-spin text-blue-500" />
|
|
{:else if step.status === 'error'}
|
|
<X class="w-4 h-4 shrink-0 text-red-500" />
|
|
{:else}
|
|
<div class="w-4 h-4 shrink-0 rounded-full border border-gray-300"></div>
|
|
{/if}
|
|
<span
|
|
class:text-tertiary={step.status === 'pending'}
|
|
class:font-medium={step.status === 'running'}
|
|
>
|
|
{step.label}
|
|
</span>
|
|
</div>
|
|
{#if step.error}
|
|
<p class="text-2xs text-red-500 ml-6">{step.error}</p>
|
|
{/if}
|
|
{/each}
|
|
</div>
|
|
</ConfirmationModal>
|
|
{/if}
|