@marimo-team/frontend 0.24.3-dev36 → 0.24.3-dev39
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-CsA_EqMs.js → ConnectedDataExplorerComponent-B52ZeTyE.js} +1 -1
- package/dist/assets/{LazyAnyLanguageCodeMirror-jAV-LD6v.js → LazyAnyLanguageCodeMirror-DLXghRUF.js} +2 -2
- package/dist/assets/{RunButton-Bo4tYCqW.js → RunButton-BQonXEus.js} +7 -7
- package/dist/assets/{abnfDiagram-VCTEODGH-Dpe20TKk.js → abnfDiagram-VCTEODGH-DAeh_Fhj.js} +1 -1
- package/dist/assets/{add-cell-with-ai-CkjbbM3K.js → add-cell-with-ai-BqPNrEkg.js} +10 -10
- package/dist/assets/{agent-panel-CJ__bZ2U.js → agent-panel-0QpsWJ6N.js} +4 -4
- package/dist/assets/{ai-model-dropdown-CFjRFxPs.js → ai-model-dropdown-JyOkbRgs.js} +2 -2
- package/dist/assets/{any-language-editor-CVvssaI-.js → any-language-editor-DktznOiF.js} +1 -1
- package/dist/assets/app-config-button-hlzBggjV.js +1 -0
- package/dist/assets/{architectureDiagram-5GKGNRK7-LjPvmKXz.js → architectureDiagram-5GKGNRK7-CLV_t53p.js} +1 -1
- package/dist/assets/{blockDiagram-I7D4REHJ-Dcw3YZc7.js → blockDiagram-I7D4REHJ-4NgsF8dG.js} +1 -1
- package/dist/assets/cache-panel-JD-3Sv8N.js +1 -0
- package/dist/assets/{cells-DcI93as7.js → cells-CP3hwqVX.js} +5 -5
- package/dist/assets/{chat-display-BxoFsvsk.js → chat-display-f6K2vP1M.js} +1 -1
- package/dist/assets/{chat-panel-OPXY9xqa.js → chat-panel-CPSz0Ygy.js} +3 -3
- package/dist/assets/{chat-ui-B7Op0m5y.js → chat-ui-DJskwz2p.js} +3 -3
- package/dist/assets/{chunk-IMKFNOWR-BhKJw_u0.js → chunk-IMKFNOWR-gOOmxUZk.js} +1 -1
- package/dist/assets/{chunk-SHT3W25Y-BLq6CG7S.js → chunk-SHT3W25Y-Vz0rwhW1.js} +1 -1
- package/dist/assets/{chunk-SVP7TREG-DYlo9xS8.js → chunk-SVP7TREG-DXVPjbAe.js} +1 -1
- package/dist/assets/{chunk-TICWLB2K-D5DDJ9ap.js → chunk-TICWLB2K-DwIBziwV.js} +1 -1
- package/dist/assets/{classDiagram-ZZMXUADV-Cpgr9tEc.js → classDiagram-ZZMXUADV-CyUk9nSP.js} +1 -1
- package/dist/assets/{classDiagram-v2-VYDZK3BY-Cpgr9tEc.js → classDiagram-v2-VYDZK3BY-CyUk9nSP.js} +1 -1
- package/dist/assets/{command-palette-BBhAYPL7.js → command-palette-B8zE6Ssx.js} +1 -1
- package/dist/assets/{common-DGrIhswR.js → common-CHWS0pGT.js} +1 -1
- package/dist/assets/{components-BzThHPL8.js → components-B8fYemsk.js} +1 -1
- package/dist/assets/{components-XxbsL-NL.js → components-v8nHOj_y.js} +1 -1
- package/dist/assets/config-CBLBAU5c.js +1 -0
- package/dist/assets/{cynefinDiagram-5FMLGOSQ-cA8HNBWT.js → cynefinDiagram-5FMLGOSQ-DgdyjbQj.js} +1 -1
- package/dist/assets/{dagre-Cgh-CozI.js → dagre-CPZ1FUhr.js} +1 -1
- package/dist/assets/{dagre-GXQ25YYZ-DMu-4gaX.js → dagre-GXQ25YYZ-D-zMjnRl.js} +1 -1
- package/dist/assets/{data-grid-overlay-editor-o8dOZGaO.js → data-grid-overlay-editor-CFK1KT2V.js} +1 -1
- package/dist/assets/{dependency-graph-panel-DSfkNvTe.js → dependency-graph-panel-D_AqnRMs.js} +3 -3
- package/dist/assets/{diagram-S7CK7UJ4-0g042cjW.js → diagram-S7CK7UJ4-DgflUEq5.js} +1 -1
- package/dist/assets/{diagram-UQ7AKVKN-CoeYBFyV.js → diagram-UQ7AKVKN-m3R2UDJM.js} +1 -1
- package/dist/assets/{diagram-VX7I27RA-CIbkeHtW.js → diagram-VX7I27RA-Blh2o8ik.js} +1 -1
- package/dist/assets/{diagram-Z3DM3KII-VNvdSBDs.js → diagram-Z3DM3KII-C4PcKTD6.js} +1 -1
- package/dist/assets/{documentation-panel-BYssFEdu.js → documentation-panel-CoSoUakB.js} +1 -1
- package/dist/assets/{ebnfDiagram-PWID7BFC-gn-sVFpd.js → ebnfDiagram-PWID7BFC-BGtaPv5f.js} +1 -1
- package/dist/assets/edit-page-Qqpz6lsF.js +184 -0
- package/dist/assets/{erDiagram-RLTQ6QDP-CHEeDG3E.js → erDiagram-RLTQ6QDP-D8JVfIur.js} +1 -1
- package/dist/assets/{error-panel-BEyUxPKv.js → error-panel-CdqCDhu4.js} +1 -1
- package/dist/assets/{file-explorer-panel-BkidiqtU.js → file-explorer-panel-DcWEU9mt.js} +1 -1
- package/dist/assets/{file-name-input-DRW2aJnO.js → file-name-input-BQw4Jo-i.js} +1 -1
- package/dist/assets/{flowDiagram-HODETNUW-juSojTPv.js → flowDiagram-HODETNUW-DMus6jv_.js} +1 -1
- package/dist/assets/{gallery-page-C8KW0aF4.js → gallery-page-_mH_5ChL.js} +1 -1
- package/dist/assets/{glide-data-editor-C-EXft2Q.js → glide-data-editor-CFcsGnit.js} +4 -4
- package/dist/assets/{graphlib-rnp4xs81.js → graphlib-Sg3tw7ov.js} +1 -1
- package/dist/assets/has-DXmEv5FL.js +1 -0
- package/dist/assets/{home-page-CG6vbN4B.js → home-page-hKIN8akr.js} +2 -2
- package/dist/assets/index-B8OSEJVD.css +2 -0
- package/dist/assets/index-qY3lu9r4.js +62 -0
- package/dist/assets/{infoDiagram-27XIBGKW-C4VdBugr.js → infoDiagram-27XIBGKW-DYsYx_PP.js} +1 -1
- package/dist/assets/{ishikawaDiagram-5VMMS53U-BS-QMwse.js → ishikawaDiagram-5VMMS53U-D1Rpw_lT.js} +1 -1
- package/dist/assets/{kanban-definition-UXKFOSKX-HVONQMlk.js → kanban-definition-UXKFOSKX-CGxkIydR.js} +1 -1
- package/dist/assets/{logs-panel-BNeI_Aah.js → logs-panel-C4k8cMGx.js} +1 -1
- package/dist/assets/{mermaid-LovyOmSB.js → mermaid-BLxkTwc2.js} +4 -4
- package/dist/assets/{mindmap-definition-YA3MSWOX-DyBY39cg.js → mindmap-definition-YA3MSWOX-BiuGfVWP.js} +1 -1
- package/dist/assets/{number-overlay-editor-CwRpjoZm.js → number-overlay-editor-Ck2sCaO7.js} +1 -1
- package/dist/assets/{outline-panel-bDklL4uf.js → outline-panel-Z6FAHcVy.js} +1 -1
- package/dist/assets/packages-panel-CgL87fcO.js +1 -0
- package/dist/assets/panels-BSvT-tNo.js +33 -0
- package/dist/assets/{pegDiagram-XKGWAZYB-kQLDO-kO.js → pegDiagram-XKGWAZYB-Ci7mDvEz.js} +1 -1
- package/dist/assets/{pieDiagram-E7YTZNPT-DJsFtYCp.js → pieDiagram-E7YTZNPT-DZbp0Icu.js} +1 -1
- package/dist/assets/{process-output-CVZMao0e.js → process-output-COWXLjOr.js} +1 -1
- package/dist/assets/{railroadDiagram-O6MQD6OU-Bcf0CSqH.js → railroadDiagram-O6MQD6OU-CzXIAsPx.js} +1 -1
- package/dist/assets/{requirementDiagram-BXWQKSXE-AGcJcQbq.js → requirementDiagram-BXWQKSXE-DsIrXWi5.js} +1 -1
- package/dist/assets/{reveal-component-Dx_L-cIu.js → reveal-component-CnW6op6f.js} +3 -3
- package/dist/assets/run-page-B_JOrxWI.js +1 -0
- package/dist/assets/{scratchpad-panel-DfvfNJDq.js → scratchpad-panel-DktgeGw9.js} +1 -1
- package/dist/assets/{secrets-panel-CZbY8wgN.js → secrets-panel-Di8040gJ.js} +1 -1
- package/dist/assets/{sequenceDiagram-WJ2MYXX4-Cokf6L0R.js → sequenceDiagram-WJ2MYXX4-YEyNvwCq.js} +1 -1
- package/dist/assets/{session-panel-Cmsewdss.js → session-panel-FmN2Ix-x.js} +1 -1
- package/dist/assets/{snippets-panel-B_XreO-C.js → snippets-panel-BmXCx6yt.js} +1 -1
- package/dist/assets/{stateDiagram-D77RDMKH-CdkNCpaH.js → stateDiagram-D77RDMKH-B3Ja9Vk7.js} +1 -1
- package/dist/assets/{stateDiagram-v2-MP3YSRHH-CpudHHc4.js → stateDiagram-v2-MP3YSRHH-Bn7tPa_c.js} +1 -1
- package/dist/assets/{swimlanes-42K2YHIH-_8ELJqVB.js → swimlanes-42K2YHIH-DTgRQfmF.js} +1 -1
- package/dist/assets/{swimlanesDiagram-VR7AAH4N-qd1AHOJZ.js → swimlanesDiagram-VR7AAH4N-DKJ2qI6J.js} +1 -1
- package/dist/assets/{swiper-component-DY4M2PJo.js → swiper-component-CYrf6kVu.js} +1 -1
- package/dist/assets/{terminal-CTP4KOwN.js → terminal-CBss1CM1.js} +2 -2
- package/dist/assets/{timeline-definition-24CTP7MA-donBeXb2.js → timeline-definition-24CTP7MA-CKo5fHyB.js} +1 -1
- package/dist/assets/{tracing-DgHfkjPY.js → tracing-BBufraD6.js} +1 -1
- package/dist/assets/{tracing-panel-Qiu_S5kw.js → tracing-panel-lUoyxh7t.js} +2 -2
- package/dist/assets/{useCellActionButton-BCWgCI24.js → useCellActionButton-VJCYmjUy.js} +1 -1
- package/dist/assets/{vega-component-CC6LYLy6.js → vega-component-BUKpKmrE.js} +1 -1
- package/dist/assets/{vega-embed-container-CMoER1M0.js → vega-embed-container-BgSrb2ci.js} +1 -1
- package/dist/assets/{vennDiagram-4TSXK5OY-BgCWyFeF.js → vennDiagram-4TSXK5OY-CA_b78pk.js} +1 -1
- package/dist/assets/{wardleyDiagram-VM6X3IG4-rkxibRUc.js → wardleyDiagram-VM6X3IG4-CT2UYDxA.js} +1 -1
- package/dist/assets/{xychartDiagram-S5SC5T6Z-Bfs-6MBb.js → xychartDiagram-S5SC5T6Z-BOvLySSM.js} +1 -1
- package/dist/index.html +5 -5
- package/package.json +1 -1
- package/src/__mocks__/requests.ts +7 -0
- package/src/components/editor/alerts/connecting-alert.tsx +1 -66
- package/src/components/editor/alerts/connection-notice.tsx +140 -0
- package/src/components/editor/app-container.tsx +9 -3
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +114 -7
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +165 -0
- package/src/components/editor/chrome/panels/packages-panel.tsx +93 -166
- package/src/components/editor/chrome/panels/sandbox-controller.tsx +106 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +173 -0
- package/src/components/editor/chrome/wrapper/footer-items/backend-status.tsx +3 -1
- package/src/components/editor/header/__tests__/status.test.tsx +8 -20
- package/src/components/editor/header/status.tsx +5 -3
- package/src/components/editor/renderers/cell-array.tsx +1 -5
- package/src/core/MarimoApp.tsx +0 -2
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/state.ts +1 -2
- package/src/core/islands/bootstrap.ts +1 -0
- package/src/core/islands/bridge.ts +3 -0
- package/src/core/network/connection-notice.ts +84 -0
- package/src/core/network/requests-lazy.ts +3 -0
- package/src/core/network/requests-network.ts +12 -0
- package/src/core/network/requests-static.ts +3 -0
- package/src/core/network/requests-toasting.tsx +3 -0
- package/src/core/network/resolve.ts +2 -1
- package/src/core/network/types.ts +7 -0
- package/src/core/packages/__tests__/usePackageAction.test.tsx +129 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +165 -0
- package/src/core/packages/package-data.ts +35 -0
- package/src/core/packages/sandbox-state.ts +15 -0
- package/src/core/packages/toast-components.tsx +11 -0
- package/src/core/packages/useInstallPackage.ts +1 -1
- package/src/core/packages/usePackageAction.ts +51 -0
- package/src/core/packages/usePackageDependencies.ts +22 -0
- package/src/core/packages/useSandboxController.ts +188 -0
- package/src/core/run-app.tsx +2 -0
- package/src/core/wasm/bridge.ts +11 -0
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +212 -2
- package/src/core/websocket/types.ts +11 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +43 -8
- package/src/stories/sandbox.stories.tsx +308 -0
- package/dist/assets/app-config-button-BgRYYpoH.js +0 -1
- package/dist/assets/cache-panel-DP6AuApU.js +0 -1
- package/dist/assets/config-D4PQ0ExB.js +0 -1
- package/dist/assets/edit-page-B5T44oKE.js +0 -184
- package/dist/assets/has-BPYf0PV2.js +0 -1
- package/dist/assets/index-Bit3pmfj.css +0 -2
- package/dist/assets/index-D3ydSabW.js +0 -62
- package/dist/assets/packages-panel-BvnEJCqo.js +0 -1
- package/dist/assets/panels-DvYa8VJO.js +0 -33
- package/dist/assets/run-page-BIX2SnOg.js +0 -1
|
@@ -1,76 +1,11 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { useAtomValue } from "jotai";
|
|
4
|
-
import { LoadingEllipsis } from "@/components/icons/loading-ellipsis";
|
|
5
|
-
import { Spinner } from "@/components/icons/spinner";
|
|
6
4
|
import { Button } from "@/components/ui/button";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
isClosedAtom,
|
|
10
|
-
isConnectingAtom,
|
|
11
|
-
isNotStartedAtom,
|
|
12
|
-
} from "@/core/network/connection";
|
|
5
|
+
import { isNotStartedAtom } from "@/core/network/connection";
|
|
13
6
|
import { useConnectToRuntime } from "@/core/runtime/config";
|
|
14
|
-
import { Banner } from "@/plugins/impl/common/error-banner";
|
|
15
|
-
import { Tooltip } from "../../ui/tooltip";
|
|
16
7
|
import { FloatingAlert } from "./floating-alert";
|
|
17
8
|
|
|
18
|
-
const SHORT_DELAY_MS = 1000; // 1 second
|
|
19
|
-
const LONG_DELAY_MS = 5000; // 5 seconds
|
|
20
|
-
|
|
21
|
-
export const ConnectingAlert: React.FC = () => {
|
|
22
|
-
const isConnecting = useAtomValue(isConnectingAtom);
|
|
23
|
-
const isClosed = useAtomValue(isClosedAtom);
|
|
24
|
-
|
|
25
|
-
if (isConnecting) {
|
|
26
|
-
const subtleNotification = (
|
|
27
|
-
<Tooltip content="Connecting to a marimo runtime">
|
|
28
|
-
<div className="flex items-center">
|
|
29
|
-
<LoadingEllipsis size={5} className="text-yellow-500" />
|
|
30
|
-
</div>
|
|
31
|
-
</Tooltip>
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const longNotification = (
|
|
35
|
-
<Banner
|
|
36
|
-
kind="info"
|
|
37
|
-
className="flex flex-col rounded py-2 px-4 animate-in slide-in-from-top w-fit"
|
|
38
|
-
>
|
|
39
|
-
<div className="flex flex-col gap-4 justify-between items-start text-muted-foreground text-base">
|
|
40
|
-
<div className="flex items-center gap-2">
|
|
41
|
-
<Spinner className="h-4 w-4" />
|
|
42
|
-
<p>Connecting to a marimo runtime ...</p>
|
|
43
|
-
</div>
|
|
44
|
-
</div>
|
|
45
|
-
</Banner>
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
// This waits for 1 second to show the subtle notification, then shows the long notification after 5 seconds.
|
|
49
|
-
return (
|
|
50
|
-
<DelayMount milliseconds={SHORT_DELAY_MS}>
|
|
51
|
-
<div className="m-0 flex items-center min-h-[28px] fixed top-5 left-1/2 transform -translate-x-1/2 z-200">
|
|
52
|
-
<DelayMount
|
|
53
|
-
milliseconds={LONG_DELAY_MS}
|
|
54
|
-
fallback={subtleNotification}
|
|
55
|
-
>
|
|
56
|
-
{longNotification}
|
|
57
|
-
</DelayMount>
|
|
58
|
-
</div>
|
|
59
|
-
</DelayMount>
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (isClosed) {
|
|
64
|
-
return (
|
|
65
|
-
<FloatingAlert show={isClosed} kind="danger">
|
|
66
|
-
<div className="flex items-center gap-2">
|
|
67
|
-
<p>Failed to connect.</p>
|
|
68
|
-
</div>
|
|
69
|
-
</FloatingAlert>
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
};
|
|
73
|
-
|
|
74
9
|
export const NotStartedConnectionAlert: React.FC = () => {
|
|
75
10
|
const isNotStarted = useAtomValue(isNotStartedAtom);
|
|
76
11
|
const connectToRuntime = useConnectToRuntime();
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { useAtomValue } from "jotai";
|
|
3
|
+
import { AlertCircleIcon, ArrowRightIcon } from "lucide-react";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
6
|
+
import { Button } from "@/components/ui/button";
|
|
7
|
+
import { hasCellsAtom } from "@/core/cells/cells";
|
|
8
|
+
import type { AppConfig } from "@/core/config/config-schema";
|
|
9
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
10
|
+
import { connectionNoticeAtom } from "@/core/network/connection-notice";
|
|
11
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
12
|
+
import { useDelayElapsed } from "@/hooks/useDelayElapsed";
|
|
13
|
+
import { useInterval } from "@/hooks/useInterval";
|
|
14
|
+
import { cn } from "@/utils/cn";
|
|
15
|
+
import { SandboxErrorOutput } from "../chrome/panels/sandbox-panel";
|
|
16
|
+
import { chromeAtom, useChromeActions } from "../chrome/state";
|
|
17
|
+
import { VerticalLayoutWrapper } from "../renderers/vertical-layout/vertical-layout-wrapper";
|
|
18
|
+
|
|
19
|
+
export const ConnectionNotice = ({
|
|
20
|
+
appConfig,
|
|
21
|
+
onRetry,
|
|
22
|
+
}: {
|
|
23
|
+
appConfig: AppConfig;
|
|
24
|
+
onRetry: () => void;
|
|
25
|
+
}) => {
|
|
26
|
+
const notice = useAtomValue(connectionNoticeAtom);
|
|
27
|
+
const connection = useAtomValue(connectionAtom);
|
|
28
|
+
const hasCells = useAtomValue(hasCellsAtom);
|
|
29
|
+
const chrome = useAtomValue(chromeAtom);
|
|
30
|
+
const packagesOpen =
|
|
31
|
+
(chrome.isSidebarOpen && chrome.selectedPanel === "packages") ||
|
|
32
|
+
(chrome.isDeveloperPanelOpen &&
|
|
33
|
+
chrome.selectedDeveloperPanelTab === "packages");
|
|
34
|
+
const elapsed = useDelayElapsed(notice?.pending ? 500 : 0);
|
|
35
|
+
const { openApplication } = useChromeActions();
|
|
36
|
+
|
|
37
|
+
// Reconnection uses the footer indicator while existing cells stay visible.
|
|
38
|
+
if (
|
|
39
|
+
!notice ||
|
|
40
|
+
!elapsed ||
|
|
41
|
+
(hasCells &&
|
|
42
|
+
connection.state === WebSocketState.CONNECTING &&
|
|
43
|
+
connection.phase === "reconnecting")
|
|
44
|
+
) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<VerticalLayoutWrapper
|
|
50
|
+
appConfig={appConfig}
|
|
51
|
+
className="shrink-0 pb-0 sm:pb-0 print:hidden"
|
|
52
|
+
innerClassName="pb-0 sm:pb-0 pr-4"
|
|
53
|
+
>
|
|
54
|
+
<section
|
|
55
|
+
aria-label="Notebook startup"
|
|
56
|
+
className={cn(
|
|
57
|
+
"text-muted-foreground",
|
|
58
|
+
hasCells
|
|
59
|
+
? "mt-4 mb-8 border rounded-lg px-5 py-5"
|
|
60
|
+
: "max-w-sm mx-auto mt-16 sm:mt-24 px-4",
|
|
61
|
+
hasCells && !notice.pending && "border-(--red-7)",
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
<div role="status" className="flex items-center gap-2.5">
|
|
65
|
+
{notice.pending ? (
|
|
66
|
+
<Spinner className="size-4 shrink-0" aria-hidden={true} />
|
|
67
|
+
) : (
|
|
68
|
+
<AlertCircleIcon
|
|
69
|
+
className="size-4 shrink-0 text-(--red-11)"
|
|
70
|
+
aria-hidden={true}
|
|
71
|
+
/>
|
|
72
|
+
)}
|
|
73
|
+
<h2
|
|
74
|
+
className={cn(
|
|
75
|
+
"text-base",
|
|
76
|
+
notice.pending
|
|
77
|
+
? "font-normal text-foreground"
|
|
78
|
+
: "font-medium text-(--red-11)",
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
{notice.title}
|
|
82
|
+
</h2>
|
|
83
|
+
</div>
|
|
84
|
+
<div className="ml-6.5">
|
|
85
|
+
<p
|
|
86
|
+
className={cn(
|
|
87
|
+
"mt-3 text-sm leading-7",
|
|
88
|
+
!notice.pending && "text-foreground",
|
|
89
|
+
)}
|
|
90
|
+
>
|
|
91
|
+
{notice.description}
|
|
92
|
+
</p>
|
|
93
|
+
{!notice.sandbox && notice.error && (
|
|
94
|
+
<SandboxErrorOutput error={notice.error} />
|
|
95
|
+
)}
|
|
96
|
+
<div className="mt-5 flex flex-wrap items-center gap-x-6 gap-y-3">
|
|
97
|
+
{!packagesOpen && (notice.sandbox || notice.pending) && (
|
|
98
|
+
<Button
|
|
99
|
+
variant="text"
|
|
100
|
+
size="xs"
|
|
101
|
+
className="h-auto p-0 text-xs gap-1.5"
|
|
102
|
+
onClick={() => openApplication("packages")}
|
|
103
|
+
>
|
|
104
|
+
{notice.pending ? "View setup details" : "Open Packages"}
|
|
105
|
+
<ArrowRightIcon className="size-3" aria-hidden={true} />
|
|
106
|
+
</Button>
|
|
107
|
+
)}
|
|
108
|
+
{!notice.pending && !notice.sandbox && (
|
|
109
|
+
<Button variant="outline" size="xs" onClick={onRetry}>
|
|
110
|
+
Try again
|
|
111
|
+
</Button>
|
|
112
|
+
)}
|
|
113
|
+
{notice.pending && <StartupElapsedTime />}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
</section>
|
|
117
|
+
</VerticalLayoutWrapper>
|
|
118
|
+
);
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
function StartupElapsedTime() {
|
|
122
|
+
const [startedAt] = useState(Date.now);
|
|
123
|
+
const [seconds, setSeconds] = useState(0);
|
|
124
|
+
useInterval(() => setSeconds(Math.floor((Date.now() - startedAt) / 1000)), {
|
|
125
|
+
delayMs: 1000,
|
|
126
|
+
whenVisible: true,
|
|
127
|
+
});
|
|
128
|
+
const duration =
|
|
129
|
+
seconds < 60
|
|
130
|
+
? `${seconds}s`
|
|
131
|
+
: `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
132
|
+
return (
|
|
133
|
+
<span
|
|
134
|
+
className="text-xs text-muted-foreground/70 tabular-nums"
|
|
135
|
+
aria-live="off"
|
|
136
|
+
>
|
|
137
|
+
Elapsed {duration}
|
|
138
|
+
</span>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
@@ -4,8 +4,11 @@ import type React from "react";
|
|
|
4
4
|
import type { PropsWithChildren } from "react";
|
|
5
5
|
import type { AppConfig } from "@/core/config/config-schema";
|
|
6
6
|
import { PyodideLoader } from "@/core/wasm/PyodideLoader";
|
|
7
|
-
import {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
type ConnectionStatus,
|
|
9
|
+
WebSocketClosedReason,
|
|
10
|
+
WebSocketState,
|
|
11
|
+
} from "@/core/websocket/types";
|
|
9
12
|
import { cn } from "@/utils/cn";
|
|
10
13
|
import { DynamicFavicon } from "./dynamic-favicon";
|
|
11
14
|
import { StatusOverlay } from "./header/status";
|
|
@@ -44,7 +47,10 @@ export const AppContainer: React.FC<PropsWithChildren<Props>> = ({
|
|
|
44
47
|
data-connection-state={connectionState}
|
|
45
48
|
className={cn(
|
|
46
49
|
"mathjax_ignore",
|
|
47
|
-
|
|
50
|
+
connection.state === WebSocketState.CLOSED &&
|
|
51
|
+
connection.code !==
|
|
52
|
+
WebSocketClosedReason.KERNEL_STARTUP_ERROR &&
|
|
53
|
+
"disconnected",
|
|
48
54
|
"bg-background w-full h-full text-textColor",
|
|
49
55
|
"flex flex-col overflow-y-auto",
|
|
50
56
|
width === "full" && "config-width-full",
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
within,
|
|
10
|
+
} from "@testing-library/react";
|
|
4
11
|
import { Provider } from "jotai";
|
|
5
12
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
13
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
@@ -10,6 +17,9 @@ import type {
|
|
|
10
17
|
DependencyTreeNode,
|
|
11
18
|
DependencyTreeResponse,
|
|
12
19
|
} from "@/core/network/types";
|
|
20
|
+
import { withPackageInvalidation } from "@/core/packages/package-data";
|
|
21
|
+
import { useInstallPackages } from "@/core/packages/useInstallPackage";
|
|
22
|
+
import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
|
|
13
23
|
import { store } from "@/core/state/jotai";
|
|
14
24
|
import PackagesPanel from "../packages-panel";
|
|
15
25
|
|
|
@@ -51,16 +61,22 @@ function renderPanel(
|
|
|
51
61
|
context: DependencyTreeResponse["context"],
|
|
52
62
|
tree: DependencyTreeNode = emptyTree,
|
|
53
63
|
) {
|
|
54
|
-
const getPackageList = vi.fn().mockResolvedValue({ packages: [] });
|
|
55
64
|
store.set(
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}),
|
|
65
|
+
sandboxAtom,
|
|
66
|
+
context.kind === "sandbox"
|
|
67
|
+
? { backend: context.backend, manifest: "", filename: "notebook.py" }
|
|
68
|
+
: null,
|
|
61
69
|
);
|
|
70
|
+
store.set(sandboxSyncAtom, { pending: false, error: null });
|
|
71
|
+
const getPackageList = vi.fn().mockResolvedValue({ packages: [] });
|
|
72
|
+
const client = MockRequestClient.create({
|
|
73
|
+
getPackageList,
|
|
74
|
+
getDependencyTree: vi.fn().mockResolvedValue({ tree, context }),
|
|
75
|
+
});
|
|
76
|
+
store.set(requestClientAtom, client);
|
|
62
77
|
|
|
63
78
|
return {
|
|
79
|
+
client,
|
|
64
80
|
getPackageList,
|
|
65
81
|
...render(
|
|
66
82
|
<Provider store={store}>
|
|
@@ -141,3 +157,94 @@ describe("PackagesPanel", () => {
|
|
|
141
157
|
expect(getPackageList).toHaveBeenCalledOnce();
|
|
142
158
|
});
|
|
143
159
|
});
|
|
160
|
+
|
|
161
|
+
it("refreshes an open panel when a package is installed elsewhere, after installation succeeds", async () => {
|
|
162
|
+
const { promise, resolve } = Promise.withResolvers<{ success: boolean }>();
|
|
163
|
+
const client = MockRequestClient.create({
|
|
164
|
+
addPackage: vi.fn(() => promise),
|
|
165
|
+
getDependencyTree: vi
|
|
166
|
+
.fn()
|
|
167
|
+
.mockResolvedValueOnce({
|
|
168
|
+
tree: emptyTree,
|
|
169
|
+
context: { kind: "sandbox", backend: "uv" },
|
|
170
|
+
})
|
|
171
|
+
.mockResolvedValue({
|
|
172
|
+
tree: populatedTree,
|
|
173
|
+
context: { kind: "sandbox", backend: "uv" },
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
store.set(sandboxAtom, {
|
|
177
|
+
backend: "uv",
|
|
178
|
+
manifest: "",
|
|
179
|
+
filename: "notebook.py",
|
|
180
|
+
});
|
|
181
|
+
store.set(sandboxSyncAtom, { pending: false, error: null });
|
|
182
|
+
store.set(requestClientAtom, withPackageInvalidation(client));
|
|
183
|
+
function InstallElsewhere() {
|
|
184
|
+
const { handleInstallPackages } = useInstallPackages();
|
|
185
|
+
return (
|
|
186
|
+
<button onClick={() => handleInstallPackages(["polars"])}>
|
|
187
|
+
Install from another panel
|
|
188
|
+
</button>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
render(
|
|
192
|
+
<Provider store={store}>
|
|
193
|
+
<TooltipProvider>
|
|
194
|
+
<PackagesPanel />
|
|
195
|
+
<InstallElsewhere />
|
|
196
|
+
</TooltipProvider>
|
|
197
|
+
</Provider>,
|
|
198
|
+
);
|
|
199
|
+
await screen.findByPlaceholderText("Add packages to uv sandbox...");
|
|
200
|
+
fireEvent.click(
|
|
201
|
+
screen.getByRole("button", { name: "Install from another panel" }),
|
|
202
|
+
);
|
|
203
|
+
expect(client.getDependencyTree).toHaveBeenCalledTimes(1);
|
|
204
|
+
await act(async () => resolve({ success: true }));
|
|
205
|
+
await waitFor(() =>
|
|
206
|
+
expect(client.getDependencyTree).toHaveBeenCalledTimes(2),
|
|
207
|
+
);
|
|
208
|
+
expect(await screen.findByText("polars")).toBeInTheDocument();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it.each(["uv", "pixi", "list"] as const)(
|
|
212
|
+
"allows upgrading marimo but not removing it in the %s view",
|
|
213
|
+
async (view) => {
|
|
214
|
+
const tree = {
|
|
215
|
+
...populatedTree,
|
|
216
|
+
dependencies: [
|
|
217
|
+
...populatedTree.dependencies,
|
|
218
|
+
{ name: "marimo", version: "0.24.0", tags: [], dependencies: [] },
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
const { client, getPackageList } = renderPanel(
|
|
222
|
+
view === "list"
|
|
223
|
+
? { kind: "package-manager", name: "pip" }
|
|
224
|
+
: { kind: "sandbox", backend: view },
|
|
225
|
+
tree,
|
|
226
|
+
);
|
|
227
|
+
getPackageList.mockResolvedValue({ packages: tree.dependencies });
|
|
228
|
+
vi.mocked(client.addPackage).mockResolvedValue({ success: true });
|
|
229
|
+
if (view === "list") {
|
|
230
|
+
fireEvent.click(await screen.findByRole("button", { name: "List" }));
|
|
231
|
+
}
|
|
232
|
+
const role = view === "list" ? "row" : "treeitem";
|
|
233
|
+
const marimo = within(await screen.findByRole(role, { name: /marimo/ }));
|
|
234
|
+
expect(marimo.getByText(/0\.24\.0/)).toBeInTheDocument();
|
|
235
|
+
expect(
|
|
236
|
+
marimo.queryByRole("button", { name: "Remove" }),
|
|
237
|
+
).not.toBeInTheDocument();
|
|
238
|
+
const polars = within(screen.getByRole(role, { name: /polars/ }));
|
|
239
|
+
expect(polars.getByRole("button", { name: "Remove" })).toBeInTheDocument();
|
|
240
|
+
fireEvent.click(marimo.getByRole("button", { name: "Upgrade" }));
|
|
241
|
+
await waitFor(() =>
|
|
242
|
+
expect(client.addPackage).toHaveBeenCalledWith({
|
|
243
|
+
package: "marimo",
|
|
244
|
+
upgrade: true,
|
|
245
|
+
group: undefined,
|
|
246
|
+
}),
|
|
247
|
+
);
|
|
248
|
+
expect(client.removePackage).not.toHaveBeenCalled();
|
|
249
|
+
},
|
|
250
|
+
);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
// @vitest-environment jsdom
|
|
3
|
+
import {
|
|
4
|
+
act,
|
|
5
|
+
fireEvent,
|
|
6
|
+
render,
|
|
7
|
+
screen,
|
|
8
|
+
waitFor,
|
|
9
|
+
within,
|
|
10
|
+
} from "@testing-library/react";
|
|
11
|
+
import { Provider } from "jotai";
|
|
12
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
13
|
+
import { MockRequestClient } from "@/__mocks__/requests";
|
|
14
|
+
import { AppConfigSchema } from "@/core/config/config-schema";
|
|
15
|
+
import { ConnectionNotice } from "@/components/editor/alerts/connection-notice";
|
|
16
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
17
|
+
import { kernelStartupErrorAtom } from "@/core/errors/state";
|
|
18
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
19
|
+
import { requestClientAtom } from "@/core/network/requests";
|
|
20
|
+
import {
|
|
21
|
+
sandboxActionsAtom,
|
|
22
|
+
sandboxAtom,
|
|
23
|
+
sandboxSyncAtom,
|
|
24
|
+
} from "@/core/packages/sandbox-state";
|
|
25
|
+
import { filenameAtom } from "@/core/saving/file-state";
|
|
26
|
+
import { store } from "@/core/state/jotai";
|
|
27
|
+
import { WebSocketClosedReason, WebSocketState } from "@/core/websocket/types";
|
|
28
|
+
import { chromeAtom } from "../../state";
|
|
29
|
+
import PackagesPanel from "../packages-panel";
|
|
30
|
+
import { SandboxController } from "../sandbox-controller";
|
|
31
|
+
|
|
32
|
+
vi.mock("@/plugins/impl/code/LazyAnyLanguageCodeMirror", () => ({
|
|
33
|
+
LazyAnyLanguageCodeMirror: ({
|
|
34
|
+
value,
|
|
35
|
+
onChange,
|
|
36
|
+
editable,
|
|
37
|
+
}: {
|
|
38
|
+
value: string;
|
|
39
|
+
onChange: (value: string) => void;
|
|
40
|
+
editable: boolean;
|
|
41
|
+
}) => (
|
|
42
|
+
<textarea
|
|
43
|
+
aria-label="Notebook manifest"
|
|
44
|
+
value={value}
|
|
45
|
+
readOnly={!editable}
|
|
46
|
+
onChange={(event) => onChange(event.target.value)}
|
|
47
|
+
/>
|
|
48
|
+
),
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const manifest =
|
|
52
|
+
'dependencies = ["numpy==0.0.0"]\n\n[tool.custom]\nlabel = "experiment"\n';
|
|
53
|
+
const failed = {
|
|
54
|
+
state: WebSocketState.CLOSED,
|
|
55
|
+
code: WebSocketClosedReason.KERNEL_STARTUP_ERROR,
|
|
56
|
+
phase: "preparing-environment",
|
|
57
|
+
reason: "Kernel startup failed",
|
|
58
|
+
} as const;
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
store.set(sandboxAtom, { backend: "uv", manifest, filename: "notebook.py" });
|
|
61
|
+
store.set(sandboxSyncAtom, { pending: false, error: null });
|
|
62
|
+
store.set(sandboxActionsAtom, null);
|
|
63
|
+
store.set(connectionAtom, failed);
|
|
64
|
+
store.set(kernelStartupErrorAtom, "resolver output\n no solution");
|
|
65
|
+
store.set(filenameAtom, "notebook.py");
|
|
66
|
+
store.set(chromeAtom, {
|
|
67
|
+
selectedPanel: "packages",
|
|
68
|
+
isSidebarOpen: false,
|
|
69
|
+
isDeveloperPanelOpen: false,
|
|
70
|
+
selectedDeveloperPanelTab: "errors",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
function mount(
|
|
74
|
+
client: ReturnType<typeof MockRequestClient.create>,
|
|
75
|
+
reconnect = vi.fn(async () => {}),
|
|
76
|
+
) {
|
|
77
|
+
store.set(requestClientAtom, client);
|
|
78
|
+
return render(
|
|
79
|
+
<Provider store={store}>
|
|
80
|
+
<TooltipProvider>
|
|
81
|
+
<SandboxController onReconnect={reconnect} />
|
|
82
|
+
<ConnectionNotice
|
|
83
|
+
appConfig={AppConfigSchema.parse({})}
|
|
84
|
+
onRetry={reconnect}
|
|
85
|
+
/>
|
|
86
|
+
<PackagesPanel />
|
|
87
|
+
</TooltipProvider>
|
|
88
|
+
</Provider>,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
function requests() {
|
|
92
|
+
return MockRequestClient.create({
|
|
93
|
+
getSandbox: vi.fn(async () => ({
|
|
94
|
+
backend: "uv",
|
|
95
|
+
manifest,
|
|
96
|
+
filename: "notebook.py",
|
|
97
|
+
})),
|
|
98
|
+
updateManifest: vi.fn(async ({ contents }) => ({
|
|
99
|
+
backend: "uv",
|
|
100
|
+
manifest: contents,
|
|
101
|
+
filename: "notebook.py",
|
|
102
|
+
})),
|
|
103
|
+
syncSandbox: vi.fn(async () => ({ success: true, reconnect: true })),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
it("keeps a whole-manifest repair open through failure and closes only after sync succeeds", async () => {
|
|
108
|
+
const client = requests();
|
|
109
|
+
const { promise: saved, resolve: finishSave } = Promise.withResolvers<void>();
|
|
110
|
+
vi.mocked(client.updateManifest).mockImplementation(async ({ contents }) => {
|
|
111
|
+
await saved;
|
|
112
|
+
return { backend: "uv", manifest: contents, filename: "notebook.py" };
|
|
113
|
+
});
|
|
114
|
+
const reconnect = vi.fn(async () => {
|
|
115
|
+
store.set(kernelStartupErrorAtom, null);
|
|
116
|
+
store.set(connectionAtom, {
|
|
117
|
+
state: WebSocketState.CONNECTING,
|
|
118
|
+
phase: "preparing-environment",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
mount(client, reconnect);
|
|
122
|
+
fireEvent.click(await screen.findByRole("button", { name: "Open Packages" }));
|
|
123
|
+
expect(store.get(chromeAtom).isSidebarOpen).toBe(true);
|
|
124
|
+
expect(screen.getByLabelText("Error details")).toHaveTextContent(
|
|
125
|
+
"resolver output",
|
|
126
|
+
);
|
|
127
|
+
fireEvent.click(screen.getByRole("button", { name: "Edit manifest…" }));
|
|
128
|
+
const editor = await screen.findByRole("textbox", {
|
|
129
|
+
name: "Notebook manifest",
|
|
130
|
+
});
|
|
131
|
+
const replacement =
|
|
132
|
+
'# A completely different manifest\n[tool.custom]\nlabel = "updated"\n';
|
|
133
|
+
fireEvent.change(editor, { target: { value: replacement } });
|
|
134
|
+
fireEvent.click(screen.getByRole("button", { name: "Save & sync" }));
|
|
135
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
136
|
+
expect(reconnect).not.toHaveBeenCalled();
|
|
137
|
+
await act(async () => finishSave());
|
|
138
|
+
await waitFor(() => expect(reconnect).toHaveBeenCalledTimes(1));
|
|
139
|
+
expect(client.updateManifest).toHaveBeenCalledWith({
|
|
140
|
+
fileKey: "notebook.py",
|
|
141
|
+
previous: manifest,
|
|
142
|
+
contents: replacement,
|
|
143
|
+
});
|
|
144
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
145
|
+
act(() => {
|
|
146
|
+
store.set(connectionAtom, failed);
|
|
147
|
+
store.set(kernelStartupErrorAtom, "another resolver failure");
|
|
148
|
+
});
|
|
149
|
+
const dialog = within(screen.getByRole("dialog"));
|
|
150
|
+
expect(await dialog.findByLabelText("Error details")).toHaveTextContent(
|
|
151
|
+
"another resolver failure",
|
|
152
|
+
);
|
|
153
|
+
expect(
|
|
154
|
+
dialog.getByRole("textbox", { name: "Notebook manifest" }),
|
|
155
|
+
).toHaveValue(replacement);
|
|
156
|
+
await waitFor(() =>
|
|
157
|
+
expect(dialog.getByRole("button", { name: "Sync" })).toBeEnabled(),
|
|
158
|
+
);
|
|
159
|
+
fireEvent.click(dialog.getByRole("button", { name: "Sync" }));
|
|
160
|
+
await waitFor(() => expect(reconnect).toHaveBeenCalledTimes(2));
|
|
161
|
+
act(() => store.set(connectionAtom, { state: WebSocketState.OPEN }));
|
|
162
|
+
await waitFor(() =>
|
|
163
|
+
expect(screen.queryByRole("dialog")).not.toBeInTheDocument(),
|
|
164
|
+
);
|
|
165
|
+
});
|