@adecore/database 0.38.0 → 0.40.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/dist/console/QueryConsole.js +2 -10
- package/dist/console/SchemaPicker.js +0 -1
- package/dist/console/StatementResultView.js +1 -1
- package/dist/ddl/diff.js +1 -1
- package/dist/explorer/tree.js +0 -1
- package/dist/export-formats.d.ts +2 -0
- package/dist/export-formats.js +1 -0
- package/dist/grid/CopyAsMenu.js +0 -1
- package/dist/grid/DataGrid.js +10 -10
- package/dist/grid/column-layout.d.ts +0 -1
- package/dist/grid/column-layout.js +1 -2
- package/dist/grid/column-view.js +3 -2
- package/dist/grid/display.js +1 -2
- package/dist/host/host.js +1 -2
- package/dist/host/validate.d.ts +3 -0
- package/dist/host/validate.js +18 -15
- package/dist/same-list.d.ts +1 -0
- package/dist/same-list.js +1 -0
- package/dist/stored-size.d.ts +1 -0
- package/dist/stored-size.js +10 -0
- package/dist/table/TableToolbar.js +1 -1
- package/dist/table/TableView.js +2 -10
- package/package.json +15 -15
- package/src/DatabaseProvider.tsx +2 -0
- package/src/KeyIcon.tsx +1 -0
- package/src/client/client.ts +13 -0
- package/src/client-context.ts +2 -0
- package/src/connections/ConnectionForm.tsx +11 -0
- package/src/connections/ConnectionManager.tsx +9 -0
- package/src/connections/connection-config.ts +21 -0
- package/src/connections/docker.ts +2 -0
- package/src/connections/use-docker-containers.ts +3 -0
- package/src/console/DestructiveDialog.tsx +1 -0
- package/src/console/QueryConsole.tsx +38 -10
- package/src/console/SchemaPicker.tsx +1 -1
- package/src/console/SqlEditor.tsx +10 -0
- package/src/console/StatementResultView.tsx +7 -2
- package/src/console/TransactionControls.tsx +1 -0
- package/src/console/destructive.ts +11 -0
- package/src/console/editing.ts +10 -0
- package/src/console/history.ts +8 -0
- package/src/console/labels.ts +1 -0
- package/src/console/outcome.ts +1 -0
- package/src/console/result-keys.ts +16 -0
- package/src/console/shortcuts.ts +2 -0
- package/src/ddl/alter-mysql.ts +16 -0
- package/src/ddl/alter-sqlite.ts +21 -0
- package/src/ddl/alter.ts +2 -0
- package/src/ddl/column.ts +11 -0
- package/src/ddl/create.ts +15 -0
- package/src/ddl/dialect.ts +4 -0
- package/src/ddl/diff.ts +22 -2
- package/src/ddl/draft.ts +1 -0
- package/src/ddl/parse.ts +31 -0
- package/src/ddl/types.ts +3 -0
- package/src/ddl/validate.ts +12 -0
- package/src/designer/ColumnsEditor.tsx +3 -0
- package/src/designer/DesignerView.tsx +2 -0
- package/src/designer/ForeignKeysEditor.tsx +4 -0
- package/src/designer/IndexesEditor.tsx +1 -0
- package/src/designer/NamePicker.tsx +2 -0
- package/src/designer/OptionsEditor.tsx +4 -0
- package/src/designer/SqlPreview.tsx +2 -0
- package/src/designer/TableDesigner.tsx +14 -0
- package/src/designer/edit.ts +13 -0
- package/src/designer/plan.ts +3 -0
- package/src/designer/recover.ts +1 -0
- package/src/designer/useReferenceColumns.ts +5 -0
- package/src/explorer/DatabaseExplorer.tsx +40 -0
- package/src/explorer/RowMenu.tsx +2 -0
- package/src/explorer/TableDialog.tsx +6 -0
- package/src/explorer/expansion.ts +8 -0
- package/src/explorer/tree.ts +73 -2
- package/src/explorer/use-explorer-loads.ts +15 -0
- package/src/explorer/use-stored-expansion.ts +4 -0
- package/src/export-formats.ts +3 -0
- package/src/grid/CellEditor.tsx +1 -0
- package/src/grid/CopyAsMenu.tsx +1 -1
- package/src/grid/DataGrid.tsx +93 -23
- package/src/grid/EnumPicker.tsx +6 -0
- package/src/grid/GridHeaderCell.tsx +11 -0
- package/src/grid/GridRowView.tsx +3 -0
- package/src/grid/RecordView.tsx +19 -0
- package/src/grid/ValueDock.tsx +2 -0
- package/src/grid/aggregates.ts +12 -0
- package/src/grid/column-layout.ts +12 -4
- package/src/grid/column-selection.ts +9 -0
- package/src/grid/column-view.ts +6 -2
- package/src/grid/copy-formats.ts +13 -0
- package/src/grid/display.ts +17 -3
- package/src/grid/edit-value.ts +11 -0
- package/src/grid/enum-type.ts +15 -0
- package/src/grid/focused-value.ts +4 -0
- package/src/grid/layout.ts +12 -0
- package/src/grid/navigation.ts +5 -0
- package/src/grid/row-selection.ts +7 -0
- package/src/grid/sort.ts +1 -0
- package/src/grid/whole-cells.ts +8 -0
- package/src/host/host.ts +16 -3
- package/src/host/spawn.ts +2 -0
- package/src/host/validate.ts +37 -15
- package/src/same-list.ts +2 -0
- package/src/sql-split.ts +28 -0
- package/src/sql.ts +20 -0
- package/src/stored-size.ts +10 -0
- package/src/structure/StructureView.tsx +18 -0
- package/src/table/CommandField.tsx +11 -0
- package/src/table/ImportDialog.tsx +4 -0
- package/src/table/TableStatusBar.tsx +1 -0
- package/src/table/TableToolbar.tsx +2 -2
- package/src/table/TableView.tsx +50 -10
- package/src/table/command-field.ts +40 -0
- package/src/table/grid-rows.ts +13 -0
- package/src/table/import-mapping.ts +5 -0
- package/src/table/layout-store.ts +7 -0
- package/src/table/paging.ts +1 -0
- package/src/table/pending.ts +18 -0
- package/src/table/references.ts +4 -0
- package/src/table/useLoaded.ts +3 -0
- package/src/table/useStoredLayout.ts +2 -0
- package/src/table/useTableTransfer.ts +17 -1
- package/src/testing/fake.ts +24 -0
- package/src/use-database-resources.ts +1 -0
- package/src/use-popup-press.ts +3 -0
- package/src/value/ValueEditor.tsx +6 -0
- package/src/value/draft.ts +7 -0
- package/src/value/hex.ts +8 -0
- package/src/value/json.ts +16 -0
- package/src/value/views.ts +14 -0
- package/src/workbench/DatabaseWorkbench.tsx +22 -0
- package/src/workbench/tabs.ts +34 -0
|
@@ -100,6 +100,7 @@ export function DatabaseExplorer({
|
|
|
100
100
|
versions: loads.versions,
|
|
101
101
|
folders: (connectionId: string) => folders?.[connectionId] ?? []
|
|
102
102
|
};
|
|
103
|
+
|
|
103
104
|
const rows = flattenTree({ ...input, filter });
|
|
104
105
|
const stop = tabStop(rows, activeKey, selectedKey);
|
|
105
106
|
|
|
@@ -118,15 +119,19 @@ export function DatabaseExplorer({
|
|
|
118
119
|
|
|
119
120
|
const setOpen = (key: string, open: boolean): void => {
|
|
120
121
|
const folder = startsOpen(key);
|
|
122
|
+
|
|
121
123
|
const toggle = (current: ReadonlySet<string>): ReadonlySet<string> => {
|
|
122
124
|
const next = new Set(current);
|
|
125
|
+
|
|
123
126
|
if (open !== folder) {
|
|
124
127
|
next.add(key);
|
|
125
128
|
} else {
|
|
126
129
|
next.delete(key);
|
|
127
130
|
}
|
|
131
|
+
|
|
128
132
|
return next;
|
|
129
133
|
};
|
|
134
|
+
|
|
130
135
|
(folder ? setCollapsed : setExpanded)(toggle);
|
|
131
136
|
};
|
|
132
137
|
|
|
@@ -138,11 +143,13 @@ export function DatabaseExplorer({
|
|
|
138
143
|
if (value === undefined) {
|
|
139
144
|
setOwn(selection);
|
|
140
145
|
}
|
|
146
|
+
|
|
141
147
|
onValueChange?.(selection);
|
|
142
148
|
};
|
|
143
149
|
|
|
144
150
|
const pick = (row: TreeRow): void => {
|
|
145
151
|
const selection = selectionOf(row);
|
|
152
|
+
|
|
146
153
|
if (selection !== null) {
|
|
147
154
|
select(selection);
|
|
148
155
|
}
|
|
@@ -152,12 +159,16 @@ export function DatabaseExplorer({
|
|
|
152
159
|
const open = (row: TreeRow, preview = false): void => {
|
|
153
160
|
if (row.kind === 'appItem') {
|
|
154
161
|
row.item.onOpen?.({ preview });
|
|
162
|
+
|
|
155
163
|
return;
|
|
156
164
|
}
|
|
165
|
+
|
|
157
166
|
const ref = openedTableOf(row);
|
|
167
|
+
|
|
158
168
|
if (ref === null) {
|
|
159
169
|
return;
|
|
160
170
|
}
|
|
171
|
+
|
|
161
172
|
pick(row);
|
|
162
173
|
const tableKind = row.kind === 'table' && row.ref === ref ? row.table.kind : tableKindIn(loads.tables(ref.connectionId, ref.schema), ref.table);
|
|
163
174
|
act?.({ kind: 'open-table', ref, view: 'data', ...(tableKind === undefined ? {} : { tableKind }), ...(openOnClick ? { preview } : {}) });
|
|
@@ -170,6 +181,7 @@ export function DatabaseExplorer({
|
|
|
170
181
|
case 'entry':
|
|
171
182
|
case 'appItem':
|
|
172
183
|
open(row);
|
|
184
|
+
|
|
173
185
|
break;
|
|
174
186
|
case 'connection':
|
|
175
187
|
case 'schema':
|
|
@@ -178,9 +190,11 @@ export function DatabaseExplorer({
|
|
|
178
190
|
case 'appFolder':
|
|
179
191
|
pick(row);
|
|
180
192
|
toggle(row);
|
|
193
|
+
|
|
181
194
|
break;
|
|
182
195
|
case 'error':
|
|
183
196
|
loads.reset(row.retry);
|
|
197
|
+
|
|
184
198
|
break;
|
|
185
199
|
}
|
|
186
200
|
};
|
|
@@ -189,16 +203,21 @@ export function DatabaseExplorer({
|
|
|
189
203
|
if ((e.target as HTMLElement).tagName === 'BUTTON') {
|
|
190
204
|
return;
|
|
191
205
|
}
|
|
206
|
+
|
|
192
207
|
const action = navigate(rows, activeKey ?? stop, e.key);
|
|
208
|
+
|
|
193
209
|
if (action === null) {
|
|
194
210
|
return;
|
|
195
211
|
}
|
|
212
|
+
|
|
196
213
|
e.preventDefault();
|
|
214
|
+
|
|
197
215
|
if (action.type === 'focus') {
|
|
198
216
|
focusAfterRender.current = true;
|
|
199
217
|
setActiveKey(action.key);
|
|
200
218
|
} else if (action.type === 'activate') {
|
|
201
219
|
const row = rows.find((candidate) => candidate.key === action.key);
|
|
220
|
+
|
|
202
221
|
if (row !== undefined) {
|
|
203
222
|
activate(row);
|
|
204
223
|
}
|
|
@@ -212,14 +231,19 @@ export function DatabaseExplorer({
|
|
|
212
231
|
if (pressedInPopup()) {
|
|
213
232
|
return;
|
|
214
233
|
}
|
|
234
|
+
|
|
215
235
|
setActiveKey(row.key);
|
|
236
|
+
|
|
216
237
|
if (openOnClick && (row.kind === 'table' || row.kind === 'appItem')) {
|
|
217
238
|
if (count <= 1) {
|
|
218
239
|
open(row, true);
|
|
219
240
|
}
|
|
241
|
+
|
|
220
242
|
return;
|
|
221
243
|
}
|
|
244
|
+
|
|
222
245
|
pick(row);
|
|
246
|
+
|
|
223
247
|
if (row.kind !== 'table' && isExpandable(row)) {
|
|
224
248
|
toggle(row);
|
|
225
249
|
} else if (row.kind === 'error') {
|
|
@@ -236,6 +260,7 @@ export function DatabaseExplorer({
|
|
|
236
260
|
const finish = (done: TableRequest, to: string | undefined): void => {
|
|
237
261
|
setRequest(null);
|
|
238
262
|
const { connectionId, schema, table } = done.ref;
|
|
263
|
+
|
|
239
264
|
if (selected?.connectionId === connectionId && selected.schema === schema && selected.table === table) {
|
|
240
265
|
select(to === undefined ? { connectionId, schema } : { connectionId, schema, table: to });
|
|
241
266
|
}
|
|
@@ -243,6 +268,7 @@ export function DatabaseExplorer({
|
|
|
243
268
|
|
|
244
269
|
const connectionOf = (row: TreeRow): Connection | undefined => {
|
|
245
270
|
const selection = selectionOf(row);
|
|
271
|
+
|
|
246
272
|
return selection === null ? undefined : connections.find((entry) => entry.id === selection.connectionId);
|
|
247
273
|
};
|
|
248
274
|
|
|
@@ -250,14 +276,17 @@ export function DatabaseExplorer({
|
|
|
250
276
|
|
|
251
277
|
const renderRow = (row: TreeRow, index: number): ReactNode => {
|
|
252
278
|
const connection = connectionOf(row);
|
|
279
|
+
|
|
253
280
|
const menu =
|
|
254
281
|
row.kind === 'appItem' ? (
|
|
255
282
|
(row.item.menu ?? null)
|
|
256
283
|
) : connection !== undefined && hasMenu(row) ? (
|
|
257
284
|
<RowMenu row={row} connection={connection} onRefresh={loads.reset} onDisconnect={disconnect} onRequest={setRequest} />
|
|
258
285
|
) : null;
|
|
286
|
+
|
|
259
287
|
const selectedRow = isSelected(row);
|
|
260
288
|
const draggable = row.kind === 'table' && onTableDragStart !== undefined;
|
|
289
|
+
|
|
261
290
|
const props = {
|
|
262
291
|
level: row.level,
|
|
263
292
|
selected: selectedRow,
|
|
@@ -288,7 +317,9 @@ export function DatabaseExplorer({
|
|
|
288
317
|
}
|
|
289
318
|
}
|
|
290
319
|
};
|
|
320
|
+
|
|
291
321
|
const content = <RowContent row={row} onToggle={isExpandable(row) ? () => toggle(row) : undefined} />;
|
|
322
|
+
|
|
292
323
|
if (menu === null) {
|
|
293
324
|
return (
|
|
294
325
|
<Tree.Row key={row.key} {...props}>
|
|
@@ -296,6 +327,7 @@ export function DatabaseExplorer({
|
|
|
296
327
|
</Tree.Row>
|
|
297
328
|
);
|
|
298
329
|
}
|
|
330
|
+
|
|
299
331
|
return (
|
|
300
332
|
<ContextMenu.Root key={row.key}>
|
|
301
333
|
<ContextMenu.Trigger render={<Tree.Row {...props} />}>{content}</ContextMenu.Trigger>
|
|
@@ -339,6 +371,7 @@ export function DatabaseExplorer({
|
|
|
339
371
|
|
|
340
372
|
function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void }) {
|
|
341
373
|
const { t } = useTranslation('database');
|
|
374
|
+
|
|
342
375
|
switch (row.kind) {
|
|
343
376
|
case 'connection':
|
|
344
377
|
return (
|
|
@@ -446,6 +479,7 @@ const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables'
|
|
|
446
479
|
function SchemaCount({ shown, total }: { shown: number; total: number }) {
|
|
447
480
|
const { t } = useTranslation('database');
|
|
448
481
|
const counts = { shown: formatNumber(shown), total: formatNumber(total) };
|
|
482
|
+
|
|
449
483
|
return (
|
|
450
484
|
<Tooltip label={t('explorer.schemaCountHint', { count: total, ...counts })}>
|
|
451
485
|
<span className="shrink-0 text-text-faint tabular-nums">{t('explorer.schemaCount', counts)}</span>
|
|
@@ -458,6 +492,7 @@ const ENTRY_ICON_SLOT = 'inline-flex size-4 shrink-0 items-center justify-center
|
|
|
458
492
|
|
|
459
493
|
function ColumnIcon(keys: ColumnKeys) {
|
|
460
494
|
const { t } = useTranslation('database');
|
|
495
|
+
|
|
461
496
|
if (!keys.primaryKey && !keys.foreignKey) {
|
|
462
497
|
return (
|
|
463
498
|
<span className={ENTRY_ICON_SLOT}>
|
|
@@ -465,6 +500,7 @@ function ColumnIcon(keys: ColumnKeys) {
|
|
|
465
500
|
</span>
|
|
466
501
|
);
|
|
467
502
|
}
|
|
503
|
+
|
|
468
504
|
return (
|
|
469
505
|
<Tooltip label={t(keyLabelOf(keys))}>
|
|
470
506
|
<span className={ENTRY_ICON_SLOT}>
|
|
@@ -478,11 +514,13 @@ function ColumnIcon(keys: ColumnKeys) {
|
|
|
478
514
|
function EntryContent({ row }: { row: EntryRow }) {
|
|
479
515
|
const { t } = useTranslation('database');
|
|
480
516
|
const { entry } = row;
|
|
517
|
+
|
|
481
518
|
const icon = (glyph: LucideIcon, className: string): ReactNode => (
|
|
482
519
|
<span className={ENTRY_ICON_SLOT}>
|
|
483
520
|
<Icon icon={glyph} size={14} className={className} />
|
|
484
521
|
</span>
|
|
485
522
|
);
|
|
523
|
+
|
|
486
524
|
switch (entry.type) {
|
|
487
525
|
case 'key':
|
|
488
526
|
return (
|
|
@@ -496,6 +534,7 @@ function EntryContent({ row }: { row: EntryRow }) {
|
|
|
496
534
|
case 'foreignKey': {
|
|
497
535
|
const { foreignKey } = entry;
|
|
498
536
|
const columns = foreignKey.columns.join(', ');
|
|
537
|
+
|
|
499
538
|
return (
|
|
500
539
|
<>
|
|
501
540
|
<Tree.ChevronSlot />
|
|
@@ -544,6 +583,7 @@ function EntryContent({ row }: { row: EntryRow }) {
|
|
|
544
583
|
|
|
545
584
|
function ErrorContent({ row }: { row: ErrorRow }) {
|
|
546
585
|
const { t } = useTranslation('database');
|
|
586
|
+
|
|
547
587
|
return (
|
|
548
588
|
<>
|
|
549
589
|
<Tree.ChevronSlot />
|
package/src/explorer/RowMenu.tsx
CHANGED
|
@@ -47,6 +47,7 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest }:
|
|
|
47
47
|
|
|
48
48
|
const copyDdl = async (ref: TableRef): Promise<void> => {
|
|
49
49
|
const structure = await client.session(connection).structure(ref.schema, ref.table);
|
|
50
|
+
|
|
50
51
|
if (structure.ddl !== null) {
|
|
51
52
|
copyText(structure.ddl);
|
|
52
53
|
}
|
|
@@ -64,6 +65,7 @@ export function RowMenu({ row, connection, onRefresh, onDisconnect, onRequest }:
|
|
|
64
65
|
|
|
65
66
|
const tableGroups = (ref: TableRef, kind: TableKind): ReactNode[][] => {
|
|
66
67
|
const request = (change: TableRequest['change']) => (): void => onRequest({ change, connection, ref, kind });
|
|
68
|
+
|
|
67
69
|
return [
|
|
68
70
|
[
|
|
69
71
|
actionItem('open-data', Table, t('explorer.openData'), { kind: 'open-table', ref, view: 'data', tableKind: kind }),
|
|
@@ -32,13 +32,16 @@ export function TableDialog({ request, onClose, onDone }: TableDialogProps) {
|
|
|
32
32
|
const dialect = dialectOf(await session.server());
|
|
33
33
|
const { results } = await session.execute(statementOf(dialect, change, ref, kind, to));
|
|
34
34
|
const failed = results.find((result) => result.kind === 'error');
|
|
35
|
+
|
|
35
36
|
if (failed?.kind === 'error') {
|
|
36
37
|
throw new Error(failed.error.message);
|
|
37
38
|
}
|
|
39
|
+
|
|
38
40
|
// The client tells its listeners after a `RENAME`, a `DROP` and a `TRUNCATE`, but not after a SQLite emptying, which is a `DELETE FROM`.
|
|
39
41
|
if (change === 'truncate' && dialect.engine === 'sqlite') {
|
|
40
42
|
client.notifySchemaChange({ connectionId: ref.connectionId, schema: ref.schema });
|
|
41
43
|
}
|
|
44
|
+
|
|
42
45
|
onDone(request, to);
|
|
43
46
|
};
|
|
44
47
|
|
|
@@ -46,13 +49,16 @@ export function TableDialog({ request, onClose, onDone }: TableDialogProps) {
|
|
|
46
49
|
if (change === 'rename') {
|
|
47
50
|
if (typed === ref.table) {
|
|
48
51
|
onClose();
|
|
52
|
+
|
|
49
53
|
return;
|
|
50
54
|
}
|
|
55
|
+
|
|
51
56
|
await run(typed);
|
|
52
57
|
} else if (change === 'drop') {
|
|
53
58
|
if (typed !== ref.table) {
|
|
54
59
|
throw new Error(t('explorer.drop.mismatch'));
|
|
55
60
|
}
|
|
61
|
+
|
|
56
62
|
await run();
|
|
57
63
|
} else {
|
|
58
64
|
await run();
|
|
@@ -14,15 +14,20 @@ export const expansionKey = (connectionId: string): string => `database:explorer
|
|
|
14
14
|
/* Reads what `serializeExpansion` wrote. Anything that does not fit gives nothing open and no folder closed. */
|
|
15
15
|
export const parseExpansion = (raw: string | null, connectionId: string): Expansion => {
|
|
16
16
|
const none: Expansion = { expanded: new Set(), collapsed: new Set() };
|
|
17
|
+
|
|
17
18
|
if (raw === null) {
|
|
18
19
|
return none;
|
|
19
20
|
}
|
|
21
|
+
|
|
20
22
|
try {
|
|
21
23
|
const parsed: unknown = JSON.parse(raw);
|
|
24
|
+
|
|
22
25
|
if (typeof parsed !== 'object' || parsed === null || (parsed as { version?: unknown }).version !== VERSION) {
|
|
23
26
|
return none;
|
|
24
27
|
}
|
|
28
|
+
|
|
25
29
|
const { expanded, collapsed } = parsed as { expanded?: unknown; collapsed?: unknown };
|
|
30
|
+
|
|
26
31
|
const own = (keys: unknown, folders: boolean): Set<string> =>
|
|
27
32
|
new Set(
|
|
28
33
|
Array.isArray(keys)
|
|
@@ -32,6 +37,7 @@ export const parseExpansion = (raw: string | null, connectionId: string): Expans
|
|
|
32
37
|
)
|
|
33
38
|
: []
|
|
34
39
|
);
|
|
40
|
+
|
|
35
41
|
return { expanded: own(expanded, false), collapsed: own(collapsed, true) };
|
|
36
42
|
} catch {
|
|
37
43
|
return none;
|
|
@@ -41,7 +47,9 @@ export const parseExpansion = (raw: string | null, connectionId: string): Expans
|
|
|
41
47
|
/* What a connection has open as the text to store, or `null` when everything is as it starts, which removes the key. */
|
|
42
48
|
export const serializeExpansion = (connectionId: string, { expanded, collapsed }: Expansion): string | null => {
|
|
43
49
|
const own = (keys: ReadonlySet<string>): string[] => [...keys].filter((key) => connectionOfKey(key) === connectionId).sort();
|
|
50
|
+
|
|
44
51
|
const open = own(expanded);
|
|
45
52
|
const closed = own(collapsed);
|
|
53
|
+
|
|
46
54
|
return open.length === 0 && closed.length === 0 ? null : JSON.stringify({ version: VERSION, expanded: open, collapsed: closed });
|
|
47
55
|
};
|
package/src/explorer/tree.ts
CHANGED
|
@@ -35,31 +35,45 @@ export interface LoadTarget {
|
|
|
35
35
|
const SEPARATOR = '\u0000';
|
|
36
36
|
|
|
37
37
|
export const connectionKey = (connectionId: string): string => `c:${connectionId}`;
|
|
38
|
+
|
|
38
39
|
export const schemaKey = (connectionId: string, schema: string): string => `s:${connectionId}${SEPARATOR}${schema}`;
|
|
40
|
+
|
|
39
41
|
export const folderKey = (connectionId: string, schema: string, group: TableKind): string => `f:${connectionId}${SEPARATOR}${schema}${SEPARATOR}${group}`;
|
|
42
|
+
|
|
40
43
|
export const tableKey = (ref: TableRef): string => `t:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}`;
|
|
44
|
+
|
|
41
45
|
/* The folders of a table, in the order they come under it. */
|
|
42
46
|
export const PARTS = ['columns', 'keys', 'foreignKeys', 'indexes', 'checks', 'triggers'] as const;
|
|
47
|
+
|
|
43
48
|
export type TablePart = (typeof PARTS)[number];
|
|
49
|
+
|
|
44
50
|
export const partKey = (ref: TableRef, part: TablePart): string => `p:${ref.connectionId}${SEPARATOR}${ref.schema}${SEPARATOR}${ref.table}${SEPARATOR}${part}`;
|
|
51
|
+
|
|
45
52
|
/* A folder of a schema, and the columns of a table, are open until the person closes them; everything else is closed until they open it. */
|
|
46
53
|
export const startsOpen = (key: string): boolean => key.startsWith('f:') || (key.startsWith('p:') && key.endsWith(`${SEPARATOR}columns`));
|
|
54
|
+
|
|
47
55
|
/* The connection a key of a connection, schema, folder or table belongs to. */
|
|
48
56
|
export const connectionOfKey = (key: string): string => {
|
|
49
57
|
const rest = key.slice(2);
|
|
50
58
|
const end = rest.indexOf(SEPARATOR);
|
|
59
|
+
|
|
51
60
|
return end === -1 ? rest : rest.slice(0, end);
|
|
52
61
|
};
|
|
62
|
+
|
|
53
63
|
export const columnKey = (ref: TableRef, column: string): string => `${tableKey(ref)}${SEPARATOR}${column}`;
|
|
64
|
+
|
|
54
65
|
/* A folder of the app's own under a connection, and an item in it. */
|
|
55
66
|
export const appFolderKey = (connectionId: string, folder: string): string => `x:${connectionId}${SEPARATOR}${folder}`;
|
|
67
|
+
|
|
56
68
|
const appItemKey = (connectionId: string, folder: string, item: string): string => `${appFolderKey(connectionId, folder)}${SEPARATOR}${item}`;
|
|
69
|
+
|
|
57
70
|
const entryKey = (ref: TableRef, part: TablePart, index: number): string => `${partKey(ref, part)}${SEPARATOR}${index}`;
|
|
58
71
|
|
|
59
72
|
export const loadKey = (target: LoadTarget): string => {
|
|
60
73
|
if (target.schema === undefined) {
|
|
61
74
|
return connectionKey(target.connectionId);
|
|
62
75
|
}
|
|
76
|
+
|
|
63
77
|
return target.table === undefined
|
|
64
78
|
? schemaKey(target.connectionId, target.schema)
|
|
65
79
|
: tableKey({ connectionId: target.connectionId, schema: target.schema, table: target.table });
|
|
@@ -142,7 +156,6 @@ export type TableEntry =
|
|
|
142
156
|
|
|
143
157
|
export interface EntryRow extends RowBase {
|
|
144
158
|
readonly kind: 'entry';
|
|
145
|
-
/* The table the entry belongs to. */
|
|
146
159
|
readonly ref: TableRef;
|
|
147
160
|
readonly entry: TableEntry;
|
|
148
161
|
}
|
|
@@ -262,7 +275,6 @@ export interface TreeInput {
|
|
|
262
275
|
folders?(connectionId: string): readonly ExplorerFolder[];
|
|
263
276
|
}
|
|
264
277
|
|
|
265
|
-
/* The schemas a list shows. */
|
|
266
278
|
export const visibleSchemas = (schemas: readonly SchemaInfo[], showSystemSchemas: boolean): readonly SchemaInfo[] =>
|
|
267
279
|
showSystemSchemas ? schemas : schemas.filter((schema) => !schema.system);
|
|
268
280
|
|
|
@@ -299,11 +311,14 @@ const matches = (name: string, query: string): boolean => name.toLowerCase().inc
|
|
|
299
311
|
/* What each folder of a table besides its columns holds: the primary key and the unique keys, the foreign keys, the other indexes, the checks and the triggers. */
|
|
300
312
|
export const tableEntries = (structure: TableStructure): Readonly<Record<Exclude<TablePart, 'columns'>, readonly TableEntry[]>> => {
|
|
301
313
|
const primaryIndex = structure.indexes.find((index) => index.primary);
|
|
314
|
+
|
|
302
315
|
const primary: TableEntry[] =
|
|
303
316
|
structure.primaryKey.length === 0 ? [] : [{ type: 'key', name: primaryIndex?.name ?? null, columns: structure.primaryKey, primary: true }];
|
|
317
|
+
|
|
304
318
|
const unique = structure.indexes
|
|
305
319
|
.filter((index) => index.unique && !index.primary)
|
|
306
320
|
.map((index): TableEntry => ({ type: 'key', name: index.name, columns: index.columns, primary: false }));
|
|
321
|
+
|
|
307
322
|
return {
|
|
308
323
|
keys: [...primary, ...unique],
|
|
309
324
|
foreignKeys: structure.foreignKeys.map((foreignKey) => ({ type: 'foreignKey', foreignKey })),
|
|
@@ -324,6 +339,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
324
339
|
const columnRows = (ref: TableRef, structure: TableStructure, level: number, parent: string): Draft[] => {
|
|
325
340
|
const primary = new Set(structure.primaryKey);
|
|
326
341
|
const foreign = new Set(structure.foreignKeys.flatMap((foreignKey) => foreignKey.columns));
|
|
342
|
+
|
|
327
343
|
return structure.columns.map((column): Draft => ({
|
|
328
344
|
make: (place) => ({
|
|
329
345
|
kind: 'column',
|
|
@@ -344,24 +360,33 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
344
360
|
/* The folders a table opens into, each only when it holds something. A view has only its columns, straight under it. */
|
|
345
361
|
const tableChildren = (ref: TableRef, kind: TableKind, level: number, parent: string): Draft[] => {
|
|
346
362
|
const load = input.structures(ref.connectionId, ref.schema, ref.table);
|
|
363
|
+
|
|
347
364
|
if (load.status !== 'ready') {
|
|
348
365
|
return [pendingDraft(load, { connectionId: ref.connectionId, schema: ref.schema, table: ref.table }, level, parent)];
|
|
349
366
|
}
|
|
367
|
+
|
|
350
368
|
const structure = load.value;
|
|
369
|
+
|
|
351
370
|
if (structure.columns.length === 0) {
|
|
352
371
|
return [statusDraft({ kind: 'empty', of: 'columns' }, level, parent, true)];
|
|
353
372
|
}
|
|
373
|
+
|
|
354
374
|
if (kind === 'view') {
|
|
355
375
|
return columnRows(ref, structure, level, parent);
|
|
356
376
|
}
|
|
377
|
+
|
|
357
378
|
const entries = tableEntries(structure);
|
|
379
|
+
|
|
358
380
|
return PARTS.flatMap((part): Draft[] => {
|
|
359
381
|
const count = part === 'columns' ? structure.columns.length : entries[part].length;
|
|
382
|
+
|
|
360
383
|
if (count === 0) {
|
|
361
384
|
return [];
|
|
362
385
|
}
|
|
386
|
+
|
|
363
387
|
const key = partKey(ref, part);
|
|
364
388
|
const open = startsOpen(key) ? !input.collapsed.has(key) : input.expanded.has(key);
|
|
389
|
+
|
|
365
390
|
const below = (): Draft[] =>
|
|
366
391
|
part === 'columns'
|
|
367
392
|
? columnRows(ref, structure, level + 1, key)
|
|
@@ -378,6 +403,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
378
403
|
}),
|
|
379
404
|
below: []
|
|
380
405
|
}));
|
|
406
|
+
|
|
381
407
|
return [
|
|
382
408
|
{
|
|
383
409
|
make: (place) => ({ kind: 'part', key, level, parent, focusable: true, ref, part, expanded: open, count, ...place }),
|
|
@@ -390,6 +416,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
390
416
|
const tableDraft = (ref: TableRef, table: TableInfo, level: number, parent: string): Draft => {
|
|
391
417
|
const key = tableKey(ref);
|
|
392
418
|
const open = input.expanded.has(key);
|
|
419
|
+
|
|
393
420
|
return {
|
|
394
421
|
make: (place) => ({ kind: 'table', key, level, parent, focusable: true, ref, table, expanded: open, ...place }),
|
|
395
422
|
below: open ? settle(tableChildren(ref, table.kind, level + 1, key)) : []
|
|
@@ -399,19 +426,25 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
399
426
|
/* The folders under a schema (or under a connection with only that schema), and how many tables they hold. */
|
|
400
427
|
const folderDrafts = (connection: Connection, schema: string, level: number, parent: string): { drafts: Draft[]; tables: number } => {
|
|
401
428
|
const load = input.tables(connection.id, schema);
|
|
429
|
+
|
|
402
430
|
if (load.status === 'ready') {
|
|
403
431
|
const shown = filtering ? load.value.filter((table) => matches(table.name, query)) : load.value;
|
|
432
|
+
|
|
404
433
|
if (shown.length === 0 && !filtering) {
|
|
405
434
|
return { drafts: [statusDraft({ kind: 'empty', of: 'tables' }, level, parent, true)], tables: 0 };
|
|
406
435
|
}
|
|
436
|
+
|
|
407
437
|
const drafts = GROUPS.flatMap((group): Draft[] => {
|
|
408
438
|
const members = shown.filter((table) => table.kind === group);
|
|
439
|
+
|
|
409
440
|
if (members.length === 0) {
|
|
410
441
|
return [];
|
|
411
442
|
}
|
|
443
|
+
|
|
412
444
|
const key = folderKey(connection.id, schema, group);
|
|
413
445
|
const open = filtering || !input.collapsed.has(key);
|
|
414
446
|
const tables = members.map((table) => tableDraft({ connectionId: connection.id, schema, table: table.name }, table, level + 1, key));
|
|
447
|
+
|
|
415
448
|
return [
|
|
416
449
|
{
|
|
417
450
|
make: (place) => ({
|
|
@@ -431,21 +464,27 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
431
464
|
}
|
|
432
465
|
];
|
|
433
466
|
});
|
|
467
|
+
|
|
434
468
|
return { drafts, tables: shown.length };
|
|
435
469
|
}
|
|
470
|
+
|
|
436
471
|
if (filtering) {
|
|
437
472
|
return { drafts: [], tables: 0 };
|
|
438
473
|
}
|
|
474
|
+
|
|
439
475
|
return { drafts: [pendingDraft(load, { connectionId: connection.id, schema }, level, parent)], tables: 0 };
|
|
440
476
|
};
|
|
441
477
|
|
|
442
478
|
const schemaDraft = (connection: Connection, schema: string, parent: string): Draft | null => {
|
|
443
479
|
const key = schemaKey(connection.id, schema);
|
|
444
480
|
const { drafts, tables } = folderDrafts(connection, schema, 3, key);
|
|
481
|
+
|
|
445
482
|
if (filtering && tables === 0) {
|
|
446
483
|
return null;
|
|
447
484
|
}
|
|
485
|
+
|
|
448
486
|
const open = filtering || input.expanded.has(key);
|
|
487
|
+
|
|
449
488
|
return {
|
|
450
489
|
make: (place) => ({ kind: 'schema', key, level: 2, parent, focusable: true, connection, schema, expanded: open, ...place }),
|
|
451
490
|
below: open ? settle(drafts) : []
|
|
@@ -456,11 +495,14 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
456
495
|
const appFolderDrafts = (connection: Connection, level: number, parent: string): Draft[] =>
|
|
457
496
|
(input.folders?.(connection.id) ?? []).flatMap((folder): Draft[] => {
|
|
458
497
|
const items = filtering ? folder.items.filter((item) => matches(item.label, query)) : folder.items;
|
|
498
|
+
|
|
459
499
|
if (items.length === 0) {
|
|
460
500
|
return [];
|
|
461
501
|
}
|
|
502
|
+
|
|
462
503
|
const key = appFolderKey(connection.id, folder.id);
|
|
463
504
|
const open = filtering || input.expanded.has(key);
|
|
505
|
+
|
|
464
506
|
const rows = items.map((item): Draft => ({
|
|
465
507
|
make: (place) => ({
|
|
466
508
|
kind: 'appItem',
|
|
@@ -474,6 +516,7 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
474
516
|
}),
|
|
475
517
|
below: []
|
|
476
518
|
}));
|
|
519
|
+
|
|
477
520
|
return [
|
|
478
521
|
{
|
|
479
522
|
make: (place) => ({
|
|
@@ -500,12 +543,15 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
500
543
|
const open = filtering ? load.status === 'ready' || appFolders.length > 0 : input.expanded.has(key);
|
|
501
544
|
let children: Draft[] = [];
|
|
502
545
|
let schemaCount: ConnectionRow['schemaCount'] = null;
|
|
546
|
+
|
|
503
547
|
if (load.status === 'ready') {
|
|
504
548
|
const visible = visibleSchemas(load.value, input.showSystemSchemas);
|
|
505
549
|
const sole = soleSchema(visible);
|
|
550
|
+
|
|
506
551
|
if (visible.length < load.value.length) {
|
|
507
552
|
schemaCount = { shown: visible.length, total: load.value.length };
|
|
508
553
|
}
|
|
554
|
+
|
|
509
555
|
if (sole !== null) {
|
|
510
556
|
children = open ? folderDrafts(connection, sole, 2, key).drafts : [];
|
|
511
557
|
} else if (visible.length === 0) {
|
|
@@ -516,12 +562,16 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
516
562
|
} else if (open && !filtering) {
|
|
517
563
|
children = [pendingDraft(load, { connectionId: connection.id }, 2, key)];
|
|
518
564
|
}
|
|
565
|
+
|
|
519
566
|
// Behind a status row too: what the app keeps of a connection does not wait on its server.
|
|
520
567
|
children = [...children, ...(open ? appFolders : [])];
|
|
568
|
+
|
|
521
569
|
if (filtering && children.length === 0) {
|
|
522
570
|
return null;
|
|
523
571
|
}
|
|
572
|
+
|
|
524
573
|
const version = input.versions(connection.id);
|
|
574
|
+
|
|
525
575
|
return {
|
|
526
576
|
make: (place) => ({ kind: 'connection', key, level: 1, parent: null, focusable: true, connection, expanded: open, version, schemaCount, ...place }),
|
|
527
577
|
below: open ? settle(children) : []
|
|
@@ -534,30 +584,41 @@ export const flattenTree = (input: TreeInput): TreeRow[] => {
|
|
|
534
584
|
/* The loads an open node is waiting for and nobody has asked for yet. */
|
|
535
585
|
export const neededLoads = (input: Omit<TreeInput, 'filter'>): LoadTarget[] => {
|
|
536
586
|
const needed: LoadTarget[] = [];
|
|
587
|
+
|
|
537
588
|
for (const connection of input.connections) {
|
|
538
589
|
if (!input.expanded.has(connectionKey(connection.id))) {
|
|
539
590
|
continue;
|
|
540
591
|
}
|
|
592
|
+
|
|
541
593
|
const load = input.schemas(connection.id);
|
|
594
|
+
|
|
542
595
|
if (load.status === 'idle') {
|
|
543
596
|
needed.push({ connectionId: connection.id });
|
|
597
|
+
|
|
544
598
|
continue;
|
|
545
599
|
}
|
|
600
|
+
|
|
546
601
|
if (load.status !== 'ready') {
|
|
547
602
|
continue;
|
|
548
603
|
}
|
|
604
|
+
|
|
549
605
|
const visible = visibleSchemas(load.value, input.showSystemSchemas);
|
|
550
606
|
const sole = soleSchema(visible);
|
|
551
607
|
const wanted = sole !== null ? [sole] : visible.map((schema) => schema.name).filter((name) => input.expanded.has(schemaKey(connection.id, name)));
|
|
608
|
+
|
|
552
609
|
for (const schema of wanted) {
|
|
553
610
|
const tables = input.tables(connection.id, schema);
|
|
611
|
+
|
|
554
612
|
if (tables.status === 'idle') {
|
|
555
613
|
needed.push({ connectionId: connection.id, schema });
|
|
614
|
+
|
|
556
615
|
continue;
|
|
557
616
|
}
|
|
617
|
+
|
|
558
618
|
if (tables.status !== 'ready') {
|
|
559
619
|
continue;
|
|
560
620
|
}
|
|
621
|
+
|
|
561
622
|
for (const table of tables.value) {
|
|
562
623
|
if (
|
|
563
624
|
input.expanded.has(tableKey({ connectionId: connection.id, schema, table: table.name })) &&
|
|
@@ -568,6 +629,7 @@ export const neededLoads = (input: Omit<TreeInput, 'filter'>): LoadTarget[] => {
|
|
|
568
629
|
}
|
|
569
630
|
}
|
|
570
631
|
}
|
|
632
|
+
|
|
571
633
|
return needed;
|
|
572
634
|
};
|
|
573
635
|
|
|
@@ -581,11 +643,14 @@ export type TreeAction =
|
|
|
581
643
|
/* What a key does to the row that has the focus, or `null` when it does nothing there. */
|
|
582
644
|
export const navigate = (rows: readonly TreeRow[], activeKey: string | null, pressed: string): TreeAction | null => {
|
|
583
645
|
const stops = rows.filter((row) => row.focusable);
|
|
646
|
+
|
|
584
647
|
if (stops.length === 0) {
|
|
585
648
|
return null;
|
|
586
649
|
}
|
|
650
|
+
|
|
587
651
|
const at = stops.findIndex((row) => row.key === activeKey);
|
|
588
652
|
const row = at === -1 ? null : stops[at]!;
|
|
653
|
+
|
|
589
654
|
switch (pressed) {
|
|
590
655
|
case 'ArrowDown':
|
|
591
656
|
return { type: 'focus', key: stops[Math.min(at + 1, stops.length - 1)]!.key };
|
|
@@ -599,19 +664,24 @@ export const navigate = (rows: readonly TreeRow[], activeKey: string | null, pre
|
|
|
599
664
|
if (row === null || !isExpandable(row)) {
|
|
600
665
|
return null;
|
|
601
666
|
}
|
|
667
|
+
|
|
602
668
|
if (!row.expanded) {
|
|
603
669
|
return { type: 'expand', key: row.key };
|
|
604
670
|
}
|
|
671
|
+
|
|
605
672
|
const child = stops.find((candidate) => candidate.parent === row.key);
|
|
673
|
+
|
|
606
674
|
return child === undefined ? null : { type: 'focus', key: child.key };
|
|
607
675
|
}
|
|
608
676
|
case 'ArrowLeft': {
|
|
609
677
|
if (row === null) {
|
|
610
678
|
return null;
|
|
611
679
|
}
|
|
680
|
+
|
|
612
681
|
if (isExpandable(row) && row.expanded) {
|
|
613
682
|
return { type: 'collapse', key: row.key };
|
|
614
683
|
}
|
|
684
|
+
|
|
615
685
|
return row.parent === null ? null : { type: 'focus', key: row.parent };
|
|
616
686
|
}
|
|
617
687
|
case 'Enter':
|
|
@@ -629,5 +699,6 @@ export const tabStop = (rows: readonly TreeRow[], activeKey: string | null, sele
|
|
|
629
699
|
return key;
|
|
630
700
|
}
|
|
631
701
|
}
|
|
702
|
+
|
|
632
703
|
return rows.find((row) => row.focusable)?.key ?? null;
|
|
633
704
|
};
|