@adecore/database 0.0.1 → 0.16.0-beta.0
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/LICENSE +21 -0
- package/README.md +152 -1
- package/dist/DatabaseProvider.d.ts +13 -0
- package/dist/DatabaseProvider.js +17 -0
- package/dist/actions.d.ts +40 -0
- package/dist/actions.js +1 -0
- package/dist/client/client.d.ts +5 -0
- package/dist/client/client.js +245 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -0
- package/dist/client/types.d.ts +97 -0
- package/dist/client/types.js +13 -0
- package/dist/client-context.d.ts +16 -0
- package/dist/client-context.js +17 -0
- package/dist/connections/ConnectionForm.d.ts +10 -0
- package/dist/connections/ConnectionForm.js +145 -0
- package/dist/connections/ConnectionManager.d.ts +12 -0
- package/dist/connections/ConnectionManager.js +93 -0
- package/dist/connections/EngineIcon.d.ts +9 -0
- package/dist/connections/EngineIcon.js +7 -0
- package/dist/connections/connection-config.d.ts +16 -0
- package/dist/connections/connection-config.js +107 -0
- package/dist/connections/docker.d.ts +9 -0
- package/dist/connections/docker.js +38 -0
- package/dist/connections/engine-icons.d.ts +3 -0
- package/dist/connections/engine-icons.js +3 -0
- package/dist/connections/use-docker-containers.d.ts +17 -0
- package/dist/connections/use-docker-containers.js +26 -0
- package/dist/console/DestructiveDialog.d.ts +9 -0
- package/dist/console/DestructiveDialog.js +9 -0
- package/dist/console/HistoryPanel.d.ts +10 -0
- package/dist/console/HistoryPanel.js +15 -0
- package/dist/console/QueryConsole.d.ts +14 -0
- package/dist/console/QueryConsole.js +261 -0
- package/dist/console/SchemaPicker.d.ts +11 -0
- package/dist/console/SchemaPicker.js +8 -0
- package/dist/console/SqlEditor.d.ts +12 -0
- package/dist/console/SqlEditor.js +61 -0
- package/dist/console/StatementResultView.d.ts +18 -0
- package/dist/console/StatementResultView.js +32 -0
- package/dist/console/TransactionControls.d.ts +13 -0
- package/dist/console/TransactionControls.js +12 -0
- package/dist/console/destructive.d.ts +7 -0
- package/dist/console/destructive.js +43 -0
- package/dist/console/editing.d.ts +12 -0
- package/dist/console/editing.js +60 -0
- package/dist/console/history.d.ts +13 -0
- package/dist/console/history.js +43 -0
- package/dist/console/labels.d.ts +1 -0
- package/dist/console/labels.js +6 -0
- package/dist/console/outcome.d.ts +6 -0
- package/dist/console/outcome.js +11 -0
- package/dist/console/schemas.d.ts +2 -0
- package/dist/console/schemas.js +5 -0
- package/dist/console/shortcuts.d.ts +2 -0
- package/dist/console/shortcuts.js +3 -0
- package/dist/console/useConsoleHistory.d.ts +7 -0
- package/dist/console/useConsoleHistory.js +22 -0
- package/dist/ddl/alter-mysql.d.ts +4 -0
- package/dist/ddl/alter-mysql.js +79 -0
- package/dist/ddl/alter-sqlite.d.ts +4 -0
- package/dist/ddl/alter-sqlite.js +80 -0
- package/dist/ddl/alter.d.ts +4 -0
- package/dist/ddl/alter.js +24 -0
- package/dist/ddl/column.d.ts +9 -0
- package/dist/ddl/column.js +62 -0
- package/dist/ddl/create.d.ts +12 -0
- package/dist/ddl/create.js +74 -0
- package/dist/ddl/dialect.d.ts +12 -0
- package/dist/ddl/dialect.js +34 -0
- package/dist/ddl/diff.d.ts +34 -0
- package/dist/ddl/diff.js +111 -0
- package/dist/ddl/draft.d.ts +46 -0
- package/dist/ddl/draft.js +70 -0
- package/dist/ddl/index.d.ts +10 -0
- package/dist/ddl/index.js +10 -0
- package/dist/ddl/parse.d.ts +17 -0
- package/dist/ddl/parse.js +131 -0
- package/dist/ddl/statements.d.ts +6 -0
- package/dist/ddl/statements.js +12 -0
- package/dist/ddl/types.d.ts +6 -0
- package/dist/ddl/types.js +52 -0
- package/dist/ddl/validate.d.ts +7 -0
- package/dist/ddl/validate.js +38 -0
- package/dist/designer/ColumnsEditor.d.ts +2 -0
- package/dist/designer/ColumnsEditor.js +24 -0
- package/dist/designer/DesignerView.d.ts +25 -0
- package/dist/designer/DesignerView.js +33 -0
- package/dist/designer/ForeignKeysEditor.d.ts +7 -0
- package/dist/designer/ForeignKeysEditor.js +24 -0
- package/dist/designer/IndexesEditor.d.ts +2 -0
- package/dist/designer/IndexesEditor.js +13 -0
- package/dist/designer/NamePicker.d.ts +11 -0
- package/dist/designer/NamePicker.js +13 -0
- package/dist/designer/OptionsEditor.d.ts +2 -0
- package/dist/designer/OptionsEditor.js +17 -0
- package/dist/designer/SqlPreview.d.ts +12 -0
- package/dist/designer/SqlPreview.js +17 -0
- package/dist/designer/TableDesigner.d.ts +10 -0
- package/dist/designer/TableDesigner.js +103 -0
- package/dist/designer/edit.d.ts +17 -0
- package/dist/designer/edit.js +115 -0
- package/dist/designer/editor-props.d.ts +10 -0
- package/dist/designer/editor-props.js +1 -0
- package/dist/designer/plan.d.ts +9 -0
- package/dist/designer/plan.js +13 -0
- package/dist/designer/recover.d.ts +2 -0
- package/dist/designer/recover.js +18 -0
- package/dist/designer/useReferenceColumns.d.ts +3 -0
- package/dist/designer/useReferenceColumns.js +36 -0
- package/dist/explorer/DatabaseExplorer.d.ts +13 -0
- package/dist/explorer/DatabaseExplorer.js +264 -0
- package/dist/explorer/RowMenu.d.ts +11 -0
- package/dist/explorer/RowMenu.js +89 -0
- package/dist/explorer/TableDialog.d.ts +15 -0
- package/dist/explorer/TableDialog.js +50 -0
- package/dist/explorer/expansion.d.ts +7 -0
- package/dist/explorer/expansion.js +31 -0
- package/dist/explorer/table-statements.d.ts +5 -0
- package/dist/explorer/table-statements.js +12 -0
- package/dist/explorer/tree.d.ts +125 -0
- package/dist/explorer/tree.js +295 -0
- package/dist/explorer/use-explorer-loads.d.ts +12 -0
- package/dist/explorer/use-explorer-loads.js +109 -0
- package/dist/explorer/use-stored-expansion.d.ts +9 -0
- package/dist/explorer/use-stored-expansion.js +47 -0
- package/dist/grid/CellEditor.d.ts +9 -0
- package/dist/grid/CellEditor.js +29 -0
- package/dist/grid/CopyAsMenu.d.ts +7 -0
- package/dist/grid/CopyAsMenu.js +10 -0
- package/dist/grid/DataGrid.d.ts +36 -0
- package/dist/grid/DataGrid.js +512 -0
- package/dist/grid/EnumPicker.d.ts +17 -0
- package/dist/grid/EnumPicker.js +53 -0
- package/dist/grid/GridHeaderCell.d.ts +36 -0
- package/dist/grid/GridHeaderCell.js +69 -0
- package/dist/grid/RecordView.d.ts +20 -0
- package/dist/grid/RecordView.js +126 -0
- package/dist/grid/ValueDock.d.ts +11 -0
- package/dist/grid/ValueDock.js +28 -0
- package/dist/grid/aggregates.d.ts +22 -0
- package/dist/grid/aggregates.js +62 -0
- package/dist/grid/cell-range.d.ts +14 -0
- package/dist/grid/cell-range.js +11 -0
- package/dist/grid/column-layout.d.ts +18 -0
- package/dist/grid/column-layout.js +49 -0
- package/dist/grid/column-selection.d.ts +11 -0
- package/dist/grid/column-selection.js +28 -0
- package/dist/grid/column-view.d.ts +10 -0
- package/dist/grid/column-view.js +23 -0
- package/dist/grid/copy-formats.d.ts +15 -0
- package/dist/grid/copy-formats.js +80 -0
- package/dist/grid/display.d.ts +20 -0
- package/dist/grid/display.js +81 -0
- package/dist/grid/edit-value.d.ts +5 -0
- package/dist/grid/edit-value.js +55 -0
- package/dist/grid/enum-type.d.ts +7 -0
- package/dist/grid/enum-type.js +52 -0
- package/dist/grid/focused-value.d.ts +4 -0
- package/dist/grid/focused-value.js +19 -0
- package/dist/grid/layout.d.ts +22 -0
- package/dist/grid/layout.js +74 -0
- package/dist/grid/navigation.d.ts +15 -0
- package/dist/grid/navigation.js +52 -0
- package/dist/grid/row-selection.d.ts +9 -0
- package/dist/grid/row-selection.js +24 -0
- package/dist/grid/sort.d.ts +7 -0
- package/dist/grid/sort.js +7 -0
- package/dist/grid/types.d.ts +30 -0
- package/dist/grid/types.js +9 -0
- package/dist/host/helper-path.d.ts +7 -0
- package/dist/host/helper-path.js +15 -0
- package/dist/host/host.d.ts +2 -0
- package/dist/host/host.js +343 -0
- package/dist/host/index.d.ts +5 -0
- package/dist/host/index.js +4 -0
- package/dist/host/spawn.d.ts +7 -0
- package/dist/host/spawn.js +98 -0
- package/dist/host/types.d.ts +19 -0
- package/dist/host/types.js +1 -0
- package/dist/host/validate.d.ts +10 -0
- package/dist/host/validate.js +339 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +13 -0
- package/dist/locales/en.json +577 -0
- package/dist/locales/nl.json +577 -0
- package/dist/locales.d.ts +4 -0
- package/dist/locales.js +14 -0
- package/dist/protocol/connection.d.ts +56 -0
- package/dist/protocol/connection.js +1 -0
- package/dist/protocol/errors.d.ts +7 -0
- package/dist/protocol/errors.js +1 -0
- package/dist/protocol/index.d.ts +5 -0
- package/dist/protocol/index.js +3 -0
- package/dist/protocol/messages.d.ts +250 -0
- package/dist/protocol/messages.js +2 -0
- package/dist/protocol/schema.d.ts +53 -0
- package/dist/protocol/schema.js +1 -0
- package/dist/protocol/values.d.ts +21 -0
- package/dist/protocol/values.js +15 -0
- package/dist/sql-split.d.ts +15 -0
- package/dist/sql-split.js +201 -0
- package/dist/sql.d.ts +29 -0
- package/dist/sql.js +104 -0
- package/dist/structure/StructureView.d.ts +14 -0
- package/dist/structure/StructureView.js +99 -0
- package/dist/structure/structure-text.d.ts +2 -0
- package/dist/structure/structure-text.js +2 -0
- package/dist/table/CommandField.d.ts +14 -0
- package/dist/table/CommandField.js +157 -0
- package/dist/table/ImportDialog.d.ts +27 -0
- package/dist/table/ImportDialog.js +28 -0
- package/dist/table/TableStatusBar.d.ts +19 -0
- package/dist/table/TableStatusBar.js +22 -0
- package/dist/table/TableToolbar.d.ts +43 -0
- package/dist/table/TableToolbar.js +16 -0
- package/dist/table/TableView.d.ts +13 -0
- package/dist/table/TableView.js +508 -0
- package/dist/table/command-field.d.ts +57 -0
- package/dist/table/command-field.js +203 -0
- package/dist/table/grid-rows.d.ts +23 -0
- package/dist/table/grid-rows.js +55 -0
- package/dist/table/import-mapping.d.ts +7 -0
- package/dist/table/import-mapping.js +28 -0
- package/dist/table/layout-store.d.ts +13 -0
- package/dist/table/layout-store.js +43 -0
- package/dist/table/paging.d.ts +12 -0
- package/dist/table/paging.js +20 -0
- package/dist/table/pending.d.ts +32 -0
- package/dist/table/pending.js +94 -0
- package/dist/table/references.d.ts +12 -0
- package/dist/table/references.js +21 -0
- package/dist/table/useLoaded.d.ts +7 -0
- package/dist/table/useLoaded.js +36 -0
- package/dist/table/useStoredLayout.d.ts +8 -0
- package/dist/table/useStoredLayout.js +39 -0
- package/dist/table/useTableTransfer.d.ts +51 -0
- package/dist/table/useTableTransfer.js +125 -0
- package/dist/testing/fake.d.ts +21 -0
- package/dist/testing/fake.js +372 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +1 -0
- package/dist/use-popup-press.d.ts +1 -0
- package/dist/use-popup-press.js +24 -0
- package/dist/use-schema-change.d.ts +1 -0
- package/dist/use-schema-change.js +13 -0
- package/dist/use-stable-callback.d.ts +1 -0
- package/dist/use-stable-callback.js +9 -0
- package/dist/value/ValuePanel.d.ts +14 -0
- package/dist/value/ValuePanel.js +103 -0
- package/dist/value/draft.d.ts +33 -0
- package/dist/value/draft.js +44 -0
- package/dist/value/hex.d.ts +14 -0
- package/dist/value/hex.js +46 -0
- package/dist/value/json.d.ts +9 -0
- package/dist/value/json.js +71 -0
- package/dist/value/views.d.ts +22 -0
- package/dist/value/views.js +54 -0
- package/dist/workbench/DatabaseWorkbench.d.ts +9 -0
- package/dist/workbench/DatabaseWorkbench.js +179 -0
- package/dist/workbench/DesignerTab.d.ts +10 -0
- package/dist/workbench/DesignerTab.js +22 -0
- package/dist/workbench/TableTab.d.ts +10 -0
- package/dist/workbench/TableTab.js +8 -0
- package/dist/workbench/tabs.d.ts +42 -0
- package/dist/workbench/tabs.js +139 -0
- package/package.json +79 -3
- package/src/DatabaseProvider.tsx +37 -0
- package/src/actions.ts +34 -0
- package/src/client/client.ts +324 -0
- package/src/client/index.ts +18 -0
- package/src/client/types.ts +161 -0
- package/src/client-context.ts +35 -0
- package/src/connections/ConnectionForm.tsx +474 -0
- package/src/connections/ConnectionManager.tsx +224 -0
- package/src/connections/EngineIcon.tsx +18 -0
- package/src/connections/connection-config.ts +143 -0
- package/src/connections/docker.ts +50 -0
- package/src/connections/engine-icons.ts +5 -0
- package/src/connections/use-docker-containers.ts +41 -0
- package/src/console/DestructiveDialog.tsx +40 -0
- package/src/console/HistoryPanel.tsx +67 -0
- package/src/console/QueryConsole.tsx +439 -0
- package/src/console/SchemaPicker.tsx +32 -0
- package/src/console/SqlEditor.tsx +95 -0
- package/src/console/StatementResultView.tsx +176 -0
- package/src/console/TransactionControls.tsx +53 -0
- package/src/console/destructive.ts +60 -0
- package/src/console/editing.ts +77 -0
- package/src/console/history.ts +62 -0
- package/src/console/labels.ts +7 -0
- package/src/console/outcome.ts +14 -0
- package/src/console/schemas.ts +7 -0
- package/src/console/shortcuts.ts +4 -0
- package/src/console/useConsoleHistory.ts +39 -0
- package/src/ddl/alter-mysql.ts +90 -0
- package/src/ddl/alter-sqlite.ts +93 -0
- package/src/ddl/alter.ts +29 -0
- package/src/ddl/column.ts +74 -0
- package/src/ddl/create.ts +92 -0
- package/src/ddl/dialect.ts +53 -0
- package/src/ddl/diff.ts +164 -0
- package/src/ddl/draft.ts +136 -0
- package/src/ddl/index.ts +10 -0
- package/src/ddl/parse.ts +161 -0
- package/src/ddl/statements.ts +20 -0
- package/src/ddl/types.ts +60 -0
- package/src/ddl/validate.ts +47 -0
- package/src/designer/ColumnsEditor.tsx +204 -0
- package/src/designer/DesignerView.tsx +184 -0
- package/src/designer/ForeignKeysEditor.tsx +132 -0
- package/src/designer/IndexesEditor.tsx +63 -0
- package/src/designer/NamePicker.tsx +44 -0
- package/src/designer/OptionsEditor.tsx +101 -0
- package/src/designer/SqlPreview.tsx +56 -0
- package/src/designer/TableDesigner.tsx +186 -0
- package/src/designer/edit.ts +137 -0
- package/src/designer/editor-props.ts +13 -0
- package/src/designer/plan.ts +25 -0
- package/src/designer/recover.ts +20 -0
- package/src/designer/useReferenceColumns.ts +54 -0
- package/src/explorer/DatabaseExplorer.tsx +450 -0
- package/src/explorer/RowMenu.tsx +134 -0
- package/src/explorer/TableDialog.tsx +80 -0
- package/src/explorer/expansion.ts +47 -0
- package/src/explorer/table-statements.ts +18 -0
- package/src/explorer/tree.ts +453 -0
- package/src/explorer/use-explorer-loads.ts +157 -0
- package/src/explorer/use-stored-expansion.ts +66 -0
- package/src/grid/CellEditor.tsx +53 -0
- package/src/grid/CopyAsMenu.tsx +33 -0
- package/src/grid/DataGrid.tsx +871 -0
- package/src/grid/EnumPicker.tsx +108 -0
- package/src/grid/GridHeaderCell.tsx +243 -0
- package/src/grid/RecordView.tsx +293 -0
- package/src/grid/ValueDock.tsx +52 -0
- package/src/grid/aggregates.ts +94 -0
- package/src/grid/cell-range.ts +26 -0
- package/src/grid/column-layout.ts +71 -0
- package/src/grid/column-selection.ts +42 -0
- package/src/grid/column-view.ts +35 -0
- package/src/grid/copy-formats.ts +108 -0
- package/src/grid/display.ts +109 -0
- package/src/grid/edit-value.ts +60 -0
- package/src/grid/enum-type.ts +62 -0
- package/src/grid/focused-value.ts +23 -0
- package/src/grid/layout.ts +91 -0
- package/src/grid/navigation.ts +74 -0
- package/src/grid/row-selection.ts +35 -0
- package/src/grid/sort.ts +12 -0
- package/src/grid/types.ts +59 -0
- package/src/host/helper-path.ts +41 -0
- package/src/host/host.ts +456 -0
- package/src/host/index.ts +5 -0
- package/src/host/spawn.ts +124 -0
- package/src/host/types.ts +46 -0
- package/src/host/validate.ts +402 -0
- package/src/index.ts +37 -0
- package/src/locales/en.json +577 -0
- package/src/locales/nl.json +577 -0
- package/src/locales.ts +18 -0
- package/src/protocol/connection.ts +86 -0
- package/src/protocol/errors.ts +38 -0
- package/src/protocol/index.ts +33 -0
- package/src/protocol/messages.ts +168 -0
- package/src/protocol/schema.ts +76 -0
- package/src/protocol/values.ts +52 -0
- package/src/sql-split.ts +220 -0
- package/src/sql.ts +152 -0
- package/src/structure/StructureView.tsx +269 -0
- package/src/structure/structure-text.ts +4 -0
- package/src/table/CommandField.tsx +318 -0
- package/src/table/ImportDialog.tsx +168 -0
- package/src/table/TableStatusBar.tsx +91 -0
- package/src/table/TableToolbar.tsx +217 -0
- package/src/table/TableView.tsx +880 -0
- package/src/table/command-field.ts +285 -0
- package/src/table/grid-rows.ts +80 -0
- package/src/table/import-mapping.ts +37 -0
- package/src/table/layout-store.ts +62 -0
- package/src/table/paging.ts +33 -0
- package/src/table/pending.ts +143 -0
- package/src/table/references.ts +38 -0
- package/src/table/useLoaded.ts +58 -0
- package/src/table/useStoredLayout.ts +53 -0
- package/src/table/useTableTransfer.ts +187 -0
- package/src/testing/fake.ts +533 -0
- package/src/testing/index.ts +1 -0
- package/src/use-popup-press.ts +27 -0
- package/src/use-schema-change.ts +19 -0
- package/src/use-stable-callback.ts +12 -0
- package/src/value/ValuePanel.tsx +254 -0
- package/src/value/draft.ts +72 -0
- package/src/value/hex.ts +62 -0
- package/src/value/json.ts +74 -0
- package/src/value/views.ts +68 -0
- package/src/workbench/DatabaseWorkbench.tsx +348 -0
- package/src/workbench/DesignerTab.tsx +37 -0
- package/src/workbench/TableTab.tsx +27 -0
- package/src/workbench/tabs.ts +184 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type Ref } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
3
|
+
import { PencilRuler, Settings2, SquareTerminal, Table, TableProperties } from 'lucide-react';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import {
|
|
6
|
+
Button,
|
|
7
|
+
ColumnResizeHandle,
|
|
8
|
+
Dialog,
|
|
9
|
+
EmptyState,
|
|
10
|
+
Icon,
|
|
11
|
+
IconButton,
|
|
12
|
+
PromptDialog,
|
|
13
|
+
Segmented,
|
|
14
|
+
Tabs,
|
|
15
|
+
isApplePlatform,
|
|
16
|
+
matchesShortcut,
|
|
17
|
+
shortcut,
|
|
18
|
+
useColumnResize
|
|
19
|
+
} from '@adecore/ui';
|
|
20
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
21
|
+
import { DatabaseProvider } from '../DatabaseProvider.tsx';
|
|
22
|
+
import type { DatabaseAction, ExplorerSelection } from '../actions.ts';
|
|
23
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage, useNumberNotation } from '../client-context.ts';
|
|
24
|
+
import type { Connection } from '../client/types.ts';
|
|
25
|
+
import { ConnectionManager } from '../connections/ConnectionManager.tsx';
|
|
26
|
+
import { QueryConsole } from '../console/QueryConsole.tsx';
|
|
27
|
+
import { DatabaseExplorer } from '../explorer/DatabaseExplorer.tsx';
|
|
28
|
+
import { DesignerTab } from './DesignerTab.tsx';
|
|
29
|
+
import { TableTab } from './TableTab.tsx';
|
|
30
|
+
import {
|
|
31
|
+
closeTab,
|
|
32
|
+
connectionIdOf,
|
|
33
|
+
focusTab,
|
|
34
|
+
openConsole,
|
|
35
|
+
openDesigner,
|
|
36
|
+
openTable,
|
|
37
|
+
parseState,
|
|
38
|
+
pruneTabs,
|
|
39
|
+
serializeState,
|
|
40
|
+
setConsoleSql,
|
|
41
|
+
setDesignerTable,
|
|
42
|
+
setTableView,
|
|
43
|
+
STORAGE_KEY,
|
|
44
|
+
type WorkbenchTab
|
|
45
|
+
} from './tabs.ts';
|
|
46
|
+
import { useStableCallback } from '../use-stable-callback.ts';
|
|
47
|
+
|
|
48
|
+
export interface DatabaseWorkbenchProps {
|
|
49
|
+
connections: readonly Connection[];
|
|
50
|
+
/* Saving is the app's. Without it the connections are fixed: there is no way to manage them from here. */
|
|
51
|
+
onConnectionsChange?(next: readonly Connection[]): void;
|
|
52
|
+
className?: string;
|
|
53
|
+
ref?: Ref<HTMLDivElement>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DEFAULT_WIDTH = 280;
|
|
57
|
+
const MIN_WIDTH = 200;
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* The whole database view in one piece: the explorer beside closable tabs for the tables, consoles and
|
|
61
|
+
* designers a person opens. It answers the actions of the views below itself; an action it cannot place
|
|
62
|
+
* goes on to the `DatabaseProvider` above it. The open tabs are kept through the provider's `storage`.
|
|
63
|
+
*/
|
|
64
|
+
export function DatabaseWorkbench({ connections, onConnectionsChange, className, ref }: DatabaseWorkbenchProps) {
|
|
65
|
+
const { t } = useTranslation('database');
|
|
66
|
+
const client = useDatabaseClient();
|
|
67
|
+
const outer = useDatabaseAction();
|
|
68
|
+
const storage = useDatabaseStorage();
|
|
69
|
+
const files = useDatabaseFiles();
|
|
70
|
+
const numberNotation = useNumberNotation();
|
|
71
|
+
const column = useRef<HTMLElement>(null);
|
|
72
|
+
const [state, setState] = useState(() => parseState(storage?.get(STORAGE_KEY) ?? null));
|
|
73
|
+
const [selection, setSelection] = useState<ExplorerSelection | null>(null);
|
|
74
|
+
const [width, setWidth] = useState(DEFAULT_WIDTH);
|
|
75
|
+
const [managing, setManaging] = useState(false);
|
|
76
|
+
const [managed, setManaged] = useState<string | null>(null);
|
|
77
|
+
/* Ids of the table tabs that hold changes nobody submitted, and the tab a person asked to close while it does. */
|
|
78
|
+
const [dirty, setDirty] = useState<ReadonlySet<string>>(() => new Set());
|
|
79
|
+
const [closing, setClosing] = useState<string | null>(null);
|
|
80
|
+
const { startResize } = useColumnResize(column, { size: width, min: MIN_WIDTH, from: 'left', max: () => window.innerWidth / 2, onSize: setWidth });
|
|
81
|
+
|
|
82
|
+
const known = useMemo(() => new Set(connections.map((connection) => connection.id)), [connections]);
|
|
83
|
+
// While there are no connections they may still be loading, so nothing is dropped then.
|
|
84
|
+
const pruned = known.size > 0 ? pruneTabs(state, known) : state;
|
|
85
|
+
if (pruned !== state) {
|
|
86
|
+
setState(pruned);
|
|
87
|
+
}
|
|
88
|
+
if ([...dirty].some((id) => !state.tabs.some((tab) => tab.id === id))) {
|
|
89
|
+
setDirty(new Set([...dirty].filter((id) => state.tabs.some((tab) => tab.id === id))));
|
|
90
|
+
}
|
|
91
|
+
const tabs = state.tabs.filter((tab) => known.has(connectionIdOf(tab)));
|
|
92
|
+
const active = tabs.find((tab) => tab.id === state.activeId) ?? tabs.at(-1) ?? null;
|
|
93
|
+
const canManage = onConnectionsChange !== undefined;
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
storage?.set(STORAGE_KEY, serializeState(state));
|
|
97
|
+
}, [storage, state]);
|
|
98
|
+
|
|
99
|
+
const onAction = useStableCallback((action: DatabaseAction): void => {
|
|
100
|
+
const connectionId = action.kind === 'open-table' || action.kind === 'edit-table' ? action.ref.connectionId : action.connectionId;
|
|
101
|
+
if (!known.has(connectionId) || (action.kind === 'manage-connection' && !canManage)) {
|
|
102
|
+
outer?.(action);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
switch (action.kind) {
|
|
106
|
+
case 'open-table':
|
|
107
|
+
setSelection(action.ref);
|
|
108
|
+
setState((current) => openTable(current, action.ref, action.view, action.where));
|
|
109
|
+
break;
|
|
110
|
+
case 'open-console':
|
|
111
|
+
setState((current) => openConsole(current, action.connectionId, action.schema, action.sql));
|
|
112
|
+
break;
|
|
113
|
+
case 'new-table':
|
|
114
|
+
setState((current) => openDesigner(current, action.connectionId, action.schema));
|
|
115
|
+
break;
|
|
116
|
+
case 'edit-table':
|
|
117
|
+
setState((current) => openDesigner(current, action.ref.connectionId, action.ref.schema, action.ref.table));
|
|
118
|
+
break;
|
|
119
|
+
case 'manage-connection':
|
|
120
|
+
setManaged(action.connectionId);
|
|
121
|
+
setManaging(true);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function newConsole(): void {
|
|
127
|
+
const context = consoleContext();
|
|
128
|
+
if (context !== null) {
|
|
129
|
+
setState((current) => openConsole(current, context.connectionId, context.schema));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Where a new console runs: the connection of the tab in front, else the one picked in the explorer, else the first. */
|
|
134
|
+
function consoleContext(): { readonly connectionId: string; readonly schema?: string } | null {
|
|
135
|
+
if (active !== null) {
|
|
136
|
+
return { connectionId: connectionIdOf(active), schema: active.kind === 'table' ? active.ref.schema : active.schema };
|
|
137
|
+
}
|
|
138
|
+
if (selection !== null && known.has(selection.connectionId)) {
|
|
139
|
+
return { connectionId: selection.connectionId, schema: selection.schema };
|
|
140
|
+
}
|
|
141
|
+
const first = connections[0];
|
|
142
|
+
return first === undefined ? null : { connectionId: first.id };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function onKeyDown(e: KeyboardEvent<HTMLDivElement>): void {
|
|
146
|
+
// A dialog opened from here is a portal: its events reach this handler through React, not through the DOM.
|
|
147
|
+
if (
|
|
148
|
+
active !== null &&
|
|
149
|
+
e.currentTarget.contains(e.target as Node) &&
|
|
150
|
+
!e.nativeEvent.isComposing &&
|
|
151
|
+
matchesShortcut(shortcut('Mod+W'), e.nativeEvent, isApplePlatform())
|
|
152
|
+
) {
|
|
153
|
+
e.preventDefault();
|
|
154
|
+
requestClose(active.id);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function closeById(id: string): void {
|
|
159
|
+
setState((current) => closeTab(current, id));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/* A table tab with changes that are not submitted asks first. */
|
|
163
|
+
function requestClose(id: string): void {
|
|
164
|
+
if (dirty.has(id)) {
|
|
165
|
+
setClosing(id);
|
|
166
|
+
} else {
|
|
167
|
+
closeById(id);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function markDirty(id: string, isDirty: boolean): void {
|
|
172
|
+
setDirty((current) => {
|
|
173
|
+
if (current.has(id) === isDirty) {
|
|
174
|
+
return current;
|
|
175
|
+
}
|
|
176
|
+
const next = new Set(current);
|
|
177
|
+
if (isDirty) {
|
|
178
|
+
next.add(id);
|
|
179
|
+
} else {
|
|
180
|
+
next.delete(id);
|
|
181
|
+
}
|
|
182
|
+
return next;
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const titleOf = (tab: WorkbenchTab): string => {
|
|
187
|
+
switch (tab.kind) {
|
|
188
|
+
case 'table':
|
|
189
|
+
return tab.where === undefined ? tab.ref.table : t('workbench.filtered', { table: tab.ref.table });
|
|
190
|
+
case 'console':
|
|
191
|
+
return t('workbench.console', { number: formatNumber(tab.number) });
|
|
192
|
+
case 'designer':
|
|
193
|
+
return tab.table === undefined ? t('workbench.newTable') : t('workbench.designer', { table: tab.table });
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const viewOf = (tab: WorkbenchTab) => {
|
|
198
|
+
const connection = connections.find((entry) => entry.id === connectionIdOf(tab));
|
|
199
|
+
if (connection === undefined) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
switch (tab.kind) {
|
|
203
|
+
case 'table':
|
|
204
|
+
return <TableTab connection={connection} tab={tab} onDirtyChange={(isDirty) => markDirty(tab.id, isDirty)} />;
|
|
205
|
+
case 'console':
|
|
206
|
+
return (
|
|
207
|
+
<QueryConsole
|
|
208
|
+
connection={connection}
|
|
209
|
+
schema={tab.schema}
|
|
210
|
+
value={tab.sql}
|
|
211
|
+
onValueChange={(sql) => setState((current) => setConsoleSql(current, tab.id, sql))}
|
|
212
|
+
autoFocus={tab.id === active?.id}
|
|
213
|
+
className="h-full"
|
|
214
|
+
/>
|
|
215
|
+
);
|
|
216
|
+
case 'designer':
|
|
217
|
+
return (
|
|
218
|
+
<DesignerTab connection={connection} tab={tab} onTableChange={(table) => setState((current) => setDesignerTable(current, tab.id, table))} />
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const canOpenConsole = consoleContext() !== null;
|
|
224
|
+
const closingTab = state.tabs.find((tab) => tab.id === closing);
|
|
225
|
+
const closingTable = closingTab?.kind === 'table' ? closingTab.ref.table : '';
|
|
226
|
+
|
|
227
|
+
return (
|
|
228
|
+
<DatabaseProvider client={client} onAction={onAction} storage={storage} files={files} numberNotation={numberNotation}>
|
|
229
|
+
<div ref={ref} className={clsx('flex h-full min-h-0 min-w-0 bg-surface text-text', className)} onKeyDown={onKeyDown}>
|
|
230
|
+
<aside ref={column} className="relative flex shrink-0 flex-col border-r border-border" style={{ width }}>
|
|
231
|
+
<header className="flex h-9 shrink-0 items-center justify-between border-b border-border pr-2 pl-3">
|
|
232
|
+
<h2 className="text-sm font-semibold text-text">{t('workbench.title')}</h2>
|
|
233
|
+
{canManage && (
|
|
234
|
+
<IconButton
|
|
235
|
+
icon={Settings2}
|
|
236
|
+
size="sm"
|
|
237
|
+
label={t('workbench.manage')}
|
|
238
|
+
onClick={() => {
|
|
239
|
+
setManaged(null);
|
|
240
|
+
setManaging(true);
|
|
241
|
+
}}
|
|
242
|
+
/>
|
|
243
|
+
)}
|
|
244
|
+
</header>
|
|
245
|
+
<DatabaseExplorer connections={connections} value={selection} onValueChange={setSelection} className="min-h-0 flex-1" />
|
|
246
|
+
<ColumnResizeHandle from="left" onPointerDown={startResize} />
|
|
247
|
+
</aside>
|
|
248
|
+
<Tabs.Root
|
|
249
|
+
value={active?.id ?? null}
|
|
250
|
+
onValueChange={(id) => setState((current) => focusTab(current, String(id)))}
|
|
251
|
+
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
|
252
|
+
>
|
|
253
|
+
<Tabs.List
|
|
254
|
+
aria-label={t('workbench.tabs')}
|
|
255
|
+
className="px-2"
|
|
256
|
+
end={
|
|
257
|
+
<>
|
|
258
|
+
{active?.kind === 'table' && (
|
|
259
|
+
<Segmented
|
|
260
|
+
label={t('workbench.views')}
|
|
261
|
+
value={active.view}
|
|
262
|
+
onValueChange={(view) => setState((current) => setTableView(current, active.id, view))}
|
|
263
|
+
options={[
|
|
264
|
+
{ id: 'data', label: t('workbench.data'), icon: Table },
|
|
265
|
+
{ id: 'structure', label: t('workbench.structure'), icon: TableProperties }
|
|
266
|
+
]}
|
|
267
|
+
/>
|
|
268
|
+
)}
|
|
269
|
+
<IconButton
|
|
270
|
+
icon={SquareTerminal}
|
|
271
|
+
size="sm"
|
|
272
|
+
label={t('workbench.newConsole')}
|
|
273
|
+
tooltip={canOpenConsole ? undefined : t('workbench.noConnection')}
|
|
274
|
+
disabled={!canOpenConsole}
|
|
275
|
+
onClick={newConsole}
|
|
276
|
+
/>
|
|
277
|
+
</>
|
|
278
|
+
}
|
|
279
|
+
>
|
|
280
|
+
{tabs.map((tab) => (
|
|
281
|
+
<Tabs.Tab key={tab.id} value={tab.id} onClose={() => requestClose(tab.id)}>
|
|
282
|
+
<Icon icon={tab.kind === 'table' ? Table : tab.kind === 'console' ? SquareTerminal : PencilRuler} size={14} />
|
|
283
|
+
{titleOf(tab)}
|
|
284
|
+
</Tabs.Tab>
|
|
285
|
+
))}
|
|
286
|
+
</Tabs.List>
|
|
287
|
+
{tabs.length === 0 ? (
|
|
288
|
+
<EmptyState
|
|
289
|
+
icon={SquareTerminal}
|
|
290
|
+
title={t('workbench.empty.title')}
|
|
291
|
+
className="flex-1"
|
|
292
|
+
action={
|
|
293
|
+
<Button variant="secondary" disabled={!canOpenConsole} onClick={newConsole}>
|
|
294
|
+
{t('workbench.newConsole')}
|
|
295
|
+
</Button>
|
|
296
|
+
}
|
|
297
|
+
>
|
|
298
|
+
{t('workbench.empty.body')}
|
|
299
|
+
</EmptyState>
|
|
300
|
+
) : (
|
|
301
|
+
tabs.map((tab) => (
|
|
302
|
+
<Tabs.Panel key={tab.id} value={tab.id} keepMounted className="min-h-0 flex-1">
|
|
303
|
+
{viewOf(tab)}
|
|
304
|
+
</Tabs.Panel>
|
|
305
|
+
))
|
|
306
|
+
)}
|
|
307
|
+
</Tabs.Root>
|
|
308
|
+
<PromptDialog
|
|
309
|
+
open={closing !== null}
|
|
310
|
+
danger
|
|
311
|
+
title={t('workbench.discard.title', { table: closingTable })}
|
|
312
|
+
description={t('workbench.discard.description')}
|
|
313
|
+
confirmLabel={t('workbench.discard.confirm')}
|
|
314
|
+
onConfirm={() => {
|
|
315
|
+
if (closing !== null) {
|
|
316
|
+
closeById(closing);
|
|
317
|
+
}
|
|
318
|
+
setClosing(null);
|
|
319
|
+
}}
|
|
320
|
+
onOpenChange={(open) => {
|
|
321
|
+
if (!open) {
|
|
322
|
+
setClosing(null);
|
|
323
|
+
}
|
|
324
|
+
}}
|
|
325
|
+
/>
|
|
326
|
+
{onConnectionsChange !== undefined && (
|
|
327
|
+
<Dialog.Root open={managing} onOpenChange={setManaging}>
|
|
328
|
+
<Dialog.Popup className="flex h-[560px] w-[900px] flex-col overflow-hidden">
|
|
329
|
+
<div className="border-b border-border px-4 py-3">
|
|
330
|
+
<Dialog.Title>{t('workbench.connections')}</Dialog.Title>
|
|
331
|
+
</div>
|
|
332
|
+
<ConnectionManager
|
|
333
|
+
value={connections}
|
|
334
|
+
onValueChange={onConnectionsChange}
|
|
335
|
+
selected={managed}
|
|
336
|
+
onSelectedChange={setManaged}
|
|
337
|
+
className="min-h-0 flex-1"
|
|
338
|
+
/>
|
|
339
|
+
<Dialog.Footer>
|
|
340
|
+
<Dialog.Close render={<Button variant="secondary" />}>{t('workbench.close')}</Dialog.Close>
|
|
341
|
+
</Dialog.Footer>
|
|
342
|
+
</Dialog.Popup>
|
|
343
|
+
</Dialog.Root>
|
|
344
|
+
)}
|
|
345
|
+
</div>
|
|
346
|
+
</DatabaseProvider>
|
|
347
|
+
);
|
|
348
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DatabaseProvider } from '../DatabaseProvider.tsx';
|
|
2
|
+
import type { DatabaseAction } from '../actions.ts';
|
|
3
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage, useNumberNotation } from '../client-context.ts';
|
|
4
|
+
import type { Connection } from '../client/types.ts';
|
|
5
|
+
import { TableDesigner } from '../designer/TableDesigner.tsx';
|
|
6
|
+
import type { WorkbenchTab } from './tabs.ts';
|
|
7
|
+
import { useStableCallback } from '../use-stable-callback.ts';
|
|
8
|
+
|
|
9
|
+
export interface DesignerTabProps {
|
|
10
|
+
connection: Connection;
|
|
11
|
+
tab: Extract<WorkbenchTab, { kind: 'designer' }>;
|
|
12
|
+
/* The designer saved the table under this name, so the tab is now the designer of that table. */
|
|
13
|
+
onTableChange(table: string): void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/* The designer answers a save with `edit-table`; in its own tab that is the same tab, not another one. Everything else goes on to the workbench. */
|
|
17
|
+
export function DesignerTab({ connection, tab, onTableChange }: DesignerTabProps) {
|
|
18
|
+
const client = useDatabaseClient();
|
|
19
|
+
const storage = useDatabaseStorage();
|
|
20
|
+
const files = useDatabaseFiles();
|
|
21
|
+
const outer = useDatabaseAction();
|
|
22
|
+
const numberNotation = useNumberNotation();
|
|
23
|
+
|
|
24
|
+
const onAction = useStableCallback((action: DatabaseAction): void => {
|
|
25
|
+
if (action.kind === 'edit-table' && action.ref.connectionId === tab.connectionId && action.ref.schema === tab.schema) {
|
|
26
|
+
onTableChange(action.ref.table);
|
|
27
|
+
} else {
|
|
28
|
+
outer?.(action);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<DatabaseProvider client={client} storage={storage} files={files} numberNotation={numberNotation} onAction={onAction}>
|
|
34
|
+
<TableDesigner connection={connection} schema={tab.schema} table={tab.table} className="h-full" />
|
|
35
|
+
</DatabaseProvider>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Connection } from '../client/types.ts';
|
|
2
|
+
import { StructureView } from '../structure/StructureView.tsx';
|
|
3
|
+
import { TableView } from '../table/TableView.tsx';
|
|
4
|
+
import type { WorkbenchTab } from './tabs.ts';
|
|
5
|
+
|
|
6
|
+
export interface TableTabProps {
|
|
7
|
+
connection: Connection;
|
|
8
|
+
tab: Extract<WorkbenchTab, { kind: 'table' }>;
|
|
9
|
+
/* Told whether the data holds changes that are not submitted. The view that switches `tab.view` is the workbench's. */
|
|
10
|
+
onDirtyChange?(dirty: boolean): void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/* The data and the structure of one table, the one that `tab.view` names in front. Both stay mounted, so pending edits in the data survive a look at the structure. */
|
|
14
|
+
export function TableTab({ connection, tab, onDirtyChange }: TableTabProps) {
|
|
15
|
+
const { schema, table } = tab.ref;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="flex h-full min-h-0 flex-col">
|
|
19
|
+
<div className="min-h-0 flex-1" hidden={tab.view !== 'data'}>
|
|
20
|
+
<TableView connection={connection} schema={schema} table={table} defaultWhere={tab.where} onDirtyChange={onDirtyChange} className="h-full" />
|
|
21
|
+
</div>
|
|
22
|
+
<div className="min-h-0 flex-1" hidden={tab.view !== 'structure'}>
|
|
23
|
+
<StructureView connection={connection} schema={schema} table={table} className="h-full" />
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { TableRef } from '../client/types.ts';
|
|
2
|
+
|
|
3
|
+
export type TableViewMode = 'data' | 'structure';
|
|
4
|
+
|
|
5
|
+
export type WorkbenchTab =
|
|
6
|
+
| { readonly id: string; readonly kind: 'table'; readonly ref: TableRef; readonly view: TableViewMode; readonly where?: string }
|
|
7
|
+
| { readonly id: string; readonly kind: 'console'; readonly connectionId: string; readonly schema?: string; readonly sql: string; readonly number: number }
|
|
8
|
+
/* Without `table` the designer makes a new table. */
|
|
9
|
+
| { readonly id: string; readonly kind: 'designer'; readonly connectionId: string; readonly schema: string; readonly table?: string };
|
|
10
|
+
|
|
11
|
+
export interface WorkbenchState {
|
|
12
|
+
readonly tabs: readonly WorkbenchTab[];
|
|
13
|
+
readonly activeId: string | null;
|
|
14
|
+
/* The next number for an id that is not derived from what the tab shows. Never reused, so a restored tab cannot clash with a new one. */
|
|
15
|
+
readonly serial: number;
|
|
16
|
+
/* The number the last console got, for its title. */
|
|
17
|
+
readonly consoles: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const STORAGE_KEY = 'database:workbench';
|
|
21
|
+
|
|
22
|
+
export const emptyState: WorkbenchState = { tabs: [], activeId: null, serial: 0, consoles: 0 };
|
|
23
|
+
|
|
24
|
+
export const connectionIdOf = (tab: WorkbenchTab): string => (tab.kind === 'table' ? tab.ref.connectionId : tab.connectionId);
|
|
25
|
+
|
|
26
|
+
const tableTabId = (ref: TableRef): string => `table:${ref.connectionId}\u0000${ref.schema}\u0000${ref.table}`;
|
|
27
|
+
|
|
28
|
+
const add = (state: WorkbenchState, tab: WorkbenchTab): WorkbenchState => ({ ...state, tabs: [...state.tabs, tab], activeId: tab.id });
|
|
29
|
+
|
|
30
|
+
const update = (state: WorkbenchState, id: string, change: (tab: WorkbenchTab) => WorkbenchTab): WorkbenchState => {
|
|
31
|
+
let changed = false;
|
|
32
|
+
const tabs = state.tabs.map((tab) => {
|
|
33
|
+
if (tab.id !== id) {
|
|
34
|
+
return tab;
|
|
35
|
+
}
|
|
36
|
+
const next = change(tab);
|
|
37
|
+
changed ||= next !== tab;
|
|
38
|
+
return next;
|
|
39
|
+
});
|
|
40
|
+
return changed ? { ...state, tabs } : state;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const focusTab = (state: WorkbenchState, id: string): WorkbenchState =>
|
|
44
|
+
state.activeId === id || !state.tabs.some((tab) => tab.id === id) ? state : { ...state, activeId: id };
|
|
45
|
+
|
|
46
|
+
/* A table opens once per table; a filtered one is always a tab of its own, since the filter is what the person asked to see. */
|
|
47
|
+
export const openTable = (state: WorkbenchState, ref: TableRef, view: TableViewMode, where?: string): WorkbenchState => {
|
|
48
|
+
if (where !== undefined && where.trim() !== '') {
|
|
49
|
+
return add({ ...state, serial: state.serial + 1 }, { id: `filtered:${state.serial + 1}`, kind: 'table', ref, view, where });
|
|
50
|
+
}
|
|
51
|
+
const id = tableTabId(ref);
|
|
52
|
+
const existing = state.tabs.find((tab) => tab.id === id);
|
|
53
|
+
if (existing === undefined) {
|
|
54
|
+
return add(state, { id, kind: 'table', ref, view });
|
|
55
|
+
}
|
|
56
|
+
return focusTab(setTableView(state, id, view), id);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export const openConsole = (state: WorkbenchState, connectionId: string, schema?: string, sql = ''): WorkbenchState => {
|
|
60
|
+
const serial = state.serial + 1;
|
|
61
|
+
const consoles = state.consoles + 1;
|
|
62
|
+
return add({ ...state, serial, consoles }, { id: `console:${serial}`, kind: 'console', connectionId, schema, sql, number: consoles });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/* A table is designed in one tab; a new table gets a tab each time. */
|
|
66
|
+
export const openDesigner = (state: WorkbenchState, connectionId: string, schema: string, table?: string): WorkbenchState => {
|
|
67
|
+
const existing =
|
|
68
|
+
table === undefined
|
|
69
|
+
? undefined
|
|
70
|
+
: state.tabs.find((tab) => tab.kind === 'designer' && tab.connectionId === connectionId && tab.schema === schema && tab.table === table);
|
|
71
|
+
if (existing !== undefined) {
|
|
72
|
+
return focusTab(state, existing.id);
|
|
73
|
+
}
|
|
74
|
+
const serial = state.serial + 1;
|
|
75
|
+
return add({ ...state, serial }, { id: `designer:${serial}`, kind: 'designer', connectionId, schema, table });
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export const setTableView = (state: WorkbenchState, id: string, view: TableViewMode): WorkbenchState =>
|
|
79
|
+
update(state, id, (tab) => (tab.kind === 'table' && tab.view !== view ? { ...tab, view } : tab));
|
|
80
|
+
|
|
81
|
+
export const setConsoleSql = (state: WorkbenchState, id: string, sql: string): WorkbenchState =>
|
|
82
|
+
update(state, id, (tab) => (tab.kind === 'console' && tab.sql !== sql ? { ...tab, sql } : tab));
|
|
83
|
+
|
|
84
|
+
/* The designer of a new table became the designer of that table, in the same tab. Closes any other designer of it. */
|
|
85
|
+
export const setDesignerTable = (state: WorkbenchState, id: string, table: string): WorkbenchState => {
|
|
86
|
+
const subject = state.tabs.find((tab) => tab.id === id);
|
|
87
|
+
if (subject?.kind !== 'designer' || subject.table === table) {
|
|
88
|
+
return state;
|
|
89
|
+
}
|
|
90
|
+
const tabs = state.tabs.filter(
|
|
91
|
+
(tab) =>
|
|
92
|
+
tab.id === id || !(tab.kind === 'designer' && tab.connectionId === subject.connectionId && tab.schema === subject.schema && tab.table === table)
|
|
93
|
+
);
|
|
94
|
+
return update({ ...state, tabs }, id, (tab) => (tab.kind === 'designer' ? { ...tab, table } : tab));
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/* The tab next to the closed one comes forward when the closed one was in front. */
|
|
98
|
+
export const closeTab = (state: WorkbenchState, id: string): WorkbenchState => {
|
|
99
|
+
const at = state.tabs.findIndex((tab) => tab.id === id);
|
|
100
|
+
if (at < 0) {
|
|
101
|
+
return state;
|
|
102
|
+
}
|
|
103
|
+
const tabs = state.tabs.filter((tab) => tab.id !== id);
|
|
104
|
+
return { ...state, tabs, activeId: state.activeId === id ? (tabs[Math.min(at, tabs.length - 1)]?.id ?? null) : state.activeId };
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/* Drops the tabs of connections that are gone. */
|
|
108
|
+
export const pruneTabs = (state: WorkbenchState, connectionIds: ReadonlySet<string>): WorkbenchState => {
|
|
109
|
+
const tabs = state.tabs.filter((tab) => connectionIds.has(connectionIdOf(tab)));
|
|
110
|
+
if (tabs.length === state.tabs.length) {
|
|
111
|
+
return state;
|
|
112
|
+
}
|
|
113
|
+
return { ...state, tabs, activeId: tabs.some((tab) => tab.id === state.activeId) ? state.activeId : (tabs.at(-1)?.id ?? null) };
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
export const serializeState = (state: WorkbenchState): string => JSON.stringify({ version: 1, ...state });
|
|
117
|
+
|
|
118
|
+
const isText = (value: unknown): value is string => typeof value === 'string' && value !== '';
|
|
119
|
+
|
|
120
|
+
const isOptionalText = (value: unknown): value is string | undefined => value === undefined || typeof value === 'string';
|
|
121
|
+
|
|
122
|
+
const refOf = (value: unknown): TableRef | null => {
|
|
123
|
+
const ref = value as Partial<Record<keyof TableRef, unknown>> | null;
|
|
124
|
+
return typeof ref === 'object' && ref !== null && isText(ref.connectionId) && isText(ref.schema) && isText(ref.table)
|
|
125
|
+
? { connectionId: ref.connectionId, schema: ref.schema, table: ref.table }
|
|
126
|
+
: null;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const tabOf = (value: unknown): WorkbenchTab | null => {
|
|
130
|
+
const raw = value as Record<string, unknown> | null;
|
|
131
|
+
if (typeof raw !== 'object' || raw === null || !isText(raw.id)) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const id = raw.id;
|
|
135
|
+
switch (raw.kind) {
|
|
136
|
+
case 'table': {
|
|
137
|
+
const ref = refOf(raw.ref);
|
|
138
|
+
const view = raw.view === 'structure' ? 'structure' : 'data';
|
|
139
|
+
return ref === null || !isOptionalText(raw.where) ? null : { id, kind: 'table', ref, view, where: raw.where };
|
|
140
|
+
}
|
|
141
|
+
case 'console':
|
|
142
|
+
return isText(raw.connectionId) && isOptionalText(raw.schema) && typeof raw.sql === 'string' && typeof raw.number === 'number'
|
|
143
|
+
? { id, kind: 'console', connectionId: raw.connectionId, schema: raw.schema, sql: raw.sql, number: raw.number }
|
|
144
|
+
: null;
|
|
145
|
+
case 'designer':
|
|
146
|
+
return isText(raw.connectionId) && isText(raw.schema) && isOptionalText(raw.table)
|
|
147
|
+
? { id, kind: 'designer', connectionId: raw.connectionId, schema: raw.schema, table: raw.table }
|
|
148
|
+
: null;
|
|
149
|
+
default:
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
/* What a past run stored, or an empty workbench for anything that is missing or not ours. A tab that is not valid is left out, the rest stays. */
|
|
155
|
+
export const parseState = (raw: string | null): WorkbenchState => {
|
|
156
|
+
if (raw === null) {
|
|
157
|
+
return emptyState;
|
|
158
|
+
}
|
|
159
|
+
let data: Record<string, unknown>;
|
|
160
|
+
try {
|
|
161
|
+
data = JSON.parse(raw) as Record<string, unknown>;
|
|
162
|
+
} catch {
|
|
163
|
+
return emptyState;
|
|
164
|
+
}
|
|
165
|
+
if (typeof data !== 'object' || data === null || data.version !== 1 || !Array.isArray(data.tabs)) {
|
|
166
|
+
return emptyState;
|
|
167
|
+
}
|
|
168
|
+
const seen = new Set<string>();
|
|
169
|
+
const tabs = data.tabs.map(tabOf).filter((tab): tab is WorkbenchTab => {
|
|
170
|
+
if (tab === null || seen.has(tab.id)) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
seen.add(tab.id);
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
const activeId = typeof data.activeId === 'string' && seen.has(data.activeId) ? data.activeId : (tabs.at(-1)?.id ?? null);
|
|
177
|
+
const counter = (value: unknown): number => (typeof value === 'number' && Number.isFinite(value) && value >= 0 ? Math.floor(value) : 0);
|
|
178
|
+
return {
|
|
179
|
+
tabs,
|
|
180
|
+
activeId,
|
|
181
|
+
serial: Math.max(counter(data.serial), ...tabs.map((tab) => Number(/^(?:filtered|console|designer):(\d+)$/.exec(tab.id)?.[1] ?? 0))),
|
|
182
|
+
consoles: Math.max(counter(data.consoles), ...tabs.map((tab) => (tab.kind === 'console' ? tab.number : 0)))
|
|
183
|
+
};
|
|
184
|
+
};
|