@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.
- package/dist/main.js +2 -2
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/chat/acp/agent-panel.tsx +29 -8
- package/src/components/databases/icons/google-drive.svg +8 -0
- package/src/components/datasources/datasources.tsx +5 -5
- package/src/components/editor/actions/useNotebookActions.tsx +15 -2
- package/src/components/editor/connections/add-connection-dialog.tsx +83 -0
- package/src/components/editor/connections/components.tsx +177 -0
- package/src/components/editor/{database → connections/database}/__tests__/as-code.test.ts +1 -1
- package/src/components/editor/connections/database/add-database-form.tsx +303 -0
- package/src/components/editor/{database → connections/database}/as-code.ts +1 -1
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +100 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +166 -0
- package/src/components/editor/connections/storage/add-storage-form.tsx +135 -0
- package/src/components/editor/connections/storage/as-code.ts +188 -0
- package/src/components/editor/connections/storage/schemas.ts +141 -0
- package/src/components/storage/components.tsx +9 -3
- package/src/components/storage/storage-inspector.tsx +20 -1
- package/src/core/codemirror/__tests__/format.test.ts +9 -1
- package/src/core/saving/file-state.ts +7 -0
- package/src/core/storage/types.ts +1 -0
- package/src/mount.tsx +6 -1
- package/src/components/editor/database/add-database-form.tsx +0 -420
- /package/src/components/editor/{database → connections}/__tests__/secrets.test.ts +0 -0
- /package/src/components/editor/{database → connections/database}/__tests__/__snapshots__/as-code.test.ts.snap +0 -0
- /package/src/components/editor/{database → connections/database}/schemas.ts +0 -0
- /package/src/components/editor/{database → connections}/form-renderers.tsx +0 -0
- /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"
|
package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap
ADDED
|
@@ -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
|
+
};
|