@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.
Files changed (89) hide show
  1. package/dist/assets/{CellStatus-e0ex7Iei.js → CellStatus-CLGvVxcw.js} +1 -1
  2. package/dist/assets/{JsonOutput-DRNPZOvX.js → JsonOutput-uEJijGXp.js} +5 -5
  3. package/dist/assets/{MarimoErrorOutput-BH6hs0Ir.js → MarimoErrorOutput-DzoKyXWR.js} +2 -2
  4. package/dist/assets/{RenderHTML-BQ1PO4Wd.js → RenderHTML-DAt48X-F.js} +1 -1
  5. package/dist/assets/{RunButton-F8pLIvFp.js → RunButton-B7msyyYi.js} +1 -1
  6. package/dist/assets/{add-cell-with-ai-Bd_3tPEt.js → add-cell-with-ai-CAkcousR.js} +20 -20
  7. package/dist/assets/{add-connection-dialog-AhwxOztN.js → add-connection-dialog-ZyXXfAVG.js} +32 -32
  8. package/dist/assets/{agent-panel-Dm0KI1sF.js → agent-panel-BfIguQXh.js} +6 -6
  9. package/dist/assets/{ai-model-dropdown-CG4B4rqH.js → ai-model-dropdown-rE64ytSX.js} +3 -3
  10. package/dist/assets/{app-config-button-D_sFrSql.js → app-config-button-DLRZ5d9c.js} +1 -1
  11. package/dist/assets/{cell-editor-B3aYYyAI.js → cell-editor-2JIpvFn4.js} +15 -12
  12. package/dist/assets/{cell-link-Bj4-yOIx.js → cell-link-DY95GSb7.js} +1 -1
  13. package/dist/assets/{cells-DOA0Gew8.js → cells-D-v2lBet.js} +67 -67
  14. package/dist/assets/{chat-display-DI0jRLIv.js → chat-display-DO-tqsbY.js} +1 -1
  15. package/dist/assets/{chat-panel-BU4HHdf5.js → chat-panel-BT5gLfs6.js} +2 -2
  16. package/dist/assets/{chat-ui-C7igY2w5.js → chat-ui-Be4yDBZS.js} +4 -4
  17. package/dist/assets/column-preview-BzvFdLI5.js +1 -0
  18. package/dist/assets/{command-palette-Bjv1Z7v8.js → command-palette-CKKzfpGt.js} +1 -1
  19. package/dist/assets/{common-fDFYY_sv.js → common-Do4N1uVK.js} +1 -1
  20. package/dist/assets/{components--C6N-DXq.js → components-DUC0f1XD.js} +1 -1
  21. package/dist/assets/{datasource-CR6RRpTi.js → datasource-BQGI7c_u.js} +2 -2
  22. package/dist/assets/{dependency-graph-panel-BEgkvdX0.js → dependency-graph-panel-Czoya7c2.js} +1 -1
  23. package/dist/assets/{documentation-panel-HvbKykbI.js → documentation-panel-LokVYDIZ.js} +1 -1
  24. package/dist/assets/{download-DhxnAw14.js → download-WGU5w_3m.js} +3 -3
  25. package/dist/assets/{edit-page-BeWwLeT8.js → edit-page-BPQO1Uuz.js} +6 -6
  26. package/dist/assets/{error-panel-WLBQ3q9p.js → error-panel-BCm_e4eM.js} +1 -1
  27. package/dist/assets/{file-explorer-panel-DtzGlnzT.js → file-explorer-panel-D1NN-z8x.js} +3 -3
  28. package/dist/assets/{file-icons-D2f3nfbq.js → file-icons-CF-YT4hq.js} +1 -1
  29. package/dist/assets/{file-name-input-BNYf9WWM.js → file-name-input-BRVXQ3OU.js} +1 -1
  30. package/dist/assets/{floating-outline-BvHFWRoz.js → floating-outline-D33Zu-Ad.js} +1 -1
  31. package/dist/assets/{focus-Ldqh99xE.js → focus-BfFAxl9X.js} +1 -1
  32. package/dist/assets/{form-CxBAInfg.js → form-CVBFx2z3.js} +1 -1
  33. package/dist/assets/{home-page-DwFpLpXK.js → home-page-CL9Hv1Hg.js} +2 -2
  34. package/dist/assets/{hooks-DyMacA-R.js → hooks-83y0XCiC.js} +1 -1
  35. package/dist/assets/{html-to-image-CyAtzePO.js → html-to-image-IPNqWX7V.js} +2 -2
  36. package/dist/assets/index-BNWrEIlp.css +2 -0
  37. package/dist/assets/{index-fNBoXCyz.js → index-CuhY66ZR.js} +14 -14
  38. package/dist/assets/{kiosk-mode-CTWHjzXs.js → kiosk-mode-BSufcIZH.js} +1 -1
  39. package/dist/assets/{layout-dRNPwA-7.js → layout-ks0WJ2vR.js} +5 -5
  40. package/dist/assets/{logs-panel-BVLYycQb.js → logs-panel-DXN6sUaA.js} +1 -1
  41. package/dist/assets/{markdown-renderer-C9Ujvj0b.js → markdown-renderer-DWRPo52L.js} +1 -1
  42. package/dist/assets/{name-cell-input-DabvuruX.js → name-cell-input-BiDIx6YQ.js} +1 -1
  43. package/dist/assets/{outline-panel-B-ncbNWI.js → outline-panel-DVSGNRgP.js} +1 -1
  44. package/dist/assets/{packages-panel-CESeW_3T.js → packages-panel-DTX6CBnm.js} +1 -1
  45. package/dist/assets/{panels-BTEEcvYR.js → panels-NhGV0SBq.js} +1 -1
  46. package/dist/assets/{process-output-pPgH0ANl.js → process-output-BiWV34b7.js} +1 -1
  47. package/dist/assets/{radio-group-D7rh6Zek.js → radio-group-DV-lQvil.js} +1 -1
  48. package/dist/assets/{readonly-python-code-CvPx4CU_.js → readonly-python-code-B0glFTKW.js} +1 -1
  49. package/dist/assets/{reveal-component-DGQQBmed.js → reveal-component-C2M2FixA.js} +12 -12
  50. package/dist/assets/{run-page-yFxABzXq.js → run-page-B6I-Lshx.js} +1 -1
  51. package/dist/assets/{scratchpad-panel-B_nxW99u.js → scratchpad-panel-DU7perVk.js} +1 -1
  52. package/dist/assets/session-panel-B4UTmf-j.js +1 -0
  53. package/dist/assets/{snippets-panel-BSgcoo-M.js → snippets-panel-KRdl4bVk.js} +1 -1
  54. package/dist/assets/{state-BWM3qRkR.js → state-BSm0OM93.js} +1 -1
  55. package/dist/assets/{state-C8yHPSLN.js → state-CbkvENzk.js} +2 -2
  56. package/dist/assets/{textarea-bBuxbFm7.js → textarea-CBbixTxi.js} +1 -1
  57. package/dist/assets/{tracing-pt7eDHWc.js → tracing-D8LGpuXa.js} +1 -1
  58. package/dist/assets/{tracing-panel-BveZ5DZw.js → tracing-panel-TUP6kAd5.js} +2 -2
  59. package/dist/assets/{tree-actions-1KQDSu1H.js → tree-actions-BvrKFU0g.js} +1 -1
  60. package/dist/assets/{useCellActionButton-C4V7J6PS.js → useCellActionButton-CtOZIXD0.js} +1 -1
  61. package/dist/assets/{useDeleteCell-BvnurAZ9.js → useDeleteCell-BLvzLWAT.js} +1 -1
  62. package/dist/assets/{useDependencyPanelTab-DNlXaxbt.js → useDependencyPanelTab-BP0sNfUz.js} +1 -1
  63. package/dist/assets/{useNotebookActions-Dw6tY2qa.js → useNotebookActions-BI5dWutQ.js} +1 -1
  64. package/dist/assets/{useRunCells-cSZpNqnR.js → useRunCells-BClkx9Pq.js} +1 -1
  65. package/dist/assets/{useSplitCell-s7dSiBUa.js → useSplitCell-CpddCX_u.js} +1 -1
  66. package/dist/index.html +23 -23
  67. package/package.json +1 -1
  68. package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
  69. package/src/components/datasources/__tests__/filter-empty.test.ts +81 -0
  70. package/src/components/datasources/__tests__/utils.test.ts +62 -1
  71. package/src/components/datasources/column-preview.tsx +2 -4
  72. package/src/components/datasources/components.tsx +15 -7
  73. package/src/components/datasources/datasources.tsx +311 -178
  74. package/src/components/datasources/utils.ts +40 -1
  75. package/src/components/editor/ai/ai-completion-editor.tsx +6 -5
  76. package/src/components/editor/connections/components.tsx +13 -0
  77. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +4 -4
  78. package/src/components/editor/connections/storage/as-code.ts +11 -4
  79. package/src/core/ai/__tests__/strip-wrapping-backticks.test.ts +133 -0
  80. package/src/core/ai/stream-completion-text.ts +48 -0
  81. package/src/core/ai/strip-wrapping-backticks.ts +88 -0
  82. package/src/core/cells/__tests__/cells.test.ts +33 -0
  83. package/src/core/cells/cells.ts +1 -1
  84. package/src/core/codemirror/ai/request.ts +2 -14
  85. package/src/core/datasets/__tests__/data-source.test.ts +226 -0
  86. package/src/core/datasets/data-source-connections.ts +88 -24
  87. package/dist/assets/column-preview-nu3Qo2OW.js +0 -1
  88. package/dist/assets/index-BAYF7dcV.css +0 -2
  89. 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 { isSchemaless, sqlCode } from "./utils";
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
- // Indentation classes for the datasource tree hierarchy.
104
+ // Left indentation (rem) for each fixed (non-nested) level of the tree.
83
105
  const INDENT = {
84
- engineEmpty: "pl-3",
85
- engine: "pl-3 pr-2",
86
- database: "pl-4",
87
- schemaEmpty: "pl-8",
88
- schema: "pl-7",
89
- schemaLoading: "pl-8",
90
- tableLoading: "pl-11",
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
- function isKnownEmptySchema(schema: DatabaseSchema): boolean {
136
- return schema.tables_resolved !== false && schema.tables.length === 0;
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 hidden.
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.schemas_resolved !== false && database.schemas.length === 0) {
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.filter(
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.length === database.schemas.length) {
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
- <DatabaseItem
359
+ <DatabaseTree
316
360
  key={database.name}
317
- engineName={connection.name}
361
+ connection={connection}
318
362
  database={database}
319
363
  hasSearch={hasSearch}
320
- >
321
- <SchemaList
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
- className={INDENT.engineEmpty}
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={cn(
417
- "text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none",
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
- const SchemaList: React.FC<{
559
+ interface SchemaListProps {
445
560
  schemas: DatabaseSchema[];
446
- defaultSchema?: string | null;
447
- defaultDatabase?: string | null;
448
- dialect: string;
449
- engineName: string;
450
- databaseName: string;
451
- hasSearch: boolean;
452
- searchValue?: string;
453
- }> = ({
454
- schemas,
455
- defaultSchema,
456
- defaultDatabase,
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
- const [schemasRequested, setSchemasRequested] = React.useState(false);
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 (schemas.length === 0 && engineName && !schemasRequested) {
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
- }, [schemas.length, engineName, databaseName, schemasRequested]);
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} className={INDENT.schemaLoading} />;
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
- {filteredSchemas.map((schema) => (
527
- <SchemaItem
528
- key={schema.name}
529
- databaseName={databaseName}
530
- schema={schema}
531
- hasSearch={hasSearch}
532
- >
533
- <TableList
534
- tables={schema.tables}
535
- searchValue={searchValue}
536
- sqlTableContext={{
537
- engine: engineName,
538
- database: databaseName,
539
- schema: schema.name,
540
- defaultSchema: defaultSchema,
541
- defaultDatabase: defaultDatabase,
542
- dialect: dialect,
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
- </SchemaItem>
546
- ))}
644
+ );
645
+ })}
547
646
  </>
548
647
  );
549
648
  };
550
649
 
551
- const SchemaItem: React.FC<{
552
- databaseName: string;
650
+ interface SchemaNodeProps {
553
651
  schema: DatabaseSchema;
554
- children: React.ReactNode;
555
- hasSearch: boolean;
556
- }> = ({ databaseName, schema, children, hasSearch }) => {
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}:${schema.name}`;
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={cn(
569
- "text-sm flex flex-row gap-1 items-center cursor-pointer rounded-none",
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 && children}
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
- }> = ({ tables, sqlTableContext, searchValue }) => {
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 (tables.length === 0 && sqlTableContext && !tablesRequested) {
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
- }, [tables.length, sqlTableContext, tablesRequested]);
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} className={INDENT.tableLoading} />;
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
- }> = ({ table, sqlTableContext, isSearching }) => {
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
- isExpanded &&
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
- ? `${sqlTableContext.database}.${table.name}`
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} className={INDENT.tableLoading} />;
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
- ? `${sqlTableContext.database}.${sqlTableContext.schema}.${table.name}`
821
- : table.name;
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
- }> = ({ table, column, sqlTableContext }) => {
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={cn(
924
- "flex flex-row gap-2 items-center flex-1",
925
- sqlTableContext ? INDENT.columnSql : INDENT.columnLocal,
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={cn(
954
- INDENT.columnPreview,
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