@marimo-team/islands 0.23.16-dev6 → 0.23.16-dev62
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-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
- package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
- package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
- package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
- package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
- package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DsZJj75A.js} +3117 -3120
- package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
- package/dist/{code-visibility-CfBftiVM.js → common-Bpnmu1Yz.js} +1085 -1053
- package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
- package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
- package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
- package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
- package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
- package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
- package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
- package/dist/{html-to-image-DDENewl2.js → html-to-image-CZ1kLKkq.js} +7350 -6356
- package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
- package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
- package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
- package/dist/main.js +259 -194
- package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
- package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
- package/dist/{process-output-2QPcgBQg.js → process-output-bVfbcx5_.js} +32 -28
- package/dist/{reveal-component-BclLkYnV.js → reveal-component-JIt3oSeU.js} +642 -634
- package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
- package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
- package/dist/toDate-CqhBWjvO.js +713 -0
- package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
- package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
- package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
- package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
- package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
- package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
- package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
- package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
- package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
- package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
- package/package.json +3 -4
- package/src/__mocks__/requests.ts +56 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/__tests__/columns.test.tsx +9 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/columns.tsx +11 -1
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +11 -14
- package/src/components/editor/actions/useNotebookActions.tsx +28 -19
- package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
- package/src/components/editor/cell/code/cell-editor.tsx +13 -7
- package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
- package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
- package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
- package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
- package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
- package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
- package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
- package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
- package/src/components/editor/chrome/state.ts +10 -0
- package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
- package/src/components/editor/connections/__tests__/paths.test.ts +39 -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/components.tsx +6 -2
- package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
- package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
- package/src/components/editor/connections/database/as-code.ts +37 -13
- package/src/components/editor/connections/database/schemas.ts +6 -5
- package/src/components/editor/connections/form-renderers.tsx +99 -1
- package/src/components/editor/connections/json-credentials.ts +57 -0
- package/src/components/editor/connections/paths.ts +21 -0
- package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
- package/src/components/editor/connections/secret-combobox.tsx +41 -20
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
- package/src/components/editor/connections/storage/as-code.ts +79 -36
- package/src/components/editor/connections/storage/schemas.ts +85 -44
- package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
- package/src/components/editor/file-tree/file-viewer.tsx +11 -7
- package/src/components/editor/file-tree/renderers.tsx +21 -0
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
- package/src/components/editor/viewer-banner.tsx +6 -1
- package/src/components/forms/form.tsx +59 -24
- package/src/components/forms/options.ts +2 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/reveal-component.tsx +22 -17
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/ai/context/providers/file.ts +8 -29
- package/src/core/cells/__tests__/cells.test.ts +12 -2
- package/src/core/cells/__tests__/logs.test.ts +5 -5
- package/src/core/cells/cells.ts +17 -4
- package/src/core/cells/document-changes.ts +1 -0
- package/src/core/cells/logs.ts +14 -4
- package/src/core/codemirror/cm.ts +3 -0
- package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
- package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
- package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
- package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
- package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
- package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
- package/src/core/codemirror/code-lens/actions.ts +33 -0
- package/src/core/codemirror/code-lens/analyzer.ts +284 -0
- package/src/core/codemirror/code-lens/entities.ts +123 -0
- package/src/core/codemirror/code-lens/extension.ts +298 -0
- package/src/core/codemirror/code-lens/icons.ts +36 -0
- package/src/core/codemirror/code-lens/popover.tsx +240 -0
- package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
- package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
- package/src/core/codemirror/copilot/language-server.ts +182 -70
- package/src/core/codemirror/copilot/transport.ts +15 -207
- package/src/core/codemirror/copilot/types.ts +9 -2
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +93 -90
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
- package/src/core/codemirror/language/languages/python.ts +23 -11
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
- package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
- package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
- package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
- package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
- package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
- package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
- package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
- package/src/core/codemirror/lsp/log-messages.ts +37 -0
- package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
- package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
- package/src/core/codemirror/lsp/transports.ts +8 -1
- package/src/core/codemirror/lsp/types.ts +68 -8
- package/src/core/codemirror/lsp/utils.ts +14 -0
- package/src/core/codemirror/python-node-names.ts +90 -0
- package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/config/feature-flag.tsx +3 -1
- package/src/core/datasets/data-source-discovery.ts +5 -0
- package/src/core/datasets/request-registry.ts +11 -0
- package/src/core/edit-app.tsx +1 -2
- package/src/core/export/__tests__/hooks.test.ts +2 -4
- package/src/core/export/hooks.ts +4 -3
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +18 -0
- package/src/core/kernel/messages.ts +2 -0
- package/src/core/lsp/__tests__/transport.test.ts +509 -337
- package/src/core/lsp/transport.ts +304 -132
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
- package/src/core/network/__tests__/requests-network.test.ts +48 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +3 -0
- package/src/core/network/requests-network.ts +31 -8
- package/src/core/network/requests-static.ts +3 -0
- package/src/core/network/requests-toasting.tsx +3 -0
- package/src/core/network/types.ts +27 -6
- package/src/core/rtc/state.ts +9 -0
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/__tests__/bridge.test.ts +99 -5
- package/src/core/wasm/bridge.ts +42 -9
- package/src/core/wasm/worker/types.ts +10 -3
- package/src/core/wasm/worker/worker.ts +7 -8
- package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
- package/src/css/app/codemirror.css +16 -2
- package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
- package/src/hooks/useDataSourceDiscovery.ts +18 -0
- package/src/plugins/impl/DataTablePlugin.tsx +4 -1
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
- package/src/plugins/impl/anywidget/model.ts +20 -5
- package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
- package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
- package/src/stories/log-viewer.stories.tsx +12 -6
- package/src/utils/__tests__/blob.test.ts +27 -1
- package/src/utils/__tests__/download.test.tsx +44 -4
- package/src/utils/blob.ts +24 -1
- package/src/utils/download.ts +14 -15
- package/dist/toDate-B3IRfHhw.js +0 -692
|
@@ -196,6 +196,16 @@ export function useChromeActions() {
|
|
|
196
196
|
return useActions();
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* Open a panel imperatively from non-React code (e.g. CodeMirror extensions).
|
|
201
|
+
* No-ops if the panel is hidden (not in the layout).
|
|
202
|
+
*/
|
|
203
|
+
export function openPanel(panelType: PanelType): void {
|
|
204
|
+
store.set(chromeAtom, (prev) =>
|
|
205
|
+
reducer(prev, { type: "openApplication", payload: panelType }),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
199
209
|
export { chromeAtom };
|
|
200
210
|
|
|
201
211
|
/**
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
escapePythonString,
|
|
5
|
+
flattenSecretValue,
|
|
6
|
+
looksLikeJson,
|
|
7
|
+
resolveJsonCredential,
|
|
8
|
+
} from "../json-credentials";
|
|
9
|
+
import { prefixPath } from "../paths";
|
|
10
|
+
import { prefixSecret } from "../secrets";
|
|
11
|
+
|
|
12
|
+
describe("resolveJsonCredential", () => {
|
|
13
|
+
test("resolves a file path", () => {
|
|
14
|
+
expect(
|
|
15
|
+
resolveJsonCredential(prefixPath("/etc/secrets/key.json"), (v) => v),
|
|
16
|
+
).toEqual({ kind: "path", path: "/etc/secrets/key.json" });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("resolves a secret via printSecret", () => {
|
|
20
|
+
expect(
|
|
21
|
+
resolveJsonCredential(prefixSecret("MY_CREDS"), () => "_my_creds"),
|
|
22
|
+
).toEqual({ kind: "json", expr: "json.loads(_my_creds)" });
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("resolves a raw JSON literal", () => {
|
|
26
|
+
expect(
|
|
27
|
+
resolveJsonCredential('{"type":"service_account"}', (v) => v),
|
|
28
|
+
).toEqual({
|
|
29
|
+
kind: "json",
|
|
30
|
+
expr: 'json.loads(r"""{"type":"service_account"}""")',
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("uses a raw string so escaped newlines in a private_key survive", () => {
|
|
35
|
+
const credentialsJson =
|
|
36
|
+
'{"private_key":"-----BEGIN PRIVATE KEY-----\\nabc123\\n-----END PRIVATE KEY-----\\n"}';
|
|
37
|
+
const credential = resolveJsonCredential(credentialsJson, (v) => v);
|
|
38
|
+
expect(credential).toEqual({
|
|
39
|
+
kind: "json",
|
|
40
|
+
expr: `json.loads(r"""${credentialsJson}""")`,
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("prefers path over secret-looking values", () => {
|
|
45
|
+
// A path-prefixed value wins even if the path text starts with "env:".
|
|
46
|
+
expect(
|
|
47
|
+
resolveJsonCredential(prefixPath("env:not-a-secret"), (v) => v),
|
|
48
|
+
).toEqual({ kind: "path", path: "env:not-a-secret" });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("looksLikeJson", () => {
|
|
53
|
+
test("detects objects and arrays, including partial paste", () => {
|
|
54
|
+
expect(looksLikeJson('{"type":"service_account"}')).toBe(true);
|
|
55
|
+
expect(looksLikeJson(" {")).toBe(true);
|
|
56
|
+
expect(looksLikeJson("[1, 2]")).toBe(true);
|
|
57
|
+
expect(looksLikeJson("/etc/secrets/key.json")).toBe(false);
|
|
58
|
+
expect(looksLikeJson("MY_SECRET")).toBe(false);
|
|
59
|
+
expect(looksLikeJson("")).toBe(false);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("drives the path vs. create-secret combobox actions exclusively", () => {
|
|
63
|
+
// These mirror how SECRET_TEXTAREA_RENDERER wires allowCustomValue /
|
|
64
|
+
// allowCreateSecret, so a path and a JSON paste never both offer (or
|
|
65
|
+
// both hide) the wrong action.
|
|
66
|
+
const allowCustomValue = (search: string) => !looksLikeJson(search);
|
|
67
|
+
const allowCreateSecret = (search: string) =>
|
|
68
|
+
!search || looksLikeJson(search);
|
|
69
|
+
|
|
70
|
+
expect(allowCustomValue("/etc/secrets/key.json")).toBe(true);
|
|
71
|
+
expect(allowCreateSecret("/etc/secrets/key.json")).toBe(false);
|
|
72
|
+
|
|
73
|
+
expect(allowCustomValue('{"type":"service_account"}')).toBe(false);
|
|
74
|
+
expect(allowCreateSecret('{"type":"service_account"}')).toBe(true);
|
|
75
|
+
|
|
76
|
+
expect(allowCreateSecret("")).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("flattenSecretValue", () => {
|
|
81
|
+
test("minifies valid JSON", () => {
|
|
82
|
+
expect(
|
|
83
|
+
flattenSecretValue(`{
|
|
84
|
+
"type": "service_account",
|
|
85
|
+
"project_id": "test"
|
|
86
|
+
}`),
|
|
87
|
+
).toBe('{"type":"service_account","project_id":"test"}');
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("strips newlines from non-JSON", () => {
|
|
91
|
+
expect(flattenSecretValue("line1\nline2\r\nline3")).toBe("line1line2line3");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("returns empty for blank input", () => {
|
|
95
|
+
expect(flattenSecretValue("")).toBe("");
|
|
96
|
+
expect(flattenSecretValue(" \n ")).toBe("");
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
describe("escapePythonString", () => {
|
|
101
|
+
test("escapes backslashes and quotes", () => {
|
|
102
|
+
expect(escapePythonString('C:\\tmp\\"creds".json')).toBe(
|
|
103
|
+
'C:\\\\tmp\\\\\\"creds\\".json',
|
|
104
|
+
);
|
|
105
|
+
expect(escapePythonString("/etc/secrets/key.json")).toBe(
|
|
106
|
+
"/etc/secrets/key.json",
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("escapes control characters", () => {
|
|
111
|
+
expect(escapePythonString("line1\nline2")).toBe("line1\\nline2");
|
|
112
|
+
expect(escapePythonString("a\tb\rc")).toBe("a\\tb\\rc");
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
isPath,
|
|
5
|
+
prefixPath,
|
|
6
|
+
type PathPlaceholder,
|
|
7
|
+
unprefixPath,
|
|
8
|
+
} from "../paths";
|
|
9
|
+
|
|
10
|
+
describe("paths", () => {
|
|
11
|
+
test("isPath", () => {
|
|
12
|
+
expect(isPath("path:/etc/secrets/key.json")).toBe(true);
|
|
13
|
+
expect(isPath("path:")).toBe(true);
|
|
14
|
+
expect(isPath("/etc/secrets/key.json")).toBe(false);
|
|
15
|
+
expect(isPath("env:MY_SECRET")).toBe(false);
|
|
16
|
+
expect(isPath("")).toBe(false);
|
|
17
|
+
expect(isPath(null)).toBe(false);
|
|
18
|
+
expect(isPath(undefined)).toBe(false);
|
|
19
|
+
expect(isPath(123)).toBe(false);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("prefixPath", () => {
|
|
23
|
+
expect(prefixPath("/etc/secrets/key.json")).toBe(
|
|
24
|
+
"path:/etc/secrets/key.json",
|
|
25
|
+
);
|
|
26
|
+
expect(prefixPath("")).toBe("path:");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("unprefixPath", () => {
|
|
30
|
+
expect(unprefixPath("path:/etc/secrets/key.json" as PathPlaceholder)).toBe(
|
|
31
|
+
"/etc/secrets/key.json",
|
|
32
|
+
);
|
|
33
|
+
expect(unprefixPath("path:" as PathPlaceholder)).toBe("");
|
|
34
|
+
// Only strips the leading prefix — path may itself contain "path:".
|
|
35
|
+
expect(unprefixPath("path:/tmp/path:weird.json" as PathPlaceholder)).toBe(
|
|
36
|
+
"/tmp/path:weird.json",
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
@@ -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)}
|
|
@@ -21,9 +21,13 @@ import { maybeAddMarimoImport } from "@/core/cells/add-missing-import";
|
|
|
21
21
|
import { useCellActions } from "@/core/cells/cells";
|
|
22
22
|
import { useLastFocusedCellId } from "@/core/cells/focus";
|
|
23
23
|
import { autoInstantiateAtom } from "@/core/config/config";
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
ENV_RENDERER,
|
|
26
|
+
SECRET_TEXTAREA_RENDERER,
|
|
27
|
+
SecretsProvider,
|
|
28
|
+
} from "./form-renderers";
|
|
25
29
|
|
|
26
|
-
const RENDERERS: FormRenderer[] = [ENV_RENDERER];
|
|
30
|
+
const RENDERERS: FormRenderer[] = [ENV_RENDERER, SECRET_TEXTAREA_RENDERER];
|
|
27
31
|
|
|
28
32
|
/**
|
|
29
33
|
* Grid layout for provider/database selector buttons.
|
package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap
CHANGED
|
@@ -4,7 +4,7 @@ exports[`generateDatabaseCode > basic connections > bigquery 1`] = `
|
|
|
4
4
|
"import json
|
|
5
5
|
import sqlmodel
|
|
6
6
|
|
|
7
|
-
credentials = json.loads("""{"type": "service_account", "project_id": "test"}""")
|
|
7
|
+
credentials = json.loads(r"""{"type": "service_account", "project_id": "test"}""")
|
|
8
8
|
engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
|
|
9
9
|
`;
|
|
10
10
|
|
|
@@ -89,6 +89,12 @@ DATABASE_URL = "data.duckdb"
|
|
|
89
89
|
engine = duckdb.connect(DATABASE_URL, read_only=True)"
|
|
90
90
|
`;
|
|
91
91
|
|
|
92
|
+
exports[`generateDatabaseCode > basic connections > iceberg configured catalog 1`] = `
|
|
93
|
+
"from pyiceberg.catalog import load_catalog
|
|
94
|
+
|
|
95
|
+
catalog = load_catalog("my_catalog")"
|
|
96
|
+
`;
|
|
97
|
+
|
|
92
98
|
exports[`generateDatabaseCode > basic connections > iceberg dynamodb 1`] = `
|
|
93
99
|
"from pyiceberg.catalog.dynamodb import DynamoDBCatalog
|
|
94
100
|
|
|
@@ -388,10 +394,26 @@ import sqlmodel
|
|
|
388
394
|
|
|
389
395
|
_project = os.environ.get("ENV_PROJECT")
|
|
390
396
|
_dataset = os.environ.get("ENV_DATASET")
|
|
391
|
-
credentials = json.loads("""{"type": "service_account", "project_id": "test"}""")
|
|
397
|
+
credentials = json.loads(r"""{"type": "service_account", "project_id": "test"}""")
|
|
392
398
|
engine = sqlmodel.create_engine(f"bigquery://{_project}/{_dataset}", credentials_info=credentials)"
|
|
393
399
|
`;
|
|
394
400
|
|
|
401
|
+
exports[`generateDatabaseCode > connections with secrets > bigquery with credentials_json as a file path 1`] = `
|
|
402
|
+
"import sqlmodel
|
|
403
|
+
|
|
404
|
+
engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_path="/etc/secrets/bigquery.json")"
|
|
405
|
+
`;
|
|
406
|
+
|
|
407
|
+
exports[`generateDatabaseCode > connections with secrets > bigquery with credentials_json as secret 1`] = `
|
|
408
|
+
"import json
|
|
409
|
+
import os
|
|
410
|
+
import sqlmodel
|
|
411
|
+
|
|
412
|
+
_credentials_json = os.environ.get("BIGQUERY_CREDENTIALS_JSON")
|
|
413
|
+
credentials = json.loads(_credentials_json)
|
|
414
|
+
engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
|
|
415
|
+
`;
|
|
416
|
+
|
|
395
417
|
exports[`generateDatabaseCode > connections with secrets > clickhouse with all connection details as secrets 1`] = `
|
|
396
418
|
"import clickhouse_connect
|
|
397
419
|
import os
|
|
@@ -493,7 +515,7 @@ exports[`generateDatabaseCode > edge cases > bigquery with long credentials 1`]
|
|
|
493
515
|
"import json
|
|
494
516
|
import sqlmodel
|
|
495
517
|
|
|
496
|
-
credentials = json.loads("""xxxxxxxxxx""")
|
|
518
|
+
credentials = json.loads(r"""xxxxxxxxxx""")
|
|
497
519
|
engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
|
|
498
520
|
`;
|
|
499
521
|
|
|
@@ -735,7 +757,7 @@ exports[`generateDatabaseCode > security cases > bigquery with malformed JSON 1`
|
|
|
735
757
|
"import json
|
|
736
758
|
import sqlmodel
|
|
737
759
|
|
|
738
|
-
credentials = json.loads("""{"type": "service_account", "project_id": "test"""")
|
|
760
|
+
credentials = json.loads(r"""{"type": "service_account", "project_id": "test"""")
|
|
739
761
|
engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
|
|
740
762
|
`;
|
|
741
763
|
|
|
@@ -743,7 +765,7 @@ exports[`generateDatabaseCode > security cases > bigquery with malformed JSON 2`
|
|
|
743
765
|
"import json
|
|
744
766
|
import sqlalchemy
|
|
745
767
|
|
|
746
|
-
credentials = json.loads("""{"type": "service_account", "project_id": "test"""")
|
|
768
|
+
credentials = json.loads(r"""{"type": "service_account", "project_id": "test"""")
|
|
747
769
|
engine = sqlalchemy.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
|
|
748
770
|
`;
|
|
749
771
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { prefixPath } from "../../paths";
|
|
3
4
|
import { prefixSecret } from "../../secrets";
|
|
4
5
|
import { type ConnectionLibrary, generateDatabaseCode } from "../as-code";
|
|
5
6
|
import type { DatabaseConnection } from "../schemas";
|
|
@@ -165,6 +166,17 @@ describe("generateDatabaseCode", () => {
|
|
|
165
166
|
},
|
|
166
167
|
};
|
|
167
168
|
|
|
169
|
+
const icebergConfiguredCatalogConnection: DatabaseConnection = {
|
|
170
|
+
type: "iceberg",
|
|
171
|
+
name: "my_catalog",
|
|
172
|
+
catalog: {
|
|
173
|
+
type: "REST",
|
|
174
|
+
uri: undefined,
|
|
175
|
+
warehouse: undefined,
|
|
176
|
+
token: undefined,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
|
|
168
180
|
const icebergSqlConnection: DatabaseConnection = {
|
|
169
181
|
type: "iceberg",
|
|
170
182
|
name: "my_catalog",
|
|
@@ -309,6 +321,11 @@ describe("generateDatabaseCode", () => {
|
|
|
309
321
|
["chdb", chdbConnection, "chdb"],
|
|
310
322
|
["timeplus", timeplusConnection, "sqlalchemy"],
|
|
311
323
|
["trino", trinoConnection, "sqlmodel"],
|
|
324
|
+
[
|
|
325
|
+
"iceberg configured catalog",
|
|
326
|
+
icebergConfiguredCatalogConnection,
|
|
327
|
+
"pyiceberg",
|
|
328
|
+
],
|
|
312
329
|
["iceberg rest", icebergRestConnection, "pyiceberg"],
|
|
313
330
|
["iceberg sql", icebergSqlConnection, "pyiceberg"],
|
|
314
331
|
["iceberg hive", icebergHiveConnection, "pyiceberg"],
|
|
@@ -383,6 +400,22 @@ describe("generateDatabaseCode", () => {
|
|
|
383
400
|
},
|
|
384
401
|
"sqlmodel",
|
|
385
402
|
],
|
|
403
|
+
[
|
|
404
|
+
"bigquery with credentials_json as secret",
|
|
405
|
+
{
|
|
406
|
+
...bigqueryConnection,
|
|
407
|
+
credentials_json: prefixSecret("BIGQUERY_CREDENTIALS_JSON"),
|
|
408
|
+
},
|
|
409
|
+
"sqlmodel",
|
|
410
|
+
],
|
|
411
|
+
[
|
|
412
|
+
"bigquery with credentials_json as a file path",
|
|
413
|
+
{
|
|
414
|
+
...bigqueryConnection,
|
|
415
|
+
credentials_json: prefixPath("/etc/secrets/bigquery.json"),
|
|
416
|
+
},
|
|
417
|
+
"sqlmodel",
|
|
418
|
+
],
|
|
386
419
|
[
|
|
387
420
|
"clickhouse with all connection details as secrets",
|
|
388
421
|
{
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import dedent from "string-dedent";
|
|
4
4
|
import { assertNever } from "@/utils/assertNever";
|
|
5
|
+
import { escapePythonString, resolveJsonCredential } from "../json-credentials";
|
|
5
6
|
import { isSecret, unprefixSecret } from "../secrets";
|
|
6
7
|
import { type DatabaseConnection, DatabaseConnectionSchema } from "./schemas";
|
|
7
8
|
|
|
@@ -415,8 +416,14 @@ ${formatUrlParams(params, (inner) => ` ${inner}`)},
|
|
|
415
416
|
}
|
|
416
417
|
|
|
417
418
|
class BigQueryGenerator extends CodeGenerator<"bigquery"> {
|
|
419
|
+
private get credential() {
|
|
420
|
+
return resolveJsonCredential(this.connection.credentials_json, (v) =>
|
|
421
|
+
this.secrets.print("credentials_json", v),
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
418
425
|
generateImports(): string[] {
|
|
419
|
-
return ["import json"];
|
|
426
|
+
return this.credential.kind === "path" ? [] : ["import json"];
|
|
420
427
|
}
|
|
421
428
|
|
|
422
429
|
generateConnectionCode(): string {
|
|
@@ -428,9 +435,16 @@ class BigQueryGenerator extends CodeGenerator<"bigquery"> {
|
|
|
428
435
|
"dataset",
|
|
429
436
|
this.connection.dataset,
|
|
430
437
|
);
|
|
438
|
+
const credential = this.credential;
|
|
439
|
+
|
|
440
|
+
if (credential.kind === "path") {
|
|
441
|
+
return dedent(`
|
|
442
|
+
engine = ${this.orm}.create_engine(f"bigquery://${project}/${dataset}", credentials_path="${escapePythonString(credential.path)}")
|
|
443
|
+
`);
|
|
444
|
+
}
|
|
431
445
|
|
|
432
446
|
return dedent(`
|
|
433
|
-
credentials =
|
|
447
|
+
credentials = ${credential.expr}
|
|
434
448
|
engine = ${this.orm}.create_engine(f"bigquery://${project}/${dataset}", credentials_info=credentials)
|
|
435
449
|
`);
|
|
436
450
|
}
|
|
@@ -589,7 +603,22 @@ class TrinoGenerator extends CodeGenerator<"trino"> {
|
|
|
589
603
|
}
|
|
590
604
|
|
|
591
605
|
class PyIcebergGenerator extends CodeGenerator<"iceberg"> {
|
|
606
|
+
private getCatalogOptions(): Record<
|
|
607
|
+
string,
|
|
608
|
+
string | number | boolean | undefined
|
|
609
|
+
> {
|
|
610
|
+
return Object.fromEntries(
|
|
611
|
+
Object.entries(this.connection.catalog).filter(
|
|
612
|
+
([key, value]) => value != null && value !== "" && key !== "type",
|
|
613
|
+
),
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
|
|
592
617
|
generateImports(): string[] {
|
|
618
|
+
if (Object.keys(this.getCatalogOptions()).length === 0) {
|
|
619
|
+
return ["from pyiceberg.catalog import load_catalog"];
|
|
620
|
+
}
|
|
621
|
+
|
|
593
622
|
switch (this.connection.catalog.type) {
|
|
594
623
|
case "REST":
|
|
595
624
|
return ["from pyiceberg.catalog.rest import RestCatalog"];
|
|
@@ -607,15 +636,12 @@ class PyIcebergGenerator extends CodeGenerator<"iceberg"> {
|
|
|
607
636
|
}
|
|
608
637
|
|
|
609
638
|
generateConnectionCode(): string {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
([k, v]) => v != null && v !== "" && k !== "type",
|
|
617
|
-
),
|
|
618
|
-
);
|
|
639
|
+
const name = `"${this.connection.name}"`;
|
|
640
|
+
const options = this.getCatalogOptions();
|
|
641
|
+
if (Object.keys(options).length === 0) {
|
|
642
|
+
return `catalog = load_catalog(${name})`;
|
|
643
|
+
}
|
|
644
|
+
|
|
619
645
|
// Convert to secrets if they are secrets
|
|
620
646
|
for (const [k, v] of Object.entries(options)) {
|
|
621
647
|
if (isSecret(v)) {
|
|
@@ -631,8 +657,6 @@ class PyIcebergGenerator extends CodeGenerator<"iceberg"> {
|
|
|
631
657
|
(line) => `${indent}${line}`,
|
|
632
658
|
);
|
|
633
659
|
|
|
634
|
-
const name = `"${this.connection.name}"`;
|
|
635
|
-
|
|
636
660
|
switch (this.connection.catalog.type) {
|
|
637
661
|
case "REST":
|
|
638
662
|
return dedent(`
|
|
@@ -295,11 +295,12 @@ export const BigQueryConnectionSchema = z
|
|
|
295
295
|
optionRegex: "(bigquery.?dataset|dataset)",
|
|
296
296
|
}),
|
|
297
297
|
),
|
|
298
|
-
credentials_json: z
|
|
299
|
-
.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
),
|
|
298
|
+
credentials_json: z.string().describe(
|
|
299
|
+
FieldOptions.of({
|
|
300
|
+
label: "Credentials JSON",
|
|
301
|
+
special: "secret_textarea",
|
|
302
|
+
}),
|
|
303
|
+
),
|
|
303
304
|
})
|
|
304
305
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
305
306
|
|