@adecore/database 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DatabaseProvider.d.ts +4 -3
- package/dist/DatabaseProvider.js +15 -3
- package/dist/KeyIcon.d.ts +4 -0
- package/dist/KeyIcon.js +14 -0
- package/dist/actions.d.ts +12 -0
- package/dist/client-context.d.ts +3 -1
- package/dist/client-context.js +2 -0
- package/dist/code-text.d.ts +1 -0
- package/dist/code-text.js +2 -0
- package/dist/column-keys.d.ts +5 -0
- package/dist/column-keys.js +2 -0
- package/dist/connections/ConnectionManager.d.ts +3 -2
- package/dist/connections/ConnectionManager.js +27 -16
- package/dist/connections/connection-config.js +2 -0
- package/dist/console/QueryConsole.d.ts +8 -3
- package/dist/console/QueryConsole.js +110 -50
- package/dist/console/SqlEditor.d.ts +2 -12
- package/dist/console/SqlEditor.js +29 -18
- package/dist/console/StatementResultView.d.ts +5 -1
- package/dist/console/StatementResultView.js +18 -5
- package/dist/console/TransactionControls.js +3 -6
- package/dist/console/editor-slot.d.ts +18 -0
- package/dist/console/editor-slot.js +1 -0
- package/dist/console/result-keys.d.ts +15 -0
- package/dist/console/result-keys.js +79 -0
- package/dist/designer/ColumnsEditor.js +2 -2
- package/dist/designer/DesignerView.d.ts +3 -2
- package/dist/designer/DesignerView.js +5 -4
- package/dist/designer/SqlPreview.js +2 -1
- package/dist/designer/TableDesigner.d.ts +3 -2
- package/dist/designer/TableDesigner.js +2 -2
- package/dist/explorer/DatabaseExplorer.d.ts +4 -1
- package/dist/explorer/DatabaseExplorer.js +79 -23
- package/dist/explorer/RowMenu.js +2 -2
- package/dist/explorer/expansion.js +2 -2
- package/dist/explorer/folders.d.ts +16 -0
- package/dist/explorer/folders.js +1 -0
- package/dist/explorer/tree.d.ts +55 -4
- package/dist/explorer/tree.js +137 -23
- package/dist/grid/CellEditor.js +3 -1
- package/dist/grid/DataGrid.d.ts +2 -2
- package/dist/grid/DataGrid.js +61 -59
- package/dist/grid/EnumPicker.js +2 -1
- package/dist/grid/GridHeaderCell.d.ts +1 -2
- package/dist/grid/GridHeaderCell.js +12 -5
- package/dist/grid/GridRowView.d.ts +49 -0
- package/dist/grid/GridRowView.js +51 -0
- package/dist/grid/RecordView.d.ts +4 -2
- package/dist/grid/RecordView.js +71 -22
- package/dist/grid/ValueDock.d.ts +3 -1
- package/dist/grid/ValueDock.js +9 -3
- package/dist/grid/cell-id.d.ts +1 -0
- package/dist/grid/cell-id.js +2 -0
- package/dist/grid/layout.d.ts +3 -2
- package/dist/grid/layout.js +15 -6
- package/dist/grid/types.d.ts +8 -1
- package/dist/grid/types.js +4 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/locales/en.json +32 -15
- package/dist/locales/nl.json +32 -15
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/messages.d.ts +1 -1
- package/dist/protocol/messages.js +1 -1
- package/dist/protocol/schema.d.ts +17 -0
- package/dist/structure/StructureView.d.ts +5 -3
- package/dist/structure/StructureView.js +8 -6
- package/dist/table/CommandField.js +2 -2
- package/dist/table/TableStatusBar.js +2 -2
- package/dist/table/TableToolbar.d.ts +3 -4
- package/dist/table/TableToolbar.js +4 -3
- package/dist/table/TableView.d.ts +4 -2
- package/dist/table/TableView.js +63 -91
- package/dist/table/grid-rows.js +4 -1
- package/dist/table/useTableTransfer.d.ts +3 -1
- package/dist/table/useTableTransfer.js +19 -5
- package/dist/testing/fake.d.ts +3 -1
- package/dist/testing/fake.js +7 -1
- package/dist/value/ValueEditor.d.ts +9 -0
- package/dist/value/ValueEditor.js +102 -0
- package/dist/value/ValuePanel.d.ts +1 -1
- package/dist/value/ValuePanel.js +3 -94
- package/dist/workbench/DatabaseWorkbench.js +4 -7
- package/package.json +6 -6
- package/src/DatabaseProvider.tsx +24 -5
- package/src/KeyIcon.tsx +15 -0
- package/src/actions.ts +28 -2
- package/src/client-context.ts +5 -1
- package/src/code-text.ts +2 -0
- package/src/column-keys.ts +8 -0
- package/src/connections/ConnectionManager.tsx +98 -68
- package/src/connections/connection-config.ts +2 -0
- package/src/console/QueryConsole.tsx +246 -150
- package/src/console/SqlEditor.tsx +33 -37
- package/src/console/StatementResultView.tsx +61 -27
- package/src/console/TransactionControls.tsx +8 -11
- package/src/console/editor-slot.ts +27 -0
- package/src/console/result-keys.ts +109 -0
- package/src/designer/ColumnsEditor.tsx +3 -3
- package/src/designer/DesignerView.tsx +14 -3
- package/src/designer/SqlPreview.tsx +2 -1
- package/src/designer/TableDesigner.tsx +5 -2
- package/src/explorer/DatabaseExplorer.tsx +170 -26
- package/src/explorer/RowMenu.tsx +2 -2
- package/src/explorer/expansion.ts +4 -4
- package/src/explorer/folders.ts +25 -0
- package/src/explorer/tree.ts +219 -28
- package/src/grid/CellEditor.tsx +3 -2
- package/src/grid/DataGrid.tsx +114 -176
- package/src/grid/EnumPicker.tsx +2 -1
- package/src/grid/GridHeaderCell.tsx +14 -9
- package/src/grid/GridRowView.tsx +184 -0
- package/src/grid/RecordView.tsx +188 -75
- package/src/grid/ValueDock.tsx +12 -3
- package/src/grid/cell-id.ts +2 -0
- package/src/grid/layout.ts +16 -6
- package/src/grid/types.ts +15 -3
- package/src/index.ts +5 -1
- package/src/locales/en.json +32 -15
- package/src/locales/nl.json +32 -15
- package/src/protocol/index.ts +13 -1
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/schema.ts +28 -0
- package/src/structure/StructureView.tsx +16 -6
- package/src/table/CommandField.tsx +2 -2
- package/src/table/TableStatusBar.tsx +3 -3
- package/src/table/TableToolbar.tsx +107 -92
- package/src/table/TableView.tsx +95 -132
- package/src/table/grid-rows.ts +5 -3
- package/src/table/useTableTransfer.ts +21 -6
- package/src/testing/fake.ts +18 -1
- package/src/value/ValueEditor.tsx +213 -0
- package/src/value/ValuePanel.tsx +4 -206
- package/src/workbench/DatabaseWorkbench.tsx +9 -10
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import { foreignKeyOf, referenceOf } from '../table/references.js';
|
|
3
|
+
const tableKey = ({ schema, table }) => `${schema}\u0000${table}`;
|
|
4
|
+
/* The tables the columns of a result come from, each once, in the order they first appear. */
|
|
5
|
+
export const sourceTablesOf = (columns) => {
|
|
6
|
+
const tables = new Map();
|
|
7
|
+
for (const { source } of columns) {
|
|
8
|
+
if (source !== undefined && !tables.has(tableKey(source))) {
|
|
9
|
+
tables.set(tableKey(source), { schema: source.schema, table: source.table });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return [...tables.values()];
|
|
13
|
+
};
|
|
14
|
+
/*
|
|
15
|
+
* The keys of a result's columns, from the structures of the tables they come from by key (`schema`, NUL,
|
|
16
|
+
* `table`). A column without a source, or of a table with no structure, is marked as nothing.
|
|
17
|
+
*/
|
|
18
|
+
export const resultKeysOf = (engine, columns, structures) => {
|
|
19
|
+
const structureOf = (column) => column?.source === undefined ? undefined : structures.get(tableKey(column.source));
|
|
20
|
+
const foreignKeyAt = (column) => {
|
|
21
|
+
const source = columns[column]?.source;
|
|
22
|
+
const structure = structureOf(columns[column]);
|
|
23
|
+
return source === undefined || structure === undefined ? undefined : foreignKeyOf(structure.foreignKeys, source.column);
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
columns: columns.map((column) => {
|
|
27
|
+
const structure = structureOf(column);
|
|
28
|
+
const name = column.source?.column;
|
|
29
|
+
if (structure === undefined || name === undefined) {
|
|
30
|
+
return column;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
...column,
|
|
34
|
+
primaryKey: structure.primaryKey.includes(name),
|
|
35
|
+
foreignKey: structure.foreignKeys.some((key) => key.columns.includes(name))
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
isReference: (column) => foreignKeyAt(column) !== undefined,
|
|
39
|
+
referenceAt: (column, cells) => {
|
|
40
|
+
const key = foreignKeyAt(column);
|
|
41
|
+
const source = columns[column]?.source;
|
|
42
|
+
if (key === undefined || source === undefined) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// The columns of the result as the columns of that table, so a join names each key column of its own table only.
|
|
46
|
+
const asTable = columns.map((entry) => ({
|
|
47
|
+
name: entry.source !== undefined && tableKey(entry.source) === tableKey(source) ? entry.source.column : '',
|
|
48
|
+
kind: entry.kind
|
|
49
|
+
}));
|
|
50
|
+
return referenceOf(engine, key, asTable, cells);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/*
|
|
55
|
+
* The keys of a result set, once the structures of the tables its columns come from have loaded: each table
|
|
56
|
+
* once per result, through the console's session. `null` while they load and for what is no result set.
|
|
57
|
+
*/
|
|
58
|
+
export function useResultKeys(session, engine, result) {
|
|
59
|
+
const [loaded, setLoaded] = useState(null);
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (session === null || result?.kind !== 'rows') {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
const tables = sourceTablesOf(result.columns);
|
|
65
|
+
if (tables.length === 0) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
const controller = new AbortController();
|
|
69
|
+
// A table the server named but cannot describe, such as a derived one, marks no column.
|
|
70
|
+
const loads = tables.map((table) => session.structure(table.schema, table.table, { signal: controller.signal }).then((structure) => [tableKey(table), structure], () => null));
|
|
71
|
+
void Promise.all(loads).then((entries) => {
|
|
72
|
+
if (!controller.signal.aborted) {
|
|
73
|
+
setLoaded({ result, structures: new Map(entries.filter((entry) => entry !== null)) });
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return () => controller.abort();
|
|
77
|
+
}, [session, result]);
|
|
78
|
+
return useMemo(() => (engine === undefined || result?.kind !== 'rows' || loaded?.result !== result ? null : resultKeysOf(engine, result.columns, loaded.structures)), [engine, result, loaded]);
|
|
79
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useId } from 'react';
|
|
|
3
3
|
import clsx from 'clsx';
|
|
4
4
|
import { ArrowDown, ArrowUp, KeyRound, Plus, Trash2 } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Button, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
|
|
6
|
+
import { Button, ButtonGroup, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
|
|
7
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
8
|
import { typeSuggestionsOf } from '../ddl/index.js';
|
|
9
9
|
import { addColumn, moveColumn, patchColumn, removeColumn, togglePrimaryKey } from './edit.js';
|
|
@@ -19,6 +19,6 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
19
19
|
const keyAt = draft.primaryKey.indexOf(column.name);
|
|
20
20
|
const locked = disabled || column.generated;
|
|
21
21
|
const patch = (change) => onChange(patchColumn(draft, column.key, change));
|
|
22
|
-
return (_jsxs("tr", { children: [_jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs(
|
|
22
|
+
return (_jsxs("tr", { children: [_jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs(ButtonGroup, { children: [_jsx(IconButton, { icon: ArrowUp, size: "xs", label: t('designer.columns.moveUp'), disabled: disabled || at === 0, onClick: () => onChange(moveColumn(draft, column.key, -1)) }), _jsx(IconButton, { icon: ArrowDown, size: "xs", label: t('designer.columns.moveDown'), disabled: disabled || at === draft.columns.length - 1, onClick: () => onChange(moveColumn(draft, column.key, 1)) })] }) }), _jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.name'), spellCheck: false, value: column.name, disabled: locked, onChange: (event) => patch({ name: event.target.value }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: _jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.type'), list: listId, spellCheck: false, value: column.type, disabled: locked, onChange: (event) => patch({ type: event.target.value }) }) }), _jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.notNullFor', { name: column.name }), checked: !column.nullable, disabled: locked, onCheckedChange: (checked) => patch({ nullable: !checked }) }) }), _jsx("td", { className: clsx(TD, 'min-w-36'), children: column.generated ? (_jsx(Pill, { mono: true, children: column.generatedClause ?? t('designer.columns.generated') })) : (_jsx(Input, { size: "sm", mono: true, className: "w-full", "aria-label": t('designer.columns.default'), placeholder: t('designer.columns.noDefault'), spellCheck: false, value: column.defaultValue ?? '', disabled: disabled, onChange: (event) => patch({ defaultValue: event.target.value === '' ? null : event.target.value }) })) }), mysql && (_jsx("td", { className: TD, children: _jsx(Checkbox, { className: "mx-auto", label: t('designer.columns.autoIncrementFor', { name: column.name }), checked: column.autoIncrement, disabled: locked, onCheckedChange: (checked) => patch({ autoIncrement: checked }) }) })), _jsx("td", { className: clsx(TD, 'whitespace-nowrap'), children: _jsxs("span", { className: "flex items-center gap-1", children: [_jsx(IconButton, { icon: KeyRound, size: "xs", label: keyAt < 0 ? t('designer.columns.addToKey') : t('designer.columns.removeFromKey'), "aria-pressed": keyAt >= 0, disabled: locked, onClick: () => onChange(togglePrimaryKey(draft, column.key)) }), keyAt >= 0 && draft.primaryKey.length > 1 && (_jsx("span", { className: "text-xs text-text-muted", children: formatNumber(keyAt + 1) }))] }) }), mysql && (_jsx("td", { className: clsx(TD, 'min-w-40'), children: _jsx(Input, { size: "sm", className: "w-full", "aria-label": t('designer.columns.comment'), value: column.comment, disabled: locked, onChange: (event) => patch({ comment: event.target.value }) }) })), _jsx("td", { className: TD, children: _jsx(IconButton, { icon: Trash2, size: "xs", label: t('designer.columns.remove'), disabled: disabled, onClick: () => onChange(removeColumn(draft, column.key)) }) })] }, column.key));
|
|
23
23
|
}) })] })] })), _jsx("div", { className: "shrink-0 border-t border-border-soft px-3 py-2", children: _jsxs(Button, { variant: "secondary", size: "sm", disabled: disabled, onClick: () => onChange(addColumn(draft, defaultType)), children: [_jsx(Icon, { icon: Plus, size: 12 }), t('designer.columns.add')] }) })] }));
|
|
24
24
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Ref } from 'react';
|
|
1
|
+
import type { ReactNode, Ref } from 'react';
|
|
2
2
|
import type { Dialect, TableDraft } from '../ddl/index.ts';
|
|
3
3
|
import { type Plan } from './plan.ts';
|
|
4
4
|
export interface DesignerViewProps {
|
|
@@ -19,7 +19,8 @@ export interface DesignerViewProps {
|
|
|
19
19
|
onCancelConfirm(): void;
|
|
20
20
|
onRevert(): void;
|
|
21
21
|
onDismissFailure(): void;
|
|
22
|
+
toolbarStart?: ReactNode;
|
|
22
23
|
className?: string;
|
|
23
24
|
ref?: Ref<HTMLDivElement>;
|
|
24
25
|
}
|
|
25
|
-
export declare function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, className, ref }: DesignerViewProps): import("react").JSX.Element;
|
|
26
|
+
export declare function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, toolbarStart, className, ref }: DesignerViewProps): import("react").JSX.Element;
|
|
@@ -2,8 +2,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { CircleAlert, Lock, Undo2 } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
|
|
5
|
+
import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Separator, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { CODE_TEXT } from '../code-text.js';
|
|
7
8
|
import { ColumnsEditor } from './ColumnsEditor.js';
|
|
8
9
|
import { ForeignKeysEditor } from './ForeignKeysEditor.js';
|
|
9
10
|
import { IndexesEditor } from './IndexesEditor.js';
|
|
@@ -13,7 +14,7 @@ import { scriptOf } from './plan.js';
|
|
|
13
14
|
import { SqlPreview } from './SqlPreview.js';
|
|
14
15
|
const APPLY_SHORTCUT = /* @__PURE__ */ shortcut('Mod+S');
|
|
15
16
|
/* The designer with everything it needs handed in: the name, the four tabs, what Apply would run and the confirmation before it does. */
|
|
16
|
-
export function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, className, ref }) {
|
|
17
|
+
export function DesignerView({ dialect, schema, draft, isNew, changed, tables, plan, readOnlyReason, failure, confirming, referenceColumns, onChange, onRequestApply, onConfirm, onCancelConfirm, onRevert, onDismissFailure, toolbarStart, className, ref }) {
|
|
17
18
|
const { t } = useTranslation('database');
|
|
18
19
|
const disabled = readOnlyReason !== null;
|
|
19
20
|
const applyReason = readOnlyReason ?? (plan.problems.length > 0 ? t('designer.fixProblems') : plan.statements.length === 0 ? t('designer.noChanges') : null);
|
|
@@ -25,9 +26,9 @@ export function DesignerView({ dialect, schema, draft, isNew, changed, tables, p
|
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
28
|
}
|
|
28
|
-
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2", children: [_jsx(Field, { label: t('designer.tableName'), className: "w-80", children: _jsx(Input, { mono: true, spellCheck: false, placeholder: t('designer.tableNamePlaceholder'), value: draft.name, disabled: disabled, onChange: (event) => onChange(renameTable(draft, event.target.value)) }) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs(Button, { variant: "secondary", disabled: !changed, onClick: onRevert, children: [_jsx(Icon, { icon: Undo2, size: 12 }), t('designer.revert')] }), _jsx(DisabledReason, { reason: applyReason, children: _jsx("span", { className: "inline-flex", children: _jsx(Button, { variant: "primary", disabled: applyReason !== null, onClick: onRequestApply, children: isNew ? t('designer.create') : t('designer.apply') }) }) })] })] }), readOnlyReason !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: Lock, tone: "neutral", className: "w-full", message: readOnlyReason }) })), failure !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: failure, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: onDismissFailure, children: t('designer.dismiss') }) }) })), _jsxs(Tabs.Root, { defaultValue: "columns", className: "flex min-h-0 grow flex-col", children: [_jsxs(Tabs.List, { className: "shrink-0 px-3", children: [_jsxs(Tabs.Tab, { value: "columns", children: [t('designer.tabs.columns'), _jsx(Tabs.Count, { value: draft.columns.length })] }), _jsxs(Tabs.Tab, { value: "indexes", children: [t('designer.tabs.indexes'), _jsx(Tabs.Count, { value: draft.indexes.length })] }), _jsxs(Tabs.Tab, { value: "foreignKeys", children: [t('designer.tabs.foreignKeys'), _jsx(Tabs.Count, { value: draft.foreignKeys.length })] }), _jsx(Tabs.Tab, { value: "options", children: t('designer.tabs.options') })] }), _jsx(Tabs.Panel, { value: "columns", className: "min-h-0 grow overflow-auto", children: _jsx(ColumnsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "indexes", className: "min-h-0 grow overflow-auto", children: _jsx(IndexesEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "foreignKeys", className: "min-h-0 grow overflow-auto", children: _jsx(ForeignKeysEditor, { draft: draft, dialect: dialect, disabled: disabled, schema: schema, tables: tables, referenceColumns: referenceColumns, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "options", className: "min-h-0 grow overflow-auto", children: _jsx(OptionsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) })] }), _jsx(SqlPreview, { plan: plan, className: "shrink-0" }), _jsxs(PromptDialog, { open: confirming, danger: plan.destructive, title: t('designer.confirm.title', { count: plan.statements.length, formatted: formatNumber(plan.statements.length) }), description: isNew ? t('designer.confirm.create', { table: draft.name }) : t('designer.confirm.alter', { table: draft.originalName }), confirmLabel: isNew ? t('designer.create') : t('designer.apply'), confirmBusyLabel: t('designer.applying'), onConfirm: onConfirm, onOpenChange: (open) => {
|
|
29
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), onKeyDown: handleKeyDown, children: [_jsxs("div", { className: "flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2", children: [toolbarStart !== undefined && toolbarStart !== null && toolbarStart !== false && (_jsxs("span", { className: "flex h-8 shrink-0 items-center gap-3", children: [toolbarStart, _jsx(Separator, {})] })), _jsx(Field, { label: t('designer.tableName'), className: "w-80", children: _jsx(Input, { mono: true, spellCheck: false, placeholder: t('designer.tableNamePlaceholder'), value: draft.name, disabled: disabled, onChange: (event) => onChange(renameTable(draft, event.target.value)) }) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [_jsxs(Button, { variant: "secondary", disabled: !changed, onClick: onRevert, children: [_jsx(Icon, { icon: Undo2, size: 12 }), t('designer.revert')] }), _jsx(DisabledReason, { reason: applyReason, children: _jsx("span", { className: "inline-flex", children: _jsx(Button, { variant: "primary", disabled: applyReason !== null, onClick: onRequestApply, children: isNew ? t('designer.create') : t('designer.apply') }) }) })] })] }), readOnlyReason !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: Lock, tone: "neutral", className: "w-full", message: readOnlyReason }) })), failure !== null && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: failure, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: onDismissFailure, children: t('designer.dismiss') }) }) })), _jsxs(Tabs.Root, { defaultValue: "columns", className: "flex min-h-0 grow flex-col", children: [_jsxs(Tabs.List, { className: "shrink-0 px-3", children: [_jsxs(Tabs.Tab, { value: "columns", children: [t('designer.tabs.columns'), _jsx(Tabs.Count, { value: draft.columns.length })] }), _jsxs(Tabs.Tab, { value: "indexes", children: [t('designer.tabs.indexes'), _jsx(Tabs.Count, { value: draft.indexes.length })] }), _jsxs(Tabs.Tab, { value: "foreignKeys", children: [t('designer.tabs.foreignKeys'), _jsx(Tabs.Count, { value: draft.foreignKeys.length })] }), _jsx(Tabs.Tab, { value: "options", children: t('designer.tabs.options') })] }), _jsx(Tabs.Panel, { value: "columns", className: "min-h-0 grow overflow-auto", children: _jsx(ColumnsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "indexes", className: "min-h-0 grow overflow-auto", children: _jsx(IndexesEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "foreignKeys", className: "min-h-0 grow overflow-auto", children: _jsx(ForeignKeysEditor, { draft: draft, dialect: dialect, disabled: disabled, schema: schema, tables: tables, referenceColumns: referenceColumns, onChange: onChange }) }), _jsx(Tabs.Panel, { value: "options", className: "min-h-0 grow overflow-auto", children: _jsx(OptionsEditor, { draft: draft, dialect: dialect, disabled: disabled, onChange: onChange }) })] }), _jsx(SqlPreview, { plan: plan, className: "shrink-0" }), _jsxs(PromptDialog, { open: confirming, danger: plan.destructive, title: t('designer.confirm.title', { count: plan.statements.length, formatted: formatNumber(plan.statements.length) }), description: isNew ? t('designer.confirm.create', { table: draft.name }) : t('designer.confirm.alter', { table: draft.originalName }), confirmLabel: isNew ? t('designer.create') : t('designer.apply'), confirmBusyLabel: t('designer.applying'), onConfirm: onConfirm, onOpenChange: (open) => {
|
|
29
30
|
if (!open) {
|
|
30
31
|
onCancelConfirm();
|
|
31
32
|
}
|
|
32
|
-
}, children: [plan.destructive && _jsx("p", { className: "mt-3 text-xs text-status-error", children: t('designer.confirm.destructive') }), _jsx("pre", { className:
|
|
33
|
+
}, children: [plan.destructive && _jsx("p", { className: "mt-3 text-xs text-status-error", children: t('designer.confirm.destructive') }), _jsx("pre", { className: clsx(CODE_TEXT, 'mt-3 max-h-64 overflow-auto rounded-md bg-surface-sunken p-2 whitespace-pre text-text select-text'), children: scriptOf(plan.statements) })] })] }));
|
|
33
34
|
}
|
|
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { Copy } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Button, Icon, copyText } from '@adecore/ui';
|
|
6
|
+
import { CODE_TEXT } from '../code-text.js';
|
|
6
7
|
import { scriptOf } from './plan.js';
|
|
7
8
|
/* The sentence a problem of the draft is read as. */
|
|
8
9
|
export function ProblemText({ problem }) {
|
|
@@ -13,5 +14,5 @@ export function ProblemText({ problem }) {
|
|
|
13
14
|
export function SqlPreview({ plan, className, ref }) {
|
|
14
15
|
const { t } = useTranslation('database');
|
|
15
16
|
const script = scriptOf(plan.statements);
|
|
16
|
-
return (_jsxs("section", { ref: ref, "aria-label": t('designer.sql'), className: clsx('relative flex min-h-0 flex-col border-t border-border-soft bg-surface-sunken', className), children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center justify-between pr-2 pl-3", children: [_jsx("span", { className: "text-xs font-medium text-text-faint", children: t('designer.sql') }), _jsxs(Button, { variant: "ghost", size: "xs", disabled: script === '', onClick: () => copyText(script), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('designer.copy')] })] }), _jsx("div", { className: "max-h-48 min-h-12 overflow-auto px-3 pb-3 select-text", children: plan.problems.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-xs text-status-error", children: plan.problems.map((problem, i) => (_jsx("li", { children: _jsx(ProblemText, { problem: problem }) }, `${problem.code}:${problem.subject}:${i}`))) })) : script === '' ? (_jsx("p", { className: "text-xs text-text-faint", children: t('designer.noChanges') })) : (_jsx("pre", { className:
|
|
17
|
+
return (_jsxs("section", { ref: ref, "aria-label": t('designer.sql'), className: clsx('relative flex min-h-0 flex-col border-t border-border-soft bg-surface-sunken', className), children: [_jsxs("div", { className: "flex h-8 shrink-0 items-center justify-between pr-2 pl-3", children: [_jsx("span", { className: "text-xs font-medium text-text-faint", children: t('designer.sql') }), _jsxs(Button, { variant: "ghost", size: "xs", disabled: script === '', onClick: () => copyText(script), children: [_jsx(Icon, { icon: Copy, size: 12 }), t('designer.copy')] })] }), _jsx("div", { className: "max-h-48 min-h-12 overflow-auto px-3 pb-3 select-text", children: plan.problems.length > 0 ? (_jsx("ul", { className: "flex flex-col gap-1 text-xs text-status-error", children: plan.problems.map((problem, i) => (_jsx("li", { children: _jsx(ProblemText, { problem: problem }) }, `${problem.code}:${problem.subject}:${i}`))) })) : script === '' ? (_jsx("p", { className: "text-xs text-text-faint", children: t('designer.noChanges') })) : (_jsx("pre", { className: clsx(CODE_TEXT, 'whitespace-pre text-text'), children: script })) })] }));
|
|
17
18
|
}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import { type Ref } from 'react';
|
|
1
|
+
import { type ReactNode, type Ref } from 'react';
|
|
2
2
|
import type { Connection } from '../client/types.ts';
|
|
3
3
|
export interface TableDesignerProps {
|
|
4
4
|
connection: Connection;
|
|
5
5
|
schema: string;
|
|
6
6
|
table?: string;
|
|
7
|
+
toolbarStart?: ReactNode;
|
|
7
8
|
className?: string;
|
|
8
9
|
ref?: Ref<HTMLDivElement>;
|
|
9
10
|
}
|
|
10
|
-
export declare function TableDesigner({ connection, schema, table, className, ref }: TableDesignerProps): import("react").JSX.Element;
|
|
11
|
+
export declare function TableDesigner({ connection, schema, table, toolbarStart, className, ref }: TableDesignerProps): import("react").JSX.Element;
|
|
@@ -13,7 +13,7 @@ import { planOf } from './plan.js';
|
|
|
13
13
|
import { recoverFrom } from './recover.js';
|
|
14
14
|
import { useReferenceColumns } from './useReferenceColumns.js';
|
|
15
15
|
/* Creates a table or modifies one: its columns, indexes, foreign keys and options, with the SQL it will run shown before it runs. */
|
|
16
|
-
export function TableDesigner({ connection, schema, table, className, ref }) {
|
|
16
|
+
export function TableDesigner({ connection, schema, table, toolbarStart, className, ref }) {
|
|
17
17
|
const { t } = useTranslation('database');
|
|
18
18
|
const client = useDatabaseClient();
|
|
19
19
|
const onAction = useDatabaseAction();
|
|
@@ -99,5 +99,5 @@ export function TableDesigner({ connection, schema, table, className, ref }) {
|
|
|
99
99
|
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), children: [(loaded.loading || snapshot === null) && loaded.error === null && (_jsx("div", { className: "grid grow place-items-center py-8 text-text-muted", children: _jsx(Spinner, { size: 20, label: t('designer.loading') }) })), loaded.error !== null && snapshot === null && (_jsx("div", { className: "p-3", children: _jsx(Banner, { icon: CircleAlert, tone: "error", className: "w-full", message: loaded.error, children: _jsx(Button, { variant: "secondary", size: "sm", onClick: loaded.reload, children: t('designer.retry') }) }) })), shapeChanged && (_jsx("div", { className: "shrink-0 px-3 pt-3", children: _jsx(Banner, { icon: RefreshCw, tone: "neutral", className: "w-full", message: t('designer.changed'), children: _jsx(Button, { variant: "secondary", size: "sm", onClick: loaded.reload, children: t('designer.reload') }) }) })), !loaded.loading && snapshot !== null && plan !== null && (_jsx(DesignerView, { className: "grow", dialect: snapshot.dialect, schema: schema, draft: draft, isNew: snapshot.structure === null, changed: draft !== editing.baseline, tables: snapshot.tables, plan: plan, readOnlyReason: readOnlyReason, failure: failure, confirming: confirming, referenceColumns: referenceColumns, onChange: (next) => setEditing({ ...editing, draft: next }), onRequestApply: () => setConfirming(true), onConfirm: apply, onCancelConfirm: () => setConfirming(false), onRevert: () => {
|
|
100
100
|
setEditing({ ...editing, draft: editing.baseline });
|
|
101
101
|
setFailure(null);
|
|
102
|
-
}, onDismissFailure: () => setFailure(null) }))] }));
|
|
102
|
+
}, onDismissFailure: () => setFailure(null), toolbarStart: toolbarStart }))] }));
|
|
103
103
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { type Ref } from 'react';
|
|
2
2
|
import type { ExplorerSelection } from '../actions.ts';
|
|
3
3
|
import type { Connection } from '../client/types.ts';
|
|
4
|
+
import type { ExplorerFolder } from './folders.ts';
|
|
4
5
|
export interface DatabaseExplorerProps {
|
|
5
6
|
connections: readonly Connection[];
|
|
6
7
|
value?: ExplorerSelection | null;
|
|
7
8
|
defaultValue?: ExplorerSelection | null;
|
|
8
9
|
onValueChange?(selection: ExplorerSelection | null): void;
|
|
9
10
|
showSystemSchemas?: boolean;
|
|
11
|
+
openOnClick?: boolean;
|
|
12
|
+
folders?: Readonly<Record<string, readonly ExplorerFolder[]>>;
|
|
10
13
|
className?: string;
|
|
11
14
|
ref?: Ref<HTMLDivElement>;
|
|
12
15
|
}
|
|
13
|
-
export declare function DatabaseExplorer({ connections, value, defaultValue, onValueChange, showSystemSchemas, className, ref }: DatabaseExplorerProps): import("react").JSX.Element;
|
|
16
|
+
export declare function DatabaseExplorer({ connections, value, defaultValue, onValueChange, showSystemSchemas, openOnClick, folders, className, ref }: DatabaseExplorerProps): import("react").JSX.Element;
|
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import { CircleAlert, Database, Eye, Folder, KeyRound,
|
|
4
|
+
import { CircleAlert, Columns2, Database, Eye, Folder, KeyRound, ListOrdered, Lock, Search, ShieldCheck, Table, Zap } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip, Tree } from '@adecore/ui';
|
|
7
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
8
|
import { useDatabaseAction, useDatabaseClient } from '../client-context.js';
|
|
9
9
|
import { EngineIcon } from '../connections/EngineIcon.js';
|
|
10
|
+
import { keyLabelOf } from '../column-keys.js';
|
|
11
|
+
import { KeyIcon } from '../KeyIcon.js';
|
|
10
12
|
import { RowMenu } from './RowMenu.js';
|
|
11
13
|
import { TableDialog } from './TableDialog.js';
|
|
12
|
-
import { connectionKey, flattenTree, hasMenu, isExpandable,
|
|
14
|
+
import { connectionKey, flattenTree, hasMenu, isExpandable, startsOpen, isSelectable, navigate, neededLoads, selectionKey, selectionOf, tabStop, openedTableOf, tableKindIn, tableOf } from './tree.js';
|
|
13
15
|
import { useExplorerLoads } from './use-explorer-loads.js';
|
|
14
16
|
import { usePopupPress } from '../use-popup-press.js';
|
|
15
17
|
import { useStoredExpansion } from './use-stored-expansion.js';
|
|
16
18
|
/*
|
|
17
19
|
* The connections, their schemas and their tables as a tree, loaded as each node opens. Every row
|
|
18
|
-
* selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction
|
|
20
|
+
* selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`,
|
|
21
|
+
* and on an item of a folder the app added opens it through the item's own `onOpen`.
|
|
19
22
|
*/
|
|
20
|
-
export function DatabaseExplorer({ connections, value, defaultValue = null, onValueChange, showSystemSchemas = false, className, ref }) {
|
|
23
|
+
export function DatabaseExplorer({ connections, value, defaultValue = null, onValueChange, showSystemSchemas = false, openOnClick = false, folders, className, ref }) {
|
|
21
24
|
const { t } = useTranslation('database');
|
|
22
25
|
const client = useDatabaseClient();
|
|
23
26
|
const act = useDatabaseAction();
|
|
@@ -40,7 +43,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
40
43
|
schemas: loads.schemas,
|
|
41
44
|
tables: loads.tables,
|
|
42
45
|
structures: loads.structures,
|
|
43
|
-
versions: loads.versions
|
|
46
|
+
versions: loads.versions,
|
|
47
|
+
folders: (connectionId) => folders?.[connectionId] ?? []
|
|
44
48
|
};
|
|
45
49
|
const rows = flattenTree({ ...input, filter });
|
|
46
50
|
const stop = tabStop(rows, activeKey, selectedKey);
|
|
@@ -56,8 +60,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
56
60
|
}
|
|
57
61
|
}, [activeKey]);
|
|
58
62
|
const setOpen = (key, open) => {
|
|
59
|
-
|
|
60
|
-
const folder = isFolderKey(key);
|
|
63
|
+
const folder = startsOpen(key);
|
|
61
64
|
const toggle = (current) => {
|
|
62
65
|
const next = new Set(current);
|
|
63
66
|
if (open !== folder) {
|
|
@@ -85,22 +88,33 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
85
88
|
select(selection);
|
|
86
89
|
}
|
|
87
90
|
};
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
/* `preview` says a click opened it, which only an explorer that opens on a click tells the app. */
|
|
92
|
+
const open = (row, preview = false) => {
|
|
93
|
+
if (row.kind === 'appItem') {
|
|
94
|
+
row.item.onOpen?.({ preview });
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const ref = openedTableOf(row);
|
|
98
|
+
if (ref === null) {
|
|
99
|
+
return;
|
|
93
100
|
}
|
|
101
|
+
pick(row);
|
|
102
|
+
const tableKind = row.kind === 'table' && row.ref === ref ? row.table.kind : tableKindIn(loads.tables(ref.connectionId, ref.schema), ref.table);
|
|
103
|
+
act?.({ kind: 'open-table', ref, view: 'data', ...(tableKind === undefined ? {} : { tableKind }), ...(openOnClick ? { preview } : {}) });
|
|
94
104
|
};
|
|
95
105
|
const activate = (row) => {
|
|
96
106
|
switch (row.kind) {
|
|
97
107
|
case 'table':
|
|
98
108
|
case 'column':
|
|
109
|
+
case 'entry':
|
|
110
|
+
case 'appItem':
|
|
99
111
|
open(row);
|
|
100
112
|
break;
|
|
101
113
|
case 'connection':
|
|
102
114
|
case 'schema':
|
|
103
115
|
case 'folder':
|
|
116
|
+
case 'part':
|
|
117
|
+
case 'appFolder':
|
|
104
118
|
pick(row);
|
|
105
119
|
toggle(row);
|
|
106
120
|
break;
|
|
@@ -132,11 +146,18 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
132
146
|
setOpen(action.key, action.type === 'expand');
|
|
133
147
|
}
|
|
134
148
|
};
|
|
135
|
-
|
|
149
|
+
/* `count` is the click's `detail`: the second click of a double click leaves the opening to the double click. */
|
|
150
|
+
const click = (row, count) => {
|
|
136
151
|
if (pressedInPopup()) {
|
|
137
152
|
return;
|
|
138
153
|
}
|
|
139
154
|
setActiveKey(row.key);
|
|
155
|
+
if (openOnClick && (row.kind === 'table' || row.kind === 'appItem')) {
|
|
156
|
+
if (count <= 1) {
|
|
157
|
+
open(row, true);
|
|
158
|
+
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
140
161
|
pick(row);
|
|
141
162
|
if (row.kind !== 'table' && isExpandable(row)) {
|
|
142
163
|
toggle(row);
|
|
@@ -167,7 +188,7 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
167
188
|
const isSelected = (row) => row !== undefined && isSelectable(row) && row.key === selectedKey;
|
|
168
189
|
const renderRow = (row, index) => {
|
|
169
190
|
const connection = connectionOf(row);
|
|
170
|
-
const menu = connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: refresh, onDisconnect: disconnect, onRequest: setRequest })) : null;
|
|
191
|
+
const menu = row.kind === 'appItem' ? ((row.item.menu ?? null)) : connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: refresh, onDisconnect: disconnect, onRequest: setRequest })) : null;
|
|
171
192
|
const selectedRow = isSelected(row);
|
|
172
193
|
const props = {
|
|
173
194
|
level: row.level,
|
|
@@ -189,8 +210,8 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
189
210
|
rowElements.current.set(row.key, element);
|
|
190
211
|
}
|
|
191
212
|
},
|
|
192
|
-
onClick: () => click(row),
|
|
193
|
-
onDoubleClick: tableOf(row) === null ? undefined : () => open(row),
|
|
213
|
+
onClick: (event) => click(row, event.detail),
|
|
214
|
+
onDoubleClick: tableOf(row) === null && row.kind !== 'appItem' ? undefined : () => open(row),
|
|
194
215
|
onFocus: () => {
|
|
195
216
|
if (row.focusable) {
|
|
196
217
|
setActiveKey(row.key);
|
|
@@ -207,21 +228,29 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
207
228
|
if (e.key === 'Escape') {
|
|
208
229
|
setFilter('');
|
|
209
230
|
}
|
|
210
|
-
} }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(Tree.Root, { "aria-label": t('explorer.label'), className: "min-h-0 grow overflow-y-auto
|
|
231
|
+
} }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(Tree.Root, { "aria-label": t('explorer.label'), overflow: "scroll", className: "min-h-0 grow overflow-y-auto pt-2", onKeyDown: onKeyDown, children: rows.map(renderRow) })), request !== null && _jsx(TableDialog, { request: request, onClose: () => setRequest(null), onDone: finish })] }));
|
|
211
232
|
}
|
|
212
233
|
function RowContent({ row, onToggle }) {
|
|
213
234
|
const { t } = useTranslation('database');
|
|
214
235
|
switch (row.kind) {
|
|
215
236
|
case 'connection':
|
|
216
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(EngineIcon, { engine: row.connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.connection.name || t('connections.untitled') }), row.connection.config.readOnly === true && (_jsx(Tooltip, { label: t('explorer.readOnly'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-faint", children: _jsx(Icon, { icon: Lock, size: 12 }) }) })), row.version !== null && _jsx("span", { className: "
|
|
237
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(EngineIcon, { engine: row.connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.connection.name || t('connections.untitled') }), row.connection.config.readOnly === true && (_jsx(Tooltip, { label: t('explorer.readOnly'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-faint", children: _jsx(Icon, { icon: Lock, size: 12 }) }) })), row.version !== null && _jsx("span", { className: "text-text-faint", children: row.version }), row.schemaCount !== null && _jsx(SchemaCount, { shown: row.schemaCount.shown, total: row.schemaCount.total })] }));
|
|
217
238
|
case 'schema':
|
|
218
239
|
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Database, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.schema })] }));
|
|
219
240
|
case 'folder':
|
|
220
241
|
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: t(row.group === 'view' ? 'explorer.views' : 'explorer.tables') }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
|
|
221
242
|
case 'table':
|
|
222
243
|
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: row.table.kind === 'view' ? Eye : Table, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.table.name })] }));
|
|
244
|
+
case 'part':
|
|
245
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: t(`explorer.parts.${row.part}`) }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
|
|
246
|
+
case 'appFolder':
|
|
247
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.folder.label }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
|
|
248
|
+
case 'appItem':
|
|
249
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Icon, { icon: row.item.icon, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.item.label })] }));
|
|
250
|
+
case 'entry':
|
|
251
|
+
return _jsx(EntryContent, { row: row });
|
|
223
252
|
case 'column':
|
|
224
|
-
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(Tree.Label, { children: row.column.name }), row.column.type !== '' && _jsx("span", { className: "
|
|
253
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(Tree.Label, { children: row.column.name }), row.column.type !== '' && _jsx("span", { className: "text-text-faint", children: row.column.type })] }));
|
|
225
254
|
case 'loading':
|
|
226
255
|
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Spinner, { size: 12 }), _jsx("span", { className: "text-text-faint", children: t('explorer.loading') })] }));
|
|
227
256
|
case 'error':
|
|
@@ -236,12 +265,39 @@ function SchemaCount({ shown, total }) {
|
|
|
236
265
|
const counts = { shown: formatNumber(shown), total: formatNumber(total) };
|
|
237
266
|
return (_jsx(Tooltip, { label: t('explorer.schemaCountHint', { count: total, ...counts }), children: _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: t('explorer.schemaCount', counts) }) }));
|
|
238
267
|
}
|
|
239
|
-
|
|
268
|
+
/* A step smaller than the icons of the rows above, in the same 16px slot, so the names stay in line. */
|
|
269
|
+
const ENTRY_ICON_SLOT = 'inline-flex size-4 shrink-0 items-center justify-center';
|
|
270
|
+
function ColumnIcon(keys) {
|
|
240
271
|
const { t } = useTranslation('database');
|
|
241
|
-
if (!primaryKey && !foreignKey) {
|
|
242
|
-
return _jsx(Icon, { icon:
|
|
272
|
+
if (!keys.primaryKey && !keys.foreignKey) {
|
|
273
|
+
return (_jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(Icon, { icon: Columns2, size: 14, className: "text-text-faint" }) }));
|
|
274
|
+
}
|
|
275
|
+
return (_jsx(Tooltip, { label: t(keyLabelOf(keys)), children: _jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(KeyIcon, { ...keys, size: 14 }) }) }));
|
|
276
|
+
}
|
|
277
|
+
/* A key, a foreign key, an index, a check or a trigger of a table: its name, and what it covers in the quiet color of a column's type. */
|
|
278
|
+
function EntryContent({ row }) {
|
|
279
|
+
const { t } = useTranslation('database');
|
|
280
|
+
const { entry } = row;
|
|
281
|
+
const icon = (glyph, className) => (_jsx("span", { className: ENTRY_ICON_SLOT, children: _jsx(Icon, { icon: glyph, size: 14, className: className }) }));
|
|
282
|
+
switch (entry.type) {
|
|
283
|
+
case 'key':
|
|
284
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(KeyRound, entry.primary ? 'text-(--file-icon-yellow)' : 'text-text-muted'), _jsx(Tree.Label, { children: entry.name ?? t('explorer.primaryKey') }), _jsx("span", { className: "text-text-faint", children: entry.columns.join(', ') })] }));
|
|
285
|
+
case 'foreignKey': {
|
|
286
|
+
const { foreignKey } = entry;
|
|
287
|
+
const columns = foreignKey.columns.join(', ');
|
|
288
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(KeyRound, 'text-(--file-icon-blue)'), _jsx(Tree.Label, { children: foreignKey.name ?? columns }), _jsx("span", { className: "text-text-faint", children: t('explorer.references', {
|
|
289
|
+
columns,
|
|
290
|
+
table: foreignKey.referencedTable,
|
|
291
|
+
referenced: foreignKey.referencedColumns.join(', ')
|
|
292
|
+
}) })] }));
|
|
293
|
+
}
|
|
294
|
+
case 'index':
|
|
295
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(ListOrdered, 'text-text-faint'), _jsx(Tree.Label, { children: entry.index.name }), _jsx("span", { className: "text-text-faint", children: entry.index.columns.join(', ') }), entry.index.unique && _jsx("span", { className: "text-text-faint", children: t('explorer.unique') })] }));
|
|
296
|
+
case 'check':
|
|
297
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(ShieldCheck, 'text-text-faint'), _jsx(Tree.Label, { children: entry.check.name ?? t('explorer.check') }), _jsx("span", { className: "font-mono text-text-faint", children: entry.check.expression })] }));
|
|
298
|
+
case 'trigger':
|
|
299
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), icon(Zap, 'text-text-faint'), _jsx(Tree.Label, { children: entry.trigger.name }), _jsx("span", { className: "text-text-faint", children: `${entry.trigger.timing} ${entry.trigger.event}` })] }));
|
|
243
300
|
}
|
|
244
|
-
return (_jsx(Tooltip, { label: t(primaryKey ? 'explorer.primaryKey' : 'explorer.foreignKey'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-muted", children: _jsx(Icon, { icon: primaryKey ? KeyRound : Link2, size: 16 }) }) }));
|
|
245
301
|
}
|
|
246
302
|
function ErrorContent({ row }) {
|
|
247
303
|
const { t } = useTranslation('database');
|
package/dist/explorer/RowMenu.js
CHANGED
|
@@ -29,8 +29,8 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest })
|
|
|
29
29
|
const request = (change) => () => onRequest({ change, connection, ref, kind });
|
|
30
30
|
return [
|
|
31
31
|
[
|
|
32
|
-
actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data' }),
|
|
33
|
-
actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure' }),
|
|
32
|
+
actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data', tableKind: kind }),
|
|
33
|
+
actionItem('open-structure', TableProperties, t('explorer.openStructure'), { kind: 'open-table', ref, view: 'structure', tableKind: kind }),
|
|
34
34
|
kind === 'table' && writable ? actionItem('edit-table', PencilRuler, t('explorer.editTable'), { kind: 'edit-table', ref }) : null,
|
|
35
35
|
actionItem('console', SquareTerminal, t('explorer.newConsoleHere'), consoleAction(ref.schema, selectAllSql(connection.config.engine, ref)))
|
|
36
36
|
],
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { connectionOfKey,
|
|
1
|
+
import { connectionOfKey, startsOpen } from './tree.js';
|
|
2
2
|
const VERSION = 1;
|
|
3
3
|
export const expansionKey = (connectionId) => `database:explorer:${connectionId}`;
|
|
4
4
|
/* Reads what `serializeExpansion` wrote. Anything that does not fit gives nothing open and no folder closed. */
|
|
@@ -14,7 +14,7 @@ export const parseExpansion = (raw, connectionId) => {
|
|
|
14
14
|
}
|
|
15
15
|
const { expanded, collapsed } = parsed;
|
|
16
16
|
const own = (keys, folders) => new Set(Array.isArray(keys)
|
|
17
|
-
? keys.filter((key) => typeof key === 'string' && key.length > 2 &&
|
|
17
|
+
? keys.filter((key) => typeof key === 'string' && key.length > 2 && startsOpen(key) === folders && connectionOfKey(key) === connectionId)
|
|
18
18
|
: []);
|
|
19
19
|
return { expanded: own(expanded, false), collapsed: own(collapsed, true) };
|
|
20
20
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
import type { LucideIcon } from 'lucide-react';
|
|
3
|
+
export interface ExplorerItem {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
readonly icon: LucideIcon;
|
|
7
|
+
onOpen?(options: {
|
|
8
|
+
readonly preview: boolean;
|
|
9
|
+
}): void;
|
|
10
|
+
menu?: ReactNode;
|
|
11
|
+
}
|
|
12
|
+
export interface ExplorerFolder {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly label: string;
|
|
15
|
+
readonly items: readonly ExplorerItem[];
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/explorer/tree.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExplorerSelection } from '../actions.ts';
|
|
2
2
|
import type { Connection, TableRef } from '../client/types.ts';
|
|
3
|
-
import type { ColumnInfo, Engine, SchemaInfo, TableInfo, TableKind, TableStructure } from '../protocol/index.ts';
|
|
3
|
+
import type { CheckInfo, ColumnInfo, Engine, ForeignKeyInfo, IndexInfo, SchemaInfo, TableInfo, TableKind, TableStructure, TriggerInfo } from '../protocol/index.ts';
|
|
4
|
+
import type { ExplorerFolder, ExplorerItem } from './folders.ts';
|
|
4
5
|
export type Load<T> = {
|
|
5
6
|
readonly status: 'idle';
|
|
6
7
|
} | {
|
|
@@ -22,9 +23,13 @@ export declare const connectionKey: (connectionId: string) => string;
|
|
|
22
23
|
export declare const schemaKey: (connectionId: string, schema: string) => string;
|
|
23
24
|
export declare const folderKey: (connectionId: string, schema: string, group: TableKind) => string;
|
|
24
25
|
export declare const tableKey: (ref: TableRef) => string;
|
|
25
|
-
export declare const
|
|
26
|
+
export declare const PARTS: readonly ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'];
|
|
27
|
+
export type TablePart = (typeof PARTS)[number];
|
|
28
|
+
export declare const partKey: (ref: TableRef, part: TablePart) => string;
|
|
29
|
+
export declare const startsOpen: (key: string) => boolean;
|
|
26
30
|
export declare const connectionOfKey: (key: string) => string;
|
|
27
31
|
export declare const columnKey: (ref: TableRef, column: string) => string;
|
|
32
|
+
export declare const appFolderKey: (connectionId: string, folder: string) => string;
|
|
28
33
|
export declare const loadKey: (target: LoadTarget) => string;
|
|
29
34
|
export declare const GROUPS: readonly TableKind[];
|
|
30
35
|
interface RowBase {
|
|
@@ -72,6 +77,36 @@ export interface ColumnRow extends RowBase {
|
|
|
72
77
|
readonly primaryKey: boolean;
|
|
73
78
|
readonly foreignKey: boolean;
|
|
74
79
|
}
|
|
80
|
+
export interface PartRow extends RowBase {
|
|
81
|
+
readonly kind: 'part';
|
|
82
|
+
readonly ref: TableRef;
|
|
83
|
+
readonly part: TablePart;
|
|
84
|
+
readonly expanded: boolean;
|
|
85
|
+
readonly count: number;
|
|
86
|
+
}
|
|
87
|
+
export type TableEntry = {
|
|
88
|
+
readonly type: 'key';
|
|
89
|
+
readonly name: string | null;
|
|
90
|
+
readonly columns: readonly string[];
|
|
91
|
+
readonly primary: boolean;
|
|
92
|
+
} | {
|
|
93
|
+
readonly type: 'foreignKey';
|
|
94
|
+
readonly foreignKey: ForeignKeyInfo;
|
|
95
|
+
} | {
|
|
96
|
+
readonly type: 'index';
|
|
97
|
+
readonly index: IndexInfo;
|
|
98
|
+
} | {
|
|
99
|
+
readonly type: 'check';
|
|
100
|
+
readonly check: CheckInfo;
|
|
101
|
+
} | {
|
|
102
|
+
readonly type: 'trigger';
|
|
103
|
+
readonly trigger: TriggerInfo;
|
|
104
|
+
};
|
|
105
|
+
export interface EntryRow extends RowBase {
|
|
106
|
+
readonly kind: 'entry';
|
|
107
|
+
readonly ref: TableRef;
|
|
108
|
+
readonly entry: TableEntry;
|
|
109
|
+
}
|
|
75
110
|
export interface LoadingRow extends RowBase {
|
|
76
111
|
readonly kind: 'loading';
|
|
77
112
|
}
|
|
@@ -80,14 +115,28 @@ export interface ErrorRow extends RowBase {
|
|
|
80
115
|
readonly message: string;
|
|
81
116
|
readonly retry: LoadTarget;
|
|
82
117
|
}
|
|
118
|
+
export interface AppFolderRow extends RowBase {
|
|
119
|
+
readonly kind: 'appFolder';
|
|
120
|
+
readonly connection: Connection;
|
|
121
|
+
readonly folder: ExplorerFolder;
|
|
122
|
+
readonly expanded: boolean;
|
|
123
|
+
readonly count: number;
|
|
124
|
+
}
|
|
125
|
+
export interface AppItemRow extends RowBase {
|
|
126
|
+
readonly kind: 'appItem';
|
|
127
|
+
readonly connection: Connection;
|
|
128
|
+
readonly item: ExplorerItem;
|
|
129
|
+
}
|
|
83
130
|
export interface EmptyRow extends RowBase {
|
|
84
131
|
readonly kind: 'empty';
|
|
85
132
|
readonly of: 'schemas' | 'tables' | 'columns';
|
|
86
133
|
}
|
|
87
|
-
export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | ColumnRow | LoadingRow | ErrorRow | EmptyRow;
|
|
88
|
-
export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow;
|
|
134
|
+
export type TreeRow = ConnectionRow | SchemaRow | FolderRow | TableRow | PartRow | ColumnRow | EntryRow | AppFolderRow | AppItemRow | LoadingRow | ErrorRow | EmptyRow;
|
|
135
|
+
export type ExpandableRow = ConnectionRow | SchemaRow | FolderRow | TableRow | PartRow | AppFolderRow;
|
|
89
136
|
export declare const isExpandable: (row: TreeRow) => row is ExpandableRow;
|
|
90
137
|
export declare const tableOf: (row: TreeRow) => TableRef | null;
|
|
138
|
+
export declare const openedTableOf: (row: TreeRow) => TableRef | null;
|
|
139
|
+
export declare const tableKindIn: (tables: Load<readonly TableInfo[]>, name: string) => TableKind | undefined;
|
|
91
140
|
export declare const selectionOf: (row: TreeRow) => ExplorerSelection | null;
|
|
92
141
|
export declare const selectionKey: (selection: ExplorerSelection) => string;
|
|
93
142
|
export declare const hasMenu: (row: TreeRow) => boolean;
|
|
@@ -103,8 +152,10 @@ export interface TreeInput {
|
|
|
103
152
|
tables(connectionId: string, schema: string): Load<readonly TableInfo[]>;
|
|
104
153
|
structures(connectionId: string, schema: string, table: string): Load<TableStructure>;
|
|
105
154
|
versions(connectionId: string): string | null;
|
|
155
|
+
folders?(connectionId: string): readonly ExplorerFolder[];
|
|
106
156
|
}
|
|
107
157
|
export declare const visibleSchemas: (schemas: readonly SchemaInfo[], showSystemSchemas: boolean) => readonly SchemaInfo[];
|
|
158
|
+
export declare const tableEntries: (structure: TableStructure) => Readonly<Record<Exclude<TablePart, 'columns'>, readonly TableEntry[]>>;
|
|
108
159
|
export declare const flattenTree: (input: TreeInput) => TreeRow[];
|
|
109
160
|
export declare const neededLoads: (input: Omit<TreeInput, 'filter'>) => LoadTarget[];
|
|
110
161
|
export type TreeAction = {
|