@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marimo-team/islands",
3
- "version": "0.20.3-dev92",
3
+ "version": "0.20.3-dev96",
4
4
  "main": "dist/main.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
@@ -0,0 +1,8 @@
1
+ <svg viewBox="0 0 87.3 78" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/>
3
+ <path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/>
4
+ <path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/>
5
+ <path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/>
6
+ <path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/>
7
+ <path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/>
8
+ </svg>
@@ -60,7 +60,7 @@ import { ErrorBoundary } from "../editor/boundary/ErrorBoundary";
60
60
  import { PythonIcon } from "../editor/cell/code/icons";
61
61
  import { useAddCodeToNewCell } from "../editor/cell/useAddCell";
62
62
  import { PanelEmptyState } from "../editor/chrome/panels/empty-state";
63
- import { AddDatabaseDialog } from "../editor/database/add-database-form";
63
+ import { AddConnectionDialog } from "../editor/connections/add-connection-dialog";
64
64
  import { DatasetColumnPreview } from "./column-preview";
65
65
  import {
66
66
  ColumnName,
@@ -160,12 +160,12 @@ export const DataSources: React.FC = () => {
160
160
  title="No tables found"
161
161
  description="Any datasets/dataframes in the global scope will be shown here."
162
162
  action={
163
- <AddDatabaseDialog>
163
+ <AddConnectionDialog>
164
164
  <Button variant="outline" size="sm">
165
165
  Add database or catalog
166
166
  <PlusIcon className="h-4 w-4 ml-2" />
167
167
  </Button>
168
- </AddDatabaseDialog>
168
+ </AddConnectionDialog>
169
169
  }
170
170
  icon={<DatabaseIcon />}
171
171
  />
@@ -201,7 +201,7 @@ export const DataSources: React.FC = () => {
201
201
  </button>
202
202
  )}
203
203
 
204
- <AddDatabaseDialog>
204
+ <AddConnectionDialog>
205
205
  <Button
206
206
  variant="ghost"
207
207
  size="sm"
@@ -209,7 +209,7 @@ export const DataSources: React.FC = () => {
209
209
  >
210
210
  <PlusIcon className="h-4 w-4" />
211
211
  </Button>
212
- </AddDatabaseDialog>
212
+ </AddConnectionDialog>
213
213
  </div>
214
214
 
215
215
  <CommandList className="flex flex-col">
@@ -24,6 +24,7 @@ import {
24
24
  FolderDownIcon,
25
25
  GithubIcon,
26
26
  GlobeIcon,
27
+ HardDrive,
27
28
  Home,
28
29
  ImageIcon,
29
30
  KeyboardIcon,
@@ -78,9 +79,9 @@ import { newNotebookURL } from "@/utils/urls";
78
79
  import { useRunAllCells } from "../cell/useRunCells";
79
80
  import { useChromeActions, useChromeState } from "../chrome/state";
80
81
  import { PANELS } from "../chrome/types";
82
+ import { AddConnectionDialogContent } from "../connections/add-connection-dialog";
81
83
  import { keyboardShortcutsAtom } from "../controls/keyboard-shortcuts";
82
84
  import { commandPaletteAtom } from "../controls/state";
83
- import { AddDatabaseDialogContent } from "../database/add-database-form";
84
85
  import { displayLayoutName, getLayoutIcon } from "../renderers/layout-select";
85
86
  import { LAYOUT_TYPES } from "../renderers/types";
86
87
  import { runServerSidePDFDownload } from "./pdf-export";
@@ -450,7 +451,19 @@ export function useNotebookActions() {
450
451
  icon: <DatabaseIcon size={14} strokeWidth={1.5} />,
451
452
  label: "Add database connection",
452
453
  handle: () => {
453
- openModal(<AddDatabaseDialogContent onClose={closeModal} />);
454
+ openModal(<AddConnectionDialogContent onClose={closeModal} />);
455
+ },
456
+ },
457
+ {
458
+ icon: <HardDrive size={14} strokeWidth={1.5} />,
459
+ label: "Add remote storage",
460
+ handle: () => {
461
+ openModal(
462
+ <AddConnectionDialogContent
463
+ defaultTab="storage"
464
+ onClose={closeModal}
465
+ />,
466
+ );
454
467
  },
455
468
  },
456
469
  {
@@ -0,0 +1,83 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogHeader,
9
+ DialogTitle,
10
+ DialogTrigger,
11
+ } from "@/components/ui/dialog";
12
+ import { ExternalLink } from "@/components/ui/links";
13
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
14
+ import { AddDatabaseForm } from "./database/add-database-form";
15
+ import { AddStorageForm } from "./storage/add-storage-form";
16
+
17
+ type ConnectionTab = "databases" | "storage";
18
+
19
+ export const AddConnectionDialog: React.FC<{
20
+ children: React.ReactNode;
21
+ defaultTab?: ConnectionTab;
22
+ }> = ({ children, defaultTab = "databases" }) => {
23
+ const [open, setOpen] = useState(false);
24
+
25
+ return (
26
+ <Dialog open={open} onOpenChange={setOpen}>
27
+ <DialogTrigger asChild={true}>{children}</DialogTrigger>
28
+ <AddConnectionDialogContent
29
+ defaultTab={defaultTab}
30
+ onClose={() => setOpen(false)}
31
+ />
32
+ </Dialog>
33
+ );
34
+ };
35
+
36
+ export const AddConnectionDialogContent: React.FC<{
37
+ defaultTab?: ConnectionTab;
38
+ onClose: () => void;
39
+ }> = ({ defaultTab = "databases", onClose }) => {
40
+ const tabHeader = (
41
+ <TabsList className="w-full mb-4">
42
+ <TabsTrigger value="databases" className="flex-1">
43
+ Databases & Catalogs
44
+ </TabsTrigger>
45
+ <TabsTrigger value="storage" className="flex-1">
46
+ Remote Storages
47
+ </TabsTrigger>
48
+ </TabsList>
49
+ );
50
+
51
+ return (
52
+ <DialogContent className="max-h-[75vh] overflow-y-auto">
53
+ <DialogHeader>
54
+ <DialogTitle>Add Connection</DialogTitle>
55
+ <DialogDescription>
56
+ Connect to a{" "}
57
+ <ExternalLink href="https://docs.marimo.io/guides/working_with_data/sql/#connecting-to-a-custom-database">
58
+ database, data catalog
59
+ </ExternalLink>{" "}
60
+ or{" "}
61
+ <ExternalLink href="https://docs.marimo.io/guides/working_with_data/remote_storage/">
62
+ remote storage
63
+ </ExternalLink>{" "}
64
+ to work with data directly from your notebook.
65
+ </DialogDescription>
66
+ </DialogHeader>
67
+ <Tabs defaultValue={defaultTab}>
68
+ <TabsContent
69
+ value="databases"
70
+ className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0"
71
+ >
72
+ <AddDatabaseForm onSubmit={onClose} header={tabHeader} />
73
+ </TabsContent>
74
+ <TabsContent
75
+ value="storage"
76
+ className="mt-0 focus-visible:ring-0 focus-visible:ring-offset-0"
77
+ >
78
+ <AddStorageForm onSubmit={onClose} header={tabHeader} />
79
+ </TabsContent>
80
+ </Tabs>
81
+ </DialogContent>
82
+ );
83
+ };
@@ -0,0 +1,177 @@
1
+ /* Copyright 2026 Marimo. All rights reserved. */
2
+
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import React from "react";
5
+ import { type DefaultValues, type FieldValues, useForm } from "react-hook-form";
6
+ import type { z } from "zod";
7
+ import { type FormRenderer, ZodForm } from "@/components/forms/form";
8
+ import { getDefaults } from "@/components/forms/form-utils";
9
+ import { Button } from "@/components/ui/button";
10
+ import { FormErrorsBanner } from "@/components/ui/form";
11
+ import {
12
+ Select,
13
+ SelectContent,
14
+ SelectGroup,
15
+ SelectItem,
16
+ SelectTrigger,
17
+ SelectValue,
18
+ } from "@/components/ui/select";
19
+ import { useCellActions } from "@/core/cells/cells";
20
+ import { useLastFocusedCellId } from "@/core/cells/focus";
21
+ import { ENV_RENDERER, SecretsProvider } from "./form-renderers";
22
+
23
+ const RENDERERS: FormRenderer[] = [ENV_RENDERER];
24
+
25
+ /**
26
+ * Grid layout for provider/database selector buttons.
27
+ */
28
+ export const SelectorGrid: React.FC<{ children: React.ReactNode }> = ({
29
+ children,
30
+ }) => <div className="grid grid-cols-2 md:grid-cols-3 gap-4">{children}</div>;
31
+
32
+ /**
33
+ * A colored button tile for selecting a provider/database.
34
+ */
35
+ export const SelectorButton: React.FC<{
36
+ name: string;
37
+ color: string;
38
+ icon: React.ReactNode;
39
+ onSelect: () => void;
40
+ }> = ({ name, color, icon, onSelect }) => (
41
+ <button
42
+ type="button"
43
+ className="py-3 flex flex-col items-center justify-center gap-1 transition-all hover:scale-105 hover:brightness-110 rounded shadow-sm-solid hover:shadow-md-solid"
44
+ style={{ backgroundColor: color }}
45
+ onClick={onSelect}
46
+ >
47
+ {icon}
48
+ <span className="text-white font-medium text-lg">{name}</span>
49
+ </button>
50
+ );
51
+
52
+ /**
53
+ * Footer with Back/Add buttons and a library picker.
54
+ */
55
+ export const ConnectionFormFooter = <L extends string>({
56
+ onBack,
57
+ isValid,
58
+ libraries,
59
+ preferredLibrary,
60
+ onLibraryChange,
61
+ displayNames,
62
+ libraryLabel = "Preferred library",
63
+ }: {
64
+ onBack: () => void;
65
+ isValid: boolean;
66
+ libraries: L[];
67
+ preferredLibrary: L;
68
+ onLibraryChange: (library: L) => void;
69
+ displayNames: Record<L, string>;
70
+ libraryLabel?: string;
71
+ }) => (
72
+ <div className="flex gap-2 justify-between">
73
+ <div className="flex gap-2">
74
+ <Button type="button" variant="outline" onClick={onBack}>
75
+ Back
76
+ </Button>
77
+ <Button type="submit" disabled={!isValid}>
78
+ Add
79
+ </Button>
80
+ </div>
81
+ <div>
82
+ <Select value={preferredLibrary} onValueChange={onLibraryChange}>
83
+ <div className="flex flex-col gap-1 items-end">
84
+ <SelectTrigger>
85
+ <SelectValue placeholder="Select a library" />
86
+ </SelectTrigger>
87
+ <span className="text-xs text-muted-foreground">{libraryLabel}</span>
88
+ </div>
89
+ <SelectContent>
90
+ <SelectGroup>
91
+ {libraries.map((library) => (
92
+ <SelectItem key={library} value={library}>
93
+ {displayNames[library] ?? library}
94
+ </SelectItem>
95
+ ))}
96
+ </SelectGroup>
97
+ </SelectContent>
98
+ </Select>
99
+ </div>
100
+ </div>
101
+ );
102
+
103
+ /**
104
+ * Returns a callback that inserts code into a new cell after the last focused cell.
105
+ */
106
+ export function useInsertCode() {
107
+ const { createNewCell } = useCellActions();
108
+ const lastFocusedCellId = useLastFocusedCellId();
109
+
110
+ return (code: string) => {
111
+ createNewCell({
112
+ code,
113
+ before: false,
114
+ cellId: lastFocusedCellId ?? "__end__",
115
+ skipIfCodeExists: true,
116
+ });
117
+ };
118
+ }
119
+
120
+ /**
121
+ * Generic connection form: Zod-driven form with secrets support, a library
122
+ * picker, and Back/Add buttons. Used by both database and storage forms.
123
+ */
124
+ export const ConnectionForm = <T extends FieldValues, L extends string>({
125
+ schema,
126
+ libraries,
127
+ preferredLibrary: initialPreferred,
128
+ displayNames,
129
+ libraryLabel,
130
+ generateCode,
131
+ onSubmit,
132
+ onBack,
133
+ }: {
134
+ schema: z.ZodType<T, FieldValues>;
135
+ libraries: L[];
136
+ preferredLibrary: L;
137
+ displayNames: Record<L, string>;
138
+ libraryLabel?: string;
139
+ generateCode: (values: T, library: L) => string;
140
+ onSubmit: () => void;
141
+ onBack: () => void;
142
+ }) => {
143
+ const defaults = getDefaults(schema);
144
+ const form = useForm<T>({
145
+ defaultValues: defaults as DefaultValues<T>,
146
+ resolver: zodResolver(schema),
147
+ reValidateMode: "onChange",
148
+ });
149
+
150
+ const [preferredLibrary, setPreferredLibrary] =
151
+ React.useState<L>(initialPreferred);
152
+ const insertCode = useInsertCode();
153
+
154
+ const handleSubmit = (values: T) => {
155
+ insertCode(generateCode(values, preferredLibrary));
156
+ onSubmit();
157
+ };
158
+
159
+ return (
160
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="space-y-4">
161
+ <SecretsProvider>
162
+ <ZodForm schema={schema} form={form} renderers={RENDERERS}>
163
+ <FormErrorsBanner />
164
+ </ZodForm>
165
+ </SecretsProvider>
166
+ <ConnectionFormFooter
167
+ onBack={onBack}
168
+ isValid={form.formState.isValid}
169
+ libraries={libraries}
170
+ preferredLibrary={preferredLibrary}
171
+ onLibraryChange={setPreferredLibrary}
172
+ displayNames={displayNames}
173
+ libraryLabel={libraryLabel}
174
+ />
175
+ </form>
176
+ );
177
+ };
@@ -1,8 +1,8 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
  import { describe, expect, it } from "vitest";
3
+ import { prefixSecret } from "../../secrets";
3
4
  import { type ConnectionLibrary, generateDatabaseCode } from "../as-code";
4
5
  import type { DatabaseConnection } from "../schemas";
5
- import { prefixSecret } from "../secrets";
6
6
 
7
7
  describe("generateDatabaseCode", () => {
8
8
  // Test fixtures
@@ -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"