@marimo-team/frontend 0.25.1-dev40 → 0.25.1-dev41
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/assets/{ConnectedDataExplorerComponent-CFIBrwTd.js → ConnectedDataExplorerComponent-BaPBcsBf.js} +1 -1
- package/dist/assets/{ErrorBoundary-_KMJcAfx.js → ErrorBoundary-CCkkO1_J.js} +2 -2
- package/dist/assets/{LazyAnyLanguageCodeMirror-DsLKeSFO.js → LazyAnyLanguageCodeMirror-CrQ9WToq.js} +2 -2
- package/dist/assets/{RunButton-Be204NcS.js → RunButton-BrmXVv2P.js} +1 -1
- package/dist/assets/{abnfDiagram-VCTEODGH-PyDjTcAu.js → abnfDiagram-VCTEODGH-BpHqqTyl.js} +1 -1
- package/dist/assets/{add-cell-with-ai-tO48yvEp.js → add-cell-with-ai-C5sHUrPl.js} +1 -1
- package/dist/assets/{agent-panel--Uggj5Hh.js → agent-panel-DeEDWiBd.js} +1 -1
- package/dist/assets/{ai-model-dropdown-B2895uWy.js → ai-model-dropdown-DSmOGWnF.js} +1 -1
- package/dist/assets/{any-language-editor-SElpZZmr.js → any-language-editor-qMiqHqI6.js} +1 -1
- package/dist/assets/{app-config-button-7l6_h8qm.js → app-config-button-B3TpWybC.js} +1 -1
- package/dist/assets/{architectureDiagram-5GKGNRK7-ClNG0RtM.js → architectureDiagram-5GKGNRK7-B5HmHPrf.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-DKQqgIZZ.js → blockDiagram-I7D4REHJ-2vuAAFxC.js} +1 -1
- package/dist/assets/{cache-panel-B4cX0gnO.js → cache-panel-B5FGKL23.js} +1 -1
- package/dist/assets/{chat-display-BAd8VNbf.js → chat-display-BC7DzAiv.js} +1 -1
- package/dist/assets/{chat-panel-AB7e9YqK.js → chat-panel-DDS9gK8U.js} +1 -1
- package/dist/assets/{chat-ui-BfY1Ovza.js → chat-ui-BD5izHBg.js} +1 -1
- package/dist/assets/{chunk-IMKFNOWR-ia6HTICI.js → chunk-IMKFNOWR-NGm5mJNS.js} +1 -1
- package/dist/assets/{chunk-SHT3W25Y-pkO_A7D_.js → chunk-SHT3W25Y-BzwSplJT.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-TdSDHW7N.js → chunk-SVP7TREG-BypgwLM_.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-Dkx1TAGF.js → chunk-TICWLB2K-N2t-AIjK.js} +1 -1
- package/dist/assets/{classDiagram-ZZMXUADV-BOjxdJXX.js → classDiagram-ZZMXUADV-CDs41IFC.js} +1 -1
- package/dist/assets/{classDiagram-v2-VYDZK3BY-BOjxdJXX.js → classDiagram-v2-VYDZK3BY-CDs41IFC.js} +1 -1
- package/dist/assets/{code-visibility-DuOSJvRx.js → code-visibility-q98Ep58Q.js} +1 -1
- package/dist/assets/{command-palette-CtM1nJeI.js → command-palette-C8Or-XyR.js} +1 -1
- package/dist/assets/{common-CfiFJljq.js → common-BPz_nWVh.js} +1 -1
- package/dist/assets/{components-cjvdDhFZ.js → components-CjOJll5R.js} +1 -1
- package/dist/assets/{components-DtYgPfLl.js → components-tngYJ6wd.js} +1 -1
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-w0GYTJKT.js → cynefinDiagram-5FMLGOSQ-DWkCdfsX.js} +1 -1
- package/dist/assets/{dagre-5EzkIBSB.js → dagre-DfoWhgrm.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-CrrpSCQu.js → dagre-GXQ25YYZ-HEwpYrVf.js} +1 -1
- package/dist/assets/{data-grid-overlay-editor-DdLsakrp.js → data-grid-overlay-editor-Dc6Hgeqg.js} +1 -1
- package/dist/assets/{dependency-graph-panel-C6-QKk5w.js → dependency-graph-panel-DYqM639f.js} +1 -1
- package/dist/assets/{diagram-S7CK7UJ4-DHizxkTS.js → diagram-S7CK7UJ4-imd4kIPI.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-Dy9mCIjz.js → diagram-UQ7AKVKN-DX3w4euf.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-IvciZBJC.js → diagram-VX7I27RA-CTWSZFmW.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-BDROaDbo.js → diagram-Z3DM3KII-C1LY-pUZ.js} +1 -1
- package/dist/assets/{documentation-panel-CHl8gIND.js → documentation-panel-BrQMzwWC.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-BaDP6CF_.js → ebnfDiagram-PWID7BFC-Cj3XFdrK.js} +1 -1
- package/dist/assets/{edit-page-BxkxPkpI.js → edit-page-LWZvyJpx.js} +6 -6
- package/dist/assets/{erDiagram-RLTQ6QDP-_6Yvwauu.js → erDiagram-RLTQ6QDP-C1UEPUjb.js} +1 -1
- package/dist/assets/{error-panel-BUJkiVkN.js → error-panel-CHfNFsxb.js} +1 -1
- package/dist/assets/{feedback-button-CO3YF_kw.js → feedback-button-B1QzFFCH.js} +1 -1
- package/dist/assets/{file-explorer-panel-B5FBrKb9.js → file-explorer-panel-CWwil53w.js} +1 -1
- package/dist/assets/{file-name-input-DqTHxhqa.js → file-name-input-4aagDTRQ.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-C-fpnEZ1.js → flowDiagram-HODETNUW-BfmO9lx7.js} +1 -1
- package/dist/assets/{gallery-page-D1_kJQDc.js → gallery-page-DovFAtws.js} +1 -1
- package/dist/assets/{glide-data-editor-XKv1XCLW.js → glide-data-editor-CZ1yTY-A.js} +4 -4
- package/dist/assets/{graphlib-DCukLgUW.js → graphlib-COGycCGs.js} +1 -1
- package/dist/assets/has-BXSzylJ4.js +1 -0
- package/dist/assets/{home-page-BUBtpUU4.js → home-page-BVI4vz16.js} +1 -1
- package/dist/assets/{index-CwgxYBE_.css → index-7pH7YeEh.css} +1 -1
- package/dist/assets/{index-B3V-vbeG.js → index-D4vDmlOv.js} +35 -35
- package/dist/assets/{infoDiagram-27XIBGKW-BFwfMBMl.js → infoDiagram-27XIBGKW-CnXcbBM7.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-CuItxXBC.js → ishikawaDiagram-5VMMS53U-22vIfKRp.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-D9onK4Qd.js → kanban-definition-UXKFOSKX-mKENIfGJ.js} +1 -1
- package/dist/assets/{logs-panel-Doy14GPL.js → logs-panel-BztBG5KX.js} +1 -1
- package/dist/assets/{mermaid-BH218Xhr.js → mermaid-oS3_13w-.js} +4 -4
- package/dist/assets/{mindmap-definition-YA3MSWOX-CHmYRbt7.js → mindmap-definition-YA3MSWOX-C1gdLZ40.js} +1 -1
- package/dist/assets/{number-overlay-editor-DQT_U43U.js → number-overlay-editor-DBSA3_TU.js} +1 -1
- package/dist/assets/{outline-panel-lmYE9C1P.js → outline-panel-D3hi06cs.js} +1 -1
- package/dist/assets/{packages-panel-Cme-dX9I.js → packages-panel-vesZNMON.js} +1 -1
- package/dist/assets/{panels-CZe_V_Qm.js → panels-ChQ3xLqg.js} +3 -3
- package/dist/assets/{pegDiagram-XKGWAZYB-BhVrXtWh.js → pegDiagram-XKGWAZYB-C2pbV6gl.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-D8Rs7E-X.js → pieDiagram-E7YTZNPT-BLRvQIlm.js} +1 -1
- package/dist/assets/{process-output-CcoazjdT.js → process-output-Cst3k00f.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-BliEdOa_.js → railroadDiagram-O6MQD6OU-Ccb_UTLI.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-BZgpwhwu.js → requirementDiagram-BXWQKSXE-DYIZcvT8.js} +1 -1
- package/dist/assets/{reveal-component-BOIC-1C9.js → reveal-component-CZXWnwl9.js} +1 -1
- package/dist/assets/{run-page-5LmUK9-b.js → run-page-DqkpD0V9.js} +1 -1
- package/dist/assets/{scratchpad-panel-DCm1vfOe.js → scratchpad-panel-2Fxg0pNx.js} +1 -1
- package/dist/assets/{secrets-panel-C0M9FPqa.js → secrets-panel-K57okm8Y.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-BSCidaOG.js → sequenceDiagram-WJ2MYXX4-CgICltvO.js} +1 -1
- package/dist/assets/{session-panel-D-OHverx.js → session-panel-CBnzFj6y.js} +1 -1
- package/dist/assets/{snippets-panel-BIaO8vsG.js → snippets-panel-DaE8aspJ.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-CdrwuVVW.js → stateDiagram-D77RDMKH-B6Kxq-9y.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-CXsak2Yr.js → stateDiagram-v2-MP3YSRHH-CSaW3Hg-.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-RsZrbnba.js → swimlanes-42K2YHIH-Dt0LP1_e.js} +1 -1
- package/dist/assets/{swimlanesDiagram-VR7AAH4N-BPnf-Uio.js → swimlanesDiagram-VR7AAH4N-2Ad8ds_3.js} +1 -1
- package/dist/assets/{swiper-component-CzXR7RtI.js → swiper-component-CFnIXlkd.js} +1 -1
- package/dist/assets/{terminal-D3jqON96.js → terminal-CkFp7KSb.js} +1 -1
- package/dist/assets/{timeline-definition-24CTP7MA-BV_fj6er.js → timeline-definition-24CTP7MA-DEY-0YjY.js} +1 -1
- package/dist/assets/{tracing-D06euID_.js → tracing-DBHLwBmf.js} +1 -1
- package/dist/assets/{tracing-panel-BTZrV6gm.js → tracing-panel-DvYpwbLM.js} +2 -2
- package/dist/assets/{useCellActionButton-De1Hp_Ck.js → useCellActionButton-CV3PUvLU.js} +1 -1
- package/dist/assets/{vega-component-DXGcKFo9.js → vega-component-QgmUboT9.js} +1 -1
- package/dist/assets/{vega-embed-container-B7oFGUzj.js → vega-embed-container-ClWegE2x.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-bblIhByE.js → vennDiagram-4TSXK5OY-cwn4u1pT.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-BXDlrvdT.js → wardleyDiagram-VM6X3IG4-DKYGzFAa.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-DNR5BUGa.js → xychartDiagram-S5SC5T6Z-BFGsu-E4.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/src/components/static-html/__tests__/share-modal.test.tsx +154 -0
- package/src/components/static-html/__tests__/stage-for-publish.test.ts +111 -0
- package/src/components/static-html/share-modal.tsx +114 -145
- package/src/components/static-html/stage-for-publish.ts +56 -0
- package/src/core/network/__tests__/export-asset-url.test.ts +45 -0
- package/src/core/network/export-asset-url.ts +19 -0
- package/src/core/network/requests-network.ts +2 -7
- package/src/core/wasm/bridge.ts +2 -7
- package/dist/assets/has-BH1-FiQK.js +0 -1
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { MAX_PUBLISH_BYTES, stageForPublish } from "../stage-for-publish";
|
|
5
|
+
|
|
6
|
+
// Opaque to the client; the shapes here are deliberately not molab's.
|
|
7
|
+
const PENDING = {
|
|
8
|
+
pendingId: "id-1",
|
|
9
|
+
presignedUrl: "https://upload.example/put?token=abc",
|
|
10
|
+
claimUrl: "https://claim.example/abc",
|
|
11
|
+
expiresAt: "2026-01-01T00:00:00Z",
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function jsonResponse(body: unknown, status = 200): Response {
|
|
15
|
+
return new Response(JSON.stringify(body), {
|
|
16
|
+
status,
|
|
17
|
+
headers: { "Content-Type": "application/json" },
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe("stageForPublish", () => {
|
|
22
|
+
const fetchMock = vi.fn<typeof fetch>();
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(() => {
|
|
29
|
+
fetchMock.mockReset();
|
|
30
|
+
vi.unstubAllGlobals();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("reserves a pending upload, PUTs the export, and returns the claim URL", async () => {
|
|
34
|
+
fetchMock
|
|
35
|
+
.mockResolvedValueOnce(jsonResponse(PENDING))
|
|
36
|
+
.mockResolvedValueOnce(new Response(null, { status: 200 }));
|
|
37
|
+
|
|
38
|
+
const claimUrl = await stageForPublish("nb.html", "<html>hi</html>");
|
|
39
|
+
|
|
40
|
+
expect(claimUrl).toBe(PENDING.claimUrl);
|
|
41
|
+
expect(fetchMock).toHaveBeenCalledTimes(2);
|
|
42
|
+
|
|
43
|
+
const [stageUrl, stageInit] = fetchMock.mock.calls[0];
|
|
44
|
+
expect(String(stageUrl)).toMatch(/\/api\/artifacts\/pending$/);
|
|
45
|
+
expect(stageInit?.method).toBe("POST");
|
|
46
|
+
expect(JSON.parse(String(stageInit?.body))).toEqual({
|
|
47
|
+
fileName: "nb.html",
|
|
48
|
+
fileSize: new Blob(["<html>hi</html>"]).size,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const [putUrl, putInit] = fetchMock.mock.calls[1];
|
|
52
|
+
expect(putUrl).toBe(PENDING.presignedUrl);
|
|
53
|
+
expect(putInit?.method).toBe("PUT");
|
|
54
|
+
expect(putInit?.body).toBeInstanceOf(Blob);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("surfaces the server's error message when staging is rejected", async () => {
|
|
58
|
+
fetchMock.mockResolvedValueOnce(
|
|
59
|
+
jsonResponse({ error: "Only .html or .htm files are supported" }, 400),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
await expect(stageForPublish("nb.txt", "x")).rejects.toThrow(
|
|
63
|
+
"Only .html or .htm files are supported",
|
|
64
|
+
);
|
|
65
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("explains a 429 when the body carries no message", async () => {
|
|
69
|
+
fetchMock.mockResolvedValueOnce(new Response("", { status: 429 }));
|
|
70
|
+
|
|
71
|
+
await expect(stageForPublish("nb.html", "x")).rejects.toThrow(
|
|
72
|
+
"Too many uploads, try again later",
|
|
73
|
+
);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("falls back to a generic message for other bodiless failures", async () => {
|
|
77
|
+
fetchMock.mockResolvedValueOnce(new Response("", { status: 500 }));
|
|
78
|
+
|
|
79
|
+
await expect(stageForPublish("nb.html", "x")).rejects.toThrow(
|
|
80
|
+
"Failed to prepare the upload",
|
|
81
|
+
);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("fails when the presigned PUT is rejected", async () => {
|
|
85
|
+
fetchMock
|
|
86
|
+
.mockResolvedValueOnce(jsonResponse(PENDING))
|
|
87
|
+
.mockResolvedValueOnce(new Response(null, { status: 403 }));
|
|
88
|
+
|
|
89
|
+
await expect(stageForPublish("nb.html", "x")).rejects.toThrow(
|
|
90
|
+
"Failed to upload the notebook export",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("rejects exports over the size limit before touching the network", async () => {
|
|
95
|
+
// Stub Blob so the test doesn't allocate 100 MB.
|
|
96
|
+
const RealBlob = globalThis.Blob;
|
|
97
|
+
vi.stubGlobal(
|
|
98
|
+
"Blob",
|
|
99
|
+
class extends RealBlob {
|
|
100
|
+
override get size() {
|
|
101
|
+
return MAX_PUBLISH_BYTES + 1;
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
await expect(stageForPublish("nb.html", "x")).rejects.toThrow(
|
|
107
|
+
"File is too large. Maximum size is 100 MB.",
|
|
108
|
+
);
|
|
109
|
+
expect(fetchMock).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import React
|
|
3
|
+
import { ExternalLinkIcon } from "lucide-react";
|
|
4
|
+
import type React from "react";
|
|
5
|
+
import { useState } from "react";
|
|
5
6
|
import { Button } from "@/components/ui/button";
|
|
6
7
|
import {
|
|
7
8
|
DialogContent,
|
|
@@ -10,176 +11,144 @@ import {
|
|
|
10
11
|
DialogHeader,
|
|
11
12
|
DialogTitle,
|
|
12
13
|
} from "@/components/ui/dialog";
|
|
13
|
-
import { toast } from "@/components/ui/use-toast";
|
|
14
14
|
import { Constants } from "@/core/constants";
|
|
15
15
|
import { getExportLayout } from "@/core/export/layout";
|
|
16
16
|
import { useRequestClient } from "@/core/network/requests";
|
|
17
17
|
import { VirtualFileTracker } from "@/core/static/virtual-file-tracker";
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
import { Spinner } from "../icons/spinner";
|
|
19
|
+
import { stageForPublish } from "./stage-for-publish";
|
|
20
|
+
|
|
21
|
+
type PublishState =
|
|
22
|
+
| { kind: "idle" }
|
|
23
|
+
| { kind: "publishing" }
|
|
24
|
+
| { kind: "staged"; claimUrl: string }
|
|
25
|
+
| { kind: "failed"; message: string };
|
|
22
26
|
|
|
23
|
-
const BASE_URL = "https://static.marimo.app";
|
|
24
27
|
export const ShareStaticNotebookModal: React.FC<{
|
|
25
28
|
onClose: () => void;
|
|
26
29
|
}> = ({ onClose }) => {
|
|
27
|
-
const [
|
|
30
|
+
const [state, setState] = useState<PublishState>({ kind: "idle" });
|
|
28
31
|
const { exportAsHTML } = useRequestClient();
|
|
29
32
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
onClose();
|
|
45
|
-
const { contents: html } = await exportAsHTML({
|
|
46
|
-
download: false,
|
|
47
|
-
includeCode: true,
|
|
48
|
-
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
49
|
-
layout: await getExportLayout(),
|
|
50
|
-
});
|
|
33
|
+
const handlePublish = async () => {
|
|
34
|
+
setState({ kind: "publishing" });
|
|
35
|
+
try {
|
|
36
|
+
const { contents: html, filename } = await exportAsHTML({
|
|
37
|
+
download: false,
|
|
38
|
+
includeCode: true,
|
|
39
|
+
files: VirtualFileTracker.INSTANCE.filenames(),
|
|
40
|
+
layout: await getExportLayout(),
|
|
41
|
+
// Published notebooks are served from a remote origin, so assets must
|
|
42
|
+
// come from the CDN — never the local server (which is the dev-mode
|
|
43
|
+
// default). The server substitutes {version}.
|
|
44
|
+
assetUrl:
|
|
45
|
+
"https://cdn.jsdelivr.net/npm/@marimo-team/frontend@{version}/dist",
|
|
46
|
+
});
|
|
51
47
|
|
|
52
|
-
|
|
53
|
-
title: "Uploading static notebook...",
|
|
54
|
-
description: "Please wait.",
|
|
55
|
-
});
|
|
48
|
+
const claimUrl = await stageForPublish(filename, html);
|
|
56
49
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
title: "Error uploading static page",
|
|
70
|
-
description: (
|
|
71
|
-
<div>
|
|
72
|
-
Please try again later. If the problem persists, please file a
|
|
73
|
-
bug report on{" "}
|
|
74
|
-
<a
|
|
75
|
-
href={Constants.issuesPage}
|
|
76
|
-
target="_blank"
|
|
77
|
-
rel="noreferrer"
|
|
78
|
-
className="underline"
|
|
79
|
-
>
|
|
80
|
-
GitHub
|
|
81
|
-
</a>
|
|
82
|
-
.
|
|
83
|
-
</div>
|
|
84
|
-
),
|
|
85
|
-
});
|
|
86
|
-
});
|
|
50
|
+
// Best effort: after the async work above, browsers may treat this as a
|
|
51
|
+
// popup and block it. The dialog keeps the link either way.
|
|
52
|
+
window.open(claimUrl, "_blank", "noopener,noreferrer");
|
|
53
|
+
setState({ kind: "staged", claimUrl });
|
|
54
|
+
} catch (error) {
|
|
55
|
+
setState({
|
|
56
|
+
kind: "failed",
|
|
57
|
+
message:
|
|
58
|
+
error instanceof Error ? error.message : "Something went wrong",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
};
|
|
87
62
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
description: (
|
|
92
|
-
<div>
|
|
93
|
-
The URL has been copied to your clipboard.
|
|
94
|
-
<br />
|
|
95
|
-
You can share it with anyone.
|
|
96
|
-
</div>
|
|
97
|
-
),
|
|
98
|
-
});
|
|
99
|
-
}}
|
|
100
|
-
>
|
|
63
|
+
if (state.kind === "staged") {
|
|
64
|
+
return (
|
|
65
|
+
<DialogContent className="sm:max-w-[480px]">
|
|
101
66
|
<DialogHeader>
|
|
102
|
-
<DialogTitle>
|
|
67
|
+
<DialogTitle>Finish publishing</DialogTitle>
|
|
103
68
|
<DialogDescription>
|
|
104
|
-
|
|
105
|
-
to the public web. We will create a link for you that lives on{" "}
|
|
106
|
-
<a href={BASE_URL} target="_blank" rel="noreferrer">
|
|
107
|
-
{BASE_URL}
|
|
108
|
-
</a>
|
|
109
|
-
.
|
|
69
|
+
If the confirmation page didn't open in a new tab, open it below.
|
|
110
70
|
</DialogDescription>
|
|
111
71
|
</DialogHeader>
|
|
112
|
-
<div className="flex flex-col gap-6 py-4">
|
|
113
|
-
<Input
|
|
114
|
-
data-testid="slug-input"
|
|
115
|
-
id="slug"
|
|
116
|
-
autoFocus={true}
|
|
117
|
-
value={slug}
|
|
118
|
-
placeholder="Notebook slug"
|
|
119
|
-
onChange={(e) => {
|
|
120
|
-
const newSlug = e.target.value
|
|
121
|
-
.toLowerCase()
|
|
122
|
-
.replaceAll(/\s/g, "-")
|
|
123
|
-
.replaceAll(/[^\da-z-]/g, "");
|
|
124
|
-
setSlug(newSlug);
|
|
125
|
-
}}
|
|
126
|
-
required={true}
|
|
127
|
-
autoComplete="off"
|
|
128
|
-
/>
|
|
129
|
-
|
|
130
|
-
<div className="font-semibold text-sm text-muted-foreground gap-2 flex flex-col">
|
|
131
|
-
Anyone will be able to access your notebook at this URL:
|
|
132
|
-
<div className="flex items-center gap-2">
|
|
133
|
-
<CopyButton text={url} />
|
|
134
|
-
<span className="text-primary">{url}</span>
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
72
|
<DialogFooter>
|
|
139
73
|
<Button
|
|
140
|
-
data-testid="
|
|
74
|
+
data-testid="done-share-static-notebook-button"
|
|
141
75
|
variant="secondary"
|
|
142
76
|
onClick={onClose}
|
|
143
77
|
>
|
|
144
|
-
|
|
78
|
+
Done
|
|
145
79
|
</Button>
|
|
146
|
-
<Button
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
80
|
+
<Button asChild={true} variant="default">
|
|
81
|
+
<a
|
|
82
|
+
data-testid="open-claim-page-link"
|
|
83
|
+
href={state.claimUrl}
|
|
84
|
+
target="_blank"
|
|
85
|
+
rel="noopener noreferrer"
|
|
86
|
+
>
|
|
87
|
+
<ExternalLinkIcon size={14} strokeWidth={1.5} className="mr-2" />
|
|
88
|
+
Open confirmation page
|
|
89
|
+
</a>
|
|
156
90
|
</Button>
|
|
157
91
|
</DialogFooter>
|
|
158
|
-
</
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
const CopyButton = (props: { text: string }) => {
|
|
164
|
-
const [copied, setCopied] = React.useState(false);
|
|
92
|
+
</DialogContent>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
165
95
|
|
|
166
|
-
const
|
|
167
|
-
e.preventDefault();
|
|
168
|
-
await copyToClipboard(props.text);
|
|
169
|
-
setCopied(true);
|
|
170
|
-
setTimeout(() => setCopied(false), 2000);
|
|
171
|
-
});
|
|
96
|
+
const isPublishing = state.kind === "publishing";
|
|
172
97
|
|
|
173
98
|
return (
|
|
174
|
-
<
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
99
|
+
<DialogContent className="sm:max-w-[480px]">
|
|
100
|
+
<DialogHeader>
|
|
101
|
+
<DialogTitle>Publish HTML to web</DialogTitle>
|
|
102
|
+
<DialogDescription>
|
|
103
|
+
Publish a static snapshot of this notebook to{" "}
|
|
104
|
+
<a
|
|
105
|
+
href={Constants.molab}
|
|
106
|
+
target="_blank"
|
|
107
|
+
rel="noopener noreferrer"
|
|
108
|
+
className="underline"
|
|
109
|
+
>
|
|
110
|
+
molab
|
|
111
|
+
</a>
|
|
112
|
+
. Snapshots can be viewed by anyone on the internet.
|
|
113
|
+
</DialogDescription>
|
|
114
|
+
</DialogHeader>
|
|
115
|
+
{state.kind === "failed" && (
|
|
116
|
+
<p
|
|
117
|
+
role="alert"
|
|
118
|
+
className="text-sm text-destructive"
|
|
119
|
+
data-testid="share-static-notebook-error"
|
|
120
|
+
>
|
|
121
|
+
Publish failed: {state.message}
|
|
122
|
+
</p>
|
|
123
|
+
)}
|
|
124
|
+
<DialogFooter>
|
|
125
|
+
<Button
|
|
126
|
+
data-testid="cancel-share-static-notebook-button"
|
|
127
|
+
variant="secondary"
|
|
128
|
+
onClick={onClose}
|
|
129
|
+
disabled={isPublishing}
|
|
130
|
+
>
|
|
131
|
+
Cancel
|
|
132
|
+
</Button>
|
|
133
|
+
<Button
|
|
134
|
+
data-testid="share-static-notebook-button"
|
|
135
|
+
variant="default"
|
|
136
|
+
disabled={isPublishing}
|
|
137
|
+
onClick={handlePublish}
|
|
138
|
+
>
|
|
139
|
+
{isPublishing ? (
|
|
140
|
+
<>
|
|
141
|
+
<Spinner className="mr-2" size="small" />
|
|
142
|
+
Snapshotting notebook ...
|
|
143
|
+
</>
|
|
144
|
+
) : (
|
|
145
|
+
<>
|
|
146
|
+
<ExternalLinkIcon size={14} strokeWidth={1.5} className="mr-2" />
|
|
147
|
+
{state.kind === "failed" ? "Try again" : "Publish"}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
</Button>
|
|
151
|
+
</DialogFooter>
|
|
152
|
+
</DialogContent>
|
|
184
153
|
);
|
|
185
154
|
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { Constants } from "@/core/constants";
|
|
4
|
+
|
|
5
|
+
interface PendingUploadResponse {
|
|
6
|
+
pendingId: string;
|
|
7
|
+
presignedUrl: string;
|
|
8
|
+
claimUrl: string;
|
|
9
|
+
expiresAt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Keep in sync with molab.
|
|
13
|
+
export const MAX_PUBLISH_BYTES = 100 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Stage an HTML export for publishing on molab: reserve a pending upload,
|
|
17
|
+
* PUT the bytes to the presigned URL, and return the claim URL where the
|
|
18
|
+
* user finishes publishing after signing in.
|
|
19
|
+
*/
|
|
20
|
+
export async function stageForPublish(
|
|
21
|
+
fileName: string,
|
|
22
|
+
html: string,
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const blob = new Blob([html], { type: "text/html" });
|
|
25
|
+
if (blob.size > MAX_PUBLISH_BYTES) {
|
|
26
|
+
throw new Error("File is too large. Maximum size is 100 MB.");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const staged = await fetch(`${Constants.molab}/api/artifacts/pending`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify({ fileName, fileSize: blob.size }),
|
|
33
|
+
});
|
|
34
|
+
if (!staged.ok) {
|
|
35
|
+
const body = (await staged.json().catch(() => null)) as {
|
|
36
|
+
error?: string;
|
|
37
|
+
} | null;
|
|
38
|
+
throw new Error(
|
|
39
|
+
body?.error ??
|
|
40
|
+
(staged.status === 429
|
|
41
|
+
? "Too many uploads, try again later"
|
|
42
|
+
: "Failed to prepare the upload"),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
const { presignedUrl, claimUrl } =
|
|
46
|
+
(await staged.json()) as PendingUploadResponse;
|
|
47
|
+
|
|
48
|
+
// The presigned PUT is capped to the declared size; the browser sends the
|
|
49
|
+
// matching Content-Length automatically from the blob.
|
|
50
|
+
const put = await fetch(presignedUrl, { method: "PUT", body: blob });
|
|
51
|
+
if (!put.ok) {
|
|
52
|
+
throw new Error("Failed to upload the notebook export");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return claimUrl;
|
|
56
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { withDevAssetUrl } from "../export-asset-url";
|
|
5
|
+
import type { ExportAsHTMLRequest } from "../types";
|
|
6
|
+
|
|
7
|
+
const REQUEST: ExportAsHTMLRequest = {
|
|
8
|
+
download: false,
|
|
9
|
+
includeCode: true,
|
|
10
|
+
files: [],
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
describe("withDevAssetUrl", () => {
|
|
14
|
+
const originalEnv = process.env.NODE_ENV;
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
process.env.NODE_ENV = originalEnv;
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it.each(["development", "test"])(
|
|
21
|
+
"defaults assetUrl to the dev server in %s",
|
|
22
|
+
(env) => {
|
|
23
|
+
process.env.NODE_ENV = env;
|
|
24
|
+
expect(withDevAssetUrl(REQUEST).assetUrl).toBe(window.location.origin);
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
it("leaves a caller-provided assetUrl alone in dev", () => {
|
|
29
|
+
process.env.NODE_ENV = "development";
|
|
30
|
+
const cdn = "https://cdn.example.com/dist";
|
|
31
|
+
expect(withDevAssetUrl({ ...REQUEST, assetUrl: cdn }).assetUrl).toBe(cdn);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("does nothing in production", () => {
|
|
35
|
+
process.env.NODE_ENV = "production";
|
|
36
|
+
expect(withDevAssetUrl(REQUEST)).toBe(REQUEST);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("does not mutate the caller's request", () => {
|
|
40
|
+
process.env.NODE_ENV = "development";
|
|
41
|
+
const request = { ...REQUEST };
|
|
42
|
+
withDevAssetUrl(request);
|
|
43
|
+
expect(request.assetUrl).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import type { ExportAsHTMLRequest } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* In dev/test the CDN has no assets for the local build, so exports default
|
|
7
|
+
* to the dev server. Callers publishing to a remote origin pass their own
|
|
8
|
+
* assetUrl, which is left untouched.
|
|
9
|
+
*/
|
|
10
|
+
export function withDevAssetUrl(
|
|
11
|
+
request: ExportAsHTMLRequest,
|
|
12
|
+
): ExportAsHTMLRequest {
|
|
13
|
+
const isDevOrTest =
|
|
14
|
+
process.env.NODE_ENV === "development" || process.env.NODE_ENV === "test";
|
|
15
|
+
if (request.assetUrl == null && isDevOrTest) {
|
|
16
|
+
return { ...request, assetUrl: window.location.origin };
|
|
17
|
+
}
|
|
18
|
+
return request;
|
|
19
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
waitForConnectionOpen,
|
|
12
12
|
waitForConnectionOpenIfNotebook,
|
|
13
13
|
} from "./connection";
|
|
14
|
+
import { withDevAssetUrl } from "./export-asset-url";
|
|
14
15
|
import type { EditRequests, RunRequests } from "./types";
|
|
15
16
|
|
|
16
17
|
/**
|
|
@@ -430,15 +431,9 @@ export function createNetworkRequests(): EditRequests & RunRequests {
|
|
|
430
431
|
.then(handleResponse);
|
|
431
432
|
},
|
|
432
433
|
exportAsHTML: async (request) => {
|
|
433
|
-
if (
|
|
434
|
-
process.env.NODE_ENV === "development" ||
|
|
435
|
-
process.env.NODE_ENV === "test"
|
|
436
|
-
) {
|
|
437
|
-
request.assetUrl = window.location.origin;
|
|
438
|
-
}
|
|
439
434
|
return getClient()
|
|
440
435
|
.POST("/api/export/html", {
|
|
441
|
-
body: request,
|
|
436
|
+
body: withDevAssetUrl(request),
|
|
442
437
|
parseAs: "text",
|
|
443
438
|
params: getParams(),
|
|
444
439
|
})
|
package/src/core/wasm/bridge.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { notebookIsRunningAtom } from "../cells/cells";
|
|
|
14
14
|
import type { CommandMessage } from "../kernel/messages";
|
|
15
15
|
import { getInitialAppMode } from "../mode";
|
|
16
16
|
import { API } from "../network/api";
|
|
17
|
+
import { withDevAssetUrl } from "../network/export-asset-url";
|
|
17
18
|
import type {
|
|
18
19
|
EditRequests,
|
|
19
20
|
EnvironmentInfo,
|
|
@@ -535,15 +536,9 @@ export class PyodideBridge implements RunRequests, EditRequests {
|
|
|
535
536
|
request: ExportAsHTMLRequest,
|
|
536
537
|
) => {
|
|
537
538
|
await this.pendingSessionSave;
|
|
538
|
-
if (
|
|
539
|
-
process.env.NODE_ENV === "development" ||
|
|
540
|
-
process.env.NODE_ENV === "test"
|
|
541
|
-
) {
|
|
542
|
-
request.assetUrl = window.location.origin;
|
|
543
|
-
}
|
|
544
539
|
const response = await this.rpc.proxy.request.bridge({
|
|
545
540
|
functionName: "export_html",
|
|
546
|
-
payload: request,
|
|
541
|
+
payload: withDevAssetUrl(request),
|
|
547
542
|
});
|
|
548
543
|
return response as ExportedFile<string>;
|
|
549
544
|
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{Ua as e}from"./index-B3V-vbeG.js";var t=Object.prototype.hasOwnProperty;function n(e,n){return e!=null&&t.call(e,n)}function r(t,r){return t!=null&&e(t,r,n)}export{r as t};
|