@adecore/database 0.16.0 → 0.17.0-beta.2
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/LICENSE +97 -8
- package/README.md +1 -1
- package/dist/explorer/DatabaseExplorer.js +19 -34
- package/package.json +7 -7
- package/src/explorer/DatabaseExplorer.tsx +26 -68
package/LICENSE
CHANGED
|
@@ -1,13 +1,102 @@
|
|
|
1
|
-
MIT License
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Abbreviation
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 - present Bas Milius
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
11
100
|
|
|
12
101
|
The above copyright notice and this permission notice shall be included in all
|
|
13
102
|
copies or substantial portions of the Software.
|
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import clsx from 'clsx';
|
|
4
|
-
import {
|
|
4
|
+
import { CircleAlert, Database, Eye, Folder, KeyRound, Link2, Lock, RectangleVertical, Search, Table } from 'lucide-react';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
-
import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip } from '@adecore/ui';
|
|
6
|
+
import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip, Tree } from '@adecore/ui';
|
|
7
7
|
import { formatNumber } from '@adecore/ui/format';
|
|
8
8
|
import { useDatabaseAction, useDatabaseClient } from '../client-context.js';
|
|
9
9
|
import { EngineIcon } from '../connections/EngineIcon.js';
|
|
@@ -13,10 +13,6 @@ import { connectionKey, flattenTree, hasMenu, isExpandable, isFolderKey, isSelec
|
|
|
13
13
|
import { useExplorerLoads } from './use-explorer-loads.js';
|
|
14
14
|
import { usePopupPress } from '../use-popup-press.js';
|
|
15
15
|
import { useStoredExpansion } from './use-stored-expansion.js';
|
|
16
|
-
const INDENT = 16;
|
|
17
|
-
/* Where the chevron slot of the first level starts inside a row. */
|
|
18
|
-
const BASE_INDENT = 4;
|
|
19
|
-
const CHEVRON_SIZE = 16;
|
|
20
16
|
/*
|
|
21
17
|
* The connections, their schemas and their tables as a tree, loaded as each node opens. Every row
|
|
22
18
|
* selects; a double click or Enter on a table asks the app to open it through `useDatabaseAction`.
|
|
@@ -174,18 +170,17 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
174
170
|
const menu = connection !== undefined && hasMenu(row) ? (_jsx(RowMenu, { row: row, connection: connection, onRefresh: refresh, onDisconnect: disconnect, onRequest: setRequest })) : null;
|
|
175
171
|
const selectedRow = isSelected(row);
|
|
176
172
|
const props = {
|
|
177
|
-
|
|
178
|
-
|
|
173
|
+
level: row.level,
|
|
174
|
+
selected: selectedRow,
|
|
175
|
+
joinedStart: selectedRow && isSelected(rows[index - 1]),
|
|
176
|
+
joinedEnd: selectedRow && isSelected(rows[index + 1]),
|
|
177
|
+
interactive: row.focusable,
|
|
179
178
|
'aria-setsize': row.setSize,
|
|
180
179
|
'aria-posinset': row.posInSet,
|
|
181
180
|
'aria-expanded': isExpandable(row) ? row.expanded : undefined,
|
|
182
181
|
'aria-selected': isSelectable(row) ? selectedRow : undefined,
|
|
183
182
|
'aria-busy': row.kind === 'loading' ? true : undefined,
|
|
184
183
|
tabIndex: row.focusable ? (row.key === stop ? 0 : -1) : undefined,
|
|
185
|
-
className: clsx(
|
|
186
|
-
// The line box is the row's height, so the text lands on whole pixels in a row of an odd height.
|
|
187
|
-
'focus-ring relative mx-2 flex h-[25px] min-w-0 items-center gap-1.5 rounded-md pr-2 text-xs leading-[25px] text-text focus-visible:-outline-offset-2', selectedRow ? 'bg-surface-active' : row.focusable && 'hover:bg-surface-hover', selectedRow && isSelected(rows[index - 1]) && 'rounded-t-none', selectedRow && isSelected(rows[index + 1]) && 'rounded-b-none'),
|
|
188
|
-
style: { paddingInlineStart: BASE_INDENT + (row.level - 1) * INDENT },
|
|
189
184
|
ref: (element) => {
|
|
190
185
|
if (element === null) {
|
|
191
186
|
rowElements.current.delete(row.key);
|
|
@@ -202,50 +197,40 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
202
197
|
}
|
|
203
198
|
}
|
|
204
199
|
};
|
|
205
|
-
const content =
|
|
200
|
+
const content = _jsx(RowContent, { row: row, onToggle: isExpandable(row) ? () => toggle(row) : undefined });
|
|
206
201
|
if (menu === null) {
|
|
207
|
-
return (_jsx(
|
|
202
|
+
return (_jsx(Tree.Row, { ...props, children: content }, row.key));
|
|
208
203
|
}
|
|
209
|
-
return (_jsxs(ContextMenu.Root, { children: [_jsx(ContextMenu.Trigger, { render: _jsx(
|
|
204
|
+
return (_jsxs(ContextMenu.Root, { children: [_jsx(ContextMenu.Trigger, { render: _jsx(Tree.Row, { ...props }), children: content }), _jsx(ContextMenu.Popup, { children: menu })] }, row.key));
|
|
210
205
|
};
|
|
211
206
|
return (_jsxs("div", { ref: ref, className: clsx('flex min-h-0 min-w-0 flex-col', className), children: [_jsx("div", { className: "shrink-0 border-b border-border p-2", children: _jsx(Input, { size: "sm", icon: Search, type: "search", value: filter, placeholder: t('explorer.filter'), "aria-label": t('explorer.filter'), spellCheck: false, onChange: (e) => setFilter(e.target.value), onKeyDown: (e) => {
|
|
212
207
|
if (e.key === 'Escape') {
|
|
213
208
|
setFilter('');
|
|
214
209
|
}
|
|
215
|
-
} }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(
|
|
210
|
+
} }) }), connections.length === 0 ? (_jsx(EmptyState, { icon: Database, children: t('explorer.noConnections') })) : rows.length === 0 ? (_jsx(EmptyState, { icon: Search, children: t('explorer.noMatches', { query: filter.trim() }) })) : (_jsx(Tree.Root, { "aria-label": t('explorer.label'), className: "min-h-0 grow overflow-y-auto py-2", onKeyDown: onKeyDown, children: rows.map(renderRow) })), request !== null && _jsx(TableDialog, { request: request, onClose: () => setRequest(null), onDone: finish })] }));
|
|
216
211
|
}
|
|
217
212
|
function RowContent({ row, onToggle }) {
|
|
218
213
|
const { t } = useTranslation('database');
|
|
219
214
|
switch (row.kind) {
|
|
220
215
|
case 'connection':
|
|
221
|
-
return (_jsxs(_Fragment, { children: [_jsx(Chevron, {
|
|
216
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(EngineIcon, { engine: row.connection.config.engine, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.connection.name || t('connections.untitled') }), row.connection.config.readOnly === true && (_jsx(Tooltip, { label: t('explorer.readOnly'), children: _jsx("span", { className: "inline-flex shrink-0 text-text-faint", children: _jsx(Icon, { icon: Lock, size: 12 }) }) })), row.version !== null && _jsx("span", { className: "min-w-0 shrink-[2] truncate text-text-faint", children: row.version }), row.schemaCount !== null && _jsx(SchemaCount, { shown: row.schemaCount.shown, total: row.schemaCount.total })] }));
|
|
222
217
|
case 'schema':
|
|
223
|
-
return (_jsxs(_Fragment, { children: [_jsx(Chevron, {
|
|
218
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Database, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.schema })] }));
|
|
224
219
|
case 'folder':
|
|
225
|
-
return (_jsxs(_Fragment, { children: [_jsx(Chevron, {
|
|
220
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: Folder, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: t(row.group === 'view' ? 'explorer.views' : 'explorer.tables') }), _jsx("span", { className: "shrink-0 text-text-faint tabular-nums", children: formatNumber(row.count) })] }));
|
|
226
221
|
case 'table':
|
|
227
|
-
return (_jsxs(_Fragment, { children: [_jsx(Chevron, {
|
|
222
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.Chevron, { expanded: row.expanded, onExpandedChange: onToggle }), _jsx(Icon, { icon: row.table.kind === 'view' ? Eye : Table, size: 16, className: "shrink-0 text-text-muted" }), _jsx(Tree.Label, { children: row.table.name })] }));
|
|
228
223
|
case 'column':
|
|
229
|
-
return (_jsxs(_Fragment, { children: [_jsx(ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(
|
|
224
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(ColumnIcon, { primaryKey: row.primaryKey, foreignKey: row.foreignKey }), _jsx(Tree.Label, { children: row.column.name }), row.column.type !== '' && _jsx("span", { className: "min-w-0 shrink-[2] truncate text-text-faint", children: row.column.type })] }));
|
|
230
225
|
case 'loading':
|
|
231
|
-
return (_jsxs(_Fragment, { children: [_jsx(ChevronSlot, {}), _jsx(Spinner, { size: 12 }), _jsx("span", { className: "text-text-faint", children: t('explorer.loading') })] }));
|
|
226
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Spinner, { size: 12 }), _jsx("span", { className: "text-text-faint", children: t('explorer.loading') })] }));
|
|
232
227
|
case 'error':
|
|
233
228
|
return _jsx(ErrorContent, { row: row });
|
|
234
229
|
case 'empty':
|
|
235
|
-
return (_jsxs(_Fragment, { children: [_jsx(ChevronSlot, {}), _jsx("span", { className: "text-text-faint", children: t(EMPTY_WORDS[row.of]) })] }));
|
|
230
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx("span", { className: "text-text-faint", children: t(EMPTY_WORDS[row.of]) })] }));
|
|
236
231
|
}
|
|
237
232
|
}
|
|
238
233
|
const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables', columns: 'explorer.noColumns' };
|
|
239
|
-
/* Where a chevron would be, so a row without one lines up with its siblings that have one. */
|
|
240
|
-
function ChevronSlot() {
|
|
241
|
-
return _jsx("span", { className: "size-4 shrink-0" });
|
|
242
|
-
}
|
|
243
|
-
function Chevron({ open, onToggle }) {
|
|
244
|
-
return (_jsx("span", { className: "grid size-4 shrink-0 place-items-center text-text-muted", onClick: (e) => {
|
|
245
|
-
e.stopPropagation();
|
|
246
|
-
onToggle?.();
|
|
247
|
-
}, onDoubleClick: (e) => e.stopPropagation(), children: _jsx(Icon, { icon: ChevronRight, size: 12, className: clsx('transition-transform', open && 'rotate-90') }) }));
|
|
248
|
-
}
|
|
249
234
|
function SchemaCount({ shown, total }) {
|
|
250
235
|
const { t } = useTranslation('database');
|
|
251
236
|
const counts = { shown: formatNumber(shown), total: formatNumber(total) };
|
|
@@ -260,5 +245,5 @@ function ColumnIcon({ primaryKey, foreignKey }) {
|
|
|
260
245
|
}
|
|
261
246
|
function ErrorContent({ row }) {
|
|
262
247
|
const { t } = useTranslation('database');
|
|
263
|
-
return (_jsxs(_Fragment, { children: [_jsx(ChevronSlot, {}), _jsx(Icon, { icon: CircleAlert, size: 16, className: "shrink-0 text-status-error" }), _jsx(Tooltip, { label: row.message, children: _jsx("span", { className: "min-w-0 truncate text-status-error", children: row.message }) }), _jsx(Button, { size: "xs", variant: "ghost", className: "ml-auto", tabIndex: -1, children: t('explorer.retry') })] }));
|
|
248
|
+
return (_jsxs(_Fragment, { children: [_jsx(Tree.ChevronSlot, {}), _jsx(Icon, { icon: CircleAlert, size: 16, className: "shrink-0 text-status-error" }), _jsx(Tooltip, { label: row.message, children: _jsx("span", { className: "min-w-0 truncate text-status-error", children: row.message }) }), _jsx(Button, { size: "xs", variant: "ghost", className: "ml-auto", tabIndex: -1, children: t('explorer.retry') })] }));
|
|
264
249
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/database",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-beta.2",
|
|
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
|
-
"license": "MIT",
|
|
5
|
+
"license": "FSL-1.1-MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
@@ -61,13 +61,13 @@
|
|
|
61
61
|
"lucide-react": "^1.47.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.17.0-beta.2",
|
|
65
|
+
"@adecore/database-linux-arm64": "0.17.0-beta.2",
|
|
66
|
+
"@adecore/database-linux-x64": "0.17.0-beta.2",
|
|
67
|
+
"@adecore/database-win32-x64": "0.17.0-beta.2"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@adecore/ui": "^0.
|
|
70
|
+
"@adecore/ui": "^0.17.0-beta.2",
|
|
71
71
|
"i18next": "^26.4.2",
|
|
72
72
|
"react": "^19.3.0",
|
|
73
73
|
"react-dom": "^19.3.0",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode, type Ref } from 'react';
|
|
2
2
|
import clsx from 'clsx';
|
|
3
|
-
import {
|
|
3
|
+
import { CircleAlert, Database, Eye, Folder, KeyRound, Link2, Lock, RectangleVertical, Search, Table } from 'lucide-react';
|
|
4
4
|
import { useTranslation } from 'react-i18next';
|
|
5
|
-
import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip } from '@adecore/ui';
|
|
5
|
+
import { Button, ContextMenu, EmptyState, Icon, Input, Spinner, Tooltip, Tree } from '@adecore/ui';
|
|
6
6
|
import { formatNumber } from '@adecore/ui/format';
|
|
7
7
|
import type { ExplorerSelection } from '../actions.ts';
|
|
8
8
|
import { useDatabaseAction, useDatabaseClient } from '../client-context.ts';
|
|
@@ -32,11 +32,6 @@ import { useExplorerLoads } from './use-explorer-loads.ts';
|
|
|
32
32
|
import { usePopupPress } from '../use-popup-press.ts';
|
|
33
33
|
import { useStoredExpansion } from './use-stored-expansion.ts';
|
|
34
34
|
|
|
35
|
-
const INDENT = 16;
|
|
36
|
-
/* Where the chevron slot of the first level starts inside a row. */
|
|
37
|
-
const BASE_INDENT = 4;
|
|
38
|
-
const CHEVRON_SIZE = 16;
|
|
39
|
-
|
|
40
35
|
export interface DatabaseExplorerProps {
|
|
41
36
|
connections: readonly Connection[];
|
|
42
37
|
/* The selected connection, schema or table. Without it the explorer keeps the selection itself, starting at `defaultValue`. */
|
|
@@ -221,22 +216,17 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
221
216
|
) : null;
|
|
222
217
|
const selectedRow = isSelected(row);
|
|
223
218
|
const props = {
|
|
224
|
-
|
|
225
|
-
|
|
219
|
+
level: row.level,
|
|
220
|
+
selected: selectedRow,
|
|
221
|
+
joinedStart: selectedRow && isSelected(rows[index - 1]),
|
|
222
|
+
joinedEnd: selectedRow && isSelected(rows[index + 1]),
|
|
223
|
+
interactive: row.focusable,
|
|
226
224
|
'aria-setsize': row.setSize,
|
|
227
225
|
'aria-posinset': row.posInSet,
|
|
228
226
|
'aria-expanded': isExpandable(row) ? row.expanded : undefined,
|
|
229
227
|
'aria-selected': isSelectable(row) ? selectedRow : undefined,
|
|
230
228
|
'aria-busy': row.kind === 'loading' ? true : undefined,
|
|
231
229
|
tabIndex: row.focusable ? (row.key === stop ? 0 : -1) : undefined,
|
|
232
|
-
className: clsx(
|
|
233
|
-
// The line box is the row's height, so the text lands on whole pixels in a row of an odd height.
|
|
234
|
-
'focus-ring relative mx-2 flex h-[25px] min-w-0 items-center gap-1.5 rounded-md pr-2 text-xs leading-[25px] text-text focus-visible:-outline-offset-2',
|
|
235
|
-
selectedRow ? 'bg-surface-active' : row.focusable && 'hover:bg-surface-hover',
|
|
236
|
-
selectedRow && isSelected(rows[index - 1]) && 'rounded-t-none',
|
|
237
|
-
selectedRow && isSelected(rows[index + 1]) && 'rounded-b-none'
|
|
238
|
-
),
|
|
239
|
-
style: { paddingInlineStart: BASE_INDENT + (row.level - 1) * INDENT },
|
|
240
230
|
ref: (element: HTMLElement | null) => {
|
|
241
231
|
if (element === null) {
|
|
242
232
|
rowElements.current.delete(row.key);
|
|
@@ -252,29 +242,17 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
252
242
|
}
|
|
253
243
|
}
|
|
254
244
|
};
|
|
255
|
-
const content = (
|
|
256
|
-
<>
|
|
257
|
-
{Array.from({ length: row.level - 1 }, (_, i) => (
|
|
258
|
-
<span
|
|
259
|
-
key={i}
|
|
260
|
-
aria-hidden
|
|
261
|
-
className="pointer-events-none absolute inset-y-0 w-px bg-border"
|
|
262
|
-
style={{ insetInlineStart: BASE_INDENT + i * INDENT + CHEVRON_SIZE / 2 - 1 }}
|
|
263
|
-
/>
|
|
264
|
-
))}
|
|
265
|
-
<RowContent row={row} onToggle={isExpandable(row) ? () => toggle(row) : undefined} />
|
|
266
|
-
</>
|
|
267
|
-
);
|
|
245
|
+
const content = <RowContent row={row} onToggle={isExpandable(row) ? () => toggle(row) : undefined} />;
|
|
268
246
|
if (menu === null) {
|
|
269
247
|
return (
|
|
270
|
-
<
|
|
248
|
+
<Tree.Row key={row.key} {...props}>
|
|
271
249
|
{content}
|
|
272
|
-
</
|
|
250
|
+
</Tree.Row>
|
|
273
251
|
);
|
|
274
252
|
}
|
|
275
253
|
return (
|
|
276
254
|
<ContextMenu.Root key={row.key}>
|
|
277
|
-
<ContextMenu.Trigger render={<
|
|
255
|
+
<ContextMenu.Trigger render={<Tree.Row {...props} />}>{content}</ContextMenu.Trigger>
|
|
278
256
|
<ContextMenu.Popup>{menu}</ContextMenu.Popup>
|
|
279
257
|
</ContextMenu.Root>
|
|
280
258
|
);
|
|
@@ -304,9 +282,9 @@ export function DatabaseExplorer({ connections, value, defaultValue = null, onVa
|
|
|
304
282
|
) : rows.length === 0 ? (
|
|
305
283
|
<EmptyState icon={Search}>{t('explorer.noMatches', { query: filter.trim() })}</EmptyState>
|
|
306
284
|
) : (
|
|
307
|
-
<
|
|
285
|
+
<Tree.Root aria-label={t('explorer.label')} className="min-h-0 grow overflow-y-auto py-2" onKeyDown={onKeyDown}>
|
|
308
286
|
{rows.map(renderRow)}
|
|
309
|
-
</
|
|
287
|
+
</Tree.Root>
|
|
310
288
|
)}
|
|
311
289
|
{request !== null && <TableDialog request={request} onClose={() => setRequest(null)} onDone={finish} />}
|
|
312
290
|
</div>
|
|
@@ -319,9 +297,9 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
319
297
|
case 'connection':
|
|
320
298
|
return (
|
|
321
299
|
<>
|
|
322
|
-
<Chevron
|
|
300
|
+
<Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
|
|
323
301
|
<EngineIcon engine={row.connection.config.engine} size={16} className="shrink-0 text-text-muted" />
|
|
324
|
-
<
|
|
302
|
+
<Tree.Label>{row.connection.name || t('connections.untitled')}</Tree.Label>
|
|
325
303
|
{row.connection.config.readOnly === true && (
|
|
326
304
|
<Tooltip label={t('explorer.readOnly')}>
|
|
327
305
|
<span className="inline-flex shrink-0 text-text-faint">
|
|
@@ -336,41 +314,41 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
336
314
|
case 'schema':
|
|
337
315
|
return (
|
|
338
316
|
<>
|
|
339
|
-
<Chevron
|
|
317
|
+
<Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
|
|
340
318
|
<Icon icon={Database} size={16} className="shrink-0 text-text-muted" />
|
|
341
|
-
<
|
|
319
|
+
<Tree.Label>{row.schema}</Tree.Label>
|
|
342
320
|
</>
|
|
343
321
|
);
|
|
344
322
|
case 'folder':
|
|
345
323
|
return (
|
|
346
324
|
<>
|
|
347
|
-
<Chevron
|
|
325
|
+
<Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
|
|
348
326
|
<Icon icon={Folder} size={16} className="shrink-0 text-text-muted" />
|
|
349
|
-
<
|
|
327
|
+
<Tree.Label>{t(row.group === 'view' ? 'explorer.views' : 'explorer.tables')}</Tree.Label>
|
|
350
328
|
<span className="shrink-0 text-text-faint tabular-nums">{formatNumber(row.count)}</span>
|
|
351
329
|
</>
|
|
352
330
|
);
|
|
353
331
|
case 'table':
|
|
354
332
|
return (
|
|
355
333
|
<>
|
|
356
|
-
<Chevron
|
|
334
|
+
<Tree.Chevron expanded={row.expanded} onExpandedChange={onToggle} />
|
|
357
335
|
<Icon icon={row.table.kind === 'view' ? Eye : Table} size={16} className="shrink-0 text-text-muted" />
|
|
358
|
-
<
|
|
336
|
+
<Tree.Label>{row.table.name}</Tree.Label>
|
|
359
337
|
</>
|
|
360
338
|
);
|
|
361
339
|
case 'column':
|
|
362
340
|
return (
|
|
363
341
|
<>
|
|
364
|
-
<ChevronSlot />
|
|
342
|
+
<Tree.ChevronSlot />
|
|
365
343
|
<ColumnIcon primaryKey={row.primaryKey} foreignKey={row.foreignKey} />
|
|
366
|
-
<
|
|
344
|
+
<Tree.Label>{row.column.name}</Tree.Label>
|
|
367
345
|
{row.column.type !== '' && <span className="min-w-0 shrink-[2] truncate text-text-faint">{row.column.type}</span>}
|
|
368
346
|
</>
|
|
369
347
|
);
|
|
370
348
|
case 'loading':
|
|
371
349
|
return (
|
|
372
350
|
<>
|
|
373
|
-
<ChevronSlot />
|
|
351
|
+
<Tree.ChevronSlot />
|
|
374
352
|
<Spinner size={12} />
|
|
375
353
|
<span className="text-text-faint">{t('explorer.loading')}</span>
|
|
376
354
|
</>
|
|
@@ -380,7 +358,7 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
380
358
|
case 'empty':
|
|
381
359
|
return (
|
|
382
360
|
<>
|
|
383
|
-
<ChevronSlot />
|
|
361
|
+
<Tree.ChevronSlot />
|
|
384
362
|
<span className="text-text-faint">{t(EMPTY_WORDS[row.of])}</span>
|
|
385
363
|
</>
|
|
386
364
|
);
|
|
@@ -389,26 +367,6 @@ function RowContent({ row, onToggle }: { row: TreeRow; onToggle?: () => void })
|
|
|
389
367
|
|
|
390
368
|
const EMPTY_WORDS = { schemas: 'explorer.noSchemas', tables: 'explorer.noTables', columns: 'explorer.noColumns' } as const;
|
|
391
369
|
|
|
392
|
-
/* Where a chevron would be, so a row without one lines up with its siblings that have one. */
|
|
393
|
-
function ChevronSlot() {
|
|
394
|
-
return <span className="size-4 shrink-0" />;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function Chevron({ open, onToggle }: { open: boolean; onToggle?: () => void }) {
|
|
398
|
-
return (
|
|
399
|
-
<span
|
|
400
|
-
className="grid size-4 shrink-0 place-items-center text-text-muted"
|
|
401
|
-
onClick={(e) => {
|
|
402
|
-
e.stopPropagation();
|
|
403
|
-
onToggle?.();
|
|
404
|
-
}}
|
|
405
|
-
onDoubleClick={(e) => e.stopPropagation()}
|
|
406
|
-
>
|
|
407
|
-
<Icon icon={ChevronRight} size={12} className={clsx('transition-transform', open && 'rotate-90')} />
|
|
408
|
-
</span>
|
|
409
|
-
);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
370
|
function SchemaCount({ shown, total }: { shown: number; total: number }) {
|
|
413
371
|
const { t } = useTranslation('database');
|
|
414
372
|
const counts = { shown: formatNumber(shown), total: formatNumber(total) };
|
|
@@ -437,7 +395,7 @@ function ErrorContent({ row }: { row: ErrorRow }) {
|
|
|
437
395
|
const { t } = useTranslation('database');
|
|
438
396
|
return (
|
|
439
397
|
<>
|
|
440
|
-
<ChevronSlot />
|
|
398
|
+
<Tree.ChevronSlot />
|
|
441
399
|
<Icon icon={CircleAlert} size={16} className="shrink-0 text-status-error" />
|
|
442
400
|
<Tooltip label={row.message}>
|
|
443
401
|
<span className="min-w-0 truncate text-status-error">{row.message}</span>
|