@adecore/database 0.16.0-beta.0 → 0.17.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adecore/database",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.0-beta.1",
|
|
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": "MIT",
|
|
6
6
|
"author": "Bas Milius",
|
|
@@ -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.1",
|
|
65
|
+
"@adecore/database-linux-arm64": "0.17.0-beta.1",
|
|
66
|
+
"@adecore/database-linux-x64": "0.17.0-beta.1",
|
|
67
|
+
"@adecore/database-win32-x64": "0.17.0-beta.1"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@adecore/ui": "^0.
|
|
70
|
+
"@adecore/ui": "^0.17.0-beta.1",
|
|
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>
|