@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.
Files changed (198) hide show
  1. package/dist/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
  2. package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
  3. package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
  4. package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
  5. package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
  6. package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DsZJj75A.js} +3117 -3120
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-CfBftiVM.js → common-Bpnmu1Yz.js} +1085 -1053
  9. package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
  10. package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
  11. package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
  12. package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
  13. package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
  14. package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
  15. package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
  16. package/dist/{html-to-image-DDENewl2.js → html-to-image-CZ1kLKkq.js} +7350 -6356
  17. package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
  18. package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
  19. package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
  20. package/dist/main.js +259 -194
  21. package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
  22. package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
  23. package/dist/{process-output-2QPcgBQg.js → process-output-bVfbcx5_.js} +32 -28
  24. package/dist/{reveal-component-BclLkYnV.js → reveal-component-JIt3oSeU.js} +642 -634
  25. package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
  26. package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
  27. package/dist/style.css +1 -1
  28. package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
  29. package/dist/toDate-CqhBWjvO.js +713 -0
  30. package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
  31. package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
  32. package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
  33. package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
  34. package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
  35. package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
  36. package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
  37. package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
  38. package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
  39. package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
  40. package/package.json +3 -4
  41. package/src/__mocks__/requests.ts +56 -4
  42. package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
  43. package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
  44. package/src/components/data-table/__tests__/columns.test.tsx +9 -1
  45. package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
  46. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  47. package/src/components/data-table/column-summary/utils.ts +26 -7
  48. package/src/components/data-table/columns.tsx +11 -1
  49. package/src/components/data-table/types.ts +1 -1
  50. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  51. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  52. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  53. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  54. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  55. package/src/components/editor/actions/pdf-export.ts +11 -14
  56. package/src/components/editor/actions/useNotebookActions.tsx +28 -19
  57. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  58. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  59. package/src/components/editor/chrome/__tests__/state.test.ts +7 -1
  60. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  61. package/src/components/editor/chrome/panels/__tests__/panel-accordion-state.test.ts +42 -0
  62. package/src/components/editor/chrome/panels/file-explorer-panel.tsx +6 -17
  63. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  64. package/src/components/editor/chrome/panels/panel-accordion-state.ts +51 -0
  65. package/src/components/editor/chrome/panels/session-panel.tsx +5 -16
  66. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  67. package/src/components/editor/chrome/state.ts +10 -0
  68. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  69. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  70. package/src/components/editor/connections/__tests__/quick-add-data-sources.test.tsx +113 -0
  71. package/src/components/editor/connections/add-connection-dialog.tsx +2 -0
  72. package/src/components/editor/connections/components.tsx +6 -2
  73. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +27 -5
  74. package/src/components/editor/connections/database/__tests__/as-code.test.ts +33 -0
  75. package/src/components/editor/connections/database/as-code.ts +37 -13
  76. package/src/components/editor/connections/database/schemas.ts +6 -5
  77. package/src/components/editor/connections/form-renderers.tsx +99 -1
  78. package/src/components/editor/connections/json-credentials.ts +57 -0
  79. package/src/components/editor/connections/paths.ts +21 -0
  80. package/src/components/editor/connections/quick-add-data-sources.tsx +106 -0
  81. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  82. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  83. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  84. package/src/components/editor/connections/storage/as-code.ts +79 -36
  85. package/src/components/editor/connections/storage/schemas.ts +85 -44
  86. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  87. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  88. package/src/components/editor/file-tree/renderers.tsx +21 -0
  89. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +2 -2
  90. package/src/components/editor/viewer-banner.tsx +6 -1
  91. package/src/components/forms/form.tsx +59 -24
  92. package/src/components/forms/options.ts +2 -1
  93. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  94. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  95. package/src/components/slides/reveal-component.tsx +22 -17
  96. package/src/components/slides/slide-cell-view.tsx +1 -1
  97. package/src/components/slides/slides.css +4 -2
  98. package/src/components/static-html/share-modal.tsx +1 -1
  99. package/src/components/ui/input.tsx +1 -1
  100. package/src/core/ai/context/providers/file.ts +8 -29
  101. package/src/core/cells/__tests__/cells.test.ts +12 -2
  102. package/src/core/cells/__tests__/logs.test.ts +5 -5
  103. package/src/core/cells/cells.ts +17 -4
  104. package/src/core/cells/document-changes.ts +1 -0
  105. package/src/core/cells/logs.ts +14 -4
  106. package/src/core/codemirror/cm.ts +3 -0
  107. package/src/core/codemirror/code-lens/__tests__/actions.test.ts +68 -0
  108. package/src/core/codemirror/code-lens/__tests__/analyzer.test.ts +224 -0
  109. package/src/core/codemirror/code-lens/__tests__/entities.test.ts +175 -0
  110. package/src/core/codemirror/code-lens/__tests__/extension.test.ts +325 -0
  111. package/src/core/codemirror/code-lens/__tests__/perf.test.ts +121 -0
  112. package/src/core/codemirror/code-lens/__tests__/popover.test.ts +93 -0
  113. package/src/core/codemirror/code-lens/actions.ts +33 -0
  114. package/src/core/codemirror/code-lens/analyzer.ts +284 -0
  115. package/src/core/codemirror/code-lens/entities.ts +123 -0
  116. package/src/core/codemirror/code-lens/extension.ts +298 -0
  117. package/src/core/codemirror/code-lens/icons.ts +36 -0
  118. package/src/core/codemirror/code-lens/popover.tsx +240 -0
  119. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  120. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  121. package/src/core/codemirror/copilot/language-server.ts +182 -70
  122. package/src/core/codemirror/copilot/transport.ts +15 -207
  123. package/src/core/codemirror/copilot/types.ts +9 -2
  124. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  125. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  126. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  127. package/src/core/codemirror/go-to-definition/commands.ts +93 -90
  128. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  129. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  130. package/src/core/codemirror/language/__tests__/sql.test.ts +627 -61
  131. package/src/core/codemirror/language/languages/python.ts +23 -11
  132. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  133. package/src/core/codemirror/language/languages/sql/renderers.tsx +3 -3
  134. package/src/core/codemirror/language/languages/sql/sql.ts +251 -16
  135. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  136. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  137. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  138. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  139. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  140. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  141. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  142. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  143. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  144. package/src/core/codemirror/lsp/transports.ts +8 -1
  145. package/src/core/codemirror/lsp/types.ts +68 -8
  146. package/src/core/codemirror/lsp/utils.ts +14 -0
  147. package/src/core/codemirror/python-node-names.ts +90 -0
  148. package/src/core/codemirror/reactive-references/analyzer.ts +52 -57
  149. package/src/core/codemirror/utils.ts +28 -0
  150. package/src/core/config/feature-flag.tsx +3 -1
  151. package/src/core/datasets/data-source-discovery.ts +5 -0
  152. package/src/core/datasets/request-registry.ts +11 -0
  153. package/src/core/edit-app.tsx +1 -2
  154. package/src/core/export/__tests__/hooks.test.ts +2 -4
  155. package/src/core/export/hooks.ts +4 -3
  156. package/src/core/hotkeys/hotkeys.ts +0 -2
  157. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  158. package/src/core/islands/bootstrap.ts +1 -0
  159. package/src/core/islands/bridge.ts +18 -0
  160. package/src/core/kernel/messages.ts +2 -0
  161. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  162. package/src/core/lsp/transport.ts +304 -132
  163. package/src/core/mode.ts +10 -0
  164. package/src/core/network/__tests__/api.test.ts +42 -0
  165. package/src/core/network/__tests__/connection.test.ts +45 -0
  166. package/src/core/network/__tests__/requests-lazy.test.ts +47 -1
  167. package/src/core/network/__tests__/requests-network.test.ts +48 -0
  168. package/src/core/network/api.ts +27 -0
  169. package/src/core/network/connection.ts +15 -0
  170. package/src/core/network/requests-lazy.ts +3 -0
  171. package/src/core/network/requests-network.ts +31 -8
  172. package/src/core/network/requests-static.ts +3 -0
  173. package/src/core/network/requests-toasting.tsx +3 -0
  174. package/src/core/network/types.ts +27 -6
  175. package/src/core/rtc/state.ts +9 -0
  176. package/src/core/static/download-html.ts +5 -14
  177. package/src/core/wasm/__tests__/bridge.test.ts +99 -5
  178. package/src/core/wasm/bridge.ts +42 -9
  179. package/src/core/wasm/worker/types.ts +10 -3
  180. package/src/core/wasm/worker/worker.ts +7 -8
  181. package/src/core/websocket/useMarimoKernelConnection.tsx +4 -0
  182. package/src/css/app/codemirror.css +16 -2
  183. package/src/hooks/__tests__/useDataSourceDiscovery.test.ts +80 -0
  184. package/src/hooks/useDataSourceDiscovery.ts +18 -0
  185. package/src/plugins/impl/DataTablePlugin.tsx +4 -1
  186. package/src/plugins/impl/FormPlugin.tsx +1 -1
  187. package/src/plugins/impl/__tests__/DataTablePlugin.test.tsx +158 -0
  188. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  189. package/src/plugins/impl/anywidget/__tests__/model.test.ts +55 -0
  190. package/src/plugins/impl/anywidget/model.ts +20 -5
  191. package/src/plugins/impl/data-frames/forms/__tests__/__snapshots__/form.test.tsx.snap +33 -21
  192. package/src/plugins/impl/data-frames/forms/renderers.tsx +11 -8
  193. package/src/stories/log-viewer.stories.tsx +12 -6
  194. package/src/utils/__tests__/blob.test.ts +27 -1
  195. package/src/utils/__tests__/download.test.tsx +44 -4
  196. package/src/utils/blob.ts +24 -1
  197. package/src/utils/download.ts +14 -15
  198. 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 { ENV_RENDERER, SecretsProvider } from "./form-renderers";
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.
@@ -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 = json.loads("""${this.connection.credentials_json}""")
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
- let options: Record<string, string | number | boolean | undefined> = {
611
- ...this.connection.catalog,
612
- };
613
- // Remove k='type' and v=nullish values
614
- options = Object.fromEntries(
615
- Object.entries(options).filter(
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
- .string()
300
- .describe(
301
- FieldOptions.of({ label: "Credentials JSON", inputType: "textarea" }),
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