Refactor DB Table schema-level operations into a single factory (#7330)

* Refactor delete table into factory

* refactored onCreate into factory

* ConfirmationModal in Portal
This commit is contained in:
Diego Imbert
2025-12-10 20:07:32 +01:00
committed by GitHub
parent 0b52109703
commit c88c235fb0
4 changed files with 138 additions and 171 deletions

View File

@@ -1,34 +1,36 @@
<script lang="ts">
import { type DBSchema } from '$lib/stores'
import { MoreVertical, Plus, Table2 } from 'lucide-svelte'
import { MoreVertical, Plus, Table2, Trash2Icon } from 'lucide-svelte'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import { ClearableInput, Drawer, DrawerContent } from './common'
import { sendUserToast } from '$lib/toast'
import { type ColumnDef } from './apps/components/display/dbtable/utils'
import DBTable from './DBTable.svelte'
import type { DbTableActionFactory, IDbTableOps } from './dbOps'
import type { IDbSchemaOps, IDbTableOps } from './dbOps'
import DropdownV2 from './DropdownV2.svelte'
import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte'
import Button from './common/button/Button.svelte'
import DbTableEditor, { type DBTableEditorProps } from './DBTableEditor.svelte'
import DbTableEditor from './DBTableEditor.svelte'
import type { DbType } from './dbTypes'
import Portal from './Portal.svelte'
type Props = {
dbType: DbType
dbSchema: DBSchema
dbSupportsSchemas: boolean
getColDefs: (tableKey: string) => Promise<ColumnDef[]>
dbTableOpsFactory: (params: { colDefs: ColumnDef[]; tableKey: string }) => IDbTableOps
dbTableActionsFactory?: DbTableActionFactory[]
dbSchemaOps: IDbSchemaOps
refresh?: () => void
dbTableEditorPropsFactory?: (params: { selectedSchemaKey?: string }) => DBTableEditorProps
}
let {
dbType,
dbSchema,
dbTableOpsFactory,
dbSchemaOps,
getColDefs,
dbTableActionsFactory,
refresh,
dbTableEditorPropsFactory,
dbSupportsSchemas
dbSupportsSchemas,
refresh
}: Props = $props()
let schemaKeys = $derived(Object.keys(dbSchema.schema ?? {}))
@@ -79,10 +81,6 @@
| undefined = $state()
let dbTableEditorState: { open: boolean } = $state({ open: false })
let dbTableEditorProps = $derived(
dbTableEditorPropsFactory?.({ selectedSchemaKey: selected.schemaKey })
)
</script>
<Splitpanes>
@@ -111,47 +109,41 @@
>
<Table2 class="text-primary shrink-0" size={16} />
<p class="truncate text-ellipsis grow text-left text-emphasis text-xs">{tableKey}</p>
{#if dbTableActionsFactory}
{@const dbTableActions = dbTableActionsFactory.map((f) =>
f({
tableKey: `${selected.schemaKey}.${tableKey}`,
refresh: refresh ?? (() => {})
})
)}
<DropdownV2
items={() =>
dbTableActions.map((tableAction) => ({
displayName: tableAction.displayName,
...(tableAction.icon ? { icon: tableAction.icon } : {}),
action: () =>
(askingForConfirmation = {
title: tableAction.confirmTitle ?? 'Are you sure ?',
confirmationText: tableAction.confirmBtnText ?? 'Confirm',
open: true,
onConfirm: async () => {
askingForConfirmation && (askingForConfirmation.loading = true)
try {
await tableAction.action()
tableAction.successText && sendUserToast(tableAction.successText)
} catch (e) {
let msg: string | undefined = (e as Error).message
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
sendUserToast(msg ?? 'Action failed!', true)
}
askingForConfirmation = undefined
<DropdownV2
items={() => [
{
displayName: 'Delete table',
icon: Trash2Icon,
action: () =>
(askingForConfirmation = {
title: `Are you sure you want to delete ${tableKey} ? This action is irreversible`,
confirmationText: 'Delete permanently',
open: true,
onConfirm: async () => {
askingForConfirmation && (askingForConfirmation.loading = true)
try {
await dbSchemaOps.onDelete({ tableKey })
refresh?.()
sendUserToast(`Table '${tableKey}' deleted successfully`)
} catch (e) {
let msg: string | undefined = (e as Error).message
if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined
sendUserToast(msg ?? 'Action failed!', true)
}
})
}))}
class="w-fit"
>
<svelte:fragment slot="buttonReplacement">
<MoreVertical
size={8}
class="w-8 h-8 p-2 hover:bg-surface-hover cursor-pointer rounded-md"
/>
</svelte:fragment>
</DropdownV2>
{/if}
askingForConfirmation = undefined
}
})
}
]}
class="w-fit"
>
<svelte:fragment slot="buttonReplacement">
<MoreVertical
size={8}
class="w-8 h-8 p-2 hover:bg-surface-hover cursor-pointer rounded-md"
/>
</svelte:fragment>
</DropdownV2>
</button>
{/each}
</div>
@@ -176,31 +168,30 @@
</Pane>
</Splitpanes>
<ConfirmationModal
{...askingForConfirmation ?? { confirmationText: '', title: '' }}
on:canceled={() => (askingForConfirmation = undefined)}
on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})}
/>
<Portal>
<ConfirmationModal
{...askingForConfirmation ?? { confirmationText: '', title: '' }}
on:canceled={() => (askingForConfirmation = undefined)}
on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})}
/>
</Portal>
{#if dbTableEditorProps}
<Drawer
size="600px"
open={dbTableEditorState.open}
on:close={() => (dbTableEditorState = { open: false })}
>
<DrawerContent
on:close={() => (dbTableEditorState = { open: false })}
title="Create a new table"
>
<DbTableEditor
{...dbTableEditorProps}
{dbSchema}
currentSchema={selected.schemaKey}
onConfirm={async (values) => {
await dbTableEditorProps.onConfirm(values)
dbTableEditorState = { open: false }
}}
/>
</DrawerContent>
</Drawer>
{/if}
<Drawer
size="600px"
open={dbTableEditorState.open}
on:close={() => (dbTableEditorState = { open: false })}
>
<DrawerContent on:close={() => (dbTableEditorState = { open: false })} title="Create a new table">
<DbTableEditor
{dbSchema}
currentSchema={selected.schemaKey}
onConfirm={async (values) => {
await dbSchemaOps.onCreate({ values, schema: selected.schemaKey })
refresh?.()
dbTableEditorState = { open: false }
}}
{dbType}
previewSql={(values) => dbSchemaOps.previewCreateSql({ values, schema: selected.schemaKey })}
/>
</DrawerContent>
</Drawer>

View File

@@ -5,26 +5,19 @@
import DrawerContent from './common/drawer/DrawerContent.svelte'
import { sendUserToast, sortArray } from '$lib/utils'
import { ArrowLeft, Expand, Loader2, Minimize, RefreshCcw } from 'lucide-svelte'
import {
dbSupportsSchemas,
getLanguageByResourceType,
type TableMetadata
} from './apps/components/display/dbtable/utils'
import { dbSupportsSchemas, type TableMetadata } from './apps/components/display/dbtable/utils'
import DbManager from './DBManager.svelte'
import {
dbDeleteTableActionWithPreviewScript,
dbSchemaOpsWithPreviewScripts,
dbTableOpsWithPreviewScripts,
getDatabaseArg,
getDbType,
getDucklakeSchema
} from './dbOps'
import { makeCreateTableQuery } from './apps/components/display/dbtable/queries/createTable'
import { runScriptAndPollResult } from './jobs/utils'
import { Pane, Splitpanes } from 'svelte-splitpanes'
import SqlRepl from './SqlRepl.svelte'
import SimpleAgTable from './SimpleAgTable.svelte'
import { untrack } from 'svelte'
import type { DbInput } from './dbTypes'
import { wrapDucklakeQuery } from './ducklake'
import {
getDbSchemas,
loadAllTablesMetaData,
@@ -163,7 +156,7 @@
>
{#if dbSchema && $workspaceStore && input}
{@const _input = input}
{@const dbType = input.type == 'database' ? input.resourceType : 'duckdb'}
{@const dbType = getDbType(_input)}
<Splitpanes horizontal>
<Pane class="relative">
<!-- svelte-ignore a11y_click_events_have_key_events -->
@@ -197,25 +190,12 @@
input: _input,
workspace: $workspaceStore
})}
dbTableActionsFactory={[
dbDeleteTableActionWithPreviewScript({ input: _input, workspace: $workspaceStore })
]}
{refresh}
dbTableEditorPropsFactory={({ selectedSchemaKey }) => ({
dbType,
previewSql: (values) => makeCreateTableQuery(values, dbType, selectedSchemaKey),
async onConfirm(values) {
const dbArg = getDatabaseArg(input)
const language = getLanguageByResourceType(dbType)
let query = makeCreateTableQuery(values, dbType, selectedSchemaKey)
if (input?.type === 'ducklake') query = wrapDucklakeQuery(query, input.ducklake)
await runScriptAndPollResult({
workspace: $workspaceStore,
requestBody: { args: dbArg, content: query, language }
})
refresh()
}
dbSchemaOps={dbSchemaOpsWithPreviewScripts({
input: _input,
workspace: $workspaceStore
})}
{dbType}
{refresh}
/>
</Pane>
<Pane bind:size={replPanelSize} minSize={REPL_MIN_SIZE} class="relative">

View File

@@ -36,14 +36,6 @@
if (Object.values(errs).every((v) => !v)) return undefined
return errs
}
export type DBTableEditorProps = {
onConfirm: (values: CreateTableValues) => void | Promise<void>
previewSql?: (values: CreateTableValues) => string
dbType: DbType
dbSchema?: DBSchema
currentSchema?: string
}
</script>
<script lang="ts">
@@ -71,8 +63,17 @@
import { safeSelectItems } from './select/utils.svelte'
import TextInput from './text_input/TextInput.svelte'
import type { DbType } from './dbTypes'
import Portal from './Portal.svelte'
const { onConfirm, dbType, previewSql, dbSchema, currentSchema }: DBTableEditorProps = $props()
type Props = {
onConfirm: (values: CreateTableValues) => void | Promise<void>
previewSql?: (values: CreateTableValues) => string
dbType: DbType
dbSchema?: DBSchema
currentSchema?: string
}
const { onConfirm, dbType, previewSql, dbSchema, currentSchema }: Props = $props()
const columnTypes = DB_TYPES[dbType]
const defaultColumnType = (
@@ -392,23 +393,25 @@
>
</div>
<ConfirmationModal
{...askingForConfirmation ?? { confirmationText: '', title: '' }}
on:canceled={() => (askingForConfirmation = undefined)}
on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})}
>
{#if askingForConfirmation?.codeContent}
<div class="bg-surface-secondary border border-surface-selected rounded-md p-2 relative">
<code class="whitespace-pre-wrap">
{askingForConfirmation.codeContent}
</code>
<Button
on:click={() => copyToClipboard(askingForConfirmation?.codeContent)}
size="xs"
startIcon={{ icon: ClipboardCopy }}
color="none"
wrapperClasses="absolute z-10 top-0 right-0"
></Button>
</div>
{/if}
</ConfirmationModal>
<Portal>
<ConfirmationModal
{...askingForConfirmation ?? { confirmationText: '', title: '' }}
on:canceled={() => (askingForConfirmation = undefined)}
on:confirmed={askingForConfirmation?.onConfirm ?? (() => {})}
>
{#if askingForConfirmation?.codeContent}
<div class="bg-surface-secondary border border-surface-selected rounded-md p-2 relative">
<code class="whitespace-pre-wrap">
{askingForConfirmation.codeContent}
</code>
<Button
on:click={() => copyToClipboard(askingForConfirmation?.codeContent)}
size="xs"
startIcon={{ icon: ClipboardCopy }}
color="none"
wrapperClasses="absolute z-10 top-0 right-0"
></Button>
</div>
{/if}
</ConfirmationModal>
</Portal>

View File

@@ -5,13 +5,16 @@ import { makeCountQuery } from './apps/components/display/dbtable/queries/count'
import { makeUpdateQuery } from './apps/components/display/dbtable/queries/update'
import { makeDeleteQuery } from './apps/components/display/dbtable/queries/delete'
import { makeInsertQuery } from './apps/components/display/dbtable/queries/insert'
import { Trash2 } from 'lucide-svelte'
import { makeDeleteTableQuery } from './apps/components/display/dbtable/queries/deleteTable'
import type { DBSchema, SQLSchema } from '$lib/stores'
import { stringifySchema } from './copilot/lib'
import type { DbInput, DbType } from './dbTypes'
import { wrapDucklakeQuery } from './ducklake'
import { assert } from '$lib/utils'
import {
makeCreateTableQuery,
type CreateTableValues
} from './apps/components/display/dbtable/queries/createTable'
export type IDbTableOps = {
dbType: DbType
@@ -108,51 +111,41 @@ export function dbTableOpsWithPreviewScripts({
}
}
export type DbTableAction = {
action: () => void | Promise<void>
displayName: string
confirmTitle?: string
confirmBtnText?: string
icon?: any
successText?: string
export type IDbSchemaOps = {
onDelete: (params: { tableKey: string }) => Promise<void>
onCreate: (params: { values: CreateTableValues; schema?: string }) => Promise<void>
previewCreateSql: (params: { values: CreateTableValues; schema?: string }) => string
}
export type DbTableActionFactory = (params: {
tableKey: string
refresh: () => void
}) => DbTableAction
export function dbDeleteTableActionWithPreviewScript({
export function dbSchemaOpsWithPreviewScripts({
workspace,
input
}: {
workspace: string
input: DbInput
}): DbTableActionFactory {
}): IDbSchemaOps {
const dbType = getDbType(input)
const dbArg = getDatabaseArg(input)
return ({ tableKey, refresh }) => ({
confirmTitle: `Are you sure you want to delete '${tableKey}' ? This action is irreversible`,
displayName: 'Delete',
confirmBtnText: `Delete permanently`,
icon: Trash2,
successText: `Table '${tableKey}' deleted successfully`,
action: async () => {
const dbType = getDbType(input)
const language = getLanguageByResourceType(dbType)
const language = getLanguageByResourceType(dbType)
return {
onDelete: async ({ tableKey }) => {
let deleteQuery = makeDeleteTableQuery(tableKey, dbType)
if (input.type === 'ducklake') deleteQuery = wrapDucklakeQuery(deleteQuery, input.ducklake)
await runScriptAndPollResult({
workspace,
requestBody: {
args: { ...dbArg },
language,
content: deleteQuery
}
requestBody: { args: { ...dbArg }, language, content: deleteQuery }
})
refresh()
}
})
},
onCreate: async ({ values, schema }) => {
let query = makeCreateTableQuery(values, dbType, schema)
if (input?.type === 'ducklake') query = wrapDucklakeQuery(query, input.ducklake)
await runScriptAndPollResult({
workspace,
requestBody: { args: dbArg, content: query, language }
})
},
previewCreateSql: ({ values, schema }) => makeCreateTableQuery(values, dbType, schema)
}
}
export async function getDucklakeSchema({