@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.
Files changed (135) hide show
  1. package/README.md +1 -1
  2. package/dist/DatabaseProvider.d.ts +4 -3
  3. package/dist/DatabaseProvider.js +15 -3
  4. package/dist/KeyIcon.d.ts +4 -0
  5. package/dist/KeyIcon.js +14 -0
  6. package/dist/actions.d.ts +12 -0
  7. package/dist/client-context.d.ts +3 -1
  8. package/dist/client-context.js +2 -0
  9. package/dist/code-text.d.ts +1 -0
  10. package/dist/code-text.js +2 -0
  11. package/dist/column-keys.d.ts +5 -0
  12. package/dist/column-keys.js +2 -0
  13. package/dist/connections/ConnectionManager.d.ts +3 -2
  14. package/dist/connections/ConnectionManager.js +27 -16
  15. package/dist/connections/connection-config.js +2 -0
  16. package/dist/console/QueryConsole.d.ts +8 -3
  17. package/dist/console/QueryConsole.js +110 -50
  18. package/dist/console/SqlEditor.d.ts +2 -12
  19. package/dist/console/SqlEditor.js +29 -18
  20. package/dist/console/StatementResultView.d.ts +5 -1
  21. package/dist/console/StatementResultView.js +18 -5
  22. package/dist/console/TransactionControls.js +3 -6
  23. package/dist/console/editor-slot.d.ts +18 -0
  24. package/dist/console/editor-slot.js +1 -0
  25. package/dist/console/result-keys.d.ts +15 -0
  26. package/dist/console/result-keys.js +79 -0
  27. package/dist/designer/ColumnsEditor.js +2 -2
  28. package/dist/designer/DesignerView.d.ts +3 -2
  29. package/dist/designer/DesignerView.js +5 -4
  30. package/dist/designer/SqlPreview.js +2 -1
  31. package/dist/designer/TableDesigner.d.ts +3 -2
  32. package/dist/designer/TableDesigner.js +2 -2
  33. package/dist/explorer/DatabaseExplorer.d.ts +4 -1
  34. package/dist/explorer/DatabaseExplorer.js +79 -23
  35. package/dist/explorer/RowMenu.js +2 -2
  36. package/dist/explorer/expansion.js +2 -2
  37. package/dist/explorer/folders.d.ts +16 -0
  38. package/dist/explorer/folders.js +1 -0
  39. package/dist/explorer/tree.d.ts +55 -4
  40. package/dist/explorer/tree.js +137 -23
  41. package/dist/grid/CellEditor.js +3 -1
  42. package/dist/grid/DataGrid.d.ts +2 -2
  43. package/dist/grid/DataGrid.js +61 -59
  44. package/dist/grid/EnumPicker.js +2 -1
  45. package/dist/grid/GridHeaderCell.d.ts +1 -2
  46. package/dist/grid/GridHeaderCell.js +12 -5
  47. package/dist/grid/GridRowView.d.ts +49 -0
  48. package/dist/grid/GridRowView.js +51 -0
  49. package/dist/grid/RecordView.d.ts +4 -2
  50. package/dist/grid/RecordView.js +71 -22
  51. package/dist/grid/ValueDock.d.ts +3 -1
  52. package/dist/grid/ValueDock.js +9 -3
  53. package/dist/grid/cell-id.d.ts +1 -0
  54. package/dist/grid/cell-id.js +2 -0
  55. package/dist/grid/layout.d.ts +3 -2
  56. package/dist/grid/layout.js +15 -6
  57. package/dist/grid/types.d.ts +8 -1
  58. package/dist/grid/types.js +4 -3
  59. package/dist/index.d.ts +5 -1
  60. package/dist/index.js +2 -0
  61. package/dist/locales/en.json +32 -15
  62. package/dist/locales/nl.json +32 -15
  63. package/dist/protocol/index.d.ts +1 -1
  64. package/dist/protocol/messages.d.ts +1 -1
  65. package/dist/protocol/messages.js +1 -1
  66. package/dist/protocol/schema.d.ts +17 -0
  67. package/dist/structure/StructureView.d.ts +5 -3
  68. package/dist/structure/StructureView.js +8 -6
  69. package/dist/table/CommandField.js +2 -2
  70. package/dist/table/TableStatusBar.js +2 -2
  71. package/dist/table/TableToolbar.d.ts +3 -4
  72. package/dist/table/TableToolbar.js +4 -3
  73. package/dist/table/TableView.d.ts +4 -2
  74. package/dist/table/TableView.js +63 -91
  75. package/dist/table/grid-rows.js +4 -1
  76. package/dist/table/useTableTransfer.d.ts +3 -1
  77. package/dist/table/useTableTransfer.js +19 -5
  78. package/dist/testing/fake.d.ts +3 -1
  79. package/dist/testing/fake.js +7 -1
  80. package/dist/value/ValueEditor.d.ts +9 -0
  81. package/dist/value/ValueEditor.js +102 -0
  82. package/dist/value/ValuePanel.d.ts +1 -1
  83. package/dist/value/ValuePanel.js +3 -94
  84. package/dist/workbench/DatabaseWorkbench.js +4 -7
  85. package/package.json +6 -6
  86. package/src/DatabaseProvider.tsx +24 -5
  87. package/src/KeyIcon.tsx +15 -0
  88. package/src/actions.ts +28 -2
  89. package/src/client-context.ts +5 -1
  90. package/src/code-text.ts +2 -0
  91. package/src/column-keys.ts +8 -0
  92. package/src/connections/ConnectionManager.tsx +98 -68
  93. package/src/connections/connection-config.ts +2 -0
  94. package/src/console/QueryConsole.tsx +246 -150
  95. package/src/console/SqlEditor.tsx +33 -37
  96. package/src/console/StatementResultView.tsx +61 -27
  97. package/src/console/TransactionControls.tsx +8 -11
  98. package/src/console/editor-slot.ts +27 -0
  99. package/src/console/result-keys.ts +109 -0
  100. package/src/designer/ColumnsEditor.tsx +3 -3
  101. package/src/designer/DesignerView.tsx +14 -3
  102. package/src/designer/SqlPreview.tsx +2 -1
  103. package/src/designer/TableDesigner.tsx +5 -2
  104. package/src/explorer/DatabaseExplorer.tsx +170 -26
  105. package/src/explorer/RowMenu.tsx +2 -2
  106. package/src/explorer/expansion.ts +4 -4
  107. package/src/explorer/folders.ts +25 -0
  108. package/src/explorer/tree.ts +219 -28
  109. package/src/grid/CellEditor.tsx +3 -2
  110. package/src/grid/DataGrid.tsx +114 -176
  111. package/src/grid/EnumPicker.tsx +2 -1
  112. package/src/grid/GridHeaderCell.tsx +14 -9
  113. package/src/grid/GridRowView.tsx +184 -0
  114. package/src/grid/RecordView.tsx +188 -75
  115. package/src/grid/ValueDock.tsx +12 -3
  116. package/src/grid/cell-id.ts +2 -0
  117. package/src/grid/layout.ts +16 -6
  118. package/src/grid/types.ts +15 -3
  119. package/src/index.ts +5 -1
  120. package/src/locales/en.json +32 -15
  121. package/src/locales/nl.json +32 -15
  122. package/src/protocol/index.ts +13 -1
  123. package/src/protocol/messages.ts +1 -1
  124. package/src/protocol/schema.ts +28 -0
  125. package/src/structure/StructureView.tsx +16 -6
  126. package/src/table/CommandField.tsx +2 -2
  127. package/src/table/TableStatusBar.tsx +3 -3
  128. package/src/table/TableToolbar.tsx +107 -92
  129. package/src/table/TableView.tsx +95 -132
  130. package/src/table/grid-rows.ts +5 -3
  131. package/src/table/useTableTransfer.ts +21 -6
  132. package/src/testing/fake.ts +18 -1
  133. package/src/value/ValueEditor.tsx +213 -0
  134. package/src/value/ValuePanel.tsx +4 -206
  135. package/src/workbench/DatabaseWorkbench.tsx +9 -10
