@adecore/database 0.17.0 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/DatabaseProvider.d.ts +4 -3
- package/dist/DatabaseProvider.js +15 -3
- package/dist/KeyIcon.d.ts +4 -0
- package/dist/KeyIcon.js +14 -0
- package/dist/actions.d.ts +12 -0
- package/dist/client-context.d.ts +3 -1
- package/dist/client-context.js +2 -0
- package/dist/code-text.d.ts +1 -0
- package/dist/code-text.js +2 -0
- package/dist/column-keys.d.ts +5 -0
- package/dist/column-keys.js +2 -0
- package/dist/connections/ConnectionManager.d.ts +3 -2
- package/dist/connections/ConnectionManager.js +27 -16
- package/dist/connections/connection-config.js +2 -0
- package/dist/console/QueryConsole.d.ts +8 -3
- package/dist/console/QueryConsole.js +110 -50
- package/dist/console/SqlEditor.d.ts +2 -12
- package/dist/console/SqlEditor.js +29 -18
- package/dist/console/StatementResultView.d.ts +5 -1
- package/dist/console/StatementResultView.js +18 -5
- package/dist/console/TransactionControls.js +3 -6
- package/dist/console/editor-slot.d.ts +18 -0
- package/dist/console/editor-slot.js +1 -0
- package/dist/console/result-keys.d.ts +15 -0
- package/dist/console/result-keys.js +79 -0
- package/dist/designer/ColumnsEditor.js +2 -2
- package/dist/designer/DesignerView.d.ts +3 -2
- package/dist/designer/DesignerView.js +5 -4
- package/dist/designer/SqlPreview.js +2 -1
- package/dist/designer/TableDesigner.d.ts +3 -2
- package/dist/designer/TableDesigner.js +2 -2
- package/dist/explorer/DatabaseExplorer.d.ts +4 -1
- package/dist/explorer/DatabaseExplorer.js +79 -23
- package/dist/explorer/RowMenu.js +2 -2
- package/dist/explorer/expansion.js +2 -2
- package/dist/explorer/folders.d.ts +16 -0
- package/dist/explorer/folders.js +1 -0
- package/dist/explorer/tree.d.ts +55 -4
- package/dist/explorer/tree.js +137 -23
- package/dist/grid/CellEditor.js +3 -1
- package/dist/grid/DataGrid.d.ts +2 -2
- package/dist/grid/DataGrid.js +61 -59
- package/dist/grid/EnumPicker.js +2 -1
- package/dist/grid/GridHeaderCell.d.ts +1 -2
- package/dist/grid/GridHeaderCell.js +12 -5
- package/dist/grid/GridRowView.d.ts +49 -0
- package/dist/grid/GridRowView.js +51 -0
- package/dist/grid/RecordView.d.ts +4 -2
- package/dist/grid/RecordView.js +71 -22
- package/dist/grid/ValueDock.d.ts +3 -1
- package/dist/grid/ValueDock.js +9 -3
- package/dist/grid/cell-id.d.ts +1 -0
- package/dist/grid/cell-id.js +2 -0
- package/dist/grid/layout.d.ts +3 -2
- package/dist/grid/layout.js +15 -6
- package/dist/grid/types.d.ts +8 -1
- package/dist/grid/types.js +4 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +2 -0
- package/dist/locales/en.json +32 -15
- package/dist/locales/nl.json +32 -15
- package/dist/protocol/index.d.ts +1 -1
- package/dist/protocol/messages.d.ts +1 -1
- package/dist/protocol/messages.js +1 -1
- package/dist/protocol/schema.d.ts +17 -0
- package/dist/structure/StructureView.d.ts +5 -3
- package/dist/structure/StructureView.js +8 -6
- package/dist/table/CommandField.js +2 -2
- package/dist/table/TableStatusBar.js +2 -2
- package/dist/table/TableToolbar.d.ts +3 -4
- package/dist/table/TableToolbar.js +4 -3
- package/dist/table/TableView.d.ts +4 -2
- package/dist/table/TableView.js +63 -91
- package/dist/table/grid-rows.js +4 -1
- package/dist/table/useTableTransfer.d.ts +3 -1
- package/dist/table/useTableTransfer.js +19 -5
- package/dist/testing/fake.d.ts +3 -1
- package/dist/testing/fake.js +7 -1
- package/dist/value/ValueEditor.d.ts +9 -0
- package/dist/value/ValueEditor.js +102 -0
- package/dist/value/ValuePanel.d.ts +1 -1
- package/dist/value/ValuePanel.js +3 -94
- package/dist/workbench/DatabaseWorkbench.js +4 -7
- package/package.json +6 -6
- package/src/DatabaseProvider.tsx +24 -5
- package/src/KeyIcon.tsx +15 -0
- package/src/actions.ts +28 -2
- package/src/client-context.ts +5 -1
- package/src/code-text.ts +2 -0
- package/src/column-keys.ts +8 -0
- package/src/connections/ConnectionManager.tsx +98 -68
- package/src/connections/connection-config.ts +2 -0
- package/src/console/QueryConsole.tsx +246 -150
- package/src/console/SqlEditor.tsx +33 -37
- package/src/console/StatementResultView.tsx +61 -27
- package/src/console/TransactionControls.tsx +8 -11
- package/src/console/editor-slot.ts +27 -0
- package/src/console/result-keys.ts +109 -0
- package/src/designer/ColumnsEditor.tsx +3 -3
- package/src/designer/DesignerView.tsx +14 -3
- package/src/designer/SqlPreview.tsx +2 -1
- package/src/designer/TableDesigner.tsx +5 -2
- package/src/explorer/DatabaseExplorer.tsx +170 -26
- package/src/explorer/RowMenu.tsx +2 -2
- package/src/explorer/expansion.ts +4 -4
- package/src/explorer/folders.ts +25 -0
- package/src/explorer/tree.ts +219 -28
- package/src/grid/CellEditor.tsx +3 -2
- package/src/grid/DataGrid.tsx +114 -176
- package/src/grid/EnumPicker.tsx +2 -1
- package/src/grid/GridHeaderCell.tsx +14 -9
- package/src/grid/GridRowView.tsx +184 -0
- package/src/grid/RecordView.tsx +188 -75
- package/src/grid/ValueDock.tsx +12 -3
- package/src/grid/cell-id.ts +2 -0
- package/src/grid/layout.ts +16 -6
- package/src/grid/types.ts +15 -3
- package/src/index.ts +5 -1
- package/src/locales/en.json +32 -15
- package/src/locales/nl.json +32 -15
- package/src/protocol/index.ts +13 -1
- package/src/protocol/messages.ts +1 -1
- package/src/protocol/schema.ts +28 -0
- package/src/structure/StructureView.tsx +16 -6
- package/src/table/CommandField.tsx +2 -2
- package/src/table/TableStatusBar.tsx +3 -3
- package/src/table/TableToolbar.tsx +107 -92
- package/src/table/TableView.tsx +95 -132
- package/src/table/grid-rows.ts +5 -3
- package/src/table/useTableTransfer.ts +21 -6
- package/src/testing/fake.ts +18 -1
- package/src/value/ValueEditor.tsx +213 -0
- package/src/value/ValuePanel.tsx +4 -206
- package/src/workbench/DatabaseWorkbench.tsx +9 -10
|
@@ -1,30 +1,29 @@
|
|
|
1
|
-
import { useEffect, useRef, type KeyboardEvent
|
|
1
|
+
import { useEffect, useImperativeHandle, useRef, type KeyboardEvent } from 'react';
|
|
2
|
+
import clsx from 'clsx';
|
|
2
3
|
import { isApplePlatform, matchesShortcut, TextArea } from '@adecore/ui';
|
|
4
|
+
import { CODE_TEXT } from '../code-text.ts';
|
|
3
5
|
import { applyEdit, indentEdit, newlineEdit, outdentEdit, type TextEdit } from './editing.ts';
|
|
6
|
+
import type { QueryConsoleEditorProps } from './editor-slot.ts';
|
|
4
7
|
import { RUN_ALL_SHORTCUT, RUN_SHORTCUT } from './shortcuts.ts';
|
|
5
8
|
|
|
6
|
-
/* What the person asked for: the selection or the statement under the caret, or the whole script. */
|
|
7
|
-
export type RunScope = 'selection-or-statement' | 'all';
|
|
8
|
-
|
|
9
|
-
export interface SqlEditorProps {
|
|
10
|
-
value: string;
|
|
11
|
-
onValueChange(value: string): void;
|
|
12
|
-
/* Mod+Enter and Mod+Shift+Enter. */
|
|
13
|
-
onRun(scope: RunScope): void;
|
|
14
|
-
label: string;
|
|
15
|
-
placeholder?: string;
|
|
16
|
-
className?: string;
|
|
17
|
-
/* The text area, which holds the selection and the caret the console reads when it runs. */
|
|
18
|
-
ref?: Ref<HTMLTextAreaElement>;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
9
|
/*
|
|
22
|
-
* Where the SQL is typed
|
|
23
|
-
*
|
|
10
|
+
* Where the SQL is typed when the app brings no editor of its own: a plain text area that fills the
|
|
11
|
+
* height it gets, with the same props an editor of the app receives.
|
|
24
12
|
*/
|
|
25
|
-
export function SqlEditor({ value, onValueChange,
|
|
13
|
+
export function SqlEditor({ ref, value, onValueChange, run, label, placeholder, autoFocus }: QueryConsoleEditorProps) {
|
|
14
|
+
const area = useRef<HTMLTextAreaElement>(null);
|
|
26
15
|
const pendingSelection = useRef<{ area: HTMLTextAreaElement; start: number; end: number } | null>(null);
|
|
27
16
|
|
|
17
|
+
useImperativeHandle(ref, () => ({ selection: () => ({ start: area.current?.selectionStart ?? 0, end: area.current?.selectionEnd ?? 0 }) }), []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (autoFocus) {
|
|
21
|
+
area.current?.focus();
|
|
22
|
+
}
|
|
23
|
+
// Only on mount: a console that later becomes active keeps the focus where the person put it.
|
|
24
|
+
// eslint-disable-next-line react/exhaustive-deps
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
28
27
|
useEffect(() => {
|
|
29
28
|
const pending = pendingSelection.current;
|
|
30
29
|
if (pending !== null) {
|
|
@@ -34,15 +33,15 @@ export function SqlEditor({ value, onValueChange, onRun, label, placeholder, cla
|
|
|
34
33
|
}, [value]);
|
|
35
34
|
|
|
36
35
|
// Through the browser's own insert, so Cmd+Z undoes the edit; the fallback sets the value itself.
|
|
37
|
-
const apply = (
|
|
38
|
-
const before =
|
|
39
|
-
|
|
36
|
+
const apply = (target: HTMLTextAreaElement, edit: TextEdit): void => {
|
|
37
|
+
const before = target.value;
|
|
38
|
+
target.setSelectionRange(edit.from, edit.to);
|
|
40
39
|
const inserted = document.execCommand('insertText', false, edit.insert);
|
|
41
|
-
if (inserted &&
|
|
42
|
-
|
|
40
|
+
if (inserted && target.value === applyEdit(before, edit)) {
|
|
41
|
+
target.setSelectionRange(edit.selectionStart, edit.selectionEnd);
|
|
43
42
|
return;
|
|
44
43
|
}
|
|
45
|
-
pendingSelection.current = { area, start: edit.selectionStart, end: edit.selectionEnd };
|
|
44
|
+
pendingSelection.current = { area: target, start: edit.selectionStart, end: edit.selectionEnd };
|
|
46
45
|
onValueChange(applyEdit(before, edit));
|
|
47
46
|
};
|
|
48
47
|
|
|
@@ -50,44 +49,41 @@ export function SqlEditor({ value, onValueChange, onRun, label, placeholder, cla
|
|
|
50
49
|
const apple = isApplePlatform();
|
|
51
50
|
if (matchesShortcut(RUN_ALL_SHORTCUT, event, apple)) {
|
|
52
51
|
event.preventDefault();
|
|
53
|
-
|
|
52
|
+
run('all');
|
|
54
53
|
return;
|
|
55
54
|
}
|
|
56
55
|
if (matchesShortcut(RUN_SHORTCUT, event, apple)) {
|
|
57
56
|
event.preventDefault();
|
|
58
|
-
|
|
57
|
+
run('selection-or-statement');
|
|
59
58
|
return;
|
|
60
59
|
}
|
|
61
60
|
if (event.nativeEvent.isComposing || event.metaKey || event.ctrlKey || event.altKey) {
|
|
62
61
|
return;
|
|
63
62
|
}
|
|
64
|
-
const
|
|
65
|
-
const { selectionStart, selectionEnd } =
|
|
63
|
+
const target = event.currentTarget;
|
|
64
|
+
const { selectionStart, selectionEnd } = target;
|
|
66
65
|
if (event.key === 'Tab') {
|
|
67
66
|
event.preventDefault();
|
|
68
|
-
const edit = event.shiftKey ? outdentEdit(
|
|
67
|
+
const edit = event.shiftKey ? outdentEdit(target.value, selectionStart, selectionEnd) : indentEdit(target.value, selectionStart, selectionEnd);
|
|
69
68
|
if (edit !== null) {
|
|
70
|
-
apply(
|
|
69
|
+
apply(target, edit);
|
|
71
70
|
}
|
|
72
71
|
} else if (event.key === 'Enter' && !event.shiftKey) {
|
|
73
72
|
event.preventDefault();
|
|
74
|
-
apply(
|
|
73
|
+
apply(target, newlineEdit(target.value, selectionStart, selectionEnd));
|
|
75
74
|
}
|
|
76
75
|
};
|
|
77
76
|
|
|
78
77
|
return (
|
|
79
78
|
<TextArea
|
|
80
|
-
ref={
|
|
81
|
-
mono
|
|
82
|
-
resize="vertical"
|
|
79
|
+
ref={area}
|
|
83
80
|
aria-label={label}
|
|
84
81
|
placeholder={placeholder}
|
|
85
82
|
spellCheck={false}
|
|
86
83
|
autoCapitalize="off"
|
|
87
84
|
autoCorrect="off"
|
|
88
|
-
rows={8}
|
|
89
85
|
value={value}
|
|
90
|
-
className={
|
|
86
|
+
className={clsx(CODE_TEXT, 'flex-1')}
|
|
91
87
|
onChange={(event) => onValueChange(event.target.value)}
|
|
92
88
|
onKeyDown={handleKeyDown}
|
|
93
89
|
/>
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
2
|
-
import { ChevronLeft, ChevronRight, CircleAlert, Download, PanelRight } from 'lucide-react';
|
|
1
|
+
import { useMemo, useState, type ReactNode } from 'react';
|
|
2
|
+
import { ArrowUpRight, ChevronLeft, ChevronRight, CircleAlert, Download, PanelRight } from 'lucide-react';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Banner, IconButton, Menu, Spinner } from '@adecore/ui';
|
|
4
|
+
import { Banner, ButtonGroup, ContextMenu, Icon, IconButton, Menu, Spinner } from '@adecore/ui';
|
|
5
5
|
import { formatDecimal, formatNumber } from '@adecore/ui/format';
|
|
6
6
|
import { DataGrid } from '../grid/DataGrid.tsx';
|
|
7
7
|
import { previewValueOf } from '../grid/focused-value.ts';
|
|
8
|
-
import type { FocusedCell, GridRow } from '../grid/types.ts';
|
|
8
|
+
import type { FocusedCell, GridMenuContext, GridRow } from '../grid/types.ts';
|
|
9
9
|
import { ValueDock } from '../grid/ValueDock.tsx';
|
|
10
10
|
import type { Engine, FileFormat, StatementResult } from '../protocol/index.ts';
|
|
11
11
|
import type { SqlTarget } from '../sql.ts';
|
|
12
|
+
import type { Reference } from '../table/references.ts';
|
|
13
|
+
import type { ResultKeys } from './result-keys.ts';
|
|
12
14
|
import { ValuePanel } from '../value/ValuePanel.tsx';
|
|
13
15
|
|
|
14
16
|
/* What "Copy as SQL INSERT" names the table of a result that comes from no table. */
|
|
@@ -39,6 +41,10 @@ export interface StatementResultViewProps {
|
|
|
39
41
|
valuePanelOpen?: boolean;
|
|
40
42
|
/* Offers the toggle of the value panel in the footer of a result set. */
|
|
41
43
|
onValuePanelOpenChange?(open: boolean): void;
|
|
44
|
+
/* What the tables of the columns say about them, which marks the keys in the headers. */
|
|
45
|
+
keys?: ResultKeys | null;
|
|
46
|
+
/* Offers Go to referenced row on a cell of a foreign key, as the table view does. */
|
|
47
|
+
onFollowReference?(reference: Reference): void;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
50
|
/* One statement's outcome: a result set in a read-only grid, a count of affected rows, or the error the server gave. */
|
|
@@ -50,7 +56,9 @@ export function StatementResultView({
|
|
|
50
56
|
exporting = false,
|
|
51
57
|
engine,
|
|
52
58
|
valuePanelOpen = false,
|
|
53
|
-
onValuePanelOpenChange
|
|
59
|
+
onValuePanelOpenChange,
|
|
60
|
+
keys = null,
|
|
61
|
+
onFollowReference
|
|
54
62
|
}: StatementResultViewProps) {
|
|
55
63
|
const { t } = useTranslation('database');
|
|
56
64
|
const [focus, setFocus] = useState<FocusedCell | null>(null);
|
|
@@ -92,6 +100,27 @@ export function StatementResultView({
|
|
|
92
100
|
);
|
|
93
101
|
}
|
|
94
102
|
|
|
103
|
+
const referenceAt = (cell: FocusedCell): Reference | null => {
|
|
104
|
+
const row = rows.find((candidate) => candidate.key === cell.rowKey);
|
|
105
|
+
return keys === null || row === undefined ? null : keys.referenceAt(cell.column, row.cells);
|
|
106
|
+
};
|
|
107
|
+
const follow =
|
|
108
|
+
keys === null || onFollowReference === undefined
|
|
109
|
+
? undefined
|
|
110
|
+
: (cell: FocusedCell): void => {
|
|
111
|
+
const reference = referenceAt(cell);
|
|
112
|
+
if (reference !== null) {
|
|
113
|
+
onFollowReference(reference);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
const referenceMenu = (context: GridMenuContext): ReactNode =>
|
|
117
|
+
follow === undefined || context.cell === null || keys?.isReference(context.cell.column) !== true ? null : (
|
|
118
|
+
<ContextMenu.Item disabled={referenceAt(context.cell) === null} onClick={() => context.cell !== null && follow(context.cell)}>
|
|
119
|
+
<Icon icon={ArrowUpRight} size={14} />
|
|
120
|
+
{t('table.goToReferenced')}
|
|
121
|
+
</ContextMenu.Item>
|
|
122
|
+
);
|
|
123
|
+
|
|
95
124
|
const focusedRow = valuePanelOpen && focus !== null ? rows.find((row) => row.key === focus.rowKey) : undefined;
|
|
96
125
|
const focusedCell = focus === null ? undefined : focusedRow?.cells[focus.column];
|
|
97
126
|
|
|
@@ -112,11 +141,14 @@ export function StatementResultView({
|
|
|
112
141
|
>
|
|
113
142
|
<DataGrid
|
|
114
143
|
label={t('console.result')}
|
|
115
|
-
columns={result.columns}
|
|
144
|
+
columns={keys?.columns ?? result.columns}
|
|
116
145
|
rows={rows}
|
|
117
146
|
empty={t('console.noRows')}
|
|
118
147
|
sqlTarget={sqlTarget}
|
|
119
148
|
onFocusedCellChange={setFocus}
|
|
149
|
+
onFollow={follow}
|
|
150
|
+
canFollow={(cell) => referenceAt(cell) !== null}
|
|
151
|
+
menu={referenceMenu}
|
|
120
152
|
/>
|
|
121
153
|
</ValueDock>
|
|
122
154
|
<footer className="flex h-10 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-text-muted">
|
|
@@ -148,27 +180,29 @@ export function StatementResultView({
|
|
|
148
180
|
/>
|
|
149
181
|
</>
|
|
150
182
|
)}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
<ButtonGroup>
|
|
184
|
+
{onExport !== undefined && (
|
|
185
|
+
<Menu.Root>
|
|
186
|
+
<IconButton icon={Download} size="sm" label={t('console.export')} disabled={exporting} render={<Menu.Trigger />} />
|
|
187
|
+
<Menu.Popup>
|
|
188
|
+
{EXPORT_FORMATS.map((format) => (
|
|
189
|
+
<Menu.Item key={format} onClick={() => onExport(format)}>
|
|
190
|
+
{t('console.exportAs', { format: format.toUpperCase() })}
|
|
191
|
+
</Menu.Item>
|
|
192
|
+
))}
|
|
193
|
+
</Menu.Popup>
|
|
194
|
+
</Menu.Root>
|
|
195
|
+
)}
|
|
196
|
+
{onValuePanelOpenChange !== undefined && (
|
|
197
|
+
<IconButton
|
|
198
|
+
icon={PanelRight}
|
|
199
|
+
size="sm"
|
|
200
|
+
label={t('console.valuePanel')}
|
|
201
|
+
aria-pressed={valuePanelOpen}
|
|
202
|
+
onClick={() => onValuePanelOpenChange(!valuePanelOpen)}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
</ButtonGroup>
|
|
172
206
|
</span>
|
|
173
207
|
</footer>
|
|
174
208
|
</>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import { Button, Pill,
|
|
4
|
+
import { Button, Pill, Switch, Tooltip } from '@adecore/ui';
|
|
5
5
|
|
|
6
6
|
export type TransactionMode = 'auto' | 'manual';
|
|
7
7
|
|
|
@@ -18,23 +18,20 @@ export interface TransactionControlsProps {
|
|
|
18
18
|
ref?: Ref<HTMLDivElement>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/* Auto or
|
|
21
|
+
/* Auto-commit on or off, and while a transaction is open, the way to end it. */
|
|
22
22
|
export function TransactionControls({ mode, onModeChange, open, busy, onCommit, onRollback, className, ref }: TransactionControlsProps) {
|
|
23
23
|
const { t } = useTranslation('database');
|
|
24
24
|
return (
|
|
25
25
|
<div ref={ref} className={clsx('flex items-center gap-2', className)}>
|
|
26
26
|
<Tooltip label={t('console.transaction.hint')}>
|
|
27
|
-
<div className="flex">
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
<div className="flex items-center gap-2 text-xs text-text">
|
|
28
|
+
<Switch
|
|
29
|
+
checked={mode === 'auto'}
|
|
30
|
+
label={t('console.transaction.autoCommit')}
|
|
31
31
|
disabled={busy}
|
|
32
|
-
|
|
33
|
-
options={[
|
|
34
|
-
{ id: 'auto', label: t('console.transaction.auto') },
|
|
35
|
-
{ id: 'manual', label: t('console.transaction.manual') }
|
|
36
|
-
]}
|
|
32
|
+
onCheckedChange={(auto) => onModeChange(auto ? 'auto' : 'manual')}
|
|
37
33
|
/>
|
|
34
|
+
<span>{t('console.transaction.autoCommit')}</span>
|
|
38
35
|
</div>
|
|
39
36
|
</Tooltip>
|
|
40
37
|
{open && (
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Ref } from 'react';
|
|
2
|
+
|
|
3
|
+
/* What a run takes: the selection, or the statement at the caret when nothing is selected; or the whole text. */
|
|
4
|
+
export type QueryConsoleRunScope = 'selection-or-statement' | 'all';
|
|
5
|
+
|
|
6
|
+
/* What the console asks the editor whenever it runs. */
|
|
7
|
+
export interface QueryConsoleEditorHandle {
|
|
8
|
+
/* Offsets into the text, `start` equal to `end` for a caret. */
|
|
9
|
+
selection(): { readonly start: number; readonly end: number };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/* What the console hands the editor it draws, its own or the app's. */
|
|
13
|
+
export interface QueryConsoleEditorProps {
|
|
14
|
+
/* Where the editor puts its handle. Without one, a run of the selection or the statement runs the whole text. */
|
|
15
|
+
ref: Ref<QueryConsoleEditorHandle>;
|
|
16
|
+
value: string;
|
|
17
|
+
onValueChange(sql: string): void;
|
|
18
|
+
/* What the editor's own keys call, as the toolbar does. It reads the selection through the handle, and does nothing while `busy` or when there is nothing to run. */
|
|
19
|
+
run(scope: QueryConsoleRunScope): void;
|
|
20
|
+
/* A run or the end of a transaction is under way. */
|
|
21
|
+
busy: boolean;
|
|
22
|
+
/* The accessible name of the editor and its placeholder, in the person's language. */
|
|
23
|
+
label: string;
|
|
24
|
+
placeholder: string;
|
|
25
|
+
/* The console was opened just now: the editor takes the caret once it is there. */
|
|
26
|
+
autoFocus: boolean;
|
|
27
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
2
|
+
import type { DatabaseSession } from '../client/types.ts';
|
|
3
|
+
import type { Shown } from '../grid/display.ts';
|
|
4
|
+
import type { GridColumn } from '../grid/types.ts';
|
|
5
|
+
import type { ColumnSource, Engine, ResultColumn, StatementResult, TableStructure } from '../protocol/index.ts';
|
|
6
|
+
import { foreignKeyOf, referenceOf, type Reference } from '../table/references.ts';
|
|
7
|
+
|
|
8
|
+
/* What the tables a result reads from say about its columns: their keys, and where a foreign key points. */
|
|
9
|
+
export interface ResultKeys {
|
|
10
|
+
readonly columns: readonly GridColumn[];
|
|
11
|
+
/* Whether a column is part of a foreign key of the table it comes from. */
|
|
12
|
+
isReference(column: number): boolean;
|
|
13
|
+
/* The row the foreign key of a column points to, from the cells of a row of the result; `null` when it points nowhere. */
|
|
14
|
+
referenceAt(column: number, cells: readonly Shown[]): Reference | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
type TableName = Pick<ColumnSource, 'schema' | 'table'>;
|
|
18
|
+
|
|
19
|
+
const tableKey = ({ schema, table }: TableName): string => `${schema}\u0000${table}`;
|
|
20
|
+
|
|
21
|
+
/* The tables the columns of a result come from, each once, in the order they first appear. */
|
|
22
|
+
export const sourceTablesOf = (columns: readonly ResultColumn[]): TableName[] => {
|
|
23
|
+
const tables = new Map<string, TableName>();
|
|
24
|
+
for (const { source } of columns) {
|
|
25
|
+
if (source !== undefined && !tables.has(tableKey(source))) {
|
|
26
|
+
tables.set(tableKey(source), { schema: source.schema, table: source.table });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return [...tables.values()];
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* The keys of a result's columns, from the structures of the tables they come from by key (`schema`, NUL,
|
|
34
|
+
* `table`). A column without a source, or of a table with no structure, is marked as nothing.
|
|
35
|
+
*/
|
|
36
|
+
export const resultKeysOf = (engine: Engine, columns: readonly ResultColumn[], structures: ReadonlyMap<string, TableStructure>): ResultKeys => {
|
|
37
|
+
const structureOf = (column: ResultColumn | undefined): TableStructure | undefined =>
|
|
38
|
+
column?.source === undefined ? undefined : structures.get(tableKey(column.source));
|
|
39
|
+
const foreignKeyAt = (column: number) => {
|
|
40
|
+
const source = columns[column]?.source;
|
|
41
|
+
const structure = structureOf(columns[column]);
|
|
42
|
+
return source === undefined || structure === undefined ? undefined : foreignKeyOf(structure.foreignKeys, source.column);
|
|
43
|
+
};
|
|
44
|
+
return {
|
|
45
|
+
columns: columns.map((column): GridColumn => {
|
|
46
|
+
const structure = structureOf(column);
|
|
47
|
+
const name = column.source?.column;
|
|
48
|
+
if (structure === undefined || name === undefined) {
|
|
49
|
+
return column;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
...column,
|
|
53
|
+
primaryKey: structure.primaryKey.includes(name),
|
|
54
|
+
foreignKey: structure.foreignKeys.some((key) => key.columns.includes(name))
|
|
55
|
+
};
|
|
56
|
+
}),
|
|
57
|
+
isReference: (column) => foreignKeyAt(column) !== undefined,
|
|
58
|
+
referenceAt: (column, cells) => {
|
|
59
|
+
const key = foreignKeyAt(column);
|
|
60
|
+
const source = columns[column]?.source;
|
|
61
|
+
if (key === undefined || source === undefined) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// The columns of the result as the columns of that table, so a join names each key column of its own table only.
|
|
65
|
+
const asTable = columns.map((entry) => ({
|
|
66
|
+
name: entry.source !== undefined && tableKey(entry.source) === tableKey(source) ? entry.source.column : '',
|
|
67
|
+
kind: entry.kind
|
|
68
|
+
}));
|
|
69
|
+
return referenceOf(engine, key, asTable, cells);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/*
|
|
75
|
+
* The keys of a result set, once the structures of the tables its columns come from have loaded: each table
|
|
76
|
+
* once per result, through the console's session. `null` while they load and for what is no result set.
|
|
77
|
+
*/
|
|
78
|
+
export function useResultKeys(session: DatabaseSession | null, engine: Engine | undefined, result: StatementResult | undefined): ResultKeys | null {
|
|
79
|
+
const [loaded, setLoaded] = useState<{ readonly result: StatementResult; readonly structures: ReadonlyMap<string, TableStructure> } | null>(null);
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (session === null || result?.kind !== 'rows') {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const tables = sourceTablesOf(result.columns);
|
|
86
|
+
if (tables.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const controller = new AbortController();
|
|
90
|
+
// A table the server named but cannot describe, such as a derived one, marks no column.
|
|
91
|
+
const loads = tables.map((table) =>
|
|
92
|
+
session.structure(table.schema, table.table, { signal: controller.signal }).then(
|
|
93
|
+
(structure): [string, TableStructure] => [tableKey(table), structure],
|
|
94
|
+
() => null
|
|
95
|
+
)
|
|
96
|
+
);
|
|
97
|
+
void Promise.all(loads).then((entries) => {
|
|
98
|
+
if (!controller.signal.aborted) {
|
|
99
|
+
setLoaded({ result, structures: new Map(entries.filter((entry) => entry !== null)) });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return () => controller.abort();
|
|
103
|
+
}, [session, result]);
|
|
104
|
+
|
|
105
|
+
return useMemo(
|
|
106
|
+
() => (engine === undefined || result?.kind !== 'rows' || loaded?.result !== result ? null : resultKeysOf(engine, result.columns, loaded.structures)),
|
|
107
|
+
[engine, result, loaded]
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -2,7 +2,7 @@ import { useId } from 'react';
|
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { ArrowDown, ArrowUp, KeyRound, Plus, Trash2 } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Button, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
|
|
5
|
+
import { Button, ButtonGroup, Checkbox, EmptyState, Icon, IconButton, Input, Pill } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
7
|
import { typeSuggestionsOf } from '../ddl/index.ts';
|
|
8
8
|
import type { EditorProps } from './editor-props.ts';
|
|
@@ -72,7 +72,7 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
72
72
|
return (
|
|
73
73
|
<tr key={column.key}>
|
|
74
74
|
<td className={clsx(TD, 'whitespace-nowrap')}>
|
|
75
|
-
<
|
|
75
|
+
<ButtonGroup>
|
|
76
76
|
<IconButton
|
|
77
77
|
icon={ArrowUp}
|
|
78
78
|
size="xs"
|
|
@@ -87,7 +87,7 @@ export function ColumnsEditor({ draft, dialect, disabled, onChange, className, r
|
|
|
87
87
|
disabled={disabled || at === draft.columns.length - 1}
|
|
88
88
|
onClick={() => onChange(moveColumn(draft, column.key, 1))}
|
|
89
89
|
/>
|
|
90
|
-
</
|
|
90
|
+
</ButtonGroup>
|
|
91
91
|
</td>
|
|
92
92
|
<td className={clsx(TD, 'min-w-40')}>
|
|
93
93
|
<Input
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { KeyboardEvent, Ref } from 'react';
|
|
1
|
+
import type { KeyboardEvent, ReactNode, Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { CircleAlert, Lock, Undo2 } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
|
|
5
|
+
import { Banner, Button, DisabledReason, Field, Icon, Input, PromptDialog, Separator, Tabs, isApplePlatform, matchesShortcut, shortcut } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { CODE_TEXT } from '../code-text.ts';
|
|
7
8
|
import type { Dialect, TableDraft } from '../ddl/index.ts';
|
|
8
9
|
import { ColumnsEditor } from './ColumnsEditor.tsx';
|
|
9
10
|
import { ForeignKeysEditor } from './ForeignKeysEditor.tsx';
|
|
@@ -39,6 +40,8 @@ export interface DesignerViewProps {
|
|
|
39
40
|
onCancelConfirm(): void;
|
|
40
41
|
onRevert(): void;
|
|
41
42
|
onDismissFailure(): void;
|
|
43
|
+
/* The app's own content before the name of the table, such as where it is, with a separator after it. */
|
|
44
|
+
toolbarStart?: ReactNode;
|
|
42
45
|
className?: string;
|
|
43
46
|
ref?: Ref<HTMLDivElement>;
|
|
44
47
|
}
|
|
@@ -64,6 +67,7 @@ export function DesignerView({
|
|
|
64
67
|
onCancelConfirm,
|
|
65
68
|
onRevert,
|
|
66
69
|
onDismissFailure,
|
|
70
|
+
toolbarStart,
|
|
67
71
|
className,
|
|
68
72
|
ref
|
|
69
73
|
}: DesignerViewProps) {
|
|
@@ -84,6 +88,13 @@ export function DesignerView({
|
|
|
84
88
|
return (
|
|
85
89
|
<div ref={ref} className={clsx('flex min-h-0 min-w-0 flex-col', className)} onKeyDown={handleKeyDown}>
|
|
86
90
|
<div className="flex shrink-0 items-end gap-3 border-b border-border-soft px-3 py-2">
|
|
91
|
+
{toolbarStart !== undefined && toolbarStart !== null && toolbarStart !== false && (
|
|
92
|
+
// As tall as the field's input and level with it, since the field's label stands above the row.
|
|
93
|
+
<span className="flex h-8 shrink-0 items-center gap-3">
|
|
94
|
+
{toolbarStart}
|
|
95
|
+
<Separator />
|
|
96
|
+
</span>
|
|
97
|
+
)}
|
|
87
98
|
<Field label={t('designer.tableName')} className="w-80">
|
|
88
99
|
<Input
|
|
89
100
|
mono
|
|
@@ -175,7 +186,7 @@ export function DesignerView({
|
|
|
175
186
|
}}
|
|
176
187
|
>
|
|
177
188
|
{plan.destructive && <p className="mt-3 text-xs text-status-error">{t('designer.confirm.destructive')}</p>}
|
|
178
|
-
<pre className=
|
|
189
|
+
<pre className={clsx(CODE_TEXT, 'mt-3 max-h-64 overflow-auto rounded-md bg-surface-sunken p-2 whitespace-pre text-text select-text')}>
|
|
179
190
|
{scriptOf(plan.statements)}
|
|
180
191
|
</pre>
|
|
181
192
|
</PromptDialog>
|
|
@@ -3,6 +3,7 @@ import clsx from 'clsx';
|
|
|
3
3
|
import { Copy } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Button, Icon, copyText } from '@adecore/ui';
|
|
6
|
+
import { CODE_TEXT } from '../code-text.ts';
|
|
6
7
|
import type { DraftProblem } from '../ddl/index.ts';
|
|
7
8
|
import type { Plan } from './plan.ts';
|
|
8
9
|
import { scriptOf } from './plan.ts';
|
|
@@ -48,7 +49,7 @@ export function SqlPreview({ plan, className, ref }: SqlPreviewProps) {
|
|
|
48
49
|
) : script === '' ? (
|
|
49
50
|
<p className="text-xs text-text-faint">{t('designer.noChanges')}</p>
|
|
50
51
|
) : (
|
|
51
|
-
<pre className=
|
|
52
|
+
<pre className={clsx(CODE_TEXT, 'whitespace-pre text-text')}>{script}</pre>
|
|
52
53
|
)}
|
|
53
54
|
</div>
|
|
54
55
|
</section>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useMemo, useRef, useState, type Ref } from 'react';
|
|
1
|
+
import { useMemo, useRef, useState, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
3
|
import { CircleAlert, RefreshCw } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
@@ -19,6 +19,8 @@ export interface TableDesignerProps {
|
|
|
19
19
|
schema: string;
|
|
20
20
|
/* The table to modify; without one the designer makes a new table. */
|
|
21
21
|
table?: string;
|
|
22
|
+
/* The app's own content at the start of the toolbar, before the name of the table, such as where it is. */
|
|
23
|
+
toolbarStart?: ReactNode;
|
|
22
24
|
className?: string;
|
|
23
25
|
ref?: Ref<HTMLDivElement>;
|
|
24
26
|
}
|
|
@@ -39,7 +41,7 @@ interface Editing {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
/* Creates a table or modifies one: its columns, indexes, foreign keys and options, with the SQL it will run shown before it runs. */
|
|
42
|
-
export function TableDesigner({ connection, schema, table, className, ref }: TableDesignerProps) {
|
|
44
|
+
export function TableDesigner({ connection, schema, table, toolbarStart, className, ref }: TableDesignerProps) {
|
|
43
45
|
const { t } = useTranslation('database');
|
|
44
46
|
const client = useDatabaseClient();
|
|
45
47
|
const onAction = useDatabaseAction();
|
|
@@ -179,6 +181,7 @@ export function TableDesigner({ connection, schema, table, className, ref }: Tab
|
|
|
179
181
|
setFailure(null);
|
|
180
182
|
}}
|
|
181
183
|
onDismissFailure={() => setFailure(null)}
|
|
184
|
+
toolbarStart={toolbarStart}
|
|
182
185
|
/>
|
|
183
186
|
)}
|
|
184
187
|
</div>
|