@marimo-team/islands 0.20.3-dev92 → 0.20.3-dev96

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 (29) hide show
  1. package/dist/main.js +8 -1
  2. package/dist/style.css +1 -1
  3. package/package.json +1 -1
  4. package/src/components/databases/icons/google-drive.svg +8 -0
  5. package/src/components/datasources/datasources.tsx +5 -5
  6. package/src/components/editor/actions/useNotebookActions.tsx +15 -2
  7. package/src/components/editor/connections/add-connection-dialog.tsx +83 -0
  8. package/src/components/editor/connections/components.tsx +177 -0
  9. package/src/components/editor/{database → connections/database}/__tests__/as-code.test.ts +1 -1
  10. package/src/components/editor/connections/database/add-database-form.tsx +303 -0
  11. package/src/components/editor/{database → connections/database}/as-code.ts +1 -1
  12. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +100 -0
  13. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +166 -0
  14. package/src/components/editor/connections/storage/add-storage-form.tsx +135 -0
  15. package/src/components/editor/connections/storage/as-code.ts +188 -0
  16. package/src/components/editor/connections/storage/schemas.ts +141 -0
  17. package/src/components/storage/components.tsx +9 -3
  18. package/src/components/storage/storage-inspector.tsx +20 -1
  19. package/src/core/cells/__tests__/session.test.ts +1 -1
  20. package/src/core/codemirror/__tests__/format.test.ts +9 -1
  21. package/src/core/storage/types.ts +1 -0
  22. package/src/plugins/core/__test__/sanitize.test.ts +47 -2
  23. package/src/plugins/core/sanitize.ts +4 -0
  24. package/src/components/editor/database/add-database-form.tsx +0 -420
  25. /package/src/components/editor/{database → connections}/__tests__/secrets.test.ts +0 -0
  26. /package/src/components/editor/{database → connections/database}/__tests__/__snapshots__/as-code.test.ts.snap +0 -0
  27. /package/src/components/editor/{database → connections/database}/schemas.ts +0 -0
  28. /package/src/components/editor/{database → connections}/form-renderers.tsx +0 -0
  29. /package/src/components/editor/{database → connections}/secrets.ts +0 -0
