diff --git a/frontend/src/lib/components/DBManager.svelte b/frontend/src/lib/components/DBManager.svelte index 45147d7ff7..1f35622dfb 100644 --- a/frontend/src/lib/components/DBManager.svelte +++ b/frontend/src/lib/components/DBManager.svelte @@ -48,9 +48,15 @@ table: string } - /** A create started from a row of another data table. Switching data table - * re-mounts this component, so the request has to travel through the parent. */ - export type PendingCreate = { kind: 'table'; schema: string } | { kind: 'schema' } + /** A row action started on a data table other than the current one. Switching + * data table re-mounts this component, so the request — which has to run + * against the connection of the data table it names — travels through the + * parent. */ + export type PendingRowAction = + | { kind: 'create-table'; schema: string } + | { kind: 'create-schema' } + | { kind: 'alter-table'; schema: string; table: string } + | { kind: 'delete-table'; schema: string; table: string } type Props = { dbType: DbType @@ -73,7 +79,7 @@ onSelectDatatable?: (datatable: string) => void /** Role the manager is connected as, when it is not the default one. */ currentRole?: string - pendingCreate?: PendingCreate | undefined + pendingAction?: PendingRowAction | undefined /** Row-menu actions on a data table, run against that row's data table. */ onDatatableAction?: (datatable: string, action: DatatableRowAction) => void canManageDatatable?: boolean @@ -104,7 +110,7 @@ datatableTreeLoading, onSelectDatatable, currentRole, - pendingCreate = $bindable(undefined), + pendingAction = $bindable(undefined), onDatatableAction, canManageDatatable = false, multiSelectMode = false, @@ -278,16 +284,14 @@ expandOverrides = next } - // Row actions stay out of the way until you are on the row. + // The row menu takes the place of the row's own icon while you are on the row. const rowActionsClass = - 'absolute right-0 opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100' + 'absolute left-1/2 -translate-x-1/2 opacity-0 transition-opacity focus-visible:opacity-100 group-hover:opacity-100' + const rowIconClass = (hasMenu: boolean) => + 'shrink-0 transition-opacity ' + (hasMenu ? 'group-hover:opacity-0' : '') - // The chevron is the resting state of that slot: it gives way to the menu - // rather than sitting beside it. const rowChevronClass = (open: boolean) => - 'absolute right-0 pointer-events-none text-secondary transition-all opacity-100 ' + - (multiSelectMode ? '' : 'group-hover:opacity-0 ') + - (open ? '' : '-rotate-90') + 'shrink-0 text-secondary transition-transform ' + (open ? '' : '-rotate-90') /** Reveal a node, dropping a stale "closed" that would hide a new selection. */ function reveal(dt: string | undefined, schemaKey?: string) { @@ -311,38 +315,71 @@ selected = { schemaKey, tableKey } } - function startCreateTable(dt: string | undefined, schemaKey: string) { - if (dt !== undefined && dt !== currentDatatable) { - selectedSchemaKey = schemaKey - pendingCreate = { kind: 'table', schema: schemaKey } - onSelectDatatable?.(dt) - return - } - selected = { schemaKey, tableKey: undefined } + /** Run a row action on the data table it belongs to, switching to it first + * when it is not the one the manager is connected to. */ + function onDatatable(dt: string | undefined, action: PendingRowAction): boolean { + if (dt === undefined || dt === currentDatatable) return true + if ('schema' in action) selectedSchemaKey = action.schema + if ('table' in action) selectedTableKey = action.table + pendingAction = action + onSelectDatatable?.(dt) + return false + } + + function startCreateTable(dt: string | undefined, schema: string) { + if (!onDatatable(dt, { kind: 'create-table', schema })) return + selected = { schemaKey: schema, tableKey: undefined } dbTableEditorState = { open: true } } function startCreateSchema(dt: string | undefined) { - if (dt !== undefined && dt !== currentDatatable) { - pendingCreate = { kind: 'schema' } - onSelectDatatable?.(dt) - return - } + if (!onDatatable(dt, { kind: 'create-schema' })) return newSchemaDialogOpen = true } - // Finishes a create requested before the switch, now that this component is + function startAlterTable(dt: string | undefined, schema: string, table: string) { + if (!onDatatable(dt, { kind: 'alter-table', schema, table })) return + selected = { schemaKey: schema, tableKey: table } + dbTableEditorState = { open: true, alterTableKey: table } + } + + function startDeleteTable(dt: string | undefined, schema: string, table: string) { + if (!onDatatable(dt, { kind: 'delete-table', schema, table })) return + askingForConfirmation = { + title: `Are you sure you want to delete ${table} ? This action is irreversible`, + confirmationText: 'Delete permanently', + open: true, + id: 'db-manager-delete-table-confirmation-modal', + onConfirm: async () => { + askingForConfirmation && (askingForConfirmation.loading = true) + try { + await dbSchemaOps.onDelete({ tableKey: table, schema }) + refresh?.() + sendUserToast(`Table '${table}' deleted successfully`) + } catch (e) { + let msg: string | undefined = (e as any).body ?? (e as Error).message + if (typeof msg !== 'string') msg = e ? JSON.stringify(e) : undefined + sendUserToast(msg ?? 'Action failed!', true) + } + askingForConfirmation = undefined + } + } + } + + // Finishes an action requested before the switch, now that this component is // mounted against the data table it targeted. $effect(() => { - const req = pendingCreate + const req = pendingAction if (!req || !schemaKeys.length) return - pendingCreate = undefined - if (req.kind === 'schema') { + pendingAction = undefined + if (req.kind === 'create-schema') { newSchemaDialogOpen = true - } else if (schemaKeys.includes(req.schema)) { - selected = { schemaKey: req.schema, tableKey: undefined } - dbTableEditorState = { open: true } + return } + if (!schemaKeys.includes(req.schema)) return + if (req.kind === 'create-table') startCreateTable(undefined, req.schema) + else if (req.kind === 'alter-table') startAlterTable(undefined, req.schema, req.table) + else startDeleteTable(undefined, req.schema, req.table) }) let search = $state('') @@ -466,6 +503,7 @@ {#each treeRoots as root (root.datatable ?? '')} {@const dtOpen = isExpanded(root.datatable)} {#if root.datatable !== undefined} + {@const hasMenu = !multiSelectMode && onDatatableAction !== undefined} {/if} @@ -552,10 +590,8 @@ class="shrink-0" /> {/if} - - {sc.schemaKey} -
- +
+ {#if !multiSelectMode} (schemaPermissionsOpen = true) } ]} - class="-mr-2 {rowActionsClass}" + class={rowActionsClass} btnId={'db-manager-schema-actions-' + onlyAlphaNumAndUnderscore(sc.schemaKey)} /> {/if}
+ {sc.schemaKey} +
+ +
{/if} {#if schemaOpen || !dbSupportsSchemas} @@ -587,6 +627,7 @@ schema: sc.schemaKey, table: tableKey }} + {@const hasMenu = !multiSelectMode} {@const isSelected = root.datatable === currentDatatable && selected.schemaKey === sc.schemaKey && @@ -608,66 +649,38 @@ class="shrink-0" /> {/if} +
+ + {#if hasMenu} + [ + { + displayName: 'Delete table', + icon: Trash2Icon, + action: () => startDeleteTable(root.datatable, sc.schemaKey, tableKey) + }, + { + displayName: 'Alter table', + icon: EditIcon, + action: () => startAlterTable(root.datatable, sc.schemaKey, tableKey) + } + ]} + class={rowActionsClass} + btnId={'db-manager-table-actions-' + onlyAlphaNumAndUnderscore(tableKey)} + /> + {/if} +
+

+ {tableKey} +

{#if asset} - - +
- - {:else} - - {/if} -

- {tableKey} -

- {#if !multiSelectMode && (root.datatable === currentDatatable || root.datatable === undefined)} - [ - { - 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, - id: 'db-manager-delete-table-confirmation-modal', - onConfirm: async () => { - askingForConfirmation && (askingForConfirmation.loading = true) - try { - await dbSchemaOps.onDelete({ - tableKey, - schema: sc.schemaKey - }) - refresh?.() - sendUserToast(`Table '${tableKey}' deleted successfully`) - } catch (e) { - let msg: string | undefined = - (e as any).body ?? (e as Error).message - if (typeof msg !== 'string') - msg = e ? JSON.stringify(e) : undefined - sendUserToast(msg ?? 'Action failed!', true) - } - askingForConfirmation = undefined - } - }) - }, - { - displayName: 'Alter table', - icon: EditIcon, - action: () => { - selected = { schemaKey: sc.schemaKey, tableKey } - dbTableEditorState = { open: true, alterTableKey: tableKey } - } - } - ]} - class="mr-1 {rowActionsClass}" - btnId={'db-manager-table-actions-' + onlyAlphaNumAndUnderscore(tableKey)} - /> +
{/if} {/each} diff --git a/frontend/src/lib/components/DBManagerContent.svelte b/frontend/src/lib/components/DBManagerContent.svelte index 560064f352..91c443a7fb 100644 --- a/frontend/src/lib/components/DBManagerContent.svelte +++ b/frontend/src/lib/components/DBManagerContent.svelte @@ -23,7 +23,7 @@ import type { DbInput } from './dbTypes' import { getDbSchemas, loadAllTablesMetaData } from './apps/components/display/dbtable/metadata' - import type { PendingCreate, SelectedTable } from './DBManager.svelte' + import type { PendingRowAction, SelectedTable } from './DBManager.svelte' import { getDbFeatures } from './apps/components/display/dbtable/dbFeatures' import { resource } from 'runed' import ConfirmationModal from './common/confirmationModal/ConfirmationModal.svelte' @@ -43,7 +43,7 @@ datatableTree?: DataTableTables[] datatableTreeLoading?: boolean onSelectDatatable?: (datatable: string) => void - pendingCreate?: PendingCreate | undefined + pendingAction?: PendingRowAction | undefined onDatatableAction?: (datatable: string, action: DatatableRowAction) => void canManageDatatable?: boolean /** Enable multi-select mode with checkboxes in sidebar */ @@ -71,7 +71,7 @@ datatableTree, datatableTreeLoading, onSelectDatatable, - pendingCreate = $bindable(undefined), + pendingAction = $bindable(undefined), onDatatableAction, canManageDatatable, multiSelectMode = false, @@ -326,7 +326,7 @@ {datatableTreeLoading} {onSelectDatatable} currentRole={input.type === 'database' ? input.role : undefined} - bind:pendingCreate + bind:pendingAction {onDatatableAction} {canManageDatatable} {onImport} diff --git a/frontend/src/lib/components/DBManagerDrawer.svelte b/frontend/src/lib/components/DBManagerDrawer.svelte index 9dee0f645d..a1ba030108 100644 --- a/frontend/src/lib/components/DBManagerDrawer.svelte +++ b/frontend/src/lib/components/DBManagerDrawer.svelte @@ -7,7 +7,7 @@ import Select from './select/Select.svelte' import { ArrowLeft, Copy, Expand, Minimize, RefreshCcw } from 'lucide-svelte' import DBManagerContent from './DBManagerContent.svelte' - import type { PendingCreate } from './DBManager.svelte' + import type { PendingRowAction } from './DBManager.svelte' import DataTableMigrationsButton from './workspaceSettings/DataTableMigrationsButton.svelte' import DataTablePermissionsButton from './workspaceSettings/DataTablePermissionsButton.svelte' import { resource } from 'runed' @@ -37,7 +37,7 @@ // A create started on a data table other than the current one: survives the // re-mount the switch causes. - let pendingCreate = $state(undefined) + let pendingAction = $state(undefined) // Every data table with its schemas and tables, in one call: this is what the // left pane's tree navigates, so it has to cover the data tables the user is @@ -257,7 +257,7 @@ datatableTree={uriState.isDatatableInput ? datatables.current : undefined} datatableTreeLoading={datatables.loading} onSelectDatatable={(dt) => (uriState.selectedDatatable = dt)} - bind:pendingCreate + bind:pendingAction canManageDatatable={!!($superadmin || $userStore?.is_admin)} onDatatableAction={runDatatableAction} bind:workerTag={() => workerTag.tag, (v) => (workerTag.tag = v)} diff --git a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte index 72bd74254b..1c52530b5f 100644 --- a/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte +++ b/frontend/src/lib/components/raw_apps/RawAppDataTableDrawer.svelte @@ -10,7 +10,7 @@ import { ArrowLeft, Expand, Minimize, Plus, RefreshCcw } from 'lucide-svelte' import DBManagerContent from '../DBManagerContent.svelte' import type { DbInput } from '../dbTypes' - import type { PendingCreate, SelectedTable } from '../DBManager.svelte' + import type { PendingRowAction, SelectedTable } from '../DBManager.svelte' import { getRawAppOperatingWorkspace } from './rawAppWorkspace' import { useDbManagerTag } from '../dbManagerTag.svelte' import DbWorkerTagButton from '../DbWorkerTagButton.svelte' @@ -41,7 +41,7 @@ let selectedTables = $state([]) // Survives the re-mount a data table switch causes. - let pendingCreate = $state(undefined) + let pendingAction = $state(undefined) // Selected schema/table from DBManager (for preview) let selectedSchemaKey = $state(undefined) @@ -184,7 +184,7 @@ datatableTree={datatableTree.current} datatableTreeLoading={datatableTree.loading} onSelectDatatable={(dt) => (selectedDatatable = dt)} - bind:pendingCreate + bind:pendingAction /> {/key} {:else}