@@ -1,12 +1,12 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useId, useMemo, useRef, useState } from 'react';
3
3
  import clsx from 'clsx';
4
- import { Check, CircleAlert, History, Play, Square } from 'lucide-react';
4
+ import { Check, CircleAlert, History, Play, Square, X } from 'lucide-react';
5
5
  import { useTranslation } from 'react-i18next';
6
- import { Banner, Button, Icon, messageOf, PromptDialog, Spinner, Tabs, Tooltip } from '@adecore/ui';
6
+ import { Banner, Button, ColumnResizeHandle, Icon, IconButton, messageOf, PromptDialog, Spinner, Tabs, Tooltip, useColumnResize } from '@adecore/ui';
7
7
  import { formatNumber } from '@adecore/ui/format';
8
8
  import { DatabaseRequestError } from '../client/types.js';
9
- import { useDatabaseClient, useDatabaseFiles } from '../client-context.js';
9
+ import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.js';
10
10
  import { splitStatements, statementAt } from '../sql-split.js';
11
11
  import { DestructiveDialog } from './DestructiveDialog.js';
12
12
  import { findDestructive } from './destructive.js';
@@ -15,6 +15,7 @@ import { statementLabel } from './labels.js';
15
15
  import { isPageable, outcomeOf } from './outcome.js';
16
16
  import { SchemaPicker } from './SchemaPicker.js';
17
17
  import { orderSchemas } from './schemas.js';
18
+ import { useResultKeys } from './result-keys.js';
18
19
  import { RUN_ALL_SHORTCUT, RUN_SHORTCUT } from './shortcuts.js';
19
20
  import { SqlEditor } from './SqlEditor.js';
20
21
  import { StatementResultView } from './StatementResultView.js';
@@ -22,35 +23,47 @@ import { TransactionControls } from './TransactionControls.js';
22
23
  import { useConsoleHistory } from './useConsoleHistory.js';
23
24
  /* Rows in the first page of a result, and in every page after it. */
24
25
  const PAGE_SIZE = 500;
