@marimo-team/islands 0.23.16-dev5 → 0.23.16-dev51

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 (156) 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-1e7PcW56.js} +3117 -3120
  7. package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
  8. package/dist/{code-visibility-C4wLKnSb.js → common-CD6phlRu.js} +1046 -1014
  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-3w0AakWW.js} +7164 -6240
  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 +1010 -1019
  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-BRwR4QGs.js} +32 -28
  24. package/dist/{reveal-component-BSD4w8YU.js → reveal-component-DmGGKS8z.js} +725 -643
  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 +45 -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/column-summary/chart-spec-model.tsx +163 -103
  45. package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
  46. package/src/components/data-table/column-summary/utils.ts +26 -7
  47. package/src/components/data-table/types.ts +1 -1
  48. package/src/components/editor/__tests__/viewer-banner.test.tsx +14 -0
  49. package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
  50. package/src/components/editor/actions/__tests__/pdf-export.test.ts +32 -13
  51. package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
  52. package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
  53. package/src/components/editor/actions/pdf-export.ts +11 -14
  54. package/src/components/editor/actions/useNotebookActions.tsx +18 -18
  55. package/src/components/editor/cell/code/__tests__/cell-editor-activity-lifecycle.test.ts +250 -0
  56. package/src/components/editor/cell/code/cell-editor.tsx +13 -7
  57. package/src/components/editor/chrome/panels/__tests__/logs-panel.test.tsx +25 -0
  58. package/src/components/editor/chrome/panels/logs-panel.tsx +12 -2
  59. package/src/components/editor/chrome/panels/write-secret-modal.tsx +40 -12
  60. package/src/components/editor/connections/__tests__/json-credentials.test.ts +114 -0
  61. package/src/components/editor/connections/__tests__/paths.test.ts +39 -0
  62. package/src/components/editor/connections/components.tsx +6 -2
  63. package/src/components/editor/connections/database/__tests__/__snapshots__/as-code.test.ts.snap +21 -5
  64. package/src/components/editor/connections/database/__tests__/as-code.test.ts +17 -0
  65. package/src/components/editor/connections/database/as-code.ts +16 -2
  66. package/src/components/editor/connections/database/schemas.ts +6 -5
  67. package/src/components/editor/connections/form-renderers.tsx +99 -1
  68. package/src/components/editor/connections/json-credentials.ts +57 -0
  69. package/src/components/editor/connections/paths.ts +21 -0
  70. package/src/components/editor/connections/secret-combobox.tsx +41 -20
  71. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +88 -2
  72. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +128 -8
  73. package/src/components/editor/connections/storage/as-code.ts +79 -36
  74. package/src/components/editor/connections/storage/schemas.ts +85 -44
  75. package/src/components/editor/file-tree/__tests__/renderers.test.ts +31 -0
  76. package/src/components/editor/file-tree/file-viewer.tsx +11 -7
  77. package/src/components/editor/file-tree/renderers.tsx +21 -0
  78. package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
  79. package/src/components/editor/viewer-banner.tsx +6 -1
  80. package/src/components/forms/options.ts +2 -1
  81. package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
  82. package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
  83. package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
  84. package/src/components/slides/reveal-component.tsx +25 -19
  85. package/src/components/slides/slide-cell-view.tsx +1 -1
  86. package/src/components/slides/slide-scroll-hint.tsx +161 -0
  87. package/src/components/slides/slides.css +4 -2
  88. package/src/components/static-html/share-modal.tsx +1 -1
  89. package/src/components/ui/input.tsx +1 -1
  90. package/src/core/ai/context/providers/file.ts +8 -29
  91. package/src/core/cells/__tests__/cells.test.ts +12 -2
  92. package/src/core/cells/__tests__/logs.test.ts +5 -5
  93. package/src/core/cells/cells.ts +17 -4
  94. package/src/core/cells/document-changes.ts +1 -0
  95. package/src/core/cells/logs.ts +14 -4
  96. package/src/core/codemirror/copilot/__tests__/language-server.test.ts +396 -100
  97. package/src/core/codemirror/copilot/__tests__/transport.test.ts +128 -540
  98. package/src/core/codemirror/copilot/language-server.ts +182 -70
  99. package/src/core/codemirror/copilot/transport.ts +15 -207
  100. package/src/core/codemirror/copilot/types.ts +9 -2
  101. package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
  102. package/src/core/codemirror/find-replace/navigate.ts +2 -0
  103. package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
  104. package/src/core/codemirror/go-to-definition/commands.ts +3 -0
  105. package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
  106. package/src/core/codemirror/keymaps/keymaps.ts +12 -0
  107. package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
  108. package/src/core/codemirror/language/languages/python.ts +23 -11
  109. package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
  110. package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
  111. package/src/core/codemirror/lsp/__tests__/federated-lsp.test.ts +161 -0
  112. package/src/core/codemirror/lsp/__tests__/log-messages.test.ts +53 -0
  113. package/src/core/codemirror/lsp/__tests__/normalize-markdown-math.test.ts +25 -0
  114. package/src/core/codemirror/lsp/__tests__/notebook-lsp.test.ts +598 -156
  115. package/src/core/codemirror/lsp/__tests__/transports.test.ts +112 -0
  116. package/src/core/codemirror/lsp/federated-lsp.ts +76 -23
  117. package/src/core/codemirror/lsp/log-messages.ts +37 -0
  118. package/src/core/codemirror/lsp/normalize-markdown-math.ts +8 -4
  119. package/src/core/codemirror/lsp/notebook-lsp.ts +220 -56
  120. package/src/core/codemirror/lsp/transports.ts +8 -1
  121. package/src/core/codemirror/lsp/types.ts +68 -8
  122. package/src/core/codemirror/lsp/utils.ts +14 -0
  123. package/src/core/codemirror/utils.ts +28 -0
  124. package/src/core/edit-app.tsx +1 -2
  125. package/src/core/export/__tests__/hooks.test.ts +2 -4
  126. package/src/core/export/hooks.ts +4 -3
  127. package/src/core/hotkeys/hotkeys.ts +0 -2
  128. package/src/core/islands/__tests__/bridge.test.ts +107 -0
  129. package/src/core/islands/bridge.ts +16 -0
  130. package/src/core/lsp/__tests__/transport.test.ts +509 -337
  131. package/src/core/lsp/transport.ts +304 -132
  132. package/src/core/mode.ts +10 -0
  133. package/src/core/network/__tests__/api.test.ts +42 -0
  134. package/src/core/network/__tests__/connection.test.ts +45 -0
  135. package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
  136. package/src/core/network/__tests__/requests-network.test.ts +21 -0
  137. package/src/core/network/api.ts +27 -0
  138. package/src/core/network/connection.ts +15 -0
  139. package/src/core/network/requests-lazy.ts +1 -0
  140. package/src/core/network/requests-network.ts +14 -8
  141. package/src/core/network/requests-static.ts +1 -0
  142. package/src/core/network/requests-toasting.tsx +1 -0
  143. package/src/core/network/types.ts +22 -6
  144. package/src/core/rtc/state.ts +9 -0
  145. package/src/core/static/download-html.ts +5 -14
  146. package/src/core/wasm/bridge.ts +4 -2
  147. package/src/core/wasm/worker/types.ts +5 -2
  148. package/src/css/app/codemirror.css +16 -2
  149. package/src/plugins/impl/FormPlugin.tsx +1 -1
  150. package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
  151. package/src/stories/log-viewer.stories.tsx +12 -6
  152. package/src/utils/__tests__/blob.test.ts +27 -1
  153. package/src/utils/__tests__/download.test.tsx +44 -4
  154. package/src/utils/blob.ts +24 -1
  155. package/src/utils/download.ts +14 -15
  156. package/dist/toDate-B3IRfHhw.js +0 -692
