@marimo-team/islands 0.23.16-dev51 → 0.23.16-dev52
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/{chat-ui-1e7PcW56.js → chat-ui-CVU_VO50.js} +2 -2
- package/dist/{common-CD6phlRu.js → common-CvsviStD.js} +2 -2
- package/dist/{html-to-image-3w0AakWW.js → html-to-image-lsMuxJbp.js} +5 -1
- package/dist/main.js +7 -5
- package/dist/{process-output-BRwR4QGs.js → process-output-DiSWOzWN.js} +1 -1
- package/dist/{reveal-component-DmGGKS8z.js → reveal-component-D6N7pZkc.js} +2 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +1 -0
- package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
- package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
- package/src/core/datasets/data-source-discovery.ts +5 -0
- package/src/core/datasets/request-registry.ts +11 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/kernel/messages.ts +2 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +15 -0
- package/src/core/network/__tests__/requests-network.test.ts +14 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +2 -0
- package/src/core/wasm/bridge.ts +10 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
- package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
- package/src/hooks/useDataSourceDiscovery.ts +18 -0
package/package.json
CHANGED
|
@@ -41,6 +41,7 @@ export const MockRequestClient = {
|
|
|
41
41
|
previewSQLTableList: vi.fn().mockResolvedValue({ tables: [] }),
|
|
42
42
|
previewSQLSchemaList: vi.fn().mockResolvedValue({ schemas: [] }),
|
|
43
43
|
previewDataSourceConnection: vi.fn().mockResolvedValue({}),
|
|
44
|
+
discoverDataSources: vi.fn().mockResolvedValue({}),
|
|
44
45
|
validateSQL: vi.fn().mockResolvedValue({}),
|
|
45
46
|
openFile: vi.fn().mockResolvedValue({}),
|
|
46
47
|
getUsageStats: vi.fn().mockResolvedValue({}),
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
|
|
6
|
+
import { QuickAddDataSources } from "../quick-add-data-sources";
|
|
7
|
+
|
|
8
|
+
const sources: DetectedDataSource[] = [
|
|
9
|
+
{
|
|
10
|
+
id: "postgres-libpq-environment",
|
|
11
|
+
integration: "postgres",
|
|
12
|
+
category: "database",
|
|
13
|
+
displayName: "PostgreSQL",
|
|
14
|
+
confidence: "high",
|
|
15
|
+
origins: [{ type: "environment", label: "Kernel environment" }],
|
|
16
|
+
configuration: [
|
|
17
|
+
{
|
|
18
|
+
field: "Host",
|
|
19
|
+
value: { kind: "environment-variable", name: "PGHOST" },
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
field: "Username",
|
|
23
|
+
value: { kind: "environment-variable", name: "PGUSER" },
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
field: "Database",
|
|
27
|
+
value: { kind: "environment-variable", name: "PGDATABASE" },
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
code: "engine = create_engine()",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "pyiceberg-prod",
|
|
34
|
+
integration: "pyiceberg",
|
|
35
|
+
category: "catalog",
|
|
36
|
+
displayName: "PyIceberg (prod)",
|
|
37
|
+
confidence: "high",
|
|
38
|
+
origins: [
|
|
39
|
+
{
|
|
40
|
+
type: "configuration",
|
|
41
|
+
label: "Resolved PyIceberg configuration",
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
configuration: [
|
|
45
|
+
{
|
|
46
|
+
field: "Catalog",
|
|
47
|
+
value: { kind: "safe-literal", value: "prod" },
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
field: "Type",
|
|
51
|
+
value: { kind: "safe-literal", value: "REST" },
|
|
52
|
+
},
|
|
53
|
+
],
|
|
54
|
+
code: 'catalog = load_catalog("prod")',
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
describe("QuickAddDataSources", () => {
|
|
59
|
+
it("does not render an empty section", () => {
|
|
60
|
+
const { container } = render(
|
|
61
|
+
<QuickAddDataSources sources={[]} onAdd={vi.fn()} />,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
expect(container).toBeEmptyDOMElement();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("renders detected sources as clickable tags", () => {
|
|
68
|
+
const onAdd = vi.fn();
|
|
69
|
+
|
|
70
|
+
render(<QuickAddDataSources sources={sources} onAdd={onAdd} />);
|
|
71
|
+
fireEvent.click(
|
|
72
|
+
screen.getByRole("button", {
|
|
73
|
+
name: "Add PostgreSQL connection",
|
|
74
|
+
}),
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(screen.getByText("Quick add")).toBeInTheDocument();
|
|
78
|
+
expect(onAdd).toHaveBeenCalledWith(sources[0]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("shows environment references on hover", async () => {
|
|
82
|
+
render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
|
|
83
|
+
const tag = screen.getByRole("button", {
|
|
84
|
+
name: "Add PostgreSQL connection",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
fireEvent.pointerMove(tag);
|
|
88
|
+
fireEvent.mouseOver(tag);
|
|
89
|
+
|
|
90
|
+
await waitFor(() => {
|
|
91
|
+
expect(screen.getAllByText('os.environ["PGHOST"]')).not.toHaveLength(0);
|
|
92
|
+
});
|
|
93
|
+
expect(screen.getAllByText('os.environ["PGUSER"]')).not.toHaveLength(0);
|
|
94
|
+
expect(screen.getAllByText('os.environ["PGDATABASE"]')).not.toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("shows safe configuration metadata on hover", async () => {
|
|
98
|
+
render(<QuickAddDataSources sources={sources} onAdd={vi.fn()} />);
|
|
99
|
+
const tag = screen.getByRole("button", {
|
|
100
|
+
name: "Add PyIceberg (prod) connection",
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
fireEvent.pointerMove(tag);
|
|
104
|
+
fireEvent.mouseOver(tag);
|
|
105
|
+
|
|
106
|
+
await waitFor(() => {
|
|
107
|
+
expect(
|
|
108
|
+
screen.getAllByText("Detected from Resolved PyIceberg configuration"),
|
|
109
|
+
).not.toHaveLength(0);
|
|
110
|
+
});
|
|
111
|
+
expect(screen.getAllByText("REST")).not.toHaveLength(0);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { ExternalLink } from "@/components/ui/links";
|
|
13
13
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
14
14
|
import { AddDatabaseForm } from "./database/add-database-form";
|
|
15
|
+
import { AutoDiscoveredDataSources } from "./quick-add-data-sources";
|
|
15
16
|
import { AddStorageForm } from "./storage/add-storage-form";
|
|
16
17
|
|
|
17
18
|
type ConnectionTab = "databases" | "storage";
|
|
@@ -86,6 +87,7 @@ export const AddConnectionDialogContent: React.FC<{
|
|
|
86
87
|
<span className="block">{codeSnippetHint}</span>
|
|
87
88
|
</DialogDescription>
|
|
88
89
|
</DialogHeader>
|
|
90
|
+
<AutoDiscoveredDataSources onSubmit={onClose} />
|
|
89
91
|
<Tabs
|
|
90
92
|
value={activeTab}
|
|
91
93
|
onValueChange={(v) => setActiveTab(v as ConnectionTab)}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { PlusIcon, SparklesIcon } from "lucide-react";
|
|
4
|
+
import { Tooltip, TooltipProvider } from "@/components/ui/tooltip";
|
|
5
|
+
import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
|
|
6
|
+
import { useDataSourceDiscovery } from "@/hooks/useDataSourceDiscovery";
|
|
7
|
+
import { useInsertCode } from "./components";
|
|
8
|
+
|
|
9
|
+
export const QuickAddDataSources: React.FC<{
|
|
10
|
+
sources: DetectedDataSource[];
|
|
11
|
+
onAdd: (source: DetectedDataSource) => void;
|
|
12
|
+
}> = ({ sources, onAdd }) => {
|
|
13
|
+
if (sources.length === 0) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<section
|
|
19
|
+
aria-labelledby="quick-add-data-sources-title"
|
|
20
|
+
className="rounded-md border bg-muted/30 px-3 py-2"
|
|
21
|
+
>
|
|
22
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
23
|
+
<div className="mr-1 flex items-center gap-1.5">
|
|
24
|
+
<SparklesIcon className="h-3.5 w-3.5 text-muted-foreground" />
|
|
25
|
+
<h3 id="quick-add-data-sources-title" className="text-sm font-medium">
|
|
26
|
+
Quick add
|
|
27
|
+
</h3>
|
|
28
|
+
</div>
|
|
29
|
+
<TooltipProvider delayDuration={200}>
|
|
30
|
+
{sources.map((source) => (
|
|
31
|
+
<Tooltip
|
|
32
|
+
key={source.id}
|
|
33
|
+
side="bottom"
|
|
34
|
+
content={<DetectedDataSourceDetails source={source} />}
|
|
35
|
+
>
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
aria-label={`Add ${source.displayName} connection`}
|
|
39
|
+
className="inline-flex items-center gap-1 rounded-full border border-(--blue-8) bg-(--blue-2) px-2.5 py-1 text-xs font-semibold text-(--blue-11) transition-colors hover:bg-(--blue-3) focus:outline-hidden focus:ring-2 focus:ring-ring focus:ring-offset-2"
|
|
40
|
+
onClick={() => onAdd(source)}
|
|
41
|
+
>
|
|
42
|
+
<PlusIcon className="h-3 w-3" />
|
|
43
|
+
{source.displayName}
|
|
44
|
+
</button>
|
|
45
|
+
</Tooltip>
|
|
46
|
+
))}
|
|
47
|
+
</TooltipProvider>
|
|
48
|
+
</div>
|
|
49
|
+
</section>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const DetectedDataSourceDetails: React.FC<{
|
|
54
|
+
source: DetectedDataSource;
|
|
55
|
+
}> = ({ source }) => (
|
|
56
|
+
<div className="min-w-64 space-y-2 py-1">
|
|
57
|
+
<div>
|
|
58
|
+
<div className="font-medium">{source.displayName}</div>
|
|
59
|
+
<div className="text-xs text-muted-foreground">
|
|
60
|
+
Detected from{" "}
|
|
61
|
+
{source.origins.map((origin) => origin.label).join(" and ")}
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-3 gap-y-1 text-xs">
|
|
65
|
+
{source.configuration.map((item) => (
|
|
66
|
+
<div
|
|
67
|
+
className="contents"
|
|
68
|
+
key={
|
|
69
|
+
item.value.kind === "environment-variable"
|
|
70
|
+
? item.value.name
|
|
71
|
+
: `${item.field}:${item.value.value}`
|
|
72
|
+
}
|
|
73
|
+
>
|
|
74
|
+
<dt className="text-muted-foreground">{item.field}</dt>
|
|
75
|
+
<dd>
|
|
76
|
+
<code>
|
|
77
|
+
{item.value.kind === "environment-variable"
|
|
78
|
+
? `os.environ["${item.value.name}"]`
|
|
79
|
+
: item.value.value}
|
|
80
|
+
</code>
|
|
81
|
+
</dd>
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</dl>
|
|
85
|
+
<div className="text-xs text-muted-foreground">
|
|
86
|
+
Click to add a configured cell.
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
export const AutoDiscoveredDataSources: React.FC<{
|
|
92
|
+
onSubmit: () => void;
|
|
93
|
+
}> = ({ onSubmit }) => {
|
|
94
|
+
const insertCode = useInsertCode();
|
|
95
|
+
const { data } = useDataSourceDiscovery();
|
|
96
|
+
|
|
97
|
+
return (
|
|
98
|
+
<QuickAddDataSources
|
|
99
|
+
sources={data ?? []}
|
|
100
|
+
onAdd={(source) => {
|
|
101
|
+
insertCode(source.code);
|
|
102
|
+
onSubmit();
|
|
103
|
+
}}
|
|
104
|
+
/>
|
|
105
|
+
);
|
|
106
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import type {
|
|
3
|
+
DataSourceDiscoveryResult,
|
|
3
4
|
SQLSchemaListPreview,
|
|
4
5
|
SQLTableListPreview,
|
|
5
6
|
SQLTablePreview,
|
|
@@ -19,6 +20,16 @@ import type {
|
|
|
19
20
|
// The backend returns data tables, which could also exist in other engines, dbs, schemas
|
|
20
21
|
// Thus, we use the request ID pattern to match the response to the request
|
|
21
22
|
|
|
23
|
+
export const DiscoverDataSources = new DeferredRequestRegistry<
|
|
24
|
+
{},
|
|
25
|
+
DataSourceDiscoveryResult
|
|
26
|
+
>("data-source-discovery-result", async (requestId) => {
|
|
27
|
+
const client = getRequestClient();
|
|
28
|
+
await client.discoverDataSources({
|
|
29
|
+
requestId,
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
22
33
|
export const PreviewSQLTable = new DeferredRequestRegistry<
|
|
23
34
|
Omit<PreviewSQLTableRequest, "requestId">,
|
|
24
35
|
SQLTablePreview
|
|
@@ -341,6 +341,7 @@ export class IslandsPyodideBridge implements RunRequests, EditRequests {
|
|
|
341
341
|
previewSQLTableList = throwNotImplemented;
|
|
342
342
|
previewSQLSchemaList = throwNotImplemented;
|
|
343
343
|
previewDataSourceConnection = throwNotImplemented;
|
|
344
|
+
discoverDataSources = throwNotImplemented;
|
|
344
345
|
validateSQL = throwNotImplemented;
|
|
345
346
|
openFile = throwNotImplemented;
|
|
346
347
|
sendListFiles = throwNotImplemented;
|
|
@@ -44,6 +44,8 @@ export type SQLTableListPreview =
|
|
|
44
44
|
export type SQLSchemaListPreview =
|
|
45
45
|
NotificationMessageData<"sql-schema-list-preview">;
|
|
46
46
|
export type ValidateSQLResult = NotificationMessageData<"validate-sql-result">;
|
|
47
|
+
export type DataSourceDiscoveryResult =
|
|
48
|
+
NotificationMessageData<"data-source-discovery-result">;
|
|
47
49
|
export type SecretKeysResult = NotificationMessageData<"secret-keys-result">;
|
|
48
50
|
export type StartupLogs = NotificationMessageData<"startup-logs">;
|
|
49
51
|
export type CellMessage = NotificationMessageData<"cell-op">;
|
|
@@ -114,6 +114,21 @@ describe("createLazyRequests", () => {
|
|
|
114
114
|
expect(mockInit).toHaveBeenCalledTimes(1);
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
it("starts the kernel for datasource discovery", async () => {
|
|
118
|
+
mockDelegate.discoverDataSources = vi.fn().mockResolvedValue(null);
|
|
119
|
+
const lazyRequests = createLazyRequests(
|
|
120
|
+
mockDelegate,
|
|
121
|
+
mockGetRuntimeManager,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await lazyRequests.discoverDataSources({
|
|
125
|
+
requestId: requestId("discovery"),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
expect(mockInit).toHaveBeenCalledOnce();
|
|
129
|
+
expect(mockDelegate.discoverDataSources).toHaveBeenCalledOnce();
|
|
130
|
+
});
|
|
131
|
+
|
|
117
132
|
it("should only call init once across multiple requests", async () => {
|
|
118
133
|
const lazyRequests = createLazyRequests(
|
|
119
134
|
mockDelegate,
|
|
@@ -130,6 +130,20 @@ describe("createNetworkRequests", () => {
|
|
|
130
130
|
expect(mockClient.GET).toHaveBeenCalledWith("/api/export/availability");
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
it("discoverDataSources should POST to the discovery endpoint", async () => {
|
|
134
|
+
const requests = createNetworkRequests();
|
|
135
|
+
const request = { requestId: "discovery-request" } as any;
|
|
136
|
+
await requests.discoverDataSources(request);
|
|
137
|
+
|
|
138
|
+
expect(mockClient.POST).toHaveBeenCalledWith(
|
|
139
|
+
"/api/datasources/discover",
|
|
140
|
+
expect.objectContaining({
|
|
141
|
+
body: request,
|
|
142
|
+
params: expect.anything(),
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
145
|
+
});
|
|
146
|
+
|
|
133
147
|
it("getPackageList should not require a kernel connection", async () => {
|
|
134
148
|
const { waitForConnectionOpen, waitForConnectionOpenIfNotebook } =
|
|
135
149
|
await import("../connection");
|
|
@@ -51,6 +51,7 @@ const ACTIONS: Record<keyof AllRequests, Action> = {
|
|
|
51
51
|
sendRunScratchpad: "startConnection",
|
|
52
52
|
saveAppConfig: "startConnection",
|
|
53
53
|
saveCellConfig: "startConnection",
|
|
54
|
+
discoverDataSources: "startConnection",
|
|
54
55
|
|
|
55
56
|
// Export operations start a connection
|
|
56
57
|
exportAsHTML: "startConnection",
|
|
@@ -267,6 +267,14 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
267
267
|
})
|
|
268
268
|
.then(handleResponseReturnNull);
|
|
269
269
|
},
|
|
270
|
+
discoverDataSources: (request) => {
|
|
271
|
+
return getClient()
|
|
272
|
+
.POST("/api/datasources/discover", {
|
|
273
|
+
body: request,
|
|
274
|
+
params: getParams(),
|
|
275
|
+
})
|
|
276
|
+
.then(handleResponseReturnNull);
|
|
277
|
+
},
|
|
270
278
|
validateSQL: (request) => {
|
|
271
279
|
return getClient()
|
|
272
280
|
.POST("/api/sql/validate", {
|
|
@@ -62,6 +62,7 @@ export function createStaticRequests(): EditRequests & RunRequests {
|
|
|
62
62
|
previewSQLTableList: throwNotInEditMode,
|
|
63
63
|
previewSQLSchemaList: throwNotInEditMode,
|
|
64
64
|
previewDataSourceConnection: throwNotInEditMode,
|
|
65
|
+
discoverDataSources: throwNotInEditMode,
|
|
65
66
|
validateSQL: throwNotInEditMode,
|
|
66
67
|
openFile: throwNotInEditMode,
|
|
67
68
|
getUsageStats: throwNotInEditMode,
|
|
@@ -43,6 +43,7 @@ export function createErrorToastingRequests(
|
|
|
43
43
|
previewSQLTableList: "Failed to fetch SQL table list",
|
|
44
44
|
previewSQLSchemaList: "Failed to fetch SQL schema list",
|
|
45
45
|
previewDataSourceConnection: "Failed to preview data source connection",
|
|
46
|
+
discoverDataSources: "Failed to discover data sources",
|
|
46
47
|
validateSQL: "Failed to validate SQL",
|
|
47
48
|
openFile: "Failed to open file",
|
|
48
49
|
getUsageStats: "", // No toast
|
|
@@ -75,6 +75,7 @@ export type ListSQLTablesRequest = schemas["ListSQLTablesRequest"];
|
|
|
75
75
|
export type ListSQLSchemasRequest = schemas["ListSQLSchemasRequest"];
|
|
76
76
|
export type ListDataSourceConnectionRequest =
|
|
77
77
|
schemas["ListDataSourceConnectionRequest"];
|
|
78
|
+
export type DiscoverDataSourcesRequest = schemas["DiscoverDataSourcesRequest"];
|
|
78
79
|
export type ValidateSQLRequest = schemas["ValidateSQLRequest"];
|
|
79
80
|
export type DebugCellRequest = schemas["DebugCellRequest"];
|
|
80
81
|
export type SetBreakpointsRequest = schemas["SetBreakpointsRequest"];
|
|
@@ -181,6 +182,7 @@ export interface EditRequests {
|
|
|
181
182
|
previewDataSourceConnection: (
|
|
182
183
|
request: ListDataSourceConnectionRequest,
|
|
183
184
|
) => Promise<null>;
|
|
185
|
+
discoverDataSources: (request: DiscoverDataSourcesRequest) => Promise<null>;
|
|
184
186
|
validateSQL: (request: ValidateSQLRequest) => Promise<null>;
|
|
185
187
|
openFile: (request: { path: string; lineNumber?: number }) => Promise<null>;
|
|
186
188
|
getUsageStats: () => Promise<UsageResponse>;
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -630,6 +630,16 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
630
630
|
return null;
|
|
631
631
|
};
|
|
632
632
|
|
|
633
|
+
discoverDataSources: EditRequests["discoverDataSources"] = async (
|
|
634
|
+
request,
|
|
635
|
+
) => {
|
|
636
|
+
await this.putControlRequest({
|
|
637
|
+
type: "discover-data-sources",
|
|
638
|
+
...request,
|
|
639
|
+
});
|
|
640
|
+
return null;
|
|
641
|
+
};
|
|
642
|
+
|
|
633
643
|
getUsageStats = throwNotImplemented;
|
|
634
644
|
getEnvironmentInfo: EditRequests["getEnvironmentInfo"] = async () => {
|
|
635
645
|
const response = await this.rpc.proxy.request.bridge({
|
|
@@ -42,6 +42,7 @@ import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
|
|
|
42
42
|
import { useDataSourceActions } from "../datasets/data-source-connections";
|
|
43
43
|
import type { ConnectionName } from "../datasets/engines";
|
|
44
44
|
import {
|
|
45
|
+
DiscoverDataSources,
|
|
45
46
|
PreviewSQLSchemaList,
|
|
46
47
|
PreviewSQLTable,
|
|
47
48
|
PreviewSQLTableList,
|
|
@@ -394,6 +395,9 @@ export function useMarimoKernelConnection(opts: {
|
|
|
394
395
|
case "validate-sql-result":
|
|
395
396
|
ValidateSQL.resolve(msg.data.request_id as RequestId, msg.data);
|
|
396
397
|
return;
|
|
398
|
+
case "data-source-discovery-result":
|
|
399
|
+
DiscoverDataSources.resolve(msg.data.request_id as RequestId, msg.data);
|
|
400
|
+
return;
|
|
397
401
|
case "secret-keys-result":
|
|
398
402
|
SECRETS_REGISTRY.resolve(msg.data.request_id, msg.data);
|
|
399
403
|
return;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { DiscoverDataSources } from "@/core/datasets/request-registry";
|
|
5
|
+
import { loadDataSourceDiscovery } from "../useDataSourceDiscovery";
|
|
6
|
+
|
|
7
|
+
describe("loadDataSourceDiscovery", () => {
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
vi.restoreAllMocks();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("returns the kernel's secret-free discovery model", async () => {
|
|
13
|
+
const sources = [
|
|
14
|
+
{
|
|
15
|
+
id: "pyiceberg-prod",
|
|
16
|
+
integration: "pyiceberg",
|
|
17
|
+
category: "catalog" as const,
|
|
18
|
+
displayName: "PyIceberg (prod)",
|
|
19
|
+
confidence: "high" as const,
|
|
20
|
+
origins: [
|
|
21
|
+
{
|
|
22
|
+
type: "configuration" as const,
|
|
23
|
+
label: "Resolved PyIceberg configuration",
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
configuration: [
|
|
27
|
+
{
|
|
28
|
+
field: "Catalog",
|
|
29
|
+
value: {
|
|
30
|
+
kind: "safe-literal" as const,
|
|
31
|
+
value: "prod",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
code: 'catalog = load_catalog("prod")',
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
const request = vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
|
|
39
|
+
request_id: "request-id",
|
|
40
|
+
sources,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
const detected = await loadDataSourceDiscovery();
|
|
44
|
+
|
|
45
|
+
expect({ detected, requests: request.mock.calls }).toMatchInlineSnapshot(`
|
|
46
|
+
{
|
|
47
|
+
"detected": [
|
|
48
|
+
{
|
|
49
|
+
"category": "catalog",
|
|
50
|
+
"code": "catalog = load_catalog("prod")",
|
|
51
|
+
"confidence": "high",
|
|
52
|
+
"configuration": [
|
|
53
|
+
{
|
|
54
|
+
"field": "Catalog",
|
|
55
|
+
"value": {
|
|
56
|
+
"kind": "safe-literal",
|
|
57
|
+
"value": "prod",
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
"displayName": "PyIceberg (prod)",
|
|
62
|
+
"id": "pyiceberg-prod",
|
|
63
|
+
"integration": "pyiceberg",
|
|
64
|
+
"origins": [
|
|
65
|
+
{
|
|
66
|
+
"label": "Resolved PyIceberg configuration",
|
|
67
|
+
"type": "configuration",
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
"requests": [
|
|
73
|
+
[
|
|
74
|
+
{},
|
|
75
|
+
],
|
|
76
|
+
],
|
|
77
|
+
}
|
|
78
|
+
`);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { DetectedDataSource } from "@/core/datasets/data-source-discovery";
|
|
4
|
+
import { DiscoverDataSources } from "@/core/datasets/request-registry";
|
|
5
|
+
import { useAsyncData } from "./useAsyncData";
|
|
6
|
+
|
|
7
|
+
export async function loadDataSourceDiscovery(): Promise<DetectedDataSource[]> {
|
|
8
|
+
const result = await DiscoverDataSources.request({});
|
|
9
|
+
return result.sources;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reusable UI-facing hook for kernel-managed datasource discovery.
|
|
14
|
+
* Consumers decide how to render, filter, or act on the detected model.
|
|
15
|
+
*/
|
|
16
|
+
export function useDataSourceDiscovery() {
|
|
17
|
+
return useAsyncData(loadDataSourceDiscovery, []);
|
|
18
|
+
}
|