25
- const ranText = (ran, t) => {
26
- switch (ran.kind) {
27
- case 'statement':
28
- return t('console.ranStatement', { statement: ran.label });
29
- case 'selection':
30
- return t('console.ranSelection');
31
- case 'all':
32
- return t('console.ranAll', { count: ran.count, formatted: formatNumber(ran.count) });
26
+ /* Where the height of the results is kept, in pixels, for every console. */
27
+ export const RESULTS_HEIGHT_KEY = 'database:console-results';
28
+ /* The results and the editor never shrink below these; the results start at half the console. */
29
+ const MIN_RESULTS_HEIGHT = 96;
30
+ const MIN_EDITOR_HEIGHT = 120;
31
+ const parseHeight = (raw) => {
32
+ try {
33
+ const value = JSON.parse(raw ?? 'null');
34
+ return typeof value === 'number' && Number.isFinite(value) && value >= MIN_RESULTS_HEIGHT ? Math.round(value) : null;
35
+ }
36
+ catch {
37
+ return null;
33
38
  }
34
39
  };
40
+ /* 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. */
41
+ function AppEditor({ render, ...editor }) {
42
+ return render(editor);
43
+ }
35
44
  /* Type SQL, run it, and read what each statement did. Several statements give a tab each. */
36
- export function QueryConsole({ connection, schema, value, defaultValue = '', onValueChange, defaultHistoryOpen = false, autoFocus = false, className, ref }) {
45
+ export function QueryConsole({ connection, schema, onSchemaChange, value, defaultValue = '', onValueChange, defaultHistoryOpen = false, autoFocus = false, renderEditor, toolbarEnd, className, ref }) {
37
46
  const { t } = useTranslation('database');
38
47
  const client = useDatabaseClient();
48
+ const act = useDatabaseAction();
49
+ const onNotice = useDatabaseNotice();
39
50
  const files = useDatabaseFiles();
51
+ const storage = useDatabaseStorage();
40
52
  // A channel of its own, so a transaction a person starts here stays out of the table views and the designer.
41
53
  const channel = `console:${useId()}`;
42
- const session = useMemo(() => client.session(connection, channel), [client, connection, channel]);
43
- const engine = connection.config.engine;
54
+ const session = useMemo(() => (connection === undefined ? null : client.session(connection, channel)), [client, connection, channel]);
55
+ const engine = connection?.config.engine;
44
56
  useEffect(() => () => {
45
- void session.close();
57
+ void session?.close();
46
58
  }, [session]);
47
- const history = useConsoleHistory(connection.id);
59
+ // Nothing is recorded without a connection, so the key of none is never written.
60
+ const history = useConsoleHistory(connection?.id ?? '');
48
61
  const running = useRef(null);
49
62
  const generation = useRef(0);
50
- const editor = useRef(null);
63
+ const editorHandle = useRef(null);
64
+ const resultsPane = useRef(null);
51
65
  const [own, setOwn] = useState(defaultValue);
52
66
  const [run, setRun] = useState({ status: 'idle' });
53
- const [ran, setRan] = useState(null);
54
67
  const [tab, setTab] = useState(0);
55
68
  const [pages, setPages] = useState({});
56
69
  const [paging, setPaging] = useState(false);
@@ -65,25 +78,34 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
65
78
  const [seededSchema, setSeededSchema] = useState(schema);
66
79
  const [pickedSchema, setPickedSchema] = useState(undefined);
67
80
  const [exporting, setExporting] = useState(false);
81
+ // What the console shows itself when the app takes no notices.
68
82
  const [notice, setNotice] = useState(null);
83
+ /* Closed until the first run, so the editor has the whole height before anything ran. */
84
+ const [resultsOpen, setResultsOpen] = useState(false);
85
+ const [resultsHeight, setResultsHeight] = useState(() => parseHeight(storage?.get(RESULTS_HEIGHT_KEY)));
86
+ const { startResize } = useColumnResize(resultsPane, {
87
+ size: resultsHeight ?? MIN_RESULTS_HEIGHT,
88
+ min: MIN_RESULTS_HEIGHT,
89
+ from: 'bottom',
90
+ max: () => (resultsPane.current?.parentElement?.clientHeight ?? 0) - MIN_EDITOR_HEIGHT,
91
+ onSize: setResultsHeight
92
+ });
69
93
  const sql = value ?? own;
70
94
  const blank = sql.trim() === '';
71
95
  const busy = run.status === 'running' || settling;
72
- const activeSchema = pickedSchema ?? schema;
96
+ const activeSchema = onSchemaChange === undefined ? (pickedSchema ?? schema) : schema;
73
97
  if (seededSchema !== schema) {
74
98
  setSeededSchema(schema);
75
99
  setPickedSchema(undefined);
76
100
  }
77
101
  useEffect(() => () => running.current?.abort(), []);
78
102
  useEffect(() => {
79
- if (autoFocus) {
80
- editor.current?.focus();
103
+ if (resultsHeight !== null) {
104
+ storage?.set(RESULTS_HEIGHT_KEY, JSON.stringify(resultsHeight));
81
105
  }
82
- // Only on mount: a console that later becomes active keeps the focus where the person put it.
83
- // eslint-disable-next-line react/exhaustive-deps
84
- }, []);
106
+ }, [storage, resultsHeight]);
85
107
  useEffect(() => {
86
- if (engine !== 'mysql') {
108
+ if (session === null || engine !== 'mysql') {
87
109
  return undefined;
88
110
  }
89
111
  const controller = new AbortController();
@@ -93,35 +115,41 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
93
115
  .catch(() => undefined);
94
116
  return () => controller.abort();
95
117
  }, [engine, session]);
118
+ const notify = (next) => {
119
+ if (onNotice === undefined) {
120
+ setNotice(next);
121
+ }
122
+ else {
123
+ onNotice(next);
124
+ }
125
+ };
96
126
  const changeSql = (next) => {
97
127
  if (value === undefined) {
98
128
  setOwn(next);
99
129
  }
100
130
  onValueChange?.(next);
101
131
  };
132
+ /* The SQL a run of the scope sends, or null when there is nothing to run. */
102
133
  const targetOf = (scope) => {
103
- if (scope === 'all') {
104
- const statements = splitStatements(sql, engine);
105
- const [only] = statements;
106
- if (only === undefined) {
107
- return null;
108
- }
109
- return { sql, ran: statements.length > 1 ? { kind: 'all', count: statements.length } : { kind: 'statement', label: statementLabel(only.text) } };
134
+ if (engine === undefined) {
135
+ return null;
136
+ }
137
+ const selection = editorHandle.current?.selection();
138
+ if (scope === 'all' || selection === undefined) {
139
+ return splitStatements(sql, engine).length === 0 ? null : sql;
110
140
  }
111
- const start = editor.current?.selectionStart ?? 0;
112
- const end = editor.current?.selectionEnd ?? 0;
141
+ const { start, end } = selection;
113
142
  const selected = sql.slice(start, end);
114
143
  if (splitStatements(selected, engine).length > 0) {
115
- return { sql: selected, ran: { kind: 'selection' } };
144
+ return selected;
116
145
  }
117
- const statement = statementAt(sql, start, engine);
118
- return statement === null ? null : { sql: statement.text, ran: { kind: 'statement', label: statementLabel(statement.text) } };
146
+ return statementAt(sql, start, engine)?.text ?? null;
119
147
  };
120
148
  const requestRun = (target) => {
121
- if (target === null || busy) {
149
+ if (target === null || busy || engine === undefined) {
122
150
  return;
123
151
  }
124
- const statements = findDestructive(target.sql, engine);
152
+ const statements = findDestructive(target, engine);
125
153
  if (statements.length > 0) {
126
154
  setConfirming({ target, statements });
127
155
  return;
@@ -129,18 +157,21 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
129
157
  void execute(target);
130
158
  };
131
159
  const execute = async (target) => {
160
+ if (session === null || connection === undefined) {
161
+ return;
162
+ }
132
163
  const controller = new AbortController();
133
164
  const mine = ++generation.current;
134
165
  running.current = controller;
166
+ setResultsOpen(true);
135
167
  setRun({ status: 'running' });
136
- setRan(target.ran);
137
168
  setNotice(null);
138
- const record = (outcome) => history.record({ sql: target.sql, at: Date.now(), connection: connection.name, ...outcome });
169
+ const record = (outcome) => history.record({ sql: target, at: Date.now(), connection: connection.name, ...outcome });
139
170
  try {
140
171
  if (mode === 'manual' && !inTransaction) {
141
172
  setInTransaction(await session.transaction('begin', { signal: controller.signal }));
142
173
  }
143
- const executed = await session.execute(target.sql, { schema: activeSchema, limit: PAGE_SIZE, signal: controller.signal });
174
+ const executed = await session.execute(target, { schema: activeSchema, limit: PAGE_SIZE, signal: controller.signal });
144
175
  if (controller.signal.aborted) {
145
176
  setRun({ status: 'cancelled' });
146
177
  return;
@@ -169,17 +200,24 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
169
200
  const pickHistory = (entry, runIt) => {
170
201
  changeSql(entry.sql);
171
202
  if (runIt) {
172
- requestRun({ sql: entry.sql, ran: { kind: 'statement', label: statementLabel(entry.sql) } });
203
+ requestRun(entry.sql);
173
204
  }
174
205
  };
175
206
  const settle = async (action) => {
207
+ if (session === null) {
208
+ return false;
209
+ }
176
210
  setSettling(true);
177
211
  try {
178
212
  setInTransaction(await session.transaction(action));
179
213
  return true;
180
214
  }
181
215
  catch (error) {
182
- setNotice({ tone: 'error', message: messageOf(error) });
216
+ notify({
217
+ tone: 'error',
218
+ title: t(action === 'commit' ? 'console.transaction.commitFailed' : 'console.transaction.rollbackFailed'),
219
+ description: messageOf(error)
220
+ });
183
221
  return false;
184
222
  }
185
223
  finally {
@@ -201,6 +239,9 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
201
239
  }
202
240
  };
203
241
  const loadPage = async (index, result, offset) => {
242
+ if (session === null) {
243
+ return;
244
+ }
204
245
  const mine = generation.current;
205
246
  setPaging(true);
206
247
  setNotice(null);
@@ -211,14 +252,14 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
211
252
  }
212
253
  }
213
254
  catch (error) {
214
- setNotice({ tone: 'error', message: messageOf(error) });
255
+ notify({ tone: 'error', title: t('console.pageFailed'), description: messageOf(error) });
215
256
  }
216
257
  finally {
217
258
  setPaging(false);
218
259
  }
219
260
  };
220
261
  const exportResult = async (result, format) => {
221
- if (files === undefined) {
262
+ if (files === undefined || session === null) {
222
263
  return;
223
264
  }
224
265
  setExporting(true);
@@ -226,11 +267,11 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
226
267
  const path = await files.save({ suggestedName: `result.${format}`, format });
227
268
  if (path !== null) {
228
269
  const exported = await session.export({ source: { kind: 'query', sql: result.sql, schema: activeSchema }, format, path });
229
- setNotice({ tone: 'neutral', message: t('console.exported', { count: exported.rows, formatted: formatNumber(exported.rows) }) });
270
+ notify({ tone: 'success', title: t('console.exported', { count: exported.rows, formatted: formatNumber(exported.rows) }) });
230
271
  }
231
272
  }
232
273
  catch (error) {
233
- setNotice({ tone: 'error', message: messageOf(error) });
274
+ notify({ tone: 'error', title: t('console.exportFailed'), description: messageOf(error) });
234
275
  }
235
276
  finally {
236
277
  setExporting(false);
@@ -242,7 +283,7 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
242
283
  const page = pages[index];
243
284
  const shown = base?.kind === 'rows' && page !== undefined ? { kind: 'rows', sql: base.sql, ...page.rows } : base;
244
285
  const offset = page?.offset ?? 0;
245
- const pager = base?.kind === 'rows' && shown?.kind === 'rows' && isPageable(base.sql, engine) && (shown.hasMore || offset > 0)
286
+ const pager = base?.kind === 'rows' && shown?.kind === 'rows' && engine !== undefined && isPageable(base.sql, engine) && (shown.hasMore || offset > 0)
246
287
  ? {
247
288
  page: offset / PAGE_SIZE + 1,
248
289
  loading: paging,
@@ -251,7 +292,26 @@ export function QueryConsole({ connection, schema, value, defaultValue = '', onV
251
292
  }
252
293
  : undefined;
253
294
  const exportBase = files !== undefined && base?.kind === 'rows' ? base : undefined;
254
- return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), children: [_jsxs("div", { className: "flex shrink-0 border-b border-border", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2 p-3", children: [_jsx(SqlEditor, { ref: editor, value: sql, onValueChange: changeSql, onRun: (scope) => requestRun(targetOf(scope)), label: t('console.editor'), placeholder: t('console.placeholder') }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [run.status === 'running' ? (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", onClick: () => running.current?.abort(), children: [_jsx(Icon, { icon: Square, size: 12 }), t('console.cancel')] }), _jsx(Spinner, { size: 14, label: t('console.running') })] })) : (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: t('console.run'), kbd: RUN_SHORTCUT, children: _jsxs(Button, { variant: "primary", size: "sm", disabled: blank || busy, onClick: () => requestRun(targetOf('selection-or-statement')), children: [_jsx(Icon, { icon: Play, size: 12 }), t('console.run')] }) }), _jsx(Tooltip, { label: t('console.runAll'), kbd: RUN_ALL_SHORTCUT, children: _jsx(Button, { variant: "secondary", size: "sm", disabled: blank || busy, onClick: () => requestRun(targetOf('all')), children: t('console.runAll') }) })] })), _jsx(TransactionControls, { mode: mode, onModeChange: changeMode, open: inTransaction, busy: busy, onCommit: () => void settle('commit'), onRollback: () => void settle('rollback') }), ran !== null && run.status !== 'idle' && _jsx("span", { className: "min-w-0 truncate text-xs text-text-muted", children: ranText(ran, t) }), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [schemas.length > 0 && (_jsx(SchemaPicker, { schemas: schemas, value: activeSchema ?? null, disabled: busy, onValueChange: setPickedSchema })), _jsxs(Button, { size: "sm", "aria-pressed": historyOpen, className: "aria-pressed:bg-surface-active aria-pressed:text-text", onClick: () => setHistoryOpen(!historyOpen), children: [_jsx(Icon, { icon: History, size: 12 }), t('console.history.title')] })] })] })] }), historyOpen && _jsx(HistoryPanel, { entries: history.entries, onPick: pickHistory, onClear: history.clear })] }), run.status === 'failed' && _jsx(Banner, { icon: CircleAlert, tone: "error", message: run.message, className: "shrink-0 pt-2" }), run.status === 'cancelled' && _jsx(Banner, { icon: CircleAlert, tone: "neutral", message: t('console.cancelled'), className: "shrink-0 pt-2" }), notice !== null && (_jsx(Banner, { icon: notice.tone === 'error' ? CircleAlert : Check, tone: notice.tone, message: notice.message, className: "shrink-0 pt-2", children: _jsx(Button, { size: "xs", onClick: () => setNotice(null), children: t('console.dismiss') }) })), results.length > 1 && (_jsx(Tabs.Root, { value: tab, onValueChange: (next) => setTab(Number(next)), children: _jsx(Tabs.List, { className: "shrink-0 px-3", children: results.map((result, position) => (_jsx(Tabs.Tab, { value: position, children: statementLabel(result.sql) }, position))) }) })), shown !== undefined && (_jsx(StatementResultView, { result: shown, offset: offset, pager: pager, onExport: exportBase === undefined ? undefined : (format) => void exportResult(exportBase, format), exporting: exporting, engine: engine, valuePanelOpen: panelOpen, onValuePanelOpenChange: setPanelOpen }, `${index}:${offset}`)), _jsx(DestructiveDialog, { statements: confirming?.statements ?? [], onCancel: () => setConfirming(null), onConfirm: () => {
295
+ // Read for the statement, not for each page of it, so paging through a result asks for nothing again.
296
+ const keys = useResultKeys(session, engine, base);
297
+ const editorProps = {
298
+ ref: editorHandle,
299
+ value: sql,
300
+ onValueChange: changeSql,
301
+ run: (scope) => requestRun(targetOf(scope)),
302
+ busy,
303
+ label: t('console.editor'),
304
+ placeholder: t('console.placeholder'),
305
+ autoFocus
306
+ };
307
+ return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 flex-col bg-surface text-text', className), children: [_jsxs("div", { className: "flex min-h-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 flex-1 flex-col", children: [_jsxs("div", { className: "flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [session !== null && (_jsxs(_Fragment, { children: [run.status === 'running' ? (_jsxs(_Fragment, { children: [_jsxs(Button, { size: "sm", onClick: () => running.current?.abort(), children: [_jsx(Icon, { icon: Square, size: 12 }), t('console.cancel')] }), _jsx(Spinner, { size: 14, label: t('console.running') })] })) : (_jsxs(_Fragment, { children: [_jsx(Tooltip, { label: t('console.run'), kbd: RUN_SHORTCUT, children: _jsxs(Button, { variant: "primary", size: "sm", disabled: blank || busy, onClick: () => requestRun(targetOf('selection-or-statement')), children: [_jsx(Icon, { icon: Play, size: 12 }), t('console.run')] }) }), _jsx(Tooltip, { label: t('console.runAll'), kbd: RUN_ALL_SHORTCUT, children: _jsx(Button, { variant: "secondary", size: "sm", disabled: blank || busy, onClick: () => requestRun(targetOf('all')), children: t('console.runAll') }) })] })), _jsx(TransactionControls, { mode: mode, onModeChange: changeMode, open: inTransaction, busy: busy, onCommit: () => void settle('commit'), onRollback: () => void settle('rollback') })] })), _jsxs("div", { className: "ml-auto flex items-center gap-2", children: [toolbarEnd, session !== null && schemas.length > 0 && (_jsx(SchemaPicker, { schemas: schemas, value: activeSchema ?? null, disabled: busy, onValueChange: onSchemaChange ?? setPickedSchema })), session !== null && (_jsxs(Button, { variant: "secondary", size: "sm", "aria-pressed": historyOpen, className: "aria-pressed:bg-surface-active aria-pressed:text-text", onClick: () => setHistoryOpen(!historyOpen), children: [_jsx(Icon, { icon: History, size: 12 }), t('console.history.title')] }))] })] }), _jsx("div", { className: clsx('flex min-h-0 flex-1 flex-col', renderEditor === undefined && 'p-3'), children: renderEditor === undefined ? _jsx(SqlEditor, { ...editorProps }) : _jsx(AppEditor, { render: renderEditor, ...editorProps }) })] }), session !== null && historyOpen && _jsx(HistoryPanel, { entries: history.entries, onPick: pickHistory, onClear: history.clear })] }), session !== null && notice !== null && (_jsx(Banner, { icon: notice.tone === 'error' ? CircleAlert : Check, tone: notice.tone === 'error' ? 'error' : 'neutral', message: notice.description ?? notice.title, className: "shrink-0 pb-2", children: _jsx(Button, { size: "xs", onClick: () => setNotice(null), children: t('console.dismiss') }) })), session !== null && resultsOpen && (_jsxs("section", { ref: resultsPane, "aria-label": t('console.results'), className: "relative flex min-h-24 shrink-0 flex-col border-t border-border", style: { height: resultsHeight ?? '50%', maxHeight: `calc(100% - ${MIN_EDITOR_HEIGHT}px)` }, children: [_jsx(ColumnResizeHandle, { from: "bottom", onPointerDown: startResize }), _jsx(Tabs.Root, { value: index, onValueChange: (next) => setTab(Number(next)), children: _jsx(Tabs.List, { "aria-label": t('console.results'), className: "shrink-0 pr-1.5 pl-3", end: _jsx(IconButton, { icon: X, size: "sm", label: t('console.closeResults'), onClick: () => setResultsOpen(false) }), children: results.map((result, position) => (_jsx(Tabs.Tab, { value: position, children: statementLabel(result.sql) }, position))) }) }), run.status === 'failed' && _jsx(Banner, { icon: CircleAlert, tone: "error", message: run.message, className: "shrink-0 pt-2" }), run.status === 'cancelled' && _jsx(Banner, { icon: CircleAlert, tone: "neutral", message: t('console.cancelled'), className: "shrink-0 pt-2" }), shown !== undefined && (_jsx(StatementResultView, { result: shown, offset: offset, pager: pager, onExport: exportBase === undefined ? undefined : (format) => void exportResult(exportBase, format), exporting: exporting, engine: engine, valuePanelOpen: panelOpen, onValuePanelOpenChange: setPanelOpen, keys: keys, onFollowReference: act === undefined || connection === undefined
308
+ ? undefined
309
+ : (reference) => act({
310
+ kind: 'open-table',
311
+ ref: { connectionId: connection.id, schema: reference.schema, table: reference.table },
312
+ view: 'data',
313
+ where: reference.where
314
+ }) }, `${index}:${offset}`))] })), _jsx(DestructiveDialog, { statements: confirming?.statements ?? [], onCancel: () => setConfirming(null), onConfirm: () => {
255
315
  const target = confirming?.target;
256
316
  setConfirming(null);
257
317
  if (target !== undefined) {
@@ -1,12 +1,2 @@
1
- import { type Ref } from 'react';
2
- export type RunScope = 'selection-or-statement' | 'all';
3
- export interface SqlEditorProps {
4
- value: string;
5
- onValueChange(value: string): void;
6
- onRun(scope: RunScope): void;
7
- label: string;
8
- placeholder?: string;
9
- className?: string;
10
- ref?: Ref<HTMLTextAreaElement>;
11
- }
12
- export declare function SqlEditor({ value, onValueChange, onRun, label, placeholder, className, ref }: SqlEditorProps): import("react").JSX.Element;
1
+ import type { QueryConsoleEditorProps } from './editor-slot.ts';
2
+ export declare function SqlEditor({ ref, value, onValueChange, run, label, placeholder, autoFocus }: QueryConsoleEditorProps): import("react").JSX.Element;
@@ -1,14 +1,25 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { useEffect, useRef } from 'react';
2
+ import { useEffect, useImperativeHandle, useRef } from 'react';
3
+ import clsx from 'clsx';
3
4
  import { isApplePlatform, matchesShortcut, TextArea } from '@adecore/ui';
5
+ import { CODE_TEXT } from '../code-text.js';
4
6
  import { applyEdit, indentEdit, newlineEdit, outdentEdit } from './editing.js';
5
7
  import { RUN_ALL_SHORTCUT, RUN_SHORTCUT } from './shortcuts.js';
6
8
  /*
7
- * Where the SQL is typed. A plain text area for now, kept to itself so an editor with highlighting
8
- * and completion can take its place without the console noticing: it needs only these props.
9
+ * Where the SQL is typed when the app brings no editor of its own: a plain text area that fills the
10
+ * height it gets, with the same props an editor of the app receives.
9
11
  */
10
- export function SqlEditor({ value, onValueChange, onRun, label, placeholder, className, ref }) {
12
+ export function SqlEditor({ ref, value, onValueChange, run, label, placeholder, autoFocus }) {
13
+ const area = useRef(null);
11
14
  const pendingSelection = useRef(null);
15
+ useImperativeHandle(ref, () => ({ selection: () => ({ start: area.current?.selectionStart ?? 0, end: area.current?.selectionEnd ?? 0 }) }), []);
16
+ useEffect(() => {
17
+ if (autoFocus) {
18
+ area.current?.focus();
19
+ }
20
+ // Only on mount: a console that later becomes active keeps the focus where the person put it.
21
+ // eslint-disable-next-line react/exhaustive-deps
22
+ }, []);
12
23
  useEffect(() => {
13
24
  const pending = pendingSelection.current;
14
25
  if (pending !== null) {
@@ -17,45 +28,45 @@ export function SqlEditor({ value, onValueChange, onRun, label, placeholder, cla
17
28
  }
18
29
  }, [value]);
19
30
  // Through the browser's own insert, so Cmd+Z undoes the edit; the fallback sets the value itself.
20
- const apply = (area, edit) => {
21
- const before = area.value;
22
- area.setSelectionRange(edit.from, edit.to);
31
+ const apply = (target, edit) => {
32
+ const before = target.value;
33
+ target.setSelectionRange(edit.from, edit.to);
23
34
  const inserted = document.execCommand('insertText', false, edit.insert);
24
- if (inserted && area.value === applyEdit(before, edit)) {
25
- area.setSelectionRange(edit.selectionStart, edit.selectionEnd);
35
+ if (inserted && target.value === applyEdit(before, edit)) {
36
+ target.setSelectionRange(edit.selectionStart, edit.selectionEnd);
26
37
  return;
27
38
  }
28
- pendingSelection.current = { area, start: edit.selectionStart, end: edit.selectionEnd };
39
+ pendingSelection.current = { area: target, start: edit.selectionStart, end: edit.selectionEnd };
29
40
  onValueChange(applyEdit(before, edit));
30
41
  };
31
42
  const handleKeyDown = (event) => {
32
43
  const apple = isApplePlatform();
33
44
  if (matchesShortcut(RUN_ALL_SHORTCUT, event, apple)) {
34
45
  event.preventDefault();
35
- onRun('all');
46
+ run('all');
36
47
  return;
37
48
  }
38
49
  if (matchesShortcut(RUN_SHORTCUT, event, apple)) {
39
50
  event.preventDefault();
40
- onRun('selection-or-statement');
51
+ run('selection-or-statement');
41
52
  return;
42
53
  }
43
54
  if (event.nativeEvent.isComposing || event.metaKey || event.ctrlKey || event.altKey) {
44
55
  return;
45
56
  }
46
- const area = event.currentTarget;
47
- const { selectionStart, selectionEnd } = area;
57
+ const target = event.currentTarget;
58
+ const { selectionStart, selectionEnd } = target;
48
59
  if (event.key === 'Tab') {
49
60
  event.preventDefault();
50
- const edit = event.shiftKey ? outdentEdit(area.value, selectionStart, selectionEnd) : indentEdit(area.value, selectionStart, selectionEnd);
61
+ const edit = event.shiftKey ? outdentEdit(target.value, selectionStart, selectionEnd) : indentEdit(target.value, selectionStart, selectionEnd);
51
62
  if (edit !== null) {
52
- apply(area, edit);
63
+ apply(target, edit);
53
64
  }
54
65
  }
55
66
  else if (event.key === 'Enter' && !event.shiftKey) {
56
67
  event.preventDefault();
57
- apply(area, newlineEdit(area.value, selectionStart, selectionEnd));
68
+ apply(target, newlineEdit(target.value, selectionStart, selectionEnd));
58
69
  }
59
70
  };
60
- return (_jsx(TextArea, { ref: ref, mono: true, resize: "vertical", "aria-label": label, placeholder: placeholder, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", rows: 8, value: value, className: className, onChange: (event) => onValueChange(event.target.value), onKeyDown: handleKeyDown }));
71
+ return (_jsx(TextArea, { ref: area, "aria-label": label, placeholder: placeholder, spellCheck: false, autoCapitalize: "off", autoCorrect: "off", value: value, className: clsx(CODE_TEXT, 'flex-1'), onChange: (event) => onValueChange(event.target.value), onKeyDown: handleKeyDown }));
61
72
  }
@@ -1,4 +1,6 @@
1
1
  import type { Engine, FileFormat, StatementResult } from '../protocol/index.ts';
2
+ import type { Reference } from '../table/references.ts';
3
+ import type { ResultKeys } from './result-keys.ts';
2
4
  export interface ResultPager {
3
5
  readonly page: number;
4
6
  readonly loading: boolean;
@@ -14,5 +16,7 @@ export interface StatementResultViewProps {
14
16
  engine?: Engine;
15
17
  valuePanelOpen?: boolean;
16
18
  onValuePanelOpenChange?(open: boolean): void;
19
+ keys?: ResultKeys | null;
20
+ onFollowReference?(reference: Reference): void;
17
21
  }
18
- export declare function StatementResultView({ result, offset, pager, onExport, exporting, engine, valuePanelOpen, onValuePanelOpenChange }: StatementResultViewProps): import("react").JSX.Element;
22
+ export declare function StatementResultView({ result, offset, pager, onExport, exporting, engine, valuePanelOpen, onValuePanelOpenChange, keys, onFollowReference }: StatementResultViewProps): import("react").JSX.Element;
@@ -1,8 +1,8 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
- import { ChevronLeft, ChevronRight, CircleAlert, Download, PanelRight } from 'lucide-react';
3
+ import { ArrowUpRight, ChevronLeft, ChevronRight, CircleAlert, Download, PanelRight } from 'lucide-react';
4
4
  import { useTranslation } from 'react-i18next';
5
- import { Banner, IconButton, Menu, Spinner } from '@adecore/ui';
5
+ import { Banner, ButtonGroup, ContextMenu, Icon, IconButton, Menu, Spinner } from '@adecore/ui';
6
6
  import { formatDecimal, formatNumber } from '@adecore/ui/format';
7
7
  import { DataGrid } from '../grid/DataGrid.js';
8
8
  import { previewValueOf } from '../grid/focused-value.js';
@@ -12,7 +12,7 @@ import { ValuePanel } from '../value/ValuePanel.js';
12
12
  const RESULT_TABLE = 'result';
13
13
  const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
14
14
  /* One statement's outcome: a result set in a read-only grid, a count of affected rows, or the error the server gave. */
15
- export function StatementResultView({ result, offset = 0, pager, onExport, exporting = false, engine, valuePanelOpen = false, onValuePanelOpenChange }) {
15
+ export function StatementResultView({ result, offset = 0, pager, onExport, exporting = false, engine, valuePanelOpen = false, onValuePanelOpenChange, keys = null, onFollowReference }) {
16
16
  const { t } = useTranslation('database');
17
17
  const [focus, setFocus] = useState(null);
18
18
  const rows = useMemo(() => (result.kind === 'rows' ? result.rows.map((cells, index) => ({ key: `row:${index}`, number: offset + index + 1, cells })) : []), [result, offset]);
@@ -24,9 +24,22 @@ export function StatementResultView({ result, offset = 0, pager, onExport, expor
24
24
  if (result.kind === 'done') {
25
25
  return (_jsxs("div", { className: "flex flex-col gap-1 p-4 text-sm text-text", children: [_jsx("p", { children: t('console.affected', { count: result.affected, formatted: formatNumber(result.affected) }) }), result.lastInsertId !== null && _jsx("p", { className: "text-xs text-text-muted", children: t('console.lastInsertId', { id: String(result.lastInsertId) }) }), _jsx("p", { className: "text-xs text-text-muted tabular-nums", children: elapsed })] }));
26
26
  }
27
+ const referenceAt = (cell) => {
28
+ const row = rows.find((candidate) => candidate.key === cell.rowKey);
29
+ return keys === null || row === undefined ? null : keys.referenceAt(cell.column, row.cells);
30
+ };
31
+ const follow = keys === null || onFollowReference === undefined
32
+ ? undefined
33
+ : (cell) => {
34
+ const reference = referenceAt(cell);
35
+ if (reference !== null) {
36
+ onFollowReference(reference);
37
+ }
38
+ };
39
+ const referenceMenu = (context) => follow === undefined || context.cell === null || keys?.isReference(context.cell.column) !== true ? null : (_jsxs(ContextMenu.Item, { disabled: referenceAt(context.cell) === null, onClick: () => context.cell !== null && follow(context.cell), children: [_jsx(Icon, { icon: ArrowUpRight, size: 14 }), t('table.goToReferenced')] }));
27
40
  const focusedRow = valuePanelOpen && focus !== null ? rows.find((row) => row.key === focus.rowKey) : undefined;
28
41
  const focusedCell = focus === null ? undefined : focusedRow?.cells[focus.column];
29
- return (_jsxs(_Fragment, { children: [_jsx(ValueDock, { open: valuePanelOpen, panel: _jsx(ValuePanel, { column: focus === null ? null : (result.columns[focus.column] ?? null), value: focusedCell === undefined ? undefined : previewValueOf(focusedCell), loading: false, editable: false, onClose: () => onValuePanelOpenChange?.(false), className: "min-w-0 flex-1" }), children: _jsx(DataGrid, { label: t('console.result'), columns: result.columns, rows: rows, empty: t('console.noRows'), sqlTarget: sqlTarget, onFocusedCellChange: setFocus }) }), _jsxs("footer", { className: "flex h-10 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-text-muted", children: [_jsx("span", { className: "text-text tabular-nums", children: offset > 0 || pager !== undefined
42
+ return (_jsxs(_Fragment, { children: [_jsx(ValueDock, { open: valuePanelOpen, panel: _jsx(ValuePanel, { column: focus === null ? null : (result.columns[focus.column] ?? null), value: focusedCell === undefined ? undefined : previewValueOf(focusedCell), loading: false, editable: false, onClose: () => onValuePanelOpenChange?.(false), className: "min-w-0 flex-1" }), children: _jsx(DataGrid, { label: t('console.result'), columns: keys?.columns ?? result.columns, rows: rows, empty: t('console.noRows'), sqlTarget: sqlTarget, onFocusedCellChange: setFocus, onFollow: follow, canFollow: (cell) => referenceAt(cell) !== null, menu: referenceMenu }) }), _jsxs("footer", { className: "flex h-10 shrink-0 items-center gap-4 border-t border-border px-3 text-xs text-text-muted", children: [_jsx("span", { className: "text-text tabular-nums", children: offset > 0 || pager !== undefined
30
43
  ? t('console.rowRange', { from: formatNumber(offset + Math.min(1, rows.length)), to: formatNumber(offset + rows.length) })
31
- : t('console.rows', { rows: formatNumber(result.rows.length) }) }), pager === undefined && result.hasMore && _jsx("span", { children: t('console.hasMore') }), _jsx("span", { className: "tabular-nums", children: elapsed }), _jsxs("span", { className: "ml-auto flex items-center gap-1", children: [pager !== undefined && (_jsxs(_Fragment, { children: [pager.loading && _jsx(Spinner, { size: 14, label: t('console.loading') }), _jsx(IconButton, { icon: ChevronLeft, size: "sm", label: t('console.previousPage'), disabled: pager.page <= 1 || pager.loading, onClick: pager.onPrevious }), _jsx("span", { className: "min-w-12 text-center tabular-nums", children: t('console.page', { page: formatNumber(pager.page) }) }), _jsx(IconButton, { icon: ChevronRight, size: "sm", label: t('console.nextPage'), disabled: !result.hasMore || pager.loading, onClick: pager.onNext })] })), onExport !== undefined && (_jsxs(Menu.Root, { children: [_jsx(IconButton, { icon: Download, size: "sm", label: t('console.export'), disabled: exporting, render: _jsx(Menu.Trigger, {}) }), _jsx(Menu.Popup, { children: EXPORT_FORMATS.map((format) => (_jsx(Menu.Item, { onClick: () => onExport(format), children: t('console.exportAs', { format: format.toUpperCase() }) }, format))) })] })), onValuePanelOpenChange !== undefined && (_jsx(IconButton, { icon: PanelRight, size: "sm", label: t('console.valuePanel'), "aria-pressed": valuePanelOpen, onClick: () => onValuePanelOpenChange(!valuePanelOpen) }))] })] })] }));
44
+ : t('console.rows', { rows: formatNumber(result.rows.length) }) }), pager === undefined && result.hasMore && _jsx("span", { children: t('console.hasMore') }), _jsx("span", { className: "tabular-nums", children: elapsed }), _jsxs("span", { className: "ml-auto flex items-center gap-1", children: [pager !== undefined && (_jsxs(_Fragment, { children: [pager.loading && _jsx(Spinner, { size: 14, label: t('console.loading') }), _jsx(IconButton, { icon: ChevronLeft, size: "sm", label: t('console.previousPage'), disabled: pager.page <= 1 || pager.loading, onClick: pager.onPrevious }), _jsx("span", { className: "min-w-12 text-center tabular-nums", children: t('console.page', { page: formatNumber(pager.page) }) }), _jsx(IconButton, { icon: ChevronRight, size: "sm", label: t('console.nextPage'), disabled: !result.hasMore || pager.loading, onClick: pager.onNext })] })), _jsxs(ButtonGroup, { children: [onExport !== undefined && (_jsxs(Menu.Root, { children: [_jsx(IconButton, { icon: Download, size: "sm", label: t('console.export'), disabled: exporting, render: _jsx(Menu.Trigger, {}) }), _jsx(Menu.Popup, { children: EXPORT_FORMATS.map((format) => (_jsx(Menu.Item, { onClick: () => onExport(format), children: t('console.exportAs', { format: format.toUpperCase() }) }, format))) })] })), onValuePanelOpenChange !== undefined && (_jsx(IconButton, { icon: PanelRight, size: "sm", label: t('console.valuePanel'), "aria-pressed": valuePanelOpen, onClick: () => onValuePanelOpenChange(!valuePanelOpen) }))] })] })] })] }));
32
45
  }
@@ -1,12 +1,9 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import clsx from 'clsx';
3
3
  import { useTranslation } from 'react-i18next';
4
- import { Button, Pill, Segmented, Tooltip } from '@adecore/ui';
5
- /* Auto or manual commits, and while a transaction is open, the way to end it. */
4
+ import { Button, Pill, Switch, Tooltip } from '@adecore/ui';
5
+ /* Auto-commit on or off, and while a transaction is open, the way to end it. */
6
6
  export function TransactionControls({ mode, onModeChange, open, busy, onCommit, onRollback, className, ref }) {
7
7
  const { t } = useTranslation('database');
8
- return (_jsxs("div", { ref: ref, className: clsx('flex items-center gap-2', className), children: [_jsx(Tooltip, { label: t('console.transaction.hint'), children: _jsx("div", { className: "flex", children: _jsx(Segmented, { value: mode, onValueChange: onModeChange, disabled: busy, label: t('console.transaction.mode'), options: [
9
- { id: 'auto', label: t('console.transaction.auto') },
10
- { id: 'manual', label: t('console.transaction.manual') }
11
- ] }) }) }), open && (_jsxs(_Fragment, { children: [_jsx(Pill, { tone: "needsYou", children: t('console.transaction.open') }), _jsx(Button, { size: "xs", variant: "secondary", disabled: busy, onClick: onCommit, children: t('console.transaction.commit') }), _jsx(Button, { size: "xs", variant: "danger-outline", disabled: busy, onClick: onRollback, children: t('console.transaction.rollback') })] }))] }));
8
+ return (_jsxs("div", { ref: ref, className: clsx('flex items-center gap-2', className), children: [_jsx(Tooltip, { label: t('console.transaction.hint'), children: _jsxs("div", { className: "flex items-center gap-2 text-xs text-text", children: [_jsx(Switch, { checked: mode === 'auto', label: t('console.transaction.autoCommit'), disabled: busy, onCheckedChange: (auto) => onModeChange(auto ? 'auto' : 'manual') }), _jsx("span", { children: t('console.transaction.autoCommit') })] }) }), open && (_jsxs(_Fragment, { children: [_jsx(Pill, { tone: "needsYou", children: t('console.transaction.open') }), _jsx(Button, { size: "xs", variant: "secondary", disabled: busy, onClick: onCommit, children: t('console.transaction.commit') }), _jsx(Button, { size: "xs", variant: "danger-outline", disabled: busy, onClick: onRollback, children: t('console.transaction.rollback') })] }))] }));
12
9
  }
@@ -0,0 +1,18 @@
1
+ import type { Ref } from 'react';
2
+ export type QueryConsoleRunScope = 'selection-or-statement' | 'all';
3
+ export interface QueryConsoleEditorHandle {
4
+ selection(): {
5
+ readonly start: number;
6
+ readonly end: number;
7
+ };
8
+ }
9
+ export interface QueryConsoleEditorProps {
10
+ ref: Ref<QueryConsoleEditorHandle>;
11
+ value: string;
12
+ onValueChange(sql: string): void;
13
+ run(scope: QueryConsoleRunScope): void;
14
+ busy: boolean;
15
+ label: string;
16
+ placeholder: string;
17
+ autoFocus: boolean;
18
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,15 @@
1
+ import type { DatabaseSession } from '../client/types.ts';
2
+ import type { Shown } from '../grid/display.ts';
3
+ import type { GridColumn } from '../grid/types.ts';
4
+ import type { ColumnSource, Engine, ResultColumn, StatementResult, TableStructure } from '../protocol/index.ts';
5
+ import { type Reference } from '../table/references.ts';
6
+ export interface ResultKeys {
7
+ readonly columns: readonly GridColumn[];
8
+ isReference(column: number): boolean;
9
+ referenceAt(column: number, cells: readonly Shown[]): Reference | null;
10
+ }
11
+ type TableName = Pick<ColumnSource, 'schema' | 'table'>;
12
+ export declare const sourceTablesOf: (columns: readonly ResultColumn[]) => TableName[];
13
+ export declare const resultKeysOf: (engine: Engine, columns: readonly ResultColumn[], structures: ReadonlyMap<string, TableStructure>) => ResultKeys;
14
+ export declare function useResultKeys(session: DatabaseSession | null, engine: Engine | undefined, result: StatementResult | undefined): ResultKeys | null;
15
+ export {};