@marimo-team/islands 0.24.1-dev3 → 0.24.1-dev30
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-BUC1MrsF.js → ConnectedDataExplorerComponent-yEsovh7C.js} +4 -4
- package/dist/arrays-Ct6XviUd.js +59 -0
- package/dist/{capabilities-BilXeZ7p.js → capabilities-DNKeBoT5.js} +1 -1
- package/dist/{chat-ui-ChBRy1iT.js → chat-ui-ChbH2I73.js} +3 -3
- package/dist/{common-BvTQu-LJ.js → common-Cy9zypnG.js} +17 -11
- package/dist/{formats-Dz0QNUEa.js → formats-BZtMpeQe.js} +1 -1
- package/dist/{glide-data-editor-BI2i_IEE.js → glide-data-editor-aimC_hok.js} +3 -1
- package/dist/{html-to-image-BtNr4zrO.js → html-to-image-BzT-fb14.js} +2357 -2325
- package/dist/{input-BRE2a7Ay.js → input-CqV2ZPgL.js} +1 -1
- package/dist/{loader-DTnbNSVZ.js → loader-Chm6nOqy.js} +1 -1
- package/dist/main.js +1108 -1014
- package/dist/{once-Boutk_9X.js → once-D1_qYl2e.js} +1 -1
- package/dist/{process-output-Dch8KCi8.js → process-output-C8Q8G6-g.js} +1 -1
- package/dist/{react-vega-zyfTF9SY.js → react-vega-BE2Uv1cq.js} +1 -1
- package/dist/{reveal-component-qNkzuPp3.js → reveal-component-2JQ2YfIT.js} +5 -5
- package/dist/{spec-BL1GSUUF.js → spec-DR1CyTOo.js} +2 -0
- package/dist/style.css +1 -1
- package/dist/{swiper-component-Cx0cRWLk.js → swiper-component-UYicacis.js} +1 -1
- package/dist/{useIframeCapabilities-CxuXXwjj.js → useIframeCapabilities-BbnoLKxG.js} +1 -1
- package/dist/{vega-component-CioKuDut.js → vega-component-xrHju6oo.js} +5 -5
- package/dist/{vega-embed-container-BRjjqRGz.js → vega-embed-container-B1tc-DoN.js} +2 -2
- package/dist/{vega-loader.browser-CGb-2Kwi.js → vega-loader.browser-VFQfHaoV.js} +1 -1
- package/package.json +1 -1
- package/src/components/app-config/optional-features.test.ts +78 -0
- package/src/components/app-config/optional-features.tsx +33 -6
- package/src/components/data-table/__tests__/column_formatting.test.ts +7 -0
- package/src/components/data-table/charts/__tests__/schemas.test.ts +17 -2
- package/src/components/data-table/charts/chart-spec/types.ts +2 -0
- package/src/components/data-table/column-formatting/feature.ts +1 -0
- package/src/components/data-table/column-formatting/types.ts +1 -0
- package/src/components/data-table/column-summary/chart-spec-model.tsx +1 -0
- package/src/components/data-table/column-summary/column-summary.tsx +1 -0
- package/src/components/data-table/columns.tsx +4 -3
- package/src/components/datasets/__tests__/icons.test.ts +10 -2
- package/src/components/datasets/icons.tsx +4 -0
- package/src/components/datasources/__tests__/components.test.tsx +14 -1
- package/src/components/datasources/datasources.tsx +2 -40
- package/src/components/editor/chrome/panels/components.tsx +35 -0
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +10 -0
- package/src/components/editor/chrome/panels/session-panel.tsx +12 -4
- package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +2 -0
- package/src/components/editor/connections/add-connection-dialog.tsx +5 -1
- package/src/components/editor/connections/quick-add-data-sources.tsx +11 -7
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +43 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +127 -0
- package/src/components/editor/connections/storage/add-storage-form.tsx +11 -1
- package/src/components/editor/connections/storage/as-code.ts +30 -1
- package/src/components/editor/connections/storage/schemas.ts +63 -0
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +13 -0
- package/src/core/codemirror/code-lens/popover.tsx +11 -5
- package/src/core/codemirror/language/languages/sql/__tests__/renderers.test.tsx +15 -4
- package/src/core/codemirror/language/languages/sql/renderers.tsx +1 -0
- package/src/core/datasets/__tests__/data-source-discovery.test.ts +277 -0
- package/src/core/datasets/data-source-connections.ts +36 -0
- package/src/core/datasets/data-source-discovery.ts +92 -0
- package/src/core/kernel/messages.ts +1 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +11 -3
- package/src/hooks/useDataSourceDiscovery.ts +50 -9
- package/src/plugins/impl/MatrixPlugin.tsx +256 -35
- package/src/plugins/impl/__tests__/DataEditorPlugin.test.ts +13 -1
- package/src/plugins/impl/__tests__/MatrixPlugin.test.tsx +540 -3
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +1 -0
- package/src/plugins/impl/data-editor/glide-utils.ts +1 -0
- package/src/plugins/impl/data-frames/__tests__/schema.test.ts +9 -1
- package/src/plugins/impl/data-frames/utils/__tests__/operators.test.ts +8 -0
- package/src/plugins/impl/data-frames/utils/operators.ts +1 -1
- package/src/plugins/impl/vega/__tests__/vega.test.ts +22 -4
- package/src/plugins/impl/vega/utils.ts +3 -0
- package/dist/arrays-C56grfjL.js +0 -55
- package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +0 -80
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
4
|
+
import { store } from "@/core/state/jotai";
|
|
5
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
6
|
+
import {
|
|
7
|
+
fetchDataSourceDiscovery,
|
|
8
|
+
invalidateDataSourceDiscovery,
|
|
9
|
+
isDetectedSourceConnected,
|
|
10
|
+
matchesDiscoveryGroup,
|
|
11
|
+
type DetectedDataSource,
|
|
12
|
+
} from "../data-source-discovery";
|
|
13
|
+
import { DiscoverDataSources } from "../request-registry";
|
|
14
|
+
|
|
15
|
+
function source(
|
|
16
|
+
overrides: Partial<DetectedDataSource> &
|
|
17
|
+
Pick<DetectedDataSource, "id" | "integration" | "category" | "displayName">,
|
|
18
|
+
): DetectedDataSource {
|
|
19
|
+
return {
|
|
20
|
+
confidence: "high",
|
|
21
|
+
origins: [],
|
|
22
|
+
configuration: [],
|
|
23
|
+
code: "",
|
|
24
|
+
hidesWhen: { kind: "dialect", substrings: [overrides.integration] },
|
|
25
|
+
...overrides,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("fetchDataSourceDiscovery", () => {
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
// Discovery waits for the session's websocket connection to be open
|
|
32
|
+
// before requesting, so it doesn't race a still-connecting session.
|
|
33
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(() => {
|
|
37
|
+
vi.restoreAllMocks();
|
|
38
|
+
invalidateDataSourceDiscovery();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("shares a single in-flight request across concurrent callers", async () => {
|
|
42
|
+
const request = vi
|
|
43
|
+
.spyOn(DiscoverDataSources, "request")
|
|
44
|
+
.mockResolvedValue({ request_id: "request-id", sources: [] });
|
|
45
|
+
|
|
46
|
+
await Promise.all([
|
|
47
|
+
fetchDataSourceDiscovery(),
|
|
48
|
+
fetchDataSourceDiscovery(),
|
|
49
|
+
fetchDataSourceDiscovery(),
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("re-runs discovery after the cache is invalidated", async () => {
|
|
56
|
+
const request = vi
|
|
57
|
+
.spyOn(DiscoverDataSources, "request")
|
|
58
|
+
.mockResolvedValue({ request_id: "request-id", sources: [] });
|
|
59
|
+
|
|
60
|
+
await fetchDataSourceDiscovery();
|
|
61
|
+
invalidateDataSourceDiscovery();
|
|
62
|
+
await fetchDataSourceDiscovery();
|
|
63
|
+
|
|
64
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("clears the cache on failure so the next call retries", async () => {
|
|
68
|
+
const request = vi
|
|
69
|
+
.spyOn(DiscoverDataSources, "request")
|
|
70
|
+
.mockRejectedValueOnce(new Error("boom"))
|
|
71
|
+
.mockResolvedValueOnce({ request_id: "request-id", sources: [] });
|
|
72
|
+
|
|
73
|
+
await expect(fetchDataSourceDiscovery()).rejects.toThrow("boom");
|
|
74
|
+
await expect(fetchDataSourceDiscovery()).resolves.toEqual([]);
|
|
75
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("does not clear a newer cache entry when an invalidated request fails", async () => {
|
|
79
|
+
let rejectFirstRequest: ((error: Error) => void) | undefined;
|
|
80
|
+
const firstRequest = new Promise<{
|
|
81
|
+
request_id: string;
|
|
82
|
+
sources: DetectedDataSource[];
|
|
83
|
+
}>((_resolve, reject) => {
|
|
84
|
+
rejectFirstRequest = reject;
|
|
85
|
+
});
|
|
86
|
+
const request = vi
|
|
87
|
+
.spyOn(DiscoverDataSources, "request")
|
|
88
|
+
.mockReturnValueOnce(firstRequest)
|
|
89
|
+
.mockResolvedValueOnce({ request_id: "request-id", sources: [] });
|
|
90
|
+
|
|
91
|
+
const staleResult = fetchDataSourceDiscovery().catch((error) => error);
|
|
92
|
+
await vi.waitFor(() => expect(request).toHaveBeenCalledTimes(1));
|
|
93
|
+
invalidateDataSourceDiscovery();
|
|
94
|
+
await fetchDataSourceDiscovery();
|
|
95
|
+
|
|
96
|
+
rejectFirstRequest?.(new Error("stale request failed"));
|
|
97
|
+
await staleResult;
|
|
98
|
+
await fetchDataSourceDiscovery();
|
|
99
|
+
|
|
100
|
+
expect(request).toHaveBeenCalledTimes(2);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("returns sources from the kernel", async () => {
|
|
104
|
+
const sources: DetectedDataSource[] = [
|
|
105
|
+
source({
|
|
106
|
+
id: "postgres",
|
|
107
|
+
integration: "postgres",
|
|
108
|
+
category: "database",
|
|
109
|
+
displayName: "PostgreSQL",
|
|
110
|
+
}),
|
|
111
|
+
source({
|
|
112
|
+
id: "mysql",
|
|
113
|
+
integration: "mysql",
|
|
114
|
+
category: "database",
|
|
115
|
+
displayName: "MySQL",
|
|
116
|
+
}),
|
|
117
|
+
];
|
|
118
|
+
vi.spyOn(DiscoverDataSources, "request").mockResolvedValue({
|
|
119
|
+
request_id: "request-id",
|
|
120
|
+
sources,
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
await expect(fetchDataSourceDiscovery()).resolves.toEqual(sources);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe("matchesDiscoveryGroup", () => {
|
|
128
|
+
const postgres = source({
|
|
129
|
+
id: "postgres",
|
|
130
|
+
integration: "postgres",
|
|
131
|
+
category: "database",
|
|
132
|
+
displayName: "PostgreSQL",
|
|
133
|
+
});
|
|
134
|
+
const iceberg = source({
|
|
135
|
+
id: "iceberg",
|
|
136
|
+
integration: "pyiceberg",
|
|
137
|
+
category: "catalog",
|
|
138
|
+
displayName: "PyIceberg",
|
|
139
|
+
});
|
|
140
|
+
const s3 = source({
|
|
141
|
+
id: "s3",
|
|
142
|
+
integration: "aws",
|
|
143
|
+
category: "object-storage",
|
|
144
|
+
displayName: "S3",
|
|
145
|
+
hidesWhen: {
|
|
146
|
+
kind: "storage",
|
|
147
|
+
protocols: ["s3"],
|
|
148
|
+
backendTypes: [],
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("keeps every source when no group is given", () => {
|
|
153
|
+
expect(matchesDiscoveryGroup(postgres)).toBe(true);
|
|
154
|
+
expect(matchesDiscoveryGroup(iceberg)).toBe(true);
|
|
155
|
+
expect(matchesDiscoveryGroup(s3)).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("groups catalogs with databases", () => {
|
|
159
|
+
expect(matchesDiscoveryGroup(postgres, "database")).toBe(true);
|
|
160
|
+
expect(matchesDiscoveryGroup(iceberg, "database")).toBe(true);
|
|
161
|
+
expect(matchesDiscoveryGroup(s3, "database")).toBe(false);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("narrows storage to object-storage sources", () => {
|
|
165
|
+
expect(matchesDiscoveryGroup(postgres, "storage")).toBe(false);
|
|
166
|
+
expect(matchesDiscoveryGroup(iceberg, "storage")).toBe(false);
|
|
167
|
+
expect(matchesDiscoveryGroup(s3, "storage")).toBe(true);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("isDetectedSourceConnected", () => {
|
|
172
|
+
const postgres = source({
|
|
173
|
+
id: "postgres",
|
|
174
|
+
integration: "postgres",
|
|
175
|
+
category: "database",
|
|
176
|
+
displayName: "PostgreSQL",
|
|
177
|
+
hidesWhen: { kind: "dialect", substrings: ["postgres"] },
|
|
178
|
+
});
|
|
179
|
+
const emptySnapshot = {
|
|
180
|
+
dialects: [] as string[],
|
|
181
|
+
storageProtocols: [] as string[],
|
|
182
|
+
storageBackendTypes: [] as string[],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
it("does not hide a source when nothing is connected", () => {
|
|
186
|
+
expect(isDetectedSourceConnected(postgres, emptySnapshot)).toBe(false);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it("hides a database suggestion when a matching dialect is live", () => {
|
|
190
|
+
expect(
|
|
191
|
+
isDetectedSourceConnected(postgres, {
|
|
192
|
+
...emptySnapshot,
|
|
193
|
+
dialects: ["postgresql"],
|
|
194
|
+
}),
|
|
195
|
+
).toBe(true);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does not hide a database suggestion for a different dialect", () => {
|
|
199
|
+
expect(
|
|
200
|
+
isDetectedSourceConnected(postgres, {
|
|
201
|
+
...emptySnapshot,
|
|
202
|
+
dialects: ["mysql", "duckdb"],
|
|
203
|
+
}),
|
|
204
|
+
).toBe(false);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("hides spark suggestions for either pyspark or spark dialects", () => {
|
|
208
|
+
const spark = source({
|
|
209
|
+
id: "spark",
|
|
210
|
+
integration: "pyspark",
|
|
211
|
+
category: "catalog",
|
|
212
|
+
displayName: "Spark",
|
|
213
|
+
hidesWhen: { kind: "dialect", substrings: ["pyspark", "spark"] },
|
|
214
|
+
});
|
|
215
|
+
expect(
|
|
216
|
+
isDetectedSourceConnected(spark, {
|
|
217
|
+
...emptySnapshot,
|
|
218
|
+
dialects: ["spark"],
|
|
219
|
+
}),
|
|
220
|
+
).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("hides object storage when a matching protocol is live", () => {
|
|
224
|
+
const s3 = source({
|
|
225
|
+
id: "s3",
|
|
226
|
+
integration: "aws",
|
|
227
|
+
category: "object-storage",
|
|
228
|
+
displayName: "S3",
|
|
229
|
+
hidesWhen: {
|
|
230
|
+
kind: "storage",
|
|
231
|
+
protocols: ["s3", "cloudflare", "coreweave"],
|
|
232
|
+
backendTypes: [],
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
expect(
|
|
236
|
+
isDetectedSourceConnected(s3, {
|
|
237
|
+
...emptySnapshot,
|
|
238
|
+
storageProtocols: ["s3"],
|
|
239
|
+
}),
|
|
240
|
+
).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("hides huggingface when the backend type matches", () => {
|
|
244
|
+
const hf = source({
|
|
245
|
+
id: "hf",
|
|
246
|
+
integration: "huggingface",
|
|
247
|
+
category: "object-storage",
|
|
248
|
+
displayName: "Hugging Face",
|
|
249
|
+
hidesWhen: {
|
|
250
|
+
kind: "storage",
|
|
251
|
+
protocols: ["hf"],
|
|
252
|
+
backendTypes: ["huggingface"],
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
expect(
|
|
256
|
+
isDetectedSourceConnected(hf, {
|
|
257
|
+
...emptySnapshot,
|
|
258
|
+
storageBackendTypes: ["huggingface"],
|
|
259
|
+
}),
|
|
260
|
+
).toBe(true);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("does not hide when hidesWhen does not match live connections", () => {
|
|
264
|
+
expect(
|
|
265
|
+
isDetectedSourceConnected(
|
|
266
|
+
source({
|
|
267
|
+
id: "custom",
|
|
268
|
+
integration: "custom",
|
|
269
|
+
category: "database",
|
|
270
|
+
displayName: "Custom",
|
|
271
|
+
hidesWhen: { kind: "dialect", substrings: ["custom"] },
|
|
272
|
+
}),
|
|
273
|
+
{ ...emptySnapshot, dialects: ["postgres"] },
|
|
274
|
+
),
|
|
275
|
+
).toBe(false);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
@@ -4,6 +4,7 @@ import { atom } from "jotai";
|
|
|
4
4
|
import { isSchemaless } from "@/components/datasources/utils";
|
|
5
5
|
import { createReducerAndAtoms } from "@/utils/createReducer";
|
|
6
6
|
import { Logger } from "@/utils/Logger";
|
|
7
|
+
import { sortBy } from "@/utils/arrays";
|
|
7
8
|
import type {
|
|
8
9
|
DatabaseSchema,
|
|
9
10
|
DataSourceConnection as DataSourceConnectionType,
|
|
@@ -13,6 +14,7 @@ import { store } from "../state/jotai";
|
|
|
13
14
|
import type { VariableName } from "../variables/types";
|
|
14
15
|
import {
|
|
15
16
|
type ConnectionName,
|
|
17
|
+
DEFAULT_DUCKDB_DATABASE,
|
|
16
18
|
DUCKDB_ENGINE,
|
|
17
19
|
INTERNAL_SQL_ENGINES,
|
|
18
20
|
} from "./engines";
|
|
@@ -341,6 +343,40 @@ export const dataConnectionsMapAtom = atom(
|
|
|
341
343
|
(get) => get(dataSourceConnectionsAtom).connectionsMap,
|
|
342
344
|
);
|
|
343
345
|
|
|
346
|
+
/**
|
|
347
|
+
* Get the data connections that are available to the user.
|
|
348
|
+
* Filters out internal engines if it has no databases or if it has only the in-memory database and no schemas.
|
|
349
|
+
*/
|
|
350
|
+
export const connectionsAtom = atom((get) => {
|
|
351
|
+
const dataConnections = new Map(get(dataConnectionsMapAtom));
|
|
352
|
+
|
|
353
|
+
// Filter out the internal engines if it has no databases
|
|
354
|
+
// Or if it has only the in-memory database and no schemas
|
|
355
|
+
for (const engine of INTERNAL_SQL_ENGINES) {
|
|
356
|
+
const connection = dataConnections.get(engine);
|
|
357
|
+
if (!connection) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (connection.databases.length === 0) {
|
|
362
|
+
dataConnections.delete(engine);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (
|
|
366
|
+
connection.databases.length === 1 &&
|
|
367
|
+
connection.databases[0].name === DEFAULT_DUCKDB_DATABASE &&
|
|
368
|
+
connection.databases[0].schemas.length === 0
|
|
369
|
+
) {
|
|
370
|
+
dataConnections.delete(engine);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Put internal engines last to prioritize user-defined connections
|
|
375
|
+
return sortBy([...dataConnections.values()], (connection) =>
|
|
376
|
+
INTERNAL_SQL_ENGINES.has(connection.name) ? 1 : 0,
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
|
|
344
380
|
export function setLatestEngineSelected(engine: ConnectionName) {
|
|
345
381
|
const existing = store.get(dataSourceConnectionsAtom);
|
|
346
382
|
// Don't update the map if the engine is not in the map
|
|
@@ -1,5 +1,97 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
+
import { atom } from "jotai";
|
|
3
4
|
import type { components } from "@marimo-team/marimo-api";
|
|
5
|
+
import { waitForConnectionOpenIfNotebook } from "../network/connection";
|
|
6
|
+
import { store } from "../state/jotai";
|
|
7
|
+
import { DiscoverDataSources } from "./request-registry";
|
|
4
8
|
|
|
5
9
|
export type DetectedDataSource = components["schemas"]["DetectedDataSource"];
|
|
10
|
+
|
|
11
|
+
export type DataSourceDiscoveryGroup = "database" | "storage";
|
|
12
|
+
|
|
13
|
+
/** Category filter for UI surfaces. Catalogs ride with databases. */
|
|
14
|
+
export function matchesDiscoveryGroup(
|
|
15
|
+
source: DetectedDataSource,
|
|
16
|
+
group?: DataSourceDiscoveryGroup,
|
|
17
|
+
): boolean {
|
|
18
|
+
if (!group) {
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
const isStorage = source.category === "object-storage";
|
|
22
|
+
return isStorage === (group === "storage");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Live engines and storage used to hide suggestions of the same type.
|
|
27
|
+
*/
|
|
28
|
+
export interface LiveConnectionSnapshot {
|
|
29
|
+
dialects: readonly string[];
|
|
30
|
+
storageProtocols: readonly string[];
|
|
31
|
+
storageBackendTypes: readonly string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a detected source already has a live connection of the same type.
|
|
36
|
+
* Match rules come from the kernel on each suggestion.
|
|
37
|
+
*/
|
|
38
|
+
export function isDetectedSourceConnected(
|
|
39
|
+
source: DetectedDataSource,
|
|
40
|
+
snapshot: LiveConnectionSnapshot,
|
|
41
|
+
): boolean {
|
|
42
|
+
const match = source.hidesWhen;
|
|
43
|
+
if (match.kind === "dialect") {
|
|
44
|
+
return match.substrings.some((alias) =>
|
|
45
|
+
snapshot.dialects.some((dialect) => dialect.includes(alias)),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (
|
|
50
|
+
match.protocols.some((protocol) =>
|
|
51
|
+
snapshot.storageProtocols.includes(protocol),
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return match.backendTypes.some((backendType) =>
|
|
58
|
+
snapshot.storageBackendTypes.includes(backendType),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Bumped when discovery should be re-fetched (kernel restart).
|
|
64
|
+
*/
|
|
65
|
+
export const dataSourceDiscoveryEpochAtom = atom(0);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Kernel-managed datasource discovery scans the environment once per epoch.
|
|
69
|
+
* Every consumer shares a single in-flight request.
|
|
70
|
+
*/
|
|
71
|
+
let discoveryPromise: Promise<DetectedDataSource[]> | undefined;
|
|
72
|
+
let discoveryEpoch = 0;
|
|
73
|
+
|
|
74
|
+
export function fetchDataSourceDiscovery(): Promise<DetectedDataSource[]> {
|
|
75
|
+
const epoch = store.get(dataSourceDiscoveryEpochAtom);
|
|
76
|
+
if (!discoveryPromise || discoveryEpoch !== epoch) {
|
|
77
|
+
discoveryEpoch = epoch;
|
|
78
|
+
discoveryPromise = waitForConnectionOpenIfNotebook()
|
|
79
|
+
.then(() => DiscoverDataSources.request({}))
|
|
80
|
+
.then((result) => result.sources)
|
|
81
|
+
.catch((error: unknown) => {
|
|
82
|
+
if (discoveryEpoch === epoch) {
|
|
83
|
+
discoveryPromise = undefined;
|
|
84
|
+
}
|
|
85
|
+
throw error;
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
return discoveryPromise;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Clears the shared discovery cache and triggers consumers to refetch.
|
|
93
|
+
*/
|
|
94
|
+
export function invalidateDataSourceDiscovery(): void {
|
|
95
|
+
discoveryPromise = undefined;
|
|
96
|
+
store.set(dataSourceDiscoveryEpochAtom, (value) => value + 1);
|
|
97
|
+
}
|
|
@@ -40,6 +40,7 @@ import type { CellData } from "../cells/types";
|
|
|
40
40
|
import { capabilitiesAtom } from "../config/capabilities";
|
|
41
41
|
import { connectionTransportTypeAtom, useSetAppConfig } from "../config/config";
|
|
42
42
|
import { useDataSourceActions } from "../datasets/data-source-connections";
|
|
43
|
+
import { invalidateDataSourceDiscovery } from "../datasets/data-source-discovery";
|
|
43
44
|
import type { ConnectionName } from "../datasets/engines";
|
|
44
45
|
import {
|
|
45
46
|
DiscoverDataSources,
|
|
@@ -251,6 +252,12 @@ export function useMarimoKernelConnection(opts: {
|
|
|
251
252
|
onError: showBoundary,
|
|
252
253
|
existingCells,
|
|
253
254
|
});
|
|
255
|
+
if (!msg.data.resumed) {
|
|
256
|
+
// A freshly started kernel may expose a different environment
|
|
257
|
+
// (e.g. new env vars); re-run discovery instead of reusing stale
|
|
258
|
+
// suggestions from the previous kernel instance.
|
|
259
|
+
invalidateDataSourceDiscovery();
|
|
260
|
+
}
|
|
254
261
|
setKioskMode(msg.data.kiosk);
|
|
255
262
|
// A freshly started kernel has no breakpoints of its own; re-push
|
|
256
263
|
// the client's set so they still apply, and clear the stale
|
|
@@ -311,6 +318,7 @@ export function useMarimoKernelConnection(opts: {
|
|
|
311
318
|
}
|
|
312
319
|
|
|
313
320
|
case "variables":
|
|
321
|
+
const variableNames = msg.data.variables.map((v) => v.name);
|
|
314
322
|
setVariables(
|
|
315
323
|
msg.data.variables.map((v) => ({
|
|
316
324
|
name: v.name,
|
|
@@ -318,9 +326,9 @@ export function useMarimoKernelConnection(opts: {
|
|
|
318
326
|
usedBy: v.used_by,
|
|
319
327
|
})),
|
|
320
328
|
);
|
|
321
|
-
filterDatasetsFromVariables(
|
|
322
|
-
filterDataSourcesFromVariables(
|
|
323
|
-
filterStorageFromVariables(
|
|
329
|
+
filterDatasetsFromVariables(variableNames);
|
|
330
|
+
filterDataSourcesFromVariables(variableNames);
|
|
331
|
+
filterStorageFromVariables(variableNames);
|
|
324
332
|
return;
|
|
325
333
|
case "variable-values":
|
|
326
334
|
setMetadata(
|
|
@@ -1,18 +1,59 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
3
|
+
import { useAtomValue } from "jotai";
|
|
4
|
+
import { useMemo } from "react";
|
|
5
|
+
import { dataConnectionsMapAtom } from "@/core/datasets/data-source-connections";
|
|
6
|
+
import {
|
|
7
|
+
dataSourceDiscoveryEpochAtom,
|
|
8
|
+
type DataSourceDiscoveryGroup,
|
|
9
|
+
type DetectedDataSource,
|
|
10
|
+
fetchDataSourceDiscovery,
|
|
11
|
+
isDetectedSourceConnected,
|
|
12
|
+
matchesDiscoveryGroup,
|
|
13
|
+
} from "@/core/datasets/data-source-discovery";
|
|
14
|
+
import { storageNamespacesAtom } from "@/core/storage/state";
|
|
5
15
|
import { useAsyncData } from "./useAsyncData";
|
|
6
16
|
|
|
7
|
-
export async function loadDataSourceDiscovery(): Promise<DetectedDataSource[]> {
|
|
8
|
-
const result = await DiscoverDataSources.request({});
|
|
9
|
-
return result.sources;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
17
|
/**
|
|
13
18
|
* Reusable UI-facing hook for kernel-managed datasource discovery.
|
|
14
|
-
* Consumers decide how to render, filter, or act on the detected model.
|
|
15
19
|
*/
|
|
16
20
|
export function useDataSourceDiscovery() {
|
|
17
|
-
|
|
21
|
+
const epoch = useAtomValue(dataSourceDiscoveryEpochAtom);
|
|
22
|
+
return useAsyncData(fetchDataSourceDiscovery, [epoch]);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Detected sources from the kernel environment that are not already backed
|
|
27
|
+
* by a live connection, optionally narrowed to "database" or "storage".
|
|
28
|
+
*/
|
|
29
|
+
export function useDetectedDataSources(
|
|
30
|
+
group?: DataSourceDiscoveryGroup,
|
|
31
|
+
): DetectedDataSource[] {
|
|
32
|
+
const { data } = useDataSourceDiscovery();
|
|
33
|
+
const connections = useAtomValue(dataConnectionsMapAtom);
|
|
34
|
+
const storageNamespaces = useAtomValue(storageNamespacesAtom);
|
|
35
|
+
|
|
36
|
+
return useMemo(() => {
|
|
37
|
+
if (!data) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const snapshot = {
|
|
42
|
+
dialects: [...connections.values()].map((connection) =>
|
|
43
|
+
connection.dialect.toLowerCase(),
|
|
44
|
+
),
|
|
45
|
+
storageProtocols: storageNamespaces.map((namespace) =>
|
|
46
|
+
namespace.protocol.toLowerCase(),
|
|
47
|
+
),
|
|
48
|
+
storageBackendTypes: storageNamespaces.map((namespace) =>
|
|
49
|
+
namespace.backendType.toLowerCase(),
|
|
50
|
+
),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
return data.filter(
|
|
54
|
+
(source) =>
|
|
55
|
+
matchesDiscoveryGroup(source, group) &&
|
|
56
|
+
!isDetectedSourceConnected(source, snapshot),
|
|
57
|
+
);
|
|
58
|
+
}, [connections, data, group, storageNamespaces]);
|
|
18
59
|
}
|