@marimo-team/islands 0.22.5-dev9 → 0.22.6-dev1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ConnectedDataExplorerComponent-mLj6D01z.js → ConnectedDataExplorerComponent-D08JKcQg.js} +1 -1
- package/dist/{chat-ui-X5KPeHrU.js → chat-ui-BXYRQ5MH.js} +3 -3
- package/dist/main.js +212 -131
- package/dist/{mermaid-B93TKi2g.js → mermaid-BZ2YHhbi.js} +1 -1
- package/dist/{process-output-C0tmJosY.js → process-output-D_uZ0o1x.js} +2097 -2090
- package/dist/style.css +1 -1
- package/dist/{toDate-D1_ZulwM.js → toDate-D0QaHNwR.js} +8 -7
- package/dist/{useAsyncData-C9ez7Ilo.js → useAsyncData-BG3ULuDU.js} +1 -1
- package/dist/{useDeepCompareMemoize-BvvMxigY.js → useDeepCompareMemoize-CkSq3l3_.js} +1 -1
- package/dist/{vega-component-Bzzut3-P.js → vega-component-z4WGXPkf.js} +3 -3
- package/package.json +2 -2
- package/src/components/data-table/__tests__/columns.test.tsx +92 -13
- package/src/components/data-table/column-header.tsx +81 -56
- package/src/components/data-table/columns.tsx +25 -32
- package/src/components/data-table/data-table.tsx +8 -1
- package/src/components/data-table/renderers.tsx +19 -6
- package/src/components/data-table/types.ts +4 -0
- package/src/components/editor/Output.tsx +1 -1
- package/src/components/editor/__tests__/Output.test.tsx +36 -1
- package/src/core/cells/__tests__/cells.test.ts +41 -0
- package/src/core/cells/__tests__/collapseConsoleOutputs.test.ts +38 -0
- package/src/core/cells/cells.ts +1 -1
- package/src/core/cells/collapseConsoleOutputs.tsx +3 -0
- package/src/core/cells/document-changes.ts +12 -0
- package/src/core/runtime/__tests__/runtime.test.ts +138 -2
- package/src/core/runtime/runtime.ts +25 -5
- package/src/core/saving/file-state.ts +16 -0
- package/src/hooks/useAsyncData.ts +1 -1
- package/src/mount.tsx +17 -1
- package/src/plugins/impl/DataTablePlugin.tsx +1 -1
- package/src/plugins/impl/plotly/__tests__/selection.test.ts +22 -0
- package/src/plugins/impl/plotly/selection.ts +1 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
import { render, screen } from "@testing-library/react";
|
|
3
3
|
import { describe, expect, it } from "vitest";
|
|
4
|
-
import {
|
|
4
|
+
import { cellId } from "@/__tests__/branded";
|
|
5
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
6
|
+
import { OutputArea, OutputRenderer } from "../Output";
|
|
5
7
|
|
|
6
8
|
describe("OutputRenderer renderFallback prop", () => {
|
|
7
9
|
it("should use renderFallback for unsupported mimetypes", () => {
|
|
@@ -65,6 +67,39 @@ describe("OutputRenderer renderFallback prop", () => {
|
|
|
65
67
|
});
|
|
66
68
|
});
|
|
67
69
|
|
|
70
|
+
describe("OutputArea null/undefined handling", () => {
|
|
71
|
+
it("should render null when output is null", () => {
|
|
72
|
+
const { container } = render(
|
|
73
|
+
<TooltipProvider>
|
|
74
|
+
<OutputArea
|
|
75
|
+
output={null}
|
|
76
|
+
cellId={cellId("test")}
|
|
77
|
+
stale={false}
|
|
78
|
+
loading={false}
|
|
79
|
+
allowExpand={true}
|
|
80
|
+
/>
|
|
81
|
+
</TooltipProvider>,
|
|
82
|
+
);
|
|
83
|
+
expect(container.innerHTML).toBe("");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should render null when output is undefined", () => {
|
|
87
|
+
const { container } = render(
|
|
88
|
+
<TooltipProvider>
|
|
89
|
+
<OutputArea
|
|
90
|
+
// @ts-expect-error -- testing runtime safety for undefined output
|
|
91
|
+
output={undefined}
|
|
92
|
+
cellId={cellId("test")}
|
|
93
|
+
stale={false}
|
|
94
|
+
loading={false}
|
|
95
|
+
allowExpand={true}
|
|
96
|
+
/>
|
|
97
|
+
</TooltipProvider>,
|
|
98
|
+
);
|
|
99
|
+
expect(container.innerHTML).toBe("");
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
68
103
|
describe("OutputRenderer image and SVG rendering", () => {
|
|
69
104
|
const plainSvgString =
|
|
70
105
|
'<svg><rect x="0" y="0" width="10" height="10"></rect></svg>';
|
|
@@ -1209,6 +1209,47 @@ describe("cell reducer", () => {
|
|
|
1209
1209
|
]);
|
|
1210
1210
|
});
|
|
1211
1211
|
|
|
1212
|
+
it("does not crash when setStdinResponse has out-of-bounds outputIndex", () => {
|
|
1213
|
+
const STDOUT: OutputMessage = {
|
|
1214
|
+
channel: "stdout",
|
|
1215
|
+
mimetype: "text/plain",
|
|
1216
|
+
data: "hello!",
|
|
1217
|
+
timestamp: 1,
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
// Set the cell to running with a console output
|
|
1221
|
+
actions.prepareForRun({ cellId: firstCellId });
|
|
1222
|
+
actions.handleCellMessage({
|
|
1223
|
+
cell_id: firstCellId,
|
|
1224
|
+
output: undefined,
|
|
1225
|
+
console: null,
|
|
1226
|
+
status: "running",
|
|
1227
|
+
stale_inputs: null,
|
|
1228
|
+
timestamp: new Date(20).getTime() as Seconds,
|
|
1229
|
+
});
|
|
1230
|
+
actions.handleCellMessage({
|
|
1231
|
+
cell_id: firstCellId,
|
|
1232
|
+
output: undefined,
|
|
1233
|
+
console: STDOUT,
|
|
1234
|
+
status: undefined,
|
|
1235
|
+
stale_inputs: null,
|
|
1236
|
+
timestamp: new Date(22).getTime() as Seconds,
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// Try to set stdin response with an out-of-bounds index
|
|
1240
|
+
// This should not crash - it should return state unchanged
|
|
1241
|
+
actions.setStdinResponse({
|
|
1242
|
+
response: "test",
|
|
1243
|
+
cellId: firstCellId,
|
|
1244
|
+
outputIndex: 999,
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Cell state should be unchanged
|
|
1248
|
+
const cell = cells[0];
|
|
1249
|
+
expect(cell.consoleOutputs).toHaveLength(1);
|
|
1250
|
+
expect(cell.consoleOutputs[0]).toMatchObject(STDOUT);
|
|
1251
|
+
});
|
|
1252
|
+
|
|
1212
1253
|
it("can receive console when the cell is idle and will clear when starts again", () => {
|
|
1213
1254
|
const OLD_STDOUT: OutputMessage = {
|
|
1214
1255
|
channel: "stdout",
|
|
@@ -241,6 +241,44 @@ describe("collapseConsoleOutputs", () => {
|
|
|
241
241
|
expect(result[2].data).toBe("<pre>E\nF\nG\nH\n</pre>");
|
|
242
242
|
});
|
|
243
243
|
|
|
244
|
+
it("should not crash when truncating with a single output at the limit boundary", () => {
|
|
245
|
+
// Create outputs that push truncation to the exact boundary
|
|
246
|
+
const consoleOutputs: OutputMessage[] = [
|
|
247
|
+
{
|
|
248
|
+
mimetype: "text/html",
|
|
249
|
+
channel: "output",
|
|
250
|
+
data: "<div>html1</div>",
|
|
251
|
+
timestamp: 0,
|
|
252
|
+
},
|
|
253
|
+
{
|
|
254
|
+
mimetype: "text/html",
|
|
255
|
+
channel: "output",
|
|
256
|
+
data: "<div>html2</div>",
|
|
257
|
+
timestamp: 0,
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
// With limit=1, truncation must handle edge cases gracefully
|
|
261
|
+
const result = collapseConsoleOutputs(consoleOutputs, 1);
|
|
262
|
+
expect(result[0].data).toContain("Streaming output truncated");
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should handle truncation when cutoff indexes past the end of the array", () => {
|
|
266
|
+
// With maxLines=0, the truncation loop never runs, causing cutoff
|
|
267
|
+
// to index past the array. This exercises the `output == null`
|
|
268
|
+
// defensive branch in truncateHead().
|
|
269
|
+
const consoleOutputs: OutputMessage[] = [
|
|
270
|
+
{
|
|
271
|
+
mimetype: "text/html",
|
|
272
|
+
channel: "output",
|
|
273
|
+
data: "<div>content</div>",
|
|
274
|
+
timestamp: 0,
|
|
275
|
+
},
|
|
276
|
+
];
|
|
277
|
+
const result = collapseConsoleOutputs(consoleOutputs, 0);
|
|
278
|
+
expect(result).toHaveLength(1);
|
|
279
|
+
expect(result[0].data).toContain("Streaming output truncated");
|
|
280
|
+
});
|
|
281
|
+
|
|
244
282
|
describe("ANSI escape sequences", () => {
|
|
245
283
|
it("should handle cursor movement with collapse", () => {
|
|
246
284
|
const consoleOutputs: OutputMessage[] = [
|
package/src/core/cells/cells.ts
CHANGED
|
@@ -947,7 +947,7 @@ const {
|
|
|
947
947
|
cellReducer: (cell) => {
|
|
948
948
|
const consoleOutputs = [...cell.consoleOutputs];
|
|
949
949
|
const stdinOutput = consoleOutputs[outputIndex];
|
|
950
|
-
if (stdinOutput.channel !== "stdin") {
|
|
950
|
+
if (stdinOutput == null || stdinOutput.channel !== "stdin") {
|
|
951
951
|
Logger.warn("Expected stdin output");
|
|
952
952
|
return cell;
|
|
953
953
|
}
|
|
@@ -108,6 +108,9 @@ function truncateHead(consoleOutputs: OutputMessage[], limit: number) {
|
|
|
108
108
|
timestamp: -1,
|
|
109
109
|
};
|
|
110
110
|
const output = consoleOutputs[cutoff];
|
|
111
|
+
if (output == null) {
|
|
112
|
+
return [warningOutput, ...consoleOutputs.slice(cutoff + 1)];
|
|
113
|
+
}
|
|
111
114
|
if (output.mimetype === "text/plain") {
|
|
112
115
|
invariant(typeof output.data === "string", "expected string");
|
|
113
116
|
const outputLines = output.data.split("\n");
|
|
@@ -25,6 +25,7 @@ import type { NotebookDocumentTransactionRequest } from "../network/types";
|
|
|
25
25
|
import { store } from "../state/jotai";
|
|
26
26
|
import type { CellActions, NotebookState } from "./cells";
|
|
27
27
|
import type { CellId } from "./ids";
|
|
28
|
+
import { SCRATCH_CELL_ID } from "./ids";
|
|
28
29
|
import type { CellData } from "./types";
|
|
29
30
|
|
|
30
31
|
export type DocumentChange =
|
|
@@ -572,10 +573,21 @@ const flushChanges = debounce(() => {
|
|
|
572
573
|
void getRequestClient().sendDocumentTransaction({ changes });
|
|
573
574
|
}, 400);
|
|
574
575
|
|
|
576
|
+
function isScratchChange(change: DocumentChange): boolean {
|
|
577
|
+
if ("cellId" in change && change.cellId === SCRATCH_CELL_ID) {
|
|
578
|
+
return true;
|
|
579
|
+
}
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
|
|
575
583
|
function enqueue(change: DocumentChange) {
|
|
576
584
|
if (store.get(kioskModeAtom)) {
|
|
577
585
|
return;
|
|
578
586
|
}
|
|
587
|
+
// The scratchpad cell is local-only — don't sync it to the document.
|
|
588
|
+
if (isScratchChange(change)) {
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
579
591
|
pendingChanges.push(change);
|
|
580
592
|
flushChanges();
|
|
581
593
|
}
|
|
@@ -86,6 +86,74 @@ describe("RuntimeManager", () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
describe("cross-origin auth token in WS URLs", () => {
|
|
90
|
+
it("should add access_token to WS URL when cross-origin with authToken", () => {
|
|
91
|
+
// example.com is cross-origin relative to the test environment (localhost)
|
|
92
|
+
const runtime = new RuntimeManager(
|
|
93
|
+
{
|
|
94
|
+
url: "https://sandbox.example.com",
|
|
95
|
+
lazy: true,
|
|
96
|
+
authToken: "my-secret-token",
|
|
97
|
+
},
|
|
98
|
+
true,
|
|
99
|
+
);
|
|
100
|
+
const url = runtime.getWsURL("s_123" as SessionId);
|
|
101
|
+
|
|
102
|
+
expect(url.searchParams.get("access_token")).toBe("my-secret-token");
|
|
103
|
+
expect(url.searchParams.get("session_id")).toBe("s_123");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("should not add access_token to WS URL when same-origin", () => {
|
|
107
|
+
const runtime = new RuntimeManager(
|
|
108
|
+
{
|
|
109
|
+
url: window.location.origin,
|
|
110
|
+
lazy: true,
|
|
111
|
+
authToken: "my-secret-token",
|
|
112
|
+
},
|
|
113
|
+
true,
|
|
114
|
+
);
|
|
115
|
+
const url = runtime.getWsURL("s_123" as SessionId);
|
|
116
|
+
|
|
117
|
+
expect(url.searchParams.get("access_token")).toBeNull();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("should not add access_token when no authToken is configured", () => {
|
|
121
|
+
const runtime = new RuntimeManager(
|
|
122
|
+
{
|
|
123
|
+
url: "https://sandbox.example.com",
|
|
124
|
+
lazy: true,
|
|
125
|
+
},
|
|
126
|
+
true,
|
|
127
|
+
);
|
|
128
|
+
const url = runtime.getWsURL("s_123" as SessionId);
|
|
129
|
+
|
|
130
|
+
expect(url.searchParams.get("access_token")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("should add access_token to all WS URL types when cross-origin", () => {
|
|
134
|
+
const runtime = new RuntimeManager(
|
|
135
|
+
{
|
|
136
|
+
url: "https://sandbox.example.com",
|
|
137
|
+
lazy: true,
|
|
138
|
+
authToken: "my-secret-token",
|
|
139
|
+
},
|
|
140
|
+
true,
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
const wsUrl = runtime.getWsURL("s_123" as SessionId);
|
|
144
|
+
const wsSyncUrl = runtime.getWsSyncURL("s_123" as SessionId);
|
|
145
|
+
const terminalUrl = runtime.getTerminalWsURL();
|
|
146
|
+
|
|
147
|
+
expect(wsUrl.searchParams.get("access_token")).toBe("my-secret-token");
|
|
148
|
+
expect(wsSyncUrl.searchParams.get("access_token")).toBe(
|
|
149
|
+
"my-secret-token",
|
|
150
|
+
);
|
|
151
|
+
expect(terminalUrl.searchParams.get("access_token")).toBe(
|
|
152
|
+
"my-secret-token",
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
89
157
|
describe("getWsSyncURL", () => {
|
|
90
158
|
it("should return WebSocket Sync URL", () => {
|
|
91
159
|
const runtime = new RuntimeManager(mockConfig);
|
|
@@ -117,12 +185,52 @@ describe("RuntimeManager", () => {
|
|
|
117
185
|
expect(url.pathname).toBe("/lsp/pylsp");
|
|
118
186
|
});
|
|
119
187
|
|
|
120
|
-
it("should return copilot URL", () => {
|
|
121
|
-
const runtime = new RuntimeManager(
|
|
188
|
+
it("should return copilot URL without non-auth query params", () => {
|
|
189
|
+
const runtime = new RuntimeManager({
|
|
190
|
+
url: "https://example.com?foo=bar&baz=qux",
|
|
191
|
+
lazy: true,
|
|
192
|
+
});
|
|
122
193
|
const url = runtime.getLSPURL("copilot");
|
|
123
194
|
|
|
124
195
|
expect(url.protocol).toBe("wss:");
|
|
125
196
|
expect(url.pathname).toBe("/lsp/copilot");
|
|
197
|
+
expect(url.searchParams.get("foo")).toBeNull();
|
|
198
|
+
expect(url.searchParams.get("baz")).toBeNull();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("should preserve access_token on copilot URL when cross-origin", () => {
|
|
202
|
+
const runtime = new RuntimeManager(
|
|
203
|
+
{
|
|
204
|
+
url: "https://sandbox.example.com?foo=bar",
|
|
205
|
+
lazy: true,
|
|
206
|
+
authToken: "my-secret-token",
|
|
207
|
+
},
|
|
208
|
+
true,
|
|
209
|
+
);
|
|
210
|
+
const url = runtime.getLSPURL("copilot");
|
|
211
|
+
|
|
212
|
+
expect(url.protocol).toBe("wss:");
|
|
213
|
+
expect(url.pathname).toBe("/lsp/copilot");
|
|
214
|
+
expect(url.searchParams.get("access_token")).toBe("my-secret-token");
|
|
215
|
+
// Other params should be stripped
|
|
216
|
+
expect(url.searchParams.get("foo")).toBeNull();
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("should not have access_token on copilot URL when same-origin", () => {
|
|
220
|
+
const runtime = new RuntimeManager(
|
|
221
|
+
{
|
|
222
|
+
url: window.location.origin,
|
|
223
|
+
lazy: true,
|
|
224
|
+
authToken: "my-secret-token",
|
|
225
|
+
},
|
|
226
|
+
true,
|
|
227
|
+
);
|
|
228
|
+
const url = runtime.getLSPURL("copilot");
|
|
229
|
+
|
|
230
|
+
expect(url.protocol).toBe("ws:");
|
|
231
|
+
expect(url.pathname).toBe("/lsp/copilot");
|
|
232
|
+
expect(url.searchParams.get("access_token")).toBeNull();
|
|
233
|
+
expect(url.search).toBe("");
|
|
126
234
|
});
|
|
127
235
|
});
|
|
128
236
|
|
|
@@ -200,6 +308,34 @@ describe("RuntimeManager", () => {
|
|
|
200
308
|
|
|
201
309
|
expect(result).toBe(false);
|
|
202
310
|
});
|
|
311
|
+
|
|
312
|
+
it("should update config.url on redirect, stripping /health from pathname", async () => {
|
|
313
|
+
global.fetch = vi.fn().mockResolvedValue({
|
|
314
|
+
ok: true,
|
|
315
|
+
redirected: true,
|
|
316
|
+
url: "https://sandbox.example.com/health?some_value=abc123",
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const runtime = new RuntimeManager(
|
|
320
|
+
{
|
|
321
|
+
...mockConfig,
|
|
322
|
+
url: "https://backend.example.com/lazy?some_value=abc123",
|
|
323
|
+
},
|
|
324
|
+
true, // lazy — don't call init() in constructor
|
|
325
|
+
);
|
|
326
|
+
const result = await runtime.isHealthy();
|
|
327
|
+
|
|
328
|
+
expect(result).toBe(true);
|
|
329
|
+
// Should strip /health from pathname but preserve query params
|
|
330
|
+
const wsUrl = runtime.getWsURL("s_test" as SessionId);
|
|
331
|
+
expect(wsUrl.pathname).toBe("/ws");
|
|
332
|
+
expect(wsUrl.hostname).toBe("sandbox.example.com");
|
|
333
|
+
expect(wsUrl.searchParams.get("some_value")).toBe("abc123");
|
|
334
|
+
|
|
335
|
+
// Clean up side effects
|
|
336
|
+
document.querySelectorAll("base").forEach((el) => el.remove());
|
|
337
|
+
global.fetch = vi.fn().mockResolvedValue({ ok: false });
|
|
338
|
+
});
|
|
203
339
|
});
|
|
204
340
|
|
|
205
341
|
describe("waitForHealthy", () => {
|
|
@@ -88,6 +88,15 @@ export class RuntimeManager {
|
|
|
88
88
|
searchParams,
|
|
89
89
|
/* restrictToKnownQueryParams =*/ false,
|
|
90
90
|
);
|
|
91
|
+
|
|
92
|
+
// For cross-origin runtimes, pass the auth token as a query parameter.
|
|
93
|
+
// WebSocket connections cannot send custom headers (no Authorization
|
|
94
|
+
// header), and cross-origin cookies are blocked by browsers, so the
|
|
95
|
+
// access_token query param is the only way to authenticate.
|
|
96
|
+
if (!this.isSameOrigin && this.config.authToken) {
|
|
97
|
+
url.searchParams.set(KnownQueryParams.accessToken, this.config.authToken);
|
|
98
|
+
}
|
|
99
|
+
|
|
91
100
|
return asWsUrl(url.toString());
|
|
92
101
|
}
|
|
93
102
|
|
|
@@ -143,9 +152,15 @@ export class RuntimeManager {
|
|
|
143
152
|
*/
|
|
144
153
|
getLSPURL(lsp: "pylsp" | "basedpyright" | "copilot" | "ty" | "pyrefly"): URL {
|
|
145
154
|
if (lsp === "copilot") {
|
|
146
|
-
// For copilot,
|
|
155
|
+
// For copilot, strip all query parameters except the auth token.
|
|
156
|
+
// Copilot doesn't understand arbitrary query params, but we still
|
|
157
|
+
// need access_token for cross-origin authentication.
|
|
147
158
|
const url = this.formatWsURL(`/lsp/${lsp}`);
|
|
159
|
+
const accessToken = url.searchParams.get(KnownQueryParams.accessToken);
|
|
148
160
|
url.search = "";
|
|
161
|
+
if (accessToken) {
|
|
162
|
+
url.searchParams.set(KnownQueryParams.accessToken, accessToken);
|
|
163
|
+
}
|
|
149
164
|
return url;
|
|
150
165
|
}
|
|
151
166
|
return this.formatWsURL(`/lsp/${lsp}`);
|
|
@@ -173,9 +188,10 @@ export class RuntimeManager {
|
|
|
173
188
|
// If there is a redirect, update the URL in the config
|
|
174
189
|
if (response.redirected) {
|
|
175
190
|
Logger.debug(`Runtime redirected to ${response.url}`);
|
|
176
|
-
// strip /health from the URL
|
|
177
|
-
const
|
|
178
|
-
|
|
191
|
+
// strip /health from the URL, using URL parsing to handle query params
|
|
192
|
+
const redirected = new URL(response.url);
|
|
193
|
+
redirected.pathname = redirected.pathname.replace(/\/health$/, "");
|
|
194
|
+
this.config.url = redirected.toString();
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
const success = response.ok;
|
|
@@ -183,7 +199,11 @@ export class RuntimeManager {
|
|
|
183
199
|
this.setDOMBaseUri(this.config.url);
|
|
184
200
|
}
|
|
185
201
|
return success;
|
|
186
|
-
} catch {
|
|
202
|
+
} catch (error) {
|
|
203
|
+
Logger.error(
|
|
204
|
+
`Failed to check health: ${error instanceof Error ? error.message : "Unknown error"}`,
|
|
205
|
+
{ cause: error },
|
|
206
|
+
);
|
|
187
207
|
return false;
|
|
188
208
|
}
|
|
189
209
|
}
|
|
@@ -16,6 +16,22 @@ export const filenameAtom = atom<string | null>(getFilenameFromDOM());
|
|
|
16
16
|
*/
|
|
17
17
|
export const cwdAtom = atom<string | null>(null);
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* LSP workspace information from the backend.
|
|
21
|
+
* Contains the project root and the document's file URI.
|
|
22
|
+
*/
|
|
23
|
+
export interface LspWorkspace {
|
|
24
|
+
rootUri: string;
|
|
25
|
+
documentUri: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Atom for storing the LSP workspace information.
|
|
30
|
+
* This is populated during active notebook sessions
|
|
31
|
+
* and null for other pages.
|
|
32
|
+
*/
|
|
33
|
+
export const lspWorkspaceAtom = atom<LspWorkspace | null>(null);
|
|
34
|
+
|
|
19
35
|
/**
|
|
20
36
|
* Set for static notebooks.
|
|
21
37
|
*/
|
|
@@ -341,7 +341,7 @@ export function useAsyncData<T>(
|
|
|
341
341
|
};
|
|
342
342
|
setResult((prevResult) => {
|
|
343
343
|
// If we have previous data, show reloading state
|
|
344
|
-
if (prevResult.status === "success") {
|
|
344
|
+
if (prevResult.status === "success" || prevResult.status === "loading") {
|
|
345
345
|
return Result.loading(prevResult.data);
|
|
346
346
|
}
|
|
347
347
|
// Otherwise, show initial loading state
|
package/src/mount.tsx
CHANGED
|
@@ -36,7 +36,12 @@ import {
|
|
|
36
36
|
DEFAULT_RUNTIME_CONFIG,
|
|
37
37
|
runtimeConfigAtom,
|
|
38
38
|
} from "./core/runtime/config";
|
|
39
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
codeAtom,
|
|
41
|
+
cwdAtom,
|
|
42
|
+
filenameAtom,
|
|
43
|
+
lspWorkspaceAtom,
|
|
44
|
+
} from "./core/saving/file-state";
|
|
40
45
|
import { store } from "./core/state/jotai";
|
|
41
46
|
import { patchFetch, patchVegaLoader } from "./core/static/files";
|
|
42
47
|
import {
|
|
@@ -150,6 +155,16 @@ const mountOptionsSchema = z.object({
|
|
|
150
155
|
* absolute working directory of the notebook
|
|
151
156
|
*/
|
|
152
157
|
cwd: z.string().nullish().default(null),
|
|
158
|
+
/**
|
|
159
|
+
* LSP workspace information
|
|
160
|
+
*/
|
|
161
|
+
lspWorkspace: z
|
|
162
|
+
.object({
|
|
163
|
+
rootUri: z.string(),
|
|
164
|
+
documentUri: z.string(),
|
|
165
|
+
})
|
|
166
|
+
.nullish()
|
|
167
|
+
.default(null),
|
|
153
168
|
/**
|
|
154
169
|
* notebook code
|
|
155
170
|
*/
|
|
@@ -287,6 +302,7 @@ function initStore(options: unknown) {
|
|
|
287
302
|
// Files
|
|
288
303
|
store.set(filenameAtom, parsedOptions.data.filename);
|
|
289
304
|
store.set(cwdAtom, parsedOptions.data.cwd ?? null);
|
|
305
|
+
store.set(lspWorkspaceAtom, parsedOptions.data.lspWorkspace);
|
|
290
306
|
store.set(codeAtom, parsedOptions.data.code);
|
|
291
307
|
store.set(initialModeAtom, mode);
|
|
292
308
|
|
|
@@ -117,6 +117,14 @@ describe("shouldHandleClickSelection", () => {
|
|
|
117
117
|
expect(shouldHandleClickSelection([linePoint])).toBe(true);
|
|
118
118
|
});
|
|
119
119
|
|
|
120
|
+
it("accepts waterfall clicks", () => {
|
|
121
|
+
const waterfallPoint = createPlotDatum({
|
|
122
|
+
data: { type: "waterfall" },
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(shouldHandleClickSelection([waterfallPoint])).toBe(true);
|
|
126
|
+
});
|
|
127
|
+
|
|
120
128
|
it("rejects non-line scatter marker clicks", () => {
|
|
121
129
|
const markerPoint = createPlotDatum({
|
|
122
130
|
data: { type: "scatter", mode: "markers" },
|
|
@@ -196,4 +204,18 @@ describe("extractPoints", () => {
|
|
|
196
204
|
|
|
197
205
|
expect(extractPoints([point])).toEqual([{ x: 1, y: 2, z: 3 }]);
|
|
198
206
|
});
|
|
207
|
+
|
|
208
|
+
it("returns x/y/pointIndex for waterfall clicks", () => {
|
|
209
|
+
const point = createPlotDatum({
|
|
210
|
+
x: "Revenue",
|
|
211
|
+
y: 400,
|
|
212
|
+
pointIndex: 1,
|
|
213
|
+
curveNumber: 0,
|
|
214
|
+
data: { type: "waterfall" },
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
expect(extractPoints([point])).toEqual([
|
|
218
|
+
{ x: "Revenue", y: 400, pointIndex: 1, curveNumber: 0 },
|
|
219
|
+
]);
|
|
220
|
+
});
|
|
199
221
|
});
|