@marimo-team/frontend 0.19.7-dev35 → 0.19.7-dev37
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/{JsonOutput-BGSqWZkD.js → JsonOutput-DtidtKaJ.js} +2 -2
- package/dist/assets/{MarimoErrorOutput-DFV7O8tN.js → MarimoErrorOutput-Cci2wITc.js} +1 -1
- package/dist/assets/{add-cell-with-ai-gDjJxct8.js → add-cell-with-ai-DT1ae2MK.js} +1 -1
- package/dist/assets/{add-database-form-CqEjMrq2.js → add-database-form-_pTLtiHN.js} +1 -1
- package/dist/assets/{agent-panel-D_hcgMlT.js → agent-panel-oLGqa4bG.js} +1 -1
- package/dist/assets/{ai-model-dropdown-C_l_zAXd.js → ai-model-dropdown-BL-PaF8o.js} +1 -1
- package/dist/assets/{app-config-button-CM05VD_Y.js → app-config-button-BiwYEPE8.js} +1 -1
- package/dist/assets/{cell-editor-ihhw9Ql3.js → cell-editor-B3U1SnYJ.js} +1 -1
- package/dist/assets/{chat-display-yGAtJXm0.js → chat-display-B34MaCGM.js} +1 -1
- package/dist/assets/{chat-panel-D3T1CjpZ.js → chat-panel-fuQFRvFm.js} +1 -1
- package/dist/assets/{column-preview-DbqM1diq.js → column-preview-B-dViv1i.js} +1 -1
- package/dist/assets/{command-palette-C2L80RlN.js → command-palette-KuNgJNix.js} +1 -1
- package/dist/assets/{common-D32S2RVD.js → common-DxKcMlJZ.js} +1 -1
- package/dist/assets/{dependency-graph-panel-C7ZXgECY.js → dependency-graph-panel-BZEIOxVz.js} +1 -1
- package/dist/assets/download-Bwa9P-Pz.js +6 -0
- package/dist/assets/{dropdown-menu-df9T83C0.js → dropdown-menu-B-6unW-7.js} +1 -1
- package/dist/assets/edit-page-DBFIML4p.js +13 -0
- package/dist/assets/{error-panel-DKIWwbhe.js → error-panel-DulelhA-.js} +1 -1
- package/dist/assets/{file-explorer-panel-D4VHkjMW.js → file-explorer-panel-Dn9tKw3E.js} +1 -1
- package/dist/assets/{form-XjMNGyzu.js → form-B1n-e_X0.js} +1 -1
- package/dist/assets/{glide-data-editor-BBSxoBI-.js → glide-data-editor-HGkaxqOo.js} +1 -1
- package/dist/assets/home-page--XVUAUCM.js +4 -0
- package/dist/assets/hooks-B1nUQK2T.js +1 -0
- package/dist/assets/{html-to-image-DKvXQkl5.js → html-to-image-Cu1p0tCK.js} +2 -2
- package/dist/assets/{index-DmMvDRRC.css → index-Bj5F80Z9.css} +1 -1
- package/dist/assets/{index-BRCNo5ma.js → index-DTpRqi46.js} +5 -5
- package/dist/assets/{layout-DiDoTAUA.js → layout-KY92f2Sm.js} +3 -3
- package/dist/assets/{markdown-renderer-xQJ1KM4c.js → markdown-renderer-Dpn5NCvn.js} +1 -1
- package/dist/assets/{packages-panel-D53RuG3X.js → packages-panel-CBc59eNR.js} +1 -1
- package/dist/assets/{panels-BhznEx5N.js → panels-B0B71dYl.js} +1 -1
- package/dist/assets/{popover-D16ZremR.js → popover-Gz-GJzym.js} +1 -1
- package/dist/assets/{readonly-python-code-kImQwJ5f.js → readonly-python-code-CCwpyiLX.js} +1 -1
- package/dist/assets/{renderShortcut-DHc-p-_c.js → renderShortcut-DEwfrKeS.js} +1 -1
- package/dist/assets/run-page-Dug0EU2T.js +1 -0
- package/dist/assets/{scratchpad-panel-C8wx6cRl.js → scratchpad-panel-SMFZ5eRQ.js} +1 -1
- package/dist/assets/{secrets-panel-Br6CcsOE.js → secrets-panel-BaEqnh6m.js} +1 -1
- package/dist/assets/{session-panel-JOOuJNOH.js → session-panel-CR_CZBSy.js} +1 -1
- package/dist/assets/table-C8uQmBAN.js +1 -0
- package/dist/assets/{terminal-DNwT6UrR.js → terminal-C7HXI-7B.js} +1 -1
- package/dist/assets/{tree-BdwmBGSx.js → tree-B1vM35Zj.js} +1 -1
- package/dist/assets/{useAddCell-BMDEXuVk.js → useAddCell-DRmuczCx.js} +1 -1
- package/dist/assets/{useCellActionButton-BDMlZzyv.js → useCellActionButton-DwRoApVS.js} +1 -1
- package/dist/assets/{useDeleteCell-BrMXAFkS.js → useDeleteCell-CR3IczUk.js} +1 -1
- package/dist/assets/{useDependencyPanelTab-B63bb4YX.js → useDependencyPanelTab-CLgnO1zH.js} +1 -1
- package/dist/assets/useNotebookActions-DhF-uJ0P.js +1 -0
- package/dist/assets/{useSplitCell-Cb0lf5MV.js → useSplitCell-IQsKBoRj.js} +1 -1
- package/dist/assets/{utilities.esm-BVFPJPyV.js → utilities.esm-DyYLtC1k.js} +2 -2
- package/dist/index.html +20 -20
- package/package.json +1 -1
- package/src/components/data-table/TableActions.tsx +5 -3
- package/src/components/data-table/download-actions.tsx +7 -2
- package/src/components/data-table/pagination.tsx +4 -4
- package/src/components/debug/indicator.tsx +1 -1
- package/src/components/editor/actions/useNotebookActions.tsx +4 -2
- package/src/components/editor/chrome/panels/context-aware-panel/context-aware-panel.tsx +1 -1
- package/src/components/editor/chrome/wrapper/app-chrome.tsx +6 -4
- package/src/components/editor/chrome/wrapper/footer-items/lsp-status.tsx +178 -0
- package/src/components/editor/chrome/wrapper/footer.tsx +1 -1
- package/src/components/editor/chrome/wrapper/sidebar.tsx +1 -1
- package/src/components/editor/controls/Controls.tsx +2 -2
- package/src/components/editor/controls/notebook-menu-dropdown.tsx +1 -1
- package/src/components/editor/file-tree/file-explorer.tsx +1 -1
- package/src/components/editor/header/status.tsx +1 -1
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +13 -4
- package/src/components/home/components.tsx +1 -1
- package/src/components/static-html/static-banner.tsx +1 -1
- package/src/components/ui/dropdown-menu.tsx +1 -1
- package/src/components/ui/table.tsx +1 -1
- package/src/core/export/__tests__/hooks.test.ts +60 -58
- package/src/core/export/hooks.ts +71 -31
- package/src/core/network/types.ts +4 -0
- package/src/css/app/print.css +0 -14
- package/src/utils/__tests__/async-capture-tracker.test.ts +353 -0
- package/src/utils/__tests__/download.test.tsx +5 -114
- package/src/utils/async-capture-tracker.ts +168 -0
- package/src/utils/download.ts +17 -57
- package/src/utils/html-to-image.ts +9 -12
- package/dist/assets/download-CzPV-R6Z.js +0 -6
- package/dist/assets/edit-page-DFpXAt-U.js +0 -12
- package/dist/assets/home-page-DBWXVxWa.js +0 -4
- package/dist/assets/hooks-CpjzmBkw.js +0 -1
- package/dist/assets/run-page-BCOAGhl5.js +0 -1
- package/dist/assets/table-BSASHvkq.js +0 -1
- package/dist/assets/useNotebookActions-Cj4FtiIb.js +0 -1
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { atom, useAtomValue, useSetAtom } from "jotai";
|
|
4
|
+
import { AlertCircleIcon, CheckCircle2Icon } from "lucide-react";
|
|
5
|
+
import type React from "react";
|
|
6
|
+
import { Spinner } from "@/components/icons/spinner";
|
|
7
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
8
|
+
import { toast } from "@/components/ui/use-toast";
|
|
9
|
+
import { API } from "@/core/network/api";
|
|
10
|
+
import { connectionAtom } from "@/core/network/connection";
|
|
11
|
+
import type {
|
|
12
|
+
LspHealthResponse,
|
|
13
|
+
LspRestartRequest,
|
|
14
|
+
LspRestartResponse,
|
|
15
|
+
} from "@/core/network/types";
|
|
16
|
+
import { isAppConnected } from "@/core/websocket/connection-utils";
|
|
17
|
+
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
18
|
+
import { useInterval } from "@/hooks/useInterval";
|
|
19
|
+
|
|
20
|
+
const CHECK_LSP_HEALTH_INTERVAL_MS = 60_000;
|
|
21
|
+
|
|
22
|
+
export const lspHealthAtom = atom<LspHealthResponse | null>(null);
|
|
23
|
+
|
|
24
|
+
export const LspStatus: React.FC = () => {
|
|
25
|
+
const connection = useAtomValue(connectionAtom).state;
|
|
26
|
+
const setLspHealth = useSetAtom(lspHealthAtom);
|
|
27
|
+
|
|
28
|
+
const { isFetching, data, refetch } = useAsyncData(async () => {
|
|
29
|
+
if (!isAppConnected(connection)) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const health = await API.get<LspHealthResponse>("/lsp/health");
|
|
35
|
+
setLspHealth(health);
|
|
36
|
+
return health;
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}, [connection]);
|
|
41
|
+
|
|
42
|
+
useInterval(refetch, {
|
|
43
|
+
delayMs: isAppConnected(connection) ? CHECK_LSP_HEALTH_INTERVAL_MS : null,
|
|
44
|
+
whenVisible: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const handleRestart = async () => {
|
|
48
|
+
try {
|
|
49
|
+
const result = await API.post<LspRestartRequest, LspRestartResponse>(
|
|
50
|
+
"/lsp/restart",
|
|
51
|
+
{},
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (result.success) {
|
|
55
|
+
toast({
|
|
56
|
+
title: "LSP Servers Restarted",
|
|
57
|
+
description:
|
|
58
|
+
result.restarted.length > 0
|
|
59
|
+
? `Restarted: ${result.restarted.join(", ")}`
|
|
60
|
+
: "No servers needed restart",
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
toast({
|
|
64
|
+
variant: "danger",
|
|
65
|
+
title: "LSP Restart Failed",
|
|
66
|
+
description: Object.entries(result.errors ?? {})
|
|
67
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
68
|
+
.join("\n"),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Refresh health status
|
|
73
|
+
refetch();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
toast({
|
|
76
|
+
variant: "danger",
|
|
77
|
+
title: "LSP Restart Failed",
|
|
78
|
+
description: error instanceof Error ? error.message : "Unknown error",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Don't show if no LSP servers are configured
|
|
84
|
+
if (!data || data.servers.length === 0) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const getStatusIcon = () => {
|
|
89
|
+
if (isFetching) {
|
|
90
|
+
return <Spinner size="small" />;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!data) {
|
|
94
|
+
return <AlertCircleIcon className="w-4 h-4" />;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
switch (data.status) {
|
|
98
|
+
case "healthy":
|
|
99
|
+
return <CheckCircle2Icon className="w-4 h-4 text-(--green-9)" />;
|
|
100
|
+
case "degraded":
|
|
101
|
+
return <AlertCircleIcon className="w-4 h-4 text-(--yellow-11)" />;
|
|
102
|
+
case "unhealthy":
|
|
103
|
+
return <AlertCircleIcon className="w-4 h-4 text-(--yellow-11)" />;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const getServerStatusDisplay = (
|
|
108
|
+
status: "starting" | "running" | "stopped" | "crashed" | "unresponsive",
|
|
109
|
+
lastPingMs: number | null | undefined,
|
|
110
|
+
) => {
|
|
111
|
+
switch (status) {
|
|
112
|
+
case "running":
|
|
113
|
+
return `✓ OK${lastPingMs == null ? "" : ` (${lastPingMs.toFixed(0)}ms)`}`;
|
|
114
|
+
case "starting":
|
|
115
|
+
return "⋯ Starting";
|
|
116
|
+
case "stopped":
|
|
117
|
+
return "✗ Stopped";
|
|
118
|
+
case "crashed":
|
|
119
|
+
return "✗ Crashed";
|
|
120
|
+
case "unresponsive":
|
|
121
|
+
return "✗ Not responding";
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const getServerStatusColor = (
|
|
126
|
+
status: "starting" | "running" | "stopped" | "crashed" | "unresponsive",
|
|
127
|
+
) => {
|
|
128
|
+
switch (status) {
|
|
129
|
+
case "running":
|
|
130
|
+
return "text-(--green-9)";
|
|
131
|
+
case "starting":
|
|
132
|
+
return "text-(--yellow-11)";
|
|
133
|
+
default:
|
|
134
|
+
return "text-(--red-9)";
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const tooltipContent = (
|
|
139
|
+
<div className="text-sm">
|
|
140
|
+
<b>LSP Status</b>
|
|
141
|
+
<div className="mt-1 text-xs space-y-1">
|
|
142
|
+
{data?.servers.map((server) => (
|
|
143
|
+
<div key={server.serverId} className="flex justify-between gap-2">
|
|
144
|
+
<span>{server.serverId}</span>
|
|
145
|
+
<span className={getServerStatusColor(server.status)}>
|
|
146
|
+
{getServerStatusDisplay(server.status, server.lastPingMs)}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
))}
|
|
150
|
+
</div>
|
|
151
|
+
{data?.status === "healthy" ? null : (
|
|
152
|
+
<div className="mt-2 text-xs text-muted-foreground">
|
|
153
|
+
Click to restart failed servers
|
|
154
|
+
</div>
|
|
155
|
+
)}
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
const handleClick = () => {
|
|
160
|
+
if (data?.status !== "healthy") {
|
|
161
|
+
void handleRestart();
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return (
|
|
166
|
+
<Tooltip content={tooltipContent} data-testid="footer-lsp-status">
|
|
167
|
+
<button
|
|
168
|
+
type="button"
|
|
169
|
+
onClick={handleClick}
|
|
170
|
+
className="p-1 hover:bg-accent rounded flex items-center gap-1.5 text-xs text-muted-foreground"
|
|
171
|
+
data-testid="lsp-status"
|
|
172
|
+
>
|
|
173
|
+
{getStatusIcon()}
|
|
174
|
+
<span>LSP</span>
|
|
175
|
+
</button>
|
|
176
|
+
</Tooltip>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
@@ -57,7 +57,7 @@ export const Footer: React.FC = () => {
|
|
|
57
57
|
});
|
|
58
58
|
|
|
59
59
|
return (
|
|
60
|
-
<footer className="h-10 py-1 gap-1 bg-background flex items-center text-muted-foreground text-md pl-2 pr-1 border-t border-border select-none
|
|
60
|
+
<footer className="h-10 py-1 gap-1 bg-background flex items-center text-muted-foreground text-md pl-2 pr-1 border-t border-border select-none print:hidden text-sm z-50 hide-on-fullscreen overflow-x-auto overflow-y-hidden scrollbar-thin">
|
|
61
61
|
<FooterItem
|
|
62
62
|
className="h-full"
|
|
63
63
|
tooltip={
|
|
@@ -115,7 +115,7 @@ export const Sidebar: React.FC = () => {
|
|
|
115
115
|
]);
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
|
-
<div className="h-full pt-4 pb-1 px-1 flex flex-col items-start text-muted-foreground text-md select-none
|
|
118
|
+
<div className="h-full pt-4 pb-1 px-1 flex flex-col items-start text-muted-foreground text-md select-none text-sm z-50 dark:bg-background print:hidden hide-on-fullscreen">
|
|
119
119
|
<ReorderableList<PanelDescriptor>
|
|
120
120
|
value={sidebarItems}
|
|
121
121
|
setValue={handleSetSidebarItems}
|
|
@@ -212,7 +212,7 @@ const StopControlButton = ({
|
|
|
212
212
|
};
|
|
213
213
|
|
|
214
214
|
const topRightControls =
|
|
215
|
-
"absolute top-3 right-5 m-0 flex items-center gap-2 min-h-[28px]
|
|
215
|
+
"absolute top-3 right-5 m-0 flex items-center gap-2 min-h-[28px] print:hidden pointer-events-auto z-30";
|
|
216
216
|
|
|
217
217
|
const bottomRightControls =
|
|
218
|
-
"absolute bottom-5 right-5 flex flex-col gap-2 items-center
|
|
218
|
+
"absolute bottom-5 right-5 flex flex-col gap-2 items-center print:hidden pointer-events-auto z-30";
|
|
@@ -111,7 +111,7 @@ export const NotebookMenuDropdown: React.FC<Props> = ({
|
|
|
111
111
|
<DropdownMenuTrigger asChild={true} disabled={disabled}>
|
|
112
112
|
{button}
|
|
113
113
|
</DropdownMenuTrigger>
|
|
114
|
-
<DropdownMenuContent align="end" className="
|
|
114
|
+
<DropdownMenuContent align="end" className="print:hidden w-[240px]">
|
|
115
115
|
{actions.map((action) => {
|
|
116
116
|
if (action.hidden || action.redundant) {
|
|
117
117
|
return null;
|
|
@@ -510,7 +510,7 @@ const Node = ({ node, style, dragHandle }: NodeRendererProps<FileInfo>) => {
|
|
|
510
510
|
return (
|
|
511
511
|
<DropdownMenuContent
|
|
512
512
|
align="end"
|
|
513
|
-
className="
|
|
513
|
+
className="print:hidden w-[220px]"
|
|
514
514
|
onClick={(e) => e.stopPropagation()}
|
|
515
515
|
onCloseAutoFocus={(e) => e.preventDefault()}
|
|
516
516
|
>
|
|
@@ -34,7 +34,7 @@ export const StatusOverlay: React.FC<{
|
|
|
34
34
|
);
|
|
35
35
|
};
|
|
36
36
|
|
|
37
|
-
const topLeftStatus = "
|
|
37
|
+
const topLeftStatus = "print:hidden pointer-events-auto hover:cursor-pointer";
|
|
38
38
|
|
|
39
39
|
const DisconnectedIcon = () => (
|
|
40
40
|
<Tooltip content="App disconnected">
|
|
@@ -41,7 +41,11 @@ import { downloadAsHTML } from "@/core/static/download-html";
|
|
|
41
41
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
42
42
|
import { isWasm } from "@/core/wasm/utils";
|
|
43
43
|
import { cn } from "@/utils/cn";
|
|
44
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
ADD_PRINTING_CLASS,
|
|
46
|
+
downloadBlob,
|
|
47
|
+
downloadHTMLAsImage,
|
|
48
|
+
} from "@/utils/download";
|
|
45
49
|
import { Filenames } from "@/utils/filenames";
|
|
46
50
|
import { FloatingOutline } from "../../chrome/panels/outline/floating-outline";
|
|
47
51
|
import { cellDomProps } from "../../common";
|
|
@@ -185,7 +189,12 @@ const ActionButtons: React.FC<{
|
|
|
185
189
|
if (!app) {
|
|
186
190
|
return;
|
|
187
191
|
}
|
|
188
|
-
await downloadHTMLAsImage({
|
|
192
|
+
await downloadHTMLAsImage({
|
|
193
|
+
element: app,
|
|
194
|
+
filename: document.title,
|
|
195
|
+
// Add body.printing ONLY when converting the whole notebook to a screenshot
|
|
196
|
+
prepare: ADD_PRINTING_CLASS,
|
|
197
|
+
});
|
|
189
198
|
};
|
|
190
199
|
|
|
191
200
|
const handleDownloadAsHTML = async () => {
|
|
@@ -271,7 +280,7 @@ const ActionButtons: React.FC<{
|
|
|
271
280
|
<div
|
|
272
281
|
data-testid="notebook-actions-dropdown"
|
|
273
282
|
className={cn(
|
|
274
|
-
"right-0 top-0 z-50 m-4
|
|
283
|
+
"right-0 top-0 z-50 m-4 print:hidden flex gap-2",
|
|
275
284
|
// If the notebook is static, we have a banner at the top, so
|
|
276
285
|
// we can't use fixed positioning. Ideally this is sticky, but the
|
|
277
286
|
// current dom structure makes that difficult.
|
|
@@ -284,7 +293,7 @@ const ActionButtons: React.FC<{
|
|
|
284
293
|
<MoreHorizontalIcon className="w-4 h-4" />
|
|
285
294
|
</Button>
|
|
286
295
|
</DropdownMenuTrigger>
|
|
287
|
-
<DropdownMenuContent align="end" className="
|
|
296
|
+
<DropdownMenuContent align="end" className="print:hidden w-[220px]">
|
|
288
297
|
{actions}
|
|
289
298
|
</DropdownMenuContent>
|
|
290
299
|
</DropdownMenu>
|
|
@@ -80,7 +80,7 @@ export const OpenTutorialDropDown: React.FC = () => {
|
|
|
80
80
|
<CaretDownIcon className="w-3 h-3 ml-1" />
|
|
81
81
|
</Button>
|
|
82
82
|
</DropdownMenuTrigger>
|
|
83
|
-
<DropdownMenuContent side="bottom" align="end" className="
|
|
83
|
+
<DropdownMenuContent side="bottom" align="end" className="print:hidden">
|
|
84
84
|
{Objects.entries(TUTORIALS).map(
|
|
85
85
|
([tutorialId, [label, Icon, description]]) => (
|
|
86
86
|
<DropdownMenuItem
|
|
@@ -36,7 +36,7 @@ export const StaticBanner: React.FC = () => {
|
|
|
36
36
|
|
|
37
37
|
return (
|
|
38
38
|
<div
|
|
39
|
-
className="px-4 py-2 bg-(--sky-2) border-b border-(--sky-7) text-(--sky-11) flex justify-between items-center gap-4
|
|
39
|
+
className="px-4 py-2 bg-(--sky-2) border-b border-(--sky-7) text-(--sky-11) flex justify-between items-center gap-4 print:hidden text-sm"
|
|
40
40
|
data-testid="static-notebook-banner"
|
|
41
41
|
>
|
|
42
42
|
<span>
|
|
@@ -83,7 +83,7 @@ const DropdownMenuContent = React.forwardRef<
|
|
|
83
83
|
sideOffset={sideOffset}
|
|
84
84
|
className={cn(
|
|
85
85
|
menuContentCommon(),
|
|
86
|
-
"animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
86
|
+
"animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 print:hidden",
|
|
87
87
|
scrollable && "overflow-auto",
|
|
88
88
|
className,
|
|
89
89
|
)}
|
|
@@ -7,7 +7,7 @@ const Table = React.forwardRef<
|
|
|
7
7
|
HTMLTableElement,
|
|
8
8
|
React.HTMLAttributes<HTMLTableElement>
|
|
9
9
|
>(({ className, ...props }, ref) => (
|
|
10
|
-
<div className="w-full overflow-auto scrollbar-thin flex-1">
|
|
10
|
+
<div className="w-full overflow-auto scrollbar-thin flex-1 print:overflow-hidden">
|
|
11
11
|
<table
|
|
12
12
|
ref={ref}
|
|
13
13
|
className={cn("w-full caption-bottom text-sm", className)}
|
|
@@ -10,6 +10,7 @@ import { CellOutputId } from "@/core/cells/ids";
|
|
|
10
10
|
import type { CellRuntimeState } from "@/core/cells/types";
|
|
11
11
|
import { ProgressState } from "@/utils/progress";
|
|
12
12
|
import {
|
|
13
|
+
captureTracker,
|
|
13
14
|
updateCellOutputsWithScreenshots,
|
|
14
15
|
useEnrichCellOutputs,
|
|
15
16
|
} from "../hooks";
|
|
@@ -52,6 +53,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
52
53
|
beforeEach(() => {
|
|
53
54
|
vi.clearAllMocks();
|
|
54
55
|
store = createStore();
|
|
56
|
+
captureTracker.reset();
|
|
55
57
|
});
|
|
56
58
|
|
|
57
59
|
const wrapper = ({ children }: { children: ReactNode }) =>
|
|
@@ -103,7 +105,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
103
105
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
104
106
|
|
|
105
107
|
const takeScreenshots = result.current;
|
|
106
|
-
const output = await takeScreenshots({ progress
|
|
108
|
+
const output = await takeScreenshots({ progress });
|
|
107
109
|
|
|
108
110
|
expect(output).toEqual({});
|
|
109
111
|
expect(document.getElementById).not.toHaveBeenCalled();
|
|
@@ -135,7 +137,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
135
137
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
136
138
|
|
|
137
139
|
const takeScreenshots = result.current;
|
|
138
|
-
const output = await takeScreenshots({ progress
|
|
140
|
+
const output = await takeScreenshots({ progress });
|
|
139
141
|
|
|
140
142
|
expect(document.getElementById).toHaveBeenCalledWith(
|
|
141
143
|
CellOutputId.create(cellId),
|
|
@@ -152,50 +154,6 @@ describe("useEnrichCellOutputs", () => {
|
|
|
152
154
|
});
|
|
153
155
|
});
|
|
154
156
|
|
|
155
|
-
it("should pass snappy=true to toPng with includeStyleProperties", async () => {
|
|
156
|
-
const cellId = "cell-1" as CellId;
|
|
157
|
-
const mockElement = document.createElement("div");
|
|
158
|
-
const mockDataUrl = "data:image/png;base64,mockImageData";
|
|
159
|
-
|
|
160
|
-
// Mock document.getElementById
|
|
161
|
-
vi.spyOn(document, "getElementById").mockReturnValue(mockElement);
|
|
162
|
-
vi.mocked(toPng).mockResolvedValue(mockDataUrl);
|
|
163
|
-
|
|
164
|
-
setCellsRuntime(
|
|
165
|
-
createMockCellRuntimes({
|
|
166
|
-
[cellId]: {
|
|
167
|
-
output: {
|
|
168
|
-
channel: "output",
|
|
169
|
-
mimetype: "text/html",
|
|
170
|
-
data: "<div>Chart</div>",
|
|
171
|
-
timestamp: 0,
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
}),
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
178
|
-
|
|
179
|
-
const takeScreenshots = result.current;
|
|
180
|
-
const output = await takeScreenshots({ progress, snappy: true });
|
|
181
|
-
|
|
182
|
-
expect(document.getElementById).toHaveBeenCalledWith(
|
|
183
|
-
CellOutputId.create(cellId),
|
|
184
|
-
);
|
|
185
|
-
// When snappy=true, includeStyleProperties should be set
|
|
186
|
-
expect(toPng).toHaveBeenCalledWith(
|
|
187
|
-
mockElement,
|
|
188
|
-
expect.objectContaining({
|
|
189
|
-
filter: expect.any(Function),
|
|
190
|
-
onImageErrorHandler: expect.any(Function),
|
|
191
|
-
includeStyleProperties: expect.any(Array),
|
|
192
|
-
}),
|
|
193
|
-
);
|
|
194
|
-
expect(output).toEqual({
|
|
195
|
-
[cellId]: ["image/png", mockDataUrl],
|
|
196
|
-
});
|
|
197
|
-
});
|
|
198
|
-
|
|
199
157
|
it("should skip cells where output has not changed", async () => {
|
|
200
158
|
const cellId = "cell-1" as CellId;
|
|
201
159
|
const mockElement = document.createElement("div");
|
|
@@ -224,7 +182,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
224
182
|
|
|
225
183
|
// First call - should capture
|
|
226
184
|
let takeScreenshots = result.current;
|
|
227
|
-
let output = await takeScreenshots({ progress
|
|
185
|
+
let output = await takeScreenshots({ progress });
|
|
228
186
|
expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
|
|
229
187
|
expect(toPng).toHaveBeenCalledTimes(1);
|
|
230
188
|
|
|
@@ -233,7 +191,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
233
191
|
|
|
234
192
|
// Second call with same output - should not capture again
|
|
235
193
|
takeScreenshots = result.current;
|
|
236
|
-
output = await takeScreenshots({ progress
|
|
194
|
+
output = await takeScreenshots({ progress });
|
|
237
195
|
expect(output).toEqual({}); // Empty because output hasn't changed
|
|
238
196
|
expect(toPng).toHaveBeenCalledTimes(1); // Still only 1 call
|
|
239
197
|
});
|
|
@@ -262,7 +220,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
262
220
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
263
221
|
|
|
264
222
|
const takeScreenshots = result.current;
|
|
265
|
-
const output = await takeScreenshots({ progress
|
|
223
|
+
const output = await takeScreenshots({ progress });
|
|
266
224
|
|
|
267
225
|
expect(output).toEqual({}); // Failed screenshot should be filtered out
|
|
268
226
|
expect(Logger.error).toHaveBeenCalledWith(
|
|
@@ -271,6 +229,50 @@ describe("useEnrichCellOutputs", () => {
|
|
|
271
229
|
);
|
|
272
230
|
});
|
|
273
231
|
|
|
232
|
+
it("should retry failed screenshots on next call", async () => {
|
|
233
|
+
const cellId = "cell-1" as CellId;
|
|
234
|
+
const mockElement = document.createElement("div");
|
|
235
|
+
const error = new Error("Screenshot failed");
|
|
236
|
+
const mockDataUrl = "data:image/png;base64,retrySuccess";
|
|
237
|
+
|
|
238
|
+
vi.spyOn(document, "getElementById").mockReturnValue(mockElement);
|
|
239
|
+
// First call fails, second call succeeds
|
|
240
|
+
vi.mocked(toPng)
|
|
241
|
+
.mockRejectedValueOnce(error)
|
|
242
|
+
.mockResolvedValueOnce(mockDataUrl);
|
|
243
|
+
|
|
244
|
+
setCellsRuntime(
|
|
245
|
+
createMockCellRuntimes({
|
|
246
|
+
[cellId]: {
|
|
247
|
+
output: {
|
|
248
|
+
channel: "output",
|
|
249
|
+
mimetype: "text/html",
|
|
250
|
+
data: "<div>Chart</div>",
|
|
251
|
+
timestamp: 0,
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
}),
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const { result, rerender } = renderHook(() => useEnrichCellOutputs(), {
|
|
258
|
+
wrapper,
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// First call - screenshot fails
|
|
262
|
+
let takeScreenshots = result.current;
|
|
263
|
+
let output = await takeScreenshots({ progress });
|
|
264
|
+
expect(output).toEqual({});
|
|
265
|
+
expect(Logger.error).toHaveBeenCalled();
|
|
266
|
+
|
|
267
|
+
rerender();
|
|
268
|
+
|
|
269
|
+
// Second call - should retry since the first one failed
|
|
270
|
+
takeScreenshots = result.current;
|
|
271
|
+
output = await takeScreenshots({ progress });
|
|
272
|
+
expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl] });
|
|
273
|
+
expect(toPng).toHaveBeenCalledTimes(2);
|
|
274
|
+
});
|
|
275
|
+
|
|
274
276
|
it("should handle missing DOM elements", async () => {
|
|
275
277
|
const cellId = "cell-1" as CellId;
|
|
276
278
|
|
|
@@ -292,7 +294,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
292
294
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
293
295
|
|
|
294
296
|
const takeScreenshots = result.current;
|
|
295
|
-
const output = await takeScreenshots({ progress
|
|
297
|
+
const output = await takeScreenshots({ progress });
|
|
296
298
|
|
|
297
299
|
expect(output).toEqual({});
|
|
298
300
|
expect(Logger.error).toHaveBeenCalledWith(
|
|
@@ -341,7 +343,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
341
343
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
342
344
|
|
|
343
345
|
const takeScreenshots = result.current;
|
|
344
|
-
const output = await takeScreenshots({ progress
|
|
346
|
+
const output = await takeScreenshots({ progress });
|
|
345
347
|
|
|
346
348
|
expect(output).toEqual({
|
|
347
349
|
[cell1]: ["image/png", mockDataUrl1],
|
|
@@ -387,7 +389,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
387
389
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
388
390
|
|
|
389
391
|
const takeScreenshots = result.current;
|
|
390
|
-
const output = await takeScreenshots({ progress
|
|
392
|
+
const output = await takeScreenshots({ progress });
|
|
391
393
|
|
|
392
394
|
// Only the successful screenshot should be in the result
|
|
393
395
|
expect(output).toEqual({
|
|
@@ -429,13 +431,13 @@ describe("useEnrichCellOutputs", () => {
|
|
|
429
431
|
|
|
430
432
|
// First screenshot
|
|
431
433
|
let takeScreenshots = result.current;
|
|
432
|
-
let output = await takeScreenshots({ progress
|
|
434
|
+
let output = await takeScreenshots({ progress });
|
|
433
435
|
expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl1] });
|
|
434
436
|
|
|
435
437
|
// Second call - same output, should not be captured
|
|
436
438
|
rerender();
|
|
437
439
|
takeScreenshots = result.current;
|
|
438
|
-
output = await takeScreenshots({ progress
|
|
440
|
+
output = await takeScreenshots({ progress });
|
|
439
441
|
expect(output).toEqual({});
|
|
440
442
|
|
|
441
443
|
// Third call - output changed, should be captured
|
|
@@ -454,7 +456,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
454
456
|
|
|
455
457
|
rerender();
|
|
456
458
|
takeScreenshots = result.current;
|
|
457
|
-
output = await takeScreenshots({ progress
|
|
459
|
+
output = await takeScreenshots({ progress });
|
|
458
460
|
expect(output).toEqual({ [cellId]: ["image/png", mockDataUrl2] });
|
|
459
461
|
expect(toPng).toHaveBeenCalledTimes(2);
|
|
460
462
|
});
|
|
@@ -494,7 +496,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
494
496
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
495
497
|
|
|
496
498
|
const takeScreenshots = result.current;
|
|
497
|
-
const output = await takeScreenshots({ progress
|
|
499
|
+
const output = await takeScreenshots({ progress });
|
|
498
500
|
|
|
499
501
|
// None of these should trigger screenshots
|
|
500
502
|
expect(output).toEqual({});
|
|
@@ -519,7 +521,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
519
521
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
520
522
|
|
|
521
523
|
const takeScreenshots = result.current;
|
|
522
|
-
const output = await takeScreenshots({ progress
|
|
524
|
+
const output = await takeScreenshots({ progress });
|
|
523
525
|
|
|
524
526
|
expect(output).toEqual({});
|
|
525
527
|
expect(document.getElementById).not.toHaveBeenCalled();
|
|
@@ -551,7 +553,7 @@ describe("useEnrichCellOutputs", () => {
|
|
|
551
553
|
const { result } = renderHook(() => useEnrichCellOutputs(), { wrapper });
|
|
552
554
|
|
|
553
555
|
const takeScreenshots = result.current;
|
|
554
|
-
const output = await takeScreenshots({ progress
|
|
556
|
+
const output = await takeScreenshots({ progress });
|
|
555
557
|
|
|
556
558
|
// Verify the exact return type structure
|
|
557
559
|
expect(output).toHaveProperty(cellId);
|