@@ -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
 
@@ -388,10 +388,26 @@ import sqlmodel
388
388
 
389
389
  _project = os.environ.get("ENV_PROJECT")
390
390
  _dataset = os.environ.get("ENV_DATASET")
391
- credentials = json.loads("""{"type": "service_account", "project_id": "test"}""")
391
+ credentials = json.loads(r"""{"type": "service_account", "project_id": "test"}""")
392
392
  engine = sqlmodel.create_engine(f"bigquery://{_project}/{_dataset}", credentials_info=credentials)"
393
393
  `;
394
394
 
395
+ exports[`generateDatabaseCode > connections with secrets > bigquery with credentials_json as a file path 1`] = `
396
+ "import sqlmodel
397
+
398
+ engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_path="/etc/secrets/bigquery.json")"
399
+ `;
400
+
401
+ exports[`generateDatabaseCode > connections with secrets > bigquery with credentials_json as secret 1`] = `
402
+ "import json
403
+ import os
404
+ import sqlmodel
405
+
406
+ _credentials_json = os.environ.get("BIGQUERY_CREDENTIALS_JSON")
407
+ credentials = json.loads(_credentials_json)
408
+ engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
409
+ `;
410
+
395
411
  exports[`generateDatabaseCode > connections with secrets > clickhouse with all connection details as secrets 1`] = `