@@ -0,0 +1,141 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { z } from "zod";
3
+ import { FieldOptions } from "@/components/forms/options";
4
+
5
+ export const S3StorageSchema = z
6
+ .object({
7
+ type: z.literal("s3"),
8
+ bucket: z
9
+ .string()
10
+ .nonempty()
11
+ .describe(
12
+ FieldOptions.of({
13
+ label: "Bucket",
14
+ placeholder: "my-bucket",
15
+ }),
16
+ ),
17
+ region: z
18
+ .string()
19
+ .optional()
20
+ .describe(
21
+ FieldOptions.of({
22
+ label: "Region",
23
+ placeholder: "us-east-1",
24
+ optionRegex: ".*region.*",
25
+ }),
26
+ ),
27
+ access_key_id: z
28
+ .string()
29
+ .optional()
30
+ .describe(
31
+ FieldOptions.of({
32
+ label: "Access Key ID",
33
+ inputType: "password",
34
+ optionRegex: ".*access_key.*",
35
+ }),
36
+ ),
37
+ secret_access_key: z
38
+ .string()
39
+ .optional()
40
+ .describe(
41
+ FieldOptions.of({
42
+ label: "Secret Access Key",
43
+ inputType: "password",
44
+ optionRegex: ".*secret.*access.*",
45
+ }),
46
+ ),
47
+ endpoint_url: z
48
+ .string()
49
+ .optional()
50
+ .describe(
51
+ FieldOptions.of({
52
+ label: "Endpoint URL",
53
+ placeholder: "https://s3.amazonaws.com",
54
+ }),
55
+ ),
56
+ })
57
+ .describe(FieldOptions.of({ direction: "two-columns" }));
58
+
59
+ export const GCSStorageSchema = z
60
+ .object({
61
+ type: z.literal("gcs"),
62
+ bucket: z
63
+ .string()
64
+ .nonempty()
65
+ .describe(
66
+ FieldOptions.of({
67
+ label: "Bucket",
68
+ placeholder: "my-bucket",
69
+ }),
70
+ ),
71
+ service_account_key: z
72
+ .string()
73
+ .optional()
74
+ .describe(
75
+ FieldOptions.of({
76
+ label: "Service Account Key (JSON)",
77
+ inputType: "textarea",
78
+ }),
79
+ ),
80
+ })
81
+ .describe(FieldOptions.of({ direction: "two-columns" }));
82
+
83
+ export const AzureStorageSchema = z
84
+ .object({
85
+ type: z.literal("azure"),
86
+ container: z
87
+ .string()
88
+ .nonempty()
89
+ .describe(
90
+ FieldOptions.of({
91
+ label: "Container",
92
+ placeholder: "my-container",
93
+ }),
94
+ ),
95
+ account_name: z
96
+ .string()
97
+ .nonempty()
98
+ .describe(
99
+ FieldOptions.of({
100
+ label: "Account Name",
101
+ placeholder: "storageaccount",
102
+ optionRegex: ".*account.*",
103
+ }),
104
+ ),
105
+ account_key: z
106
+ .string()
107
+ .optional()
108
+ .describe(
109
+ FieldOptions.of({
110
+ label: "Account Key",
111
+ inputType: "password",
112
+ optionRegex: ".*azure.*key.*",
113
+ }),
114
+ ),
115
+ })
116
+ .describe(FieldOptions.of({ direction: "two-columns" }));
117
+
118
+ export const GoogleDriveStorageSchema = z
119
+ .object({
120
+ type: z.literal("gdrive"),
121
+ credentials_json: z
122
+ .string()
123
+ .optional()
124
+ .describe(
125
+ FieldOptions.of({
126
+ label: "Service Account JSON",
127
+ description: "Leave empty to use browser-based authentication",
128
+ inputType: "textarea",
129
+ }),
130
+ ),
131
+ })
132
+ .describe(FieldOptions.of({ direction: "two-columns" }));
133
+
134
+ export const StorageConnectionSchema = z.discriminatedUnion("type", [
135
+ S3StorageSchema,
136
+ GCSStorageSchema,
137
+ AzureStorageSchema,
138
+ GoogleDriveStorageSchema,
139
+ ]);
140
+
141
+ export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
@@ -19,8 +19,10 @@ import {
19
19
  ImageIcon,
20
20
  } from "lucide-react";
21
21
  import GoogleCloudIcon from "@/components/databases/icons/google-cloud-storage.svg?inline";
22
+ import GoogleDriveIcon from "@/components/databases/icons/google-drive.svg?inline";
22
23
  import type { KnownStorageProtocol } from "@/core/storage/types";
23
24
  import { useTheme } from "@/theme/useTheme";
25
+ import { cn } from "@/utils/cn";
24
26
 
