@adecore/database 0.17.0-beta.2 → 0.18.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/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 +251 -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,15 +1,16 @@
|
|
|
1
|
-
import { useEffect, useId, useMemo, useRef, useState, type Ref } from 'react';
|
|
1
|
+
import { useEffect, useId, useMemo, useRef, useState, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import { Check, CircleAlert, History, Play, Square } from 'lucide-react';
|
|
4
|
-
import type { TFunction } from 'i18next';
|
|
3
|
+
import { Check, CircleAlert, History, Play, Square, X } from 'lucide-react';
|
|
5
4
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Banner, Button, Icon, messageOf, PromptDialog, Spinner, Tabs, Tooltip } from '@adecore/ui';
|
|
5
|
+
import { Banner, Button, ColumnResizeHandle, Icon, IconButton, messageOf, PromptDialog, Spinner, Tabs, Tooltip, useColumnResize } from '@adecore/ui';
|
|
7
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
7
|
import { DatabaseRequestError, type Connection } from '../client/types.ts';
|
|
9
|
-
import {
|
|
8
|
+
import type { DatabaseNotice } from '../actions.ts';
|
|
9
|
+
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.ts';
|
|
10
10
|
import type { FileFormat, RowsResult, SchemaInfo, StatementResult } from '../protocol/index.ts';
|
|
11
11
|
import { splitStatements, statementAt } from '../sql-split.ts';
|
|
12
12
|
import { DestructiveDialog } from './DestructiveDialog.tsx';
|
|
13
|
+
import type { QueryConsoleEditorHandle, QueryConsoleEditorProps, QueryConsoleRunScope } from './editor-slot.ts';
|
|
13
14
|
import { findDestructive, type DestructiveStatement } from './destructive.ts';
|
|
14
15
|
import type { HistoryEntry } from './history.ts';
|
|
15
16
|
import { HistoryPanel } from './HistoryPanel.tsx';
|
|
@@ -17,16 +18,20 @@ import { statementLabel } from './labels.ts';
|
|
|
17
18
|
import { isPageable, outcomeOf } from './outcome.ts';
|
|
18
19
|
import { SchemaPicker } from './SchemaPicker.tsx';
|
|
19
20
|
import { orderSchemas } from './schemas.ts';
|
|
21
|
+
import { useResultKeys } from './result-keys.ts';
|
|
20
22
|
import { RUN_ALL_SHORTCUT, RUN_SHORTCUT } from './shortcuts.ts';
|
|
21
|
-
import { SqlEditor
|
|
23
|
+
import { SqlEditor } from './SqlEditor.tsx';
|
|
22
24
|
import { StatementResultView, type ResultPager } from './StatementResultView.tsx';
|
|
23
25
|
import { TransactionControls, type TransactionMode } from './TransactionControls.tsx';
|
|
24
26
|
import { useConsoleHistory } from './useConsoleHistory.ts';
|
|
25
27
|
|
|
26
28
|
export interface QueryConsoleProps {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
/* Without one the console draws its editor and a bar with only `toolbarEnd`, where an app can offer to pick one; nothing runs. */
|
|
30
|
+
connection?: Connection;
|
|
31
|
+
/* The schema the statements run in. Without `onSchemaChange` the picker starts from it, and a new one resets the pick. */
|
|
29
32
|
schema?: string;
|
|
33
|
+
/* Makes the schema the app's: the picker shows `schema` and asks for another through this. */
|
|
34
|
+
onSchemaChange?(schema: string): void;
|
|
30
35
|
/* The SQL, when the app keeps it (a tab that survives a reload). Without it the console keeps the text itself, starting from `defaultValue`. */
|
|
31
36
|
value?: string;
|
|
32
37
|
defaultValue?: string;
|
|
@@ -35,6 +40,10 @@ export interface QueryConsoleProps {
|
|
|
35
40
|
defaultHistoryOpen?: boolean;
|
|
36
41
|
/* Puts the caret in the editor when the console mounts, for one a person just opened. */
|
|
37
42
|
autoFocus?: boolean;
|
|
43
|
+
/* Draws the app's own editor in place of the console's text area. It owns its keys and runs through `run`. */
|
|
44
|
+
renderEditor?(editor: QueryConsoleEditorProps): ReactNode;
|
|
45
|
+
/* The app's own content in the group at the end of the bar, before the schema picker and History, such as where the console runs. */
|
|
46
|
+
toolbarEnd?: ReactNode;
|
|
38
47
|
className?: string;
|
|
39
48
|
ref?: Ref<HTMLDivElement>;
|
|
40
49
|
}
|
|
@@ -46,70 +55,74 @@ type Run =
|
|
|
46
55
|
| { readonly status: 'failed'; readonly message: string }
|
|
47
56
|
| { readonly status: 'cancelled' };
|
|
48
57
|
|
|
49
|
-
/* What a run is about to execute, and how the toolbar names it afterwards. */
|
|
50
|
-
interface RunTarget {
|
|
51
|
-
readonly sql: string;
|
|
52
|
-
readonly ran: Ran;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
type Ran = { readonly kind: 'statement'; readonly label: string } | { readonly kind: 'selection' } | { readonly kind: 'all'; readonly count: number };
|
|
56
|
-
|
|
57
58
|
interface PageState {
|
|
58
59
|
readonly offset: number;
|
|
59
60
|
readonly rows: RowsResult;
|
|
60
61
|
}
|
|
61
62
|
|
|
62
|
-
interface Notice {
|
|
63
|
-
readonly tone: 'error' | 'neutral';
|
|
64
|
-
readonly message: string;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
63
|
/* Rows in the first page of a result, and in every page after it. */
|
|
68
64
|
const PAGE_SIZE = 500;
|
|
69
65
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
66
|
+
/* Where the height of the results is kept, in pixels, for every console. */
|
|
67
|
+
export const RESULTS_HEIGHT_KEY = 'database:console-results';
|
|
68
|
+
|
|
69
|
+
/* The results and the editor never shrink below these; the results start at half the console. */
|
|
70
|
+
const MIN_RESULTS_HEIGHT = 96;
|
|
71
|
+
const MIN_EDITOR_HEIGHT = 120;
|
|
72
|
+
|
|
73
|
+
const parseHeight = (raw: string | null | undefined): number | null => {
|
|
74
|
+
try {
|
|
75
|
+
const value: unknown = JSON.parse(raw ?? 'null');
|
|
76
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= MIN_RESULTS_HEIGHT ? Math.round(value) : null;
|
|
77
|
+
} catch {
|
|
78
|
+
return null;
|
|
78
79
|
}
|
|
79
80
|
};
|
|
80
81
|
|
|
82
|
+
/* The app's editor as an element of its own, so it gets its props the way `SqlEditor` does and not during the console's render. */
|
|
83
|
+
function AppEditor({ render, ...editor }: QueryConsoleEditorProps & { render(editor: QueryConsoleEditorProps): ReactNode }) {
|
|
84
|
+
return render(editor);
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
/* Type SQL, run it, and read what each statement did. Several statements give a tab each. */
|
|
82
88
|
export function QueryConsole({
|
|
83
89
|
connection,
|
|
84
90
|
schema,
|
|
91
|
+
onSchemaChange,
|
|
85
92
|
value,
|
|
86
93
|
defaultValue = '',
|
|
87
94
|
onValueChange,
|
|
88
95
|
defaultHistoryOpen = false,
|
|
89
96
|
autoFocus = false,
|
|
97
|
+
renderEditor,
|
|
98
|
+
toolbarEnd,
|
|
90
99
|
className,
|
|
91
100
|
ref
|
|
92
101
|
}: QueryConsoleProps) {
|
|
93
102
|
const { t } = useTranslation('database');
|
|
94
103
|
const client = useDatabaseClient();
|
|
104
|
+
const act = useDatabaseAction();
|
|
105
|
+
const onNotice = useDatabaseNotice();
|
|
95
106
|
const files = useDatabaseFiles();
|
|
107
|
+
const storage = useDatabaseStorage();
|
|
96
108
|
// A channel of its own, so a transaction a person starts here stays out of the table views and the designer.
|
|
97
109
|
const channel = `console:${useId()}`;
|
|
98
|
-
const session = useMemo(() => client.session(connection, channel), [client, connection, channel]);
|
|
99
|
-
const engine = connection
|
|
110
|
+
const session = useMemo(() => (connection === undefined ? null : client.session(connection, channel)), [client, connection, channel]);
|
|
111
|
+
const engine = connection?.config.engine;
|
|
100
112
|
useEffect(
|
|
101
113
|
() => () => {
|
|
102
|
-
void session
|
|
114
|
+
void session?.close();
|
|
103
115
|
},
|
|
104
116
|
[session]
|
|
105
117
|
);
|
|
106
|
-
|
|
118
|
+
// Nothing is recorded without a connection, so the key of none is never written.
|
|
119
|
+
const history = useConsoleHistory(connection?.id ?? '');
|
|
107
120
|
const running = useRef<AbortController | null>(null);
|
|
108
121
|
const generation = useRef(0);
|
|
109
|
-
const
|
|
122
|
+
const editorHandle = useRef<QueryConsoleEditorHandle>(null);
|
|
123
|
+
const resultsPane = useRef<HTMLElement>(null);
|
|
110
124
|
const [own, setOwn] = useState(defaultValue);
|
|
111
125
|
const [run, setRun] = useState<Run>({ status: 'idle' });
|
|
112
|
-
const [ran, setRan] = useState<Ran | null>(null);
|
|
113
126
|
const [tab, setTab] = useState(0);
|
|
114
127
|
const [pages, setPages] = useState<Readonly<Record<number, PageState>>>({});
|
|
115
128
|
const [paging, setPaging] = useState(false);
|
|
@@ -119,16 +132,27 @@ export function QueryConsole({
|
|
|
119
132
|
const [inTransaction, setInTransaction] = useState(false);
|
|
120
133
|
const [settling, setSettling] = useState(false);
|
|
121
134
|
const [endingManual, setEndingManual] = useState(false);
|
|
122
|
-
const [confirming, setConfirming] = useState<{ readonly target:
|
|
135
|
+
const [confirming, setConfirming] = useState<{ readonly target: string; readonly statements: readonly DestructiveStatement[] } | null>(null);
|
|
123
136
|
const [schemas, setSchemas] = useState<readonly SchemaInfo[]>([]);
|
|
124
137
|
const [seededSchema, setSeededSchema] = useState(schema);
|
|
125
138
|
const [pickedSchema, setPickedSchema] = useState<string | undefined>(undefined);
|
|
126
139
|
const [exporting, setExporting] = useState(false);
|
|
127
|
-
|
|
140
|
+
// What the console shows itself when the app takes no notices.
|
|
141
|
+
const [notice, setNotice] = useState<DatabaseNotice | null>(null);
|
|
142
|
+
/* Closed until the first run, so the editor has the whole height before anything ran. */
|
|
143
|
+
const [resultsOpen, setResultsOpen] = useState(false);
|
|
144
|
+
const [resultsHeight, setResultsHeight] = useState(() => parseHeight(storage?.get(RESULTS_HEIGHT_KEY)));
|
|
145
|
+
const { startResize } = useColumnResize(resultsPane, {
|
|
146
|
+
size: resultsHeight ?? MIN_RESULTS_HEIGHT,
|
|
147
|
+
min: MIN_RESULTS_HEIGHT,
|
|
148
|
+
from: 'bottom',
|
|
149
|
+
max: () => (resultsPane.current?.parentElement?.clientHeight ?? 0) - MIN_EDITOR_HEIGHT,
|
|
150
|
+
onSize: setResultsHeight
|
|
151
|
+
});
|
|
128
152
|
const sql = value ?? own;
|
|
129
153
|
const blank = sql.trim() === '';
|
|
130
154
|
const busy = run.status === 'running' || settling;
|
|
131
|
-
const activeSchema = pickedSchema ?? schema;
|
|
155
|
+
const activeSchema = onSchemaChange === undefined ? (pickedSchema ?? schema) : schema;
|
|
132
156
|
|
|
133
157
|
if (seededSchema !== schema) {
|
|
134
158
|
setSeededSchema(schema);
|
|
@@ -138,15 +162,13 @@ export function QueryConsole({
|
|
|
138
162
|
useEffect(() => () => running.current?.abort(), []);
|
|
139
163
|
|
|
140
164
|
useEffect(() => {
|
|
141
|
-
if (
|
|
142
|
-
|
|
165
|
+
if (resultsHeight !== null) {
|
|
166
|
+
storage?.set(RESULTS_HEIGHT_KEY, JSON.stringify(resultsHeight));
|
|
143
167
|
}
|
|
144
|
-
|
|
145
|
-
// eslint-disable-next-line react/exhaustive-deps
|
|
146
|
-
}, []);
|
|
168
|
+
}, [storage, resultsHeight]);
|
|
147
169
|
|
|
148
170
|
useEffect(() => {
|
|
149
|
-
if (engine !== 'mysql') {
|
|
171
|
+
if (session === null || engine !== 'mysql') {
|
|
150
172
|
return undefined;
|
|
151
173
|
}
|
|
152
174
|
const controller = new AbortController();
|
|
@@ -157,6 +179,14 @@ export function QueryConsole({
|
|
|
157
179
|
return () => controller.abort();
|
|
158
180
|
}, [engine, session]);
|
|
159
181
|
|
|
182
|
+
const notify = (next: DatabaseNotice): void => {
|
|
183
|
+
if (onNotice === undefined) {
|
|
184
|
+
setNotice(next);
|
|
185
|
+
} else {
|
|
186
|
+
onNotice(next);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
160
190
|
const changeSql = (next: string): void => {
|
|
161
191
|
if (value === undefined) {
|
|
162
192
|
setOwn(next);
|
|
@@ -164,30 +194,28 @@ export function QueryConsole({
|
|
|
164
194
|
onValueChange?.(next);
|
|
165
195
|
};
|
|
166
196
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return
|
|
197
|
+
/* The SQL a run of the scope sends, or null when there is nothing to run. */
|
|
198
|
+
const targetOf = (scope: QueryConsoleRunScope): string | null => {
|
|
199
|
+
if (engine === undefined) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const selection = editorHandle.current?.selection();
|
|
203
|
+
if (scope === 'all' || selection === undefined) {
|
|
204
|
+
return splitStatements(sql, engine).length === 0 ? null : sql;
|
|
175
205
|
}
|
|
176
|
-
const start
|
|
177
|
-
const end = editor.current?.selectionEnd ?? 0;
|
|
206
|
+
const { start, end } = selection;
|
|
178
207
|
const selected = sql.slice(start, end);
|
|
179
208
|
if (splitStatements(selected, engine).length > 0) {
|
|
180
|
-
return
|
|
209
|
+
return selected;
|
|
181
210
|
}
|
|
182
|
-
|
|
183
|
-
return statement === null ? null : { sql: statement.text, ran: { kind: 'statement', label: statementLabel(statement.text) } };
|
|
211
|
+
return statementAt(sql, start, engine)?.text ?? null;
|
|
184
212
|
};
|
|
185
213
|
|
|
186
|
-
const requestRun = (target:
|
|
187
|
-
if (target === null || busy) {
|
|
214
|
+
const requestRun = (target: string | null): void => {
|
|
215
|
+
if (target === null || busy || engine === undefined) {
|
|
188
216
|
return;
|
|
189
217
|
}
|
|
190
|
-
const statements = findDestructive(target
|
|
218
|
+
const statements = findDestructive(target, engine);
|
|
191
219
|
if (statements.length > 0) {
|
|
192
220
|
setConfirming({ target, statements });
|
|
193
221
|
return;
|
|
@@ -195,20 +223,23 @@ export function QueryConsole({
|
|
|
195
223
|
void execute(target);
|
|
196
224
|
};
|
|
197
225
|
|
|
198
|
-
const execute = async (target:
|
|
226
|
+
const execute = async (target: string): Promise<void> => {
|
|
227
|
+
if (session === null || connection === undefined) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
199
230
|
const controller = new AbortController();
|
|
200
231
|
const mine = ++generation.current;
|
|
201
232
|
running.current = controller;
|
|
233
|
+
setResultsOpen(true);
|
|
202
234
|
setRun({ status: 'running' });
|
|
203
|
-
setRan(target.ran);
|
|
204
235
|
setNotice(null);
|
|
205
236
|
const record = (outcome: Pick<HistoryEntry, 'ok' | 'rows'>): void =>
|
|
206
|
-
history.record({ sql: target
|
|
237
|
+
history.record({ sql: target, at: Date.now(), connection: connection.name, ...outcome });
|
|
207
238
|
try {
|
|
208
239
|
if (mode === 'manual' && !inTransaction) {
|
|
209
240
|
setInTransaction(await session.transaction('begin', { signal: controller.signal }));
|
|
210
241
|
}
|
|
211
|
-
const executed = await session.execute(target
|
|
242
|
+
const executed = await session.execute(target, { schema: activeSchema, limit: PAGE_SIZE, signal: controller.signal });
|
|
212
243
|
if (controller.signal.aborted) {
|
|
213
244
|
setRun({ status: 'cancelled' });
|
|
214
245
|
return;
|
|
@@ -235,17 +266,24 @@ export function QueryConsole({
|
|
|
235
266
|
const pickHistory = (entry: HistoryEntry, runIt: boolean): void => {
|
|
236
267
|
changeSql(entry.sql);
|
|
237
268
|
if (runIt) {
|
|
238
|
-
requestRun(
|
|
269
|
+
requestRun(entry.sql);
|
|
239
270
|
}
|
|
240
271
|
};
|
|
241
272
|
|
|
242
273
|
const settle = async (action: 'commit' | 'rollback'): Promise<boolean> => {
|
|
274
|
+
if (session === null) {
|
|
275
|
+
return false;
|
|
276
|
+
}
|
|
243
277
|
setSettling(true);
|
|
244
278
|
try {
|
|
245
279
|
setInTransaction(await session.transaction(action));
|
|
246
280
|
return true;
|
|
247
281
|
} catch (error) {
|
|
248
|
-
|
|
282
|
+
notify({
|
|
283
|
+
tone: 'error',
|
|
284
|
+
title: t(action === 'commit' ? 'console.transaction.commitFailed' : 'console.transaction.rollbackFailed'),
|
|
285
|
+
description: messageOf(error)
|
|
286
|
+
});
|
|
249
287
|
return false;
|
|
250
288
|
} finally {
|
|
251
289
|
setSettling(false);
|
|
@@ -268,6 +306,9 @@ export function QueryConsole({
|
|
|
268
306
|
};
|
|
269
307
|
|
|
270
308
|
const loadPage = async (index: number, result: StatementResult, offset: number): Promise<void> => {
|
|
309
|
+
if (session === null) {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
271
312
|
const mine = generation.current;
|
|
272
313
|
setPaging(true);
|
|
273
314
|
setNotice(null);
|
|
@@ -277,14 +318,14 @@ export function QueryConsole({
|
|
|
277
318
|
setPages((current) => ({ ...current, [index]: { offset, rows } }));
|
|
278
319
|
}
|
|
279
320
|
} catch (error) {
|
|
280
|
-
|
|
321
|
+
notify({ tone: 'error', title: t('console.pageFailed'), description: messageOf(error) });
|
|
281
322
|
} finally {
|
|
282
323
|
setPaging(false);
|
|
283
324
|
}
|
|
284
325
|
};
|
|
285
326
|
|
|
286
327
|
const exportResult = async (result: StatementResult, format: FileFormat): Promise<void> => {
|
|
287
|
-
if (files === undefined) {
|
|
328
|
+
if (files === undefined || session === null) {
|
|
288
329
|
return;
|
|
289
330
|
}
|
|
290
331
|
setExporting(true);
|
|
@@ -292,10 +333,10 @@ export function QueryConsole({
|
|
|
292
333
|
const path = await files.save({ suggestedName: `result.${format}`, format });
|
|
293
334
|
if (path !== null) {
|
|
294
335
|
const exported = await session.export({ source: { kind: 'query', sql: result.sql, schema: activeSchema }, format, path });
|
|
295
|
-
|
|
336
|
+
notify({ tone: 'success', title: t('console.exported', { count: exported.rows, formatted: formatNumber(exported.rows) }) });
|
|
296
337
|
}
|
|
297
338
|
} catch (error) {
|
|
298
|
-
|
|
339
|
+
notify({ tone: 'error', title: t('console.exportFailed'), description: messageOf(error) });
|
|
299
340
|
} finally {
|
|
300
341
|
setExporting(false);
|
|
301
342
|
}
|
|
@@ -308,7 +349,7 @@ export function QueryConsole({
|
|
|
308
349
|
const shown: StatementResult | undefined = base?.kind === 'rows' && page !== undefined ? { kind: 'rows', sql: base.sql, ...page.rows } : base;
|
|
309
350
|
const offset = page?.offset ?? 0;
|
|
310
351
|
const pager: ResultPager | undefined =
|
|
311
|
-
base?.kind === 'rows' && shown?.kind === 'rows' && isPageable(base.sql, engine) && (shown.hasMore || offset > 0)
|
|
352
|
+
base?.kind === 'rows' && shown?.kind === 'rows' && engine !== undefined && isPageable(base.sql, engine) && (shown.hasMore || offset > 0)
|
|
312
353
|
? {
|
|
313
354
|
page: offset / PAGE_SIZE + 1,
|
|
314
355
|
loading: paging,
|
|
@@ -317,102 +358,162 @@ export function QueryConsole({
|
|
|
317
358
|
}
|
|
318
359
|
: undefined;
|
|
319
360
|
const exportBase = files !== undefined && base?.kind === 'rows' ? base : undefined;
|
|
361
|
+
// Read for the statement, not for each page of it, so paging through a result asks for nothing again.
|
|
362
|
+
const keys = useResultKeys(session, engine, base);
|
|
363
|
+
|
|
364
|
+
const editorProps: QueryConsoleEditorProps = {
|
|
365
|
+
ref: editorHandle,
|
|
366
|
+
value: sql,
|
|
367
|
+
onValueChange: changeSql,
|
|
368
|
+
run: (scope) => requestRun(targetOf(scope)),
|
|
369
|
+
busy,
|
|
370
|
+
label: t('console.editor'),
|
|
371
|
+
placeholder: t('console.placeholder'),
|
|
372
|
+
autoFocus
|
|
373
|
+
};
|
|
320
374
|
|
|
321
375
|
return (
|
|
322
376
|
<div ref={ref} className={clsx('flex min-h-0 flex-col bg-surface text-text', className)}>
|
|
323
|
-
<div className="flex
|
|
324
|
-
<div className="flex min-w-0 flex-1 flex-col
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
<Button size="sm" onClick={() => running.current?.abort()}>
|
|
337
|
-
<Icon icon={Square} size={12} />
|
|
338
|
-
{t('console.cancel')}
|
|
339
|
-
</Button>
|
|
340
|
-
<Spinner size={14} label={t('console.running')} />
|
|
341
|
-
</>
|
|
342
|
-
) : (
|
|
377
|
+
<div className="flex min-h-0 flex-1">
|
|
378
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
379
|
+
{/* The app's editor runs edge to edge, the way it draws a file; the text area keeps the inset of a field. */}
|
|
380
|
+
<div className={clsx('flex min-h-0 flex-1 flex-col', renderEditor === undefined && 'px-3 pt-3')}>
|
|
381
|
+
{renderEditor === undefined ? <SqlEditor {...editorProps} /> : <AppEditor render={renderEditor} {...editorProps} />}
|
|
382
|
+
</div>
|
|
383
|
+
<div
|
|
384
|
+
className={clsx(
|
|
385
|
+
'flex shrink-0 flex-wrap items-center gap-2 px-3 pb-3',
|
|
386
|
+
renderEditor === undefined ? 'pt-2' : 'border-t border-border pt-3'
|
|
387
|
+
)}
|
|
388
|
+
>
|
|
389
|
+
{session !== null && (
|
|
343
390
|
<>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
<
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
391
|
+
{run.status === 'running' ? (
|
|
392
|
+
<>
|
|
393
|
+
<Button size="sm" onClick={() => running.current?.abort()}>
|
|
394
|
+
<Icon icon={Square} size={12} />
|
|
395
|
+
{t('console.cancel')}
|
|
396
|
+
</Button>
|
|
397
|
+
<Spinner size={14} label={t('console.running')} />
|
|
398
|
+
</>
|
|
399
|
+
) : (
|
|
400
|
+
<>
|
|
401
|
+
<Tooltip label={t('console.run')} kbd={RUN_SHORTCUT}>
|
|
402
|
+
<Button
|
|
403
|
+
variant="primary"
|
|
404
|
+
size="sm"
|
|
405
|
+
disabled={blank || busy}
|
|
406
|
+
onClick={() => requestRun(targetOf('selection-or-statement'))}
|
|
407
|
+
>
|
|
408
|
+
<Icon icon={Play} size={12} />
|
|
409
|
+
{t('console.run')}
|
|
410
|
+
</Button>
|
|
411
|
+
</Tooltip>
|
|
412
|
+
<Tooltip label={t('console.runAll')} kbd={RUN_ALL_SHORTCUT}>
|
|
413
|
+
<Button variant="secondary" size="sm" disabled={blank || busy} onClick={() => requestRun(targetOf('all'))}>
|
|
414
|
+
{t('console.runAll')}
|
|
415
|
+
</Button>
|
|
416
|
+
</Tooltip>
|
|
417
|
+
</>
|
|
418
|
+
)}
|
|
419
|
+
<TransactionControls
|
|
420
|
+
mode={mode}
|
|
421
|
+
onModeChange={changeMode}
|
|
422
|
+
open={inTransaction}
|
|
423
|
+
busy={busy}
|
|
424
|
+
onCommit={() => void settle('commit')}
|
|
425
|
+
onRollback={() => void settle('rollback')}
|
|
426
|
+
/>
|
|
355
427
|
</>
|
|
356
428
|
)}
|
|
357
|
-
<TransactionControls
|
|
358
|
-
mode={mode}
|
|
359
|
-
onModeChange={changeMode}
|
|
360
|
-
open={inTransaction}
|
|
361
|
-
busy={busy}
|
|
362
|
-
onCommit={() => void settle('commit')}
|
|
363
|
-
onRollback={() => void settle('rollback')}
|
|
364
|
-
/>
|
|
365
|
-
{ran !== null && run.status !== 'idle' && <span className="min-w-0 truncate text-xs text-text-muted">{ranText(ran, t)}</span>}
|
|
366
429
|
<div className="ml-auto flex items-center gap-2">
|
|
367
|
-
{
|
|
368
|
-
|
|
430
|
+
{toolbarEnd}
|
|
431
|
+
{session !== null && schemas.length > 0 && (
|
|
432
|
+
<SchemaPicker
|
|
433
|
+
schemas={schemas}
|
|
434
|
+
value={activeSchema ?? null}
|
|
435
|
+
disabled={busy}
|
|
436
|
+
onValueChange={onSchemaChange ?? setPickedSchema}
|
|
437
|
+
/>
|
|
438
|
+
)}
|
|
439
|
+
{session !== null && (
|
|
440
|
+
<Button
|
|
441
|
+
variant="secondary"
|
|
442
|
+
size="sm"
|
|
443
|
+
aria-pressed={historyOpen}
|
|
444
|
+
className="aria-pressed:bg-surface-active aria-pressed:text-text"
|
|
445
|
+
onClick={() => setHistoryOpen(!historyOpen)}
|
|
446
|
+
>
|
|
447
|
+
<Icon icon={History} size={12} />
|
|
448
|
+
{t('console.history.title')}
|
|
449
|
+
</Button>
|
|
369
450
|
)}
|
|
370
|
-
<Button
|
|
371
|
-
size="sm"
|
|
372
|
-
aria-pressed={historyOpen}
|
|
373
|
-
className="aria-pressed:bg-surface-active aria-pressed:text-text"
|
|
374
|
-
onClick={() => setHistoryOpen(!historyOpen)}
|
|
375
|
-
>
|
|
376
|
-
<Icon icon={History} size={12} />
|
|
377
|
-
{t('console.history.title')}
|
|
378
|
-
</Button>
|
|
379
451
|
</div>
|
|
380
452
|
</div>
|
|
381
453
|
</div>
|
|
382
|
-
{historyOpen && <HistoryPanel entries={history.entries} onPick={pickHistory} onClear={history.clear} />}
|
|
454
|
+
{session !== null && historyOpen && <HistoryPanel entries={history.entries} onPick={pickHistory} onClear={history.clear} />}
|
|
383
455
|
</div>
|
|
384
|
-
{
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
456
|
+
{session !== null && notice !== null && (
|
|
457
|
+
<Banner
|
|
458
|
+
icon={notice.tone === 'error' ? CircleAlert : Check}
|
|
459
|
+
tone={notice.tone === 'error' ? 'error' : 'neutral'}
|
|
460
|
+
message={notice.description ?? notice.title}
|
|
461
|
+
className="shrink-0 pb-2"
|
|
462
|
+
>
|
|
388
463
|
<Button size="xs" onClick={() => setNotice(null)}>
|
|
389
464
|
{t('console.dismiss')}
|
|
390
465
|
</Button>
|
|
391
466
|
</Banner>
|
|
392
467
|
)}
|
|
393
|
-
{
|
|
394
|
-
<
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
468
|
+
{session !== null && resultsOpen && (
|
|
469
|
+
<section
|
|
470
|
+
ref={resultsPane}
|
|
471
|
+
aria-label={t('console.results')}
|
|
472
|
+
className="relative flex min-h-24 shrink-0 flex-col border-t border-border"
|
|
473
|
+
style={{ height: resultsHeight ?? '50%', maxHeight: `calc(100% - ${MIN_EDITOR_HEIGHT}px)` }}
|
|
474
|
+
>
|
|
475
|
+
<ColumnResizeHandle from="bottom" onPointerDown={startResize} />
|
|
476
|
+
<Tabs.Root value={index} onValueChange={(next) => setTab(Number(next))}>
|
|
477
|
+
<Tabs.List
|
|
478
|
+
aria-label={t('console.results')}
|
|
479
|
+
className="shrink-0 pr-1.5 pl-3"
|
|
480
|
+
end={<IconButton icon={X} size="sm" label={t('console.closeResults')} onClick={() => setResultsOpen(false)} />}
|
|
481
|
+
>
|
|
482
|
+
{results.map((result, position) => (
|
|
483
|
+
<Tabs.Tab key={position} value={position}>
|
|
484
|
+
{statementLabel(result.sql)}
|
|
485
|
+
</Tabs.Tab>
|
|
486
|
+
))}
|
|
487
|
+
</Tabs.List>
|
|
488
|
+
</Tabs.Root>
|
|
489
|
+
{run.status === 'failed' && <Banner icon={CircleAlert} tone="error" message={run.message} className="shrink-0 pt-2" />}
|
|
490
|
+
{run.status === 'cancelled' && <Banner icon={CircleAlert} tone="neutral" message={t('console.cancelled')} className="shrink-0 pt-2" />}
|
|
491
|
+
{shown !== undefined && (
|
|
492
|
+
<StatementResultView
|
|
493
|
+
key={`${index}:${offset}`}
|
|
494
|
+
result={shown}
|
|
495
|
+
offset={offset}
|
|
496
|
+
pager={pager}
|
|
497
|
+
onExport={exportBase === undefined ? undefined : (format) => void exportResult(exportBase, format)}
|
|
498
|
+
exporting={exporting}
|
|
499
|
+
engine={engine}
|
|
500
|
+
valuePanelOpen={panelOpen}
|
|
501
|
+
onValuePanelOpenChange={setPanelOpen}
|
|
502
|
+
keys={keys}
|
|
503
|
+
onFollowReference={
|
|
504
|
+
act === undefined || connection === undefined
|
|
505
|
+
? undefined
|
|
506
|
+
: (reference) =>
|
|
507
|
+
act({
|
|
508
|
+
kind: 'open-table',
|
|
509
|
+
ref: { connectionId: connection.id, schema: reference.schema, table: reference.table },
|
|
510
|
+
view: 'data',
|
|
511
|
+
where: reference.where
|
|
512
|
+
})
|
|
513
|
+
}
|
|
514
|
+
/>
|
|
515
|
+
)}
|
|
516
|
+
</section>
|
|
416
517
|
)}
|
|
417
518
|
<DestructiveDialog
|
|
418
519
|
statements={confirming?.statements ?? []}
|