@marimo-team/islands 0.23.16-dev3 → 0.23.16-dev34
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/{ConnectedDataExplorerComponent-CJaUI-S-.js → ConnectedDataExplorerComponent-f9vLjQiF.js} +13 -13
- package/dist/{ErrorBoundary-DXlxsrTF.js → ErrorBoundary-DhLqwSWA.js} +3 -3
- package/dist/{any-language-editor-P9tjkj4n.js → any-language-editor-DifE-fl7.js} +5 -5
- package/dist/{button-BcSml68y.js → button-BSm8IXRU.js} +2 -4
- package/dist/{capabilities-BYLFYTYv.js → capabilities-C6OQ5YBO.js} +1 -1
- package/dist/{chat-ui-BdSDiVjk.js → chat-ui-DuPGKU9z.js} +3128 -3128
- package/dist/{check-Cs-KV5XY.js → check-DP7U0H0G.js} +1 -1
- package/dist/{code-visibility-D-umBozt.js → common-CZKqr-N4.js} +1054 -1014
- package/dist/{copy-Cs3yt6Vt.js → copy-DNq8q3zw.js} +2 -2
- package/dist/{dist-B96tMAst.js → dist-DIlxLFXG.js} +2 -2
- package/dist/{error-banner-DR7o3CbX.js → error-banner-B_GQfxxD.js} +2 -2
- package/dist/{esm-k8PIvuZW.js → esm-DzZVaNH4.js} +1 -1
- package/dist/{extends-Cq2JYHKy.js → extends-w2_S44bV.js} +4 -4
- package/dist/{formats-BFhkZJwd.js → formats-Dwq1THhr.js} +3 -3
- package/dist/{glide-data-editor--CaN-f27.js → glide-data-editor-B9K6-MJe.js} +9 -9
- package/dist/{html-to-image-DDENewl2.js → html-to-image-Bza3T2dl.js} +4140 -2748
- package/dist/{input-DStQIuqD.js → input-C5K4VBvB.js} +7 -7
- package/dist/{label-DIk0QoFe.js → label-DjoYwqmj.js} +2 -2
- package/dist/{loader-CTHCI-qe.js → loader-DX-IDS7n.js} +1 -1
- package/dist/main.js +986 -995
- package/dist/{mermaid-CdSvixW2.js → mermaid-hv6IckVw.js} +5 -5
- package/dist/{node-sql-parser-DLl_zNw0.js → node-sql-parser-v1Vi-Fft.js} +16867 -16665
- package/dist/{process-output-2QPcgBQg.js → process-output-pCEXvnhG.js} +3 -3
- package/dist/{reveal-component-DALqCV7w.js → reveal-component-CJHfXZXF.js} +725 -643
- package/dist/{spec-CnaxwDqy.js → spec-CQKL_D3i.js} +4 -4
- package/dist/{strings-BYAhZnQi.js → strings-DIy9NAmg.js} +4 -4
- package/dist/style.css +1 -1
- package/dist/{swiper-component-DE94711u.js → swiper-component-CEdAYjb7.js} +2 -2
- package/dist/toDate-CqhBWjvO.js +713 -0
- package/dist/{tooltip-B-qh7UPB.js → tooltip-f-qR0iUx.js} +3 -3
- package/dist/{types-DQl7jVYK.js → types-DicH9Oi0.js} +1 -1
- package/dist/{useAsyncData-CvO8WUy3.js → useAsyncData-CYOQb1EW.js} +1 -1
- package/dist/{useDateFormatter-BSoGim-G.js → useDateFormatter-Bt2qr_XS.js} +2 -2
- package/dist/{useDeepCompareMemoize-J3MKRdA4.js → useDeepCompareMemoize-CSHQeQEI.js} +1 -1
- package/dist/{useIframeCapabilities-POcFXHbw.js → useIframeCapabilities-kA09ykNq.js} +1 -1
- package/dist/{useLifecycle-DqPxHgAN.js → useLifecycle-CMJGqBaY.js} +3 -3
- package/dist/{useTheme-XMq9mNf_.js → useTheme-DYfby65G.js} +2 -2
- package/dist/{vega-component-CrFoZT1S.js → vega-component-C7nO5zcf.js} +10 -10
- package/dist/{zod-D0yOqoGe.js → zod-BUcyXSdR.js} +1 -1
- package/package.json +2 -2
- package/src/__mocks__/requests.ts +45 -4
- package/src/components/data-table/__tests__/__snapshots__/chart-spec-model.test.ts.snap +174 -21
- package/src/components/data-table/__tests__/chart-spec-model.test.ts +102 -1
- package/src/components/data-table/column-summary/chart-spec-model.tsx +163 -103
- package/src/components/data-table/column-summary/legacy-chart-spec.ts +15 -7
- package/src/components/data-table/column-summary/utils.ts +26 -7
- package/src/components/data-table/types.ts +1 -1
- package/src/components/editor/actions/__tests__/pair-with-agent-commands.test.ts +99 -18
- package/src/components/editor/actions/__tests__/pdf-export.test.ts +0 -4
- package/src/components/editor/actions/pair-with-agent-commands.ts +21 -10
- package/src/components/editor/actions/pair-with-agent-modal.tsx +2 -2
- package/src/components/editor/actions/pdf-export.ts +1 -4
- package/src/components/editor/actions/useNotebookActions.tsx +7 -13
- package/src/components/editor/connections/storage/__tests__/__snapshots__/as-code.test.ts.snap +50 -0
- package/src/components/editor/connections/storage/__tests__/as-code.test.ts +87 -8
- package/src/components/editor/connections/storage/as-code.ts +40 -21
- package/src/components/editor/connections/storage/schemas.ts +83 -42
- package/src/components/editor/file-tree/__tests__/file-render-mode.test.ts +27 -0
- package/src/components/editor/file-tree/__tests__/file-viewer.test.tsx +150 -0
- package/src/components/editor/file-tree/file-preview-metadata.tsx +51 -0
- package/src/components/editor/file-tree/file-render-mode.ts +32 -0
- package/src/components/editor/file-tree/file-viewer.tsx +76 -50
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +1 -1
- package/src/components/slides/__tests__/cell-output-block.test.tsx +40 -0
- package/src/components/slides/__tests__/slide-cell-view.test.tsx +33 -0
- package/src/components/slides/__tests__/slide-scroll-hint.test.tsx +198 -0
- package/src/components/slides/reveal-component.tsx +25 -19
- package/src/components/slides/slide-cell-view.tsx +1 -1
- package/src/components/slides/slide-scroll-hint.tsx +161 -0
- package/src/components/slides/slides.css +4 -2
- package/src/components/static-html/share-modal.tsx +1 -1
- package/src/components/ui/input.tsx +1 -1
- package/src/core/codemirror/find-replace/__tests__/navigate.test.ts +42 -30
- package/src/core/codemirror/find-replace/navigate.ts +2 -0
- package/src/core/codemirror/go-to-definition/__tests__/commands.test.ts +24 -3
- package/src/core/codemirror/go-to-definition/commands.ts +3 -0
- package/src/core/codemirror/keymaps/__tests__/keymaps.test.ts +31 -2
- package/src/core/codemirror/keymaps/keymaps.ts +12 -0
- package/src/core/codemirror/language/__tests__/sql.test.ts +443 -61
- package/src/core/codemirror/language/languages/sql/completion-store.ts +5 -1
- package/src/core/codemirror/language/languages/sql/sql.ts +210 -3
- package/src/core/codemirror/utils.ts +28 -0
- package/src/core/hotkeys/hotkeys.ts +0 -2
- package/src/core/islands/__tests__/bridge.test.ts +107 -0
- package/src/core/islands/bridge.ts +16 -0
- package/src/core/mode.ts +10 -0
- package/src/core/network/__tests__/api.test.ts +42 -0
- package/src/core/network/__tests__/connection.test.ts +45 -0
- package/src/core/network/__tests__/requests-lazy.test.ts +32 -1
- package/src/core/network/__tests__/requests-network.test.ts +21 -0
- package/src/core/network/api.ts +27 -0
- package/src/core/network/connection.ts +15 -0
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +14 -8
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +1 -0
- package/src/core/network/types.ts +25 -7
- package/src/core/static/download-html.ts +5 -14
- package/src/core/wasm/bridge.ts +4 -2
- package/src/core/wasm/worker/types.ts +5 -2
- package/src/plugins/impl/FormPlugin.tsx +1 -1
- package/src/plugins/impl/__tests__/FormPlugin.test.tsx +162 -0
- package/src/utils/__tests__/download.test.tsx +24 -1
- package/src/utils/download.ts +11 -6
- package/dist/toDate-B3IRfHhw.js +0 -692
|
@@ -3,7 +3,11 @@
|
|
|
3
3
|
import dedent from "string-dedent";
|
|
4
4
|
import { assertNever } from "@/utils/assertNever";
|
|
5
5
|
import { isSecret, unprefixSecret } from "../secrets";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type S3Auth,
|
|
8
|
+
type StorageConnection,
|
|
9
|
+
StorageConnectionSchema,
|
|
10
|
+
} from "./schemas";
|
|
7
11
|
|
|
8
12
|
export type StorageLibrary = "obstore" | "fsspec";
|
|
9
13
|
|
|
@@ -50,6 +54,36 @@ class SecretContainer {
|
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Credential kwargs shared by every S3-compatible store.
|
|
59
|
+
*/
|
|
60
|
+
function s3AuthParams(
|
|
61
|
+
auth: S3Auth | undefined,
|
|
62
|
+
secrets: SecretContainer,
|
|
63
|
+
): string[] {
|
|
64
|
+
// obstore polls the credential endpoint with the token read from the file,
|
|
65
|
+
// refreshing credentials for the lifetime of the store.
|
|
66
|
+
if (auth?.type === "Container credentials") {
|
|
67
|
+
return [
|
|
68
|
+
` container_credentials_full_uri=${secrets.print("container_credentials_full_uri", auth.container_credentials_full_uri)},`,
|
|
69
|
+
` container_authorization_token_file=${secrets.print("container_authorization_token_file", auth.container_authorization_token_file)},`,
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const params: string[] = [];
|
|
74
|
+
if (auth?.access_key_id) {
|
|
75
|
+
params.push(
|
|
76
|
+
` access_key_id=${secrets.print("access_key_id", auth.access_key_id)},`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
if (auth?.secret_access_key) {
|
|
80
|
+
params.push(
|
|
81
|
+
` secret_access_key=${secrets.print("secret_access_key", auth.secret_access_key)},`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return params;
|
|
85
|
+
}
|
|
86
|
+
|
|
53
87
|
function generateS3Code(
|
|
54
88
|
connection: Extract<StorageConnection, { type: "s3" }>,
|
|
55
89
|
secrets: SecretContainer,
|
|
@@ -61,21 +95,15 @@ function generateS3Code(
|
|
|
61
95
|
if (connection.region) {
|
|
62
96
|
params.push(` region=${secrets.print("region", connection.region)},`);
|
|
63
97
|
}
|
|
64
|
-
|
|
65
|
-
params.push(
|
|
66
|
-
` access_key_id=${secrets.print("access_key_id", connection.access_key_id)},`,
|
|
67
|
-
);
|
|
68
|
-
}
|
|
69
|
-
if (connection.secret_access_key) {
|
|
70
|
-
params.push(
|
|
71
|
-
` secret_access_key=${secrets.print("secret_access_key", connection.secret_access_key)},`,
|
|
72
|
-
);
|
|
73
|
-
}
|
|
98
|
+
params.push(...s3AuthParams(connection.auth, secrets));
|
|
74
99
|
if (connection.endpoint_url) {
|
|
75
100
|
params.push(
|
|
76
101
|
` endpoint_url=${secrets.print("endpoint_url", connection.endpoint_url)},`,
|
|
77
102
|
);
|
|
78
103
|
}
|
|
104
|
+
if (connection.allow_http) {
|
|
105
|
+
params.push(" allow_http=True,");
|
|
106
|
+
}
|
|
79
107
|
|
|
80
108
|
const paramsStr = params.length > 0 ? `\n${params.join("\n")}\n` : "";
|
|
81
109
|
|
|
@@ -140,16 +168,7 @@ function generateCoreWeaveCode(
|
|
|
140
168
|
` region=${secrets.print("region", connection.region)},`,
|
|
141
169
|
];
|
|
142
170
|
|
|
143
|
-
|
|
144
|
-
params.push(
|
|
145
|
-
` access_key_id=${secrets.print("access_key_id", connection.access_key_id)},`,
|
|
146
|
-
);
|
|
147
|
-
}
|
|
148
|
-
if (connection.secret_access_key) {
|
|
149
|
-
params.push(
|
|
150
|
-
` secret_access_key=${secrets.print("secret_access_key", connection.secret_access_key)},`,
|
|
151
|
-
);
|
|
152
|
-
}
|
|
171
|
+
params.push(...s3AuthParams(connection.auth, secrets));
|
|
153
172
|
|
|
154
173
|
params.push(
|
|
155
174
|
` endpoint="https://${connection.bucket}.cwobject.com",`,
|
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { FieldOptions } from "@/components/forms/options";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Credentials shared by every S3-compatible store (Amazon S3, CoreWeave, ...).
|
|
7
|
+
*
|
|
8
|
+
* Rendered as tabs: either long-lived access keys, or a credential-vending
|
|
9
|
+
* endpoint (ECS/EKS task roles, CoreWeave sandboxes, ...) that obstore polls
|
|
10
|
+
* with a bearer token read from disk.
|
|
11
|
+
*/
|
|
12
|
+
const s3AuthField = () =>
|
|
13
|
+
z
|
|
14
|
+
.discriminatedUnion("type", [
|
|
15
|
+
z.object({
|
|
16
|
+
type: z.literal("Access keys"),
|
|
17
|
+
access_key_id: z
|
|
18
|
+
.string()
|
|
19
|
+
.optional()
|
|
20
|
+
.describe(
|
|
21
|
+
FieldOptions.of({
|
|
22
|
+
label: "Access Key ID",
|
|
23
|
+
description: "Leave empty to use the default credential chain",
|
|
24
|
+
inputType: "password",
|
|
25
|
+
optionRegex:
|
|
26
|
+
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
secret_access_key: z
|
|
30
|
+
.string()
|
|
31
|
+
.optional()
|
|
32
|
+
.describe(
|
|
33
|
+
FieldOptions.of({
|
|
34
|
+
label: "Secret Access Key",
|
|
35
|
+
inputType: "password",
|
|
36
|
+
optionRegex:
|
|
37
|
+
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
38
|
+
}),
|
|
39
|
+
),
|
|
40
|
+
}),
|
|
41
|
+
z.object({
|
|
42
|
+
type: z.literal("Container credentials"),
|
|
43
|
+
container_credentials_full_uri: z
|
|
44
|
+
.string()
|
|
45
|
+
.nonempty()
|
|
46
|
+
.describe(
|
|
47
|
+
FieldOptions.of({
|
|
48
|
+
label: "Credentials URI",
|
|
49
|
+
description: "Endpoint that vends credentials to the container",
|
|
50
|
+
placeholder: "http://169.254.170.23/v1/credentials",
|
|
51
|
+
optionRegex: "container.?credentials",
|
|
52
|
+
}),
|
|
53
|
+
),
|
|
54
|
+
container_authorization_token_file: z
|
|
55
|
+
.string()
|
|
56
|
+
.nonempty()
|
|
57
|
+
.describe(
|
|
58
|
+
FieldOptions.of({
|
|
59
|
+
label: "Authorization Token File",
|
|
60
|
+
description: "File holding the token sent to the credentials URI",
|
|
61
|
+
placeholder: "/var/run/secrets/token",
|
|
62
|
+
optionRegex:
|
|
63
|
+
"(container.?authorization.?token.?file|container.?auth.?token)",
|
|
64
|
+
}),
|
|
65
|
+
),
|
|
66
|
+
}),
|
|
67
|
+
])
|
|
68
|
+
.default({ type: "Access keys" })
|
|
69
|
+
.describe(FieldOptions.of({ label: "Credentials", special: "tabs" }));
|
|
70
|
+
|
|
71
|
+
const allowHttpField = () =>
|
|
72
|
+
z
|
|
73
|
+
.boolean()
|
|
74
|
+
.default(false)
|
|
75
|
+
.describe(
|
|
76
|
+
FieldOptions.of({
|
|
77
|
+
label: "Allow HTTP",
|
|
78
|
+
description: "Required for plain-http (non-TLS) endpoints",
|
|
79
|
+
}),
|
|
80
|
+
);
|
|
81
|
+
|
|
5
82
|
export const S3StorageSchema = z
|
|
6
83
|
.object({
|
|
7
84
|
type: z.literal("s3"),
|
|
@@ -25,36 +102,20 @@ export const S3StorageSchema = z
|
|
|
25
102
|
optionRegex: "(region|aws.?region)",
|
|
26
103
|
}),
|
|
27
104
|
),
|
|
28
|
-
access_key_id: z
|
|
29
|
-
.string()
|
|
30
|
-
.optional()
|
|
31
|
-
.describe(
|
|
32
|
-
FieldOptions.of({
|
|
33
|
-
label: "Access Key ID",
|
|
34
|
-
inputType: "password",
|
|
35
|
-
optionRegex: "(access.?key.?id|aws.?access.?key)",
|
|
36
|
-
}),
|
|
37
|
-
),
|
|
38
|
-
secret_access_key: z
|
|
39
|
-
.string()
|
|
40
|
-
.optional()
|
|
41
|
-
.describe(
|
|
42
|
-
FieldOptions.of({
|
|
43
|
-
label: "Secret Access Key",
|
|
44
|
-
inputType: "password",
|
|
45
|
-
optionRegex: "(secret.?access.?key|aws.?secret)",
|
|
46
|
-
}),
|
|
47
|
-
),
|
|
48
105
|
endpoint_url: z
|
|
49
106
|
.string()
|
|
50
107
|
.optional()
|
|
51
108
|
.describe(
|
|
52
109
|
FieldOptions.of({
|
|
53
110
|
label: "Endpoint URL",
|
|
111
|
+
description:
|
|
112
|
+
"Ignored if the AWS_ENDPOINT_URL_S3 environment variable is set",
|
|
54
113
|
placeholder: "https://s3.amazonaws.com",
|
|
55
114
|
optionRegex: "(endpoint|s3.?url|s3.?endpoint)",
|
|
56
115
|
}),
|
|
57
116
|
),
|
|
117
|
+
allow_http: allowHttpField(),
|
|
118
|
+
auth: s3AuthField(),
|
|
58
119
|
})
|
|
59
120
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
60
121
|
|
|
@@ -139,28 +200,7 @@ export const CoreWeaveStorageSchema = z
|
|
|
139
200
|
placeholder: "US-EAST-04A",
|
|
140
201
|
}),
|
|
141
202
|
),
|
|
142
|
-
|
|
143
|
-
.string()
|
|
144
|
-
.optional()
|
|
145
|
-
.describe(
|
|
146
|
-
FieldOptions.of({
|
|
147
|
-
label: "Access Key ID",
|
|
148
|
-
inputType: "password",
|
|
149
|
-
optionRegex:
|
|
150
|
-
"(access.?key.?id|object.?storage.?key|aws.?access.?key)",
|
|
151
|
-
}),
|
|
152
|
-
),
|
|
153
|
-
secret_access_key: z
|
|
154
|
-
.string()
|
|
155
|
-
.optional()
|
|
156
|
-
.describe(
|
|
157
|
-
FieldOptions.of({
|
|
158
|
-
label: "Secret Access Key",
|
|
159
|
-
inputType: "password",
|
|
160
|
-
optionRegex:
|
|
161
|
-
"(secret.?access.?key|object.?storage.?secret|aws.?secret)",
|
|
162
|
-
}),
|
|
163
|
-
),
|
|
203
|
+
auth: s3AuthField(),
|
|
164
204
|
})
|
|
165
205
|
.describe(FieldOptions.of({ direction: "two-columns" }));
|
|
166
206
|
|
|
@@ -189,3 +229,4 @@ export const StorageConnectionSchema = z.discriminatedUnion("type", [
|
|
|
189
229
|
]);
|
|
190
230
|
|
|
191
231
|
export type StorageConnection = z.infer<typeof StorageConnectionSchema>;
|
|
232
|
+
export type S3Auth = Extract<StorageConnection, { type: "s3" }>["auth"];
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { getFileRenderMode } from "../file-render-mode";
|
|
5
|
+
|
|
6
|
+
describe("getFileRenderMode", () => {
|
|
7
|
+
it.each([
|
|
8
|
+
["text/plain", false, "text"],
|
|
9
|
+
["text/x-rust", false, "text"],
|
|
10
|
+
["application/json", false, "text"],
|
|
11
|
+
["application/xml", false, "text"],
|
|
12
|
+
["text/csv", false, "csv"],
|
|
13
|
+
["image/png", true, "media"],
|
|
14
|
+
["video/mp4", true, "media"],
|
|
15
|
+
["application/pdf", true, "media"],
|
|
16
|
+
["application/x-apple-diskimage", true, "unsupported"],
|
|
17
|
+
["application/zip", true, "unsupported"],
|
|
18
|
+
["application/octet-stream", false, "unsupported"],
|
|
19
|
+
[null, false, "text"],
|
|
20
|
+
[null, true, "unsupported"],
|
|
21
|
+
] as const)(
|
|
22
|
+
"maps %s with isBase64=%s to %s",
|
|
23
|
+
(mimeType, isBase64, expected) => {
|
|
24
|
+
expect(getFileRenderMode(mimeType, isBase64)).toBe(expected);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
4
|
+
import { Provider } from "jotai";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
8
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
9
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
10
|
+
import type { FileDetailsResponse, FileInfo } from "@/core/network/types";
|
|
11
|
+
import { store } from "@/core/state/jotai";
|
|
12
|
+
import { FileViewer, MAX_FILE_PREVIEW_BYTES } from "../file-viewer";
|
|
13
|
+
|
|
14
|
+
vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
|
|
15
|
+
LazyAnyLanguageCodeMirror: ({
|
|
16
|
+
value,
|
|
17
|
+
onChange,
|
|
18
|
+
}: {
|
|
19
|
+
value?: string;
|
|
20
|
+
onChange?: (value: string) => void;
|
|
21
|
+
}) => (
|
|
22
|
+
<textarea
|
|
23
|
+
data-testid="code-editor"
|
|
24
|
+
value={value ?? ""}
|
|
25
|
+
onChange={(evt) => onChange?.(evt.target.value)}
|
|
26
|
+
/>
|
|
27
|
+
),
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@/core/wasm/utils", () => ({ isWasm: () => false }));
|
|
31
|
+
|
|
32
|
+
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
|
33
|
+
<Provider store={store}>
|
|
34
|
+
<TooltipProvider>{children}</TooltipProvider>
|
|
35
|
+
</Provider>
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
const file: FileInfo = {
|
|
39
|
+
id: "/workspace/installer.dmg",
|
|
40
|
+
path: "/workspace/installer.dmg",
|
|
41
|
+
name: "installer.dmg",
|
|
42
|
+
isDirectory: false,
|
|
43
|
+
isMarimoFile: false,
|
|
44
|
+
size: 20 * 1024 * 1024,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function renderViewer(response: FileDetailsResponse) {
|
|
48
|
+
const client = MockRequestClient.create({
|
|
49
|
+
sendFileDetails: vi.fn().mockResolvedValue(response),
|
|
50
|
+
});
|
|
51
|
+
store.set(requestClientAtom, client);
|
|
52
|
+
render(<FileViewer file={file} onOpenNotebook={vi.fn()} />, { wrapper });
|
|
53
|
+
return client;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("FileViewer bounded previews", () => {
|
|
57
|
+
beforeEach(() => {
|
|
58
|
+
store.set(requestClientAtom, null);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("requests a bounded preview and shows oversized metadata", async () => {
|
|
62
|
+
const client = renderViewer({
|
|
63
|
+
file,
|
|
64
|
+
contents: null,
|
|
65
|
+
mimeType: "application/x-apple-diskimage",
|
|
66
|
+
isBase64: false,
|
|
67
|
+
isTooLarge: true,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(await screen.findByText(/too large to preview/i)).toBeVisible();
|
|
71
|
+
expect(screen.getByText("20 MB")).toBeVisible();
|
|
72
|
+
expect(screen.getByRole("button", { name: /download/i })).toBeVisible();
|
|
73
|
+
expect(screen.queryByTestId("code-editor")).not.toBeInTheDocument();
|
|
74
|
+
expect(client.sendFileDetails).toHaveBeenCalledWith({
|
|
75
|
+
path: file.path,
|
|
76
|
+
maxBytes: MAX_FILE_PREVIEW_BYTES,
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("shows unsupported metadata with a download action, not base64 text", async () => {
|
|
81
|
+
renderViewer({
|
|
82
|
+
file: { ...file, size: 4 },
|
|
83
|
+
contents: "AAEC",
|
|
84
|
+
mimeType: "application/x-apple-diskimage",
|
|
85
|
+
isBase64: true,
|
|
86
|
+
isTooLarge: false,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(await screen.findByText(/cannot be previewed/i)).toBeVisible();
|
|
90
|
+
expect(screen.getByRole("button", { name: /download/i })).toBeVisible();
|
|
91
|
+
expect(screen.queryByTestId("code-editor")).not.toBeInTheDocument();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("continues to render small text files", async () => {
|
|
95
|
+
renderViewer({
|
|
96
|
+
file: { ...file, name: "notes.txt", size: 5 },
|
|
97
|
+
contents: "hello",
|
|
98
|
+
mimeType: "text/plain",
|
|
99
|
+
isBase64: false,
|
|
100
|
+
isTooLarge: false,
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await waitFor(() => {
|
|
104
|
+
expect(screen.getByTestId("code-editor")).toBeInTheDocument();
|
|
105
|
+
});
|
|
106
|
+
expect(screen.queryByText(/too large to preview/i)).not.toBeInTheDocument();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("preserves edits to an initially empty text file across remount", async () => {
|
|
110
|
+
const emptyFile: FileInfo = {
|
|
111
|
+
id: "/workspace/empty.txt",
|
|
112
|
+
path: "/workspace/empty.txt",
|
|
113
|
+
name: "empty.txt",
|
|
114
|
+
isDirectory: false,
|
|
115
|
+
isMarimoFile: false,
|
|
116
|
+
size: 0,
|
|
117
|
+
};
|
|
118
|
+
const response: FileDetailsResponse = {
|
|
119
|
+
file: emptyFile,
|
|
120
|
+
contents: "",
|
|
121
|
+
mimeType: "text/plain",
|
|
122
|
+
isBase64: false,
|
|
123
|
+
isTooLarge: false,
|
|
124
|
+
};
|
|
125
|
+
const client = MockRequestClient.create({
|
|
126
|
+
sendFileDetails: vi.fn().mockResolvedValue(response),
|
|
127
|
+
});
|
|
128
|
+
store.set(requestClientAtom, client);
|
|
129
|
+
|
|
130
|
+
const { unmount } = render(
|
|
131
|
+
<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />,
|
|
132
|
+
{ wrapper },
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const editor =
|
|
136
|
+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
|
|
137
|
+
fireEvent.change(editor, { target: { value: "draft" } });
|
|
138
|
+
unmount();
|
|
139
|
+
|
|
140
|
+
render(<FileViewer file={emptyFile} onOpenNotebook={vi.fn()} />, {
|
|
141
|
+
wrapper,
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
const reopened =
|
|
145
|
+
await screen.findByTestId<HTMLTextAreaElement>("code-editor");
|
|
146
|
+
await waitFor(() => {
|
|
147
|
+
expect(reopened.value).toBe("draft");
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { DownloadIcon } from "lucide-react";
|
|
4
|
+
import { useLocale } from "react-aria";
|
|
5
|
+
import { Button } from "@/components/ui/button";
|
|
6
|
+
import type { FileInfo } from "@/core/network/types";
|
|
7
|
+
import { formatBytes } from "@/utils/formatting";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
file: FileInfo;
|
|
11
|
+
mimeType: string;
|
|
12
|
+
message: string;
|
|
13
|
+
onDownload?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const FilePreviewMetadata = ({
|
|
17
|
+
file,
|
|
18
|
+
mimeType,
|
|
19
|
+
message,
|
|
20
|
+
onDownload,
|
|
21
|
+
}: Props) => {
|
|
22
|
+
const { locale } = useLocale();
|
|
23
|
+
return (
|
|
24
|
+
<div className="p-4 text-sm">
|
|
25
|
+
<div className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2">
|
|
26
|
+
<span className="text-muted-foreground font-medium">Path</span>
|
|
27
|
+
<span className="font-mono text-xs break-all">{file.path}</span>
|
|
28
|
+
<span className="text-muted-foreground font-medium">Type</span>
|
|
29
|
+
<span>{mimeType}</span>
|
|
30
|
+
{file.size != null && (
|
|
31
|
+
<>
|
|
32
|
+
<span className="text-muted-foreground font-medium">Size</span>
|
|
33
|
+
<span>{formatBytes(file.size, locale)}</span>
|
|
34
|
+
</>
|
|
35
|
+
)}
|
|
36
|
+
</div>
|
|
37
|
+
<div className="mt-4 text-muted-foreground italic">{message}</div>
|
|
38
|
+
{onDownload && (
|
|
39
|
+
<Button
|
|
40
|
+
variant="outline"
|
|
41
|
+
size="sm"
|
|
42
|
+
className="mt-4"
|
|
43
|
+
onClick={onDownload}
|
|
44
|
+
>
|
|
45
|
+
<DownloadIcon className="h-3.5 w-3.5 mr-2" />
|
|
46
|
+
Download
|
|
47
|
+
</Button>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { isMediaMime, MIME_TO_LANGUAGE } from "./renderers";
|
|
4
|
+
|
|
5
|
+
export type FileRenderMode = "text" | "csv" | "media" | "unsupported";
|
|
6
|
+
|
|
7
|
+
export function getFileRenderMode(
|
|
8
|
+
mimeType: string | null | undefined,
|
|
9
|
+
isBase64: boolean | undefined,
|
|
10
|
+
): FileRenderMode {
|
|
11
|
+
const mime = mimeType || "text/plain";
|
|
12
|
+
|
|
13
|
+
if (isMediaMime(mime)) {
|
|
14
|
+
return "media";
|
|
15
|
+
}
|
|
16
|
+
if (isBase64) {
|
|
17
|
+
return "unsupported";
|
|
18
|
+
}
|
|
19
|
+
if (mime === "text/csv") {
|
|
20
|
+
return "csv";
|
|
21
|
+
}
|
|
22
|
+
if (
|
|
23
|
+
mime.startsWith("text/") ||
|
|
24
|
+
(mime !== "default" && mime in MIME_TO_LANGUAGE)
|
|
25
|
+
) {
|
|
26
|
+
return "text";
|
|
27
|
+
}
|
|
28
|
+
if (mimeType == null) {
|
|
29
|
+
return "text";
|
|
30
|
+
}
|
|
31
|
+
return "unsupported";
|
|
32
|
+
}
|