@marimo-team/frontend 0.23.1-dev8 → 0.23.1
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-BY31ccA7.js → JsonOutput-CavtrueA.js} +1 -1
- package/dist/assets/{MarimoErrorOutput--Yd2Aw0J.js → MarimoErrorOutput-Bmp8DLLo.js} +1 -1
- package/dist/assets/RenderHTML-CM3WMmA8.js +1 -0
- package/dist/assets/{add-connection-dialog-CjvNOKgb.js → add-connection-dialog-BGZvJkor.js} +1 -1
- package/dist/assets/{agent-panel-C24uwabG.js → agent-panel-BvL9Lu9c.js} +1 -1
- package/dist/assets/{cell-editor-zW0u82sK.js → cell-editor-B40o_zx_.js} +1 -1
- package/dist/assets/{chat-display-DsHMZa9F.js → chat-display-M_nvYuHH.js} +1 -1
- package/dist/assets/{chat-panel-o9D3upnX.js → chat-panel-BMOW93uQ.js} +1 -1
- package/dist/assets/{chat-ui-BYS03y86.js → chat-ui-DyeimpVh.js} +1 -1
- package/dist/assets/{column-preview-Dwv5a_zE.js → column-preview-AfcgbFG_.js} +1 -1
- package/dist/assets/{command-palette-BYbKGSF3.js → command-palette-BgvdyU3B.js} +1 -1
- package/dist/assets/{documentation-panel-CA2pWMgB.js → documentation-panel-DUPcsi8P.js} +1 -1
- package/dist/assets/{edit-page-CMUN3ESy.js → edit-page-DD4uEDmX.js} +4 -4
- package/dist/assets/{error-panel-CbqfK1HJ.js → error-panel-DQOeSv5-.js} +1 -1
- package/dist/assets/{file-explorer-panel-CbS8z-JR.js → file-explorer-panel-B67zjs2X.js} +1 -1
- package/dist/assets/{form-DLyXacSF.js → form-BJ6VFU8l.js} +1 -1
- package/dist/assets/{hooks-kZJc1iBf.js → hooks-DvwShzDb.js} +1 -1
- package/dist/assets/index-y6osgSWB.js +42 -0
- package/dist/assets/{layout-tmN-U1zs.js → layout-erv8pLIP.js} +1 -1
- package/dist/assets/{panels-CLfdzLPR.js → panels-1u-RE72f.js} +1 -1
- package/dist/assets/{run-page-DPuH6QY4.js → run-page-DfWH_1mz.js} +1 -1
- package/dist/assets/{scratchpad-panel-BsMm0GQP.js → scratchpad-panel-CnaiXtoJ.js} +1 -1
- package/dist/assets/{session-panel-CTDzGShO.js → session-panel-C68GBFwH.js} +1 -1
- package/dist/assets/{snippets-panel-CWof0wHk.js → snippets-panel-BmIdR0lc.js} +1 -1
- package/dist/assets/state-D1n-olwf.js +3 -0
- package/dist/assets/{useNotebookActions-DHBEqrc_.js → useNotebookActions-Ch1o32Jw.js} +1 -1
- package/dist/index.html +7 -7
- package/package.json +4 -4
- package/src/core/islands/__tests__/bridge.test.ts +2 -12
- package/src/core/islands/__tests__/islands-harness.test.ts +348 -0
- package/src/core/islands/__tests__/parse.test.ts +466 -24
- package/src/core/islands/__tests__/test-utils.tsx +263 -0
- package/src/core/islands/bootstrap.ts +265 -0
- package/src/core/islands/bridge.ts +154 -75
- package/src/core/islands/components/IslandControls.tsx +103 -0
- package/src/core/islands/components/__tests__/IslandControls.test.tsx +185 -0
- package/src/core/islands/components/__tests__/useIslandControls.test.ts +208 -0
- package/src/core/islands/components/output-wrapper.tsx +76 -93
- package/src/core/islands/components/useIslandControls.ts +60 -0
- package/src/core/islands/components/web-components.tsx +168 -40
- package/src/core/islands/constants.ts +28 -0
- package/src/core/islands/main.ts +7 -205
- package/src/core/islands/parse.ts +73 -26
- package/src/core/islands/worker-factory.ts +86 -0
- package/src/plugins/core/RenderHTML.tsx +9 -0
- package/src/plugins/core/__test__/RenderHTML.test.ts +27 -0
- package/src/plugins/core/__test__/trusted-url.test.ts +48 -0
- package/src/plugins/core/registerReactComponent.tsx +11 -8
- package/src/plugins/core/trusted-url.ts +20 -0
- package/src/plugins/impl/ButtonPlugin.tsx +4 -6
- package/src/plugins/impl/CodeEditorPlugin.tsx +15 -18
- package/src/plugins/impl/DataEditorPlugin.tsx +8 -14
- package/src/plugins/impl/DataTablePlugin.tsx +8 -9
- package/src/plugins/impl/FileUploadPlugin.tsx +39 -43
- package/src/plugins/impl/FormPlugin.tsx +2 -6
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +27 -1
- package/src/plugins/impl/anywidget/widget-binding.ts +13 -0
- package/src/plugins/impl/chat/ChatPlugin.tsx +17 -20
- package/src/plugins/impl/data-explorer/DataExplorerPlugin.tsx +5 -8
- package/src/plugins/impl/matplotlib/matplotlib-renderer.ts +38 -14
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +21 -0
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +119 -0
- package/src/plugins/impl/panel/PanelPlugin.tsx +31 -10
- package/src/plugins/impl/panel/__tests__/PanelPlugin.test.ts +60 -0
- package/src/plugins/impl/vega/VegaPlugin.tsx +5 -8
- package/src/plugins/layout/NavigationMenuPlugin.tsx +2 -6
- package/dist/assets/RenderHTML-CbuarQqA.js +0 -1
- package/dist/assets/index-bjxpaV0V.js +0 -42
- package/dist/assets/state-BvnlMKdT.js +0 -3
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { getMarimoVersion } from "../meta/globals";
|
|
4
|
+
import workerUrl from "./worker/worker.tsx?worker&url";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Interface for creating Web Workers for islands
|
|
8
|
+
*/
|
|
9
|
+
export interface WorkerFactory {
|
|
10
|
+
/**
|
|
11
|
+
* Creates a new worker instance
|
|
12
|
+
*/
|
|
13
|
+
create(): Worker;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Configuration for the default worker factory
|
|
18
|
+
*/
|
|
19
|
+
export interface DefaultWorkerFactoryConfig {
|
|
20
|
+
/**
|
|
21
|
+
* The URL to the worker script
|
|
22
|
+
* Defaults to the bundled worker
|
|
23
|
+
*/
|
|
24
|
+
workerUrl?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The name to give the worker (shows in DevTools)
|
|
28
|
+
* Defaults to the marimo version
|
|
29
|
+
*/
|
|
30
|
+
workerName?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Default implementation of WorkerFactory that creates Pyodide workers
|
|
35
|
+
* for islands mode.
|
|
36
|
+
*/
|
|
37
|
+
export class DefaultWorkerFactory implements WorkerFactory {
|
|
38
|
+
private readonly url: string;
|
|
39
|
+
private readonly name: string;
|
|
40
|
+
|
|
41
|
+
constructor(config: DefaultWorkerFactoryConfig = {}) {
|
|
42
|
+
this.url = config.workerUrl || this.getDefaultWorkerUrl();
|
|
43
|
+
this.name = config.workerName || getMarimoVersion();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Creates a new Pyodide worker
|
|
48
|
+
*/
|
|
49
|
+
create(): Worker {
|
|
50
|
+
const js = `import ${JSON.stringify(new URL(this.url, import.meta.url))}`;
|
|
51
|
+
const blob = new Blob([js], { type: "application/javascript" });
|
|
52
|
+
const objURL = URL.createObjectURL(blob);
|
|
53
|
+
|
|
54
|
+
const worker = new Worker(objURL, {
|
|
55
|
+
type: "module",
|
|
56
|
+
/* @vite-ignore */
|
|
57
|
+
name: this.name,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Blob URL can be revoked once the worker has loaded the script
|
|
61
|
+
URL.revokeObjectURL(objURL);
|
|
62
|
+
|
|
63
|
+
return worker;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Gets the default worker URL based on environment
|
|
68
|
+
*/
|
|
69
|
+
private getDefaultWorkerUrl(): string {
|
|
70
|
+
const url = import.meta.env.DEV
|
|
71
|
+
? workerUrl
|
|
72
|
+
: makeRelativeWorkerUrl(workerUrl);
|
|
73
|
+
return url;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Makes worker URLs relative for production builds
|
|
79
|
+
*/
|
|
80
|
+
function makeRelativeWorkerUrl(url: string): string {
|
|
81
|
+
return url.startsWith("./")
|
|
82
|
+
? url
|
|
83
|
+
: url.startsWith("/")
|
|
84
|
+
? `.${url}`
|
|
85
|
+
: `./${url}`;
|
|
86
|
+
}
|
|
@@ -164,11 +164,20 @@ const wrapDocHoverTargets: TransformFn = (
|
|
|
164
164
|
// Wrap elements with data-tooltip attribute in a Tooltip component.
|
|
165
165
|
// This renders the tooltip in a portal (top layer), fixing clipping inside
|
|
166
166
|
// containers with overflow:hidden (e.g. grid cells).
|
|
167
|
+
//
|
|
168
|
+
// Marimo custom elements (marimo-button, etc.) are skipped — they handle
|
|
169
|
+
// tooltips via the plugin system inside their Shadow DOM. Wrapping them here
|
|
170
|
+
// would create a duplicate tooltip with incorrect positioning and
|
|
171
|
+
// un-decoded JSON content (the data-* value is JSON-encoded by the backend).
|
|
167
172
|
const wrapTooltipTargets: TransformFn = (
|
|
168
173
|
reactNode: ReactNode,
|
|
169
174
|
domNode: DOMNode,
|
|
170
175
|
): JSX.Element | undefined => {
|
|
171
176
|
if (domNode instanceof Element && domNode.attribs?.["data-tooltip"]) {
|
|
177
|
+
const tagName = domNode.name?.toLowerCase() ?? "";
|
|
178
|
+
if (tagName.startsWith("marimo-")) {
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
172
181
|
const tooltipContent = domNode.attribs["data-tooltip"];
|
|
173
182
|
return (
|
|
174
183
|
<Tooltip content={tooltipContent}>{reactNode as JSX.Element}</Tooltip>
|
|
@@ -240,6 +240,33 @@ describe("wrapTooltipTargets", () => {
|
|
|
240
240
|
</p>
|
|
241
241
|
`);
|
|
242
242
|
});
|
|
243
|
+
|
|
244
|
+
test("data-tooltip on marimo custom elements is not wrapped", () => {
|
|
245
|
+
const html =
|
|
246
|
+
'<marimo-button data-tooltip=""Run clicky"">click</marimo-button>';
|
|
247
|
+
expect(parseHtml({ html })).toMatchInlineSnapshot(`
|
|
248
|
+
<marimo-button
|
|
249
|
+
data-tooltip=""Run clicky""
|
|
250
|
+
>
|
|
251
|
+
click
|
|
252
|
+
</marimo-button>
|
|
253
|
+
`);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test("data-tooltip on non-marimo custom elements is still wrapped", () => {
|
|
257
|
+
const html = '<my-widget data-tooltip="info">content</my-widget>';
|
|
258
|
+
expect(parseHtml({ html })).toMatchInlineSnapshot(`
|
|
259
|
+
<Tooltip
|
|
260
|
+
content="info"
|
|
261
|
+
>
|
|
262
|
+
<my-widget
|
|
263
|
+
data-tooltip="info"
|
|
264
|
+
>
|
|
265
|
+
content
|
|
266
|
+
</my-widget>
|
|
267
|
+
</Tooltip>
|
|
268
|
+
`);
|
|
269
|
+
});
|
|
243
270
|
});
|
|
244
271
|
|
|
245
272
|
describe("parseHtml with < nad >", () => {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { isTrustedVirtualFileUrl } from "../trusted-url";
|
|
4
|
+
|
|
5
|
+
describe("isTrustedVirtualFileUrl", () => {
|
|
6
|
+
it.each([
|
|
7
|
+
"./@file/123-mpl.js",
|
|
8
|
+
"./@file/456-mpl.css",
|
|
9
|
+
"@file/789-bokeh.js",
|
|
10
|
+
"/@file/0-empty.txt",
|
|
11
|
+
"./@file/1234-name.with.dots.js",
|
|
12
|
+
])("accepts virtual file path %s", (url) => {
|
|
13
|
+
expect(isTrustedVirtualFileUrl(url)).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it.each([
|
|
17
|
+
// Attack vector from the vulnerability report
|
|
18
|
+
"http://127.0.0.1:8820/poc.js",
|
|
19
|
+
"https://evil.example.com/x.js",
|
|
20
|
+
// Protocol-relative → takes attacker's origin
|
|
21
|
+
"//evil.example.com/x.js",
|
|
22
|
+
// Dangerous schemes
|
|
23
|
+
"javascript:alert(1)",
|
|
24
|
+
"data:text/javascript;base64,YWxlcnQoMSk=",
|
|
25
|
+
"file:///etc/passwd",
|
|
26
|
+
"blob:http://127.0.0.1/abc",
|
|
27
|
+
// Almost-but-not virtual file paths
|
|
28
|
+
"./evil.js",
|
|
29
|
+
"../@file/x.js",
|
|
30
|
+
"./malicious/@file/x.js",
|
|
31
|
+
"@file",
|
|
32
|
+
"@files/x.js",
|
|
33
|
+
// Query/fragment smuggling
|
|
34
|
+
"./@file/x.js?redirect=http://evil.com",
|
|
35
|
+
"./@file/x.js#http://evil.com",
|
|
36
|
+
// Empty and non-string
|
|
37
|
+
"",
|
|
38
|
+
])("rejects %s", (url) => {
|
|
39
|
+
expect(isTrustedVirtualFileUrl(url)).toBe(false);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("rejects non-string input", () => {
|
|
43
|
+
expect(isTrustedVirtualFileUrl(null)).toBe(false);
|
|
44
|
+
expect(isTrustedVirtualFileUrl(undefined)).toBe(false);
|
|
45
|
+
expect(isTrustedVirtualFileUrl(42)).toBe(false);
|
|
46
|
+
expect(isTrustedVirtualFileUrl({})).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -23,6 +23,7 @@ import React, {
|
|
|
23
23
|
import ReactDOM, { type Root } from "react-dom/client";
|
|
24
24
|
import useEvent from "react-use-event-hook";
|
|
25
25
|
import { type ZodSchema, z } from "zod";
|
|
26
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
26
27
|
import { notebookAtom } from "@/core/cells/cells.ts";
|
|
27
28
|
import { HTMLCellId } from "@/core/cells/ids.ts";
|
|
28
29
|
import { isUninstantiated } from "@/core/cells/utils";
|
|
@@ -250,14 +251,16 @@ function PluginSlotInternal<T>(
|
|
|
250
251
|
<StyleNamespace>
|
|
251
252
|
<div className={`contents ${theme}`}>
|
|
252
253
|
<Suspense fallback={<div />}>
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
<TooltipProvider>
|
|
255
|
+
{plugin.render({
|
|
256
|
+
setValue: setValueAndSendInput,
|
|
257
|
+
value,
|
|
258
|
+
data: parsedResult.data,
|
|
259
|
+
children: childNodes,
|
|
260
|
+
host: hostElement,
|
|
261
|
+
functions: functionMethods,
|
|
262
|
+
})}
|
|
263
|
+
</TooltipProvider>
|
|
261
264
|
</Suspense>
|
|
262
265
|
</div>
|
|
263
266
|
</StyleNamespace>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Whether a URL can be trusted to point at a marimo-served virtual file.
|
|
5
|
+
*
|
|
6
|
+
* Plugins that load remote scripts or stylesheets (e.g. MplInteractive, Panel)
|
|
7
|
+
* must call this before turning a plugin-supplied URL into a `<script src>` or
|
|
8
|
+
* `<link href>`. The backend always serializes these URLs as virtual file
|
|
9
|
+
* paths of the form `./@file/<byte_length>-<filename>` (see
|
|
10
|
+
* `VirtualFile.create_and_register`). Accepting anything else would let a
|
|
11
|
+
* maliciously crafted `<marimo-*>` element embedded in markdown load
|
|
12
|
+
* attacker-controlled JavaScript at same origin, since the HTML sanitizer
|
|
13
|
+
* lets arbitrary marimo custom elements and attributes through.
|
|
14
|
+
*/
|
|
15
|
+
export function isTrustedVirtualFileUrl(url: unknown): url is string {
|
|
16
|
+
if (typeof url !== "string" || url.length === 0) {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
return /^(\.?\/)?@file\/[^?#]+$/.test(url);
|
|
20
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import type { JSX } from "react";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { KeyboardHotkeys } from "@/components/shortcuts/renderShortcut";
|
|
6
|
-
import { Tooltip
|
|
6
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
7
7
|
import { cn } from "@/utils/cn";
|
|
8
8
|
import { Button } from "../../components/ui/button";
|
|
9
9
|
import { renderHTML } from "../core/RenderHTML";
|
|
@@ -69,11 +69,9 @@ export class ButtonPlugin implements IPlugin<number, Data> {
|
|
|
69
69
|
|
|
70
70
|
if (tooltipContent) {
|
|
71
71
|
return (
|
|
72
|
-
<
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</Tooltip>
|
|
76
|
-
</TooltipProvider>
|
|
72
|
+
<Tooltip content={tooltipContent} delayDuration={200}>
|
|
73
|
+
{button}
|
|
74
|
+
</Tooltip>
|
|
77
75
|
);
|
|
78
76
|
}
|
|
79
77
|
|
|
@@ -4,7 +4,6 @@ import { EditorView } from "@codemirror/view";
|
|
|
4
4
|
import { type JSX, useEffect, useMemo, useState } from "react";
|
|
5
5
|
import useEvent from "react-use-event-hook";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
8
7
|
import { useDebounceControlledState } from "@/hooks/useDebounce";
|
|
9
8
|
import { type Theme, useTheme } from "@/theme/useTheme";
|
|
10
9
|
import type { IPlugin, IPluginProps, Setter } from "../types";
|
|
@@ -98,22 +97,20 @@ const CodeEditorComponent = (props: CodeEditorComponentProps) => {
|
|
|
98
97
|
}, [props.debounce, onBlur]);
|
|
99
98
|
|
|
100
99
|
return (
|
|
101
|
-
<
|
|
102
|
-
<
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
</Labeled>
|
|
117
|
-
</TooltipProvider>
|
|
100
|
+
<Labeled label={props.label} align="top" fullWidth={true}>
|
|
101
|
+
<LazyAnyLanguageCodeMirror
|
|
102
|
+
className={`cm *:outline-hidden border rounded overflow-hidden ${finalTheme}`}
|
|
103
|
+
theme={finalTheme === "dark" ? "dark" : "light"}
|
|
104
|
+
minHeight={minHeight}
|
|
105
|
+
maxHeight={maxHeight}
|
|
106
|
+
placeholder={props.placeholder}
|
|
107
|
+
editable={!props.disabled}
|
|
108
|
+
value={localValue}
|
|
109
|
+
language={props.language}
|
|
110
|
+
onChange={handleChange}
|
|
111
|
+
showCopyButton={props.showCopyButton}
|
|
112
|
+
extensions={extensions}
|
|
113
|
+
/>
|
|
114
|
+
</Labeled>
|
|
118
115
|
);
|
|
119
116
|
};
|
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import glideCss from "@glideapps/glide-data-grid/dist/index.css?inline";
|
|
4
|
-
import { Tooltip } from "radix-ui";
|
|
5
|
-
|
|
6
|
-
const TooltipProvider = Tooltip.Provider;
|
|
7
|
-
|
|
8
4
|
import React, { useState } from "react";
|
|
9
5
|
import { z } from "zod";
|
|
10
6
|
import { inferFieldTypes } from "@/components/data-table/columns";
|
|
@@ -63,16 +59,14 @@ export const DataEditorPlugin = createPlugin<Edits>("marimo-data-editor", {
|
|
|
63
59
|
.withFunctions({})
|
|
64
60
|
.renderer((props) => {
|
|
65
61
|
return (
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
/>
|
|
75
|
-
</TooltipProvider>
|
|
62
|
+
<LoadingDataEditor
|
|
63
|
+
data={props.data.data}
|
|
64
|
+
fieldTypes={props.data.fieldTypes}
|
|
65
|
+
edits={props.value}
|
|
66
|
+
onEdits={props.setValue}
|
|
67
|
+
host={props.host}
|
|
68
|
+
editableColumns={props.data.editableColumns}
|
|
69
|
+
/>
|
|
76
70
|
);
|
|
77
71
|
});
|
|
78
72
|
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
import { Provider as SlotzProvider } from "@marimo-team/react-slotz";
|
|
4
|
-
import { Tooltip } from "radix-ui";
|
|
5
|
-
|
|
6
|
-
const TooltipProvider = Tooltip.Provider;
|
|
7
4
|
|
|
8
5
|
import type {
|
|
9
6
|
ColumnFiltersState,
|
|
@@ -71,6 +68,7 @@ import {
|
|
|
71
68
|
import { slotsController } from "@/core/slots/slots";
|
|
72
69
|
import { store } from "@/core/state/jotai";
|
|
73
70
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
71
|
+
import { isIslands } from "@/core/islands/utils";
|
|
74
72
|
import { isInVscodeExtension } from "@/core/vscode/is-in-vscode";
|
|
75
73
|
import { useAsyncData } from "@/hooks/useAsyncData";
|
|
76
74
|
import { useDeepCompareMemoize } from "@/hooks/useDeepCompareMemoize";
|
|
@@ -1009,6 +1007,7 @@ const DataTableComponent = ({
|
|
|
1009
1007
|
const canShowColumnExplorer = showColumnExplorer && !!preview_column;
|
|
1010
1008
|
|
|
1011
1009
|
const isInVscode = isInVscodeExtension();
|
|
1010
|
+
const isIslandsMode = isIslands();
|
|
1012
1011
|
|
|
1013
1012
|
return (
|
|
1014
1013
|
<>
|
|
@@ -1094,13 +1093,15 @@ const DataTableComponent = ({
|
|
|
1094
1093
|
onCellSelectionChange={handleCellSelectionChange}
|
|
1095
1094
|
getRowIds={get_row_ids}
|
|
1096
1095
|
toggleDisplayHeader={toggleDisplayHeader}
|
|
1097
|
-
showChartBuilder={showChartBuilder}
|
|
1096
|
+
showChartBuilder={showChartBuilder && !isIslandsMode}
|
|
1098
1097
|
isChartBuilderOpen={isChartBuilderOpen}
|
|
1099
1098
|
showPageSizeSelector={showPageSizeSelector}
|
|
1100
|
-
// Hidden in VSCode
|
|
1099
|
+
// Hidden in VSCode and islands because there's no panel to show
|
|
1101
1100
|
// the table explorer.
|
|
1102
1101
|
showTableExplorer={
|
|
1103
|
-
(showRowExplorer || canShowColumnExplorer) &&
|
|
1102
|
+
(showRowExplorer || canShowColumnExplorer) &&
|
|
1103
|
+
!isInVscode &&
|
|
1104
|
+
!isIslandsMode
|
|
1104
1105
|
}
|
|
1105
1106
|
togglePanel={togglePanel}
|
|
1106
1107
|
isPanelOpen={isPanelOpen}
|
|
@@ -1123,9 +1124,7 @@ export const TableProviders: React.FC<{ children: React.ReactNode }> = ({
|
|
|
1123
1124
|
return (
|
|
1124
1125
|
<ErrorBoundary>
|
|
1125
1126
|
<Provider store={store}>
|
|
1126
|
-
<SlotzProvider controller={slotsController}>
|
|
1127
|
-
<TooltipProvider>{children}</TooltipProvider>
|
|
1128
|
-
</SlotzProvider>
|
|
1127
|
+
<SlotzProvider controller={slotsController}>{children}</SlotzProvider>
|
|
1129
1128
|
</Provider>
|
|
1130
1129
|
</ErrorBoundary>
|
|
1131
1130
|
);
|
|
@@ -4,7 +4,7 @@ import { MousePointerSquareDashedIcon, Upload } from "lucide-react";
|
|
|
4
4
|
import type { JSX } from "react";
|
|
5
5
|
import { useDropzone } from "react-dropzone";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import { Tooltip
|
|
7
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
8
8
|
import { toast } from "@/components/ui/use-toast";
|
|
9
9
|
import { cn } from "@/utils/cn";
|
|
10
10
|
import { Logger } from "@/utils/Logger";
|
|
@@ -184,42 +184,40 @@ export const FileUpload = (props: FileUploadProps): JSX.Element => {
|
|
|
184
184
|
// link button to the hidden input element
|
|
185
185
|
const label = props.label ?? "Upload";
|
|
186
186
|
return (
|
|
187
|
-
<
|
|
188
|
-
<
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{value.length} {value.length === 1 ? "file" : "files"}.
|
|
208
|
-
</span>
|
|
187
|
+
<div className="flex flex-row items-center justify-start gap-2">
|
|
188
|
+
<button
|
|
189
|
+
data-testid="marimo-plugin-file-upload-button"
|
|
190
|
+
{...getRootProps({})}
|
|
191
|
+
className={buttonVariants({
|
|
192
|
+
variant: "secondary",
|
|
193
|
+
size: "xs",
|
|
194
|
+
})}
|
|
195
|
+
>
|
|
196
|
+
{renderHTML({ html: label })}
|
|
197
|
+
<Upload size={14} className="ml-2" />
|
|
198
|
+
</button>
|
|
199
|
+
<input {...getInputProps({})} type="file" />
|
|
200
|
+
{uploaded ? (
|
|
201
|
+
<>
|
|
202
|
+
<Tooltip content={uploadedFiles}>
|
|
203
|
+
<span className="text-xs text-muted-foreground">
|
|
204
|
+
Uploaded{" "}
|
|
205
|
+
<span className="underline cursor-pointer">
|
|
206
|
+
{value.length} {value.length === 1 ? "file" : "files"}.
|
|
209
207
|
</span>
|
|
210
|
-
</
|
|
208
|
+
</span>
|
|
209
|
+
</Tooltip>
|
|
211
210
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
</TooltipProvider>
|
|
211
|
+
<button
|
|
212
|
+
className={cn("text-xs text-destructive hover:underline")}
|
|
213
|
+
onClick={() => setValue([])}
|
|
214
|
+
type="button"
|
|
215
|
+
>
|
|
216
|
+
Click to clear files.
|
|
217
|
+
</button>
|
|
218
|
+
</>
|
|
219
|
+
) : null}
|
|
220
|
+
</div>
|
|
223
221
|
);
|
|
224
222
|
}
|
|
225
223
|
|
|
@@ -274,14 +272,12 @@ export const FileUpload = (props: FileUploadProps): JSX.Element => {
|
|
|
274
272
|
{uploaded ? (
|
|
275
273
|
<div className="flex flex-row gap-1">
|
|
276
274
|
<div className="text-xs text-muted-foreground">
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
</Tooltip>
|
|
284
|
-
</TooltipProvider>
|
|
275
|
+
Uploaded{" "}
|
|
276
|
+
<Tooltip content={uploadedFiles}>
|
|
277
|
+
<span className="underline cursor-pointer">
|
|
278
|
+
{value.length} {value.length === 1 ? "file" : "files"}.
|
|
279
|
+
</span>
|
|
280
|
+
</Tooltip>
|
|
285
281
|
</div>
|
|
286
282
|
<span className="text-xs text-destructive hover:underline hover:cursor-pointer">
|
|
287
283
|
<button
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { Loader2Icon } from "lucide-react";
|
|
4
4
|
import { type JSX, useEffect, useRef, useState } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { Tooltip
|
|
6
|
+
import { Tooltip } from "@/components/ui/tooltip";
|
|
7
7
|
import type { UIElementId } from "@/core/cells/ids";
|
|
8
8
|
import {
|
|
9
9
|
MarimoValueInputEvent,
|
|
@@ -72,11 +72,7 @@ export const FormPlugin = createPlugin("marimo-form")
|
|
|
72
72
|
.output(z.string().nullish()),
|
|
73
73
|
})
|
|
74
74
|
.renderer(({ data, functions, ...rest }) => {
|
|
75
|
-
return
|
|
76
|
-
<TooltipProvider>
|
|
77
|
-
<Form {...data} {...rest} {...functions} />
|
|
78
|
-
</TooltipProvider>
|
|
79
|
-
);
|
|
75
|
+
return <Form {...data} {...rest} {...functions} />;
|
|
80
76
|
});
|
|
81
77
|
|
|
82
78
|
export interface FormWrapperProps<T>
|
|
@@ -59,13 +59,39 @@ describe("WidgetDefRegistry", () => {
|
|
|
59
59
|
|
|
60
60
|
it("should remove from cache on import failure so retry creates new promise", async () => {
|
|
61
61
|
const promise1 = registry.getModule("http://localhost/a.js", "fail-hash");
|
|
62
|
-
// The
|
|
62
|
+
// The URL is rejected by the trusted-URL validator.
|
|
63
63
|
await expect(promise1).rejects.toThrow();
|
|
64
64
|
// After failure, cache should be cleared, so next call creates a new promise
|
|
65
65
|
const promise2 = registry.getModule("http://localhost/a.js", "fail-hash");
|
|
66
66
|
expect(promise1).not.toBe(promise2);
|
|
67
67
|
promise2.catch(() => undefined);
|
|
68
68
|
});
|
|
69
|
+
|
|
70
|
+
describe("URL validation", () => {
|
|
71
|
+
it.each([
|
|
72
|
+
// Attack vector: raw <marimo-anywidget data-js-url=...> in markdown
|
|
73
|
+
"http://127.0.0.1:8820/poc.mjs",
|
|
74
|
+
"https://evil.example.com/widget.mjs",
|
|
75
|
+
"//evil.example.com/widget.mjs",
|
|
76
|
+
"javascript:alert(1)",
|
|
77
|
+
"data:text/javascript;base64,YWxlcnQoMSk=",
|
|
78
|
+
"./@file/x.js?redirect=http://evil.com",
|
|
79
|
+
"",
|
|
80
|
+
])("rejects untrusted URL: %s", async (url) => {
|
|
81
|
+
await expect(registry.getModule(url, `hash-${url}`)).rejects.toThrow(
|
|
82
|
+
/untrusted/i,
|
|
83
|
+
);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("accepts virtual file paths (fails later at import time)", async () => {
|
|
87
|
+
// The URL passes validation but the import still fails because this
|
|
88
|
+
// is a Node test environment with no server. We only assert that
|
|
89
|
+
// the rejection reason is NOT the "untrusted URL" refusal.
|
|
90
|
+
await expect(
|
|
91
|
+
registry.getModule("./@file/123-widget.js", "trusted-hash"),
|
|
92
|
+
).rejects.not.toThrow(/untrusted/i);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
69
95
|
});
|
|
70
96
|
|
|
71
97
|
describe("WidgetBinding", () => {
|
|
@@ -5,6 +5,7 @@ import type { AnyWidget, Experimental } from "@anywidget/types";
|
|
|
5
5
|
import { asRemoteURL } from "@/core/runtime/config";
|
|
6
6
|
import { resolveVirtualFileURL } from "@/core/static/files";
|
|
7
7
|
import { isStaticNotebook } from "@/core/static/static-state";
|
|
8
|
+
import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
|
|
8
9
|
import { Logger } from "@/utils/Logger";
|
|
9
10
|
import type { Model } from "./model";
|
|
10
11
|
import type { ModelState, WidgetModelId } from "./types";
|
|
@@ -80,6 +81,18 @@ class WidgetDefRegistry {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
async #doImport(jsUrl: string): Promise<any> {
|
|
84
|
+
// Only trust marimo virtual file paths. Accepting arbitrary URLs
|
|
85
|
+
// would let a raw `<marimo-anywidget data-js-url=...>` element
|
|
86
|
+
// embedded in a markdown cell dynamically import attacker-controlled
|
|
87
|
+
// JavaScript at same origin (the HTML sanitizer allows any marimo-*
|
|
88
|
+
// custom element with any attribute through to the plugin layer).
|
|
89
|
+
if (!isTrustedVirtualFileUrl(jsUrl)) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Refusing to load anywidget module from untrusted URL: ${String(
|
|
92
|
+
jsUrl,
|
|
93
|
+
)}`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
83
96
|
let url = asRemoteURL(jsUrl).toString();
|
|
84
97
|
if (isStaticNotebook()) {
|
|
85
98
|
url = resolveVirtualFileURL(url);
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
import type { UIMessage } from "ai";
|
|
4
4
|
import React, { Suspense } from "react";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
6
|
import { createPlugin } from "@/plugins/core/builder";
|
|
8
7
|
import { rpc } from "@/plugins/core/rpc";
|
|
9
8
|
import { Arrays } from "@/utils/arrays";
|
|
@@ -73,23 +72,21 @@ export const ChatPlugin = createPlugin<{ messages: UIMessage[] }>(
|
|
|
73
72
|
.output(z.unknown()),
|
|
74
73
|
})
|
|
75
74
|
.renderer((props) => (
|
|
76
|
-
<
|
|
77
|
-
<
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
</Suspense>
|
|
94
|
-
</TooltipProvider>
|
|
75
|
+
<Suspense>
|
|
76
|
+
<LazyChatbot
|
|
77
|
+
prompts={props.data.prompts}
|
|
78
|
+
showConfigurationControls={props.data.showConfigurationControls}
|
|
79
|
+
maxHeight={props.data.maxHeight}
|
|
80
|
+
allowAttachments={props.data.allowAttachments}
|
|
81
|
+
disabled={props.data.disabled}
|
|
82
|
+
config={props.data.config}
|
|
83
|
+
get_chat_history={props.functions.get_chat_history}
|
|
84
|
+
delete_chat_history={props.functions.delete_chat_history}
|
|
85
|
+
delete_chat_message={props.functions.delete_chat_message}
|
|
86
|
+
send_prompt={props.functions.send_prompt}
|
|
87
|
+
value={props.value?.messages || Arrays.EMPTY}
|
|
88
|
+
setValue={(messages) => props.setValue({ messages })}
|
|
89
|
+
host={props.host}
|
|
90
|
+
/>
|
|
91
|
+
</Suspense>
|
|
95
92
|
));
|