@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
|
@@ -8,6 +8,7 @@ import { formatNumber } from '@adecore/ui/format';
|
|
|
8
8
|
import { isCancelled } from '../client/types.js';
|
|
9
9
|
import { useDatabaseAction, useDatabaseClient, useDatabaseFiles, useDatabaseNotice, useDatabaseStorage } from '../client-context.js';
|
|
10
10
|
import { splitStatements, statementAt } from '../sql-split.js';
|
|
11
|
+
import { parseStoredSize } from '../stored-size.js';
|
|
11
12
|
import { DestructiveDialog } from './DestructiveDialog.js';
|
|
12
13
|
import { findDestructive } from './destructive.js';
|
|
13
14
|
import { HistoryPanel } from './HistoryPanel.js';
|
|
@@ -28,15 +29,6 @@ export const RESULTS_HEIGHT_KEY = 'database:console-results';
|
|
|
28
29
|
/* The results and the editor never shrink below these; the results start at half the console. */
|
|
29
30
|
const MIN_RESULTS_HEIGHT = 96;
|
|
30
31
|
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;
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
32
|
/* 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
33
|
function AppEditor({ render, ...editor }) {
|
|
42
34
|
return render(editor);
|
|
@@ -90,7 +82,7 @@ export function QueryConsole({ connection, schema, onSchemaChange, value, defaul
|
|
|
90
82
|
const [notice, setNotice] = useState(null);
|
|
91
83
|
// Closed until the first run, so the editor has the whole height before anything ran.
|
|
92
84
|
const [resultsOpen, setResultsOpen] = useState(false);
|
|
93
|
-
const [resultsHeight, setResultsHeight] = useState(() =>
|
|
85
|
+
const [resultsHeight, setResultsHeight] = useState(() => parseStoredSize(storage?.get(RESULTS_HEIGHT_KEY), MIN_RESULTS_HEIGHT));
|
|
94
86
|
const { startResize } = useColumnResize(resultsPane, {
|
|
95
87
|
size: resultsHeight ?? MIN_RESULTS_HEIGHT,
|
|
96
88
|
min: MIN_RESULTS_HEIGHT,
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import { Select } from '@adecore/ui';
|
|
4
|
-
/* Picks the schema the console's statements run in. */
|
|
5
4
|
export function SchemaPicker({ schemas, value, onValueChange, disabled, className, ref }) {
|
|
6
5
|
const { t } = useTranslation('database');
|
|
7
6
|
return (_jsx(Select, { ref: ref, size: "sm", label: t('console.schema'), placeholder: t('console.schemaPlaceholder'), value: value, disabled: disabled, className: className, items: schemas.map((schema) => ({ value: schema.name, label: schema.name })), onValueChange: onValueChange }));
|
|
@@ -4,13 +4,13 @@ import { ArrowUpRight, ChevronLeft, ChevronRight, CircleAlert, Download, PanelRi
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Banner, ButtonGroup, ContextMenu, Icon, IconButton, Menu, Spinner } from '@adecore/ui';
|
|
6
6
|
import { formatDecimal, formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { EXPORT_FORMATS } from '../export-formats.js';
|
|
7
8
|
import { DataGrid } from '../grid/DataGrid.js';
|
|
8
9
|
import { previewValueOf } from '../grid/focused-value.js';
|
|
9
10
|
import { ValueDock } from '../grid/ValueDock.js';
|
|
10
11
|
import { ValuePanel } from '../value/ValuePanel.js';
|
|
11
12
|
/* What "Copy as SQL INSERT" names the table of a result that comes from no table. */
|
|
12
13
|
const RESULT_TABLE = 'result';
|
|
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
15
|
export function StatementResultView({ result, offset = 0, pager, onExport, exporting = false, engine, valuePanelOpen = false, onValuePanelOpenChange, keys = null, onFollowReference, onNotice }) {
|
|
16
16
|
const { t } = useTranslation('database');
|
package/dist/ddl/diff.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { sameDefinition } from './column.js';
|
|
2
|
-
|
|
2
|
+
import { sameList } from '../same-list.js';
|
|
3
3
|
/* The positions of a longest run that already is in order: those columns can stay where they are. */
|
|
4
4
|
const longestRun = (positions) => {
|
|
5
5
|
const length = [];
|
package/dist/explorer/tree.js
CHANGED
|
@@ -70,7 +70,6 @@ export const hasMenu = (row) => row.kind === 'connection' ||
|
|
|
70
70
|
export const isSelectable = (row) => row.kind === 'connection' || row.kind === 'schema' || row.kind === 'table';
|
|
71
71
|
/* The statement behind New console here on a table: every row, as the engine quotes it. */
|
|
72
72
|
export const selectAllSql = (engine, ref) => `SELECT * FROM ${qualifiedName({ engine, schema: ref.schema, table: ref.table })}`;
|
|
73
|
-
/* The schemas a list shows. */
|
|
74
73
|
export const visibleSchemas = (schemas, showSystemSchemas) => showSystemSchemas ? schemas : schemas.filter((schema) => !schema.system);
|
|
75
74
|
/* One schema is no level worth a row: its folders hang straight under the connection. */
|
|
76
75
|
const soleSchema = (visible) => (visible.length === 1 ? visible[0].name : null);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
|
package/dist/grid/CopyAsMenu.js
CHANGED
|
@@ -3,7 +3,6 @@ import { Braces, Copy, Database, FileSpreadsheet, Table2 } from 'lucide-react';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { ContextMenu, Icon } from '@adecore/ui';
|
|
5
5
|
const ICONS = { tsv: Table2, csv: FileSpreadsheet, json: Braces, sql: Database };
|
|
6
|
-
/* The "Copy as" submenu of the grid's context menu. */
|
|
7
6
|
export function CopyAsMenu({ disabled, formats, onCopy }) {
|
|
8
7
|
const { t } = useTranslation('database');
|
|
9
8
|
return (_jsxs(ContextMenu.SubmenuRoot, { children: [_jsxs(ContextMenu.SubmenuTrigger, { disabled: disabled, children: [_jsx(Icon, { icon: Copy, size: 14 }), t('grid.copyAs.label')] }), _jsx(ContextMenu.Popup, { children: formats.map((format) => (_jsxs(ContextMenu.Item, { onClick: () => onCopy(format), children: [_jsx(Icon, { icon: ICONS[format], size: 14 }), t(`grid.copyAs.${format}`)] }, format))) })] }));
|
package/dist/grid/DataGrid.js
CHANGED
|
@@ -31,7 +31,8 @@ const NO_KEYS = new Set();
|
|
|
31
31
|
const NO_SORTS = [];
|
|
32
32
|
const signatureOf = (columns) => columns.map((column) => column.name).join('\u0000');
|
|
33
33
|
const textOf = (view) => (view.suffix === undefined ? view.text : `${view.text} ${view.suffix}`);
|
|
34
|
-
const
|
|
34
|
+
const columnTexts = (column, index, rows, notation) => rows.map((row) => textOf(cellView(row.cells[index] ?? null, column.kind, columnNotation(column, notation))));
|
|
35
|
+
const estimateWidths = (columns, rows, notation) => columns.map((column, index) => estimateColumnWidth(column.name, columnTexts(column, index, rows.slice(0, 50), notation), column.primaryKey === true));
|
|
35
36
|
/*
|
|
36
37
|
* The table of the table view and of the console, and of any rows an app has on its own: it needs no
|
|
37
38
|
* `DatabaseProvider`, only the `UIProvider` of `@adecore/ui`. Only the rows in view are drawn, at a fixed
|
|
@@ -289,11 +290,11 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
|
|
|
289
290
|
scroller.current?.focus();
|
|
290
291
|
};
|
|
291
292
|
const followCell = (cell) => onFollow !== undefined && (canFollow?.(cell) ?? true);
|
|
293
|
+
const menuCell = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
|
|
292
294
|
const commitValue = (value) => {
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
onCommit?.(row.key, position.column, value);
|
|
295
|
+
const row = menuCell === null ? undefined : rows[menuCell.row];
|
|
296
|
+
if (menuCell !== null && row !== undefined && canEdit(menuCell)) {
|
|
297
|
+
onCommit?.(row.key, menuCell.column, value);
|
|
297
298
|
}
|
|
298
299
|
};
|
|
299
300
|
/* The cells from row `top` to `bottom` in the given columns, with the whole value of every preview `loadValue` can fetch. */
|
|
@@ -335,8 +336,8 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
|
|
|
335
336
|
}
|
|
336
337
|
};
|
|
337
338
|
const copyCell = () => {
|
|
338
|
-
if (
|
|
339
|
-
void copyOne(
|
|
339
|
+
if (menuCell !== null && rows[menuCell.row] !== undefined) {
|
|
340
|
+
void copyOne(menuCell.row, menuCell.column);
|
|
340
341
|
}
|
|
341
342
|
};
|
|
342
343
|
const selectAll = () => {
|
|
@@ -448,7 +449,7 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
|
|
|
448
449
|
const fitColumn = (index) => {
|
|
449
450
|
const column = columns[index];
|
|
450
451
|
if (column !== undefined) {
|
|
451
|
-
resizeColumn(index, fitColumnWidth(column.name,
|
|
452
|
+
resizeColumn(index, fitColumnWidth(column.name, columnTexts(column, index, rows, notation), column.primaryKey === true));
|
|
452
453
|
}
|
|
453
454
|
};
|
|
454
455
|
const changeView = (change) => {
|
|
@@ -513,10 +514,9 @@ export function DataGrid({ columns, rows, label, editable = false, selectedKeys,
|
|
|
513
514
|
return (_jsx(GridHeaderCell, { column: column, index: columnIndex, position: position, width: shownWidths[position], sortable: sortable, selected: pickedSet.has(columnIndex), id: headerId(columnIndex), sort: sortable ? sortStateOf(sortList, column.name) : null, multipleSorts: sortList.length > 1, hasSorts: sortList.length > 0, pinned: position < pinnedShown, lastPinned: position === pinnedShown - 1, last: position === order.length - 1, hasHidden: current.view.hidden.size > 0, canHide: order.length > 1, actions: headerActions }, columnIndex));
|
|
514
515
|
});
|
|
515
516
|
const menuRow = menuTarget === null ? undefined : rows[menuTarget.row];
|
|
516
|
-
const menuCell = menuTarget === null || menuTarget.column === null ? null : { row: menuTarget.row, column: menuTarget.column };
|
|
517
517
|
const settable = menuCell !== null && canEdit(menuCell);
|
|
518
|
-
const nullable = menuCell !== null && columns[menuCell.column]?.nullable !== false;
|
|
519
518
|
const menuColumn = menuCell === null ? undefined : columns[menuCell.column];
|
|
519
|
+
const nullable = menuColumn?.nullable !== false;
|
|
520
520
|
const hasDefault = menuColumn !== undefined && ((menuColumn.defaultValue ?? null) !== null || menuColumn.autoIncrement === true);
|
|
521
521
|
const menuContext = menuRow === undefined || menuTarget === null
|
|
522
522
|
? null
|
|
@@ -4,7 +4,6 @@ export interface GridLayout {
|
|
|
4
4
|
readonly hidden: readonly string[];
|
|
5
5
|
readonly pinned: readonly string[];
|
|
6
6
|
}
|
|
7
|
-
export declare const NO_GRID_LAYOUT: GridLayout;
|
|
8
7
|
export interface ColumnState {
|
|
9
8
|
readonly resized: Readonly<Record<number, number>>;
|
|
10
9
|
readonly view: ColumnView;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MAX_RESIZED_WIDTH, MIN_COLUMN_WIDTH } from './layout.js';
|
|
2
|
-
|
|
2
|
+
import { sameList } from '../same-list.js';
|
|
3
3
|
/* The state of a grid for columns it has, from a layout. Names it does not have are skipped; a layout that would hide every column hides none. */
|
|
4
4
|
export const restoreLayout = (columns, layout) => {
|
|
5
5
|
const resized = {};
|
|
@@ -40,7 +40,6 @@ export const captureLayout = (columns, state) => {
|
|
|
40
40
|
pinned: namesOf(state.view.pinned)
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
|
-
const sameList = (a, b) => a.length === b.length && a.every((item, index) => item === b[index]);
|
|
44
43
|
export const sameLayout = (a, b) => {
|
|
45
44
|
const names = Object.keys(a.widths);
|
|
46
45
|
return (names.length === Object.keys(b.widths).length &&
|
package/dist/grid/column-view.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export const NO_COLUMN_VIEW = { hidden: new Set(), pinned: [] };
|
|
2
|
+
const shownPins = (count, view) => view.pinned.filter((index) => index < count && !view.hidden.has(index));
|
|
2
3
|
/* The columns to draw as indexes: the pinned ones first in pin order, then the rest as they come, hidden ones left out. */
|
|
3
4
|
export const displayOrder = (count, view) => {
|
|
4
|
-
const pinned =
|
|
5
|
+
const pinned = shownPins(count, view);
|
|
5
6
|
const rest = [];
|
|
6
7
|
for (let i = 0; i < count; i++) {
|
|
7
8
|
if (!view.hidden.has(i) && !pinned.includes(i)) {
|
|
@@ -20,4 +21,4 @@ export const togglePin = (view, index) => ({
|
|
|
20
21
|
pinned: view.pinned.includes(index) ? view.pinned.filter((pinned) => pinned !== index) : [...view.pinned, index]
|
|
21
22
|
});
|
|
22
23
|
/* How many of the first columns of `displayOrder` are pinned. */
|
|
23
|
-
export const pinnedCount = (count, view) =>
|
|
24
|
+
export const pinnedCount = (count, view) => shownPins(count, view).length;
|
package/dist/grid/display.js
CHANGED
|
@@ -83,8 +83,7 @@ export const copyTextOf = (cell) => {
|
|
|
83
83
|
/* An edit as a cell, so the grid draws a pending value the way it draws a loaded one. */
|
|
84
84
|
export const shownOfEdit = (value) => {
|
|
85
85
|
if (value !== null && typeof value === 'object' && value.kind === 'binary') {
|
|
86
|
-
|
|
87
|
-
return { kind: 'binary', hex: binary.hex, length: binary.hex.length / 2 };
|
|
86
|
+
return { kind: 'binary', hex: value.hex, length: value.hex.length / 2 };
|
|
88
87
|
}
|
|
89
88
|
return value;
|
|
90
89
|
};
|
package/dist/host/host.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PROTOCOL_VERSION } from '../protocol/index.js';
|
|
2
|
-
import { parseRequest } from './validate.js';
|
|
2
|
+
import { isDict, parseRequest } from './validate.js';
|
|
3
3
|
const DEFAULT_READY_TIMEOUT_MS = 10000;
|
|
4
4
|
/* How long a release or a dispose waits for a helper to answer its `close` requests. */
|
|
5
5
|
const CLOSE_GRACE_MS = 2000;
|
|
@@ -21,7 +21,6 @@ const parseLine = (line) => {
|
|
|
21
21
|
return undefined;
|
|
22
22
|
}
|
|
23
23
|
};
|
|
24
|
-
const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
25
24
|
/* Settles when every promise has, or after the grace period; a helper that hangs must not hold up an app that quits. */
|
|
26
25
|
const settleWithin = async (promises) => {
|
|
27
26
|
let timer;
|
package/dist/host/validate.d.ts
CHANGED
|
@@ -7,4 +7,7 @@ export type ParsedRequest = {
|
|
|
7
7
|
readonly id: string;
|
|
8
8
|
readonly error: DatabaseError;
|
|
9
9
|
};
|
|
10
|
+
type Dict = Record<string, unknown>;
|
|
11
|
+
export declare const isDict: (value: unknown) => value is Dict;
|
|
10
12
|
export declare const parseRequest: (input: unknown) => ParsedRequest;
|
|
13
|
+
export {};
|
package/dist/host/validate.js
CHANGED
|
@@ -13,7 +13,7 @@ class InvalidRequest extends Error {
|
|
|
13
13
|
const fail = (message) => {
|
|
14
14
|
throw new InvalidRequest(message);
|
|
15
15
|
};
|
|
16
|
-
const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
export const isDict = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
17
17
|
const isRequestId = (value) => typeof value === 'string' && value.length > 0 && value.length <= MAX_ID_LENGTH;
|
|
18
18
|
/* Rejects a key nobody asked for, so a typo cannot pass for an option the helper ignores. */
|
|
19
19
|
const shape = (value, label, required, optional = []) => {
|
|
@@ -203,6 +203,15 @@ const exportSource = (input, label) => {
|
|
|
203
203
|
};
|
|
204
204
|
const cellLimit = (params) => optional(params, 'cellLimit', 'params', (limit, name) => integer(limit, name, 1, MAX_CELL_LIMIT));
|
|
205
205
|
const maxBytes = (params) => optional(params, 'maxBytes', 'params', (limit, name) => integer(limit, name, 1, MAX_BYTES));
|
|
206
|
+
const rowLimit = (params) => optional(params, 'limit', 'params', (limit, name) => integer(limit, name, 1, MAX_LIMIT));
|
|
207
|
+
const pageWindow = (params) => {
|
|
208
|
+
integer(params.offset, 'params.offset', 0, Number.MAX_SAFE_INTEGER);
|
|
209
|
+
integer(params.limit, 'params.limit', 1, MAX_LIMIT);
|
|
210
|
+
cellLimit(params);
|
|
211
|
+
maxBytes(params);
|
|
212
|
+
};
|
|
213
|
+
const connectionParams = (input) => connection(shape(input, 'params', ['connection']).connection, 'params.connection');
|
|
214
|
+
const sessionParams = (input) => filledText(shape(input, 'params', ['session']).session, 'params.session');
|
|
206
215
|
const table = (params) => {
|
|
207
216
|
filledText(params.session, 'params.session');
|
|
208
217
|
filledText(params.schema, 'params.schema');
|
|
@@ -210,10 +219,10 @@ const table = (params) => {
|
|
|
210
219
|
};
|
|
211
220
|
const TARGET = ['session', 'schema', 'table'];
|
|
212
221
|
const PARAMS = {
|
|
213
|
-
open:
|
|
214
|
-
test:
|
|
215
|
-
close:
|
|
216
|
-
schemas:
|
|
222
|
+
open: connectionParams,
|
|
223
|
+
test: connectionParams,
|
|
224
|
+
close: sessionParams,
|
|
225
|
+
schemas: sessionParams,
|
|
217
226
|
tables: (input) => {
|
|
218
227
|
const params = shape(input, 'params', ['session', 'schema']);
|
|
219
228
|
filledText(params.session, 'params.session');
|
|
@@ -225,10 +234,7 @@ const PARAMS = {
|
|
|
225
234
|
table(params);
|
|
226
235
|
optional(params, 'where', 'params', anyText);
|
|
227
236
|
optional(params, 'orderBy', 'params', anyText);
|
|
228
|
-
|
|
229
|
-
integer(params.limit, 'params.limit', 1, MAX_LIMIT);
|
|
230
|
-
cellLimit(params);
|
|
231
|
-
maxBytes(params);
|
|
237
|
+
pageWindow(params);
|
|
232
238
|
},
|
|
233
239
|
count: (input) => {
|
|
234
240
|
const params = shape(input, 'params', TARGET, ['where']);
|
|
@@ -255,7 +261,7 @@ const PARAMS = {
|
|
|
255
261
|
filledText(params.session, 'params.session');
|
|
256
262
|
filledText(params.sql, 'params.sql');
|
|
257
263
|
optional(params, 'schema', 'params', filledText);
|
|
258
|
-
|
|
264
|
+
rowLimit(params);
|
|
259
265
|
cellLimit(params);
|
|
260
266
|
maxBytes(params);
|
|
261
267
|
},
|
|
@@ -264,10 +270,7 @@ const PARAMS = {
|
|
|
264
270
|
filledText(params.session, 'params.session');
|
|
265
271
|
filledText(params.sql, 'params.sql');
|
|
266
272
|
optional(params, 'schema', 'params', filledText);
|
|
267
|
-
|
|
268
|
-
integer(params.limit, 'params.limit', 1, MAX_LIMIT);
|
|
269
|
-
cellLimit(params);
|
|
270
|
-
maxBytes(params);
|
|
273
|
+
pageWindow(params);
|
|
271
274
|
},
|
|
272
275
|
transaction: (input) => {
|
|
273
276
|
const params = shape(input, 'params', ['session', 'action']);
|
|
@@ -288,7 +291,7 @@ const PARAMS = {
|
|
|
288
291
|
absolutePath(params.path, 'params.path');
|
|
289
292
|
oneOf(DELIMITED_FORMATS)(params.format, 'params.format');
|
|
290
293
|
flag(params.header, 'params.header');
|
|
291
|
-
|
|
294
|
+
rowLimit(params);
|
|
292
295
|
},
|
|
293
296
|
import: (input) => {
|
|
294
297
|
const params = shape(input, 'params', [...TARGET, 'path', 'format', 'header', 'columns']);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sameList: (left: readonly string[], right: readonly string[]) => boolean;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const sameList = (left, right) => left.length === right.length && left.every((item, index) => item === right[index]);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const parseStoredSize: (raw: string | null | undefined, minimum: number) => number | null;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/* A size in pixels a view wrote to the app's storage, or `null` when the text is missing, broken or not at least `minimum`. */
|
|
2
|
+
export const parseStoredSize = (raw, minimum) => {
|
|
3
|
+
try {
|
|
4
|
+
const value = JSON.parse(raw ?? 'null');
|
|
5
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= minimum ? Math.round(value) : null;
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
};
|
|
@@ -4,9 +4,9 @@ import { ChevronsLeft, ChevronsRight, CopyPlus, Download, Ellipsis, FileInput, P
|
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
5
|
import { Button, ButtonGroup, Icon, IconButton, Menu, Separator } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
|
+
import { EXPORT_FORMATS } from '../export-formats.js';
|
|
7
8
|
import { CommandField } from './CommandField.js';
|
|
8
9
|
import { PAGE_SIZES } from './paging.js';
|
|
9
|
-
const EXPORT_FORMATS = ['csv', 'tsv', 'json', 'sql'];
|
|
10
10
|
/* The command field for filtering, sorting and jumping to a column, refresh, add row, the submit of pending changes, and a menu of the rest. */
|
|
11
11
|
export function TableToolbar({ start, refreshing, readOnlyReason, hasSelection, canRevertSelection, recordViewOpen = false, transfer, pendingCount, submitting, engine, chips, columns, pageSize, bounds, commandRef, onChipsChange, onJumpToColumn, onRefresh, onAddRow, onDeleteRows, onCloneRows, onRevertRows, onToggleRecordView, onPageSizeChange, onFirstPage, onLastPage, onSubmit, onRevert, className, ref }) {
|
|
12
12
|
const { t } = useTranslation('database');
|
package/dist/table/TableView.js
CHANGED
|
@@ -12,6 +12,7 @@ import { DataGrid } from '../grid/DataGrid.js';
|
|
|
12
12
|
import { RecordView } from '../grid/RecordView.js';
|
|
13
13
|
import { ValueDock } from '../grid/ValueDock.js';
|
|
14
14
|
import { valueOfCell } from '../protocol/index.js';
|
|
15
|
+
import { parseStoredSize } from '../stored-size.js';
|
|
15
16
|
import { useSchemaChange } from '../use-schema-change.js';
|
|
16
17
|
import { useStableCallback } from '../use-stable-callback.js';
|
|
17
18
|
import { addChip, cellChip, chipsFromQuery, chipsToOrderBy, chipsToWhere, shorten, sortsOf, toggleSort, withSorts } from './command-field.js';
|
|
@@ -30,15 +31,6 @@ import { useTableTransfer } from './useTableTransfer.js';
|
|
|
30
31
|
/* Where the width of the record view is kept, in pixels, for every table. */
|
|
31
32
|
export const RECORD_WIDTH_KEY = 'database:record-view';
|
|
32
33
|
const RECORD_WIDTH = 360;
|
|
33
|
-
const parseRecordWidth = (raw) => {
|
|
34
|
-
try {
|
|
35
|
-
const value = JSON.parse(raw ?? 'null');
|
|
36
|
-
return typeof value === 'number' && Number.isFinite(value) && value > 0 ? Math.round(value) : null;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
34
|
const NO_KEYS = new Set();
|
|
43
35
|
const readOnlyReasonOf = (connection, structure, t) => {
|
|
44
36
|
if (connection.config.readOnly === true) {
|
|
@@ -108,7 +100,7 @@ function TableBody({ connection, schema, table, toolbarStart, defaultWhere, defa
|
|
|
108
100
|
const [discard, setDiscard] = useState(null);
|
|
109
101
|
const [focus, setFocus] = useState(null);
|
|
110
102
|
const [recordOpen, setRecordOpen] = useState(false);
|
|
111
|
-
const [recordWidth, setRecordWidth] = useState(() =>
|
|
103
|
+
const [recordWidth, setRecordWidth] = useState(() => parseStoredSize(storage?.get(RECORD_WIDTH_KEY), Number.MIN_VALUE));
|
|
112
104
|
const [focusRequest, setFocusRequest] = useState(null);
|
|
113
105
|
const [columnRequest, setColumnRequest] = useState(null);
|
|
114
106
|
const [range, setRange] = useState(null);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/database",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.0",
|
|
4
4
|
"description": "Browse, query and edit SQLite and MySQL databases: React views for the page, a host for the backend and a native helper that talks to the server.",
|
|
5
5
|
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -58,32 +58,32 @@
|
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"@base-ui/react": "^1.9.0",
|
|
60
60
|
"clsx": "^2.1.1",
|
|
61
|
-
"lucide-react": "^1.
|
|
61
|
+
"lucide-react": "^1.61.0"
|
|
62
62
|
},
|
|
63
63
|
"optionalDependencies": {
|
|
64
|
-
"@adecore/database-darwin-arm64": "0.
|
|
65
|
-
"@adecore/database-linux-arm64": "0.
|
|
66
|
-
"@adecore/database-linux-x64": "0.
|
|
67
|
-
"@adecore/database-win32-x64": "0.
|
|
64
|
+
"@adecore/database-darwin-arm64": "0.40.0",
|
|
65
|
+
"@adecore/database-linux-arm64": "0.40.0",
|
|
66
|
+
"@adecore/database-linux-x64": "0.40.0",
|
|
67
|
+
"@adecore/database-win32-x64": "0.40.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@adecore/ui": "^0.
|
|
70
|
+
"@adecore/ui": "^0.40.0",
|
|
71
71
|
"i18next": "^26.4.2",
|
|
72
72
|
"react": "^19.3.0",
|
|
73
73
|
"react-dom": "^19.3.0",
|
|
74
74
|
"react-i18next": "^17.0.15"
|
|
75
75
|
},
|
|
76
76
|
"devDependencies": {
|
|
77
|
-
"@adecore/ui": "0.
|
|
78
|
-
"@happy-dom/global-registrator": "^20.14.
|
|
79
|
-
"@types/bun": "^1.4.
|
|
80
|
-
"@types/node": "^24.
|
|
77
|
+
"@adecore/ui": "0.40.0",
|
|
78
|
+
"@happy-dom/global-registrator": "^20.14.6",
|
|
79
|
+
"@types/bun": "^1.4.4",
|
|
80
|
+
"@types/node": "^24.19.2",
|
|
81
81
|
"@types/react": "^19.3.0",
|
|
82
82
|
"@types/react-dom": "^19.3.0",
|
|
83
|
-
"i18next": "^26.
|
|
84
|
-
"react": "^19.3.
|
|
85
|
-
"react-dom": "^19.3.
|
|
86
|
-
"react-i18next": "^17.
|
|
83
|
+
"i18next": "^26.5.11",
|
|
84
|
+
"react": "^19.3.1",
|
|
85
|
+
"react-dom": "^19.3.1",
|
|
86
|
+
"react-i18next": "^17.1.9",
|
|
87
87
|
"typescript": "^7.0.2"
|
|
88
88
|
}
|
|
89
89
|
}
|
package/src/DatabaseProvider.tsx
CHANGED
|
@@ -34,6 +34,7 @@ export function DatabaseProvider({ client, onAction, onNotice, storage, files, n
|
|
|
34
34
|
const ownStorage = storage ?? outer?.storage;
|
|
35
35
|
const ownFiles = files ?? outer?.files;
|
|
36
36
|
const ownNotation = numberNotation ?? outer?.numberNotation ?? 'database';
|
|
37
|
+
|
|
37
38
|
const value = useMemo(
|
|
38
39
|
() =>
|
|
39
40
|
ownClient === undefined
|
|
@@ -41,6 +42,7 @@ export function DatabaseProvider({ client, onAction, onNotice, storage, files, n
|
|
|
41
42
|
: { client: ownClient, onAction: ownAction, onNotice: ownNotice, storage: ownStorage, files: ownFiles, numberNotation: ownNotation },
|
|
42
43
|
[ownClient, ownAction, ownNotice, ownStorage, ownFiles, ownNotation]
|
|
43
44
|
);
|
|
45
|
+
|
|
44
46
|
if (value === null) {
|
|
45
47
|
throw new Error('A DatabaseProvider needs a client, or a DatabaseProvider above it to take one from.');
|
|
46
48
|
}
|
package/src/KeyIcon.tsx
CHANGED
|
@@ -11,5 +11,6 @@ export function KeyIcon({ primaryKey, foreignKey, size }: ColumnKeys & { size: 1
|
|
|
11
11
|
if (!primaryKey && !foreignKey) {
|
|
12
12
|
return null;
|
|
13
13
|
}
|
|
14
|
+
|
|
14
15
|
return <Icon icon={KeyRound} size={size} className={clsx('shrink-0', primaryKey ? 'text-(--file-icon-yellow)' : 'text-(--file-icon-blue)')} />;
|
|
15
16
|
}
|
package/src/client/client.ts
CHANGED
|
@@ -56,11 +56,13 @@ const raceAbort = <T>(promise: Promise<T>, signal: AbortSignal | undefined): Pro
|
|
|
56
56
|
|
|
57
57
|
if (signal.aborted) {
|
|
58
58
|
promise.catch(() => {});
|
|
59
|
+
|
|
59
60
|
return Promise.reject(cancelledError());
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
return new Promise<T>((resolve, reject) => {
|
|
63
64
|
const onAbort = (): void => reject(cancelledError());
|
|
65
|
+
|
|
64
66
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
65
67
|
promise.then(resolve, reject).finally(() => signal.removeEventListener('abort', onAbort));
|
|
66
68
|
});
|
|
@@ -162,6 +164,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
162
164
|
|
|
163
165
|
track();
|
|
164
166
|
held.add(created);
|
|
167
|
+
|
|
165
168
|
opening ??= send('open', { connection: connection.config })
|
|
166
169
|
.then((result) => (opened = result))
|
|
167
170
|
.finally(() => {
|
|
@@ -247,6 +250,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
247
250
|
},
|
|
248
251
|
async count(schema, table, where, requestOptions) {
|
|
249
252
|
const params = (id: string) => (where === undefined ? { session: id, schema, table } : { session: id, schema, table, where });
|
|
253
|
+
|
|
250
254
|
return (await request('count', params, requestOptions, true)).count;
|
|
251
255
|
},
|
|
252
256
|
async cell(schema, table, key, column, requestOptions) {
|
|
@@ -287,6 +291,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
287
291
|
connection = next;
|
|
288
292
|
}
|
|
289
293
|
};
|
|
294
|
+
|
|
290
295
|
return created;
|
|
291
296
|
};
|
|
292
297
|
|
|
@@ -296,6 +301,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
296
301
|
},
|
|
297
302
|
async discover(kind, discoverOptions) {
|
|
298
303
|
const params = discoverOptions?.context === undefined ? { kind } : { kind, context: discoverOptions.context };
|
|
304
|
+
|
|
299
305
|
return (await send('discover', params, discoverOptions?.signal)).containers;
|
|
300
306
|
},
|
|
301
307
|
sample(path, format, header, requestOptions) {
|
|
@@ -304,6 +310,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
304
310
|
notifySchemaChange,
|
|
305
311
|
onSchemaChange(listener) {
|
|
306
312
|
schemaListeners.add(listener);
|
|
313
|
+
|
|
307
314
|
return () => {
|
|
308
315
|
schemaListeners.delete(listener);
|
|
309
316
|
};
|
|
@@ -313,6 +320,7 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
313
320
|
|
|
314
321
|
if (existing !== undefined && isEqual(existing.session.connection.config, connection.config)) {
|
|
315
322
|
existing.update(connection);
|
|
323
|
+
|
|
316
324
|
return existing.session;
|
|
317
325
|
}
|
|
318
326
|
|
|
@@ -320,23 +328,28 @@ export const createDatabaseClient = (transport: DatabaseTransport, options: Data
|
|
|
320
328
|
|
|
321
329
|
const created = createSession(connection, channel);
|
|
322
330
|
channelsOf(connection.id).set(channel, created);
|
|
331
|
+
|
|
323
332
|
return created.session;
|
|
324
333
|
},
|
|
325
334
|
async disconnect(connectionId) {
|
|
326
335
|
const existing = new Set(live.get(connectionId)?.values());
|
|
336
|
+
|
|
327
337
|
for (const entry of held) {
|
|
328
338
|
if (entry.session.connection.id === connectionId) {
|
|
329
339
|
existing.add(entry);
|
|
330
340
|
}
|
|
331
341
|
}
|
|
342
|
+
|
|
332
343
|
live.delete(connectionId);
|
|
333
344
|
await Promise.all([...existing].map((entry) => entry.close()));
|
|
334
345
|
},
|
|
335
346
|
async dispose() {
|
|
336
347
|
const all = new Set([...live.values()].flatMap((channels) => [...channels.values()]));
|
|
348
|
+
|
|
337
349
|
for (const entry of held) {
|
|
338
350
|
all.add(entry);
|
|
339
351
|
}
|
|
352
|
+
|
|
340
353
|
live.clear();
|
|
341
354
|
await Promise.all([...all].map((entry) => entry.close()));
|
|
342
355
|
}
|
package/src/client-context.ts
CHANGED
|
@@ -16,9 +16,11 @@ export const DatabaseContext = createContext<DatabaseContextValue | null>(null);
|
|
|
16
16
|
|
|
17
17
|
const useDatabaseContext = (): DatabaseContextValue => {
|
|
18
18
|
const value = use(DatabaseContext);
|
|
19
|
+
|
|
19
20
|
if (value === null) {
|
|
20
21
|
throw new Error('A database view needs a DatabaseProvider above it.');
|
|
21
22
|
}
|
|
23
|
+
|
|
22
24
|
return value;
|
|
23
25
|
};
|
|
24
26
|
|
|
@@ -53,6 +53,7 @@ export function ConnectionForm({ value, onValueChange, onBrowse, className, ref
|
|
|
53
53
|
const { t } = useTranslation('database');
|
|
54
54
|
const [touched, setTouched] = useState<Touched>(new Set());
|
|
55
55
|
const config = value.config;
|
|
56
|
+
|
|
56
57
|
const options: readonly SegmentedOption<Engine>[] = [
|
|
57
58
|
{ id: 'sqlite', label: t('engine.sqlite'), icon: ENGINE_ICONS.sqlite },
|
|
58
59
|
{ id: 'mysql', label: t('engine.mysql'), icon: ENGINE_ICONS.mysql }
|
|
@@ -109,9 +110,11 @@ export function ConnectionForm({ value, onValueChange, onBrowse, className, ref
|
|
|
109
110
|
/* The sentence for a problem, once the field has something to answer for. */
|
|
110
111
|
function useProblem(problem: ConfigProblem | undefined, field: string, touched: Touched, filled: boolean): string | undefined {
|
|
111
112
|
const { t } = useTranslation('database');
|
|
113
|
+
|
|
112
114
|
if (problem === undefined || !(touched.has(field) || filled)) {
|
|
113
115
|
return undefined;
|
|
114
116
|
}
|
|
117
|
+
|
|
115
118
|
return t(`connections.form.problems.${problem}`);
|
|
116
119
|
}
|
|
117
120
|
|
|
@@ -124,10 +127,12 @@ function SqliteFields({ config, onConfigChange, touched, touch, onBrowse }: Fiel
|
|
|
124
127
|
const browse = async (): Promise<void> => {
|
|
125
128
|
await browsing.run(async () => {
|
|
126
129
|
const picked = await onBrowse?.('database');
|
|
130
|
+
|
|
127
131
|
if (picked !== null && picked !== undefined) {
|
|
128
132
|
onConfigChange({ ...config, path: picked });
|
|
129
133
|
}
|
|
130
134
|
});
|
|
135
|
+
|
|
131
136
|
touch('path');
|
|
132
137
|
};
|
|
133
138
|
|
|
@@ -239,6 +244,7 @@ function MysqlFields({ config, onConfigChange, touched, touch, onBrowse }: Field
|
|
|
239
244
|
const mode = modeOf(config);
|
|
240
245
|
const tunnel = config.tunnel;
|
|
241
246
|
const modes: readonly SegmentedOption<ConnectMode>[] = CONNECT_MODES.map((id) => ({ id, label: t(`connections.form.mode.${id}`), icon: MODE_ICONS[id] }));
|
|
247
|
+
|
|
242
248
|
const tlsItems: SelectItem<MysqlTlsMode>[] = TLS_MODES.map((tls) => ({
|
|
243
249
|
value: tls,
|
|
244
250
|
label: t(`connections.form.tls.${tls}`),
|
|
@@ -351,11 +357,13 @@ function SshFields({ config, tunnel, onConfigChange, touched, touch, onBrowse }:
|
|
|
351
357
|
const problems = configProblems(config);
|
|
352
358
|
const hostError = useProblem(problems.sshHost, 'sshHost', touched, tunnel.host !== '');
|
|
353
359
|
const portError = useProblem(problems.sshPort, 'sshPort', touched, tunnel.port !== undefined);
|
|
360
|
+
|
|
354
361
|
const update = (patch: Partial<SshTunnel>): void => onConfigChange({ ...config, tunnel: { ...tunnel, ...patch } });
|
|
355
362
|
|
|
356
363
|
const browse = async (): Promise<void> => {
|
|
357
364
|
await browsing.run(async () => {
|
|
358
365
|
const picked = await onBrowse?.('identity');
|
|
366
|
+
|
|
359
367
|
if (picked !== null && picked !== undefined) {
|
|
360
368
|
update({ identityFile: picked });
|
|
361
369
|
}
|
|
@@ -421,6 +429,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
|
|
|
421
429
|
|
|
422
430
|
const publishedText = (container: DockerContainer, port: number): string => {
|
|
423
431
|
const host = publishedPortOf(container, port);
|
|
432
|
+
|
|
424
433
|
return host === null ? t('connections.form.docker.notPublished') : t('connections.form.docker.published', { port: host });
|
|
425
434
|
};
|
|
426
435
|
|
|
@@ -429,6 +438,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
|
|
|
429
438
|
label: container.name,
|
|
430
439
|
description: containerDescription(container, publishedText(container, containerPortOf(container)))
|
|
431
440
|
}));
|
|
441
|
+
|
|
432
442
|
if (tunnel.container !== '' && picked === undefined) {
|
|
433
443
|
items.unshift({ value: tunnel.container, label: tunnel.container, description: t('connections.form.docker.notRunning') });
|
|
434
444
|
}
|
|
@@ -448,6 +458,7 @@ function DockerFields({ config, tunnel, onConfigChange, touched, touch }: Fields
|
|
|
448
458
|
|
|
449
459
|
const pick = (name: string): void => {
|
|
450
460
|
const container = containers.find((candidate) => candidate.name === name);
|
|
461
|
+
|
|
451
462
|
if (container !== undefined) {
|
|
452
463
|
onConfigChange(withContainer(config, container));
|
|
453
464
|
}
|