mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-21 00:03:31 +00:00
feat: prompt to create migration on DDL in datatable SQL editors
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<script lang="ts">
|
||||
import { Button } from './common'
|
||||
import Modal2 from './common/modal/Modal2.svelte'
|
||||
import NewDataTableMigrationModal from './workspaceSettings/NewDataTableMigrationModal.svelte'
|
||||
import { splitSqlStatements, isDdlStatement } from './sqlDdl'
|
||||
|
||||
let { workspace, datatable }: { workspace: string; datatable: string } = $props()
|
||||
|
||||
type Choice = 'run' | 'migrate' | 'cancel'
|
||||
|
||||
let promptStatement = $state<string | undefined>(undefined)
|
||||
let promptOpen = $state(false)
|
||||
let resolvePrompt: ((choice: Choice) => void) | undefined = undefined
|
||||
let resolveMigrationClosed: (() => void) | undefined = undefined
|
||||
let newMigrationModal = $state<NewDataTableMigrationModal | undefined>(undefined)
|
||||
|
||||
function finishPrompt(choice: Choice) {
|
||||
const r = resolvePrompt
|
||||
resolvePrompt = undefined
|
||||
promptOpen = false
|
||||
promptStatement = undefined
|
||||
r?.(choice)
|
||||
}
|
||||
|
||||
// Closing the modal (X / escape) while a prompt is pending counts as cancel.
|
||||
$effect(() => {
|
||||
if (!promptOpen && resolvePrompt) {
|
||||
finishPrompt('cancel')
|
||||
}
|
||||
})
|
||||
|
||||
function promptDdl(statement: string): Promise<Choice> {
|
||||
return new Promise((resolve) => {
|
||||
resolvePrompt = resolve
|
||||
promptStatement = statement
|
||||
promptOpen = true
|
||||
})
|
||||
}
|
||||
|
||||
function handleMigrationClosed() {
|
||||
const r = resolveMigrationClosed
|
||||
resolveMigrationClosed = undefined
|
||||
r?.()
|
||||
}
|
||||
|
||||
// Open the prefilled new-migration modal and resolve once it closes (whether
|
||||
// the migration was created or the user cancelled).
|
||||
function openMigrationModal(statement: string): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
resolveMigrationClosed = resolve
|
||||
newMigrationModal?.open({ codeUp: statement })
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Inspect `code` for DDL statements. For each one, prompt the user to run it
|
||||
* anyway or turn it into a migration (prompts shown one at a time). Returns
|
||||
* whether to proceed and the code to run (with migrated statements stripped).
|
||||
*/
|
||||
export async function guard(code: string): Promise<{ proceed: boolean; code: string }> {
|
||||
const statements = splitSqlStatements(code)
|
||||
if (!statements.some((s) => isDdlStatement(s))) {
|
||||
return { proceed: true, code }
|
||||
}
|
||||
|
||||
const kept: string[] = []
|
||||
for (const statement of statements) {
|
||||
if (!isDdlStatement(statement)) {
|
||||
kept.push(statement)
|
||||
continue
|
||||
}
|
||||
const choice = await promptDdl(statement)
|
||||
if (choice === 'cancel') {
|
||||
return { proceed: false, code }
|
||||
}
|
||||
if (choice === 'run') {
|
||||
kept.push(statement)
|
||||
continue
|
||||
}
|
||||
// migrate: strip from the executed code and open the prefilled modal
|
||||
await openMigrationModal(statement)
|
||||
}
|
||||
|
||||
return { proceed: true, code: kept.join(';\n') }
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal2
|
||||
title="Schema change detected"
|
||||
fixedWidth="md"
|
||||
fixedHeight="adaptive"
|
||||
bind:isOpen={promptOpen}
|
||||
closeOnOutsideClick={false}
|
||||
>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<p class="text-sm text-secondary">
|
||||
This looks like a schema-changing (DDL) statement. Schema changes are best tracked as
|
||||
migrations rather than run ad-hoc. Create a migration for it instead?
|
||||
</p>
|
||||
<pre
|
||||
class="text-xs whitespace-pre-wrap font-mono bg-surface-secondary rounded p-3 max-h-48 overflow-auto"
|
||||
>{promptStatement ?? ''}</pre
|
||||
>
|
||||
<div class="flex justify-end gap-2 pt-2">
|
||||
<Button variant="default" size="sm" on:click={() => finishPrompt('run')}>Run anyway</Button>
|
||||
<Button variant="accent" size="sm" on:click={() => finishPrompt('migrate')}>
|
||||
Create a migration
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal2>
|
||||
|
||||
<NewDataTableMigrationModal
|
||||
bind:this={newMigrationModal}
|
||||
{workspace}
|
||||
{datatable}
|
||||
onClose={handleMigrationClosed}
|
||||
/>
|
||||
@@ -43,6 +43,7 @@
|
||||
import { editorFontSize } from '$lib/editorFontSize.svelte'
|
||||
import { createHash as randomHash } from '$lib/editorLangUtils'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
|
||||
import {
|
||||
type Preview,
|
||||
ResourceService,
|
||||
@@ -213,6 +214,26 @@
|
||||
|
||||
let lang = $state(scriptLangToEditorLang(untrack(() => scriptLang)))
|
||||
|
||||
// On a postgres script targeting a datatable, DDL statements are intercepted
|
||||
// on run (cmd+enter) and offered as migrations instead.
|
||||
let datatableForMigrations = $derived(
|
||||
scriptLang === 'postgresql' &&
|
||||
typeof args?.database === 'string' &&
|
||||
args.database.startsWith('datatable://')
|
||||
? args.database.slice('datatable://'.length).split('/')[0]
|
||||
: undefined
|
||||
)
|
||||
let ddlGuard = $state<DdlMigrationGuard | undefined>(undefined)
|
||||
|
||||
async function runCmdEnterWithDdlGuard() {
|
||||
if (datatableForMigrations && ddlGuard) {
|
||||
const res = await ddlGuard.guard(getCode())
|
||||
if (!res.proceed) return
|
||||
if (res.code !== getCode()) setCode(res.code)
|
||||
}
|
||||
cmdEnterAction?.()
|
||||
}
|
||||
|
||||
let filePath = $state(computePath(untrack(() => path)))
|
||||
|
||||
let initialPath: string | undefined = $state(untrack(() => path))
|
||||
@@ -1539,7 +1560,8 @@
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyCode.Enter, function () {
|
||||
updateCode()
|
||||
shouldBindKey && cmdEnterAction && cmdEnterAction()
|
||||
if (!shouldBindKey || !cmdEnterAction) return
|
||||
void runCmdEnterWithDdlGuard()
|
||||
})
|
||||
|
||||
editor?.addCommand(KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Digit7, function () {
|
||||
@@ -2067,6 +2089,13 @@
|
||||
|
||||
<svelte:window onkeydown={onKeyDown} />
|
||||
<EditorTheme />
|
||||
{#if datatableForMigrations && $workspaceStore}
|
||||
<DdlMigrationGuard
|
||||
bind:this={ddlGuard}
|
||||
workspace={$workspaceStore}
|
||||
datatable={datatableForMigrations}
|
||||
/>
|
||||
{/if}
|
||||
{#if !editor}
|
||||
<div class="inset-0 absolute overflow-clip">
|
||||
<FakeMonacoPlaceHolder {code} lineNumbersWidth={51} />
|
||||
|
||||
@@ -1,49 +1,3 @@
|
||||
<script module lang="ts">
|
||||
// code may be composed of many sql statements separated by ';'
|
||||
// this function splits them while taking into account that ';' may not
|
||||
// be the end of a statement (string or escaped)
|
||||
function splitSqlStatements(code: string) {
|
||||
const statements: string[] = []
|
||||
let currentStatement = ''
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let inBacktick = false
|
||||
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
const char = code[i]
|
||||
const prevChar = i > 0 ? code[i - 1] : null
|
||||
|
||||
if (char === "'" && !inDoubleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inSingleQuote = !inSingleQuote
|
||||
} else if (char === '"' && !inSingleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
} else if (char === '`' && !inSingleQuote && !inDoubleQuote && prevChar !== '\\') {
|
||||
inBacktick = !inBacktick
|
||||
}
|
||||
|
||||
if (char === ';' && !inSingleQuote && !inDoubleQuote && !inBacktick) {
|
||||
statements.push(currentStatement.trim())
|
||||
currentStatement = ''
|
||||
} else {
|
||||
currentStatement += char
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatement.trim()) {
|
||||
statements.push(currentStatement.trim())
|
||||
}
|
||||
|
||||
return statements
|
||||
}
|
||||
|
||||
function pruneComments(code: string) {
|
||||
return code
|
||||
.replace(/--.*?(\r?\n|$)/g, '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.trim()
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { CornerDownLeft, Loader2 } from 'lucide-svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
@@ -57,6 +11,8 @@
|
||||
import { getDatabaseArg, getDbType } from './dbOps'
|
||||
import type { DbInput } from './dbTypes'
|
||||
import { wrapDucklakeQuery } from './ducklake'
|
||||
import { splitSqlStatements, pruneComments } from './sqlDdl'
|
||||
import DdlMigrationGuard from './DdlMigrationGuard.svelte'
|
||||
|
||||
type Props = {
|
||||
input: DbInput
|
||||
@@ -66,6 +22,15 @@
|
||||
let { input, onData, placeholderTableName }: Props = $props()
|
||||
let dbType = $derived(getDbType(input))
|
||||
|
||||
// A datatable REPL targets `datatable://<name>`; surface DDL statements as
|
||||
// migration prompts instead of running them ad-hoc.
|
||||
let datatableName = $derived(
|
||||
input.type === 'database' && input.resourcePath.startsWith('datatable://')
|
||||
? input.resourcePath.slice('datatable://'.length).split('/')[0]
|
||||
: undefined
|
||||
)
|
||||
let ddlGuard = $state<DdlMigrationGuard | undefined>(undefined)
|
||||
|
||||
const DEFAULT_SQL = 'SELECT * FROM _'
|
||||
let code = $state(DEFAULT_SQL)
|
||||
$effect(() => {
|
||||
@@ -81,6 +46,16 @@
|
||||
async function run({ doPostgresRowToJsonFix }: { doPostgresRowToJsonFix?: boolean } = {}) {
|
||||
if (isRunning || !$workspaceStore) return
|
||||
const READ_OPS = ['SELECT', 'WITH', 'SHOW', 'EXPLAIN', 'DESCRIBE']
|
||||
|
||||
// On a datatable, intercept DDL statements and offer to make them
|
||||
// migrations; the user may strip some, leaving the rest to run.
|
||||
if (datatableName && ddlGuard && !doPostgresRowToJsonFix) {
|
||||
const res = await ddlGuard.guard(code)
|
||||
if (!res.proceed) return
|
||||
if (res.code !== code) code = res.code
|
||||
if (pruneComments(code).trim() === '') return
|
||||
}
|
||||
|
||||
isRunning = true
|
||||
try {
|
||||
const statements = splitSqlStatements(pruneComments(code))
|
||||
@@ -197,3 +172,7 @@
|
||||
/>
|
||||
</Pane>
|
||||
</Splitpanes>
|
||||
|
||||
{#if datatableName && $workspaceStore}
|
||||
<DdlMigrationGuard bind:this={ddlGuard} workspace={$workspaceStore} datatable={datatableName} />
|
||||
{/if}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
// Heuristics for splitting a SQL script into individual statements and
|
||||
// detecting Data Definition Language (DDL) statements. Shared by the datatable
|
||||
// SQL REPL and the postgres editor to steer schema changes into migrations.
|
||||
|
||||
// code may be composed of many sql statements separated by ';'
|
||||
// this splits them while taking into account that ';' may appear inside a
|
||||
// string or quoted identifier and is then not the end of a statement.
|
||||
export function splitSqlStatements(code: string): string[] {
|
||||
const statements: string[] = []
|
||||
let currentStatement = ''
|
||||
let inSingleQuote = false
|
||||
let inDoubleQuote = false
|
||||
let inBacktick = false
|
||||
|
||||
for (let i = 0; i < code.length; i++) {
|
||||
const char = code[i]
|
||||
const prevChar = i > 0 ? code[i - 1] : null
|
||||
|
||||
if (char === "'" && !inDoubleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inSingleQuote = !inSingleQuote
|
||||
} else if (char === '"' && !inSingleQuote && !inBacktick && prevChar !== '\\') {
|
||||
inDoubleQuote = !inDoubleQuote
|
||||
} else if (char === '`' && !inSingleQuote && !inDoubleQuote && prevChar !== '\\') {
|
||||
inBacktick = !inBacktick
|
||||
}
|
||||
|
||||
if (char === ';' && !inSingleQuote && !inDoubleQuote && !inBacktick) {
|
||||
statements.push(currentStatement.trim())
|
||||
currentStatement = ''
|
||||
} else {
|
||||
currentStatement += char
|
||||
}
|
||||
}
|
||||
|
||||
if (currentStatement.trim()) {
|
||||
statements.push(currentStatement.trim())
|
||||
}
|
||||
|
||||
return statements.filter((s) => s.length > 0)
|
||||
}
|
||||
|
||||
export function pruneComments(code: string): string {
|
||||
return code
|
||||
.replace(/--.*?(\r?\n|$)/g, '')
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '')
|
||||
.trim()
|
||||
}
|
||||
|
||||
// Schema-changing keywords. GRANT/REVOKE are included since permission changes
|
||||
// also belong in migrations rather than ad-hoc execution.
|
||||
const DDL_KEYWORDS = ['CREATE', 'ALTER', 'DROP', 'TRUNCATE', 'RENAME', 'COMMENT', 'GRANT', 'REVOKE']
|
||||
|
||||
/** Heuristic: a statement is DDL if its first keyword is schema-changing. */
|
||||
export function isDdlStatement(statement: string): boolean {
|
||||
const firstWord = pruneComments(statement).trim().split(/\s+/)[0]?.toUpperCase()
|
||||
return !!firstWord && DDL_KEYWORDS.includes(firstWord)
|
||||
}
|
||||
@@ -1,17 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '../common'
|
||||
import Modal2 from '../common/modal/Modal2.svelte'
|
||||
import Tabs from '../common/tabs/Tabs.svelte'
|
||||
import Tab from '../common/tabs/Tab.svelte'
|
||||
import TabContent from '../common/tabs/TabContent.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import ConfirmationModal from '../common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { createAsyncConfirmationModal } from '../common/confirmationModal/asyncConfirmationModal.svelte'
|
||||
import { ChevronDown, Play, Trash2, Plus, Undo2, Loader2 } from 'lucide-svelte'
|
||||
import { WorkspaceService, type DatatableMigrationWithStatus } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import NewDataTableMigrationModal from './NewDataTableMigrationModal.svelte'
|
||||
|
||||
let {
|
||||
workspace,
|
||||
@@ -25,13 +20,7 @@
|
||||
let loading = $state(false)
|
||||
let busy = $state(false)
|
||||
|
||||
let newOpen = $state(false)
|
||||
let newTab = $state('up')
|
||||
let newName = $state('')
|
||||
let newCodeUp = $state('')
|
||||
let enableDown = $state(false)
|
||||
let newCodeDown = $state('')
|
||||
let creating = $state(false)
|
||||
let newMigrationModal = $state<NewDataTableMigrationModal | undefined>(undefined)
|
||||
|
||||
const confirmationModal = createAsyncConfirmationModal()
|
||||
|
||||
@@ -128,52 +117,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resetNew() {
|
||||
newName = ''
|
||||
newCodeUp = ''
|
||||
newCodeDown = ''
|
||||
enableDown = false
|
||||
newTab = 'up'
|
||||
}
|
||||
|
||||
function openNew() {
|
||||
resetNew()
|
||||
newOpen = true
|
||||
}
|
||||
|
||||
async function create(run: boolean) {
|
||||
if (newName.trim() === '') {
|
||||
sendUserToast('Migration name is required', true)
|
||||
return
|
||||
}
|
||||
creating = true
|
||||
try {
|
||||
const created = await WorkspaceService.createDatatableMigration({
|
||||
workspace,
|
||||
datatableName: datatable,
|
||||
requestBody: {
|
||||
name: newName.trim(),
|
||||
code_up: newCodeUp,
|
||||
code_down: enableDown ? newCodeDown : undefined
|
||||
}
|
||||
})
|
||||
if (run) {
|
||||
await WorkspaceService.runDatatableMigrations({
|
||||
workspace,
|
||||
datatableName: datatable,
|
||||
upTo: created.timestamp
|
||||
})
|
||||
}
|
||||
newOpen = false
|
||||
sendUserToast(run ? 'Migration created and run' : 'Migration created')
|
||||
await loadMigrations()
|
||||
} catch (e) {
|
||||
sendUserToast(`Failed to create migration: ${e}`, true)
|
||||
} finally {
|
||||
creating = false
|
||||
}
|
||||
}
|
||||
|
||||
const statusColor = {
|
||||
ran: 'bg-green-500',
|
||||
not_run: 'bg-orange-500',
|
||||
@@ -245,7 +188,12 @@
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex justify-between gap-2 pt-2">
|
||||
<Button variant="default" size="sm" startIcon={{ icon: Plus }} on:click={openNew}>New</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
startIcon={{ icon: Plus }}
|
||||
on:click={() => newMigrationModal?.open()}>New</Button
|
||||
>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="default"
|
||||
@@ -270,52 +218,11 @@
|
||||
</div>
|
||||
</Modal2>
|
||||
|
||||
<Modal2 bind:isOpen={newOpen} title="New migration" fixedWidth="md" fixedHeight="lg">
|
||||
<div class="flex flex-col gap-3 w-full grow min-h-0">
|
||||
<TextInput
|
||||
bind:value={newName}
|
||||
inputProps={{ placeholder: 'Migration name (e.g. add_index_to_customers)' }}
|
||||
/>
|
||||
<Tabs bind:selected={newTab} class="grow min-h-0">
|
||||
<Tab value="up" label="Up" />
|
||||
<Tab value="down" label="Down" />
|
||||
{#snippet content()}
|
||||
<TabContent value="up" class="h-80">
|
||||
<SimpleEditor class="h-full" lang="sql" bind:code={newCodeUp} />
|
||||
</TabContent>
|
||||
<TabContent value="down" class="h-80">
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<Toggle
|
||||
bind:checked={enableDown}
|
||||
options={{ right: 'Enable down migration' }}
|
||||
size="sm"
|
||||
/>
|
||||
{#if enableDown}
|
||||
<div class="grow min-h-0">
|
||||
<SimpleEditor class="h-full" lang="sql" bind:code={newCodeDown} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
disabled={creating}
|
||||
on:click={() => create(true)}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Create without running',
|
||||
onClick: () => create(false)
|
||||
}
|
||||
]}
|
||||
>
|
||||
Create and run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal2>
|
||||
<NewDataTableMigrationModal
|
||||
bind:this={newMigrationModal}
|
||||
{workspace}
|
||||
{datatable}
|
||||
onCreated={loadMigrations}
|
||||
/>
|
||||
|
||||
<ConfirmationModal {...confirmationModal.props} />
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
<script lang="ts">
|
||||
import { Button } from '../common'
|
||||
import Modal2 from '../common/modal/Modal2.svelte'
|
||||
import Tabs from '../common/tabs/Tabs.svelte'
|
||||
import Tab from '../common/tabs/Tab.svelte'
|
||||
import TabContent from '../common/tabs/TabContent.svelte'
|
||||
import Toggle from '../Toggle.svelte'
|
||||
import TextInput from '../text_input/TextInput.svelte'
|
||||
import SimpleEditor from '../SimpleEditor.svelte'
|
||||
import { WorkspaceService } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
|
||||
let {
|
||||
workspace,
|
||||
datatable,
|
||||
onCreated,
|
||||
onClose
|
||||
}: {
|
||||
workspace: string
|
||||
datatable: string
|
||||
onCreated?: () => void
|
||||
onClose?: () => void
|
||||
} = $props()
|
||||
|
||||
let isOpen = $state(false)
|
||||
// Notify the parent whenever the modal closes (after a create or a cancel),
|
||||
// so callers that sequence modals (e.g. the DDL guard) can advance.
|
||||
let prevOpen = false
|
||||
$effect(() => {
|
||||
if (prevOpen && !isOpen) onClose?.()
|
||||
prevOpen = isOpen
|
||||
})
|
||||
let tab = $state('up')
|
||||
let name = $state('')
|
||||
let codeUp = $state('')
|
||||
let enableDown = $state(false)
|
||||
let codeDown = $state('')
|
||||
let creating = $state(false)
|
||||
|
||||
export function open(prefill?: { name?: string; codeUp?: string; codeDown?: string }) {
|
||||
name = prefill?.name ?? ''
|
||||
codeUp = prefill?.codeUp ?? ''
|
||||
codeDown = prefill?.codeDown ?? ''
|
||||
enableDown = (prefill?.codeDown ?? '') !== ''
|
||||
tab = 'up'
|
||||
isOpen = true
|
||||
}
|
||||
|
||||
async function create(run: boolean) {
|
||||
if (name.trim() === '') {
|
||||
sendUserToast('Migration name is required', true)
|
||||
return
|
||||
}
|
||||
creating = true
|
||||
try {
|
||||
const created = await WorkspaceService.createDatatableMigration({
|
||||
workspace,
|
||||
datatableName: datatable,
|
||||
requestBody: {
|
||||
name: name.trim(),
|
||||
code_up: codeUp,
|
||||
code_down: enableDown ? codeDown : undefined
|
||||
}
|
||||
})
|
||||
if (run) {
|
||||
await WorkspaceService.runDatatableMigrations({
|
||||
workspace,
|
||||
datatableName: datatable,
|
||||
upTo: created.timestamp
|
||||
})
|
||||
}
|
||||
isOpen = false
|
||||
sendUserToast(run ? 'Migration created and run' : 'Migration created')
|
||||
onCreated?.()
|
||||
} catch (e) {
|
||||
sendUserToast(`Failed to create migration: ${e}`, true)
|
||||
} finally {
|
||||
creating = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal2 bind:isOpen title="New migration — {datatable}" fixedWidth="md" fixedHeight="lg">
|
||||
<div class="flex flex-col gap-3 w-full grow min-h-0">
|
||||
<TextInput
|
||||
bind:value={name}
|
||||
inputProps={{ placeholder: 'Migration name (e.g. add_index_to_customers)' }}
|
||||
/>
|
||||
<Tabs bind:selected={tab} class="grow min-h-0">
|
||||
<Tab value="up" label="Up" />
|
||||
<Tab value="down" label="Down" />
|
||||
{#snippet content()}
|
||||
<TabContent value="up" class="h-80">
|
||||
<SimpleEditor class="h-full" lang="sql" bind:code={codeUp} />
|
||||
</TabContent>
|
||||
<TabContent value="down" class="h-80">
|
||||
<div class="flex flex-col gap-2 h-full">
|
||||
<Toggle
|
||||
bind:checked={enableDown}
|
||||
options={{ right: 'Enable down migration' }}
|
||||
size="sm"
|
||||
/>
|
||||
{#if enableDown}
|
||||
<div class="grow min-h-0">
|
||||
<SimpleEditor class="h-full" lang="sql" bind:code={codeDown} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</TabContent>
|
||||
{/snippet}
|
||||
</Tabs>
|
||||
<div class="flex justify-end pt-2">
|
||||
<Button
|
||||
variant="accent"
|
||||
size="sm"
|
||||
disabled={creating}
|
||||
on:click={() => create(true)}
|
||||
dropdownItems={[
|
||||
{
|
||||
label: 'Create without running',
|
||||
onClick: () => create(false)
|
||||
}
|
||||
]}
|
||||
>
|
||||
Create and run
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal2>
|
||||
Reference in New Issue
Block a user