@marimo-team/frontend 0.23.10 → 0.23.11-dev10
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/assets/{CellStatus-e0ex7Iei.js → CellStatus-CLGvVxcw.js} +1 -1
- package/dist/assets/{JsonOutput-DRNPZOvX.js → JsonOutput-uEJijGXp.js} +5 -5
- package/dist/assets/{MarimoErrorOutput-BH6hs0Ir.js → MarimoErrorOutput-DzoKyXWR.js} +2 -2
- package/dist/assets/{RenderHTML-BQ1PO4Wd.js → RenderHTML-DAt48X-F.js} +1 -1
- package/dist/assets/{RunButton-F8pLIvFp.js → RunButton-B7msyyYi.js} +1 -1
- package/dist/assets/{add-cell-with-ai-Bd_3tPEt.js → add-cell-with-ai-CAkcousR.js} +20 -20
- package/dist/assets/{add-connection-dialog-AhwxOztN.js → add-connection-dialog-ZyXXfAVG.js} +32 -32
- package/dist/assets/{agent-panel-Dm0KI1sF.js → agent-panel-BfIguQXh.js} +6 -6
- package/dist/assets/{ai-model-dropdown-CG4B4rqH.js → ai-model-dropdown-rE64ytSX.js} +3 -3
- package/dist/assets/{app-config-button-D_sFrSql.js → app-config-button-DLRZ5d9c.js} +1 -1
- package/dist/assets/{cell-editor-B3aYYyAI.js → cell-editor-2JIpvFn4.js} +15 -12
- package/dist/assets/{cell-link-Bj4-yOIx.js → cell-link-DY95GSb7.js} +1 -1
- package/dist/assets/{cells-DOA0Gew8.js → cells-D-v2lBet.js} +67 -67
- package/dist/assets/{chat-display-DI0jRLIv.js → chat-display-DO-tqsbY.js} +1 -1
- package/dist/assets/{chat-panel-BU4HHdf5.js → chat-panel-BT5gLfs6.js} +2 -2
- package/dist/assets/{chat-ui-C7igY2w5.js → chat-ui-Be4yDBZS.js} +4 -4
- package/dist/assets/column-preview-BzvFdLI5.js +1 -0
- package/dist/assets/{command-palette-Bjv1Z7v8.js → command-palette-CKKzfpGt.js} +1 -1
- package/dist/assets/{common-fDFYY_sv.js → common-Do4N1uVK.js} +1 -1
- package/dist/assets/{components--C6N-DXq.js → components-DUC0f1XD.js} +1 -1
- package/dist/assets/{datasource-CR6RRpTi.js → datasource-BQGI7c_u.js} +2 -2
- package/dist/assets/{dependency-graph-panel-BEgkvdX0.js → dependency-graph-panel-Czoya7c2.js} +1 -1
- package/dist/assets/{documentation-panel-HvbKykbI.js → documentation-panel-LokVYDIZ.js} +1 -1
- package/dist/assets/{download-DhxnAw14.js → download-WGU5w_3m.js} +3 -3
- package/dist/assets/{edit-page-BeWwLeT8.js → edit-page-BPQO1Uuz.js} +6 -6
- package/dist/assets/{error-panel-WLBQ3q9p.js → error-panel-BCm_e4eM.js} +1 -1
- package/dist/assets/{file-explorer-panel-DtzGlnzT.js → file-explorer-panel-D1NN-z8x.js} +3 -3
- package/dist/assets/{file-icons-D2f3nfbq.js → file-icons-CF-YT4hq.js} +1 -1
- package/dist/assets/{file-name-input-BNYf9WWM.js → file-name-input-BRVXQ3OU.js} +1 -1
- package/dist/assets/{floating-outline-BvHFWRoz.js → floating-outline-D33Zu-Ad.js} +1 -1
- package/dist/assets/{focus-Ldqh99xE.js → focus-BfFAxl9X.js} +1 -1
- package/dist/assets/{form-CxBAInfg.js → form-CVBFx2z3.js} +1 -1
- package/dist/assets/{home-page-DwFpLpXK.js → home-page-CL9Hv1Hg.js} +2 -2
- package/dist/assets/{hooks-DyMacA-R.js → hooks-83y0XCiC.js} +1 -1
- package/dist/assets/{html-to-image-CyAtzePO.js → html-to-image-IPNqWX7V.js} +2 -2
- package/dist/assets/index-BNWrEIlp.css +2 -0
- package/dist/assets/{index-fNBoXCyz.js → index-CuhY66ZR.js} +14 -14
- package/dist/assets/{kiosk-mode-CTWHjzXs.js → kiosk-mode-BSufcIZH.js} +1 -1
- package/dist/assets/{layout-dRNPwA-7.js → layout-ks0WJ2vR.js} +5 -5
- package/dist/assets/{logs-panel-BVLYycQb.js → logs-panel-DXN6sUaA.js} +1 -1
- package/dist/assets/{markdown-renderer-C9Ujvj0b.js → markdown-renderer-DWRPo52L.js} +1 -1
- package/dist/assets/{name-cell-input-DabvuruX.js → name-cell-input-BiDIx6YQ.js} +1 -1
- package/dist/assets/{outline-panel-B-ncbNWI.js → outline-panel-DVSGNRgP.js} +1 -1
- package/dist/assets/{packages-panel-CESeW_3T.js → packages-panel-DTX6CBnm.js} +1 -1
- package/dist/assets/{panels-BTEEcvYR.js → panels-NhGV0SBq.js} +1 -1
- package/dist/assets/{process-output-pPgH0ANl.js → process-output-BiWV34b7.js} +1 -1
- package/dist/assets/{radio-group-D7rh6Zek.js → radio-group-DV-lQvil.js} +1 -1
- package/dist/assets/{readonly-python-code-CvPx4CU_.js → readonly-python-code-B0glFTKW.js} +1 -1
- package/dist/assets/{reveal-component-DGQQBmed.js → reveal-component-C2M2FixA.js} +12 -12
- package/dist/assets/{run-page-yFxABzXq.js → run-page-B6I-Lshx.js} +1 -1
- package/dist/assets/{scratchpad-panel-B_nxW99u.js → scratchpad-panel-DU7perVk.js} +1 -1
- package/dist/assets/session-panel-B4UTmf-j.js +1 -0
- package/dist/assets/{snippets-panel-BSgcoo-M.js → snippets-panel-KRdl4bVk.js} +1 -1
- package/dist/assets/{state-BWM3qRkR.js → state-BSm0OM93.js} +1 -1
- package/dist/assets/{state-C8yHPSLN.js → state-CbkvENzk.js} +2 -2
- package/dist/assets/{textarea-bBuxbFm7.js → textarea-CBbixTxi.js} +1 -1
- package/dist/assets/{tracing-pt7eDHWc.js → tracing-D8LGpuXa.js} +1 -1
- package/dist/assets/{tracing-panel-BveZ5DZw.js → tracing-panel-TUP6kAd5.js} +2 -2
- package/dist/assets/{tree-actions-1KQDSu1H.js → tree-actions-BvrKFU0g.js} +1 -1
- package/dist/assets/{useCellActionButton-C4V7J6PS.js → useCellActionButton-CtOZIXD0.js} +1 -1
- package/dist/assets/{useDeleteCell-BvnurAZ9.js → useDeleteCell-BLvzLWAT.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-DNlXaxbt.js → useDependencyPanelTab-BP0sNfUz.js} +1 -1
- package/dist/assets/{useNotebookActions-Dw6tY2qa.js → useNotebookActions-BI5dWutQ.js} +1 -1
- package/dist/assets/{useRunCells-cSZpNqnR.js → useRunCells-BClkx9Pq.js} +1 -1
- package/dist/assets/{useSplitCell-s7dSiBUa.js → useSplitCell-CpddCX_u.js} +1 -1
- package/dist/index.html +23 -23
- package/package.json +1 -1
- package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
- package/src/components/datasources/__tests__/filter-empty.test.ts +81 -0
- package/src/components/datasources/__tests__/utils.test.ts +62 -1
- package/src/components/datasources/column-preview.tsx +2 -4
- package/src/components/datasources/components.tsx +15 -7
- package/src/components/datasources/datasources.tsx +311 -178
- package/src/components/datasources/utils.ts +40 -1
- package/src/components/editor/ai/ai-completion-editor.tsx +6 -5
- package/src/components/editor/connections/components.tsx +13 -0
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +4 -4
- package/src/components/editor/connections/storage/as-code.ts +11 -4
- package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +133 -0
- package/src/core/ai/stream-completion-text.ts +48 -0
- package/src/core/ai/strip-wrapping-backticks.ts +88 -0
- package/src/core/cells/__tests__/cells.test.ts +33 -0
- package/src/core/cells/cells.ts +1 -1
- package/src/core/codemirror/ai/request.ts +2 -14
- package/src/core/datasets/__tests__/data-source.test.ts +226 -0
- package/src/core/datasets/data-source-connections.ts +88 -24
- package/dist/assets/column-preview-nu3Qo2OW.js +0 -1
- package/dist/assets/index-BAYF7dcV.css +0 -2
- package/dist/assets/session-panel-CTJPYbAa.js +0 -1
|
@@ -52,6 +52,7 @@ import { useRequestClient } from "@/core/network/requests";
|
|
|
52
52
|
import { variablesAtom } from "@/core/variables/state";
|
|
53
53
|
import type { VariableName } from "@/core/variables/types";
|
|
54
54
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
55
|
+
import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
|
|
55
56
|
import { sortBy } from "@/utils/arrays";
|
|
56
57
|
import { logNever } from "@/utils/assertNever";
|
|
57
58
|
import { cn } from "@/utils/cn";
|
|
@@ -77,24 +78,44 @@ import {
|
|
|
77
78
|
LoadingState,
|
|
78
79
|
RotatingChevron,
|
|
79
80
|
} from "./components";
|
|
80
|
-
import {
|
|
81
|
+
import {
|
|
82
|
+
areChildSchemasResolved,
|
|
83
|
+
areSchemasResolved,
|
|
84
|
+
areTablesResolved,
|
|
85
|
+
isSchemaless,
|
|
86
|
+
sqlCode,
|
|
87
|
+
tableUniqueId,
|
|
88
|
+
} from "./utils";
|
|
89
|
+
|
|
90
|
+
const INDENT_STEP = 1; // rem per schema nesting level (depth 0 = top-level)
|
|
91
|
+
|
|
92
|
+
// Indentation (rem) for a schema and its contents at a given nesting depth.
|
|
93
|
+
// Depth 0 is a top-level schema; schemaless tables/columns reuse depth 0 too.
|
|
94
|
+
function schemaHeaderIndentRem(depth: number): number {
|
|
95
|
+
return 1.75 + depth * INDENT_STEP;
|
|
96
|
+
}
|
|
97
|
+
function schemaTableIndentRem(depth: number): number {
|
|
98
|
+
return 3 + depth * INDENT_STEP;
|
|
99
|
+
}
|
|
100
|
+
function schemaColumnIndentRem(depth: number): number {
|
|
101
|
+
return 3.25 + depth * INDENT_STEP;
|
|
102
|
+
}
|
|
81
103
|
|
|
82
|
-
//
|
|
104
|
+
// Left indentation (rem) for each fixed (non-nested) level of the tree.
|
|
83
105
|
const INDENT = {
|
|
84
|
-
engineEmpty:
|
|
85
|
-
engine:
|
|
86
|
-
database:
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
tableSchemaless: "pl-8",
|
|
92
|
-
tableWithSchema: "pl-12",
|
|
93
|
-
columnLocal: "pl-5",
|
|
94
|
-
columnSql: "pl-13",
|
|
95
|
-
columnPreview: "pl-10",
|
|
106
|
+
engineEmpty: 0.75,
|
|
107
|
+
engine: 0.75,
|
|
108
|
+
database: 1,
|
|
109
|
+
tableLoading: 2.75,
|
|
110
|
+
tableSchemaless: 2,
|
|
111
|
+
columnLocal: 1.25,
|
|
112
|
+
columnPreview: 2.5,
|
|
96
113
|
};
|
|
97
114
|
|
|
115
|
+
function indentStyle(rem: number): React.CSSProperties {
|
|
116
|
+
return { paddingLeft: `${rem}rem` };
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
const sortedTablesAtom = atom((get) => {
|
|
99
120
|
const tables = get(datasetTablesAtom);
|
|
100
121
|
const variables = get(variablesAtom);
|
|
@@ -132,14 +153,39 @@ export const hideEmptyDatasourcesAtom = atomWithStorage<boolean>(
|
|
|
132
153
|
{ getOnInit: true },
|
|
133
154
|
);
|
|
134
155
|
|
|
135
|
-
|
|
136
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Recursively hide schemas confirmed empty (no tables and no visible child
|
|
158
|
+
* schemas). Deferred schemas are kept so the user can expand them.
|
|
159
|
+
*/
|
|
160
|
+
function filterEmptySchemas(schemas: DatabaseSchema[]): DatabaseSchema[] {
|
|
161
|
+
let changed = false;
|
|
162
|
+
const result: DatabaseSchema[] = [];
|
|
163
|
+
for (const schema of schemas) {
|
|
164
|
+
if (!areTablesResolved(schema) || !areChildSchemasResolved(schema)) {
|
|
165
|
+
result.push(schema);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const childSchemas = schema.child_schemas ?? [];
|
|
169
|
+
const visibleChildren = filterEmptySchemas(childSchemas);
|
|
170
|
+
if (schema.tables.length === 0 && visibleChildren.length === 0) {
|
|
171
|
+
changed = true;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (visibleChildren === childSchemas) {
|
|
175
|
+
result.push(schema);
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
changed = true;
|
|
179
|
+
result.push({ ...schema, child_schemas: visibleChildren });
|
|
180
|
+
}
|
|
181
|
+
return changed ? result : schemas;
|
|
137
182
|
}
|
|
138
183
|
|
|
139
184
|
/**
|
|
140
185
|
* Apply the "hide empty" filter to a connection's databases.
|
|
141
186
|
*
|
|
142
|
-
* - Schemas with confirmed-empty table lists are
|
|
187
|
+
* - Schemas with confirmed-empty table lists (and no child schemas) are
|
|
188
|
+
* hidden, recursively.
|
|
143
189
|
* - Databases are hidden when either (a) their schemas have been enumerated
|
|
144
190
|
* and the list is empty, or (b) every schema in them was hidden by the
|
|
145
191
|
* schema-level filter.
|
|
@@ -152,7 +198,7 @@ export function filterEmptyDatabases(databases: Database[]): Database[] {
|
|
|
152
198
|
const result: Database[] = [];
|
|
153
199
|
for (const database of databases) {
|
|
154
200
|
// Known-empty database: schema list was enumerated and is empty.
|
|
155
|
-
if (database
|
|
201
|
+
if (areSchemasResolved(database) && database.schemas.length === 0) {
|
|
156
202
|
changed = true;
|
|
157
203
|
continue;
|
|
158
204
|
}
|
|
@@ -161,14 +207,12 @@ export function filterEmptyDatabases(databases: Database[]): Database[] {
|
|
|
161
207
|
result.push(database);
|
|
162
208
|
continue;
|
|
163
209
|
}
|
|
164
|
-
const visibleSchemas = database.schemas
|
|
165
|
-
(schema) => !isKnownEmptySchema(schema),
|
|
166
|
-
);
|
|
210
|
+
const visibleSchemas = filterEmptySchemas(database.schemas);
|
|
167
211
|
if (visibleSchemas.length === 0) {
|
|
168
212
|
changed = true;
|
|
169
213
|
continue;
|
|
170
214
|
}
|
|
171
|
-
if (visibleSchemas
|
|
215
|
+
if (visibleSchemas === database.schemas) {
|
|
172
216
|
result.push(database);
|
|
173
217
|
continue;
|
|
174
218
|
}
|
|
@@ -312,29 +356,19 @@ export const DataSources: React.FC = () => {
|
|
|
312
356
|
hasChildren={connection.databases.length > 0}
|
|
313
357
|
>
|
|
314
358
|
{connection.databases.map((database) => (
|
|
315
|
-
<
|
|
359
|
+
<DatabaseTree
|
|
316
360
|
key={database.name}
|
|
317
|
-
|
|
361
|
+
connection={connection}
|
|
318
362
|
database={database}
|
|
319
363
|
hasSearch={hasSearch}
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
schemas={database.schemas}
|
|
323
|
-
defaultSchema={connection.default_schema}
|
|
324
|
-
defaultDatabase={connection.default_database}
|
|
325
|
-
engineName={connection.name}
|
|
326
|
-
databaseName={database.name}
|
|
327
|
-
hasSearch={hasSearch}
|
|
328
|
-
searchValue={searchValue}
|
|
329
|
-
dialect={connection.dialect}
|
|
330
|
-
/>
|
|
331
|
-
</DatabaseItem>
|
|
364
|
+
searchValue={searchValue}
|
|
365
|
+
/>
|
|
332
366
|
))}
|
|
333
367
|
</Engine>
|
|
334
368
|
))}
|
|
335
369
|
|
|
336
370
|
{dataConnections.length > 0 && tables.length > 0 && (
|
|
337
|
-
<DatasourceLabel className={INDENT.engine}>
|
|
371
|
+
<DatasourceLabel className="pr-2" style={indentStyle(INDENT.engine)}>
|
|
338
372
|
<PythonIcon className="h-4 w-4 text-muted-foreground" />
|
|
339
373
|
<span className="text-xs">Python</span>
|
|
340
374
|
</DatasourceLabel>
|
|
@@ -365,7 +399,7 @@ const Engine: React.FC<{
|
|
|
365
399
|
|
|
366
400
|
return (
|
|
367
401
|
<>
|
|
368
|
-
<DatasourceLabel className={INDENT.engine}>
|
|
402
|
+
<DatasourceLabel className="pr-2" style={indentStyle(INDENT.engine)}>
|
|
369
403
|
<DatabaseLogo
|
|
370
404
|
className="h-4 w-4 text-muted-foreground"
|
|
371
405
|
name={connection.dialect}
|
|
@@ -388,13 +422,96 @@ const Engine: React.FC<{
|
|
|
388
422
|
) : (
|
|
389
423
|
<EmptyState
|
|
390
424
|
content="No databases available"
|
|
391
|
-
|
|
425
|
+
style={indentStyle(INDENT.engineEmpty)}
|
|
392
426
|
/>
|
|
393
427
|
)}
|
|
394
428
|
</>
|
|
395
429
|
);
|
|
396
430
|
};
|
|
397
431
|
|
|
432
|
+
interface DataSourceTree {
|
|
433
|
+
defaultSchema?: string | null;
|
|
434
|
+
defaultDatabase?: string | null;
|
|
435
|
+
dialect: string;
|
|
436
|
+
engineName: string;
|
|
437
|
+
databaseName: string;
|
|
438
|
+
hasSearch: boolean;
|
|
439
|
+
searchValue?: string;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const DataSourceTreeContext = React.createContext<DataSourceTree | null>(null);
|
|
443
|
+
|
|
444
|
+
function useDataSourceTree(): DataSourceTree {
|
|
445
|
+
const tree = React.useContext(DataSourceTreeContext);
|
|
446
|
+
if (tree == null) {
|
|
447
|
+
throw new Error(
|
|
448
|
+
"useDataSourceTree must be used within a DataSourceTreeContext.Provider",
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
return tree;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Build the table context for a (possibly schemaless) schema
|
|
455
|
+
function buildSqlTableContext(
|
|
456
|
+
tree: DataSourceTree,
|
|
457
|
+
{ schema, schemaPath }: { schema: string; schemaPath: string[] },
|
|
458
|
+
): SQLTableContext {
|
|
459
|
+
return {
|
|
460
|
+
engine: tree.engineName,
|
|
461
|
+
database: tree.databaseName,
|
|
462
|
+
schema,
|
|
463
|
+
schemaPath,
|
|
464
|
+
defaultSchema: tree.defaultSchema,
|
|
465
|
+
defaultDatabase: tree.defaultDatabase,
|
|
466
|
+
dialect: tree.dialect,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const DatabaseTree: React.FC<{
|
|
471
|
+
connection: DataSourceConnection;
|
|
472
|
+
database: Database;
|
|
473
|
+
hasSearch: boolean;
|
|
474
|
+
searchValue?: string;
|
|
475
|
+
}> = ({ connection, database, hasSearch, searchValue }) => {
|
|
476
|
+
const tree = React.useMemo<DataSourceTree>(
|
|
477
|
+
() => ({
|
|
478
|
+
engineName: connection.name,
|
|
479
|
+
databaseName: database.name,
|
|
480
|
+
dialect: connection.dialect,
|
|
481
|
+
defaultSchema: connection.default_schema,
|
|
482
|
+
defaultDatabase: connection.default_database,
|
|
483
|
+
hasSearch,
|
|
484
|
+
searchValue,
|
|
485
|
+
}),
|
|
486
|
+
[
|
|
487
|
+
connection.name,
|
|
488
|
+
connection.dialect,
|
|
489
|
+
connection.default_schema,
|
|
490
|
+
connection.default_database,
|
|
491
|
+
database.name,
|
|
492
|
+
hasSearch,
|
|
493
|
+
searchValue,
|
|
494
|
+
],
|
|
495
|
+
);
|
|
496
|
+
|
|
497
|
+
return (
|
|
498
|
+
<DatabaseItem
|
|
499
|
+
engineName={connection.name}
|
|
500
|
+
database={database}
|
|
501
|
+
hasSearch={hasSearch}
|
|
502
|
+
>
|
|
503
|
+
<DataSourceTreeContext.Provider value={tree}>
|
|
504
|
+
<SchemaList
|
|
505
|
+
schemas={database.schemas}
|
|
506
|
+
schemasResolved={areSchemasResolved(database)}
|
|
507
|
+
schemaPath={[]}
|
|
508
|
+
depth={0}
|
|
509
|
+
/>
|
|
510
|
+
</DataSourceTreeContext.Provider>
|
|
511
|
+
</DatabaseItem>
|
|
512
|
+
);
|
|
513
|
+
};
|
|
514
|
+
|
|
398
515
|
const DatabaseItem: React.FC<{
|
|
399
516
|
hasSearch: boolean;
|
|
400
517
|
engineName: string;
|
|
@@ -413,10 +530,8 @@ const DatabaseItem: React.FC<{
|
|
|
413
530
|
return (
|
|
414
531
|
<>
|
|
415
532
|
<CommandItem
|
|
416
|
-
className=
|
|
417
|
-
|
|
418
|
-
INDENT.database,
|
|
419
|
-
)}
|
|
533
|
+
className="text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none"
|
|
534
|
+
style={indentStyle(INDENT.database)}
|
|
420
535
|
onSelect={() => {
|
|
421
536
|
setIsExpanded(!isExpanded);
|
|
422
537
|
setIsSelected(!isSelected);
|
|
@@ -441,40 +556,35 @@ const DatabaseItem: React.FC<{
|
|
|
441
556
|
);
|
|
442
557
|
};
|
|
443
558
|
|
|
444
|
-
|
|
559
|
+
interface SchemaListProps {
|
|
445
560
|
schemas: DatabaseSchema[];
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
schemas,
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
dialect,
|
|
458
|
-
engineName,
|
|
459
|
-
databaseName,
|
|
460
|
-
hasSearch,
|
|
461
|
-
searchValue,
|
|
462
|
-
}) => {
|
|
561
|
+
schemasResolved: boolean;
|
|
562
|
+
// Parent schema path (relative to the database). Empty at the top level.
|
|
563
|
+
schemaPath: string[];
|
|
564
|
+
// Nesting depth (0 = top-level).
|
|
565
|
+
depth: number;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
const SchemaList: React.FC<SchemaListProps> = (props) => {
|
|
569
|
+
const { schemas, schemasResolved, depth } = props;
|
|
570
|
+
const tree = useDataSourceTree();
|
|
571
|
+
const { engineName, databaseName, searchValue } = tree;
|
|
463
572
|
const { addSchemaList } = useDataSourceActions();
|
|
464
|
-
|
|
573
|
+
// Stable identity so the useAsyncData below doesn't refire each render.
|
|
574
|
+
const schemaPath = useDeepCompareMemoize(props.schemaPath);
|
|
465
575
|
|
|
466
576
|
// Custom loading state, we need to wait for the data to propagate once requested
|
|
467
577
|
// useAsyncData's loading state may return false before data has propagated
|
|
468
578
|
const [schemasLoading, setSchemasLoading] = React.useState(false);
|
|
469
579
|
|
|
470
580
|
const { isPending, error } = useAsyncData(async () => {
|
|
471
|
-
if (
|
|
472
|
-
setSchemasRequested(true);
|
|
581
|
+
if (!schemasResolved && engineName) {
|
|
473
582
|
setSchemasLoading(true);
|
|
474
583
|
try {
|
|
475
584
|
const previewSchemaList = await PreviewSQLSchemaList.request({
|
|
476
585
|
engine: engineName,
|
|
477
586
|
database: databaseName,
|
|
587
|
+
schemaPath: schemaPath,
|
|
478
588
|
});
|
|
479
589
|
|
|
480
590
|
addSchemaList({
|
|
@@ -482,93 +592,82 @@ const SchemaList: React.FC<{
|
|
|
482
592
|
sqlSchemaContext: {
|
|
483
593
|
engine: engineName,
|
|
484
594
|
database: databaseName,
|
|
595
|
+
schemaPath: schemaPath,
|
|
485
596
|
},
|
|
486
597
|
});
|
|
487
598
|
} finally {
|
|
488
599
|
setSchemasLoading(false);
|
|
489
600
|
}
|
|
490
601
|
}
|
|
491
|
-
}, [
|
|
602
|
+
}, [schemasResolved, engineName, databaseName, schemaPath]);
|
|
603
|
+
|
|
604
|
+
const stateStyle = indentStyle(schemaHeaderIndentRem(depth));
|
|
492
605
|
|
|
493
606
|
if (isPending || schemasLoading) {
|
|
494
|
-
return
|
|
495
|
-
<LoadingState
|
|
496
|
-
message="Loading schemas..."
|
|
497
|
-
className={INDENT.schemaLoading}
|
|
498
|
-
/>
|
|
499
|
-
);
|
|
607
|
+
return <LoadingState message="Loading schemas..." style={stateStyle} />;
|
|
500
608
|
}
|
|
501
609
|
|
|
502
610
|
if (error) {
|
|
503
|
-
return <ErrorState error={error}
|
|
611
|
+
return <ErrorState error={error} style={stateStyle} />;
|
|
504
612
|
}
|
|
505
613
|
|
|
506
614
|
if (schemas.length === 0) {
|
|
507
|
-
return
|
|
508
|
-
<EmptyState
|
|
509
|
-
content="No schemas available"
|
|
510
|
-
className={INDENT.schemaEmpty}
|
|
511
|
-
/>
|
|
512
|
-
);
|
|
615
|
+
return <EmptyState content="No schemas available" style={stateStyle} />;
|
|
513
616
|
}
|
|
514
617
|
|
|
515
|
-
const filteredSchemas = schemas.filter((schema) => {
|
|
516
|
-
if (searchValue) {
|
|
517
|
-
return schema.tables.some((table) =>
|
|
518
|
-
table.name.toLowerCase().includes(searchValue.toLowerCase()),
|
|
519
|
-
);
|
|
520
|
-
}
|
|
521
|
-
return true;
|
|
522
|
-
});
|
|
523
|
-
|
|
524
618
|
return (
|
|
525
619
|
<>
|
|
526
|
-
{
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
620
|
+
{schemas.map((schema) => {
|
|
621
|
+
// Schemaless schemas (the database's own tables) render their tables
|
|
622
|
+
// directly under the database with no expandable node.
|
|
623
|
+
if (isSchemaless(schema.name)) {
|
|
624
|
+
return (
|
|
625
|
+
<TableList
|
|
626
|
+
key={schema.name}
|
|
627
|
+
tables={schema.tables}
|
|
628
|
+
tablesResolved={areTablesResolved(schema)}
|
|
629
|
+
searchValue={searchValue}
|
|
630
|
+
sqlTableContext={buildSqlTableContext(tree, {
|
|
631
|
+
schema: schema.name,
|
|
632
|
+
schemaPath,
|
|
633
|
+
})}
|
|
634
|
+
/>
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
return (
|
|
638
|
+
<SchemaNode
|
|
639
|
+
key={schema.name}
|
|
640
|
+
schema={schema}
|
|
641
|
+
schemaPath={[...schemaPath, schema.name]}
|
|
642
|
+
depth={depth}
|
|
544
643
|
/>
|
|
545
|
-
|
|
546
|
-
)
|
|
644
|
+
);
|
|
645
|
+
})}
|
|
547
646
|
</>
|
|
548
647
|
);
|
|
549
648
|
};
|
|
550
649
|
|
|
551
|
-
|
|
552
|
-
databaseName: string;
|
|
650
|
+
interface SchemaNodeProps {
|
|
553
651
|
schema: DatabaseSchema;
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
652
|
+
// Path of this schema relative to the database (includes this node).
|
|
653
|
+
schemaPath: string[];
|
|
654
|
+
depth: number;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const SchemaNode: React.FC<SchemaNodeProps> = (props) => {
|
|
658
|
+
const { schema, schemaPath, depth } = props;
|
|
659
|
+
const tree = useDataSourceTree();
|
|
660
|
+
const { databaseName, hasSearch, searchValue } = tree;
|
|
557
661
|
const [isExpanded, setIsExpanded] = React.useState(hasSearch);
|
|
558
662
|
const [isSelected, setIsSelected] = React.useState(false);
|
|
559
|
-
const uniqueValue = `${databaseName}:${
|
|
560
|
-
|
|
561
|
-
if (isSchemaless(schema.name)) {
|
|
562
|
-
return children;
|
|
563
|
-
}
|
|
663
|
+
const uniqueValue = `${databaseName}:${schemaPath.join(".")}`;
|
|
664
|
+
const childSchemas = schema.child_schemas ?? [];
|
|
564
665
|
|
|
565
666
|
return (
|
|
566
667
|
<>
|
|
567
668
|
<CommandItem
|
|
568
|
-
className=
|
|
569
|
-
|
|
570
|
-
INDENT.schema,
|
|
571
|
-
)}
|
|
669
|
+
className="text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none"
|
|
670
|
+
style={indentStyle(schemaHeaderIndentRem(depth))}
|
|
572
671
|
onSelect={() => {
|
|
573
672
|
setIsExpanded(!isExpanded);
|
|
574
673
|
setIsSelected(!isSelected);
|
|
@@ -586,7 +685,31 @@ const SchemaItem: React.FC<{
|
|
|
586
685
|
{schema.name}
|
|
587
686
|
</span>
|
|
588
687
|
</CommandItem>
|
|
589
|
-
{isExpanded &&
|
|
688
|
+
{isExpanded && (
|
|
689
|
+
<>
|
|
690
|
+
{/* Nested child schemas */}
|
|
691
|
+
{(childSchemas.length > 0 || !areChildSchemasResolved(schema)) && (
|
|
692
|
+
<SchemaList
|
|
693
|
+
schemas={childSchemas}
|
|
694
|
+
schemasResolved={areChildSchemasResolved(schema)}
|
|
695
|
+
schemaPath={schemaPath}
|
|
696
|
+
depth={depth + 1}
|
|
697
|
+
/>
|
|
698
|
+
)}
|
|
699
|
+
{/* Tables that live directly in this schema */}
|
|
700
|
+
<TableList
|
|
701
|
+
tables={schema.tables}
|
|
702
|
+
tablesResolved={areTablesResolved(schema)}
|
|
703
|
+
searchValue={searchValue}
|
|
704
|
+
tableIndentRem={schemaTableIndentRem(depth)}
|
|
705
|
+
columnIndentRem={schemaColumnIndentRem(depth)}
|
|
706
|
+
sqlTableContext={buildSqlTableContext(tree, {
|
|
707
|
+
schema: schema.name,
|
|
708
|
+
schemaPath,
|
|
709
|
+
})}
|
|
710
|
+
/>
|
|
711
|
+
</>
|
|
712
|
+
)}
|
|
590
713
|
</>
|
|
591
714
|
);
|
|
592
715
|
};
|
|
@@ -595,24 +718,39 @@ const TableList: React.FC<{
|
|
|
595
718
|
tables: DataTable[];
|
|
596
719
|
sqlTableContext?: SQLTableContext;
|
|
597
720
|
searchValue?: string;
|
|
598
|
-
|
|
721
|
+
// Whether `tables` has been enumerated; when false, discovery is deferred and
|
|
722
|
+
// a request is issued on mount (i.e. when the parent is expanded).
|
|
723
|
+
tablesResolved?: boolean;
|
|
724
|
+
// Depth-based indentation (rem) for nested schema tables/columns. When
|
|
725
|
+
// omitted, the fixed INDENT levels are used (top-level / schemaless tables).
|
|
726
|
+
tableIndentRem?: number;
|
|
727
|
+
columnIndentRem?: number;
|
|
728
|
+
}> = ({
|
|
729
|
+
tables,
|
|
730
|
+
sqlTableContext,
|
|
731
|
+
searchValue,
|
|
732
|
+
tablesResolved = true,
|
|
733
|
+
tableIndentRem,
|
|
734
|
+
columnIndentRem,
|
|
735
|
+
}) => {
|
|
599
736
|
const { addTableList } = useDataSourceActions();
|
|
600
|
-
const [tablesRequested, setTablesRequested] = React.useState(false);
|
|
601
737
|
|
|
602
738
|
// Custom loading state, we need to wait for the data to propagate once requested
|
|
603
739
|
// useAsyncData's loading state may return false before data has propagated
|
|
604
740
|
const [tablesLoading, setTablesLoading] = React.useState(false);
|
|
605
741
|
|
|
742
|
+
// Fetch when discovery is deferred (also re-fetches after a refresh, which
|
|
743
|
+
// resets tablesResolved to false).
|
|
606
744
|
const { isPending, error } = useAsyncData(async () => {
|
|
607
|
-
if (
|
|
608
|
-
setTablesRequested(true);
|
|
745
|
+
if (!tablesResolved && sqlTableContext) {
|
|
609
746
|
setTablesLoading(true);
|
|
610
747
|
|
|
611
|
-
const { engine, database, schema } = sqlTableContext;
|
|
748
|
+
const { engine, database, schema, schemaPath } = sqlTableContext;
|
|
612
749
|
const previewTableList = await PreviewSQLTableList.request({
|
|
613
750
|
engine: engine,
|
|
614
751
|
database: database,
|
|
615
752
|
schema: schema,
|
|
753
|
+
schemaPath: schemaPath ?? [],
|
|
616
754
|
});
|
|
617
755
|
|
|
618
756
|
if (!previewTableList?.tables) {
|
|
@@ -626,25 +764,20 @@ const TableList: React.FC<{
|
|
|
626
764
|
});
|
|
627
765
|
setTablesLoading(false);
|
|
628
766
|
}
|
|
629
|
-
}, [
|
|
767
|
+
}, [tablesResolved, sqlTableContext]);
|
|
768
|
+
|
|
769
|
+
const stateStyle = indentStyle(tableIndentRem ?? INDENT.tableLoading);
|
|
630
770
|
|
|
631
771
|
if (isPending || tablesLoading) {
|
|
632
|
-
return
|
|
633
|
-
<LoadingState
|
|
634
|
-
message="Loading tables..."
|
|
635
|
-
className={INDENT.tableLoading}
|
|
636
|
-
/>
|
|
637
|
-
);
|
|
772
|
+
return <LoadingState message="Loading tables..." style={stateStyle} />;
|
|
638
773
|
}
|
|
639
774
|
|
|
640
775
|
if (error) {
|
|
641
|
-
return <ErrorState error={error}
|
|
776
|
+
return <ErrorState error={error} style={stateStyle} />;
|
|
642
777
|
}
|
|
643
778
|
|
|
644
779
|
if (tables.length === 0) {
|
|
645
|
-
return
|
|
646
|
-
<EmptyState content="No tables found" className={INDENT.tableLoading} />
|
|
647
|
-
);
|
|
780
|
+
return <EmptyState content="No tables found" style={stateStyle} />;
|
|
648
781
|
}
|
|
649
782
|
|
|
650
783
|
const filteredTables = tables.filter((table) => {
|
|
@@ -662,6 +795,8 @@ const TableList: React.FC<{
|
|
|
662
795
|
table={table}
|
|
663
796
|
sqlTableContext={sqlTableContext}
|
|
664
797
|
isSearching={!!searchValue}
|
|
798
|
+
tableIndentRem={tableIndentRem}
|
|
799
|
+
columnIndentRem={columnIndentRem}
|
|
665
800
|
/>
|
|
666
801
|
))}
|
|
667
802
|
</>
|
|
@@ -672,28 +807,29 @@ const DatasetTableItem: React.FC<{
|
|
|
672
807
|
table: DataTable;
|
|
673
808
|
sqlTableContext?: SQLTableContext;
|
|
674
809
|
isSearching: boolean;
|
|
675
|
-
|
|
810
|
+
tableIndentRem?: number;
|
|
811
|
+
columnIndentRem?: number;
|
|
812
|
+
}> = ({
|
|
813
|
+
table,
|
|
814
|
+
sqlTableContext,
|
|
815
|
+
isSearching,
|
|
816
|
+
tableIndentRem,
|
|
817
|
+
columnIndentRem,
|
|
818
|
+
}) => {
|
|
676
819
|
const { addTable } = useDataSourceActions();
|
|
677
820
|
|
|
678
821
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
679
|
-
const [tableDetailsRequested, setTableDetailsRequested] =
|
|
680
|
-
React.useState(false);
|
|
681
822
|
const tableDetailsExist = table.columns.length > 0;
|
|
682
823
|
|
|
683
824
|
const { isFetching, isPending, error } = useAsyncData(async () => {
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
!tableDetailsExist &&
|
|
687
|
-
sqlTableContext &&
|
|
688
|
-
!tableDetailsRequested
|
|
689
|
-
) {
|
|
690
|
-
setTableDetailsRequested(true);
|
|
691
|
-
const { engine, database, schema } = sqlTableContext;
|
|
825
|
+
if (isExpanded && !tableDetailsExist && sqlTableContext) {
|
|
826
|
+
const { engine, database, schema, schemaPath } = sqlTableContext;
|
|
692
827
|
const previewTable = await PreviewSQLTable.request({
|
|
693
828
|
engine: engine,
|
|
694
829
|
database: database,
|
|
695
830
|
schema: schema,
|
|
696
831
|
tableName: table.name,
|
|
832
|
+
schemaPath: schemaPath ?? [],
|
|
697
833
|
});
|
|
698
834
|
|
|
699
835
|
if (!previewTable?.table) {
|
|
@@ -720,8 +856,14 @@ const DatasetTableItem: React.FC<{
|
|
|
720
856
|
});
|
|
721
857
|
const getCode = () => {
|
|
722
858
|
if (table.source_type === "catalog") {
|
|
859
|
+
// Build the fully-qualified, dotted name including any nested
|
|
860
|
+
// schema path, e.g. `top.nested.table`.
|
|
723
861
|
const identifier = sqlTableContext?.database
|
|
724
|
-
?
|
|
862
|
+
? [
|
|
863
|
+
sqlTableContext.database,
|
|
864
|
+
...(sqlTableContext.schemaPath ?? []),
|
|
865
|
+
table.name,
|
|
866
|
+
].join(".")
|
|
725
867
|
: table.name;
|
|
726
868
|
return `${table.engine}.load_table("${identifier}")`;
|
|
727
869
|
}
|
|
@@ -768,28 +910,20 @@ const DatasetTableItem: React.FC<{
|
|
|
768
910
|
};
|
|
769
911
|
|
|
770
912
|
const renderColumns = () => {
|
|
913
|
+
const stateStyle = indentStyle(columnIndentRem ?? INDENT.tableLoading);
|
|
914
|
+
|
|
771
915
|
if (isPending || isFetching) {
|
|
772
|
-
return
|
|
773
|
-
<LoadingState
|
|
774
|
-
message="Loading columns..."
|
|
775
|
-
className={INDENT.tableLoading}
|
|
776
|
-
/>
|
|
777
|
-
);
|
|
916
|
+
return <LoadingState message="Loading columns..." style={stateStyle} />;
|
|
778
917
|
}
|
|
779
918
|
|
|
780
919
|
if (error) {
|
|
781
|
-
return <ErrorState error={error}
|
|
920
|
+
return <ErrorState error={error} style={stateStyle} />;
|
|
782
921
|
}
|
|
783
922
|
|
|
784
923
|
const columns = table.columns;
|
|
785
924
|
|
|
786
925
|
if (columns.length === 0) {
|
|
787
|
-
return
|
|
788
|
-
<EmptyState
|
|
789
|
-
content="No columns found"
|
|
790
|
-
className={INDENT.tableLoading}
|
|
791
|
-
/>
|
|
792
|
-
);
|
|
926
|
+
return <EmptyState content="No columns found" style={stateStyle} />;
|
|
793
927
|
}
|
|
794
928
|
|
|
795
929
|
return columns.map((column) => (
|
|
@@ -798,6 +932,7 @@ const DatasetTableItem: React.FC<{
|
|
|
798
932
|
table={table}
|
|
799
933
|
column={column}
|
|
800
934
|
sqlTableContext={sqlTableContext}
|
|
935
|
+
columnIndentRem={columnIndentRem}
|
|
801
936
|
/>
|
|
802
937
|
));
|
|
803
938
|
};
|
|
@@ -816,21 +951,19 @@ const DatasetTableItem: React.FC<{
|
|
|
816
951
|
);
|
|
817
952
|
};
|
|
818
953
|
|
|
819
|
-
const uniqueId = sqlTableContext
|
|
820
|
-
|
|
821
|
-
|
|
954
|
+
const uniqueId = tableUniqueId(sqlTableContext, table.name);
|
|
955
|
+
|
|
956
|
+
const tableRem =
|
|
957
|
+
tableIndentRem ?? (sqlTableContext ? INDENT.tableSchemaless : 0);
|
|
822
958
|
|
|
823
959
|
return (
|
|
824
960
|
<>
|
|
825
961
|
<CommandItem
|
|
826
962
|
className={cn(
|
|
827
963
|
"rounded-none group h-8 cursor-pointer",
|
|
828
|
-
sqlTableContext &&
|
|
829
|
-
(isSchemaless(sqlTableContext.schema)
|
|
830
|
-
? INDENT.tableSchemaless
|
|
831
|
-
: INDENT.tableWithSchema),
|
|
832
964
|
(isExpanded || isSearching) && "font-semibold",
|
|
833
965
|
)}
|
|
966
|
+
style={indentStyle(tableRem)}
|
|
834
967
|
value={uniqueId}
|
|
835
968
|
aria-selected={isExpanded}
|
|
836
969
|
forceMount={true}
|
|
@@ -861,7 +994,8 @@ const DatasetColumnItem: React.FC<{
|
|
|
861
994
|
table: DataTable;
|
|
862
995
|
column: DataTableColumn;
|
|
863
996
|
sqlTableContext?: SQLTableContext;
|
|
864
|
-
|
|
997
|
+
columnIndentRem?: number;
|
|
998
|
+
}> = ({ table, column, sqlTableContext, columnIndentRem }) => {
|
|
865
999
|
const [isExpanded, setIsExpanded] = React.useState(false);
|
|
866
1000
|
const closeAllColumns = useAtomValue(closeAllColumnsAtom);
|
|
867
1001
|
const setExpandedColumns = useSetAtom(expandedColumnsAtom);
|
|
@@ -920,9 +1054,10 @@ const DatasetColumnItem: React.FC<{
|
|
|
920
1054
|
onSelect={() => setIsExpanded(!isExpanded)}
|
|
921
1055
|
>
|
|
922
1056
|
<div
|
|
923
|
-
className=
|
|
924
|
-
|
|
925
|
-
|
|
1057
|
+
className="flex flex-row gap-2 items-center flex-1"
|
|
1058
|
+
style={indentStyle(
|
|
1059
|
+
columnIndentRem ??
|
|
1060
|
+
(sqlTableContext ? schemaColumnIndentRem(0) : INDENT.columnLocal),
|
|
926
1061
|
)}
|
|
927
1062
|
>
|
|
928
1063
|
<ColumnName columnName={columnText} dataType={column.type} />
|
|
@@ -950,10 +1085,8 @@ const DatasetColumnItem: React.FC<{
|
|
|
950
1085
|
</CommandItem>
|
|
951
1086
|
{isExpanded && (
|
|
952
1087
|
<div
|
|
953
|
-
className=
|
|
954
|
-
|
|
955
|
-
"pr-2 py-2 bg-(--slate-1) shadow-inner border-b",
|
|
956
|
-
)}
|
|
1088
|
+
className="pr-2 py-2 bg-(--slate-1) shadow-inner border-b"
|
|
1089
|
+
style={indentStyle(INDENT.columnPreview)}
|
|
957
1090
|
>
|
|
958
1091
|
<ErrorBoundary>
|
|
959
1092
|
<DatasetColumnPreview
|