25
27
  export function renderFileIcon(name: string): React.ReactNode {
26
28
  const ext = name.split(".").pop()?.toLowerCase();
@@ -67,12 +69,14 @@ const PROTOCOL_ICONS: Record<KnownStorageProtocol, IconEntry> = {
67
69
  http: GlobeIcon,
68
70
  file: HardDriveIcon,
69
71
  "in-memory": DatabaseZapIcon,
72
+ gdrive: { src: GoogleDriveIcon },
70
73
  github: GithubIcon,
71
74
  };
72
75
 
73
76
  export const ProtocolIcon: React.FC<{
74
77
  protocol: KnownStorageProtocol | (string & {});
75
- }> = ({ protocol }) => {
78
+ className?: string;
79
+ }> = ({ protocol, className }) => {
76
80
  const { theme } = useTheme();
77
81
  const entry =
78
82
  PROTOCOL_ICONS[protocol.toLowerCase() as KnownStorageProtocol] ??
@@ -80,9 +84,11 @@ export const ProtocolIcon: React.FC<{
80
84
 
81
85
  if ("src" in entry) {
82
86
  const src = theme === "dark" && entry.dark ? entry.dark : entry.src;
83
- return <img src={src} alt={protocol} className="h-3.5 w-3.5" />;
87
+ return (
88
+ <img src={src} alt={protocol} className={cn("h-3.5 w-3.5", className)} />
89
+ );
84
90
  }
85
91
 
86
92
  const Icon = entry;
87
- return <Icon className="h-3.5 w-3.5" />;
93
+ return <Icon className={cn("h-3.5 w-3.5", className)} />;
88
94
  };
@@ -10,6 +10,7 @@ import {
10
10
  HelpCircleIcon,
11
11
  LoaderCircle,
12
12
  MoreVerticalIcon,
13
+ PlusIcon,
13
14
  RefreshCwIcon,
14
15
  ViewIcon,
15
16
  XIcon,
@@ -18,6 +19,7 @@ import React, { useCallback, useState } from "react";
18
19
  import { useLocale } from "react-aria";
19
20
  import { EngineVariable } from "@/components/databases/engine-variable";
20
21
  import { PanelEmptyState } from "@/components/editor/chrome/panels/empty-state";
22
+ import { AddConnectionDialog } from "@/components/editor/connections/add-connection-dialog";
21
23
  import { Command, CommandInput, CommandItem } from "@/components/ui/command";
22
24
  import {
23
25
  DropdownMenu,
@@ -550,6 +552,14 @@ export const StorageInspector: React.FC = () => {
550
552
  .
551
553
  </span>
552
554
  }
555
+ action={
556
+ <AddConnectionDialog defaultTab="storage">
557
+ <Button variant="outline" size="sm">
558
+ Add remote storage
559
+ <PlusIcon className="h-4 w-4 ml-2" />
560
+ </Button>
561
+ </AddConnectionDialog>
562
+ }
553
563
  icon={<HardDriveIcon className="h-8 w-8" />}
554
564
  />
555
565
  );
@@ -595,8 +605,17 @@ export const StorageInspector: React.FC = () => {
595
605
  content="Filters loaded entries only. Expand directories to include their contents in the search."
596
606
  delayDuration={200}
597
607
  >
598
- <HelpCircleIcon className="h-3.5 w-3.5 mr-2 shrink-0 cursor-help text-muted-foreground hover:text-foreground" />
608
+ <HelpCircleIcon className="h-3.5 w-3.5 shrink-0 cursor-help text-muted-foreground hover:text-foreground mr-2" />
599
609
  </Tooltip>
610
+ <AddConnectionDialog defaultTab="storage">
611
+ <Button
612
+ variant="ghost"
613
+ size="sm"
614
+ className="px-2 border-0 border-l border-muted-background rounded-none focus-visible:ring-0 focus-visible:ring-offset-0"
615
+ >
616
+ <PlusIcon className="h-4 w-4" />
617
+ </Button>
618
+ </AddConnectionDialog>
600
619
  </div>
601
620
  <CommandList className="flex flex-col">
602
621
  {namespaces.map((ns) => (
@@ -70,7 +70,7 @@ describe("notebookStateFromSession", () => {
70
70
  cells: SessionCell[],
71
71
  ): api.Session["NotebookSessionV1"] => ({
72
72
  version: "1",
73
- metadata: { marimo_version: "1" },
73
+ metadata: { marimo_version: "1", script_metadata_hash: null },
74
74
  cells,
75
75
  });
76
76
 
@@ -4,7 +4,7 @@ import { python } from "@codemirror/lang-python";
4
4
  import { EditorState } from "@codemirror/state";
5
5
  import { EditorView } from "@codemirror/view";
6
6
  import { atom } from "jotai";
7
- import { beforeEach, describe, expect, it, vi } from "vitest";
7
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
8
8
  import { MockRequestClient } from "@/__mocks__/requests";
9
9
  import type { NotebookState } from "@/core/cells/cells";
10
10
  import { getNotebook } from "@/core/cells/cells";
@@ -44,6 +44,7 @@ vi.mock("@/core/config/config", () => ({
44
44
  }));
45
45
 
46
46
  const updateCellCode = vi.fn();
47
+ const createdViews: EditorView[] = [];
47
48
 
48
49
  function createEditor(content: string, cellId: CellId) {
49
50
  const state = EditorState.create({
@@ -74,6 +75,7 @@ function createEditor(content: string, cellId: CellId) {
74
75
  parent: document.body,
75
76
  });
76
77
 
78
+ createdViews.push(view);
77
79
  return view;
78
80
  }
79
81
 
@@ -88,6 +90,12 @@ beforeEach(() => {
88
90
  store.set(requestClientAtom, mockRequestClient);
89
91
  });
90
92
 
93
+ afterEach(() => {
94
+ for (const view of createdViews) {
95
+ view.destroy();
96
+ }
97
+ });
98
+
91
99
  describe("format", () => {
92
100
  describe("formatEditorViews", () => {
93
101
  it("should format code in editor views", async () => {
@@ -54,5 +54,6 @@ export type KnownStorageProtocol =
54
54
  | "cloudflare"
55
55
  | "http"
56
56
  | "file"
57
+ | "gdrive"
57
58
  | "in-memory"
58
59
  | "github";
@@ -288,9 +288,54 @@ describe("sanitizeHtml", () => {
288
288
  expect(sanitizeHtml(html)).toMatchInlineSnapshot(`"<div>Text</div>"`);
289
289
  });
290
290
 
291
- test("removes use element from SVG", () => {
291
+ test("preserves use element in SVG", () => {
292
292
  const html = '<svg><use xlink:href="#icon"></use></svg>';
293
- expect(sanitizeHtml(html)).toMatchInlineSnapshot(`"<svg></svg>"`);
293
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
294
+ `"<svg><use xlink:href="#icon"></use></svg>"`,
295
+ );
296
+ });
297
+
298
+ test("preserves SVG defs and use pattern", () => {
299
+ const html = [
300
+ '<svg width="60" height="60">',
301
+ '<circle cx="30" cy="30" r="30" fill="orange"></circle>',
302
+ '<defs><circle id="myCircle" cx="0" cy="0" r="10" fill="green"></circle></defs>',
303
+ '<use href="#myCircle" x="20" y="20"></use>',
304
+ "</svg>",
305
+ ].join("");
306
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
307
+ `"<svg width="60" height="60"><circle cx="30" cy="30" r="30" fill="orange"></circle><defs><circle id="myCircle" cx="0" cy="0" r="10" fill="green"></circle></defs><use href="#myCircle" x="20" y="20"></use></svg>"`,
308
+ );
309
+ });
310
+
311
+ test("strips javascript: href from SVG use element", () => {
312
+ const html = '<svg><use href="javascript:alert(1)"></use></svg>';
313
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
314
+ `"<svg><use></use></svg>"`,
315
+ );
316
+ });
317
+
318
+ test("strips javascript: xlink:href from SVG use element", () => {
319
+ const html = '<svg><use xlink:href="javascript:alert(1)"></use></svg>';
320
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
321
+ `"<svg><use></use></svg>"`,
322
+ );
323
+ });
324
+
325
+ test("preserves external href on SVG use element", () => {
326
+ const html =
327
+ '<svg><use href="https://example.com/sprite.svg#icon"></use></svg>';
328
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
329
+ `"<svg><use href="https://example.com/sprite.svg#icon"></use></svg>"`,
330
+ );
331
+ });
332
+
333
+ test("preserves external xlink:href on SVG use element", () => {
334
+ const html =
335
+ '<svg><use xlink:href="https://example.com/sprite.svg#icon"></use></svg>';
336
+ expect(sanitizeHtml(html)).toMatchInlineSnapshot(
337
+ `"<svg><use xlink:href="https://example.com/sprite.svg#icon"></use></svg>"`,
338
+ );
294
339
  });
295
340
 
296
341
  test("removes javascript in SVG href", () => {
@@ -74,6 +74,10 @@ export function sanitizeHtml(html: string) {
74
74
  const sanitizationOptions: Config = {
75
75
  // Default to permit HTML, SVG and MathML, this limits to HTML only
76
76
  USE_PROFILES: { html: true, svg: true, mathMl: true },
77
+ // Allow SVG <use> elements and their href attributes, which are needed
78
+ // for SVGs that reference <defs> (e.g., Matplotlib SVG output).
79
+ ADD_TAGS: ["use"],
80
+ ADD_ATTR: ["href", "xlink:href"],
77
81
  // glue elements like style, script or others to document.body and prevent unintuitive browser behavior in several edge-cases
78
82
  FORCE_BODY: true,
79
83
  CUSTOM_ELEMENT_HANDLING: {