396
412
  "import clickhouse_connect
397
413
  import os
@@ -493,7 +509,7 @@ exports[`generateDatabaseCode > edge cases > bigquery with long credentials 1`]
493
509
  "import json
494
510
  import sqlmodel
495
511
 
496
- credentials = json.loads("""xxxxxxxxxx""")
512
+ credentials = json.loads(r"""xxxxxxxxxx""")
497
513
  engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
498
514
  `;
499
515
 
@@ -735,7 +751,7 @@ exports[`generateDatabaseCode > security cases > bigquery with malformed JSON 1`
735
751
  "import json
736
752
  import sqlmodel
737
753
 
738
- credentials = json.loads("""{"type": "service_account", "project_id": "test"""")
754
+ credentials = json.loads(r"""{"type": "service_account", "project_id": "test"""")
739
755
  engine = sqlmodel.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
740
756
  `;
741
757
 
@@ -743,7 +759,7 @@ exports[`generateDatabaseCode > security cases > bigquery with malformed JSON 2`
743
759
  "import json
744
760
  import sqlalchemy
745
761
 
746
- credentials = json.loads("""{"type": "service_account", "project_id": "test"""")
762
+ credentials = json.loads(r"""{"type": "service_account", "project_id": "test"""")
747
763
  engine = sqlalchemy.create_engine(f"bigquery://my-project/my_dataset", credentials_info=credentials)"
748
764
  `;
749
765
 
@@ -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";
@@ -383,6 +384,22 @@ describe("generateDatabaseCode", () => {
383
384
  },
384
385
  "sqlmodel",
385
386
  ],
387
+ [
388
+ "bigquery with credentials_json as secret",
389
+ {
390
+ ...bigqueryConnection,
391
+ credentials_json: prefixSecret("BIGQUERY_CREDENTIALS_JSON"),
392
+ },
393
+ "sqlmodel",
394
+ ],
395
+ [
396
+ "bigquery with credentials_json as a file path",
397
+ {
398
+ ...bigqueryConnection,
399
+ credentials_json: prefixPath("/etc/secrets/bigquery.json"),
400
+ },
401
+ "sqlmodel",
402
+ ],
386
403
  [
387
404
  "clickhouse with all connection details as secrets",
388
405
  {
@@ -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
  }
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { InfoIcon } from "lucide-react";
3
4
  import { createContext, type ReactNode, use, useMemo } from "react";
4
5
  import { z } from "zod";
5
6
  import type { FormRenderer } from "@/components/forms/form";
@@ -13,6 +14,7 @@ import {
13
14
  FormLabel,
14
15
  FormMessage,
15
16
  } from "@/components/ui/form";
17
+ import { Tooltip } from "@/components/ui/tooltip";
16
18
  import { SECRETS_REGISTRY } from "@/core/secrets/request-registry";
17
19
  import { useAsyncData } from "@/hooks/useAsyncData";
18
20
  import { Functions } from "@/utils/functions";
@@ -20,8 +22,10 @@ import {
20
22
  sortProviders,
21
23
  WriteSecretModal,
22
24
  } from "../chrome/panels/write-secret-modal";
25
+ import { looksLikeJson } from "./json-credentials";
26
+ import { isPath, prefixPath, unprefixPath } from "./paths";
23
27
  import { partitionSecretKeys, SecretCombobox } from "./secret-combobox";
24
- import { prefixSecret } from "./secrets";
28
+ import { isSecret, prefixSecret } from "./secrets";
25
29
 
26
30
  interface SecretsContextType {
27
31
  providerNames: string[];
@@ -141,3 +145,97 @@ export const ENV_RENDERER: FormRenderer<z.ZodString> = {
141
145
  );
142
146
  },
143
147
  };
148
+
149
+ /**
150
+ * For credentials that shouldn't be baked into the generated notebook code
151
+ * (e.g. a service-account JSON key).
152
+ */
153
+ export const SECRET_TEXTAREA_RENDERER: FormRenderer<z.ZodString> = {
154
+ isMatch: (schema: z.ZodType): schema is z.ZodString => {
155
+ if (schema instanceof z.ZodString) {
156
+ const { special } = FieldOptions.parse(schema.description || "");
157
+ return special === "secret_textarea";
158
+ }
159
+ return false;
160
+ },
161
+ Component: ({ schema, form, path }) => {
162
+ const { secretKeys, providerNames, refreshSecrets } = useSecrets();
163
+ const { openModal, closeModal } = useImperativeModal();
164
+ const { label, description, placeholder } = FieldOptions.parse(
165
+ schema.description || "",
166
+ );
167
+
168
+ return (
169
+ <FormField
170
+ control={form.control}
171
+ name={path}
172
+ render={({ field }) => {
173
+ const value = field.value ? String(field.value) : "";
174
+ // Non-secret values are file paths; display them unprefixed.
175
+ const displayValue = isPath(value) ? unprefixPath(value) : value;
176
+
177
+ return (
178
+ <FormItem>
179
+ <FormLabel className="flex items-center gap-1">
180
+ {label}
181
+ <Tooltip
182
+ content="Enter a path to a file on disk, or create a secret to paste JSON directly."
183
+ delayDuration={300}
184
+ >
185
+ <button
186
+ type="button"
187
+ className="inline-flex"
188
+ aria-label="Credential input help"
189
+ >
190
+ <InfoIcon className="h-3.5 w-3.5" />
191
+ </button>
192
+ </Tooltip>
193
+ </FormLabel>
194
+ <FormDescription>{description}</FormDescription>
195
+ <FormControl>
196
+ <SecretCombobox
197
+ value={displayValue}
198
+ onChange={(next) => {
199
+ field.onChange(
200
+ !next || isSecret(next) || isPath(next)
201
+ ? next
202
+ : prefixPath(next),
203
+ );
204
+ }}
205
+ placeholder={placeholder}
206
+ searchPlaceholder="Type a file path or paste JSON"
207
+ formatCustomValueLabel={(custom) =>
208
+ `Use "${custom}" as a file path`
209
+ }
210
+ createSecretLabel="Paste JSON credentials"
211
+ allowCustomValue={(search) => !looksLikeJson(search)}
212
+ allowCreateSecret={(search) =>
213
+ !search || looksLikeJson(search)
214
+ }
215
+ recommendedKeys={[]}
216
+ otherKeys={secretKeys}
217
+ onCreateSecret={(suggestedValue) => {
218
+ openModal(
219
+ <WriteSecretModal
220
+ providerNames={providerNames}
221
+ initialValue={suggestedValue}
222
+ multiline={true}
223
+ onSuccess={(secretKey) => {
224
+ refreshSecrets();
225
+ field.onChange(prefixSecret(secretKey));
226
+ closeModal();
227
+ }}
228
+ onClose={closeModal}
229
+ />,
230
+ );
231
+ }}
232
+ />
233
+ </FormControl>
234
+ <FormMessage />
235
+ </FormItem>
236
+ );
237
+ }}
238
+ />
239
+ );
240
+ },
241
+ };
@@ -0,0 +1,57 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { isPath, unprefixPath } from "./paths";
3
+ import { isSecret } from "./secrets";
4
+
5
+ export type JsonCredential =
6
+ | { kind: "path"; path: string }
7
+ | { kind: "json"; expr: string };
8
+
9
+ /**
10
+ * Resolves a `secret_textarea` field's value — a file path, a secret
11
+ * reference, or a raw literal — into either a path to hand to a
12
+ * provider's path-based auth kwarg, or a `json.loads(...)` expression.
13
+ */
14
+ export function resolveJsonCredential(
15
+ value: string,
16
+ printSecret: (value: string) => string,
17
+ ): JsonCredential {
18
+ if (isPath(value)) {
19
+ return { kind: "path", path: unprefixPath(value) };
20
+ }
21
+ const expr = isSecret(value)
22
+ ? `json.loads(${printSecret(value)})`
23
+ : // Use raw string to avoid interpreting \n escapes as newlines.
24
+ `json.loads(r"""${value}""")`;
25
+ return { kind: "json", expr };
26
+ }
27
+
28
+ export function looksLikeJson(value: string): boolean {
29
+ const trimmed = value.trimStart();
30
+ return trimmed.startsWith("{") || trimmed.startsWith("[");
31
+ }
32
+
33
+ /**
34
+ * Collapse a credential value to a single dotenv-safe line: minify JSON
35
+ * when parseable, otherwise strip newlines.
36
+ */
37
+ export function flattenSecretValue(value: string): string {
38
+ const trimmed = value.trim();
39
+ if (!trimmed) {
40
+ return "";
41
+ }
42
+ try {
43
+ return JSON.stringify(JSON.parse(trimmed));
44
+ } catch {
45
+ return value.replaceAll(/\r?\n/g, "");
46
+ }
47
+ }
48
+
49
+ /** Escape a filesystem path for embedding in a Python double-quoted string. */
50
+ export function escapePythonString(value: string): string {
51
+ return value
52
+ .replaceAll("\\", "\\\\")
53
+ .replaceAll('"', '\\"')
54
+ .replaceAll("\n", "\\n")
55
+ .replaceAll("\r", "\\r")
56
+ .replaceAll("\t", "\\t");
57
+ }
@@ -0,0 +1,21 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import type { TypedString } from "@/utils/typed";
3
+
4
+ const PREFIX = "path:";
5
+
6
+ export type PathPlaceholder = TypedString<"path">;
7
+
8
+ export function isPath(value: unknown): value is PathPlaceholder {
9
+ if (typeof value !== "string") {
10
+ return false;
11
+ }
12
+ return value.startsWith(PREFIX);
13
+ }
14
+
15
+ export function prefixPath(value: string): PathPlaceholder {
16
+ return `${PREFIX}${value}` as PathPlaceholder;
17
+ }
18
+
19
+ export function unprefixPath(value: PathPlaceholder): string {
20
+ return value.slice(PREFIX.length);
21
+ }
@@ -56,6 +56,16 @@ interface SecretComboboxProps {
56
56
  /** Opens the create-secret flow; `suggestedValue` is the current search text when present. */
57
57
  onCreateSecret: (suggestedValue?: string) => void;
58
58
  className?: string;
59
+ /** Placeholder for the search input inside the popover. */
60
+ searchPlaceholder?: string;
61
+ /** Label for the "commit a free-text literal" item, e.g. `Use "${value}" as a path`. */
62
+ formatCustomValueLabel?: (value: string) => string;
63
+ /** Label for the "create a new secret" item. */
64
+ createSecretLabel?: string;
65
+ /** When false for the current search, hide the free-text / path option. */
66
+ allowCustomValue?: (search: string) => boolean;
67
+ /** When false for the current search, hide the "create a new secret" option. */
68
+ allowCreateSecret?: (search: string) => boolean;
59
69
  }
60
70
 
61
71
  /**
@@ -73,6 +83,11 @@ export const SecretCombobox: React.FC<SecretComboboxProps> = ({
73
83
  otherKeys,
74
84
  onCreateSecret,
75
85
  className,
86
+ searchPlaceholder,
87
+ formatCustomValueLabel = (custom) => `Use "${custom}"`,
88
+ createSecretLabel = "Create a new secret",
89
+ allowCustomValue,
90
+ allowCreateSecret,
76
91
  }) => {
77
92
  const [open, setOpen] = useState(false);
78
93
  const [search, setSearch] = useState("");
@@ -81,7 +96,11 @@ export const SecretCombobox: React.FC<SecretComboboxProps> = ({
81
96
  // Non-secret fields may commit a literal even when it collides with an
82
97
  // existing secret key, so we intentionally don't filter out matches here.
83
98
  const showCustomValue =
84
- !secretsOnly && trimmedSearch.length > 0 && trimmedSearch !== value;
99
+ !secretsOnly &&
100
+ trimmedSearch.length > 0 &&
101
+ trimmedSearch !== value &&
102
+ (allowCustomValue?.(trimmedSearch) ?? true);
103
+ const showCreateSecret = allowCreateSecret?.(trimmedSearch) ?? true;
85
104
 
86
105
  const displayValue = (() => {
87
106
  if (!value) {
@@ -158,9 +177,10 @@ export const SecretCombobox: React.FC<SecretComboboxProps> = ({
158
177
  <Command>
159
178
  <CommandInput
160
179
  placeholder={
161
- secretsOnly
180
+ searchPlaceholder ??
181
+ (secretsOnly
162
182
  ? "Search secrets..."
163
- : "Type a value or search secrets..."
183
+ : "Type a value or search secrets...")
164
184
  }
165
185
  rootClassName="px-1 h-8"
166
186
  autoFocus={true}
@@ -174,26 +194,27 @@ export const SecretCombobox: React.FC<SecretComboboxProps> = ({
174
194
  value={`use custom value ${trimmedSearch}`}
175
195
  onSelect={() => selectCustom(trimmedSearch)}
176
196
  >
177
- Use "{trimmedSearch}"
197
+ {formatCustomValueLabel(trimmedSearch)}
198
+ </CommandItem>
199
+ </CommandGroup>
200
+ )}
201
+ {showCreateSecret && (
202
+ <CommandGroup className="mt-0">
203
+ <CommandItem
204
+ // Include search so this stays visible while filtering
205
+ value={`create new secret ${trimmedSearch}`}
206
+ onSelect={() => {
207
+ const suggestedValue = trimmedSearch || undefined;
208
+ setOpen(false);
209
+ setSearch("");
210
+ onCreateSecret(suggestedValue);
211
+ }}
212
+ >
213
+ <PlusCircleIcon className="mr-2 h-3.5 w-3.5" />
214
+ {createSecretLabel}
178
215
  </CommandItem>
179
216
  </CommandGroup>
180
217
  )}
181
- {showCustomValue && <CommandSeparator />}
182
- <CommandGroup className="mt-0">
183
- <CommandItem
184
- // Include search so this stays visible while filtering
185
- value={`create new secret ${trimmedSearch}`}
186
- onSelect={() => {
187
- const suggestedValue = trimmedSearch || undefined;
188
- setOpen(false);
189
- setSearch("");
190
- onCreateSecret(suggestedValue);
191
- }}
192
- >
193
- <PlusCircleIcon className="mr-2 h-3.5 w-3.5" />
194
- Create a new secret
195
- </CommandItem>
196
- </CommandGroup>
197
218
  {recommendedKeys.length > 0 && (
198
219
  <>
199
220
  <CommandSeparator className="mt-0" />
@@ -51,6 +51,21 @@ store = S3Store("operator-bucket",
51
51
  )"
52
52
  `;
53
53
 
54
+ exports[`generateStorageCode > CoreWeave > with container credentials 1`] = `
55
+ "from obstore.store import S3Store
56
+ import os
57
+
58
+ _container_credentials_full_uri = os.environ.get("AWS_CONTAINER_CREDENTIALS_FULL_URI")
59
+ _container_authorization_token_file = os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
60
+ store = S3Store("operator-bucket",
61
+ region="US-EAST-04A",
62
+ container_credentials_full_uri=_container_credentials_full_uri,
63
+ container_authorization_token_file=_container_authorization_token_file,
64
+ endpoint="https://operator-bucket.cwobject.com",
65
+ virtual_hosted_style_request=True,
66
+ )"
67
+ `;
68
+
54
69
  exports[`generateStorageCode > CoreWeave > with secrets 1`] = `
55
70
  "from obstore.store import S3Store
56
71
  import os
@@ -70,7 +85,27 @@ exports[`generateStorageCode > GCS > with service account key 1`] = `
70
85
  "from obstore.store import GCSStore
71
86
  import json
72
87
 
73
- _credentials = json.loads("""{"type": "service_account", "project_id": "test"}""")
88
+ _credentials = json.loads(r"""{"type": "service_account", "project_id": "test"}""")
89
+ store = GCSStore("my-bucket",
90
+ service_account_key=_credentials,
91
+ )"
92
+ `;
93
+
94
+ exports[`generateStorageCode > GCS > with service account key from a file path 1`] = `
95
+ "from obstore.store import GCSStore
96
+
97
+ store = GCSStore("my-bucket",
98
+ service_account_path="/etc/secrets/gcs.json",
99
+ )"
100
+ `;
101
+
102
+ exports[`generateStorageCode > GCS > with service account key from secrets 1`] = `
103
+ "from obstore.store import GCSStore
104
+ import json
105
+ import os
106
+
107
+ _service_account_key = os.environ.get("GCS_SERVICE_ACCOUNT_KEY")
108
+ _credentials = json.loads(_service_account_key)
74
109
  store = GCSStore("my-bucket",
75
110
  service_account_key=_credentials,
76
111
  )"
@@ -99,7 +134,23 @@ exports[`generateStorageCode > Google Drive > with service account credentials 1
99
134
  "from gdrive_fsspec import GoogleDriveFileSystem
100
135
  import json
101
136
 
102
- _creds = json.loads("""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
137
+ _creds = json.loads(r"""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
138
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
139
+ `;
140
+
141
+ exports[`generateStorageCode > Google Drive > with service account credentials from a file path 1`] = `
142
+ "from gdrive_fsspec import GoogleDriveFileSystem
143
+
144
+ fs = GoogleDriveFileSystem(creds="/etc/secrets/gdrive.json", token="service_account", use_listings_cache=False, skip_instance_cache=True)"
145
+ `;
146
+
147
+ exports[`generateStorageCode > Google Drive > with service account credentials from secrets 1`] = `
148
+ "from gdrive_fsspec import GoogleDriveFileSystem
149
+ import json
150
+ import os
151
+
152
+ _credentials_json = os.environ.get("GDRIVE_CREDENTIALS_JSON")
153
+ _creds = json.loads(_credentials_json)
103
154
  fs = GoogleDriveFileSystem(creds=_creds, token="service_account", use_listings_cache=False, skip_instance_cache=True)"
104
155
  `;
105
156
 
@@ -119,6 +170,29 @@ exports[`generateStorageCode > S3 > minimal connection (bucket only) 1`] = `
119
170
  store = S3Store("my-bucket",)"
120
171
  `;
121
172
 
173
+ exports[`generateStorageCode > S3 > with container credentials 1`] = `
174
+ "from obstore.store import S3Store
175
+
176
+ store = S3Store("my-bucket",
177
+ region="us-east-1",
178
+ container_credentials_full_uri="http://169.254.170.23/v1/credentials",
179
+ container_authorization_token_file="/var/run/secrets/token",
180
+ )"
181
+ `;
182
+
183
+ exports[`generateStorageCode > S3 > with container credentials from secrets 1`] = `
184
+ "from obstore.store import S3Store
185
+ import os
186
+
187
+ _container_credentials_full_uri = os.environ.get("AWS_CONTAINER_CREDENTIALS_FULL_URI")
188
+ _container_authorization_token_file = os.environ.get("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE")
189
+ store = S3Store("my-bucket",
190
+ region="us-east-1",
191
+ container_credentials_full_uri=_container_credentials_full_uri,
192
+ container_authorization_token_file=_container_authorization_token_file,
193
+ )"
194
+ `;
195
+
122
196
  exports[`generateStorageCode > S3 > with custom endpoint 1`] = `
123
197
  "from obstore.store import S3Store
124
198
 
@@ -130,6 +204,18 @@ store = S3Store("my-bucket",
130
204
  )"
131
205
  `;
132
206
 
207
+ exports[`generateStorageCode > S3 > with plain-http endpoint 1`] = `
208
+ "from obstore.store import S3Store
209
+
210
+ store = S3Store("my-bucket",
211
+ region="us-east-1",
212
+ access_key_id="AKIAIOSFODNN7EXAMPLE",
213
+ secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
214
+ endpoint_url="http://cwlota.com",
215
+ allow_http=True,
216
+ )"
217
+ `;
218
+
133
219
  exports[`generateStorageCode > S3 > with secrets 1`] = `
134
220
  "from obstore.store import S3Store
135
221
  import os