@marimo-team/islands 0.20.3-dev94 → 0.20.3-dev97

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 +2 -2
  2. package/dist/style.css +1 -1
  3. package/package.json +1 -1
  4. package/src/components/chat/acp/agent-panel.tsx +29 -8
  5. package/src/components/databases/icons/google-drive.svg +8 -0
  6. package/src/components/datasources/datasources.tsx +5 -5
  7. package/src/components/editor/actions/useNotebookActions.tsx +15 -2
  8. package/src/components/editor/connections/add-connection-dialog.tsx +83 -0
  9. package/src/components/editor/connections/components.tsx +177 -0
  10. package/src/components/editor/{database → connections/database}/__tests__/as-code.test.ts +1 -1
  11. package/src/components/editor/connections/database/add-database-form.tsx +303 -0
  12. package/src/components/editor/{database → connections/database}/as-code.ts +1 -1
  13. package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +100 -0
  14. package/src/components/editor/connections/storage/__tests__/as-code.test.ts +166 -0
  15. package/src/components/editor/connections/storage/add-storage-form.tsx +135 -0
  16. package/src/components/editor/connections/storage/as-code.ts +188 -0
  17. package/src/components/editor/connections/storage/schemas.ts +141 -0
  18. package/src/components/storage/components.tsx +9 -3
  19. package/src/components/storage/storage-inspector.tsx +20 -1
  20. package/src/core/codemirror/__tests__/format.test.ts +9 -1
  21. package/src/core/saving/file-state.ts +7 -0
  22. package/src/core/storage/types.ts +1 -0
  23. package/src/mount.tsx +6 -1
  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,303 @@
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 { DatabaseLogo, type DBLogoName } from "@/components/databases/icon";
7
+ import { ConnectionForm, SelectorButton, SelectorGrid } from "../components";
8
+ import {
9
+ ConnectionDisplayNames,
10
+ type ConnectionLibrary,
11
+ generateDatabaseCode,
12
+ } from "./as-code";
13
+ import {
14
+ BigQueryConnectionSchema,
15
+ ChdbConnectionSchema,
16
+ ClickhouseConnectionSchema,
17
+ type DatabaseConnection,
18
+ DatabricksConnectionSchema,
19
+ DataFusionConnectionSchema,
20
+ DuckDBConnectionSchema,
21
+ IcebergConnectionSchema,
22
+ MotherDuckConnectionSchema,
23
+ MySQLConnectionSchema,
24
+ PostgresConnectionSchema,
25
+ PySparkConnectionSchema,
26
+ RedshiftConnectionSchema,
27
+ SnowflakeConnectionSchema,
28
+ SQLiteConnectionSchema,
29
+ SupabaseConnectionSchema,
30
+ TimeplusConnectionSchema,
31
+ TrinoConnectionSchema,
32
+ } from "./schemas";
33
+
34
+ interface ConnectionSchema {
35
+ name: string;
36
+ schema: z.ZodType<DatabaseConnection, FieldValues>;
37
+ color: string;
38
+ logo: DBLogoName;
39
+ connectionLibraries: {
40
+ libraries: ConnectionLibrary[];
41
+ preferred: ConnectionLibrary;
42
+ };
43
+ }
44
+
45
+ const DATABASES = [
46
+ {
47
+ name: "PostgreSQL",
48
+ schema: PostgresConnectionSchema,
49
+ color: "#336791",
50
+ logo: "postgres",
51
+ connectionLibraries: {
52
+ libraries: ["sqlalchemy", "sqlmodel"],
53
+ preferred: "sqlalchemy",
54
+ },
55
+ },
56
+ {
57
+ name: "MySQL",
58
+ schema: MySQLConnectionSchema,
59
+ color: "#00758F",
60
+ logo: "mysql",
61
+ connectionLibraries: {
62
+ libraries: ["sqlalchemy", "sqlmodel"],
63
+ preferred: "sqlalchemy",
64
+ },
65
+ },
66
+ {
67
+ name: "SQLite",
68
+ schema: SQLiteConnectionSchema,
69
+ color: "#003B57",
70
+ logo: "sqlite",
71
+ connectionLibraries: {
72
+ libraries: ["sqlalchemy", "sqlmodel"],
73
+ preferred: "sqlalchemy",
74
+ },
75
+ },
76
+ {
77
+ name: "DuckDB",
78
+ schema: DuckDBConnectionSchema,
79
+ color: "#FFD700",
80
+ logo: "duckdb",
81
+ connectionLibraries: {
82
+ libraries: ["duckdb"],
83
+ preferred: "duckdb",
84
+ },
85
+ },
86
+ {
87
+ name: "MotherDuck",
88
+ schema: MotherDuckConnectionSchema,
89
+ color: "#ff9538",
90
+ logo: "motherduck",
91
+ connectionLibraries: {
92
+ libraries: ["duckdb"],
93
+ preferred: "duckdb",
94
+ },
95
+ },
96
+ {
97
+ name: "Snowflake",
98
+ schema: SnowflakeConnectionSchema,
99
+ color: "#29B5E8",
100
+ logo: "snowflake",
101
+ connectionLibraries: {
102
+ libraries: ["sqlalchemy", "sqlmodel"],
103
+ preferred: "sqlalchemy",
104
+ },
105
+ },
106
+ {
107
+ name: "ClickHouse",
108
+ schema: ClickhouseConnectionSchema,
109
+ color: "#2C2C1D",
110
+ logo: "clickhouse",
111
+ connectionLibraries: {
112
+ libraries: ["clickhouse_connect"],
113
+ preferred: "clickhouse_connect",
114
+ },
115
+ },
116
+ {
117
+ name: "Timeplus",
118
+ schema: TimeplusConnectionSchema,
119
+ color: "#B83280",
120
+ logo: "timeplus",
121
+ connectionLibraries: {
122
+ libraries: ["sqlalchemy", "sqlmodel"],
123
+ preferred: "sqlalchemy",
124
+ },
125
+ },
126
+ {
127
+ name: "BigQuery",
128
+ schema: BigQueryConnectionSchema,
129
+ color: "#4285F4",
130
+ logo: "bigquery",
131
+ connectionLibraries: {
132
+ libraries: ["sqlalchemy", "sqlmodel"],
133
+ preferred: "sqlalchemy",
134
+ },
135
+ },
136
+ {
137
+ name: "ClickHouse Embedded",
138
+ schema: ChdbConnectionSchema,
139
+ color: "#f2b611",
140
+ logo: "clickhouse",
141
+ connectionLibraries: {
142
+ libraries: ["chdb"],
143
+ preferred: "chdb",
144
+ },
145
+ },
146
+ {
147
+ name: "Trino",
148
+ schema: TrinoConnectionSchema,
149
+ color: "#d466b6",
150
+ logo: "trino",
151
+ connectionLibraries: {
152
+ libraries: ["sqlalchemy", "sqlmodel"],
153
+ preferred: "sqlalchemy",
154
+ },
155
+ },
156
+ {
157
+ name: "DataFusion",
158
+ schema: DataFusionConnectionSchema,
159
+ color: "#202A37",
160
+ logo: "datafusion",
161
+ connectionLibraries: {
162
+ libraries: ["ibis"],
163
+ preferred: "ibis",
164
+ },
165
+ },
166
+ {
167
+ name: "PySpark",
168
+ schema: PySparkConnectionSchema,
169
+ color: "#1C5162",
170
+ logo: "pyspark",
171
+ connectionLibraries: {
172
+ libraries: ["ibis"],
173
+ preferred: "ibis",
174
+ },
175
+ },
176
+ {
177
+ name: "Redshift",
178
+ schema: RedshiftConnectionSchema,
179
+ color: "#522BAE",
180
+ logo: "redshift",
181
+ connectionLibraries: {
182
+ libraries: ["redshift"],
183
+ preferred: "redshift",
184
+ },
185
+ },
186
+ {
187
+ name: "Databricks",
188
+ schema: DatabricksConnectionSchema,
189
+ color: "#c41e0c",
190
+ logo: "databricks",
191
+ connectionLibraries: {
192
+ libraries: ["sqlalchemy", "sqlmodel", "ibis"],
193
+ preferred: "sqlalchemy",
194
+ },
195
+ },
196
+ {
197
+ name: "Supabase",
198
+ schema: SupabaseConnectionSchema,
199
+ color: "#238F5F",
200
+ logo: "supabase",
201
+ connectionLibraries: {
202
+ libraries: ["sqlalchemy", "sqlmodel"],
203
+ preferred: "sqlalchemy",
204
+ },
205
+ },
206
+ ] satisfies ConnectionSchema[];
207
+
208
+ const DATA_CATALOGS = [
209
+ {
210
+ name: "Iceberg",
211
+ schema: IcebergConnectionSchema,
212
+ color: "#000000",
213
+ logo: "iceberg",
214
+ connectionLibraries: {
215
+ libraries: ["pyiceberg"],
216
+ preferred: "pyiceberg",
217
+ },
218
+ },
219
+ ] satisfies ConnectionSchema[];
220
+
221
+ const ALL_ENTRIES = [...DATABASES, ...DATA_CATALOGS];
222
+
223
+ const DatabaseSchemaSelector: React.FC<{
224
+ onSelect: (schema: z.ZodType<DatabaseConnection, FieldValues>) => void;
225
+ }> = ({ onSelect }) => {
226
+ return (
227
+ <>
228
+ <SelectorGrid>
229
+ {DATABASES.map(({ name, schema, color, logo }) => (
230
+ <SelectorButton
231
+ key={name}
232
+ name={name}
233
+ color={color}
234
+ icon={
235
+ <DatabaseLogo
236
+ name={logo}
237
+ className="w-8 h-8 text-white brightness-0 invert dark:invert"
238
+ />
239
+ }
240
+ onSelect={() => onSelect(schema)}
241
+ />
242
+ ))}
243
+ </SelectorGrid>
244
+ <h4 className="font-semibold text-muted-foreground text-lg flex items-center gap-4 my-2">
245
+ Data Catalogs
246
+ <hr className="flex-1" />
247
+ </h4>
248
+ <SelectorGrid>
249
+ {DATA_CATALOGS.map(({ name, schema, color, logo }) => (
250
+ <SelectorButton
251
+ key={name}
252
+ name={name}
253
+ color={color}
254
+ icon={
255
+ <DatabaseLogo
256
+ name={logo}
257
+ className="w-8 h-8 text-white brightness-0 invert dark:invert"
258
+ />
259
+ }
260
+ onSelect={() => onSelect(schema)}
261
+ />
262
+ ))}
263
+ </SelectorGrid>
264
+ </>
265
+ );
266
+ };
267
+
268
+ export const AddDatabaseForm: React.FC<{
269
+ onSubmit: () => void;
270
+ header?: React.ReactNode;
271
+ }> = ({ onSubmit, header }) => {
272
+ const [selectedSchema, setSelectedSchema] = useState<z.ZodType<
273
+ DatabaseConnection,
274
+ FieldValues
275
+ > | null>(null);
276
+
277
+ if (!selectedSchema) {
278
+ return (
279
+ <>
280
+ {header}
281
+ <div>
282
+ <DatabaseSchemaSelector onSelect={setSelectedSchema} />
283
+ </div>
284
+ </>
285
+ );
286
+ }
287
+
288
+ const entry = ALL_ENTRIES.find((e) => e.schema === selectedSchema);
289
+ const libs = entry?.connectionLibraries;
290
+
291
+ return (
292
+ <ConnectionForm<DatabaseConnection, ConnectionLibrary>
293
+ schema={selectedSchema}
294
+ libraries={libs?.libraries ?? []}
295
+ preferredLibrary={libs?.preferred ?? "sqlalchemy"}
296
+ displayNames={ConnectionDisplayNames}
297
+ libraryLabel="Preferred connection library"
298
+ generateCode={(values, library) => generateDatabaseCode(values, library)}
299
+ onSubmit={onSubmit}
300
+ onBack={() => setSelectedSchema(null)}
301
+ />
302
+ );
303
+ };
@@ -2,8 +2,8 @@
2
2
 
3
3
  import dedent from "string-dedent";
4
4
  import { assertNever } from "@/utils/assertNever";
5
+ import { isSecret, unprefixSecret } from "../secrets";
5
6
  import { type DatabaseConnection, DatabaseConnectionSchema } from "./schemas";
6
- import { isSecret, unprefixSecret } from "./secrets";
7
7
 
8
8
  export type ConnectionLibrary =
9
9
  | "sqlmodel"
@@ -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
+ };