@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,14 @@
|
|
|
1
|
+
import { type Ref } from 'react';
|
|
2
|
+
import type { GridColumn } from '../grid/types.ts';
|
|
3
|
+
import type { EditValue, Value } from '../protocol/index.ts';
|
|
4
|
+
export interface ValuePanelProps {
|
|
5
|
+
column: GridColumn | null;
|
|
6
|
+
value: Value | undefined;
|
|
7
|
+
loading: boolean;
|
|
8
|
+
editable: boolean;
|
|
9
|
+
onCommit?(value: EditValue): void;
|
|
10
|
+
onClose(): void;
|
|
11
|
+
className?: string;
|
|
12
|
+
ref?: Ref<HTMLDivElement>;
|
|
13
|
+
}
|
|
14
|
+
export declare function ValuePanel({ column, value, loading, editable, onCommit, onClose, className, ref }: ValuePanelProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { Copy, Ellipsis, MousePointerClick, WrapText, X } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Button, copyText, EmptyState, FormError, IconButton, isApplePlatform, KEY_SHORTCUTS, matchesShortcut, Menu, PanelHeader, Segmented, TextArea, Tooltip } from '@adecore/ui';
|
|
7
|
+
import { formatBytes, formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { editValueOf, sameDraft, sameSource, sourceOf } from './draft.js';
|
|
9
|
+
import { hexDumpLines, parseHex } from './hex.js';
|
|
10
|
+
import { renderView, sizeOf, viewsOf } from './views.js';
|
|
11
|
+
const WRAPPING_VIEWS = new Set(['text', 'formatted', 'utf8']);
|
|
12
|
+
const READING = 'min-h-0 flex-1 overflow-auto rounded-lg border border-border bg-surface p-2 font-mono text-code text-text';
|
|
13
|
+
/* The whole value of the focused cell beside the grid, to read and edit what a cell is too small for. */
|
|
14
|
+
export function ValuePanel({ column, value, loading, editable, onCommit, onClose, className, ref }) {
|
|
15
|
+
const { t } = useTranslation('database');
|
|
16
|
+
const ready = column !== null && !loading && value !== undefined;
|
|
17
|
+
return (_jsxs("div", { ref: ref, className: clsx('flex h-full min-h-0 min-w-0 flex-col', className), children: [_jsxs(PanelHeader, { title: column === null ? t('value.title') : undefined, children: [column !== null && (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 truncate text-sm font-medium text-text", children: column.name }), column.type !== '' && _jsx("span", { className: "min-w-0 shrink-0 truncate text-xs text-text-faint", children: column.type })] })), _jsx(IconButton, { icon: X, size: "sm", label: t('value.close'), className: "ml-auto", onClick: onClose })] }), ready ? (_jsx(ValueEditor, { column: column, value: value, editable: editable, onCommit: onCommit })) : (_jsx("div", { className: "grid min-h-0 grow place-items-center", children: _jsx(EmptyState, { icon: column === null ? MousePointerClick : undefined, busy: column !== null && loading, children: column === null ? t('value.selectCell') : loading ? t('value.loading') : t('value.notLoaded') }) }))] }));
|
|
18
|
+
}
|
|
19
|
+
function ValueEditor({ column, value, editable, onCommit }) {
|
|
20
|
+
const { t } = useTranslation('database');
|
|
21
|
+
const { name, type, kind } = column;
|
|
22
|
+
const source = useMemo(() => sourceOf({ name, type, kind }, value), [name, type, kind, value]);
|
|
23
|
+
const [seen, setSeen] = useState(source);
|
|
24
|
+
const [draft, setDraft] = useState(source.original);
|
|
25
|
+
const [choice, setChoice] = useState(null);
|
|
26
|
+
const [problem, setProblem] = useState(null);
|
|
27
|
+
const [wrap, setWrap] = useState(true);
|
|
28
|
+
// A new cell, or the same cell with a new value, starts over from that value.
|
|
29
|
+
if (!sameSource(seen, source)) {
|
|
30
|
+
setSeen(source);
|
|
31
|
+
setDraft(source.original);
|
|
32
|
+
setChoice(null);
|
|
33
|
+
setProblem(null);
|
|
34
|
+
}
|
|
35
|
+
const views = viewsOf(source, draft);
|
|
36
|
+
const view = choice !== null && views.includes(choice) ? choice : views[0];
|
|
37
|
+
const dirty = !sameDraft(draft, source.original);
|
|
38
|
+
const rendered = useMemo(() => (view === 'text' ? null : renderView(view, draft)), [view, draft]);
|
|
39
|
+
const hex = source.binary && draft.mode === 'value' ? parseHex(draft.text) : null;
|
|
40
|
+
const shownProblem = problem ?? (hex !== null && !hex.ok ? { kind: 'hex', problem: hex.problem } : null);
|
|
41
|
+
const size = sizeOf(source, draft);
|
|
42
|
+
const placeholder = draft.mode === 'null' ? 'NULL' : draft.mode === 'default' ? 'DEFAULT' : undefined;
|
|
43
|
+
const change = (next) => {
|
|
44
|
+
setDraft(next);
|
|
45
|
+
setProblem(null);
|
|
46
|
+
};
|
|
47
|
+
const apply = () => {
|
|
48
|
+
if (!editable || onCommit === undefined || !dirty) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const conversion = editValueOf(draft, source);
|
|
52
|
+
if (conversion.ok) {
|
|
53
|
+
onCommit(conversion.value);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setProblem(conversion.problem);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
const handleKeyDown = (event) => {
|
|
60
|
+
if (!event.nativeEvent.isComposing && matchesShortcut(KEY_SHORTCUTS.modEnter, event.nativeEvent, isApplePlatform())) {
|
|
61
|
+
event.preventDefault();
|
|
62
|
+
apply();
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const copy = () => {
|
|
66
|
+
if (view === 'hex' && hex !== null && hex.ok) {
|
|
67
|
+
copyText(hexDumpLines(hex.hex).join('\n'));
|
|
68
|
+
}
|
|
69
|
+
else if (rendered !== null && rendered.kind === 'text') {
|
|
70
|
+
copyText(rendered.text);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
copyText(draft.mode === 'value' ? draft.text : draft.mode === 'null' ? 'NULL' : 'DEFAULT');
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
const editor = (extra) => (_jsx(TextArea, { mono: true, "aria-label": t(source.binary ? 'value.hexEditor' : 'value.editor', { column: column.name }), wrap: wrap || source.binary ? 'soft' : 'off', spellCheck: false, readOnly: !editable, placeholder: placeholder, value: draft.mode === 'value' ? draft.text : '', className: extra, onChange: (event) => change({ text: event.target.value, mode: 'value' }), onKeyDown: handleKeyDown }));
|
|
77
|
+
const problemText = (found) => found.kind === 'json'
|
|
78
|
+
? t('value.invalidJson', { reason: found.reason })
|
|
79
|
+
: t(found.problem === 'odd' ? 'value.invalidHexOdd' : 'value.invalidHexCharacters');
|
|
80
|
+
const reading = (content, wrapped) => {
|
|
81
|
+
if (content.kind === 'empty') {
|
|
82
|
+
return _jsx(Faint, { children: placeholder });
|
|
83
|
+
}
|
|
84
|
+
if (content.kind === 'problem') {
|
|
85
|
+
return _jsx(Faint, { children: problemText(content.problem) });
|
|
86
|
+
}
|
|
87
|
+
return (_jsxs(_Fragment, { children: [_jsx("pre", { className: clsx(READING, wrapped ? 'break-words whitespace-pre-wrap' : 'whitespace-pre'), children: content.text }), content.truncated && (_jsx("p", { className: "shrink-0 text-xs text-text-faint", children: t('value.dumpTruncated', { shown: formatNumber(content.truncated.shown), total: formatNumber(content.truncated.total) }) }))] }));
|
|
88
|
+
};
|
|
89
|
+
const body = () => {
|
|
90
|
+
if (view === 'text') {
|
|
91
|
+
return editor('min-h-0 flex-1');
|
|
92
|
+
}
|
|
93
|
+
if (view === 'hex' && editable) {
|
|
94
|
+
return (_jsxs(_Fragment, { children: [draft.mode === 'value' && rendered !== null && reading(rendered, false), editor(draft.mode === 'value' ? 'flex-none' : 'min-h-0 flex-1')] }));
|
|
95
|
+
}
|
|
96
|
+
return rendered === null ? null : reading(rendered, wrap && WRAPPING_VIEWS.has(view));
|
|
97
|
+
};
|
|
98
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex h-10 shrink-0 items-center gap-2 px-2", children: [views.length > 1 && (_jsx(Segmented, { value: view, onValueChange: (id) => setChoice(id), label: t('value.views'), options: views.map((id) => ({ id, label: t(`value.view.${id}`) })) })), _jsxs("div", { className: "ml-auto flex items-center gap-1", children: [WRAPPING_VIEWS.has(view) && (_jsx(IconButton, { icon: WrapText, size: "sm", label: t('value.wrap'), "aria-pressed": wrap, active: wrap, onClick: () => setWrap(!wrap) })), _jsx(IconButton, { icon: Copy, size: "sm", label: t('value.copy'), onClick: copy })] })] }), _jsx("div", { className: "flex min-h-0 flex-1 flex-col gap-2 px-2 pb-2", children: body() }), shownProblem !== null && _jsx(FormError, { className: "shrink-0 px-3 pb-2 text-xs", children: problemText(shownProblem) }), editable && (_jsxs("div", { className: "flex shrink-0 items-center gap-1.5 border-t border-border p-2", children: [_jsxs(Menu.Root, { children: [_jsx(IconButton, { icon: Ellipsis, size: "sm", label: t('value.more'), render: _jsx(Menu.Trigger, {}) }), _jsxs(Menu.Popup, { children: [column.nullable !== false && (_jsx(Menu.Item, { disabled: draft.mode === 'null', onClick: () => change({ text: '', mode: 'null' }), children: t('value.setNull') })), _jsx(Menu.Item, { disabled: draft.mode === 'default', onClick: () => change({ text: '', mode: 'default' }), children: t('value.setDefault') })] })] }), _jsxs("span", { className: "ml-auto flex items-center gap-1.5", children: [_jsx(Button, { size: "xs", disabled: !dirty, onClick: () => change(source.original), children: t('value.revert') }), _jsx(Tooltip, { label: t('value.applyHint'), kbd: KEY_SHORTCUTS.modEnter, children: _jsx(Button, { size: "xs", variant: "primary", disabled: !dirty || onCommit === undefined, onClick: apply, children: t('value.apply') }) })] })] })), _jsx("footer", { className: "flex h-8 shrink-0 items-center border-t border-border px-3 text-xs text-text-muted tabular-nums", children: size !== null &&
|
|
99
|
+
(size.unit === 'bytes' ? formatBytes(size.count) : t('value.characters', { count: size.count, formatted: formatNumber(size.count) })) })] }));
|
|
100
|
+
}
|
|
101
|
+
function Faint({ children }) {
|
|
102
|
+
return _jsx("div", { className: "grid min-h-0 flex-1 place-items-center font-mono text-code text-text-faint", children: children });
|
|
103
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GridColumn } from '../grid/types.ts';
|
|
2
|
+
import type { EditValue, Value } from '../protocol/index.ts';
|
|
3
|
+
import { type HexProblem } from './hex.ts';
|
|
4
|
+
export interface Draft {
|
|
5
|
+
readonly text: string;
|
|
6
|
+
readonly mode: 'value' | 'null' | 'default';
|
|
7
|
+
}
|
|
8
|
+
export interface Source {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly type: string;
|
|
11
|
+
readonly kind: GridColumn['kind'];
|
|
12
|
+
readonly binary: boolean;
|
|
13
|
+
readonly json: boolean;
|
|
14
|
+
readonly original: Draft;
|
|
15
|
+
}
|
|
16
|
+
export type DraftProblem = {
|
|
17
|
+
readonly kind: 'json';
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly kind: 'hex';
|
|
21
|
+
readonly problem: HexProblem;
|
|
22
|
+
};
|
|
23
|
+
export type Conversion = {
|
|
24
|
+
readonly ok: true;
|
|
25
|
+
readonly value: EditValue;
|
|
26
|
+
} | {
|
|
27
|
+
readonly ok: false;
|
|
28
|
+
readonly problem: DraftProblem;
|
|
29
|
+
};
|
|
30
|
+
export declare const sourceOf: (column: Pick<GridColumn, 'name' | 'type' | 'kind'>, value: Value) => Source;
|
|
31
|
+
export declare const sameSource: (a: Source, b: Source) => boolean;
|
|
32
|
+
export declare const sameDraft: (a: Draft, b: Draft) => boolean;
|
|
33
|
+
export declare const editValueOf: (draft: Draft, source: Source) => Conversion;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { parseDraft } from '../grid/edit-value.js';
|
|
2
|
+
import { parseHex } from './hex.js';
|
|
3
|
+
import { checkJson, isJsonDocument } from './json.js';
|
|
4
|
+
/* The text a value is edited as: a binary value as its hex, everything else as it is written. */
|
|
5
|
+
const textOf = (value) => {
|
|
6
|
+
if (typeof value === 'object') {
|
|
7
|
+
return value.hex;
|
|
8
|
+
}
|
|
9
|
+
return String(value);
|
|
10
|
+
};
|
|
11
|
+
export const sourceOf = (column, value) => {
|
|
12
|
+
const binary = column.kind === 'binary' || (value !== null && typeof value === 'object');
|
|
13
|
+
const original = value === null ? { text: '', mode: 'null' } : { text: textOf(value), mode: 'value' };
|
|
14
|
+
return {
|
|
15
|
+
name: column.name,
|
|
16
|
+
type: column.type,
|
|
17
|
+
kind: column.kind,
|
|
18
|
+
binary,
|
|
19
|
+
json: !binary && (column.kind === 'json' ? value !== null : typeof value === 'string' && isJsonDocument(value)),
|
|
20
|
+
original
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
export const sameSource = (a, b) => a.name === b.name && a.type === b.type && a.kind === b.kind && a.binary === b.binary && a.json === b.json && sameDraft(a.original, b.original);
|
|
24
|
+
export const sameDraft = (a, b) => a.mode === b.mode && (a.mode !== 'value' || a.text === b.text);
|
|
25
|
+
/* A draft becomes the value to send. Text goes as text, and a number only where the column holds numbers and the text spells one. */
|
|
26
|
+
export const editValueOf = (draft, source) => {
|
|
27
|
+
if (draft.mode === 'null') {
|
|
28
|
+
return { ok: true, value: null };
|
|
29
|
+
}
|
|
30
|
+
if (draft.mode === 'default') {
|
|
31
|
+
return { ok: true, value: { kind: 'default' } };
|
|
32
|
+
}
|
|
33
|
+
if (source.binary) {
|
|
34
|
+
const parsed = parseHex(draft.text);
|
|
35
|
+
return parsed.ok ? { ok: true, value: { kind: 'binary', hex: parsed.hex } } : { ok: false, problem: { kind: 'hex', problem: parsed.problem } };
|
|
36
|
+
}
|
|
37
|
+
if (source.json) {
|
|
38
|
+
const checked = checkJson(draft.text);
|
|
39
|
+
if (!checked.ok) {
|
|
40
|
+
return { ok: false, problem: { kind: 'json', reason: checked.reason } };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return { ok: true, value: parseDraft(draft.text, source.kind) };
|
|
44
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type HexProblem = 'characters' | 'odd';
|
|
2
|
+
export type ParsedHex = {
|
|
3
|
+
readonly ok: true;
|
|
4
|
+
readonly hex: string;
|
|
5
|
+
} | {
|
|
6
|
+
readonly ok: false;
|
|
7
|
+
readonly problem: HexProblem;
|
|
8
|
+
};
|
|
9
|
+
export declare const BYTES_PER_LINE = 16;
|
|
10
|
+
export declare const parseHex: (text: string) => ParsedHex;
|
|
11
|
+
export declare const bytesOf: (hex: string) => Uint8Array;
|
|
12
|
+
export declare const decodeUtf8: (hex: string) => string;
|
|
13
|
+
export declare const formatUuid: (hex: string) => string | null;
|
|
14
|
+
export declare const hexDumpLines: (hex: string, maxBytes?: number) => string[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/* Binary values cross as lowercase hex; these turn it into what a person reads and back. */
|
|
2
|
+
export const BYTES_PER_LINE = 16;
|
|
3
|
+
const GROUP = 8;
|
|
4
|
+
const GROUP_WIDTH = GROUP * 3 - 1;
|
|
5
|
+
const NOT_HEX = /[^0-9a-f]/;
|
|
6
|
+
const UUID_BYTES = 16;
|
|
7
|
+
/* Spaces and newlines may separate the digits, and a leading `0x` is the way the grid writes a binary value. */
|
|
8
|
+
export const parseHex = (text) => {
|
|
9
|
+
const stripped = text.replace(/\s+/g, '').toLowerCase();
|
|
10
|
+
const hex = stripped.startsWith('0x') ? stripped.slice(2) : stripped;
|
|
11
|
+
if (NOT_HEX.test(hex)) {
|
|
12
|
+
return { ok: false, problem: 'characters' };
|
|
13
|
+
}
|
|
14
|
+
return hex.length % 2 === 0 ? { ok: true, hex } : { ok: false, problem: 'odd' };
|
|
15
|
+
};
|
|
16
|
+
export const bytesOf = (hex) => {
|
|
17
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
18
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
19
|
+
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
20
|
+
}
|
|
21
|
+
return bytes;
|
|
22
|
+
};
|
|
23
|
+
/* Bytes that are not valid UTF-8 become replacement characters rather than an error. */
|
|
24
|
+
export const decodeUtf8 = (hex) => new TextDecoder('utf-8').decode(bytesOf(hex));
|
|
25
|
+
/* A UUID is exactly 16 bytes; anything else has no UUID form. */
|
|
26
|
+
export const formatUuid = (hex) => {
|
|
27
|
+
if (hex.length !== UUID_BYTES * 2) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
31
|
+
};
|
|
32
|
+
const isPrintable = (byte) => byte >= 0x20 && byte <= 0x7e;
|
|
33
|
+
const slotsOf = (bytes, from, to) => Array.from(bytes.subarray(from, to), (byte) => byte.toString(16).padStart(2, '0'))
|
|
34
|
+
.join(' ')
|
|
35
|
+
.padEnd(GROUP_WIDTH);
|
|
36
|
+
/* One line per 16 bytes: the offset, the bytes in two groups of eight, and the same bytes as ASCII with `.` for what does not print. */
|
|
37
|
+
export const hexDumpLines = (hex, maxBytes = Number.POSITIVE_INFINITY) => {
|
|
38
|
+
const bytes = bytesOf(hex.slice(0, Math.min(hex.length, maxBytes * 2)));
|
|
39
|
+
const lines = [];
|
|
40
|
+
for (let offset = 0; offset < bytes.length; offset += BYTES_PER_LINE) {
|
|
41
|
+
const line = bytes.subarray(offset, offset + BYTES_PER_LINE);
|
|
42
|
+
const ascii = Array.from(line, (byte) => (isPrintable(byte) ? String.fromCharCode(byte) : '.')).join('');
|
|
43
|
+
lines.push(`${offset.toString(16).padStart(8, '0')} ${slotsOf(bytes, offset, offset + GROUP)} ${slotsOf(bytes, offset + GROUP, offset + BYTES_PER_LINE)} |${ascii}|`);
|
|
44
|
+
}
|
|
45
|
+
return lines;
|
|
46
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type JsonCheck = {
|
|
2
|
+
readonly ok: true;
|
|
3
|
+
} | {
|
|
4
|
+
readonly ok: false;
|
|
5
|
+
readonly reason: string;
|
|
6
|
+
};
|
|
7
|
+
export declare const checkJson: (text: string) => JsonCheck;
|
|
8
|
+
export declare const isJsonDocument: (text: string) => boolean;
|
|
9
|
+
export declare const prettyJson: (text: string) => string | null;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* JSON detection and layout. Numbers stay as the text the server wrote, so a 64-bit integer is not rounded by a parse. */
|
|
2
|
+
const INDENT = ' ';
|
|
3
|
+
export const checkJson = (text) => {
|
|
4
|
+
try {
|
|
5
|
+
JSON.parse(text);
|
|
6
|
+
return { ok: true };
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/* Whether a string is a JSON object or array. A scalar such as `42` or `"x"` is a plain value, not a document. */
|
|
13
|
+
export const isJsonDocument = (text) => {
|
|
14
|
+
const start = text.trimStart()[0];
|
|
15
|
+
return (start === '{' || start === '[') && checkJson(text).ok;
|
|
16
|
+
};
|
|
17
|
+
/* The text laid out with two spaces per level, or `null` when it is not JSON. */
|
|
18
|
+
export const prettyJson = (text) => {
|
|
19
|
+
if (!checkJson(text).ok) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let output = '';
|
|
23
|
+
let depth = 0;
|
|
24
|
+
let i = 0;
|
|
25
|
+
const lineBreak = () => `\n${INDENT.repeat(depth)}`;
|
|
26
|
+
const nextToken = (from) => {
|
|
27
|
+
let at = from;
|
|
28
|
+
while (/\s/.test(text[at] ?? '')) {
|
|
29
|
+
at++;
|
|
30
|
+
}
|
|
31
|
+
return text[at] ?? '';
|
|
32
|
+
};
|
|
33
|
+
while (i < text.length) {
|
|
34
|
+
const char = text[i];
|
|
35
|
+
if (char === '"') {
|
|
36
|
+
let end = i + 1;
|
|
37
|
+
while (text[end] !== '"') {
|
|
38
|
+
end += text[end] === '\\' ? 2 : 1;
|
|
39
|
+
}
|
|
40
|
+
output += text.slice(i, end + 1);
|
|
41
|
+
i = end + 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (char === '{' || char === '[') {
|
|
45
|
+
const empty = nextToken(i + 1) === (char === '{' ? '}' : ']');
|
|
46
|
+
output += char;
|
|
47
|
+
if (empty) {
|
|
48
|
+
output += char === '{' ? '}' : ']';
|
|
49
|
+
i = text.indexOf(char === '{' ? '}' : ']', i + 1) + 1;
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
depth++;
|
|
53
|
+
output += lineBreak();
|
|
54
|
+
}
|
|
55
|
+
else if (char === '}' || char === ']') {
|
|
56
|
+
depth--;
|
|
57
|
+
output += `${lineBreak()}${char}`;
|
|
58
|
+
}
|
|
59
|
+
else if (char === ',') {
|
|
60
|
+
output += `,${lineBreak()}`;
|
|
61
|
+
}
|
|
62
|
+
else if (char === ':') {
|
|
63
|
+
output += ': ';
|
|
64
|
+
}
|
|
65
|
+
else if (!/\s/.test(char)) {
|
|
66
|
+
output += char;
|
|
67
|
+
}
|
|
68
|
+
i++;
|
|
69
|
+
}
|
|
70
|
+
return output.trim();
|
|
71
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Draft, DraftProblem, Source } from './draft.ts';
|
|
2
|
+
export type ViewId = 'text' | 'formatted' | 'hex' | 'utf8' | 'uuid';
|
|
3
|
+
export declare const viewsOf: (source: Source, draft: Draft) => ViewId[];
|
|
4
|
+
export declare const sizeOf: (source: Source, draft: Draft) => {
|
|
5
|
+
unit: 'bytes' | 'characters';
|
|
6
|
+
count: number;
|
|
7
|
+
} | null;
|
|
8
|
+
export declare const DUMP_LIMIT_BYTES: number;
|
|
9
|
+
export type Rendered = {
|
|
10
|
+
readonly kind: 'empty';
|
|
11
|
+
} | {
|
|
12
|
+
readonly kind: 'text';
|
|
13
|
+
readonly text: string;
|
|
14
|
+
readonly truncated?: {
|
|
15
|
+
readonly shown: number;
|
|
16
|
+
readonly total: number;
|
|
17
|
+
};
|
|
18
|
+
} | {
|
|
19
|
+
readonly kind: 'problem';
|
|
20
|
+
readonly problem: DraftProblem;
|
|
21
|
+
};
|
|
22
|
+
export declare const renderView: (view: Exclude<ViewId, 'text'>, draft: Draft) => Rendered;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { decodeUtf8, formatUuid, hexDumpLines, parseHex } from './hex.js';
|
|
2
|
+
import { checkJson, prettyJson } from './json.js';
|
|
3
|
+
/* The views that make sense for this value, the first being the one it opens in. */
|
|
4
|
+
export const viewsOf = (source, draft) => {
|
|
5
|
+
if (source.binary) {
|
|
6
|
+
const parsed = parseHex(draft.text);
|
|
7
|
+
const hex = parsed.ok ? parsed.hex : source.original.text;
|
|
8
|
+
return formatUuid(hex) === null ? ['hex', 'utf8'] : ['hex', 'utf8', 'uuid'];
|
|
9
|
+
}
|
|
10
|
+
return source.json ? ['formatted', 'text'] : ['text'];
|
|
11
|
+
};
|
|
12
|
+
/* What a draft is, in bytes for a binary value and in characters for the rest; `null` when there is nothing to measure. */
|
|
13
|
+
export const sizeOf = (source, draft) => {
|
|
14
|
+
if (draft.mode !== 'value') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
if (!source.binary) {
|
|
18
|
+
return { unit: 'characters', count: draft.text.length };
|
|
19
|
+
}
|
|
20
|
+
const parsed = parseHex(draft.text);
|
|
21
|
+
return parsed.ok ? { unit: 'bytes', count: parsed.hex.length / 2 } : null;
|
|
22
|
+
};
|
|
23
|
+
/* A read only view stops its dump here; the text view and a copy are not limited. */
|
|
24
|
+
export const DUMP_LIMIT_BYTES = 16 * 1024;
|
|
25
|
+
/* What a read only view shows for the draft. The text view is not here, since it is the editor itself. */
|
|
26
|
+
export const renderView = (view, draft) => {
|
|
27
|
+
if (draft.mode !== 'value') {
|
|
28
|
+
return { kind: 'empty' };
|
|
29
|
+
}
|
|
30
|
+
if (view === 'formatted') {
|
|
31
|
+
const pretty = prettyJson(draft.text);
|
|
32
|
+
if (pretty !== null) {
|
|
33
|
+
return { kind: 'text', text: pretty };
|
|
34
|
+
}
|
|
35
|
+
const checked = checkJson(draft.text);
|
|
36
|
+
return { kind: 'problem', problem: { kind: 'json', reason: checked.ok ? '' : checked.reason } };
|
|
37
|
+
}
|
|
38
|
+
const parsed = parseHex(draft.text);
|
|
39
|
+
if (!parsed.ok) {
|
|
40
|
+
return { kind: 'problem', problem: { kind: 'hex', problem: parsed.problem } };
|
|
41
|
+
}
|
|
42
|
+
if (view === 'utf8') {
|
|
43
|
+
return { kind: 'text', text: decodeUtf8(parsed.hex) };
|
|
44
|
+
}
|
|
45
|
+
if (view === 'uuid') {
|
|
46
|
+
return { kind: 'text', text: formatUuid(parsed.hex) ?? '' };
|
|
47
|
+
}
|
|
48
|
+
const total = parsed.hex.length / 2;
|
|
49
|
+
return {
|
|
50
|
+
kind: 'text',
|
|
51
|
+
text: hexDumpLines(parsed.hex, DUMP_LIMIT_BYTES).join('\n'),
|
|
52
|
+
truncated: total > DUMP_LIMIT_BYTES ? { shown: DUMP_LIMIT_BYTES, total } : undefined
|
|
53
|
+
};
|
|
54
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Ref } from 'react';
|
|
2
|
+
import type { Connection } from '../client/types.ts';
|
|
3
|
+
export interface DatabaseWorkbenchProps {
|
|
4
|
+
connections: readonly Connection[];
|
|
5
|
+
onConnectionsChange?(next: readonly Connection[]): void;
|
|
6
|
+
className?: string;
|
|
7
|
+
ref?: Ref<HTMLDivElement>;
|
|
8
|
+
}
|
|
9
|
+
export declare function DatabaseWorkbench({ connections, onConnectionsChange, className, ref }: DatabaseWorkbenchProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { PencilRuler, Settings2, SquareTerminal, Table, TableProperties } from 'lucide-react';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { Button, ColumnResizeHandle, Dialog, EmptyState, Icon, IconButton, PromptDialog, Segmented, Tabs, isApplePlatform, matchesShortcut, shortcut, useColumnResize } from '@adecore/ui';
|
|
7
|
+
import { formatNumber } from '@adecore/ui/format';
|
|
8
|
+
import { DatabaseProvider } from '../DatabaseProvider.js';
|
|
9
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage, useNumberNotation } from '../client-context.js';
|
|
10
|
+
import { ConnectionManager } from '../connections/ConnectionManager.js';
|
|
11
|
+
import { QueryConsole } from '../console/QueryConsole.js';
|
|
12
|
+
import { DatabaseExplorer } from '../explorer/DatabaseExplorer.js';
|
|
13
|
+
import { DesignerTab } from './DesignerTab.js';
|
|
14
|
+
import { TableTab } from './TableTab.js';
|
|
15
|
+
import { closeTab, connectionIdOf, focusTab, openConsole, openDesigner, openTable, parseState, pruneTabs, serializeState, setConsoleSql, setDesignerTable, setTableView, STORAGE_KEY } from './tabs.js';
|
|
16
|
+
import { useStableCallback } from '../use-stable-callback.js';
|
|
17
|
+
const DEFAULT_WIDTH = 280;
|
|
18
|
+
const MIN_WIDTH = 200;
|
|
19
|
+
/*
|
|
20
|
+
* The whole database view in one piece: the explorer beside closable tabs for the tables, consoles and
|
|
21
|
+
* designers a person opens. It answers the actions of the views below itself; an action it cannot place
|
|
22
|
+
* goes on to the `DatabaseProvider` above it. The open tabs are kept through the provider's `storage`.
|
|
23
|
+
*/
|
|
24
|
+
export function DatabaseWorkbench({ connections, onConnectionsChange, className, ref }) {
|
|
25
|
+
const { t } = useTranslation('database');
|
|
26
|
+
const client = useDatabaseClient();
|
|
27
|
+
const outer = useDatabaseAction();
|
|
28
|
+
const storage = useDatabaseStorage();
|
|
29
|
+
const files = useDatabaseFiles();
|
|
30
|
+
const numberNotation = useNumberNotation();
|
|
31
|
+
const column = useRef(null);
|
|
32
|
+
const [state, setState] = useState(() => parseState(storage?.get(STORAGE_KEY) ?? null));
|
|
33
|
+
const [selection, setSelection] = useState(null);
|
|
34
|
+
const [width, setWidth] = useState(DEFAULT_WIDTH);
|
|
35
|
+
const [managing, setManaging] = useState(false);
|
|
36
|
+
const [managed, setManaged] = useState(null);
|
|
37
|
+
/* Ids of the table tabs that hold changes nobody submitted, and the tab a person asked to close while it does. */
|
|
38
|
+
const [dirty, setDirty] = useState(() => new Set());
|
|
39
|
+
const [closing, setClosing] = useState(null);
|
|
40
|
+
const { startResize } = useColumnResize(column, { size: width, min: MIN_WIDTH, from: 'left', max: () => window.innerWidth / 2, onSize: setWidth });
|
|
41
|
+
const known = useMemo(() => new Set(connections.map((connection) => connection.id)), [connections]);
|
|
42
|
+
// While there are no connections they may still be loading, so nothing is dropped then.
|
|
43
|
+
const pruned = known.size > 0 ? pruneTabs(state, known) : state;
|
|
44
|
+
if (pruned !== state) {
|
|
45
|
+
setState(pruned);
|
|
46
|
+
}
|
|
47
|
+
if ([...dirty].some((id) => !state.tabs.some((tab) => tab.id === id))) {
|
|
48
|
+
setDirty(new Set([...dirty].filter((id) => state.tabs.some((tab) => tab.id === id))));
|
|
49
|
+
}
|
|
50
|
+
const tabs = state.tabs.filter((tab) => known.has(connectionIdOf(tab)));
|
|
51
|
+
const active = tabs.find((tab) => tab.id === state.activeId) ?? tabs.at(-1) ?? null;
|
|
52
|
+
const canManage = onConnectionsChange !== undefined;
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
storage?.set(STORAGE_KEY, serializeState(state));
|
|
55
|
+
}, [storage, state]);
|
|
56
|
+
const onAction = useStableCallback((action) => {
|
|
57
|
+
const connectionId = action.kind === 'open-table' || action.kind === 'edit-table' ? action.ref.connectionId : action.connectionId;
|
|
58
|
+
if (!known.has(connectionId) || (action.kind === 'manage-connection' && !canManage)) {
|
|
59
|
+
outer?.(action);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
switch (action.kind) {
|
|
63
|
+
case 'open-table':
|
|
64
|
+
setSelection(action.ref);
|
|
65
|
+
setState((current) => openTable(current, action.ref, action.view, action.where));
|
|
66
|
+
break;
|
|
67
|
+
case 'open-console':
|
|
68
|
+
setState((current) => openConsole(current, action.connectionId, action.schema, action.sql));
|
|
69
|
+
break;
|
|
70
|
+
case 'new-table':
|
|
71
|
+
setState((current) => openDesigner(current, action.connectionId, action.schema));
|
|
72
|
+
break;
|
|
73
|
+
case 'edit-table':
|
|
74
|
+
setState((current) => openDesigner(current, action.ref.connectionId, action.ref.schema, action.ref.table));
|
|
75
|
+
break;
|
|
76
|
+
case 'manage-connection':
|
|
77
|
+
setManaged(action.connectionId);
|
|
78
|
+
setManaging(true);
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
function newConsole() {
|
|
83
|
+
const context = consoleContext();
|
|
84
|
+
if (context !== null) {
|
|
85
|
+
setState((current) => openConsole(current, context.connectionId, context.schema));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
/* Where a new console runs: the connection of the tab in front, else the one picked in the explorer, else the first. */
|
|
89
|
+
function consoleContext() {
|
|
90
|
+
if (active !== null) {
|
|
91
|
+
return { connectionId: connectionIdOf(active), schema: active.kind === 'table' ? active.ref.schema : active.schema };
|
|
92
|
+
}
|
|
93
|
+
if (selection !== null && known.has(selection.connectionId)) {
|
|
94
|
+
return { connectionId: selection.connectionId, schema: selection.schema };
|
|
95
|
+
}
|
|
96
|
+
const first = connections[0];
|
|
97
|
+
return first === undefined ? null : { connectionId: first.id };
|
|
98
|
+
}
|
|
99
|
+
function onKeyDown(e) {
|
|
100
|
+
// A dialog opened from here is a portal: its events reach this handler through React, not through the DOM.
|
|
101
|
+
if (active !== null &&
|
|
102
|
+
e.currentTarget.contains(e.target) &&
|
|
103
|
+
!e.nativeEvent.isComposing &&
|
|
104
|
+
matchesShortcut(shortcut('Mod+W'), e.nativeEvent, isApplePlatform())) {
|
|
105
|
+
e.preventDefault();
|
|
106
|
+
requestClose(active.id);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function closeById(id) {
|
|
110
|
+
setState((current) => closeTab(current, id));
|
|
111
|
+
}
|
|
112
|
+
/* A table tab with changes that are not submitted asks first. */
|
|
113
|
+
function requestClose(id) {
|
|
114
|
+
if (dirty.has(id)) {
|
|
115
|
+
setClosing(id);
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
closeById(id);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function markDirty(id, isDirty) {
|
|
122
|
+
setDirty((current) => {
|
|
123
|
+
if (current.has(id) === isDirty) {
|
|
124
|
+
return current;
|
|
125
|
+
}
|
|
126
|
+
const next = new Set(current);
|
|
127
|
+
if (isDirty) {
|
|
128
|
+
next.add(id);
|
|
129
|
+
}
|
|
130
|
+
else {
|
|
131
|
+
next.delete(id);
|
|
132
|
+
}
|
|
133
|
+
return next;
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const titleOf = (tab) => {
|
|
137
|
+
switch (tab.kind) {
|
|
138
|
+
case 'table':
|
|
139
|
+
return tab.where === undefined ? tab.ref.table : t('workbench.filtered', { table: tab.ref.table });
|
|
140
|
+
case 'console':
|
|
141
|
+
return t('workbench.console', { number: formatNumber(tab.number) });
|
|
142
|
+
case 'designer':
|
|
143
|
+
return tab.table === undefined ? t('workbench.newTable') : t('workbench.designer', { table: tab.table });
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
const viewOf = (tab) => {
|
|
147
|
+
const connection = connections.find((entry) => entry.id === connectionIdOf(tab));
|
|
148
|
+
if (connection === undefined) {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
switch (tab.kind) {
|
|
152
|
+
case 'table':
|
|
153
|
+
return _jsx(TableTab, { connection: connection, tab: tab, onDirtyChange: (isDirty) => markDirty(tab.id, isDirty) });
|
|
154
|
+
case 'console':
|
|
155
|
+
return (_jsx(QueryConsole, { connection: connection, schema: tab.schema, value: tab.sql, onValueChange: (sql) => setState((current) => setConsoleSql(current, tab.id, sql)), autoFocus: tab.id === active?.id, className: "h-full" }));
|
|
156
|
+
case 'designer':
|
|
157
|
+
return (_jsx(DesignerTab, { connection: connection, tab: tab, onTableChange: (table) => setState((current) => setDesignerTable(current, tab.id, table)) }));
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
const canOpenConsole = consoleContext() !== null;
|
|
161
|
+
const closingTab = state.tabs.find((tab) => tab.id === closing);
|
|
162
|
+
const closingTable = closingTab?.kind === 'table' ? closingTab.ref.table : '';
|
|
163
|
+
return (_jsx(DatabaseProvider, { client: client, onAction: onAction, storage: storage, files: files, numberNotation: numberNotation, children: _jsxs("div", { ref: ref, className: clsx('flex h-full min-h-0 min-w-0 bg-surface text-text', className), onKeyDown: onKeyDown, children: [_jsxs("aside", { ref: column, className: "relative flex shrink-0 flex-col border-r border-border", style: { width }, children: [_jsxs("header", { className: "flex h-9 shrink-0 items-center justify-between border-b border-border pr-2 pl-3", children: [_jsx("h2", { className: "text-sm font-semibold text-text", children: t('workbench.title') }), canManage && (_jsx(IconButton, { icon: Settings2, size: "sm", label: t('workbench.manage'), onClick: () => {
|
|
164
|
+
setManaged(null);
|
|
165
|
+
setManaging(true);
|
|
166
|
+
} }))] }), _jsx(DatabaseExplorer, { connections: connections, value: selection, onValueChange: setSelection, className: "min-h-0 flex-1" }), _jsx(ColumnResizeHandle, { from: "left", onPointerDown: startResize })] }), _jsxs(Tabs.Root, { value: active?.id ?? null, onValueChange: (id) => setState((current) => focusTab(current, String(id))), className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx(Tabs.List, { "aria-label": t('workbench.tabs'), className: "px-2", end: _jsxs(_Fragment, { children: [active?.kind === 'table' && (_jsx(Segmented, { label: t('workbench.views'), value: active.view, onValueChange: (view) => setState((current) => setTableView(current, active.id, view)), options: [
|
|
167
|
+
{ id: 'data', label: t('workbench.data'), icon: Table },
|
|
168
|
+
{ id: 'structure', label: t('workbench.structure'), icon: TableProperties }
|
|
169
|
+
] })), _jsx(IconButton, { icon: SquareTerminal, size: "sm", label: t('workbench.newConsole'), tooltip: canOpenConsole ? undefined : t('workbench.noConnection'), disabled: !canOpenConsole, onClick: newConsole })] }), children: tabs.map((tab) => (_jsxs(Tabs.Tab, { value: tab.id, onClose: () => requestClose(tab.id), children: [_jsx(Icon, { icon: tab.kind === 'table' ? Table : tab.kind === 'console' ? SquareTerminal : PencilRuler, size: 14 }), titleOf(tab)] }, tab.id))) }), tabs.length === 0 ? (_jsx(EmptyState, { icon: SquareTerminal, title: t('workbench.empty.title'), className: "flex-1", action: _jsx(Button, { variant: "secondary", disabled: !canOpenConsole, onClick: newConsole, children: t('workbench.newConsole') }), children: t('workbench.empty.body') })) : (tabs.map((tab) => (_jsx(Tabs.Panel, { value: tab.id, keepMounted: true, className: "min-h-0 flex-1", children: viewOf(tab) }, tab.id))))] }), _jsx(PromptDialog, { open: closing !== null, danger: true, title: t('workbench.discard.title', { table: closingTable }), description: t('workbench.discard.description'), confirmLabel: t('workbench.discard.confirm'), onConfirm: () => {
|
|
170
|
+
if (closing !== null) {
|
|
171
|
+
closeById(closing);
|
|
172
|
+
}
|
|
173
|
+
setClosing(null);
|
|
174
|
+
}, onOpenChange: (open) => {
|
|
175
|
+
if (!open) {
|
|
176
|
+
setClosing(null);
|
|
177
|
+
}
|
|
178
|
+
} }), onConnectionsChange !== undefined && (_jsx(Dialog.Root, { open: managing, onOpenChange: setManaging, children: _jsxs(Dialog.Popup, { className: "flex h-[560px] w-[900px] flex-col overflow-hidden", children: [_jsx("div", { className: "border-b border-border px-4 py-3", children: _jsx(Dialog.Title, { children: t('workbench.connections') }) }), _jsx(ConnectionManager, { value: connections, onValueChange: onConnectionsChange, selected: managed, onSelectedChange: setManaged, className: "min-h-0 flex-1" }), _jsx(Dialog.Footer, { children: _jsx(Dialog.Close, { render: _jsx(Button, { variant: "secondary" }), children: t('workbench.close') }) })] }) }))] }) }));
|
|
179
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Connection } from '../client/types.ts';
|
|
2
|
+
import type { WorkbenchTab } from './tabs.ts';
|
|
3
|
+
export interface DesignerTabProps {
|
|
4
|
+
connection: Connection;
|
|
5
|
+
tab: Extract<WorkbenchTab, {
|
|
6
|
+
kind: 'designer';
|
|
7
|
+
}>;
|
|
8
|
+
onTableChange(table: string): void;
|
|
9
|
+
}
|
|
10
|
+
export declare function DesignerTab({ connection, tab, onTableChange }: DesignerTabProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { DatabaseProvider } from '../DatabaseProvider.js';
|
|
3
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseStorage, useNumberNotation } from '../client-context.js';
|
|
4
|
+
import { TableDesigner } from '../designer/TableDesigner.js';
|
|
5
|
+
import { useStableCallback } from '../use-stable-callback.js';
|
|
6
|
+
/* 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. */
|
|
7
|
+
export function DesignerTab({ connection, tab, onTableChange }) {
|
|
8
|
+
const client = useDatabaseClient();
|
|
9
|
+
const storage = useDatabaseStorage();
|
|
10
|
+
const files = useDatabaseFiles();
|
|
11
|
+
const outer = useDatabaseAction();
|
|
12
|
+
const numberNotation = useNumberNotation();
|
|
13
|
+
const onAction = useStableCallback((action) => {
|
|
14
|
+
if (action.kind === 'edit-table' && action.ref.connectionId === tab.connectionId && action.ref.schema === tab.schema) {
|
|
15
|
+
onTableChange(action.ref.table);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
outer?.(action);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
return (_jsx(DatabaseProvider, { client: client, storage: storage, files: files, numberNotation: numberNotation, onAction: onAction, children: _jsx(TableDesigner, { connection: connection, schema: tab.schema, table: tab.table, className: "h-full" }) }));
|
|
22
|
+
}
|