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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user