@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,263 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { type RenderOptions, render } from "@testing-library/react";
|
|
4
|
+
import { createStore, Provider } from "jotai";
|
|
5
|
+
import type { ReactElement } from "react";
|
|
6
|
+
import {
|
|
7
|
+
ISLAND_DATA_ATTRIBUTES,
|
|
8
|
+
ISLAND_TAG_NAMES,
|
|
9
|
+
} from "@/core/islands/constants";
|
|
10
|
+
import type { WorkerFactory } from "@/core/islands/worker-factory";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Test utilities for islands components and logic
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// DOM Test Utilities
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates a mock marimo-island element in the DOM
|
|
22
|
+
*/
|
|
23
|
+
export function createMockIslandElement(options: {
|
|
24
|
+
appId?: string;
|
|
25
|
+
cellIdx?: string;
|
|
26
|
+
code?: string;
|
|
27
|
+
innerHTML?: string;
|
|
28
|
+
}): HTMLElement {
|
|
29
|
+
const {
|
|
30
|
+
appId = "test-app",
|
|
31
|
+
cellIdx = "0",
|
|
32
|
+
code = "import marimo as mo",
|
|
33
|
+
innerHTML = "",
|
|
34
|
+
} = options;
|
|
35
|
+
|
|
36
|
+
const element = document.createElement(ISLAND_TAG_NAMES.ISLAND);
|
|
37
|
+
element.setAttribute(ISLAND_DATA_ATTRIBUTES.APP_ID, appId);
|
|
38
|
+
element.setAttribute(ISLAND_DATA_ATTRIBUTES.CELL_IDX, cellIdx);
|
|
39
|
+
|
|
40
|
+
if (code) {
|
|
41
|
+
const codeElement = document.createElement(ISLAND_TAG_NAMES.CELL_CODE);
|
|
42
|
+
codeElement.textContent = encodeURIComponent(code);
|
|
43
|
+
element.appendChild(codeElement);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (innerHTML) {
|
|
47
|
+
const outputElement = document.createElement(ISLAND_TAG_NAMES.CELL_OUTPUT);
|
|
48
|
+
outputElement.innerHTML = innerHTML;
|
|
49
|
+
element.appendChild(outputElement);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return element;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Creates multiple island elements for testing
|
|
57
|
+
*/
|
|
58
|
+
export function createMockIslands(
|
|
59
|
+
count: number,
|
|
60
|
+
appId = "test-app",
|
|
61
|
+
): HTMLElement[] {
|
|
62
|
+
return Array.from({ length: count }, (_, idx) =>
|
|
63
|
+
createMockIslandElement({
|
|
64
|
+
appId,
|
|
65
|
+
cellIdx: String(idx),
|
|
66
|
+
code: `cell_${idx} = ${idx}`,
|
|
67
|
+
innerHTML: `<div>output ${idx}</div>`,
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// React Test Utilities
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
interface IslandsRenderOptions extends Omit<RenderOptions, "wrapper"> {
|
|
77
|
+
initialStore?: ReturnType<typeof createStore>;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Renders a React component with Islands providers
|
|
82
|
+
*/
|
|
83
|
+
export function renderWithIslandsProviders(
|
|
84
|
+
ui: ReactElement,
|
|
85
|
+
options?: IslandsRenderOptions,
|
|
86
|
+
) {
|
|
87
|
+
const { initialStore, ...renderOptions } = options || {};
|
|
88
|
+
const store = initialStore || createStore();
|
|
89
|
+
|
|
90
|
+
function Wrapper({ children }: { children: React.ReactNode }) {
|
|
91
|
+
return <Provider store={store}>{children}</Provider>;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
store,
|
|
96
|
+
...render(ui, { wrapper: Wrapper, ...renderOptions }),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Assertion Helpers
|
|
102
|
+
// ============================================================================
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Waits for a condition to be true with timeout
|
|
106
|
+
*/
|
|
107
|
+
export async function waitForCondition(
|
|
108
|
+
condition: () => boolean,
|
|
109
|
+
timeout = 1000,
|
|
110
|
+
interval = 50,
|
|
111
|
+
): Promise<void> {
|
|
112
|
+
const startTime = Date.now();
|
|
113
|
+
while (!condition()) {
|
|
114
|
+
if (Date.now() - startTime > timeout) {
|
|
115
|
+
throw new Error("Timeout waiting for condition");
|
|
116
|
+
}
|
|
117
|
+
await new Promise((resolve) => setTimeout(resolve, interval));
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Waits for an async function to not throw
|
|
123
|
+
*/
|
|
124
|
+
export async function waitForNoError<T>(
|
|
125
|
+
fn: () => T | Promise<T>,
|
|
126
|
+
timeout = 1000,
|
|
127
|
+
): Promise<T> {
|
|
128
|
+
const startTime = Date.now();
|
|
129
|
+
let lastError: Error | undefined;
|
|
130
|
+
|
|
131
|
+
while (Date.now() - startTime < timeout) {
|
|
132
|
+
try {
|
|
133
|
+
return await Promise.resolve(fn());
|
|
134
|
+
} catch (error) {
|
|
135
|
+
lastError = error as Error;
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
throw lastError || new Error("Timeout waiting for function to succeed");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ============================================================================
|
|
144
|
+
// HTML-based Island Harness
|
|
145
|
+
// ============================================================================
|
|
146
|
+
|
|
147
|
+
export interface IslandSpec {
|
|
148
|
+
appId?: string;
|
|
149
|
+
reactive?: boolean;
|
|
150
|
+
code?: string;
|
|
151
|
+
output?: string;
|
|
152
|
+
displayCode?: boolean;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Builds island HTML from a declarative spec — mirrors what generate.py produces.
|
|
157
|
+
* Use with `createIslandHarness()` to parse and inspect the resulting state.
|
|
158
|
+
*/
|
|
159
|
+
export function buildIslandHTML(islands: IslandSpec[]): string {
|
|
160
|
+
return islands
|
|
161
|
+
.map((spec) => {
|
|
162
|
+
const appId = spec.appId ?? "test-app";
|
|
163
|
+
const reactive = spec.reactive ?? true;
|
|
164
|
+
const output = spec.output ?? "<div>output</div>";
|
|
165
|
+
const code = spec.code ?? 'print("hello")';
|
|
166
|
+
|
|
167
|
+
const codeTag = code
|
|
168
|
+
? `<${ISLAND_TAG_NAMES.CELL_CODE}>${encodeURIComponent(code)}</${ISLAND_TAG_NAMES.CELL_CODE}>`
|
|
169
|
+
: "";
|
|
170
|
+
const outputTag = `<${ISLAND_TAG_NAMES.CELL_OUTPUT}>${output}</${ISLAND_TAG_NAMES.CELL_OUTPUT}>`;
|
|
171
|
+
|
|
172
|
+
return `<${ISLAND_TAG_NAMES.ISLAND} ${ISLAND_DATA_ATTRIBUTES.APP_ID}="${appId}" ${ISLAND_DATA_ATTRIBUTES.REACTIVE}="${reactive}">${outputTag}${codeTag}</${ISLAND_TAG_NAMES.ISLAND}>`;
|
|
173
|
+
})
|
|
174
|
+
.join("\n");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface IslandHarness {
|
|
178
|
+
/** The container element holding all islands */
|
|
179
|
+
container: HTMLElement;
|
|
180
|
+
/** All island elements found in the container */
|
|
181
|
+
islands: HTMLElement[];
|
|
182
|
+
/** Cleanup — removes container from DOM */
|
|
183
|
+
cleanup: () => void;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Creates a test harness from raw island HTML.
|
|
188
|
+
*
|
|
189
|
+
* Parses the HTML into real DOM elements attached to `document.body` so that
|
|
190
|
+
* `querySelectorAll`, `getAttribute`, etc. work correctly.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* const harness = createIslandHarness(buildIslandHTML([
|
|
195
|
+
* { reactive: true, code: 'x = 1', output: '<div>1</div>' },
|
|
196
|
+
* { reactive: false, output: '<div>static</div>' },
|
|
197
|
+
* ]));
|
|
198
|
+
* // ... assertions ...
|
|
199
|
+
* harness.cleanup();
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export function createIslandHarness(html: string): IslandHarness {
|
|
203
|
+
const container = document.createElement("div");
|
|
204
|
+
container.innerHTML = html;
|
|
205
|
+
document.body.appendChild(container);
|
|
206
|
+
|
|
207
|
+
// eslint-disable-next-line unicorn/prefer-spread
|
|
208
|
+
const islands = Array.from(
|
|
209
|
+
container.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND),
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
container,
|
|
214
|
+
islands,
|
|
215
|
+
cleanup: () => container.remove(),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ============================================================================
|
|
220
|
+
// Mock Worker Factory
|
|
221
|
+
// ============================================================================
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Mock worker factory for testing
|
|
225
|
+
*/
|
|
226
|
+
export class MockWorkerFactory implements WorkerFactory {
|
|
227
|
+
public workers: Worker[] = [];
|
|
228
|
+
private readonly mockWorker?: Worker;
|
|
229
|
+
|
|
230
|
+
constructor(mockWorker?: Worker) {
|
|
231
|
+
this.mockWorker = mockWorker;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
create(): Worker {
|
|
235
|
+
const worker = this.mockWorker || this.createMockWorker();
|
|
236
|
+
this.workers.push(worker);
|
|
237
|
+
return worker;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
private createMockWorker(): Worker {
|
|
241
|
+
return {
|
|
242
|
+
postMessage: () => {},
|
|
243
|
+
terminate: () => {},
|
|
244
|
+
addEventListener: () => {},
|
|
245
|
+
removeEventListener: () => {},
|
|
246
|
+
dispatchEvent: () => true,
|
|
247
|
+
onmessage: null,
|
|
248
|
+
onerror: null,
|
|
249
|
+
onmessageerror: null,
|
|
250
|
+
} as unknown as Worker;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
getCreatedWorkers(): Worker[] {
|
|
254
|
+
return this.workers;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
terminateAll(): void {
|
|
258
|
+
for (const worker of this.workers) {
|
|
259
|
+
worker.terminate();
|
|
260
|
+
}
|
|
261
|
+
this.workers = [];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
/* Copyright 2026 Marimo. All rights reserved. */
|
|
2
|
+
|
|
3
|
+
import { toast } from "@/components/ui/use-toast";
|
|
4
|
+
import { ISLAND_CSS_CLASSES, ISLAND_TAG_NAMES } from "@/core/islands/constants";
|
|
5
|
+
import { renderHTML } from "@/plugins/core/RenderHTML";
|
|
6
|
+
import {
|
|
7
|
+
handleWidgetMessage,
|
|
8
|
+
MODEL_MANAGER,
|
|
9
|
+
} from "@/plugins/impl/anywidget/model";
|
|
10
|
+
import { initializePlugins } from "@/plugins/plugins";
|
|
11
|
+
import { Functions } from "@/utils/functions";
|
|
12
|
+
import {
|
|
13
|
+
safeExtractSetUIElementMessageBuffers,
|
|
14
|
+
type JsonString,
|
|
15
|
+
} from "@/utils/json/base64";
|
|
16
|
+
import { jsonParseWithSpecialChar } from "@/utils/json/json-parser";
|
|
17
|
+
import { Logger } from "@/utils/Logger";
|
|
18
|
+
import { logNever } from "@/utils/assertNever";
|
|
19
|
+
import {
|
|
20
|
+
createNotebookActions,
|
|
21
|
+
notebookAtom,
|
|
22
|
+
notebookReducer,
|
|
23
|
+
} from "../cells/cells";
|
|
24
|
+
import { defineCustomElement } from "../dom/defineCustomElement";
|
|
25
|
+
import { MarimoValueInputEvent } from "../dom/events";
|
|
26
|
+
import { UI_ELEMENT_REGISTRY } from "../dom/uiregistry";
|
|
27
|
+
import { FUNCTIONS_REGISTRY } from "../functions/FunctionRegistry";
|
|
28
|
+
import {
|
|
29
|
+
handleCellNotificationeration,
|
|
30
|
+
handleKernelReady,
|
|
31
|
+
handleRemoveUIElements,
|
|
32
|
+
} from "../kernel/handlers";
|
|
33
|
+
import type {
|
|
34
|
+
NotificationMessage,
|
|
35
|
+
NotificationMessageType,
|
|
36
|
+
} from "../kernel/messages";
|
|
37
|
+
import { queryParamHandlers } from "../kernel/queryParamHandlers";
|
|
38
|
+
import { RuntimeState } from "../kernel/RuntimeState";
|
|
39
|
+
import { initialModeAtom } from "../mode";
|
|
40
|
+
import { requestClientAtom } from "../network/requests";
|
|
41
|
+
import { store as defaultStore } from "../state/jotai";
|
|
42
|
+
import type { IslandsPyodideBridge } from "./bridge";
|
|
43
|
+
import { MarimoIslandElement } from "./components/web-components";
|
|
44
|
+
import {
|
|
45
|
+
shouldShowIslandsWarningIndicatorAtom,
|
|
46
|
+
userTriedToInteractWithIslandsAtom,
|
|
47
|
+
} from "./state";
|
|
48
|
+
import { dismissIslandsLoadingToast, toastIslandsLoading } from "./toast";
|
|
49
|
+
|
|
50
|
+
type Store = typeof defaultStore;
|
|
51
|
+
type NotebookActions = ReturnType<typeof createNotebookActions>;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Configuration for the islands bootstrap process
|
|
55
|
+
*/
|
|
56
|
+
export interface IslandsBootstrapConfig {
|
|
57
|
+
bridge: IslandsPyodideBridge;
|
|
58
|
+
store?: Store;
|
|
59
|
+
root?: Document | Element;
|
|
60
|
+
autoInitializePlugins?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize marimo islands: sets up networking, discovers island elements,
|
|
65
|
+
* wires up the message consumer, and starts the runtime.
|
|
66
|
+
*/
|
|
67
|
+
export async function initializeIslands(
|
|
68
|
+
config: IslandsBootstrapConfig,
|
|
69
|
+
): Promise<void> {
|
|
70
|
+
const store = config.store || defaultStore;
|
|
71
|
+
const root = config.root || document;
|
|
72
|
+
const bridge = config.bridge;
|
|
73
|
+
|
|
74
|
+
// Setup networking
|
|
75
|
+
store.set(requestClientAtom, bridge);
|
|
76
|
+
store.set(initialModeAtom, "read");
|
|
77
|
+
|
|
78
|
+
// Initialize plugins for rendering static HTML
|
|
79
|
+
if (config.autoInitializePlugins !== false) {
|
|
80
|
+
initializePlugins();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Find all island elements
|
|
84
|
+
// eslint-disable-next-line prefer-spread
|
|
85
|
+
const islands = Array.from(
|
|
86
|
+
root.querySelectorAll<HTMLElement>(ISLAND_TAG_NAMES.ISLAND),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (islands.length === 0) {
|
|
90
|
+
Logger.log("No islands found, skipping initialization");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
Logger.log(`Initializing ${islands.length} island(s)`);
|
|
95
|
+
|
|
96
|
+
// Apply styles
|
|
97
|
+
for (const island of islands) {
|
|
98
|
+
island.classList.add(ISLAND_CSS_CLASSES.NAMESPACE);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Setup notebook actions
|
|
102
|
+
const actions = createNotebookActions((action) => {
|
|
103
|
+
store.set(notebookAtom, (state: typeof notebookAtom.init) =>
|
|
104
|
+
notebookReducer(state, action),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Loading indicator: dim islands while Pyodide initializes
|
|
109
|
+
store.sub(shouldShowIslandsWarningIndicatorAtom, () => {
|
|
110
|
+
const showing = store.get(shouldShowIslandsWarningIndicatorAtom);
|
|
111
|
+
if (showing) {
|
|
112
|
+
toastIslandsLoading();
|
|
113
|
+
for (const island of islands) {
|
|
114
|
+
island.style.setProperty("opacity", "0.5");
|
|
115
|
+
}
|
|
116
|
+
} else {
|
|
117
|
+
dismissIslandsLoadingToast();
|
|
118
|
+
for (const island of islands) {
|
|
119
|
+
island.style.removeProperty("opacity");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Wire up kernel message handling
|
|
125
|
+
// The wire format is {"op": "...", "data": {"op": "...", ...}} — both
|
|
126
|
+
// the envelope and the payload carry the op. The bridge types the message
|
|
127
|
+
// as NotificationPayload (just {data}), but the actual wire format
|
|
128
|
+
// includes the outer op too.
|
|
129
|
+
bridge.consumeMessages((message) => {
|
|
130
|
+
handleMessage(
|
|
131
|
+
message as unknown as JsonString<IslandsNotificationMessage>,
|
|
132
|
+
actions,
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Track first user interaction before initialization completes
|
|
137
|
+
document.addEventListener(
|
|
138
|
+
MarimoValueInputEvent.TYPE,
|
|
139
|
+
() => {
|
|
140
|
+
store.set(userTriedToInteractWithIslandsAtom, true);
|
|
141
|
+
},
|
|
142
|
+
{ once: true },
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
// Start the runtime
|
|
146
|
+
RuntimeState.INSTANCE.start(bridge.sendComponentValues);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
type IslandsNotificationMessage = {
|
|
150
|
+
[K in NotificationMessageType]: {
|
|
151
|
+
data: Extract<NotificationMessage, { op: K }>;
|
|
152
|
+
op: K;
|
|
153
|
+
};
|
|
154
|
+
}[NotificationMessageType];
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Handles a single message from the kernel.
|
|
158
|
+
*
|
|
159
|
+
* Wire format from Python: {"op": "<name>", "data": {"op": "<name>", ...}}
|
|
160
|
+
*/
|
|
161
|
+
function handleMessage(
|
|
162
|
+
message: JsonString<IslandsNotificationMessage>,
|
|
163
|
+
actions: NotebookActions,
|
|
164
|
+
): void {
|
|
165
|
+
try {
|
|
166
|
+
const msg = jsonParseWithSpecialChar(message);
|
|
167
|
+
Logger.debug("Islands received message:", msg.op);
|
|
168
|
+
|
|
169
|
+
switch (msg.op) {
|
|
170
|
+
// Unsupported operations in islands mode
|
|
171
|
+
case "banner":
|
|
172
|
+
case "missing-package-alert":
|
|
173
|
+
case "installing-package-alert":
|
|
174
|
+
case "completion-result":
|
|
175
|
+
case "reload":
|
|
176
|
+
case "focus-cell":
|
|
177
|
+
case "variables":
|
|
178
|
+
case "variable-values":
|
|
179
|
+
case "data-column-preview":
|
|
180
|
+
case "sql-table-preview":
|
|
181
|
+
case "sql-table-list-preview":
|
|
182
|
+
case "sql-schema-list-preview":
|
|
183
|
+
case "datasets":
|
|
184
|
+
case "data-source-connections":
|
|
185
|
+
case "validate-sql-result":
|
|
186
|
+
case "storage-namespaces":
|
|
187
|
+
case "storage-entries":
|
|
188
|
+
case "storage-download-ready":
|
|
189
|
+
case "secret-keys-result":
|
|
190
|
+
case "startup-logs":
|
|
191
|
+
case "completed-run":
|
|
192
|
+
case "interrupted":
|
|
193
|
+
case "reconnected":
|
|
194
|
+
case "cache-cleared":
|
|
195
|
+
case "cache-info":
|
|
196
|
+
case "kernel-startup-error":
|
|
197
|
+
case "notebook-document-transaction":
|
|
198
|
+
return;
|
|
199
|
+
|
|
200
|
+
case "kernel-ready":
|
|
201
|
+
handleKernelReady(msg.data, {
|
|
202
|
+
autoInstantiate: true,
|
|
203
|
+
setCells: actions.setCells,
|
|
204
|
+
setLayoutData: Functions.NOOP,
|
|
205
|
+
setAppConfig: Functions.NOOP,
|
|
206
|
+
setCapabilities: Functions.NOOP,
|
|
207
|
+
setKernelState: Functions.NOOP,
|
|
208
|
+
onError: Logger.error,
|
|
209
|
+
});
|
|
210
|
+
defineCustomElement(ISLAND_TAG_NAMES.ISLAND, MarimoIslandElement);
|
|
211
|
+
return;
|
|
212
|
+
|
|
213
|
+
case "send-ui-element-message":
|
|
214
|
+
UI_ELEMENT_REGISTRY.broadcastMessage(
|
|
215
|
+
msg.data.ui_element,
|
|
216
|
+
msg.data.message,
|
|
217
|
+
safeExtractSetUIElementMessageBuffers(msg.data),
|
|
218
|
+
);
|
|
219
|
+
return;
|
|
220
|
+
|
|
221
|
+
case "remove-ui-elements":
|
|
222
|
+
handleRemoveUIElements(msg.data);
|
|
223
|
+
return;
|
|
224
|
+
|
|
225
|
+
case "function-call-result":
|
|
226
|
+
FUNCTIONS_REGISTRY.resolve(msg.data.function_call_id, msg.data);
|
|
227
|
+
return;
|
|
228
|
+
|
|
229
|
+
case "cell-op":
|
|
230
|
+
handleCellNotificationeration(msg.data, actions.handleCellMessage);
|
|
231
|
+
return;
|
|
232
|
+
|
|
233
|
+
case "alert":
|
|
234
|
+
toast({
|
|
235
|
+
title: msg.data.title,
|
|
236
|
+
description: renderHTML({ html: msg.data.description }),
|
|
237
|
+
variant: msg.data.variant,
|
|
238
|
+
});
|
|
239
|
+
return;
|
|
240
|
+
|
|
241
|
+
case "query-params-append":
|
|
242
|
+
queryParamHandlers.append(msg.data);
|
|
243
|
+
return;
|
|
244
|
+
case "query-params-set":
|
|
245
|
+
queryParamHandlers.set(msg.data);
|
|
246
|
+
return;
|
|
247
|
+
case "query-params-delete":
|
|
248
|
+
queryParamHandlers.delete(msg.data);
|
|
249
|
+
return;
|
|
250
|
+
case "query-params-clear":
|
|
251
|
+
queryParamHandlers.clear();
|
|
252
|
+
return;
|
|
253
|
+
|
|
254
|
+
case "model-lifecycle":
|
|
255
|
+
handleWidgetMessage(MODEL_MANAGER, msg.data);
|
|
256
|
+
return;
|
|
257
|
+
|
|
258
|
+
default:
|
|
259
|
+
logNever(msg);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
} catch (error) {
|
|
263
|
+
Logger.error("Failed to handle kernel message:", error);
|
|
264
|
+
}
|
|
265
|
+
}
|