@marimo-team/islands 0.23.11-dev2 → 0.23.11-dev21
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/{ConnectedDataExplorerComponent-Z3RP7Vmm.js → ConnectedDataExplorerComponent-BnPKA-xR.js} +4 -4
- package/dist/assets/__vite-browser-external-DAm_YW43.js +1 -0
- package/dist/assets/{worker-CC0Oul9k.js → worker-DEDLIQQV.js} +2 -2
- package/dist/{chat-ui-c1FdlK-C.js → chat-ui-Bf2NICxi.js} +6 -6
- package/dist/{code-visibility-CdW8VWzm.js → code-visibility-DvrnpkbY.js} +8 -8
- package/dist/{formats-C4wO47tk.js → formats-BOYEeoL_.js} +1 -1
- package/dist/{glide-data-editor-BPkCPs7L.js → glide-data-editor-A_l7_PQZ.js} +2 -2
- package/dist/{html-to-image-D5-EpALB.js → html-to-image-BJbLjZyG.js} +6 -6
- package/dist/{input-OdWHkobi.js → input-Cn-SZdXY.js} +1 -1
- package/dist/main.js +18 -18
- package/dist/{mermaid-BotvIKg9.js → mermaid-Br973nIL.js} +2 -2
- package/dist/{process-output-WDZE0cyS.js → process-output-Dw_ngzH2.js} +1 -1
- package/dist/{reveal-component-cfStduPb.js → reveal-component-BsmfWYqg.js} +5 -5
- package/dist/{spec-X7FwLJni.js → spec-f8-xgaui.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-d8RCRrRd.js → toDate-B4-pUFYh.js} +1 -1
- package/dist/{useAsyncData-PonK__yh.js → useAsyncData-B3PfqrwD.js} +1 -1
- package/dist/{useDeepCompareMemoize-D3NGWke6.js → useDeepCompareMemoize-YInAvJP3.js} +1 -1
- package/dist/{useLifecycle-00mO3OSS.js → useLifecycle-B3NOEiY5.js} +1 -1
- package/dist/{useTheme-DEhDzATN.js → useTheme-BPeFKs1Q.js} +2 -1
- package/dist/{vega-component-DGPUhbDs.js → vega-component-CMicMpWX.js} +5 -5
- package/package.json +1 -1
- package/src/components/ai/ai-model-dropdown.tsx +6 -0
- package/src/components/app-config/ai-config.tsx +73 -55
- package/src/components/app-config/data-form.tsx +33 -43
- package/src/components/app-config/is-overridden.tsx +125 -58
- package/src/components/app-config/user-config-form.tsx +178 -227
- package/src/components/chat/chat-panel.tsx +17 -8
- package/src/components/datasources/__tests__/column-preview.test.tsx +97 -0
- package/src/components/datasources/__tests__/utils.test.ts +127 -2
- package/src/components/datasources/column-preview.tsx +2 -4
- package/src/components/datasources/datasources.tsx +44 -17
- package/src/components/datasources/utils.ts +45 -0
- package/src/components/editor/actions/useNotebookActions.tsx +35 -17
- 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/cells/__tests__/cells.test.ts +33 -0
- package/src/core/cells/cells.ts +1 -1
- package/src/core/config/config-schema.ts +6 -1
- package/src/css/md.css +12 -0
- package/dist/assets/__vite-browser-external-eshhtsgZ.js +0 -1
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
PlusIcon,
|
|
24
24
|
SparklesIcon,
|
|
25
25
|
SettingsIcon,
|
|
26
|
+
CodeIcon,
|
|
26
27
|
} from "lucide-react";
|
|
27
28
|
import { memo, useEffect, useRef, useState } from "react";
|
|
28
29
|
import useEvent from "react-use-event-hook";
|
|
@@ -268,24 +269,32 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
268
269
|
{
|
|
269
270
|
value: "ask",
|
|
270
271
|
label: "Ask",
|
|
271
|
-
subtitle:
|
|
272
|
-
"Use AI with access to read-only tools like documentation search",
|
|
272
|
+
subtitle: "AI with access to read-only tools like documentation search",
|
|
273
273
|
Icon: NotebookText,
|
|
274
274
|
},
|
|
275
275
|
{
|
|
276
276
|
value: "agent",
|
|
277
|
-
label: "Agent
|
|
278
|
-
subtitle: "
|
|
277
|
+
label: "Agent",
|
|
278
|
+
subtitle: "AI with access to read and write tools",
|
|
279
279
|
Icon: HatGlasses,
|
|
280
280
|
},
|
|
281
281
|
];
|
|
282
282
|
|
|
283
|
+
if (import.meta.env.DEV) {
|
|
284
|
+
modeOptions.push({
|
|
285
|
+
value: "code_mode",
|
|
286
|
+
label: "Code Mode (experimental)",
|
|
287
|
+
subtitle: "AI with access to the notebook's kernel. Use with caution.",
|
|
288
|
+
Icon: CodeIcon,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
283
292
|
const isAttachmentSupported =
|
|
284
293
|
PROVIDERS_THAT_SUPPORT_ATTACHMENTS.has(currentProvider);
|
|
285
294
|
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
const currentModeOption = modeOptions.find((o) => o.value === currentMode);
|
|
296
|
+
const CurrentModeIcon = currentModeOption?.Icon;
|
|
297
|
+
const CurrentModeLabel = currentModeOption?.label;
|
|
289
298
|
|
|
290
299
|
return (
|
|
291
300
|
<TooltipProvider>
|
|
@@ -294,7 +303,7 @@ const ChatInputFooter: React.FC<ChatInputFooterProps> = memo(
|
|
|
294
303
|
<Select value={currentMode} onValueChange={saveModeChange}>
|
|
295
304
|
<SelectTrigger className="h-6 text-xs border-border shadow-none! ring-0! bg-muted hover:bg-muted/30 py-0 px-2 gap-1.5">
|
|
296
305
|
{CurrentModeIcon && <CurrentModeIcon className="h-3 w-3" />}
|
|
297
|
-
<span
|
|
306
|
+
<span>{CurrentModeLabel}</span>
|
|
298
307
|
</SelectTrigger>
|
|
299
308
|
<SelectContent>
|
|
300
309
|
<SelectGroup>
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { render } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import type { SQLTableContext } from "@/core/datasets/data-source-connections";
|
|
9
|
+
import type { DataTable, DataTableColumn } from "@/core/kernel/messages";
|
|
10
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
11
|
+
import { store } from "@/core/state/jotai";
|
|
12
|
+
import { DatasetColumnPreview } from "../column-preview";
|
|
13
|
+
|
|
14
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
15
|
+
<Provider store={store}>{children}</Provider>
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
const table: DataTable = {
|
|
19
|
+
name: "users",
|
|
20
|
+
columns: [],
|
|
21
|
+
source: "my_engine",
|
|
22
|
+
// Not "connection"/"catalog", so a preview is requested on mount.
|
|
23
|
+
source_type: "duckdb",
|
|
24
|
+
type: "table",
|
|
25
|
+
engine: "my_engine" as DataTable["engine"],
|
|
26
|
+
indexes: null,
|
|
27
|
+
num_columns: null,
|
|
28
|
+
num_rows: null,
|
|
29
|
+
variable_name: null,
|
|
30
|
+
primary_keys: null,
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const column = { name: "email" } as DataTableColumn;
|
|
34
|
+
|
|
35
|
+
function renderPreview(sqlTableContext?: SQLTableContext) {
|
|
36
|
+
const client = MockRequestClient.create();
|
|
37
|
+
store.set(requestClientAtom, client);
|
|
38
|
+
render(
|
|
39
|
+
<DatasetColumnPreview
|
|
40
|
+
table={table}
|
|
41
|
+
column={column}
|
|
42
|
+
preview={undefined}
|
|
43
|
+
onAddColumnChart={vi.fn()}
|
|
44
|
+
sqlTableContext={sqlTableContext}
|
|
45
|
+
/>,
|
|
46
|
+
{ wrapper },
|
|
47
|
+
);
|
|
48
|
+
return client;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const ctx = (
|
|
52
|
+
overrides: Partial<SQLTableContext> & { database: string; schema: string },
|
|
53
|
+
): SQLTableContext => ({
|
|
54
|
+
engine: "my_engine",
|
|
55
|
+
dialect: "duckdb",
|
|
56
|
+
...overrides,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("DatasetColumnPreview fullyQualifiedTableName", () => {
|
|
60
|
+
beforeEach(() => {
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("uses just the table name without a SQL context", () => {
|
|
65
|
+
const client = renderPreview(undefined);
|
|
66
|
+
expect(client.previewDatasetColumn).toHaveBeenCalledWith(
|
|
67
|
+
expect.objectContaining({ fullyQualifiedTableName: "users" }),
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("qualifies with database and schema for flat engines", () => {
|
|
72
|
+
const client = renderPreview(ctx({ database: "db", schema: "public" }));
|
|
73
|
+
expect(client.previewDatasetColumn).toHaveBeenCalledWith(
|
|
74
|
+
expect.objectContaining({ fullyQualifiedTableName: "db.public.users" }),
|
|
75
|
+
);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not emit a double dot for schemaless tables", () => {
|
|
79
|
+
// Regression: previously produced "db..users".
|
|
80
|
+
const client = renderPreview(ctx({ database: "db", schema: "" }));
|
|
81
|
+
expect(client.previewDatasetColumn).toHaveBeenCalledWith(
|
|
82
|
+
expect.objectContaining({ fullyQualifiedTableName: "db.users" }),
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("includes the full schema path for nested namespaces", () => {
|
|
87
|
+
// Regression: previously dropped schemaPath and emitted "top.deep.users".
|
|
88
|
+
const client = renderPreview(
|
|
89
|
+
ctx({ database: "top", schema: "deep", schemaPath: ["nested", "deep"] }),
|
|
90
|
+
);
|
|
91
|
+
expect(client.previewDatasetColumn).toHaveBeenCalledWith(
|
|
92
|
+
expect.objectContaining({
|
|
93
|
+
fullyQualifiedTableName: "top.nested.deep.users",
|
|
94
|
+
}),
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -3,8 +3,18 @@
|
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
4
|
import type { SQLTableContext } from "@/core/datasets/data-source-connections";
|
|
5
5
|
import { DUCKDB_ENGINE } from "@/core/datasets/engines";
|
|
6
|
-
import type {
|
|
7
|
-
|
|
6
|
+
import type {
|
|
7
|
+
Database,
|
|
8
|
+
DatabaseSchema,
|
|
9
|
+
DataTable,
|
|
10
|
+
DataTableColumn,
|
|
11
|
+
} from "@/core/kernel/messages";
|
|
12
|
+
import {
|
|
13
|
+
schemaSubtreeMatchesSearch,
|
|
14
|
+
shouldExpandDatabaseForSearch,
|
|
15
|
+
sqlCode,
|
|
16
|
+
tableUniqueId,
|
|
17
|
+
} from "../utils";
|
|
8
18
|
|
|
9
19
|
describe("sqlCode", () => {
|
|
10
20
|
const mockTable: DataTable = {
|
|
@@ -519,3 +529,118 @@ describe("tableUniqueId", () => {
|
|
|
519
529
|
);
|
|
520
530
|
});
|
|
521
531
|
});
|
|
532
|
+
|
|
533
|
+
function makeTable(name: string): DataTable {
|
|
534
|
+
return {
|
|
535
|
+
name,
|
|
536
|
+
columns: [],
|
|
537
|
+
source: "memory",
|
|
538
|
+
source_type: "local",
|
|
539
|
+
type: "table",
|
|
540
|
+
engine: null,
|
|
541
|
+
indexes: null,
|
|
542
|
+
num_columns: null,
|
|
543
|
+
num_rows: null,
|
|
544
|
+
variable_name: null,
|
|
545
|
+
primary_keys: null,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function makeSchema(opts: {
|
|
550
|
+
name: string;
|
|
551
|
+
tables: DataTable[];
|
|
552
|
+
tables_resolved?: boolean;
|
|
553
|
+
child_schemas?: DatabaseSchema[];
|
|
554
|
+
child_schemas_resolved?: boolean;
|
|
555
|
+
}): DatabaseSchema {
|
|
556
|
+
return {
|
|
557
|
+
name: opts.name,
|
|
558
|
+
tables: opts.tables,
|
|
559
|
+
tables_resolved: opts.tables_resolved ?? true,
|
|
560
|
+
child_schemas: opts.child_schemas ?? [],
|
|
561
|
+
child_schemas_resolved: opts.child_schemas_resolved ?? true,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function makeDatabase(
|
|
566
|
+
name: string,
|
|
567
|
+
schemas: DatabaseSchema[],
|
|
568
|
+
schemas_resolved = true,
|
|
569
|
+
): Database {
|
|
570
|
+
return { name, dialect: "duckdb", schemas, schemas_resolved, engine: null };
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
describe("schemaSubtreeMatchesSearch", () => {
|
|
574
|
+
it("returns false for an empty query", () => {
|
|
575
|
+
const schema = makeSchema({ name: "main", tables: [makeTable("users")] });
|
|
576
|
+
expect(schemaSubtreeMatchesSearch(schema, "")).toBe(false);
|
|
577
|
+
expect(schemaSubtreeMatchesSearch(schema, " ")).toBe(false);
|
|
578
|
+
expect(schemaSubtreeMatchesSearch(schema, undefined)).toBe(false);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
it("matches a table name case-insensitively", () => {
|
|
582
|
+
const schema = makeSchema({ name: "main", tables: [makeTable("Users")] });
|
|
583
|
+
expect(schemaSubtreeMatchesSearch(schema, "user")).toBe(true);
|
|
584
|
+
expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
it("matches a table in a resolved child schema", () => {
|
|
588
|
+
const schema = makeSchema({
|
|
589
|
+
name: "parent",
|
|
590
|
+
tables: [],
|
|
591
|
+
child_schemas: [
|
|
592
|
+
makeSchema({ name: "child", tables: [makeTable("orders")] }),
|
|
593
|
+
],
|
|
594
|
+
});
|
|
595
|
+
expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(true);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it("ignores deferred tables so search never triggers a fetch", () => {
|
|
599
|
+
const schema = makeSchema({
|
|
600
|
+
name: "main",
|
|
601
|
+
tables: [makeTable("users")],
|
|
602
|
+
tables_resolved: false,
|
|
603
|
+
});
|
|
604
|
+
expect(schemaSubtreeMatchesSearch(schema, "users")).toBe(false);
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
it("ignores deferred child schemas", () => {
|
|
608
|
+
const schema = makeSchema({
|
|
609
|
+
name: "parent",
|
|
610
|
+
tables: [],
|
|
611
|
+
child_schemas: [
|
|
612
|
+
makeSchema({ name: "child", tables: [makeTable("orders")] }),
|
|
613
|
+
],
|
|
614
|
+
child_schemas_resolved: false,
|
|
615
|
+
});
|
|
616
|
+
expect(schemaSubtreeMatchesSearch(schema, "orders")).toBe(false);
|
|
617
|
+
});
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
describe("shouldExpandDatabaseForSearch", () => {
|
|
621
|
+
it("returns false for an empty query", () => {
|
|
622
|
+
const db = makeDatabase("memory", [
|
|
623
|
+
makeSchema({ name: "main", tables: [makeTable("users")] }),
|
|
624
|
+
]);
|
|
625
|
+
expect(shouldExpandDatabaseForSearch(db, "")).toBe(false);
|
|
626
|
+
expect(shouldExpandDatabaseForSearch(db, undefined)).toBe(false);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
it("expands when a loaded schema contains a matching table", () => {
|
|
630
|
+
const db = makeDatabase("memory", [
|
|
631
|
+
makeSchema({ name: "main", tables: [makeTable("users")] }),
|
|
632
|
+
makeSchema({ name: "other", tables: [makeTable("orders")] }),
|
|
633
|
+
]);
|
|
634
|
+
expect(shouldExpandDatabaseForSearch(db, "user")).toBe(true);
|
|
635
|
+
expect(shouldExpandDatabaseForSearch(db, "nomatch")).toBe(false);
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it("does not expand when the schema list itself is deferred", () => {
|
|
639
|
+
const db = makeDatabase(
|
|
640
|
+
"memory",
|
|
641
|
+
[makeSchema({ name: "main", tables: [makeTable("users")] })],
|
|
642
|
+
/* schemas_resolved */ false,
|
|
643
|
+
);
|
|
644
|
+
expect(shouldExpandDatabaseForSearch(db, "users")).toBe(false);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
@@ -29,7 +29,7 @@ import { Button } from "../ui/button";
|
|
|
29
29
|
import { Tooltip } from "../ui/tooltip";
|
|
30
30
|
import { ColumnPreviewContainer } from "./components";
|
|
31
31
|
import { InstallPackageButton } from "./install-package-button";
|
|
32
|
-
import { convertStatsName, sqlCode } from "./utils";
|
|
32
|
+
import { convertStatsName, sqlCode, tableUniqueId } from "./utils";
|
|
33
33
|
|
|
34
34
|
export const DatasetColumnPreview: React.FC<{
|
|
35
35
|
table: DataTable;
|
|
@@ -48,9 +48,7 @@ export const DatasetColumnPreview: React.FC<{
|
|
|
48
48
|
tableName: table.name,
|
|
49
49
|
columnName: column.name,
|
|
50
50
|
sourceType: table.source_type,
|
|
51
|
-
fullyQualifiedTableName: sqlTableContext
|
|
52
|
-
? `${sqlTableContext.database}.${sqlTableContext.schema}.${table.name}`
|
|
53
|
-
: table.name,
|
|
51
|
+
fullyQualifiedTableName: tableUniqueId(sqlTableContext, table.name),
|
|
54
52
|
});
|
|
55
53
|
};
|
|
56
54
|
|
|
@@ -83,6 +83,8 @@ import {
|
|
|
83
83
|
areSchemasResolved,
|
|
84
84
|
areTablesResolved,
|
|
85
85
|
isSchemaless,
|
|
86
|
+
schemaSubtreeMatchesSearch,
|
|
87
|
+
shouldExpandDatabaseForSearch,
|
|
86
88
|
sqlCode,
|
|
87
89
|
tableUniqueId,
|
|
88
90
|
} from "./utils";
|
|
@@ -360,7 +362,6 @@ export const DataSources: React.FC = () => {
|
|
|
360
362
|
key={database.name}
|
|
361
363
|
connection={connection}
|
|
362
364
|
database={database}
|
|
363
|
-
hasSearch={hasSearch}
|
|
364
365
|
searchValue={searchValue}
|
|
365
366
|
/>
|
|
366
367
|
))}
|
|
@@ -435,7 +436,6 @@ interface DataSourceTree {
|
|
|
435
436
|
dialect: string;
|
|
436
437
|
engineName: string;
|
|
437
438
|
databaseName: string;
|
|
438
|
-
hasSearch: boolean;
|
|
439
439
|
searchValue?: string;
|
|
440
440
|
}
|
|
441
441
|
|
|
@@ -470,9 +470,8 @@ function buildSqlTableContext(
|
|
|
470
470
|
const DatabaseTree: React.FC<{
|
|
471
471
|
connection: DataSourceConnection;
|
|
472
472
|
database: Database;
|
|
473
|
-
hasSearch: boolean;
|
|
474
473
|
searchValue?: string;
|
|
475
|
-
}> = ({ connection, database,
|
|
474
|
+
}> = ({ connection, database, searchValue }) => {
|
|
476
475
|
const tree = React.useMemo<DataSourceTree>(
|
|
477
476
|
() => ({
|
|
478
477
|
engineName: connection.name,
|
|
@@ -480,7 +479,6 @@ const DatabaseTree: React.FC<{
|
|
|
480
479
|
dialect: connection.dialect,
|
|
481
480
|
defaultSchema: connection.default_schema,
|
|
482
481
|
defaultDatabase: connection.default_database,
|
|
483
|
-
hasSearch,
|
|
484
482
|
searchValue,
|
|
485
483
|
}),
|
|
486
484
|
[
|
|
@@ -489,7 +487,6 @@ const DatabaseTree: React.FC<{
|
|
|
489
487
|
connection.default_schema,
|
|
490
488
|
connection.default_database,
|
|
491
489
|
database.name,
|
|
492
|
-
hasSearch,
|
|
493
490
|
searchValue,
|
|
494
491
|
],
|
|
495
492
|
);
|
|
@@ -498,7 +495,7 @@ const DatabaseTree: React.FC<{
|
|
|
498
495
|
<DatabaseItem
|
|
499
496
|
engineName={connection.name}
|
|
500
497
|
database={database}
|
|
501
|
-
|
|
498
|
+
searchValue={searchValue}
|
|
502
499
|
>
|
|
503
500
|
<DataSourceTreeContext.Provider value={tree}>
|
|
504
501
|
<SchemaList
|
|
@@ -513,18 +510,30 @@ const DatabaseTree: React.FC<{
|
|
|
513
510
|
};
|
|
514
511
|
|
|
515
512
|
const DatabaseItem: React.FC<{
|
|
516
|
-
|
|
513
|
+
searchValue?: string;
|
|
517
514
|
engineName: string;
|
|
518
515
|
database: Database;
|
|
519
516
|
children: React.ReactNode;
|
|
520
|
-
}> = ({
|
|
521
|
-
|
|
517
|
+
}> = ({ searchValue, engineName, database, children }) => {
|
|
518
|
+
// Auto-expand during search only when a loaded schema under this database
|
|
519
|
+
// contains a matching table.
|
|
520
|
+
const expandForSearch = shouldExpandDatabaseForSearch(database, searchValue);
|
|
521
|
+
const [isExpanded, setIsExpanded] = React.useState(expandForSearch);
|
|
522
522
|
const [isSelected, setIsSelected] = React.useState(false);
|
|
523
|
-
const [
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
523
|
+
const [prevSearchValue, setPrevSearchValue] = React.useState(searchValue);
|
|
524
|
+
const [prevExpandForSearch, setPrevExpandForSearch] =
|
|
525
|
+
React.useState(expandForSearch);
|
|
526
|
+
|
|
527
|
+
// Re-sync when the query changes or when newly resolved data turns this
|
|
528
|
+
// branch into a match under the same query (deferred subtrees resolve
|
|
529
|
+
// asynchronously, so the decision can flip without the query changing).
|
|
530
|
+
if (
|
|
531
|
+
prevSearchValue !== searchValue ||
|
|
532
|
+
prevExpandForSearch !== expandForSearch
|
|
533
|
+
) {
|
|
534
|
+
setPrevSearchValue(searchValue);
|
|
535
|
+
setPrevExpandForSearch(expandForSearch);
|
|
536
|
+
setIsExpanded(expandForSearch);
|
|
528
537
|
}
|
|
529
538
|
|
|
530
539
|
return (
|
|
@@ -657,12 +666,30 @@ interface SchemaNodeProps {
|
|
|
657
666
|
const SchemaNode: React.FC<SchemaNodeProps> = (props) => {
|
|
658
667
|
const { schema, schemaPath, depth } = props;
|
|
659
668
|
const tree = useDataSourceTree();
|
|
660
|
-
const { databaseName,
|
|
661
|
-
|
|
669
|
+
const { databaseName, searchValue } = tree;
|
|
670
|
+
// Auto-expand during search only when this schema's loaded subtree contains a
|
|
671
|
+
// matching table.
|
|
672
|
+
const expandForSearch = schemaSubtreeMatchesSearch(schema, searchValue);
|
|
673
|
+
const [isExpanded, setIsExpanded] = React.useState(expandForSearch);
|
|
662
674
|
const [isSelected, setIsSelected] = React.useState(false);
|
|
675
|
+
const [prevSearchValue, setPrevSearchValue] = React.useState(searchValue);
|
|
676
|
+
const [prevExpandForSearch, setPrevExpandForSearch] =
|
|
677
|
+
React.useState(expandForSearch);
|
|
663
678
|
const uniqueValue = `${databaseName}:${schemaPath.join(".")}`;
|
|
664
679
|
const childSchemas = schema.child_schemas ?? [];
|
|
665
680
|
|
|
681
|
+
// Re-sync when the query changes or when newly resolved data turns this
|
|
682
|
+
// subtree into a match under the same query (deferred tables/child schemas
|
|
683
|
+
// resolve asynchronously, so the decision can flip without the query changing).
|
|
684
|
+
if (
|
|
685
|
+
prevSearchValue !== searchValue ||
|
|
686
|
+
prevExpandForSearch !== expandForSearch
|
|
687
|
+
) {
|
|
688
|
+
setPrevSearchValue(searchValue);
|
|
689
|
+
setPrevExpandForSearch(expandForSearch);
|
|
690
|
+
setIsExpanded(expandForSearch);
|
|
691
|
+
}
|
|
692
|
+
|
|
666
693
|
return (
|
|
667
694
|
<>
|
|
668
695
|
<CommandItem
|
|
@@ -52,6 +52,51 @@ export function areChildSchemasResolved(schema: DatabaseSchema): boolean {
|
|
|
52
52
|
return schema.child_schemas_resolved !== false;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Whether a loaded schema subtree contains a table whose name matches
|
|
57
|
+
* `searchValue`. Deferred (not-yet-fetched) tables and child schemas are
|
|
58
|
+
* treated as non-matching, so searching never triggers a lazy catalog fetch on
|
|
59
|
+
* large/remote connections.
|
|
60
|
+
*/
|
|
61
|
+
export function schemaSubtreeMatchesSearch(
|
|
62
|
+
schema: DatabaseSchema,
|
|
63
|
+
searchValue: string | undefined,
|
|
64
|
+
): boolean {
|
|
65
|
+
const query = searchValue?.trim().toLowerCase();
|
|
66
|
+
if (!query) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
if (
|
|
70
|
+
areTablesResolved(schema) &&
|
|
71
|
+
schema.tables.some((table) => table.name.toLowerCase().includes(query))
|
|
72
|
+
) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (areChildSchemasResolved(schema)) {
|
|
76
|
+
return (schema.child_schemas ?? []).some((child) =>
|
|
77
|
+
schemaSubtreeMatchesSearch(child, query),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Auto-expand a database row during search only when one of its loaded schemas
|
|
85
|
+
* contains a matching table. Returns false when the schema list itself is
|
|
86
|
+
* deferred.
|
|
87
|
+
*/
|
|
88
|
+
export function shouldExpandDatabaseForSearch(
|
|
89
|
+
database: Database,
|
|
90
|
+
searchValue: string | undefined,
|
|
91
|
+
): boolean {
|
|
92
|
+
if (!searchValue?.trim() || !areSchemasResolved(database)) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
return database.schemas.some((schema) =>
|
|
96
|
+
schemaSubtreeMatchesSearch(schema, searchValue),
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
55
100
|
interface SqlCodeFormatter {
|
|
56
101
|
/**
|
|
57
102
|
* Format the table path based on dialect-specific rules
|
|
@@ -41,7 +41,10 @@ import {
|
|
|
41
41
|
YoutubeIcon,
|
|
42
42
|
ZapIcon,
|
|
43
43
|
} from "lucide-react";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
settingDialogAtom,
|
|
46
|
+
useOpenSettingsToTab,
|
|
47
|
+
} from "@/components/app-config/state";
|
|
45
48
|
import { MarkdownIcon } from "@/components/editor/cell/code/icons";
|
|
46
49
|
import { MarimoPlusIcon } from "@/components/icons/marimo-icons";
|
|
47
50
|
import { useImperativeModal } from "@/components/modal/ImperativeModal";
|
|
@@ -57,7 +60,8 @@ import {
|
|
|
57
60
|
useCellActions,
|
|
58
61
|
} from "@/core/cells/cells";
|
|
59
62
|
import { disabledCellIds } from "@/core/cells/utils";
|
|
60
|
-
import {
|
|
63
|
+
import { capabilitiesAtom } from "@/core/config/capabilities";
|
|
64
|
+
import { aiEnabledAtom, useResolvedMarimoConfig } from "@/core/config/config";
|
|
61
65
|
import { Constants } from "@/core/constants";
|
|
62
66
|
import {
|
|
63
67
|
updateCellOutputsWithScreenshots,
|
|
@@ -86,7 +90,7 @@ import { Strings } from "@/utils/strings";
|
|
|
86
90
|
import { newNotebookURL } from "@/utils/urls";
|
|
87
91
|
import { useRunAllCells } from "../cell/useRunCells";
|
|
88
92
|
import { useChromeActions, useChromeState } from "../chrome/state";
|
|
89
|
-
import { PANELS } from "../chrome/types";
|
|
93
|
+
import { isPanelHidden, PANELS } from "../chrome/types";
|
|
90
94
|
import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
|
|
91
95
|
import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
|
|
92
96
|
import { commandPaletteAtom } from "../controls/state";
|
|
@@ -112,6 +116,8 @@ export function useNotebookActions() {
|
|
|
112
116
|
const kioskMode = useAtomValue(kioskModeAtom);
|
|
113
117
|
const hideAllMarkdownCode = useHideAllMarkdownCode();
|
|
114
118
|
const [resolvedConfig] = useResolvedMarimoConfig();
|
|
119
|
+
const capabilities = useAtomValue(capabilitiesAtom);
|
|
120
|
+
const aiEnabled = useAtomValue(aiEnabledAtom);
|
|
115
121
|
|
|
116
122
|
const {
|
|
117
123
|
updateCellConfig,
|
|
@@ -126,6 +132,7 @@ export function useNotebookActions() {
|
|
|
126
132
|
const copyNotebook = useCopyNotebook(filename);
|
|
127
133
|
const setCommandPaletteOpen = useSetAtom(commandPaletteAtom);
|
|
128
134
|
const setSettingsDialogOpen = useSetAtom(settingDialogAtom);
|
|
135
|
+
const { handleClick: openSettings } = useOpenSettingsToTab();
|
|
129
136
|
const setKeyboardShortcutsOpen = useSetAtom(keyboardShortcutsAtom);
|
|
130
137
|
const {
|
|
131
138
|
exportAsIPYNB,
|
|
@@ -407,20 +414,31 @@ export function useNotebookActions() {
|
|
|
407
414
|
label: "Helper panel",
|
|
408
415
|
redundant: true,
|
|
409
416
|
handle: NOOP_HANDLER,
|
|
410
|
-
dropdown: PANELS.flatMap(
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
417
|
+
dropdown: PANELS.flatMap((panel) => {
|
|
418
|
+
// Still show the AI panel in the command palette so users can try AI
|
|
419
|
+
// features. When AI is disabled, open settings instead of the panel.
|
|
420
|
+
const openAiSettingsWhenDisabled = panel.type === "ai" && !aiEnabled;
|
|
421
|
+
if (
|
|
422
|
+
isPanelHidden({ panel, capabilities, aiEnabled }) &&
|
|
423
|
+
!openAiSettingsWhenDisabled
|
|
424
|
+
) {
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
const { type: id, Icon, additionalKeywords } = panel;
|
|
428
|
+
return {
|
|
429
|
+
label: Strings.startCase(id),
|
|
430
|
+
rightElement: renderCheckboxElement(selectedPanel === id),
|
|
431
|
+
icon: <Icon size={14} strokeWidth={1.5} />,
|
|
432
|
+
handle: () => {
|
|
433
|
+
if (openAiSettingsWhenDisabled) {
|
|
434
|
+
openSettings("ai", "ai-features");
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
toggleApplication(id);
|
|
438
|
+
},
|
|
439
|
+
additionalKeywords,
|
|
440
|
+
};
|
|
441
|
+
}),
|
|
424
442
|
},
|
|
425
443
|
|
|
426
444
|
{
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
|
+
import { useAtomValue } from "jotai";
|
|
4
5
|
import React from "react";
|
|
5
6
|
import { type DefaultValues, type FieldValues, useForm } from "react-hook-form";
|
|
6
7
|
import type { z } from "zod";
|
|
@@ -16,8 +17,10 @@ import {
|
|
|
16
17
|
SelectTrigger,
|
|
17
18
|
SelectValue,
|
|
18
19
|
} from "@/components/ui/select";
|
|
20
|
+
import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
|
|
19
21
|
import { useCellActions } from "@/core/cells/cells";
|
|
20
22
|
import { useLastFocusedCellId } from "@/core/cells/focus";
|
|
23
|
+
import { autoInstantiateAtom } from "@/core/config/config";
|
|
21
24
|
import { ENV_RENDERER, SecretsProvider } from "./form-renderers";
|
|
22
25
|
|
|
23
26
|
const RENDERERS: FormRenderer[] = [ENV_RENDERER];
|
|
@@ -106,8 +109,18 @@ export const ConnectionFormFooter = <L extends string>({
|
|
|
106
109
|
export function useInsertCode() {
|
|
107
110
|
const { createNewCell } = useCellActions();
|
|
108
111
|
const lastFocusedCellId = useLastFocusedCellId();
|
|
112
|
+
const autoInstantiate = useAtomValue(autoInstantiateAtom);
|
|
109
113
|
|
|
110
114
|
return (code: string) => {
|
|
115
|
+
// Ensure `mo` is importable when the generated code references it
|
|
116
|
+
if (/\bmo\./.test(code)) {
|
|
117
|
+
maybeAddMarimoImport({
|
|
118
|
+
autoInstantiate,
|
|
119
|
+
createNewCell,
|
|
120
|
+
fromCellId: lastFocusedCellId,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
111
124
|
createNewCell({
|
|
112
125
|
code,
|
|
113
126
|
before: false,
|
package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap
CHANGED
|
@@ -85,14 +85,14 @@ store = GCSStore("my-bucket")"
|
|
|
85
85
|
exports[`generateStorageCode > Google Drive > with default auth (no credentials) 1`] = `
|
|
86
86
|
"from gdrive_fsspec import GoogleDriveFileSystem
|
|
87
87
|
|
|
88
|
-
fs = GoogleDriveFileSystem(use_listings_cache=False)"
|
|
88
|
+
fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True)"
|
|
89
89
|
`;
|
|
90
90
|
|
|
91
91
|
exports[`generateStorageCode > Google Drive > with embedded auth (no credentials) 1`] = `
|
|
92
92
|
"from gdrive_fsspec import GoogleDriveFileSystem
|
|
93
93
|
|
|
94
|
-
fs = GoogleDriveFileSystem(use_listings_cache=False, auth_kwargs={"use_local_webserver": False})
|
|
95
|
-
|
|
94
|
+
fs = GoogleDriveFileSystem(use_listings_cache=False, skip_instance_cache=True, auth_kwargs={"use_local_webserver": False})
|
|
95
|
+
mo.output.clear_console()"
|
|
96
96
|
`;
|
|
97
97
|
|
|
98
98
|
exports[`generateStorageCode > Google Drive > with service account credentials 1`] = `
|
|
@@ -100,7 +100,7 @@ exports[`generateStorageCode > Google Drive > with service account credentials 1
|
|
|
100
100
|
import json
|
|
101
101
|
|
|
102
102
|
_creds = json.loads("""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
|
|
103
|
-
fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False)"
|
|
103
|
+
fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
|
|
104
104
|
`;
|
|
105
105
|
|
|
106
106
|
exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
|