@marimo-team/islands 0.23.14-dev6 → 0.23.14-dev61
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-Du3_nUzI.js → ConnectedDataExplorerComponent-CU4fZJzG.js} +4 -4
- package/dist/assets/__vite-browser-external-BQCLNwri.js +1 -0
- package/dist/assets/{worker-DEDLIQQV.js → worker-DAWRHcPq.js} +2 -2
- package/dist/{chat-ui-BZxLHwyD.js → chat-ui-DinOvbWu.js} +3248 -3044
- package/dist/{click-outside-container-BDd67_1U.js → click-outside-container-BLPjMamz.js} +141 -113
- package/dist/{code-visibility-C90Am97q.js → code-visibility-EvXXzjfW.js} +1169 -960
- package/dist/data-grid-overlay-editor-C6lxUJp-.js +136 -0
- package/dist/{dist-D_bzzWBm.js → dist-Bf9f8MuT.js} +3 -3
- package/dist/{formats-d6MhLuQ9.js → formats-Dzx4J_z1.js} +1 -1
- package/dist/{glide-data-editor-DkzAInWG.js → glide-data-editor-CjTu7ukN.js} +2084 -1889
- package/dist/{html-to-image-CGp_08St.js → html-to-image-_wGfk8V-.js} +2389 -2322
- package/dist/{input-CbEz_aj_.js → input-DtsN7xm-.js} +1 -1
- package/dist/main.js +3630 -1801
- package/dist/{mermaid-CJW9vIyO.js → mermaid-BYqXy_NE.js} +2 -2
- package/dist/{number-overlay-editor-D-a0qCT8.js → number-overlay-editor-BLJXvX9c.js} +1 -1
- package/dist/{process-output-R6JsYrv3.js → process-output-Mh4UrjwM.js} +1 -1
- package/dist/{reveal-component-C_u9FY4_.js → reveal-component-CKfV5wlk.js} +600 -596
- package/dist/{spec-Bv-XlYiv.js → spec-Cz-Bj1JI.js} +1 -1
- package/dist/style.css +1 -1
- package/dist/{toDate-D-l5s8nn.js → toDate-CWNNlFEX.js} +15 -7
- package/dist/{useAsyncData-1Dhzjfwf.js → useAsyncData-KfHB8wQR.js} +1 -1
- package/dist/{useDeepCompareMemoize-CDWT3BDz.js → useDeepCompareMemoize-nJMtxhm4.js} +1 -1
- package/dist/{useLifecycle-AHlswLw-.js → useLifecycle-DegSo0lV.js} +1 -1
- package/dist/{useTheme-BrYvK-_A.js → useTheme-6eZ3GOTS.js} +31 -26
- package/dist/{vega-component-Pk6lyc_a.js → vega-component-DzyyM9fc.js} +5 -5
- package/package.json +4 -4
- package/src/__mocks__/requests.ts +1 -0
- package/src/__tests__/CellStatus.test.tsx +1 -2
- package/src/components/app-config/user-config-form.tsx +52 -0
- package/src/components/chat/acp/agent-panel.tsx +35 -1
- package/src/components/chat/chat-panel.tsx +68 -29
- package/src/components/data-table/__tests__/column-explorer.test.tsx +25 -0
- package/src/components/data-table/__tests__/column-visibility-dropdown.test.tsx +60 -3
- package/src/components/data-table/charts/__tests__/altair-generator.test.ts +24 -0
- package/src/components/data-table/charts/__tests__/merge-index-fields.test.ts +33 -0
- package/src/components/data-table/charts/chart-spec/altair-generator.ts +4 -1
- package/src/components/data-table/charts/charts.tsx +23 -1
- package/src/components/data-table/column-explorer-panel/column-explorer.tsx +33 -12
- package/src/components/data-table/column-visibility-dropdown.tsx +15 -0
- package/src/components/debugger/debugger-code.tsx +7 -2
- package/src/components/editor/actions/useNotebookActions.tsx +2 -2
- package/src/components/editor/cell/CellStatus.tsx +1 -20
- package/src/components/editor/cell/PendingDeleteConfirmation.tsx +1 -1
- package/src/components/editor/cell/cell-context-menu.tsx +2 -2
- package/src/components/editor/chrome/panels/snippets-panel.tsx +3 -3
- package/src/components/editor/chrome/wrapper/__tests__/useOpenAiAssistant.test.ts +36 -0
- package/src/components/editor/chrome/wrapper/footer-items/pyodide-status.tsx +6 -36
- package/src/components/editor/chrome/wrapper/useAiPanel.ts +3 -1
- package/src/components/editor/chrome/wrapper/useOpenAiAssistant.ts +88 -0
- package/src/components/editor/code/__tests__/readonly-python-code.test.tsx +79 -0
- package/src/components/editor/code/readonly-python-code.tsx +60 -33
- package/src/components/editor/errors/__tests__/auto-fix.test.ts +23 -0
- package/src/components/editor/errors/auto-fix.tsx +88 -34
- package/src/components/editor/errors/fix-mode.ts +1 -1
- package/src/components/editor/notebook-cell.tsx +7 -0
- package/src/components/editor/output/ImageOutput.tsx +20 -6
- package/src/components/editor/output/MarimoTracebackOutput.tsx +28 -2
- package/src/components/editor/output/__tests__/ImageOutput.test.tsx +53 -0
- package/src/components/editor/output/__tests__/traceback.test.tsx +14 -6
- package/src/components/editor/renderers/grid-layout/grid-layout.tsx +7 -2
- package/src/components/editor/renderers/vertical-layout/vertical-layout.tsx +7 -3
- package/src/components/lifecycle/ProgressiveBoundary.tsx +42 -0
- package/src/components/lifecycle/RuntimeStatusBadge.tsx +59 -0
- package/src/components/lifecycle/__tests__/ProgressiveBoundary.test.tsx +147 -0
- package/src/components/lifecycle/__tests__/RuntimeStatusBadge.test.tsx +72 -0
- package/src/components/slides/__tests__/slide.test.tsx +33 -0
- package/src/components/slides/minimap.tsx +7 -1
- package/src/components/slides/reveal-component.tsx +3 -0
- package/src/components/slides/slide-cell-view.tsx +10 -6
- package/src/components/slides/slide.tsx +16 -13
- package/src/components/tracing/tracing.tsx +2 -1
- package/src/core/ai/config.ts +2 -2
- package/src/core/ai/context/__tests__/registry.test.ts +133 -3
- package/src/core/ai/context/providers/__tests__/datasource.test.ts +3 -1
- package/src/core/ai/context/providers/__tests__/error.test.ts +200 -15
- package/src/core/ai/context/providers/datasource.ts +27 -2
- package/src/core/ai/context/providers/error.ts +226 -36
- package/src/core/ai/context/registry.ts +77 -43
- package/src/core/ai/state.ts +11 -0
- package/src/core/cells/__tests__/cell.test.ts +39 -0
- package/src/core/cells/__tests__/readonly-code-display.test.ts +46 -0
- package/src/core/cells/cell.ts +5 -3
- package/src/core/cells/readonly-code-display.ts +35 -0
- package/src/core/codemirror/__tests__/setup.test.ts +33 -1
- package/src/core/codemirror/ai/resources.ts +15 -10
- package/src/core/codemirror/cells/__tests__/debugger-decorations.test.ts +185 -0
- package/src/core/codemirror/cells/__tests__/debugger-state.test.ts +224 -0
- package/src/core/codemirror/cells/__tests__/line-timing-decorations.test.ts +126 -0
- package/src/core/codemirror/cells/debugger-decorations.ts +188 -0
- package/src/core/codemirror/cells/debugger-state.ts +133 -0
- package/src/core/codemirror/cells/extensions.ts +32 -4
- package/src/core/codemirror/cells/line-timing-decorations.ts +165 -0
- package/src/core/codemirror/go-to-definition/__tests__/utils.test.ts +75 -3
- package/src/core/codemirror/go-to-definition/extension.ts +3 -3
- package/src/core/codemirror/go-to-definition/underline.ts +5 -14
- package/src/core/codemirror/go-to-definition/utils.ts +49 -1
- package/src/core/codemirror/utils.ts +15 -0
- package/src/core/config/__tests__/config-schema.test.ts +17 -0
- package/src/core/config/config-schema.ts +1 -0
- package/src/core/config/config.ts +10 -0
- package/src/core/config/feature-flag.tsx +4 -0
- package/src/core/edit-app.tsx +8 -5
- package/src/core/errors/errors.ts +2 -1
- package/src/core/islands/bootstrap.ts +4 -3
- package/src/core/islands/bridge.ts +1 -0
- package/src/core/lifecycle/__tests__/render-policy.test.ts +247 -0
- package/src/core/lifecycle/render-policy.ts +125 -0
- package/src/core/mime.ts +11 -4
- package/src/core/network/__tests__/api.test.ts +17 -0
- package/src/core/network/__tests__/requests-toasting.test.tsx +46 -0
- package/src/core/network/api.ts +17 -6
- package/src/core/network/requests-lazy.ts +1 -0
- package/src/core/network/requests-network.ts +8 -0
- package/src/core/network/requests-static.ts +1 -0
- package/src/core/network/requests-toasting.tsx +10 -1
- package/src/core/network/types.ts +2 -0
- package/src/core/run-app.tsx +27 -23
- package/src/core/runtime/__tests__/adapter.test.ts +160 -0
- package/src/core/runtime/__tests__/runtime.test.ts +45 -0
- package/src/core/runtime/adapter.ts +182 -0
- package/src/core/runtime/runtime.ts +30 -16
- package/src/core/wasm/PyodideLoader.tsx +20 -62
- package/src/core/wasm/bridge.ts +13 -4
- package/src/core/wasm/state.ts +8 -19
- package/src/core/websocket/__tests__/useMarimoKernelConnection.test.ts +4 -0
- package/src/core/websocket/__tests__/useWebSocket.test.ts +49 -0
- package/src/core/websocket/transports/__tests__/sse.test.ts +338 -0
- package/src/core/websocket/transports/sse.ts +308 -0
- package/src/core/websocket/useMarimoKernelConnection.tsx +58 -7
- package/src/core/websocket/useWebSocket.tsx +12 -2
- package/src/css/app/codemirror.css +14 -0
- package/src/hooks/__tests__/useDelayElapsed.test.tsx +55 -0
- package/src/hooks/useDelayElapsed.ts +27 -0
- package/src/mount.tsx +3 -3
- package/src/plugins/core/RenderHTML.tsx +35 -1
- package/src/plugins/core/__test__/RenderHTML.test.ts +44 -0
- package/src/plugins/impl/DataTablePlugin.tsx +1 -0
- package/src/plugins/impl/anywidget/AnyWidgetPlugin.tsx +33 -254
- package/src/plugins/impl/anywidget/__tests__/AnyWidgetPlugin.test.tsx +116 -174
- package/src/plugins/impl/anywidget/__tests__/host.test.ts +313 -0
- package/src/plugins/impl/anywidget/__tests__/model-proxy.test.ts +158 -0
- package/src/plugins/impl/anywidget/__tests__/model.test.ts +8 -177
- package/src/plugins/impl/anywidget/__tests__/registry.test.ts +708 -0
- package/src/plugins/impl/anywidget/__tests__/resolve-widget.test.ts +132 -0
- package/src/plugins/impl/anywidget/__tests__/widget-binding.test.ts +305 -133
- package/src/plugins/impl/anywidget/__tests__/widget-ref.test.ts +28 -0
- package/src/plugins/impl/anywidget/host.ts +54 -0
- package/src/plugins/impl/anywidget/model-proxy.ts +70 -0
- package/src/plugins/impl/anywidget/model.ts +59 -239
- package/src/plugins/impl/anywidget/registry.ts +268 -0
- package/src/plugins/impl/anywidget/resolve-widget.ts +138 -0
- package/src/plugins/impl/anywidget/runtime.ts +422 -0
- package/src/plugins/impl/anywidget/types.ts +14 -0
- package/src/plugins/impl/anywidget/widget-binding.ts +280 -119
- package/src/plugins/impl/anywidget/widget-ref.ts +29 -0
- package/src/plugins/impl/data-editor/__tests__/glide-data-editor.test.tsx +187 -0
- package/src/plugins/impl/data-editor/glide-data-editor.tsx +6 -0
- package/src/plugins/impl/data-editor/glide-portal.tsx +73 -0
- package/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx +3 -2
- package/src/plugins/impl/mpl-interactive/__tests__/MplInteractivePlugin.test.tsx +16 -13
- package/src/plugins/impl/mpl-interactive/mpl-websocket-shim.ts +1 -1
- package/src/utils/errors.ts +15 -0
- package/src/utils/time.ts +20 -0
- package/dist/assets/__vite-browser-external-DAm_YW43.js +0 -1
- package/dist/data-grid-overlay-editor-mfEJ5475.js +0 -128
- package/src/components/editor/chrome/panels/__tests__/snippet-display.test.ts +0 -22
- package/src/components/editor/chrome/panels/snippet-display.ts +0 -27
- package/src/core/ai/context/providers/__tests__/__snapshots__/error.test.ts.snap +0 -3
- package/src/core/wasm/__tests__/PyodideLoader.test.ts +0 -72
- package/src/core/wasm/__tests__/state.test.ts +0 -124
- package/src/plugins/impl/anywidget/schemas.ts +0 -32
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import React from "react";
|
|
4
|
+
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
5
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { TooltipProvider } from "@/components/ui/tooltip";
|
|
7
|
+
import { GlideDataEditor } from "../glide-data-editor";
|
|
8
|
+
|
|
9
|
+
const capturedPortalRef = vi.hoisted(() => ({
|
|
10
|
+
ref: undefined as React.RefObject<HTMLElement> | undefined,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
vi.mock("@glideapps/glide-data-grid", async () => {
|
|
14
|
+
const React = await import("react");
|
|
15
|
+
return {
|
|
16
|
+
default: React.forwardRef(function MockDataEditor(
|
|
17
|
+
props: { portalElementRef?: React.RefObject<HTMLElement> },
|
|
18
|
+
_ref: React.Ref<HTMLDivElement>,
|
|
19
|
+
) {
|
|
20
|
+
capturedPortalRef.ref = props.portalElementRef;
|
|
21
|
+
return <div data-testid="mock-data-editor" />;
|
|
22
|
+
}),
|
|
23
|
+
CompactSelection: {
|
|
24
|
+
empty: () => ({}),
|
|
25
|
+
},
|
|
26
|
+
GridCellKind: {
|
|
27
|
+
Text: "text",
|
|
28
|
+
Number: "number",
|
|
29
|
+
Boolean: "boolean",
|
|
30
|
+
},
|
|
31
|
+
GridColumnIcon: {
|
|
32
|
+
ProtectedColumnOverlay: "protected",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
vi.mock("@/theme/useTheme", () => ({
|
|
38
|
+
useTheme: () => ({ theme: "light" }),
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const editorProps = {
|
|
42
|
+
data: [{ name: "alice" }],
|
|
43
|
+
setData: vi.fn(),
|
|
44
|
+
columnFields: new Map([["name", "string"]]) as Map<string, "string">,
|
|
45
|
+
setColumnFields: vi.fn(),
|
|
46
|
+
editableColumns: "all" as const,
|
|
47
|
+
edits: [],
|
|
48
|
+
onAddEdits: vi.fn(),
|
|
49
|
+
onAddRows: vi.fn(),
|
|
50
|
+
onDeleteRows: vi.fn(),
|
|
51
|
+
onRenameColumn: vi.fn(),
|
|
52
|
+
onDeleteColumn: vi.fn(),
|
|
53
|
+
onAddColumn: vi.fn(),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
describe("GlideDataEditor portal", () => {
|
|
57
|
+
let fullscreenElement: Element | null;
|
|
58
|
+
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
fullscreenElement = null;
|
|
61
|
+
Object.defineProperty(document, "fullscreenElement", {
|
|
62
|
+
get: () => fullscreenElement,
|
|
63
|
+
configurable: true,
|
|
64
|
+
});
|
|
65
|
+
document.body
|
|
66
|
+
.querySelectorAll("[data-testid='glide-data-editor-portal']")
|
|
67
|
+
.forEach((node) => {
|
|
68
|
+
node.remove();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("renders a body-level portal and passes it to DataEditor", async () => {
|
|
73
|
+
const { container } = render(
|
|
74
|
+
<TooltipProvider>
|
|
75
|
+
<GlideDataEditor {...editorProps} />
|
|
76
|
+
</TooltipProvider>,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const portal = screen.getByTestId("glide-data-editor-portal");
|
|
80
|
+
expect(portal.parentElement).toBe(document.body);
|
|
81
|
+
expect(container.contains(portal)).toBe(false);
|
|
82
|
+
expect(capturedPortalRef.ref).toBeDefined();
|
|
83
|
+
await waitFor(() => {
|
|
84
|
+
expect(capturedPortalRef.ref?.current).toBe(portal);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("mounts into the fullscreen element when fullscreen is already active", () => {
|
|
89
|
+
const fullscreenContainer = document.createElement("div");
|
|
90
|
+
document.body.appendChild(fullscreenContainer);
|
|
91
|
+
fullscreenElement = fullscreenContainer;
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<TooltipProvider>
|
|
95
|
+
<GlideDataEditor {...editorProps} />
|
|
96
|
+
</TooltipProvider>,
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const portal = fullscreenContainer.querySelector(
|
|
100
|
+
"[data-testid='glide-data-editor-portal']",
|
|
101
|
+
);
|
|
102
|
+
expect(portal).not.toBeNull();
|
|
103
|
+
expect(portal?.parentElement).toBe(fullscreenContainer);
|
|
104
|
+
|
|
105
|
+
fullscreenContainer.remove();
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("moves the portal into the fullscreen element while fullscreen is active", async () => {
|
|
109
|
+
render(
|
|
110
|
+
<TooltipProvider>
|
|
111
|
+
<GlideDataEditor {...editorProps} />
|
|
112
|
+
</TooltipProvider>,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(
|
|
116
|
+
document.body.querySelector("[data-testid='glide-data-editor-portal']"),
|
|
117
|
+
).not.toBeNull();
|
|
118
|
+
|
|
119
|
+
const fullscreenContainer = document.createElement("div");
|
|
120
|
+
document.body.appendChild(fullscreenContainer);
|
|
121
|
+
|
|
122
|
+
act(() => {
|
|
123
|
+
fullscreenElement = fullscreenContainer;
|
|
124
|
+
document.dispatchEvent(new Event("fullscreenchange"));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await waitFor(() => {
|
|
128
|
+
const portal = fullscreenContainer.querySelector(
|
|
129
|
+
"[data-testid='glide-data-editor-portal']",
|
|
130
|
+
);
|
|
131
|
+
expect(portal?.parentElement).toBe(fullscreenContainer);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
act(() => {
|
|
135
|
+
fullscreenElement = null;
|
|
136
|
+
document.dispatchEvent(new Event("fullscreenchange"));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await waitFor(() => {
|
|
140
|
+
const portal = document.body.querySelector(
|
|
141
|
+
"[data-testid='glide-data-editor-portal']",
|
|
142
|
+
);
|
|
143
|
+
expect(portal?.parentElement).toBe(document.body);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
fullscreenContainer.remove();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("unmounts cleanly while fullscreen is active", async () => {
|
|
150
|
+
const fullscreenContainer = document.createElement("div");
|
|
151
|
+
document.body.appendChild(fullscreenContainer);
|
|
152
|
+
|
|
153
|
+
const { unmount } = render(
|
|
154
|
+
<TooltipProvider>
|
|
155
|
+
<GlideDataEditor {...editorProps} />
|
|
156
|
+
</TooltipProvider>,
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
act(() => {
|
|
160
|
+
fullscreenElement = fullscreenContainer;
|
|
161
|
+
document.dispatchEvent(new Event("fullscreenchange"));
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
await waitFor(() => {
|
|
165
|
+
expect(
|
|
166
|
+
fullscreenContainer.querySelector(
|
|
167
|
+
"[data-testid='glide-data-editor-portal']",
|
|
168
|
+
),
|
|
169
|
+
).not.toBeNull();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
expect(() => {
|
|
173
|
+
act(() => unmount());
|
|
174
|
+
}).not.toThrow();
|
|
175
|
+
|
|
176
|
+
expect(
|
|
177
|
+
fullscreenContainer.querySelector(
|
|
178
|
+
"[data-testid='glide-data-editor-portal']",
|
|
179
|
+
),
|
|
180
|
+
).toBeNull();
|
|
181
|
+
expect(
|
|
182
|
+
document.body.querySelector("[data-testid='glide-data-editor-portal']"),
|
|
183
|
+
).toBeNull();
|
|
184
|
+
|
|
185
|
+
fullscreenContainer.remove();
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
removeColumn,
|
|
57
57
|
renameColumn,
|
|
58
58
|
} from "./data-utils";
|
|
59
|
+
import { GlideDataEditorPortal } from "./glide-portal";
|
|
59
60
|
|
|
60
61
|
interface GlideDataEditorProps<T> {
|
|
61
62
|
data: T[];
|
|
@@ -88,6 +89,7 @@ export const GlideDataEditor = <T,>({
|
|
|
88
89
|
}: GlideDataEditorProps<T>) => {
|
|
89
90
|
const { theme } = useTheme();
|
|
90
91
|
const dataEditorRef = useRef<DataEditorRef>(null);
|
|
92
|
+
const portalElementRef = useRef<HTMLDivElement>(null);
|
|
91
93
|
|
|
92
94
|
const [menu, setMenu] = useState<{ col: number; bounds: Rectangle }>();
|
|
93
95
|
const [showSearch, setShowSearch] = useState<boolean>(false);
|
|
@@ -613,9 +615,13 @@ export const GlideDataEditor = <T,>({
|
|
|
613
615
|
|
|
614
616
|
return (
|
|
615
617
|
<div className="relative w-full min-w-0">
|
|
618
|
+
<GlideDataEditorPortal portalRef={portalElementRef} />
|
|
616
619
|
<ErrorBoundary>
|
|
617
620
|
<DataEditor
|
|
618
621
|
ref={dataEditorRef}
|
|
622
|
+
// Glide types portalElementRef as RefObject<HTMLElement> (non-null); React 19 refs include null.
|
|
623
|
+
// @ts-expect-error glide-data-grid stale RefObject typing
|
|
624
|
+
portalElementRef={portalElementRef}
|
|
619
625
|
getCellContent={getCellContent}
|
|
620
626
|
columns={columns}
|
|
621
627
|
gridSelection={selection}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import React, { useEffect, useState } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dismiss any open Glide Data Grid overlay by firing Escape on it.
|
|
8
|
+
*/
|
|
9
|
+
export function dismissGlideOverlay() {
|
|
10
|
+
const overlay = document.querySelector(
|
|
11
|
+
"[data-testid='glide-data-editor-portal'] .gdg-clip-region",
|
|
12
|
+
);
|
|
13
|
+
if (overlay) {
|
|
14
|
+
overlay.dispatchEvent(
|
|
15
|
+
new KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function getGlidePortalContainer(): Element {
|
|
21
|
+
return document.fullscreenElement ?? document.body;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function useGlidePortalContainer(): Element {
|
|
25
|
+
const [container, setContainer] = useState<Element>(getGlidePortalContainer);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
const handleFullscreenChange = () => {
|
|
29
|
+
if (document.fullscreenElement) {
|
|
30
|
+
setContainer(document.fullscreenElement);
|
|
31
|
+
} else {
|
|
32
|
+
dismissGlideOverlay();
|
|
33
|
+
setContainer(document.body);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
document.addEventListener("fullscreenchange", handleFullscreenChange);
|
|
38
|
+
// Sync on mount in case fullscreen became active before this editor mounted.
|
|
39
|
+
handleFullscreenChange();
|
|
40
|
+
|
|
41
|
+
return () => {
|
|
42
|
+
document.removeEventListener("fullscreenchange", handleFullscreenChange);
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
return container;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Per-instance Glide Data Grid overlay portal on document.body.
|
|
51
|
+
* Moves into the active fullscreen element via createPortal so edit overlays stay visible
|
|
52
|
+
*/
|
|
53
|
+
export function GlideDataEditorPortal({
|
|
54
|
+
portalRef,
|
|
55
|
+
}: {
|
|
56
|
+
portalRef: React.RefObject<HTMLDivElement | null>;
|
|
57
|
+
}) {
|
|
58
|
+
const container = useGlidePortalContainer();
|
|
59
|
+
|
|
60
|
+
return createPortal(
|
|
61
|
+
<div
|
|
62
|
+
ref={portalRef}
|
|
63
|
+
data-testid="glide-data-editor-portal"
|
|
64
|
+
style={{
|
|
65
|
+
position: "fixed",
|
|
66
|
+
left: 0,
|
|
67
|
+
top: 0,
|
|
68
|
+
zIndex: 9999,
|
|
69
|
+
}}
|
|
70
|
+
/>,
|
|
71
|
+
container,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -6,7 +6,8 @@ import { z } from "zod";
|
|
|
6
6
|
import { useEventListener } from "@/hooks/useEventListener";
|
|
7
7
|
import { createPlugin } from "@/plugins/core/builder";
|
|
8
8
|
import { isTrustedVirtualFileUrl } from "@/plugins/core/trusted-url";
|
|
9
|
-
import {
|
|
9
|
+
import type { Model } from "@/plugins/impl/anywidget/model";
|
|
10
|
+
import { WIDGET_REGISTRY } from "@/plugins/impl/anywidget/registry";
|
|
10
11
|
import type { ModelState, WidgetModelId } from "@/plugins/impl/anywidget/types";
|
|
11
12
|
import type { IPluginProps } from "@/plugins/types";
|
|
12
13
|
import { downloadBlob } from "@/utils/download";
|
|
@@ -204,7 +205,7 @@ const MplInteractiveSlot = (props: IPluginProps<ModelIdRef, Data>) => {
|
|
|
204
205
|
|
|
205
206
|
let model: Model<ModelState>;
|
|
206
207
|
try {
|
|
207
|
-
model = await
|
|
208
|
+
model = await WIDGET_REGISTRY.getModel(id);
|
|
208
209
|
} catch {
|
|
209
210
|
Logger.error("Failed to get model for mpl interactive", id);
|
|
210
211
|
return;
|
|
@@ -11,8 +11,9 @@ import { parseUserConfig } from "@/core/config/config-schema";
|
|
|
11
11
|
import { initialModeAtom } from "@/core/mode";
|
|
12
12
|
import { store } from "@/core/state/jotai";
|
|
13
13
|
import { Logger } from "@/utils/Logger";
|
|
14
|
-
import {
|
|
15
|
-
import
|
|
14
|
+
import { Model } from "@/plugins/impl/anywidget/model";
|
|
15
|
+
import { WIDGET_REGISTRY } from "@/plugins/impl/anywidget/registry";
|
|
16
|
+
import type { ModelState, WidgetModelId } from "@/plugins/impl/anywidget/types";
|
|
16
17
|
import { visibleForTesting } from "../MplInteractivePlugin";
|
|
17
18
|
|
|
18
19
|
const { ensureMplJs, injectCss, MplInteractiveSlot, resetMplJsLoading } =
|
|
@@ -118,7 +119,9 @@ describe("MplInteractivePlugin URL validation", () => {
|
|
|
118
119
|
describe("injectCss", () => {
|
|
119
120
|
it("refuses to append <link> for the PoC attack CSS URL", () => {
|
|
120
121
|
const container = document.createElement("div");
|
|
121
|
-
const loggerSpy = vi
|
|
122
|
+
const loggerSpy = vi
|
|
123
|
+
.spyOn(Logger, "error")
|
|
124
|
+
.mockImplementation(() => undefined);
|
|
122
125
|
|
|
123
126
|
const cleanup = injectCss(container, "http://127.0.0.1:8820/x.css");
|
|
124
127
|
|
|
@@ -136,7 +139,7 @@ describe("MplInteractivePlugin URL validation", () => {
|
|
|
136
139
|
"data:text/css,body{background:red}",
|
|
137
140
|
])("refuses to append <link> for %s", (url) => {
|
|
138
141
|
const container = document.createElement("div");
|
|
139
|
-
vi.spyOn(Logger, "error").mockImplementation(() =>
|
|
142
|
+
vi.spyOn(Logger, "error").mockImplementation(() => undefined);
|
|
140
143
|
|
|
141
144
|
injectCss(container, url);
|
|
142
145
|
|
|
@@ -194,8 +197,8 @@ function installMplFigureMock(): ReturnType<typeof vi.fn> {
|
|
|
194
197
|
return ctor;
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
function makeModel(): Model<
|
|
198
|
-
return new Model(
|
|
200
|
+
function makeModel(): Model<ModelState> {
|
|
201
|
+
return new Model<ModelState>(
|
|
199
202
|
{},
|
|
200
203
|
{
|
|
201
204
|
sendUpdate: vi.fn().mockResolvedValue(undefined),
|
|
@@ -222,7 +225,7 @@ function makeProps(modelId: WidgetModelId) {
|
|
|
222
225
|
|
|
223
226
|
describe("MplInteractiveSlot rerun rebinding", () => {
|
|
224
227
|
beforeEach(() => {
|
|
225
|
-
vi.spyOn(Logger, "error").mockImplementation(() =>
|
|
228
|
+
vi.spyOn(Logger, "error").mockImplementation(() => undefined);
|
|
226
229
|
resetMplJsLoading();
|
|
227
230
|
});
|
|
228
231
|
|
|
@@ -235,8 +238,8 @@ describe("MplInteractiveSlot rerun rebinding", () => {
|
|
|
235
238
|
const ctor = installMplFigureMock();
|
|
236
239
|
const idA = asModelId("model-a");
|
|
237
240
|
const idB = asModelId("model-b");
|
|
238
|
-
|
|
239
|
-
|
|
241
|
+
WIDGET_REGISTRY.setModel(idA, makeModel());
|
|
242
|
+
WIDGET_REGISTRY.setModel(idB, makeModel());
|
|
240
243
|
|
|
241
244
|
const { container, rerender } = render(
|
|
242
245
|
<MplInteractiveSlot {...makeProps(idA)} />,
|
|
@@ -267,7 +270,7 @@ describe("MplInteractiveSlot rerun rebinding", () => {
|
|
|
267
270
|
|
|
268
271
|
it("detaches the previous model's listener on each rerun (no buildup)", async () => {
|
|
269
272
|
installMplFigureMock();
|
|
270
|
-
// Unique ids:
|
|
273
|
+
// Unique ids: WIDGET_REGISTRY is a module singleton whose deferreds resolve
|
|
271
274
|
// once, so reusing ids from another test would return that test's models.
|
|
272
275
|
const idA = asModelId("leak-a");
|
|
273
276
|
const idB = asModelId("leak-b");
|
|
@@ -275,9 +278,9 @@ describe("MplInteractiveSlot rerun rebinding", () => {
|
|
|
275
278
|
const modelA = makeModel();
|
|
276
279
|
const modelB = makeModel();
|
|
277
280
|
const modelC = makeModel();
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
+
WIDGET_REGISTRY.setModel(idA, modelA);
|
|
282
|
+
WIDGET_REGISTRY.setModel(idB, modelB);
|
|
283
|
+
WIDGET_REGISTRY.setModel(idC, modelC);
|
|
281
284
|
|
|
282
285
|
const onA = vi.spyOn(modelA, "on");
|
|
283
286
|
const offA = vi.spyOn(modelA, "off");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Fake WebSocket that routes messages through MarimoComm /
|
|
4
|
+
* Fake WebSocket that routes messages through MarimoComm / WIDGET_REGISTRY
|
|
5
5
|
* instead of a real network WebSocket.
|
|
6
6
|
*
|
|
7
7
|
* mpl.js expects a WebSocket-like object with:
|
package/src/utils/errors.ts
CHANGED
|
@@ -62,6 +62,21 @@ function safeJSONParse(message: string): unknown {
|
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
/**
|
|
66
|
+
* A failed HTTP response. Carries the status code so callers can branch on it
|
|
67
|
+
* (e.g. treat a capability 403 as a benign viewer state). The response body is
|
|
68
|
+
* kept as `cause` so `prettyError` can surface its `detail`.
|
|
69
|
+
*/
|
|
70
|
+
export class HTTPError extends Error {
|
|
71
|
+
readonly status: number;
|
|
72
|
+
|
|
73
|
+
constructor(status: number, statusText: string, body?: unknown) {
|
|
74
|
+
super(statusText, { cause: body });
|
|
75
|
+
this.name = "HTTPError";
|
|
76
|
+
this.status = status;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
65
80
|
export class CellNotInitializedError extends Error {
|
|
66
81
|
constructor(
|
|
67
82
|
message = "The cell containing this UI element has not been run yet. Please run the cell first.",
|
package/src/utils/time.ts
CHANGED
|
@@ -43,3 +43,23 @@ export class Time {
|
|
|
43
43
|
return (this.ms / 1000) as Seconds;
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/** Format a duration in milliseconds, e.g. `500ms`, `1.50s`, `1m30s`. */
|
|
48
|
+
export function formatElapsedTime(elapsedTime: number | null): string {
|
|
49
|
+
if (elapsedTime === null) {
|
|
50
|
+
return "";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const milliseconds = elapsedTime;
|
|
54
|
+
const seconds = milliseconds / 1000;
|
|
55
|
+
|
|
56
|
+
if (seconds >= 60) {
|
|
57
|
+
const minutes = Math.floor(seconds / 60);
|
|
58
|
+
const remainingSeconds = Math.floor(seconds % 60);
|
|
59
|
+
return `${minutes}m${remainingSeconds}s`;
|
|
60
|
+
}
|
|
61
|
+
if (seconds >= 1) {
|
|
62
|
+
return `${seconds.toFixed(2).toString()}s`;
|
|
63
|
+
}
|
|
64
|
+
return `${milliseconds.toFixed(0).toString()}ms`;
|
|
65
|
+
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{t as e}from"./worker-DEDLIQQV.js";var t=e(((e,t)=>{t.exports={}}));export default t();
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import { s as __toESM } from "./chunk-BNovOVIE.js";
|
|
2
|
-
import { t as require_react } from "./react-DA-nE2FX.js";
|
|
3
|
-
import { t as require_react_dom } from "./react-dom-BTJzcVJ9.js";
|
|
4
|
-
import { _ as isEditableGridCell, i as makeCSSStyle, n as ThemeContext, t as ClickOutsideContainer, v as isInnerOnlyCell, y as isObjectEditorCallbackResult } from "./click-outside-container-BDd67_1U.js";
|
|
5
|
-
import { t as styled_default } from "./dist-D_bzzWBm.js";
|
|
6
|
-
var import_react_dom = require_react_dom(), import_react = /* @__PURE__ */ __toESM(require_react(), 1), _exp2 = () => (n) => n.targetX, _exp3 = () => (n) => n.targetY, _exp4 = () => (n) => n.targetWidth, _exp5 = () => (n) => n.targetHeight, _exp6 = () => (n) => n.targetY + 10, _exp7 = () => (n) => Math.max(0, (n.targetHeight - 28) / 2);
|
|
7
|
-
const DataGridOverlayEditorStyle = /* @__PURE__ */ styled_default("div")({
|
|
8
|
-
name: "DataGridOverlayEditorStyle",
|
|
9
|
-
class: "gdg-d19meir1",
|
|
10
|
-
propsAsIs: false,
|
|
11
|
-
vars: {
|
|
12
|
-
"d19meir1-0": [_exp3(), "px"],
|
|
13
|
-
"d19meir1-1": [_exp2(), "px"],
|
|
14
|
-
"d19meir1-2": [_exp4(), "px"],
|
|
15
|
-
"d19meir1-3": [_exp5(), "px"],
|
|
16
|
-
"d19meir1-4": [_exp6(), "px"],
|
|
17
|
-
"d19meir1-5": [_exp7(), "px"]
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
function useRefState() {
|
|
21
|
-
let [n, c] = import_react.useState();
|
|
22
|
-
return [n ?? void 0, c];
|
|
23
|
-
}
|
|
24
|
-
function useStayOnScreen() {
|
|
25
|
-
let [n, c] = useRefState(), [l, u] = import_react.useState(0), [d, f] = import_react.useState(true);
|
|
26
|
-
return import_react.useLayoutEffect(() => {
|
|
27
|
-
if (n === void 0 || !("IntersectionObserver" in window)) return;
|
|
28
|
-
let c2 = new IntersectionObserver((n2) => {
|
|
29
|
-
n2.length !== 0 && f(n2[0].isIntersecting);
|
|
30
|
-
}, { threshold: 1 });
|
|
31
|
-
return c2.observe(n), () => c2.disconnect();
|
|
32
|
-
}, [n]), import_react.useEffect(() => {
|
|
33
|
-
if (d || n === void 0) return;
|
|
34
|
-
let c2, l2 = () => {
|
|
35
|
-
let { right: d2 } = n.getBoundingClientRect();
|
|
36
|
-
u((n2) => Math.min(n2 + window.innerWidth - d2 - 10, 0)), c2 = requestAnimationFrame(l2);
|
|
37
|
-
};
|
|
38
|
-
return c2 = requestAnimationFrame(l2), () => {
|
|
39
|
-
c2 !== void 0 && cancelAnimationFrame(c2);
|
|
40
|
-
};
|
|
41
|
-
}, [n, d]), {
|
|
42
|
-
ref: c,
|
|
43
|
-
style: import_react.useMemo(() => ({ transform: `translateX(${l}px)` }), [l])
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
var data_grid_overlay_editor_default = (n) => {
|
|
47
|
-
let { target: c, content: l, onFinishEditing: p, forceEditMode: m, initialValue: h, imageEditorOverride: g, markdownDivCreateNode: _, highlight: v, className: y, theme: b, id: x, cell: S, bloom: C, validateCell: w, getCellRenderer: T, provideEditor: E, isOutsideClick: D, customEventTarget: O } = n, [k, A] = import_react.useState(m ? l : void 0), j = import_react.useRef(k ?? l);
|
|
48
|
-
j.current = k ?? l;
|
|
49
|
-
let [M, N] = import_react.useState(() => w === void 0 ? true : !(isEditableGridCell(l) && (w == null ? void 0 : w(S, l, j.current)) === false)), P = import_react.useCallback((n2, c2) => {
|
|
50
|
-
p(M ? n2 : void 0, c2);
|
|
51
|
-
}, [M, p]), F = import_react.useCallback((n2) => {
|
|
52
|
-
if (w !== void 0 && n2 !== void 0 && isEditableGridCell(n2)) {
|
|
53
|
-
let c2 = w(S, n2, j.current);
|
|
54
|
-
c2 === false ? N(false) : (typeof c2 == "object" && (n2 = c2), N(true));
|
|
55
|
-
}
|
|
56
|
-
A(n2);
|
|
57
|
-
}, [S, w]), I = import_react.useRef(false), L = import_react.useRef(void 0), R = import_react.useCallback(() => {
|
|
58
|
-
P(k, [0, 0]), I.current = true;
|
|
59
|
-
}, [k, P]), z = import_react.useCallback((n2, c2) => {
|
|
60
|
-
P(n2, c2 ?? L.current ?? [0, 0]), I.current = true;
|
|
61
|
-
}, [P]), B = import_react.useCallback(async (n2) => {
|
|
62
|
-
let c2 = false;
|
|
63
|
-
n2.key === "Escape" ? (n2.stopPropagation(), n2.preventDefault(), L.current = [0, 0]) : n2.key === "Enter" && !n2.shiftKey ? (n2.stopPropagation(), n2.preventDefault(), L.current = [0, 1], c2 = true) : n2.key === "Tab" && (n2.stopPropagation(), n2.preventDefault(), L.current = [n2.shiftKey ? -1 : 1, 0], c2 = true), window.setTimeout(() => {
|
|
64
|
-
!I.current && L.current !== void 0 && (P(c2 ? k : void 0, L.current), I.current = true);
|
|
65
|
-
}, 0);
|
|
66
|
-
}, [P, k]), V = k ?? l, [H, U] = import_react.useMemo(() => {
|
|
67
|
-
var _a, _b;
|
|
68
|
-
if (isInnerOnlyCell(l)) return [];
|
|
69
|
-
let n2 = E == null ? void 0 : E(l);
|
|
70
|
-
return n2 === void 0 ? [(_b = (_a = T(l)) == null ? void 0 : _a.provideEditor) == null ? void 0 : _b.call(_a, l), false] : [n2, false];
|
|
71
|
-
}, [
|
|
72
|
-
l,
|
|
73
|
-
T,
|
|
74
|
-
E
|
|
75
|
-
]), { ref: W, style: G } = useStayOnScreen(), K = true, q, J = true, Y;
|
|
76
|
-
if (H !== void 0) {
|
|
77
|
-
K = H.disablePadding !== true, J = H.disableStyling !== true;
|
|
78
|
-
let n2 = isObjectEditorCallbackResult(H);
|
|
79
|
-
n2 && (Y = H.styleOverride);
|
|
80
|
-
let l2 = n2 ? H.editor : H;
|
|
81
|
-
q = import_react.createElement(l2, {
|
|
82
|
-
isHighlighted: v,
|
|
83
|
-
onChange: F,
|
|
84
|
-
value: V,
|
|
85
|
-
initialValue: h,
|
|
86
|
-
onFinishedEditing: z,
|
|
87
|
-
validatedSelection: isEditableGridCell(V) ? V.selectionRange : void 0,
|
|
88
|
-
forceEditMode: m,
|
|
89
|
-
target: c,
|
|
90
|
-
imageEditorOverride: g,
|
|
91
|
-
markdownDivCreateNode: _,
|
|
92
|
-
isValid: M,
|
|
93
|
-
theme: b
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
Y = {
|
|
97
|
-
...Y,
|
|
98
|
-
...G
|
|
99
|
-
};
|
|
100
|
-
let X = document.getElementById("portal");
|
|
101
|
-
if (X === null) return console.error('Cannot open Data Grid overlay editor, because portal not found. Please add `<div id="portal" />` as the last child of your `<body>`.'), null;
|
|
102
|
-
let Z = J ? "gdg-style" : "gdg-unstyle";
|
|
103
|
-
M || (Z += " gdg-invalid"), K && (Z += " gdg-pad");
|
|
104
|
-
let Q = (C == null ? void 0 : C[0]) ?? 1, $ = (C == null ? void 0 : C[1]) ?? 1;
|
|
105
|
-
return (0, import_react_dom.createPortal)(import_react.createElement(ThemeContext.Provider, { value: b }, import_react.createElement(ClickOutsideContainer, {
|
|
106
|
-
style: makeCSSStyle(b),
|
|
107
|
-
className: y,
|
|
108
|
-
onClickOutside: R,
|
|
109
|
-
isOutsideClick: D,
|
|
110
|
-
customEventTarget: O
|
|
111
|
-
}, import_react.createElement(DataGridOverlayEditorStyle, {
|
|
112
|
-
ref: W,
|
|
113
|
-
id: x,
|
|
114
|
-
className: Z,
|
|
115
|
-
style: Y,
|
|
116
|
-
as: U === true ? "label" : void 0,
|
|
117
|
-
targetX: c.x - Q,
|
|
118
|
-
targetY: c.y - $,
|
|
119
|
-
targetWidth: c.width + Q * 2,
|
|
120
|
-
targetHeight: c.height + $ * 2
|
|
121
|
-
}, import_react.createElement("div", {
|
|
122
|
-
className: "gdg-clip-region",
|
|
123
|
-
onKeyDown: B
|
|
124
|
-
}, q)))), X);
|
|
125
|
-
};
|
|
126
|
-
export {
|
|
127
|
-
data_grid_overlay_editor_default as default
|
|
128
|
-
};
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { describe, expect, it } from "vitest";
|
|
4
|
-
import { getSnippetDisplay } from "../snippet-display";
|
|
5
|
-
|
|
6
|
-
describe("getSnippetDisplay", () => {
|
|
7
|
-
it("shows sql cells as the sql query", () => {
|
|
8
|
-
const { language, value } = getSnippetDisplay(
|
|
9
|
-
'df = mo.sql("""SELECT * FROM users LIMIT 5""")',
|
|
10
|
-
);
|
|
11
|
-
expect(language).toBe("sql");
|
|
12
|
-
expect(value).toBe("SELECT * FROM users LIMIT 5");
|
|
13
|
-
});
|
|
14
|
-
|
|
15
|
-
it("keeps plain python cells as python", () => {
|
|
16
|
-
const code = "x = 1 + 2\nprint(x)";
|
|
17
|
-
expect(getSnippetDisplay(code)).toEqual({
|
|
18
|
-
language: "python",
|
|
19
|
-
value: code,
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
-
|
|
3
|
-
import { SQLParser } from "@marimo-team/smart-cells";
|
|
4
|
-
|
|
5
|
-
export type SnippetLanguage = "python" | "sql";
|
|
6
|
-
|
|
7
|
-
export interface SnippetDisplay {
|
|
8
|
-
language: SnippetLanguage;
|
|
9
|
-
value: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const sqlParser = new SQLParser();
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Decide how a snippet's code should be shown in the panel.
|
|
16
|
-
*
|
|
17
|
-
* SQL cells are stored as python `mo.sql(...)`. Unwrap the inner query and
|
|
18
|
-
* highlight it as SQL, matching how the cell renders once the snippet is
|
|
19
|
-
* inserted. Everything else stays python.
|
|
20
|
-
*/
|
|
21
|
-
export function getSnippetDisplay(code: string): SnippetDisplay {
|
|
22
|
-
if (sqlParser.isSupported(code)) {
|
|
23
|
-
const { code: query } = sqlParser.transformIn(code);
|
|
24
|
-
return { language: "sql", value: query };
|
|
25
|
-
}
|
|
26
|
-
return { language: "python", value: code };
|
|
27
|
-
}
|