@marimo-team/islands 0.24.3-dev81 → 0.24.3-dev83
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/{ErrorBoundary-CCi-VkF7.js → ErrorBoundary-Uf_LvjvY.js} +1 -1
- package/dist/{cells-BIfOnGBK.js → cells-DYIkSxs6.js} +4 -0
- package/dist/{chat-ui-LWkjhObe.js → chat-ui-BXj3uYjy.js} +4 -4
- package/dist/{code-visibility-Bsfqbe2z.js → code-visibility-DxgQWu1j.js} +2 -2
- package/dist/{common-BnKC1tWB.js → common-ByAtncLr.js} +3 -3
- package/dist/{data-grid-overlay-editor-CJnlWykO.js → data-grid-overlay-editor-DL_blFyL.js} +1 -1
- package/dist/{error-entries-CR0oClSL.js → error-entries-C0NsUqXl.js} +1 -1
- package/dist/{feedback-button-MWISfIIf.js → feedback-button-AZcBqfoV.js} +4 -4
- package/dist/{glide-data-editor-dcmP8mKS.js → glide-data-editor-BJdZncJk.js} +3 -3
- package/dist/{globals-fCMBgbAD.js → globals-yWarpyN4.js} +1 -1
- package/dist/{html-to-image-_i03yR4q.js → html-to-image-CeYYvQNb.js} +1 -1
- package/dist/main.js +12 -11
- package/dist/{number-overlay-editor-DhNE6pBe.js → number-overlay-editor-BatBVYxr.js} +1 -1
- package/dist/{process-output-D3zQyTAS.js → process-output-BCTjlyuH.js} +2 -2
- package/dist/{reveal-component-Cdcgngkc.js → reveal-component-D02jcpiJ.js} +3 -3
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/src/components/editor/alerts/__tests__/connection-notice.test.tsx +186 -0
- package/src/components/editor/alerts/__tests__/startup-progress.test.tsx +185 -0
- package/src/components/editor/alerts/connection-notice.tsx +100 -54
- package/src/components/editor/alerts/startup-output.tsx +173 -0
- package/src/components/editor/alerts/startup-progress.tsx +204 -0
- package/src/components/editor/chrome/panels/__tests__/packages-panel.test.tsx +53 -4
- package/src/components/editor/chrome/panels/__tests__/sandbox-details.test.tsx +358 -0
- package/src/components/editor/chrome/panels/__tests__/sandbox-recovery.test.tsx +5 -2
- package/src/components/editor/chrome/panels/packages-panel.tsx +32 -21
- package/src/components/editor/chrome/panels/sandbox-details-state.ts +38 -0
- package/src/components/editor/chrome/panels/sandbox-panel.tsx +188 -131
- package/src/components/editor/chrome/panels/sandbox-toggle.tsx +52 -0
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +7 -0
- package/src/components/editor/package-alert.tsx +112 -66
- package/src/core/alerts/__tests__/environment.test.ts +183 -0
- package/src/core/alerts/__tests__/state.test.tsx +240 -0
- package/src/core/alerts/environment.ts +57 -0
- package/src/core/alerts/state.ts +155 -99
- package/src/core/islands/bootstrap.ts +2 -1
- package/src/core/kernel/messages.ts +2 -2
- package/src/core/network/connection-notice.ts +45 -12
- package/src/core/network/connection.ts +19 -0
- package/src/core/network/useConnectionNotice.ts +53 -0
- package/src/core/packages/__tests__/useSandboxController.test.tsx +86 -5
- package/src/core/packages/sandbox-state.ts +56 -4
- package/src/core/packages/useSandboxController.ts +27 -7
- package/src/core/websocket/__tests__/useMarimoKernelConnection.hook.test.tsx +199 -8
- package/src/core/websocket/types.ts +1 -1
- package/src/core/websocket/useMarimoKernelConnection.tsx +31 -19
- package/src/stories/sandbox.stories.tsx +230 -50
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { ArrowDownIcon, ChevronUpIcon } from "lucide-react";
|
|
3
|
+
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { CopyClipboardIcon } from "@/components/icons/copy-icon";
|
|
5
|
+
import { useEventListener } from "@/hooks/useEventListener";
|
|
6
|
+
import { cn } from "@/utils/cn";
|
|
7
|
+
|
|
8
|
+
function hasSelection(element: HTMLElement) {
|
|
9
|
+
const selection = element.ownerDocument.getSelection();
|
|
10
|
+
return (
|
|
11
|
+
selection && !selection.isCollapsed && selection.containsNode(element, true)
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type StartupOutputState = "running" | "succeeded" | "failed";
|
|
16
|
+
|
|
17
|
+
export function StartupOutput({
|
|
18
|
+
logs,
|
|
19
|
+
label,
|
|
20
|
+
state = "running",
|
|
21
|
+
}: {
|
|
22
|
+
logs: string;
|
|
23
|
+
label: string;
|
|
24
|
+
/** Running keeps the newest line bright; failed keeps all of it bright. */
|
|
25
|
+
state?: StartupOutputState;
|
|
26
|
+
}) {
|
|
27
|
+
const [expanded, setExpanded] = useState(false);
|
|
28
|
+
const [following, setFollowing] = useState(true);
|
|
29
|
+
const outputRef = useRef<HTMLPreElement>(null);
|
|
30
|
+
const toggleRef = useRef<HTMLButtonElement>(null);
|
|
31
|
+
const outputId = useId();
|
|
32
|
+
const lines = logs.trimEnd().split(/\r?\n/);
|
|
33
|
+
const emphasis =
|
|
34
|
+
state === "failed"
|
|
35
|
+
? "text-foreground"
|
|
36
|
+
: state === "running"
|
|
37
|
+
? "last:text-foreground"
|
|
38
|
+
: undefined;
|
|
39
|
+
|
|
40
|
+
useEventListener(outputRef, "keydown", (event) => {
|
|
41
|
+
if (event.key === "Escape") {
|
|
42
|
+
event.preventDefault();
|
|
43
|
+
setExpanded(false);
|
|
44
|
+
toggleRef.current?.focus();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
useEventListener(document, "selectionchange", () => {
|
|
49
|
+
if (outputRef.current && hasSelection(outputRef.current)) {
|
|
50
|
+
setFollowing(false);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
useLayoutEffect(() => {
|
|
55
|
+
const output = outputRef.current;
|
|
56
|
+
if (!output) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const previous = output.textContent ?? "";
|
|
60
|
+
// Append without replacing selected text nodes as more output arrives.
|
|
61
|
+
if (logs.startsWith(previous)) {
|
|
62
|
+
if (logs.length > previous.length) {
|
|
63
|
+
output.append(document.createTextNode(logs.slice(previous.length)));
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
output.textContent = logs;
|
|
67
|
+
}
|
|
68
|
+
if (expanded && following && !hasSelection(output)) {
|
|
69
|
+
output.scrollTop = output.scrollHeight;
|
|
70
|
+
}
|
|
71
|
+
}, [logs, expanded, following]);
|
|
72
|
+
|
|
73
|
+
if (!logs.trim()) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"relative mt-2.5 min-w-0 contain-inline-size rounded text-muted-foreground",
|
|
81
|
+
// One surface: it washes on hover and the wash stays while open.
|
|
82
|
+
expanded ? "bg-muted/40" : "hover:bg-muted/40",
|
|
83
|
+
)}
|
|
84
|
+
>
|
|
85
|
+
<div className={cn(expanded && "flex items-center pr-1")}>
|
|
86
|
+
<button
|
|
87
|
+
ref={toggleRef}
|
|
88
|
+
type="button"
|
|
89
|
+
aria-label={`${expanded ? "Collapse" : "Expand"} ${label}`}
|
|
90
|
+
aria-expanded={expanded}
|
|
91
|
+
aria-controls={outputId}
|
|
92
|
+
title={expanded ? "Collapse output" : "Expand output"}
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex w-full min-w-0 rounded px-2.5 py-1 text-left focus-visible:outline-2 focus-visible:outline-ring",
|
|
95
|
+
expanded &&
|
|
96
|
+
"items-center justify-between gap-2 text-xs hover:text-foreground",
|
|
97
|
+
)}
|
|
98
|
+
onClick={() => {
|
|
99
|
+
setExpanded(!expanded);
|
|
100
|
+
setFollowing(true);
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{expanded ? (
|
|
104
|
+
<>
|
|
105
|
+
<span className="leading-5">Logs</span>
|
|
106
|
+
<ChevronUpIcon className="size-3.5 shrink-0" aria-hidden={true} />
|
|
107
|
+
</>
|
|
108
|
+
) : (
|
|
109
|
+
<span
|
|
110
|
+
className={cn(
|
|
111
|
+
"min-w-0 flex-1 font-mono text-[11px] leading-5",
|
|
112
|
+
// Fade the top of the tail so it reads as a stream, not as
|
|
113
|
+
// greyed-out prose.
|
|
114
|
+
lines.length > 1 &&
|
|
115
|
+
"[mask-image:linear-gradient(to_bottom,transparent,#000_70%)]",
|
|
116
|
+
)}
|
|
117
|
+
aria-hidden={true}
|
|
118
|
+
>
|
|
119
|
+
{lines.slice(-3).map((line, index) => (
|
|
120
|
+
<span key={index} className={cn("block truncate", emphasis)}>
|
|
121
|
+
{line || "\u00A0"}
|
|
122
|
+
</span>
|
|
123
|
+
))}
|
|
124
|
+
</span>
|
|
125
|
+
)}
|
|
126
|
+
</button>
|
|
127
|
+
{expanded && (
|
|
128
|
+
<CopyClipboardIcon
|
|
129
|
+
value={logs}
|
|
130
|
+
ariaLabel={`Copy ${label}`}
|
|
131
|
+
tooltip="Copy output"
|
|
132
|
+
className="size-3.5"
|
|
133
|
+
buttonClassName="size-6 shrink-0 flex items-center justify-center rounded hover:bg-accent focus-visible:outline-2 focus-visible:outline-ring"
|
|
134
|
+
/>
|
|
135
|
+
)}
|
|
136
|
+
</div>
|
|
137
|
+
<pre
|
|
138
|
+
id={outputId}
|
|
139
|
+
ref={outputRef}
|
|
140
|
+
hidden={!expanded}
|
|
141
|
+
tabIndex={0}
|
|
142
|
+
aria-label={label}
|
|
143
|
+
className="max-h-48 overflow-auto overscroll-contain whitespace-pre px-2.5 pb-2.5 font-mono text-[11px] leading-5 scrollbar-thin focus-visible:outline-2 focus-visible:outline-ring"
|
|
144
|
+
onScroll={(event) => {
|
|
145
|
+
const output = event.currentTarget;
|
|
146
|
+
setFollowing(
|
|
147
|
+
!hasSelection(output) &&
|
|
148
|
+
output.scrollHeight - output.scrollTop - output.clientHeight < 12,
|
|
149
|
+
);
|
|
150
|
+
}}
|
|
151
|
+
/>
|
|
152
|
+
{expanded && !following && (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="absolute bottom-2 left-1/2 -translate-x-1/2 flex items-center gap-1 whitespace-nowrap rounded-full border bg-background px-2 py-1 text-xs shadow-xs hover:text-foreground focus-visible:outline-2 focus-visible:outline-ring"
|
|
156
|
+
onClick={() => {
|
|
157
|
+
const output = outputRef.current;
|
|
158
|
+
if (output) {
|
|
159
|
+
if (hasSelection(output)) {
|
|
160
|
+
output.ownerDocument.getSelection()?.removeAllRanges();
|
|
161
|
+
}
|
|
162
|
+
output.focus({ preventScroll: true });
|
|
163
|
+
}
|
|
164
|
+
setFollowing(true);
|
|
165
|
+
}}
|
|
166
|
+
>
|
|
167
|
+
<ArrowDownIcon className="size-3" aria-hidden={true} />
|
|
168
|
+
Jump to latest
|
|
169
|
+
</button>
|
|
170
|
+
)}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { useAtomValue } from "jotai";
|
|
3
|
+
import { AlertCircleIcon, CheckIcon, CircleIcon } from "lucide-react";
|
|
4
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
5
|
+
import { preparationAtom } from "@/core/packages/sandbox-state";
|
|
6
|
+
import { startupProgressAtom } from "@/core/network/connection";
|
|
7
|
+
import type { DisplayConnectionNotice } from "@/core/network/useConnectionNotice";
|
|
8
|
+
import { cn } from "@/utils/cn";
|
|
9
|
+
import { StartupOutput, type StartupOutputState } from "./startup-output";
|
|
10
|
+
|
|
11
|
+
const STEPS = [
|
|
12
|
+
{
|
|
13
|
+
phase: "preparing-environment",
|
|
14
|
+
titles: {
|
|
15
|
+
complete: "Environment prepared",
|
|
16
|
+
failed: "Environment setup failed",
|
|
17
|
+
pending: "Preparing environment",
|
|
18
|
+
waiting: "Prepare environment",
|
|
19
|
+
},
|
|
20
|
+
descriptions: {
|
|
21
|
+
complete: "Python and dependencies are available.",
|
|
22
|
+
failed: "Setup could not finish.",
|
|
23
|
+
pending: "Set up Python and dependencies.",
|
|
24
|
+
waiting: "Set up Python and dependencies.",
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
phase: "starting-kernel",
|
|
29
|
+
titles: {
|
|
30
|
+
complete: "Kernel started",
|
|
31
|
+
failed: "Kernel failed to start",
|
|
32
|
+
pending: "Starting kernel",
|
|
33
|
+
waiting: "Start kernel",
|
|
34
|
+
},
|
|
35
|
+
descriptions: {
|
|
36
|
+
complete: "The notebook is ready to run.",
|
|
37
|
+
failed: "The kernel could not connect.",
|
|
38
|
+
pending: "Connecting to this notebook.",
|
|
39
|
+
waiting: "After the environment is ready.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
interface StartupProgressProps {
|
|
45
|
+
notice: DisplayConnectionNotice;
|
|
46
|
+
surface: "notebook" | "sidebar";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function StartupStepOutput({
|
|
50
|
+
phase,
|
|
51
|
+
state,
|
|
52
|
+
}: {
|
|
53
|
+
phase: (typeof STEPS)[number]["phase"];
|
|
54
|
+
state: StartupOutputState;
|
|
55
|
+
}) {
|
|
56
|
+
const preparation = useAtomValue(preparationAtom);
|
|
57
|
+
const progress = useAtomValue(startupProgressAtom);
|
|
58
|
+
const preparing = phase === "preparing-environment";
|
|
59
|
+
const logs = preparing
|
|
60
|
+
? (preparation?.logs.environment ?? "")
|
|
61
|
+
: progress?.phase === phase
|
|
62
|
+
? progress.logs
|
|
63
|
+
: "";
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<StartupOutput
|
|
67
|
+
key={preparation?.operation_id}
|
|
68
|
+
logs={logs}
|
|
69
|
+
state={state}
|
|
70
|
+
label={
|
|
71
|
+
preparing ? "environment preparation output" : "kernel startup output"
|
|
72
|
+
}
|
|
73
|
+
/>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function StartupSummary({ notice }: { notice: DisplayConnectionNotice }) {
|
|
78
|
+
return (
|
|
79
|
+
<header>
|
|
80
|
+
<output className="sr-only">{notice.title}</output>
|
|
81
|
+
<h2 className="text-foreground font-heading text-2xl leading-8 tracking-tight font-normal">
|
|
82
|
+
{notice.ready
|
|
83
|
+
? "Your notebook is ready"
|
|
84
|
+
: "Getting your notebook ready"}
|
|
85
|
+
</h2>
|
|
86
|
+
<p className="text-muted-foreground mt-3 text-sm leading-6">
|
|
87
|
+
{notice.ready
|
|
88
|
+
? "Your environment and kernel are ready."
|
|
89
|
+
: notice.pending
|
|
90
|
+
? "Preparing Python and dependencies for this notebook."
|
|
91
|
+
: "Review the setup details below to continue."}
|
|
92
|
+
</p>
|
|
93
|
+
</header>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function ConnectionStatusIcon({
|
|
98
|
+
notice,
|
|
99
|
+
}: {
|
|
100
|
+
notice: DisplayConnectionNotice;
|
|
101
|
+
}) {
|
|
102
|
+
if (notice.ready) {
|
|
103
|
+
return (
|
|
104
|
+
<CheckIcon
|
|
105
|
+
className="size-4 shrink-0 text-(--grass-11)"
|
|
106
|
+
aria-hidden={true}
|
|
107
|
+
/>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (notice.pending) {
|
|
111
|
+
return (
|
|
112
|
+
<Spinner
|
|
113
|
+
className="size-4 shrink-0 motion-reduce:animate-none"
|
|
114
|
+
aria-hidden={true}
|
|
115
|
+
/>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return (
|
|
119
|
+
<AlertCircleIcon
|
|
120
|
+
className="size-4 shrink-0 text-(--red-11)"
|
|
121
|
+
aria-hidden={true}
|
|
122
|
+
/>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function StartupProgress({ notice, surface }: StartupProgressProps) {
|
|
127
|
+
return (
|
|
128
|
+
<>
|
|
129
|
+
{surface === "notebook" && <StartupSummary notice={notice} />}
|
|
130
|
+
<ol
|
|
131
|
+
className={cn("space-y-6", surface === "notebook" && "mt-8")}
|
|
132
|
+
aria-label="Notebook startup stages"
|
|
133
|
+
>
|
|
134
|
+
{STEPS.map(({ phase, titles, descriptions }, index) => {
|
|
135
|
+
const complete =
|
|
136
|
+
notice.ready || (index === 0 && notice.phase === "starting-kernel");
|
|
137
|
+
const active = !notice.ready && notice.phase === phase;
|
|
138
|
+
const failed = active && !notice.pending;
|
|
139
|
+
const state = complete
|
|
140
|
+
? "complete"
|
|
141
|
+
: failed
|
|
142
|
+
? "failed"
|
|
143
|
+
: active
|
|
144
|
+
? "pending"
|
|
145
|
+
: "waiting";
|
|
146
|
+
return (
|
|
147
|
+
<li
|
|
148
|
+
key={phase}
|
|
149
|
+
aria-current={active ? "step" : undefined}
|
|
150
|
+
className="relative flex items-start gap-3"
|
|
151
|
+
>
|
|
152
|
+
{index === 0 && (
|
|
153
|
+
<span
|
|
154
|
+
aria-hidden={true}
|
|
155
|
+
className="absolute left-2 top-6 -bottom-4 border-l border-border"
|
|
156
|
+
/>
|
|
157
|
+
)}
|
|
158
|
+
<span
|
|
159
|
+
className={cn(
|
|
160
|
+
"mt-0.5",
|
|
161
|
+
complete ? "text-(--grass-11)" : "text-muted-foreground",
|
|
162
|
+
)}
|
|
163
|
+
>
|
|
164
|
+
{complete ? (
|
|
165
|
+
<CheckIcon className="size-4" aria-hidden={true} />
|
|
166
|
+
) : active ? (
|
|
167
|
+
<ConnectionStatusIcon notice={notice} />
|
|
168
|
+
) : (
|
|
169
|
+
<CircleIcon
|
|
170
|
+
className="size-4 text-muted-foreground/50"
|
|
171
|
+
aria-hidden={true}
|
|
172
|
+
/>
|
|
173
|
+
)}
|
|
174
|
+
</span>
|
|
175
|
+
<div className="min-w-0 flex-1">
|
|
176
|
+
<div
|
|
177
|
+
className={cn(
|
|
178
|
+
"text-sm",
|
|
179
|
+
failed
|
|
180
|
+
? "text-(--red-11)"
|
|
181
|
+
: complete || active
|
|
182
|
+
? "text-foreground"
|
|
183
|
+
: "text-muted-foreground",
|
|
184
|
+
)}
|
|
185
|
+
>
|
|
186
|
+
{titles[state]}
|
|
187
|
+
</div>
|
|
188
|
+
<p className="mt-1 text-xs text-muted-foreground">
|
|
189
|
+
{descriptions[state]}
|
|
190
|
+
</p>
|
|
191
|
+
{surface === "sidebar" && (
|
|
192
|
+
<StartupStepOutput
|
|
193
|
+
phase={phase}
|
|
194
|
+
state={failed ? "failed" : active ? "running" : "succeeded"}
|
|
195
|
+
/>
|
|
196
|
+
)}
|
|
197
|
+
</div>
|
|
198
|
+
</li>
|
|
199
|
+
);
|
|
200
|
+
})}
|
|
201
|
+
</ol>
|
|
202
|
+
</>
|
|
203
|
+
);
|
|
204
|
+
}
|
|
@@ -12,6 +12,7 @@ import { Provider } from "jotai";
|
|
|
12
12
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
13
13
|
import { MockRequestClient } from "@/__mocks__/requests";
|
|
14
14
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
15
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
15
16
|
import { requestClientAtom } from "@/core/network/requests";
|
|
16
17
|
import type {
|
|
17
18
|
DependencyTreeNode,
|
|
@@ -21,7 +22,10 @@ import { withPackageInvalidation } from "@/core/packages/package-data";
|
|
|
21
22
|
import { useInstallPackages } from "@/core/packages/useInstallPackage";
|
|
22
23
|
import { sandboxAtom, sandboxSyncAtom } from "@/core/packages/sandbox-state";
|
|
23
24
|
import { store } from "@/core/state/jotai";
|
|
25
|
+
import { WebSocketState } from "@/core/websocket/types";
|
|
24
26
|
import PackagesPanel from "../packages-panel";
|
|
27
|
+
import { PanelSectionProvider } from "../panel-context";
|
|
28
|
+
import { SandboxToggle } from "../sandbox-toggle";
|
|
25
29
|
|
|
26
30
|
const { openSettings } = vi.hoisted(() => ({
|
|
27
31
|
openSettings: vi.fn(),
|
|
@@ -67,7 +71,8 @@ function renderPanel(
|
|
|
67
71
|
? { backend: context.backend, manifest: "", filename: "notebook.py" }
|
|
68
72
|
: null,
|
|
69
73
|
);
|
|
70
|
-
store.set(
|
|
74
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
75
|
+
store.set(sandboxSyncAtom, { kind: "succeeded" });
|
|
71
76
|
const getPackageList = vi.fn().mockResolvedValue({ packages: [] });
|
|
72
77
|
const client = MockRequestClient.create({
|
|
73
78
|
getPackageList,
|
|
@@ -81,7 +86,10 @@ function renderPanel(
|
|
|
81
86
|
...render(
|
|
82
87
|
<Provider store={store}>
|
|
83
88
|
<TooltipProvider>
|
|
84
|
-
<
|
|
89
|
+
<PanelSectionProvider value="sidebar">
|
|
90
|
+
<SandboxToggle section="sidebar" />
|
|
91
|
+
<PackagesPanel />
|
|
92
|
+
</PanelSectionProvider>
|
|
85
93
|
</TooltipProvider>
|
|
86
94
|
</Provider>,
|
|
87
95
|
),
|
|
@@ -178,7 +186,8 @@ it("refreshes an open panel when a package is installed elsewhere, after install
|
|
|
178
186
|
manifest: "",
|
|
179
187
|
filename: "notebook.py",
|
|
180
188
|
});
|
|
181
|
-
store.set(
|
|
189
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
190
|
+
store.set(sandboxSyncAtom, { kind: "succeeded" });
|
|
182
191
|
store.set(requestClientAtom, withPackageInvalidation(client));
|
|
183
192
|
function InstallElsewhere() {
|
|
184
193
|
const { handleInstallPackages } = useInstallPackages();
|
|
@@ -191,7 +200,10 @@ it("refreshes an open panel when a package is installed elsewhere, after install
|
|
|
191
200
|
render(
|
|
192
201
|
<Provider store={store}>
|
|
193
202
|
<TooltipProvider>
|
|
194
|
-
<
|
|
203
|
+
<PanelSectionProvider value="sidebar">
|
|
204
|
+
<SandboxToggle section="sidebar" />
|
|
205
|
+
<PackagesPanel />
|
|
206
|
+
</PanelSectionProvider>
|
|
195
207
|
<InstallElsewhere />
|
|
196
208
|
</TooltipProvider>
|
|
197
209
|
</Provider>,
|
|
@@ -208,6 +220,43 @@ it("refreshes an open panel when a package is installed elsewhere, after install
|
|
|
208
220
|
expect(await screen.findByText("polars")).toBeInTheDocument();
|
|
209
221
|
});
|
|
210
222
|
|
|
223
|
+
it("keeps packages and the install draft visible while blocking mutations during sync", async () => {
|
|
224
|
+
store.set(connectionAtom, { state: WebSocketState.OPEN });
|
|
225
|
+
renderPanel({ kind: "sandbox", backend: "pixi" }, populatedTree);
|
|
226
|
+
await screen.findByRole("treeitem", { name: /polars/ });
|
|
227
|
+
const input = screen.getByPlaceholderText("Add packages to pixi sandbox...");
|
|
228
|
+
fireEvent.change(input, { target: { value: "altair" } });
|
|
229
|
+
act(() => store.set(sandboxSyncAtom, { kind: "running" }));
|
|
230
|
+
const row = screen.getByRole("treeitem", { name: /polars/ });
|
|
231
|
+
expect(
|
|
232
|
+
screen.getByPlaceholderText("Add packages to pixi sandbox..."),
|
|
233
|
+
).toBeDisabled();
|
|
234
|
+
expect(within(row).getByRole("button", { name: "Remove" })).toBeDisabled();
|
|
235
|
+
expect(
|
|
236
|
+
screen.queryByRole("list", { name: "Notebook startup stages" }),
|
|
237
|
+
).not.toBeInTheDocument();
|
|
238
|
+
const toggle = screen.getByRole("button", { name: "pixi sandbox" });
|
|
239
|
+
fireEvent.click(toggle);
|
|
240
|
+
expect(toggle).toHaveAttribute("aria-expanded", "false");
|
|
241
|
+
act(() =>
|
|
242
|
+
store.set(sandboxSyncAtom, {
|
|
243
|
+
kind: "failed",
|
|
244
|
+
error: "Could not resolve dependencies",
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
expect(screen.getByRole("treeitem", { name: /polars/ })).toBeInTheDocument();
|
|
248
|
+
expect(screen.getByLabelText("Error details")).toHaveTextContent(
|
|
249
|
+
"Could not resolve dependencies",
|
|
250
|
+
);
|
|
251
|
+
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
|
252
|
+
expect(screen.getByRole("button", { name: "Retry sync" })).toBeVisible();
|
|
253
|
+
const restoredInput = screen.getByPlaceholderText(
|
|
254
|
+
"Add packages to pixi sandbox...",
|
|
255
|
+
);
|
|
256
|
+
expect(restoredInput).toHaveValue("altair");
|
|
257
|
+
expect(restoredInput).toBeEnabled();
|
|
258
|
+
});
|
|
259
|
+
|
|
211
260
|
it.each(["uv", "pixi", "list"] as const)(
|
|
212
261
|
"allows upgrading marimo but not removing it in the %s view",
|
|
213
262
|
async (view) => {
|