@marimo-team/islands 0.20.3-dev94 → 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 (26) hide show
  1. package/dist/main.js +1 -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/codemirror/__tests__/format.test.ts +9 -1
  20. package/src/core/storage/types.ts +1 -0
  21. package/src/components/editor/database/add-database-form.tsx +0 -420
  22. /package/src/components/editor/{database → connections}/__tests__/secrets.test.ts +0 -0
  23. /package/src/components/editor/{database → connections/database}/__tests__/__snapshots__/as-code.test.ts.snap +0 -0
  24. /package/src/components/editor/{database → connections/database}/schemas.ts +0 -0
  25. /package/src/components/editor/{database → connections}/form-renderers.tsx +0 -0
  26. /package/src/components/editor/{database → connections}/secrets.ts +0 -0
@@ -0,0 +1,100 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`generateStorageCode > Azure > basic connection with account key 1`] = `
4
+ "from obstore.store import AzureStore
5
+
6
+ store = AzureStore("my-container",
7
+ account_name="storageaccount",
8
+ account_key="base64accountkey==",
9
+ )"
10
+ `;
11
+
12
+ exports[`generateStorageCode > Azure > with secrets 1`] = `
13
+ "from obstore.store import AzureStore
14
+ import os
15
+
16
+ _account_name = os.environ.get("AZURE_ACCOUNT")
17
+ _account_key = os.environ.get("AZURE_KEY")
18
+ store = AzureStore("my-container",
19
+ account_name=_account_name,
20
+ account_key=_account_key,
21
+ )"
22
+ `;
23
+
24
+ exports[`generateStorageCode > Azure > without account key 1`] = `
25
+ "from obstore.store import AzureStore
26
+
27
+ store = AzureStore("my-container",
28
+ account_name="storageaccount",
29
+ )"
30
+ `;
31
+
32
+ exports[`generateStorageCode > GCS > with service account key 1`] = `
33
+ "from obstore.store import GCSStore
34
+ import json
35
+
36
+ _credentials = json.loads("""{"type": "service_account", "project_id": "test"}""")
37
+ store = GCSStore("my-bucket",
38
+ service_account_key=_credentials,
39
+ )"
40
+ `;
41
+
42
+ exports[`generateStorageCode > GCS > without service account key (default credentials) 1`] = `
43
+ "from obstore.store import GCSStore
44
+
45
+ store = GCSStore("my-bucket")"
46
+ `;
47
+
48
+ exports[`generateStorageCode > Google Drive > with browser auth (no credentials) 1`] = `
49
+ "from gdrive_fsspec import GoogleDriveFileSystem
50
+
51
+ fs = GoogleDriveFileSystem(token="browser")"
52
+ `;
53
+
54
+ exports[`generateStorageCode > Google Drive > with service account credentials 1`] = `
55
+ "from gdrive_fsspec import GoogleDriveFileSystem
56
+ import json
57
+
58
+ _creds = json.loads("""{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}""")
59
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account")"
60
+ `;
61
+
62
+ exports[`generateStorageCode > S3 > basic connection with all fields 1`] = `
63
+ "from obstore.store import S3Store
64
+
65
+ store = S3Store("my-bucket",
66
+ region="us-east-1",
67
+ access_key_id="AKIAIOSFODNN7EXAMPLE",
68
+ secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
69
+ )"
70
+ `;
71
+
72
+ exports[`generateStorageCode > S3 > minimal connection (bucket only) 1`] = `
73
+ "from obstore.store import S3Store
74
+
75
+ store = S3Store("my-bucket",)"
76
+ `;
77
+
78
+ exports[`generateStorageCode > S3 > with custom endpoint 1`] = `
79
+ "from obstore.store import S3Store
80
+
81
+ store = S3Store("my-bucket",
82
+ region="us-east-1",
83
+ access_key_id="AKIAIOSFODNN7EXAMPLE",
84
+ secret_access_key="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
85
+ endpoint_url="https://minio.example.com:9000",
86
+ )"
87
+ `;
88
+
89
+ exports[`generateStorageCode > S3 > with secrets 1`] = `
90
+ "from obstore.store import S3Store
91
+ import os
92
+
93
+ _access_key_id = os.environ.get("AWS_ACCESS_KEY_ID")
94
+ _secret_access_key = os.environ.get("AWS_SECRET_ACCESS_KEY")
95
+ store = S3Store("my-bucket",
96
+ region="us-east-1",
97
+ access_key_id=_access_key_id,
98
+ secret_access_key=_secret_access_key,
99
+ )"
100
+ `;
@@ -0,0 +1,166 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+ import { describe, expect, it } from "vitest";
3
+ import { prefixSecret } from "../../secrets";
4
+ import { generateStorageCode } from "../as-code";
5
+ import type { StorageConnection } from "../schemas";
6
+
7
+ describe("generateStorageCode", () => {
8
+ const baseS3: StorageConnection = {
9
+ type: "s3",
10
+ bucket: "my-bucket",
11
+ region: "us-east-1",
12
+ access_key_id: "AKIAIOSFODNN7EXAMPLE",
13
+ secret_access_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
14
+ endpoint_url: undefined,
15
+ };
16
+
17
+ const baseGCS: StorageConnection = {
18
+ type: "gcs",
19
+ bucket: "my-bucket",
20
+ service_account_key: '{"type": "service_account", "project_id": "test"}',
21
+ };
22
+
23
+ const baseAzure: StorageConnection = {
24
+ type: "azure",
25
+ container: "my-container",
26
+ account_name: "storageaccount",
27
+ account_key: "base64accountkey==",
28
+ };
29
+
30
+ const baseGDrive: StorageConnection = {
31
+ type: "gdrive",
32
+ credentials_json:
33
+ '{"type": "service_account", "client_email": "test@test.iam.gserviceaccount.com"}',
34
+ };
35
+
36
+ describe("S3", () => {
37
+ it("basic connection with all fields", () => {
38
+ expect(generateStorageCode(baseS3, "obstore")).toMatchSnapshot();
39
+ });
40
+
41
+ it("minimal connection (bucket only)", () => {
42
+ const conn: StorageConnection = {
43
+ type: "s3",
44
+ bucket: "my-bucket",
45
+ };
46
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
47
+ });
48
+
49
+ it("with custom endpoint", () => {
50
+ const conn: StorageConnection = {
51
+ ...baseS3,
52
+ endpoint_url: "https://minio.example.com:9000",
53
+ };
54
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
55
+ });
56
+
57
+ it("with secrets", () => {
58
+ const conn: StorageConnection = {
59
+ type: "s3",
60
+ bucket: "my-bucket",
61
+ region: "us-east-1",
62
+ access_key_id: prefixSecret("AWS_ACCESS_KEY_ID"),
63
+ secret_access_key: prefixSecret("AWS_SECRET_ACCESS_KEY"),
64
+ };
65
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
66
+ });
67
+ });
68
+
69
+ describe("GCS", () => {
70
+ it("with service account key", () => {
71
+ expect(generateStorageCode(baseGCS, "obstore")).toMatchSnapshot();
72
+ });
73
+
74
+ it("without service account key (default credentials)", () => {
75
+ const conn: StorageConnection = {
76
+ type: "gcs",
77
+ bucket: "my-bucket",
78
+ };
79
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
80
+ });
81
+ });
82
+
83
+ describe("Azure", () => {
84
+ it("basic connection with account key", () => {
85
+ expect(generateStorageCode(baseAzure, "obstore")).toMatchSnapshot();
86
+ });
87
+
88
+ it("without account key", () => {
89
+ const conn: StorageConnection = {
90
+ type: "azure",
91
+ container: "my-container",
92
+ account_name: "storageaccount",
93
+ };
94
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
95
+ });
96
+
97
+ it("with secrets", () => {
98
+ const conn: StorageConnection = {
99
+ type: "azure",
100
+ container: "my-container",
101
+ account_name: prefixSecret("AZURE_ACCOUNT"),
102
+ account_key: prefixSecret("AZURE_KEY"),
103
+ };
104
+ expect(generateStorageCode(conn, "obstore")).toMatchSnapshot();
105
+ });
106
+ });
107
+
108
+ describe("Google Drive", () => {
109
+ it("with service account credentials", () => {
110
+ expect(generateStorageCode(baseGDrive, "fsspec")).toMatchSnapshot();
111
+ });
112
+
113
+ it("with browser auth (no credentials)", () => {
114
+ const conn: StorageConnection = {
115
+ type: "gdrive",
116
+ };
117
+ expect(generateStorageCode(conn, "fsspec")).toMatchSnapshot();
118
+ });
119
+ });
120
+
121
+ describe("invalid cases", () => {
122
+ it("throws for empty S3 bucket", () => {
123
+ expect(() =>
124
+ generateStorageCode(
125
+ { type: "s3", bucket: "" } as StorageConnection,
126
+ "obstore",
127
+ ),
128
+ ).toThrow();
129
+ });
130
+
131
+ it("throws for empty GCS bucket", () => {
132
+ expect(() =>
133
+ generateStorageCode(
134
+ { type: "gcs", bucket: "" } as StorageConnection,
135
+ "obstore",
136
+ ),
137
+ ).toThrow();
138
+ });
139
+
140
+ it("throws for empty Azure container", () => {
141
+ expect(() =>
142
+ generateStorageCode(
143
+ {
144
+ type: "azure",
145
+ container: "",
146
+ account_name: "acct",
147
+ } as StorageConnection,
148
+ "obstore",
149
+ ),
150
+ ).toThrow();
151
+ });
152
+
153
+ it("throws for empty Azure account name", () => {
154
+ expect(() =>
155
+ generateStorageCode(
156
+ {
157
+ type: "azure",
158
+ container: "my-container",
159
+ account_name: "",
160
+ } as StorageConnection,
161
+ "obstore",
162
+ ),
163
+ ).toThrow();
164
+ });
165
+ });
166
+ });
@@ -0,0 +1,135 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useState } from "react";
4
+ import type { FieldValues } from "react-hook-form";
5
+ import type { z } from "zod";
6
+ import { ProtocolIcon } from "@/components/storage/components";
7
+ import { ConnectionForm, SelectorButton, SelectorGrid } from "../components";
8
+ import {
9
+ generateStorageCode,
10
+ type StorageLibrary,
11
+ StorageLibraryDisplayNames,
12
+ } from "./as-code";
13
+ import {
14
+ AzureStorageSchema,
15
+ GCSStorageSchema,
16
+ GoogleDriveStorageSchema,
17
+ S3StorageSchema,
18
+ type StorageConnection,
19
+ } from "./schemas";
20
+
21
+ interface StorageProviderSchema {
22
+ name: string;
23
+ schema: z.ZodType<StorageConnection, FieldValues>;
24
+ color: string;
25
+ protocol: string;
26
+ storageLibraries: {
27
+ libraries: StorageLibrary[];
28
+ preferred: StorageLibrary;
29
+ };
30
+ }
31
+
32
+ const STORAGE_PROVIDERS = [
33
+ {
34
+ name: "Amazon S3",
35
+ schema: S3StorageSchema,
36
+ color: "#232F3E",
37
+ protocol: "s3",
38
+ storageLibraries: {
39
+ libraries: ["obstore"],
40
+ preferred: "obstore",
41
+ },
42
+ },
43
+ {
44
+ name: "Google Cloud Storage",
45
+ schema: GCSStorageSchema,
46
+ color: "#4285F4",
47
+ protocol: "gcs",
48
+ storageLibraries: {
49
+ libraries: ["obstore"],
50
+ preferred: "obstore",
51
+ },
52
+ },
53
+ {
54
+ name: "Azure Blob Storage",
55
+ schema: AzureStorageSchema,
56
+ color: "#0062AD",
57
+ protocol: "azure",
58
+ storageLibraries: {
59
+ libraries: ["obstore"],
60
+ preferred: "obstore",
61
+ },
62
+ },
63
+ {
64
+ name: "Google Drive",
65
+ schema: GoogleDriveStorageSchema,
66
+ color: "#177834",
67
+ protocol: "gdrive",
68
+ storageLibraries: {
69
+ libraries: ["fsspec"],
70
+ preferred: "fsspec",
71
+ },
72
+ },
73
+ ] satisfies StorageProviderSchema[];
74
+
75
+ const StorageProviderSelector: React.FC<{
76
+ onSelect: (schema: z.ZodType<StorageConnection, FieldValues>) => void;
77
+ }> = ({ onSelect }) => {
78
+ return (
79
+ <SelectorGrid>
80
+ {STORAGE_PROVIDERS.map(({ name, schema, color, protocol }) => (
81
+ <SelectorButton
82
+ key={name}
83
+ name={name}
84
+ color={color}
85
+ icon={
86
+ <span className="w-8 h-8 flex items-center justify-center">
87
+ <ProtocolIcon
88
+ protocol={protocol}
89
+ className="w-7 h-7 brightness-0 invert"
90
+ />
91
+ </span>
92
+ }
93
+ onSelect={() => onSelect(schema)}
94
+ />
95
+ ))}
96
+ </SelectorGrid>
97
+ );
98
+ };
99
+
100
+ export const AddStorageForm: React.FC<{
101
+ onSubmit: () => void;
102
+ header?: React.ReactNode;
103
+ }> = ({ onSubmit, header }) => {
104
+ const [selectedSchema, setSelectedSchema] = useState<z.ZodType<
105
+ StorageConnection,
106
+ FieldValues
107
+ > | null>(null);
108
+
109
+ if (!selectedSchema) {
110
+ return (
111
+ <>
112
+ {header}
113
+ <div>
114
+ <StorageProviderSelector onSelect={setSelectedSchema} />
115
+ </div>
116
+ </>
117
+ );
118
+ }
119
+
120
+ const provider = STORAGE_PROVIDERS.find((p) => p.schema === selectedSchema);
121
+ const libs = provider?.storageLibraries;
122
+
123
+ return (
124
+ <ConnectionForm<StorageConnection, StorageLibrary>
125
+ schema={selectedSchema}
126
+ libraries={libs?.libraries ?? []}
127
+ preferredLibrary={libs?.preferred ?? "obstore"}
128
+ displayNames={StorageLibraryDisplayNames}
129
+ libraryLabel="Preferred storage library"
130
+ generateCode={(values, library) => generateStorageCode(values, library)}
131
+ onSubmit={onSubmit}
132
+ onBack={() => setSelectedSchema(null)}
133
+ />
134
+ );
135
+ };
@@ -0,0 +1,188 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import dedent from "string-dedent";
4
+ import { assertNever } from "@/utils/assertNever";
5
+ import { isSecret, unprefixSecret } from "../secrets";
6
+ import { type StorageConnection, StorageConnectionSchema } from "./schemas";
7
+
8
+ export type StorageLibrary = "obstore" | "fsspec";
9
+
10
+ export const StorageLibraryDisplayNames: Record<StorageLibrary, string> = {
11
+ obstore: "Obstore",
12
+ fsspec: "fsspec",
13
+ };
14
+
15
+ class SecretContainer {
16
+ private secrets: Record<string, string> = {};
17
+
18
+ get imports(): Set<string> {
19
+ if (Object.keys(this.secrets).length === 0) {
20
+ return new Set<string>();
21
+ }
22
+ return new Set<string>(["import os"]);
23
+ }
24
+
25
+ print(varName: string, value: string | undefined): string {
26
+ if (value === undefined || value === "") {
27
+ return "";
28
+ }
29
+ const privateVar = `_${varName}`;
30
+ if (isSecret(value)) {
31
+ const envVar = unprefixSecret(value);
32
+ this.secrets[privateVar] = `os.environ.get("${envVar}")`;
33
+ return privateVar;
34
+ }
35
+ return `"${value}"`;
36
+ }
37
+
38
+ formatSecrets(): string {
39
+ if (Object.keys(this.secrets).length === 0) {
40
+ return "";
41
+ }
42
+ return Object.entries(this.secrets)
43
+ .map(([k, v]) => `${k} = ${v}`)
44
+ .join("\n");
45
+ }
46
+ }
47
+
48
+ function generateS3Code(
49
+ connection: Extract<StorageConnection, { type: "s3" }>,
50
+ secrets: SecretContainer,
51
+ ): { imports: Set<string>; code: string } {
52
+ const bucket = secrets.print("bucket", connection.bucket);
53
+ const imports = new Set(["from obstore.store import S3Store"]);
54
+ const params: string[] = [];
55
+
56
+ if (connection.region) {
57
+ params.push(` region=${secrets.print("region", connection.region)},`);
58
+ }
59
+ if (connection.access_key_id) {
60
+ params.push(
61
+ ` access_key_id=${secrets.print("access_key_id", connection.access_key_id)},`,
62
+ );
63
+ }
64
+ if (connection.secret_access_key) {
65
+ params.push(
66
+ ` secret_access_key=${secrets.print("secret_access_key", connection.secret_access_key)},`,
67
+ );
68
+ }
69
+ if (connection.endpoint_url) {
70
+ params.push(
71
+ ` endpoint_url=${secrets.print("endpoint_url", connection.endpoint_url)},`,
72
+ );
73
+ }
74
+
75
+ const paramsStr = params.length > 0 ? `\n${params.join("\n")}\n` : "";
76
+
77
+ const code = dedent(`
78
+ store = S3Store(${bucket},${paramsStr})
79
+ `);
80
+ return { imports, code };
81
+ }
82
+
83
+ function generateGCSCode(
84
+ connection: Extract<StorageConnection, { type: "gcs" }>,
85
+ secrets: SecretContainer,
86
+ ): { imports: Set<string>; code: string } {
87
+ const bucket = secrets.print("bucket", connection.bucket);
88
+ const imports = new Set(["from obstore.store import GCSStore"]);
89
+
90
+ let code: string;
91
+ if (connection.service_account_key) {
92
+ imports.add("import json");
93
+ code = dedent(`
94
+ _credentials = json.loads("""${connection.service_account_key}""")
95
+ store = GCSStore(${bucket},
96
+ service_account_key=_credentials,
97
+ )
98
+ `);
99
+ } else {
100
+ code = dedent(`
101
+ store = GCSStore(${bucket})
102
+ `);
103
+ }
104
+ return { imports, code };
105
+ }
106
+
107
+ function generateAzureCode(
108
+ connection: Extract<StorageConnection, { type: "azure" }>,
109
+ secrets: SecretContainer,
110
+ ): { imports: Set<string>; code: string } {
111
+ const container = secrets.print("container", connection.container);
112
+ const accountName = secrets.print("account_name", connection.account_name);
113
+ const imports = new Set(["from obstore.store import AzureStore"]);
114
+ const params: string[] = [`account_name=${accountName},`];
115
+
116
+ if (connection.account_key) {
117
+ params.push(
118
+ `account_key=${secrets.print("account_key", connection.account_key)},`,
119
+ );
120
+ }
121
+
122
+ const paramsStr = params.map((p) => ` ${p}`).join("\n");
123
+
124
+ const code = `store = AzureStore(${container},\n${paramsStr}\n)`;
125
+ return { imports, code };
126
+ }
127
+
128
+ function generateGDriveCode(
129
+ connection: Extract<StorageConnection, { type: "gdrive" }>,
130
+ secrets: SecretContainer,
131
+ ): { imports: Set<string>; code: string } {
132
+ const imports = new Set(["from gdrive_fsspec import GoogleDriveFileSystem"]);
133
+
134
+ if (connection.credentials_json) {
135
+ imports.add("import json");
136
+ const creds = secrets.print(
137
+ "credentials_json",
138
+ connection.credentials_json,
139
+ );
140
+ const code = dedent(`
141
+ _creds = json.loads("""${connection.credentials_json?.startsWith("ENV:") ? `{${creds}}` : connection.credentials_json}""")
142
+ fs = GoogleDriveFileSystem(creds=_creds, token="service_account")
143
+ `);
144
+ return { imports, code };
145
+ }
146
+
147
+ const code = dedent(`
148
+ fs = GoogleDriveFileSystem(token="browser")
149
+ `);
150
+ return { imports, code };
151
+ }
152
+
153
+ export function generateStorageCode(
154
+ connection: StorageConnection,
155
+ _library: StorageLibrary,
156
+ ): string {
157
+ StorageConnectionSchema.parse(connection);
158
+
159
+ const secrets = new SecretContainer();
160
+ let result: { imports: Set<string>; code: string };
161
+
162
+ switch (connection.type) {
163
+ case "s3":
164
+ result = generateS3Code(connection, secrets);
165
+ break;
166
+ case "gcs":
167
+ result = generateGCSCode(connection, secrets);
168
+ break;
169
+ case "azure":
170
+ result = generateAzureCode(connection, secrets);
171
+ break;
172
+ case "gdrive":
173
+ result = generateGDriveCode(connection, secrets);
174
+ break;
175
+ default:
176
+ assertNever(connection);
177
+ }
178
+
179
+ const allImports = new Set([...secrets.imports, ...result.imports]);
180
+ const lines = [...allImports].sort();
181
+ lines.push("");
182
+ const secretsStr = secrets.formatSecrets();
183
+ if (secretsStr) {
184
+ lines.push(secretsStr);
185
+ }
186
+ lines.push(result.code.trim());
187
+ return lines.join("\n");
188
+ }