@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,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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
|
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
|
|
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) => (
|
|
@@ -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 () => {
|
|
@@ -9,6 +9,13 @@ import { getFilenameFromDOM } from "../dom/htmlUtils";
|
|
|
9
9
|
*/
|
|
10
10
|
export const filenameAtom = atom<string | null>(getFilenameFromDOM());
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Atom for storing the notebook's working directory (absolute path).
|
|
14
|
+
* In directory mode, filenameAtom may be a relative display path;
|
|
15
|
+
* this atom holds the absolute directory containing the notebook.
|
|
16
|
+
*/
|
|
17
|
+
export const cwdAtom = atom<string | null>(null);
|
|
18
|
+
|
|
12
19
|
/**
|
|
13
20
|
* Set for static notebooks.
|
|
14
21
|
*/
|
package/src/mount.tsx
CHANGED
|
@@ -36,7 +36,7 @@ import {
|
|
|
36
36
|
DEFAULT_RUNTIME_CONFIG,
|
|
37
37
|
runtimeConfigAtom,
|
|
38
38
|
} from "./core/runtime/config";
|
|
39
|
-
import { codeAtom, filenameAtom } from "./core/saving/file-state";
|
|
39
|
+
import { codeAtom, cwdAtom, filenameAtom } from "./core/saving/file-state";
|
|
40
40
|
import { store } from "./core/state/jotai";
|
|
41
41
|
import { patchFetch, patchVegaLoader } from "./core/static/files";
|
|
42
42
|
import {
|
|
@@ -146,6 +146,10 @@ const mountOptionsSchema = z.object({
|
|
|
146
146
|
Logger.warn("No filename provided, using fallback");
|
|
147
147
|
return getFilenameFromDOM();
|
|
148
148
|
}),
|
|
149
|
+
/**
|
|
150
|
+
* absolute working directory of the notebook
|
|
151
|
+
*/
|
|
152
|
+
cwd: z.string().nullish().default(null),
|
|
149
153
|
/**
|
|
150
154
|
* notebook code
|
|
151
155
|
*/
|
|
@@ -282,6 +286,7 @@ function initStore(options: unknown) {
|
|
|
282
286
|
|
|
283
287
|
// Files
|
|
284
288
|
store.set(filenameAtom, parsedOptions.data.filename);
|
|
289
|
+
store.set(cwdAtom, parsedOptions.data.cwd ?? null);
|
|
285
290
|
store.set(codeAtom, parsedOptions.data.code);
|
|
286
291
|
store.set(initialModeAtom, mode);
|
|
287
292
|
|