From 6a7537fab8cef321a452e397312602ad27854855 Mon Sep 17 00:00:00 2001 From: Andrew Stein Date: Tue, 26 May 2026 02:00:49 -0400 Subject: [PATCH] Add `@perspective-dev/anywidget` for vscode/jupyter Signed-off-by: Andrew Stein # Conflicts: # packages/jupyterlab/package.json --- .github/workflows/build.yaml | 5 +- .gitignore | 2 + packages/anywidget/build.mjs | 79 ++++ .../src/js/utils.js => anywidget/clean.mjs} | 10 +- packages/anywidget/package.json | 29 ++ packages/anywidget/src/css/index.css | 43 ++ packages/anywidget/src/js/index.js | 192 ++++++++ packages/jupyterlab/DEVELOPMENT.md | 35 +- packages/jupyterlab/build.mjs | 103 +---- packages/jupyterlab/package.json | 17 +- packages/jupyterlab/src/js/index.js | 32 +- packages/jupyterlab/src/js/model.js | 43 -- packages/jupyterlab/src/js/notebook/css.js | 20 - .../jupyterlab/src/js/notebook/extension.js | 32 -- packages/jupyterlab/src/js/notebook/index.js | 26 -- packages/jupyterlab/src/js/plugin.js | 38 -- packages/jupyterlab/src/js/psp_widget.js | 199 --------- packages/jupyterlab/src/js/renderer.js | 110 +++-- packages/jupyterlab/src/js/version.js | 14 - packages/jupyterlab/src/js/view.js | 420 ------------------ packages/jupyterlab/src/js/widget.js | 54 --- packages/jupyterlab/test/html/resize.html | 26 -- packages/jupyterlab/test/js/resize.spec.mjs | 114 ----- packages/jupyterlab/webpack.config.js | 7 - packages/viewer-charts/src/ts/index.ts | 20 +- packages/viewer-datagrid/src/ts/index.ts | 7 + pnpm-lock.yaml | 308 ++----------- pnpm-workspace.yaml | 1 + rust/perspective-python/Cargo.toml | 1 - rust/perspective-python/build.mjs | 9 +- rust/perspective-python/clean.mjs | 1 + .../finos-perspective-nbextension.json | 5 - .../tests/widget/test_anywidget_smoke.py | 74 +++ .../perspective/widget/__init__.py | 22 +- rust/perspective-python/pyproject.toml | 4 +- rust/perspective-python/requirements.txt | 1 + .../src/rust/utils/custom_element.rs | 5 + tools/scripts/setup.mjs | 5 + 38 files changed, 638 insertions(+), 1475 deletions(-) create mode 100644 packages/anywidget/build.mjs rename packages/{jupyterlab/src/js/utils.js => anywidget/clean.mjs} (89%) create mode 100644 packages/anywidget/package.json create mode 100644 packages/anywidget/src/css/index.css create mode 100644 packages/anywidget/src/js/index.js delete mode 100644 packages/jupyterlab/src/js/model.js delete mode 100644 packages/jupyterlab/src/js/notebook/css.js delete mode 100644 packages/jupyterlab/src/js/notebook/extension.js delete mode 100644 packages/jupyterlab/src/js/notebook/index.js delete mode 100644 packages/jupyterlab/src/js/plugin.js delete mode 100644 packages/jupyterlab/src/js/psp_widget.js delete mode 100644 packages/jupyterlab/src/js/version.js delete mode 100644 packages/jupyterlab/src/js/view.js delete mode 100644 packages/jupyterlab/src/js/widget.js delete mode 100644 packages/jupyterlab/test/html/resize.html delete mode 100644 packages/jupyterlab/test/js/resize.spec.mjs delete mode 100644 rust/perspective-python/perspective/extension/finos-perspective-nbextension.json create mode 100644 rust/perspective-python/perspective/tests/widget/test_anywidget_smoke.py diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 09e85ce380..5e9603a6a8 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -559,7 +559,10 @@ jobs: - name: Build extension run: pnpm run build env: - PACKAGE: "jupyterlab" + # `anywidget` builds the widget bundle into the python static + # dir; `jupyterlab` builds the renderers labextension. Order + # matters: anywidget first so the static bundle exists. + PACKAGE: "anywidget,jupyterlab" - run: node tools/scripts/repack_wheel.mjs diff --git a/.gitignore b/.gitignore index b889829b72..a8dd00ef04 100644 --- a/.gitignore +++ b/.gitignore @@ -52,6 +52,7 @@ rust/perspective-js/src/ts/ts-rs rust/perspective-server/build rust/perspective-python/LICENSE_THIRDPARTY_cargo.yml rust/perspective-python/LICENSE.md +rust/perspective-python/perspective/widget/static/*.js.map rust/perspective-server/docs/lib_gen.md rust/perspective-viewer/src/ts/ts-rs rust/perspective/src/ts/ts-rs @@ -69,3 +70,4 @@ docs/static/react rust/perspective-server/build target/ dist-gh-pages +rust/perspective-python/perspective/widget/static diff --git a/packages/anywidget/build.mjs b/packages/anywidget/build.mjs new file mode 100644 index 0000000000..70418939b8 --- /dev/null +++ b/packages/anywidget/build.mjs @@ -0,0 +1,79 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import { WasmPlugin } from "@perspective-dev/esbuild-plugin/wasm.js"; +import { WorkerPlugin } from "@perspective-dev/esbuild-plugin/worker.js"; +import { build } from "@perspective-dev/esbuild-plugin/build.js"; +import * as path from "node:path"; +import { bundleAsync as bundleCss } from "lightningcss"; +import * as fs from "node:fs"; +import * as url from "node:url"; +import { + resolveNPM, + inlineUrlVisitor, +} from "@perspective-dev/viewer/tools.mjs"; + +const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1); + +// The anywidget bundle is `PerspectiveWidget._esm`/`._css`, loaded by anywidget +// from the python package at import time. Emit it (wasm inlined) directly into +// the python package's static dir. The filename stays `perspective-jupyter.*` +// to match `perspective/widget/__init__.py` + the maturin `include`. +const STATIC = path.resolve( + __dirname, + "..", + "..", + "rust", + "perspective-python", + "perspective", + "widget", + "static", +); + +const ANYWIDGET_BUILD = { + entryPoints: ["src/js/index.js"], + plugins: [WasmPlugin(true), WorkerPlugin({ inline: true })], + outfile: path.join(STATIC, "perspective-anywidget.js"), + format: "esm", + define: { + global: "window", + }, + loader: { + ".css": "text", + ".html": "text", + ".ttf": "file", + }, +}; + +async function build_css(filename, outfile) { + const { code } = await bundleCss({ + filename, + minify: true, + visitor: inlineUrlVisitor(filename), + resolver: resolveNPM(import.meta.url), + }); + + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + fs.writeFileSync(outfile, code); +} + +async function build_all() { + fs.mkdirSync(STATIC, { recursive: true }); + await build_css( + path.resolve(__dirname, "src/css/index.css"), + path.join(STATIC, "perspective-anywidget.css"), + ); + + await build(ANYWIDGET_BUILD).catch(() => process.exit(1)); +} + +build_all(); diff --git a/packages/jupyterlab/src/js/utils.js b/packages/anywidget/clean.mjs similarity index 89% rename from packages/jupyterlab/src/js/utils.js rename to packages/anywidget/clean.mjs index 87e321251a..cadb1ffe01 100644 --- a/packages/jupyterlab/src/js/utils.js +++ b/packages/anywidget/clean.mjs @@ -10,6 +10,10 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -export const MIME_TYPE = "application/psp+json"; -export const PSP_CLASS = "PSPViewer"; -export const PSP_CONTAINER_CLASS = "PSPContainer"; +import * as fs from "node:fs"; + +// The build output is the anywidget bundle in the python package's static dir. +fs.rmSync("../../rust/perspective-python/perspective/widget/static", { + recursive: true, + force: true, +}); diff --git a/packages/anywidget/package.json b/packages/anywidget/package.json new file mode 100644 index 0000000000..ee0cf1bc96 --- /dev/null +++ b/packages/anywidget/package.json @@ -0,0 +1,29 @@ +{ + "name": "@perspective-dev/anywidget", + "version": "4.5.1", + "description": "The `PerspectiveWidget` anywidget bundle for `perspective-python` (shipped in the wheel and loaded by anywidget). Not a JupyterLab labextension.", + "type": "module", + "private": true, + "exports": { + ".": "./src/js/index.js" + }, + "files": [ + "src/**/*" + ], + "license": "Apache-2.0", + "scripts": { + "build": "node ./build.mjs", + "clean": "node ./clean.mjs" + }, + "dependencies": { + "@perspective-dev/client": "workspace:", + "@perspective-dev/server": "workspace:", + "@perspective-dev/viewer": "workspace:", + "@perspective-dev/viewer-charts": "workspace:", + "@perspective-dev/viewer-datagrid": "workspace:" + }, + "devDependencies": { + "@perspective-dev/esbuild-plugin": "workspace:", + "lightningcss": "catalog:" + } +} diff --git a/packages/anywidget/src/css/index.css b/packages/anywidget/src/css/index.css new file mode 100644 index 0000000000..acc9a5fd28 --- /dev/null +++ b/packages/anywidget/src/css/index.css @@ -0,0 +1,43 @@ +/* ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ + * ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ + * ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ + * ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ + * ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ + * ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ + * ┃ Copyright (c) 2017, the Perspective Authors. ┃ + * ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ + * ┃ This file is part of the Perspective library, distributed under the terms ┃ + * ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ + * ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + */ + +@import "@perspective-dev/viewer/dist/css/themes.css"; + +.PSPContainer { + overflow: auto; + padding-right: 5px; + padding-bottom: 5px; + height: 520px; + width: 100%; + flex: 1; + display: block; +} + +.jp-Notebook .PSPContainer { + resize: vertical; +} + +body[data-voila="voila"] .jp-OutputArea-output .PSPContainer, +body[data-vscode-theme-id] .cell-output-ipywidget-background .PSPContainer { + min-height: 520px; + height: 520px; +} + +.PSPContainer perspective-viewer[theme="Pro Light"] { + --psp-plugin--border: 1px solid #e0e0e0; +} + +.PSPContainer perspective-viewer { + display: block; + height: 100%; +} diff --git a/packages/anywidget/src/js/index.js b/packages/anywidget/src/js/index.js new file mode 100644 index 0000000000..c4d5b1c26a --- /dev/null +++ b/packages/anywidget/src/js/index.js @@ -0,0 +1,192 @@ +// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +// ┃ Copyright (c) 2017, the Perspective Authors. ┃ +// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +// ┃ This file is part of the Perspective library, distributed under the terms ┃ +// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import perspective from "@perspective-dev/client"; +import perspective_viewer from "@perspective-dev/viewer"; + +import server_wasm from "@perspective-dev/server/dist/wasm/perspective-server.wasm"; +import client_wasm from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"; + +import "@perspective-dev/viewer-datagrid"; +import "@perspective-dev/viewer-charts"; + +const ready = Promise.all([ + perspective_viewer.init_client(client_wasm), + perspective.init_server(server_wasm), +]); + +export { ready }; + +export async function worker() { + await ready; + return await perspective.worker(); +} + +const PERSISTENT_ATTRIBUTES = [ + "plugin", + "columns", + "group_by", + "split_by", + "aggregates", + "sort", + "filter", + "expressions", + "plugin_config", + "settings", + "theme", + "title", + "version", +]; + +function isEqual(a, b) { + if (a === b) return true; + if (typeof a != "object" || typeof b != "object" || a == null || b == null) + return false; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + if (keysA.length != keysB.length) return false; + for (const key of keysA) { + if (!keysB.includes(key)) return false; + if (typeof a[key] === "function" || typeof b[key] === "function") { + if (a[key].toString() != b[key].toString()) return false; + } else { + if (!isEqual(a[key], b[key])) return false; + } + } + return true; +} + +async function get_psp_wasm_module() { + let elem = customElements.get("perspective-viewer"); + if (!elem) { + await customElements.whenDefined("perspective-viewer"); + elem = customElements.get("perspective-viewer"); + } + return elem.__wasm_module__; +} + +async function render({ model, el }) { + await ready; + + el.classList.add("PSPContainer"); + const viewer = document.createElement("perspective-viewer"); + viewer.classList.add("PSPViewer"); + viewer.setAttribute("type", "application/psp+json"); + viewer.addEventListener( + "contextmenu", + (event) => event.stopPropagation(), + false, + ); + el.appendChild(viewer); + + const client_id = `${Math.random()}`; + const wasm_module = await get_psp_wasm_module(); + const psp_client = new wasm_module.Client( + async (binary_msg) => { + const buffer = binary_msg.slice().buffer; + model.send({ type: "binary_msg", client_id }, null, [buffer]); + }, + () => { + model.send({ type: "hangup", client_id }, null); + }, + ); + + const on_custom_msg = (msg, buffers) => { + if (msg.type === "binary_msg" && msg.client_id === client_id) { + const [dataview] = buffers; + psp_client.handle_response(dataview.buffer); + } + }; + model.on("msg:custom", on_custom_msg); + model.send({ type: "connect", client_id }, null); + + const binding_mode = model.get("binding_mode"); + const table_name = model.get("table_name"); + if (!table_name) { + throw new Error("table_name not set in model"); + } + + let load_complete = false; + const table_promise = psp_client.open_table(table_name).then(async (t) => { + if (binding_mode === "client-server") { + const local_client = await perspective.worker(); + const remote_view = await t.view(); + return await local_client.table(remote_view); + } else if (binding_mode === "server") { + return t; + } else { + throw new Error(`unknown binding mode: ${binding_mode}`); + } + }); + + await viewer.load(table_promise); + await viewer.restore( + Object.fromEntries(PERSISTENT_ATTRIBUTES.map((k) => [k, model.get(k)])), + ); + load_complete = true; + + const sync_to_python = async () => { + if (!load_complete) { + return; + } + const config = await viewer.save(); + for (const name of Object.keys(config)) { + let new_value = config[name]; + const current_value = model.get(name); + if (typeof new_value === "undefined") { + continue; + } + if ( + new_value && + typeof new_value === "string" && + name !== "plugin" && + name !== "theme" && + name !== "title" && + name !== "version" + ) { + new_value = JSON.parse(new_value); + } + if (new_value === null && name === "plugin_config") { + new_value = {}; + } + if (!isEqual(new_value, current_value)) { + model.set(name, new_value); + } + } + model.save_changes(); + }; + viewer.addEventListener("perspective-config-update", sync_to_python); + + const trait_listeners = PERSISTENT_ATTRIBUTES.map((name) => { + const cb = () => { + viewer.restore({ [name]: model.get(name) }); + }; + model.on(`change:${name}`, cb); + return [name, cb]; + }); + + return () => { + for (const [name, cb] of trait_listeners) { + model.off(`change:${name}`, cb); + } + model.off("msg:custom", on_custom_msg); + viewer.removeEventListener("perspective-config-update", sync_to_python); + psp_client.terminate(); + viewer.delete(); + if (viewer.parentNode === el) { + el.removeChild(viewer); + } + }; +} + +export default { render }; diff --git a/packages/jupyterlab/DEVELOPMENT.md b/packages/jupyterlab/DEVELOPMENT.md index cc16dc5fd8..9f636fb797 100644 --- a/packages/jupyterlab/DEVELOPMENT.md +++ b/packages/jupyterlab/DEVELOPMENT.md @@ -1,16 +1,29 @@ ## Build -As of March 2025, the build process is a little different from the jupyterlab -extension cookiecutter template. The build works as follows: +This package builds the **JupyterLab labextension (file renderers)**. -### labextension +Registers `Perspective` as a viewer for `.csv`, `.json`, and `.arrow` files in +the JupyterLab file browser. Pure JupyterLab plugin — no ipywidgets, no +`PerspectiveWidget`. -1. esbuild produces a bundle in `dist/esm` with `src/index.js` as its entrypoint -2. `jupyter labextension build` packages that bundle, which is read from the - `main` field in `package.json`, into `dist/cjs` -3. the `dist/cjs` output is copied to the perspective-python package's `.data` - directory. +1. esbuild produces a bundle in `dist/esm/perspective-jupyterlab.js` from + `src/js/index.js`. +2. `jupyter labextension build` packages that bundle (read from `main` in + `package.json`) into `dist/cjs/`. +3. `dist/cjs/` is copied to the `perspective-python` wheel data dir at + `perspective_python-*.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab/`. -This means running `jupyter labextension build` or `watch` out-of-band from the -build script won't rebuild the labextension on its own; the `labextension` step -runs on the output of the esbuild step. +This means running `jupyter labextension build` or `watch` out-of-band from +`build.mjs` won't rebuild on its own; it reads the esbuild output. + +The renderer lazy-loads the Perspective runtime from `@perspective-dev/anywidget` +on first file-open (custom-element registration is idempotent, so it coexists +with a `PerspectiveWidget` on the same page). + +## `PerspectiveWidget` (the AnyWidget bundle) + +The widget itself is **no longer built here** — it lives in the +`@perspective-dev/anywidget` package, which builds a single wasm-inlined ESM +bundle to `rust/perspective-python/perspective/widget/static/perspective-jupyter.{js,css}` +and is shipped in the `perspective-python` wheel (loaded by anywidget at widget +instantiation). See `packages/anywidget/`. diff --git a/packages/jupyterlab/build.mjs b/packages/jupyterlab/build.mjs index 9b460357db..a591bfd248 100644 --- a/packages/jupyterlab/build.mjs +++ b/packages/jupyterlab/build.mjs @@ -14,46 +14,17 @@ import { WasmPlugin } from "@perspective-dev/esbuild-plugin/wasm.js"; import { WorkerPlugin } from "@perspective-dev/esbuild-plugin/worker.js"; import { build } from "@perspective-dev/esbuild-plugin/build.js"; import * as path from "node:path"; -import { bundleAsync as bundleCss, composeVisitors } from "lightningcss"; +import { bundleAsync as bundleCss } from "lightningcss"; import * as fs from "node:fs"; import * as url from "node:url"; -// import { createRequire } from "node:module"; import { execSync } from "node:child_process"; import { resolveNPM, inlineUrlVisitor, } from "@perspective-dev/viewer/tools.mjs"; -// const _require = createRequire(import.meta.url); const __dirname = url.fileURLToPath(new URL(".", import.meta.url)).slice(0, -1); -const NBEXTENSION_PATH = path.resolve( - __dirname, - "..", - "..", - "python", - "perspective", - "perspective", - "nbextension", - "static", -); - -const TEST_BUILD = { - entryPoints: ["src/js/psp_widget.js"], - define: { - global: "window", - }, - plugins: [WasmPlugin(true), WorkerPlugin({ inline: true })], - globalName: "PerspectiveLumino", - format: "esm", - loader: { - ".html": "text", - ".ttf": "file", - ".css": "text", - }, - outfile: "dist/esm/lumino.js", -}; - const LAB_BUILD = { entryPoints: ["src/js/index.js"], define: { @@ -70,73 +41,41 @@ const LAB_BUILD = { outfile: "dist/esm/perspective-jupyterlab.js", }; -const NB_BUILDS = [ - // { - // entryPoints: ["src/js/notebook/extension.js"], - // define: { - // global: "window", - // }, - // plugins: [ - // WasmPlugin(true), - // WorkerPlugin({ inline: true }), - // AMDLoader([]), - // ], - // loader: { - // ".ttf": "file", - // ".css": "text", - // }, - // external: ["@jupyter*", "@lumino*"], - // format: "cjs", - // outfile: path.join(NBEXTENSION_PATH, "extension.js"), - // }, - // { - // entryPoints: ["src/js/notebook/index.js"], - // define: { - // global: "window", - // }, - // plugins: [ - // WasmPlugin(true), - // WorkerPlugin({ inline: true }), - // AMDLoader(["@jupyter-widgets/base"]), - // ], - // external: ["@jupyter*"], - // format: "cjs", - // loader: { - // ".ttf": "file", - // ".css": "text", - // }, - // outfile: path.join(NBEXTENSION_PATH, "index.js"), - // }, -]; - -const IS_TEST = process.argv.some((x) => x === "--test"); -const BUILD = IS_TEST - ? [LAB_BUILD, ...NB_BUILDS, TEST_BUILD] - : [LAB_BUILD, ...NB_BUILDS]; - -async function build_all() { - fs.mkdirSync("dist/css", { recursive: true }); - const filename = path.resolve(__dirname, "src/css/index.css"); +async function build_css(filename, outfile) { const { code } = await bundleCss({ filename, minify: true, visitor: inlineUrlVisitor(filename), resolver: resolveNPM(import.meta.url), }); + fs.mkdirSync(path.dirname(outfile), { recursive: true }); + fs.writeFileSync(outfile, code); +} + +async function build_all() { + fs.mkdirSync("dist/css", { recursive: true }); + + await build_css( + path.resolve(__dirname, "src/css/index.css"), + path.resolve(__dirname, "dist/css/perspective-jupyterlab.css"), + ); + + await build(LAB_BUILD).catch(() => process.exit(1)); - fs.writeFileSync("dist/css/perspective-jupyterlab.css", code); - await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); fs.cpSync("src/css", "dist/css/src", { recursive: true }); execSync("jupyter labextension build .", { stdio: "inherit", }); + fs.copyFileSync("install.json", "dist/install.json"); const pkg = JSON.parse(fs.readFileSync("../../package.json").toString()); const labext_dest = `../../rust/perspective-python/perspective_python-${pkg.version}.data/data/share/jupyter/labextensions/@perspective-dev/jupyterlab`; fs.cpSync("dist/cjs", labext_dest, { recursive: true }); - if (IS_TEST) { - fs.cpSync("test/arrow", "dist/esm", { recursive: true }); - } + fs.copyFileSync("install.json", path.join(labext_dest, "install.json")); + + // jlab_start.ts serves dist/esm as the JupyterLab root; widget.spec.mjs + // notebooks read test.arrow from cwd + fs.cpSync("test/arrow", "dist/esm", { recursive: true }); } build_all(); diff --git a/packages/jupyterlab/package.json b/packages/jupyterlab/package.json index b7e9356df9..f22070d39f 100644 --- a/packages/jupyterlab/package.json +++ b/packages/jupyterlab/package.json @@ -1,7 +1,7 @@ { "name": "@perspective-dev/jupyterlab", "version": "4.5.1", - "description": "A Jupyterlab extension for the Perspective library, designed to be used with perspective-python.", + "description": "JupyterLab file renderers for the Perspective library (CSV, JSON, Arrow). The PerspectiveWidget itself ships via the perspective-python wheel using anywidget.", "files": [ "dist/**/*", "src/**/*" @@ -20,7 +20,7 @@ "build": "node ./build.mjs", "clean": "node ./clean.mjs", "test:jupyter": "__JUPYTERLAB_PORT__=6538 npx playwright test --config ../../tools/test/playwright.config.ts -- --jupyter", - "test:jupyter:build": "node ./build.mjs --test" + "test:jupyter:build": "node ./build.mjs" }, "dependencies": { "@perspective-dev/viewer-charts": "workspace:", @@ -28,29 +28,20 @@ "@perspective-dev/viewer": "workspace:", "@perspective-dev/client": "workspace:", "@perspective-dev/server": "workspace:", - "@jupyter-widgets/base": ">2 <5", "@jupyterlab/application": ">2 <5", - "@lumino/application": "<3", "@lumino/widgets": "<3" }, "devDependencies": { + "@perspective-dev/anywidget": "workspace:", "@perspective-dev/esbuild-plugin": "workspace:", "@perspective-dev/test": "workspace:", "@jupyterlab/builder": "^4", - "lightningcss": "catalog:", - "copy-webpack-plugin": "~12", - "zx": "^8.1.8" + "lightningcss": "catalog:" }, "jupyterlab": { "webpackConfig": "./webpack.config.js", "extension": true, "outputDir": "./dist/cjs", - "sharedPackages": { - "@jupyter-widgets/base": { - "bundled": false, - "singleton": true - } - }, "discovery": { "server": { "base": { diff --git a/packages/jupyterlab/src/js/index.js b/packages/jupyterlab/src/js/index.js index 0f1ea99040..9735644d36 100644 --- a/packages/jupyterlab/src/js/index.js +++ b/packages/jupyterlab/src/js/index.js @@ -10,34 +10,6 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -import perspective from "@perspective-dev/client"; -import perspective_viewer from "@perspective-dev/viewer"; +import { PerspectiveRenderers } from "./renderer"; -import server_wasm from "@perspective-dev/server/dist/wasm/perspective-server.wasm"; -import client_wasm from "@perspective-dev/viewer/dist/wasm/perspective-viewer.wasm"; - -await Promise.all([ - perspective_viewer.init_client(client_wasm), - perspective.init_server(server_wasm), -]); - -export * from "./model"; -export * from "./version"; -export * from "./view"; -export * from "./widget"; - -import "@perspective-dev/viewer-datagrid"; -import "@perspective-dev/viewer-charts"; - -// NOTE: only expose the widget here -import { PerspectiveJupyterPlugin } from "./plugin"; - -let plugins = [PerspectiveJupyterPlugin]; - -// Conditionally import renderers if running in jupyterlab only -if (window && window._JUPYTERLAB) { - const { PerspectiveRenderers } = await import("./renderer"); - plugins.push(PerspectiveRenderers); -} - -export default plugins; +export default [PerspectiveRenderers]; diff --git a/packages/jupyterlab/src/js/model.js b/packages/jupyterlab/src/js/model.js deleted file mode 100644 index 9e84bc345b..0000000000 --- a/packages/jupyterlab/src/js/model.js +++ /dev/null @@ -1,43 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { DOMWidgetModel } from "@jupyter-widgets/base"; -import { PERSPECTIVE_VERSION } from "./version"; - -/** - * TODO: document - */ -export class PerspectiveModel extends DOMWidgetModel { - defaults() { - return { - ...super.defaults(), - _model_name: PerspectiveModel.model_name, - _model_module: PerspectiveModel.model_module, - _model_module_version: PerspectiveModel.model_module_version, - _view_name: PerspectiveModel.view_name, - _view_module: PerspectiveModel.view_module, - _view_module_version: PerspectiveModel.view_module_version, - }; - } -} - -PerspectiveModel.serializers = { - ...DOMWidgetModel.serializers, - // Add any extra serializers here -}; - -PerspectiveModel.model_name = "PerspectiveModel"; -PerspectiveModel.model_module = "@perspective-dev/jupyterlab"; -PerspectiveModel.model_module_version = PERSPECTIVE_VERSION; -PerspectiveModel.view_name = "PerspectiveView"; -PerspectiveModel.view_module = "@perspective-dev/jupyterlab"; -PerspectiveModel.view_module_version = PERSPECTIVE_VERSION; diff --git a/packages/jupyterlab/src/js/notebook/css.js b/packages/jupyterlab/src/js/notebook/css.js deleted file mode 100644 index 047af3216c..0000000000 --- a/packages/jupyterlab/src/js/notebook/css.js +++ /dev/null @@ -1,20 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import THEMES from "../../../dist/css/perspective-jupyterlab.css"; - -// Export the required load_ipython_extension -exports.load_css = () => { - const style = document.createElement("style"); - style.textContent = THEMES; - document.head.appendChild(style); -}; diff --git a/packages/jupyterlab/src/js/notebook/extension.js b/packages/jupyterlab/src/js/notebook/extension.js deleted file mode 100644 index 28fa544903..0000000000 --- a/packages/jupyterlab/src/js/notebook/extension.js +++ /dev/null @@ -1,32 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -/* eslint-disable no-underscore-dangle */ -import { load_css } from "./css"; - -// This file contains the javascript that is run when the notebook is loaded. -// It contains some requirejs configuration and the `load_ipython_extension` -// which is required for any notebook extension. -if (window.require) { - window.require.config({ - map: { - "*": { - "@perspective-dev/jupyterlab": - "nbextensions/@perspective-dev/jupyterlab/index", - }, - }, - }); -} - -exports.load_ipython_extension = () => { - load_css(); -}; diff --git a/packages/jupyterlab/src/js/notebook/index.js b/packages/jupyterlab/src/js/notebook/index.js deleted file mode 100644 index 2ee10787f2..0000000000 --- a/packages/jupyterlab/src/js/notebook/index.js +++ /dev/null @@ -1,26 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import "@perspective-dev/viewer-datagrid"; -import "@perspective-dev/viewer-charts"; - -import { load_css } from "./css"; -import { PerspectiveView } from "../view"; -import { PerspectiveModel } from "../model"; - -exports.PerspectiveModel = PerspectiveModel; -exports.PerspectiveView = PerspectiveView; - -// Run if in vs-code -if (window.vscIPyWidgets) { - load_css(); -} diff --git a/packages/jupyterlab/src/js/plugin.js b/packages/jupyterlab/src/js/plugin.js deleted file mode 100644 index b2646bbcc4..0000000000 --- a/packages/jupyterlab/src/js/plugin.js +++ /dev/null @@ -1,38 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { IJupyterWidgetRegistry } from "@jupyter-widgets/base"; -import { PerspectiveModel } from "./model"; -import { PerspectiveView } from "./view"; -import { PERSPECTIVE_VERSION } from "./version"; -const EXTENSION_ID = "@perspective-dev/jupyterlab"; - -/** - * PerspectiveJupyterPlugin Defines the Jupyterlab plugin, and registers `PerspectiveModel` and `PerspectiveView` - * to be called on initialization. - */ -export const PerspectiveJupyterPlugin = { - id: EXTENSION_ID, - // @ts-ignore - requires: [IJupyterWidgetRegistry], - activate: (app, registry) => { - registry.registerWidget({ - name: EXTENSION_ID, - version: PERSPECTIVE_VERSION, - exports: { - PerspectiveModel: PerspectiveModel, - PerspectiveView: PerspectiveView, - }, - }); - }, - autoStart: true, -}; diff --git a/packages/jupyterlab/src/js/psp_widget.js b/packages/jupyterlab/src/js/psp_widget.js deleted file mode 100644 index 5a37a61e96..0000000000 --- a/packages/jupyterlab/src/js/psp_widget.js +++ /dev/null @@ -1,199 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import "@perspective-dev/viewer"; -import { Widget } from "@lumino/widgets"; -import { MIME_TYPE, PSP_CLASS, PSP_CONTAINER_CLASS } from "./utils"; - -let _increment = 0; - -/** - * Class for perspective lumino widget. - * - * @class PerspectiveWidget (name) TODO: document - */ -export class PerspectiveWidget extends Widget { - constructor(name = "Perspective", elem, bindingMode) { - super({ - node: elem || document.createElement("div"), - }); - - this.bindingMode = bindingMode; - this._viewer = PerspectiveWidget.createNode(this.node); - this.title.label = name; - this.title.caption = `${name}`; - this.id = `${name}-` + _increment; - _increment += 1; - } - - /**********************/ - /* Lumino Overrides */ - /**********************/ - /** - * Lumino: after visible - * - */ - - onAfterShow(msg) { - this.viewer.resize(true); - super.onAfterShow(msg); - } - - onActivateRequest(msg) { - if (this.isAttached) { - this.viewer.focus(); - } - super.onActivateRequest(msg); - } - - async toggleConfig() { - await this.viewer.toggleConfig(); - } - - async save() { - return await this.viewer.save(); - } - - async restore(config) { - return await this.viewer.restore(config); - } - - /** - * Load a `perspective.table` into the viewer. - * - * @param table A `perspective.table` object. - */ - - async load(table) { - await this.viewer.load(table); - this._load_complete = true; - } - - /** - * Update the viewer with new data. - * - * @param data - */ - - async _update(data) { - const table = await this.viewer.getTable(true); - await table.update(data); - } - - /** - * Removes all rows from the viewer's table. Does not reset viewer state. - */ - - async clear() { - const table = await this.viewer.getTable(true); - await table.clear(); - } - - /** - * Replaces the data of the viewer's table with new data. New data must - * conform to the schema of the Table. - * - * @param data - */ - - async replace(data) { - const table = await this.viewer.getTable(true); - await table.replace(data); - } - - /** - * Deletes this element's data and clears it's internal state (but not its - * user state). This (or the underlying `perspective.table`'s equivalent - * method) must be called in order for its memory to be reclaimed. - */ - - async delete() { - await this.viewer.delete(); - } - - /** - * Returns a promise that resolves to the element's edit port ID, used - * internally when edits are made using datagrid in client/server mode. - */ - - async getEditPort() { - return await this.viewer.getEditPort(); - } - - async getTable() { - return await this.viewer.getTable(); - } - - /*************************************************************************** - * - * Getters - * - */ - /** - * Returns the underlying `PerspectiveViewer` instance. - * - * @returns {PerspectiveViewer} The widget's viewer instance. - */ - - get viewer() { - return this._viewer; - } - - /** - * Returns the name of the widget. - * - * @returns {string} the widget name - "Perspective" if not set by the user. - */ - - get name() { - return this.title.label; - } - - get selectable() { - return this.viewer.hasAttribute("selectable"); - } - - set selectable(row_selection) { - if (row_selection) { - this.viewer.setAttribute("selectable", ""); - } else { - this.viewer.removeAttribute("selectable"); - } - } - - static createNode(node) { - node.classList.add("p-Widget"); - node.classList.add(PSP_CONTAINER_CLASS); - const viewer = document.createElement("perspective-viewer"); - viewer.classList.add(PSP_CLASS); - viewer.setAttribute("type", MIME_TYPE); - while (node.lastChild) { - node.removeChild(node.lastChild); - } - - node.appendChild(viewer); - - // allow perspective's event handlers to do their work - viewer.addEventListener( - "contextmenu", - (event) => event.stopPropagation(), - false, - ); - - const div = document.createElement("div"); - div.style.setProperty("display", "flex"); - div.style.setProperty("flex-direction", "row"); - node.appendChild(div); - - return viewer; - } -} diff --git a/packages/jupyterlab/src/js/renderer.js b/packages/jupyterlab/src/js/renderer.js index 09d7d05111..f1429ba6c4 100644 --- a/packages/jupyterlab/src/js/renderer.js +++ b/packages/jupyterlab/src/js/renderer.js @@ -20,19 +20,20 @@ import { } from "@jupyterlab/apputils"; import { ABCWidgetFactory, DocumentWidget } from "@jupyterlab/docregistry"; -import { PerspectiveWidget } from "./psp_widget"; +import { Widget } from "@lumino/widgets"; -import perspective from "@perspective-dev/client"; +// The Perspective runtime lives in the anywidget bundle and is loaded lazily on +// first file-open (see `_update`), so the labextension doesn't load it at +// startup. Importing the bundle eagerly registers the custom elements +// (idempotent across bundle copies); `worker()` resolves its wasm from the +// registered `` element. +const runtime = () => import("@perspective-dev/anywidget"); -/** - * The name of the factories that creates widgets. - */ const FACTORY_CSV = "Perspective-CSV"; const FACTORY_JSON = "Perspective-JSON"; const FACTORY_ARROW = "Perspective-Arrow"; const RENDER_TIMEOUT = 1000; -// create here to reuse for exception handling const baddialog = () => { showDialog({ body: "Perspective could not render the data", @@ -46,10 +47,53 @@ const baddialog = () => { }); }; +class PerspectiveViewerWidget extends Widget { + constructor() { + const node = document.createElement("div"); + node.classList.add("PSPContainer"); + const viewer = document.createElement("perspective-viewer"); + viewer.classList.add("PSPViewer"); + viewer.addEventListener( + "contextmenu", + (event) => event.stopPropagation(), + false, + ); + node.appendChild(viewer); + super({ node }); + this._viewer = viewer; + } + + get viewer() { + return this._viewer; + } + + set theme(t) { + this._viewer.restore({ theme: t }); + } + + onAfterShow() { + this._viewer.resize(true); + } + + onActivateRequest() { + if (this.isAttached) { + this._viewer.focus(); + } + } + + dispose() { + if (this.isDisposed) { + return; + } + this._viewer.delete(); + super.dispose(); + } +} + export class PerspectiveDocumentWidget extends DocumentWidget { constructor(options, type = "csv") { super({ - content: new PerspectiveWidget("Perspective"), + content: new PerspectiveViewerWidget(), context: options.context, reveal: options.reveal, }); @@ -69,13 +113,18 @@ export class PerspectiveDocumentWidget extends DocumentWidget { } async _update() { + // Lazy-load the Perspective runtime bundle; importing it eagerly + // registers the custom elements (idempotent across the widget/renderer + // bundle copies) and `await ready` ensures wasm init + that + // `` is defined before the constructor's element is + // used to load a table. + const psp_runtime = await runtime(); + await psp_runtime.ready; try { let data; if (this._type === "csv") { - // load csv directly data = this._context.model.toString(); } else if (this._type === "arrow") { - // load arrow directly data = Uint8Array.from( atob(this._context.model.toString()), (c) => c.charCodeAt(0), @@ -83,32 +132,23 @@ export class PerspectiveDocumentWidget extends DocumentWidget { } else if (this._type === "json") { data = this._context.model.toJSON(); if (Array.isArray(data) && data.length > 0) { - // already is records form, load directly data = data; } else { - // Column-oriented or single records JSON - // don't handle for now, just need to implement - // a simple transform but we can't handle all - // cases throw "Not handled"; } } else { - // don't handle other mimetypes for now throw "Not handled"; } try { const table = await this._psp.viewer.getTable(); table.replace(data); } catch (e) { - // construct new table - this.worker = await perspective.worker(); + this.worker = await psp_runtime.worker(); const table_promise = this.worker.table(data); - // load data await this._psp.viewer.load(table_promise); const table = await this._psp.viewer.getTable(); - // create a flat view const view = await table.view(); view.on_update(async () => { if (this._type === "csv") { @@ -140,7 +180,6 @@ export class PerspectiveDocumentWidget extends DocumentWidget { throw e; } - // pickup theme from env this._psp.theme = document.body.getAttribute("data-jp-theme-light") === "false" ? "Pro Light" @@ -152,7 +191,9 @@ export class PerspectiveDocumentWidget extends DocumentWidget { this._monitor.dispose(); } - this.worker.terminate(); + if (this.worker) { + this.worker.terminate(); + } super.dispose(); } @@ -161,9 +202,6 @@ export class PerspectiveDocumentWidget extends DocumentWidget { } } -/** - * A widget factory for CSV widgets. - */ export class PerspectiveCSVFactory extends ABCWidgetFactory { createNewWidget(context) { return new PerspectiveDocumentWidget( @@ -175,9 +213,6 @@ export class PerspectiveCSVFactory extends ABCWidgetFactory { } } -/** - * A widget factory for JSON widgets. - */ export class PerspectiveJSONFactory extends ABCWidgetFactory { createNewWidget(context) { return new PerspectiveDocumentWidget( @@ -189,9 +224,6 @@ export class PerspectiveJSONFactory extends ABCWidgetFactory { } } -/** - * A widget factory for arrow widgets. - */ export class PerspectiveArrowFactory extends ABCWidgetFactory { createNewWidget(context) { return new PerspectiveDocumentWidget( @@ -203,10 +235,6 @@ export class PerspectiveArrowFactory extends ABCWidgetFactory { } } -/** - * Activate cssviewer extension for CSV files - */ - function activate(app, restorer, themeManager) { const factorycsv = new PerspectiveCSVFactory({ name: FACTORY_CSV, @@ -256,7 +284,6 @@ function activate(app, restorer, themeManager) { }); if (restorer) { - // Handle state restoration. void restorer.restore(trackercsv, { command: "docmanager:open", args: (widget) => ({ @@ -292,10 +319,7 @@ function activate(app, restorer, themeManager) { const ftjson = app.docRegistry.getFileType("json"); const ftarrow = app.docRegistry.getFileType("arrow"); factorycsv.widgetCreated.connect((sender, widget) => { - // Track the widget. void trackercsv.add(widget); - - // Notify the widget tracker if restore data needs to update. widget.context.pathChanged.connect(() => { void trackercsv.save(widget); }); @@ -307,10 +331,7 @@ function activate(app, restorer, themeManager) { }); factoryjson.widgetCreated.connect((sender, widget) => { - // Track the widget. void trackerjson.add(widget); - - // Notify the widget tracker if restore data needs to update. widget.context.pathChanged.connect(() => { void trackerjson.save(widget); }); @@ -322,10 +343,7 @@ function activate(app, restorer, themeManager) { }); factoryarrow.widgetCreated.connect((sender, widget) => { - // Track the widget. void trackerarrow.add(widget); - - // Notify the widget tracker if restore data needs to update. widget.context.pathChanged.connect(() => { void trackerarrow.save(widget); }); @@ -336,7 +354,6 @@ function activate(app, restorer, themeManager) { } }); - // Keep the themes up-to-date. const updateThemes = () => { const isLight = themeManager && themeManager.theme @@ -362,9 +379,6 @@ function activate(app, restorer, themeManager) { } } -/** - * The perspective extension for files - */ export const PerspectiveRenderers = { activate: activate, id: "@perspective-dev/jupyterlab-renderers", diff --git a/packages/jupyterlab/src/js/version.js b/packages/jupyterlab/src/js/version.js deleted file mode 100644 index 9cadda3aba..0000000000 --- a/packages/jupyterlab/src/js/version.js +++ /dev/null @@ -1,14 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -const pkg_json = require("../../package.json"); -export const PERSPECTIVE_VERSION = pkg_json.version; diff --git a/packages/jupyterlab/src/js/view.js b/packages/jupyterlab/src/js/view.js deleted file mode 100644 index 8351262278..0000000000 --- a/packages/jupyterlab/src/js/view.js +++ /dev/null @@ -1,420 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { DOMWidgetView } from "@jupyter-widgets/base"; -import { PerspectiveJupyterWidget } from "./widget"; - -import perspective from "@perspective-dev/client"; - -function isEqual(a, b) { - if (a === b) return true; - if (typeof a != "object" || typeof b != "object" || a == null || b == null) - return false; - - let keysA = Object.keys(a), - keysB = Object.keys(b); - - if (keysA.length != keysB.length) return false; - for (let key of keysA) { - if (!keysB.includes(key)) return false; - if (typeof a[key] === "function" || typeof b[key] === "function") { - if (a[key].toString() != b[key].toString()) return false; - } else { - if (!isEqual(a[key], b[key])) return false; - } - } - - return true; -} - -async function get_psp_wasm_module() { - let elem = customElements.get("perspective-viewer"); - if (!elem) { - await customElements.whenDefined("perspective-viewer"); - elem = customElements.get("perspective-viewer"); - } - - return elem.__wasm_module__; -} - -/** - * `PerspectiveView` defines the plugin's DOM and how the plugin interacts with - * the DOM. - */ -export class PerspectiveView extends DOMWidgetView { - #psp_client_id = `${Math.random()}`; - - _createElement() { - const bindingMode = this.model.get("binding_mode"); - this.luminoWidget = new PerspectiveJupyterWidget( - undefined, - this, - bindingMode, - ); - - // set up perspective_client - get_psp_wasm_module().then(async (wasm_module) => { - this.send({ - type: "connect", - client_id: this.psp_client_id, - }); - - const { Client } = wasm_module; - // Responses are fed to the client in the widget's msg:custom handler - this.perspective_client = new Client( - async (binary_msg) => { - const buffer = binary_msg.slice().buffer; - this.send( - { type: "binary_msg", client_id: this.psp_client_id }, - [buffer], - ); - }, - () => { - this.send({ - type: "hangup", - client_id: this.psp_client_id, - }); - }, - ); - - const tableName = this.model.get("table_name"); - if (!tableName) throw new Error("table_name not set in model"); - const table = this.perspective_client - .open_table(tableName) - .then(async (table) => { - if (bindingMode === "client-server") { - // TODO make this a global lazy singleton - const client = await perspective.worker(); - const remote_view = await table.view(); - const local_table = await client.table(remote_view); - return local_table; - } else if (bindingMode === "server") { - return table; - } else { - throw new Error(`unknown binding mode: ${bindingMode}`); - } - }); - - this.luminoWidget.load(table); - this._restore_from_model(); - }); - this._synchronize_state_dbg = (event) => { - console.log("perspective-config-update event", event); - this._synchronize_state(); - }; - this._synchronize_state = this._synchronize_state.bind(this); - - // add event handler to synchronize traitlet values - this.luminoWidget.viewer.addEventListener( - "perspective-config-update", - this._synchronize_state_dbg, - ); - - // bind toggle_editable to this - this._toggle_editable = this._toggle_editable.bind(this); - - // return the node against witch pWidget is bound - return this.luminoWidget.node; - } - - _setElement(el) { - if (this.el || el !== this.luminoWidget.node) { - // Do not allow the view to be reassigned to a different element. - throw new Error("Cannot reset the DOM element."); - } - this.el = this.luminoWidget.node; - } - - /** - * When state changes on the viewer DOM, apply it to the widget state. - * - * @param mutations - */ - - async _synchronize_state(event) { - if (!this.luminoWidget._load_complete) { - return; - } - - const config = await this.luminoWidget.viewer.save(); - - for (const name of Object.keys(config)) { - let new_value = config[name]; - - const current_value = this.model.get(name); - if (typeof new_value === "undefined") { - continue; - } - - if ( - new_value && - typeof new_value === "string" && - name !== "plugin" && - name !== "theme" && - name !== "title" && - name !== "version" - ) { - new_value = JSON.parse(new_value); - } - - if (new_value === null && name === "plugin_config") { - new_value = {}; - } - - if (!isEqual(new_value, current_value)) { - this.model.set(name, new_value); - } - } - - // propagate changes back to Python - this.touch(); - } - - get psp_client_id() { - return this.#psp_client_id; - } - - /** - * Attach event handlers to the model for state changes in order to - * reflect them back to the DOM. - */ - - render() { - super.render(); - this.model.on("msg:custom", this._handle_message, this); - this.model.on("change:plugin", this.plugin_changed, this); - this.model.on("change:columns", this.columns_changed, this); - this.model.on("change:group_by", this.group_by_changed, this); - this.model.on("change:split_by", this.split_by_changed, this); - this.model.on("change:aggregates", this.aggregates_changed, this); - this.model.on("change:sort", this.sort_changed, this); - this.model.on("change:filter", this.filter_changed, this); - this.model.on("change:expressions", this.expressions_changed, this); - this.model.on("change:plugin_config", this.plugin_config_changed, this); - this.model.on("change:theme", this.theme_changed, this); - this.model.on("change:settings", this.settings_changed, this); - this.model.on("change:title", this.title_changed, this); - this.model.on("change:table_name", this.table_name_changed, this); - } - - /** - * Handle messages from the Python PerspectiveViewer instance. - */ - _handle_message(msg, buffers) { - if (msg.type === "binary_msg" && msg.client_id === this.psp_client_id) { - const [dataview] = buffers; - this.perspective_client.handle_response(dataview.buffer); - } - } - - get client_worker() { - if (!this._client_worker) { - this._client_worker = perspective.worker(); - } - return this._client_worker; - } - - async _restore_from_model() { - await this.luminoWidget.restore({ - plugin: this.model.get("plugin"), - columns: this.model.get("columns"), - group_by: this.model.get("group_by"), - split_by: this.model.get("split_by"), - aggregates: this.model.get("aggregates"), - sort: this.model.get("sort"), - filter: this.model.get("filter"), - expressions: this.model.get("expressions"), - plugin_config: this.model.get("plugin_config"), - theme: this.model.get("theme"), - settings: this.model.get("settings"), - title: this.model.get("title"), - version: this.model.get("version"), - }); - } - - // XXX(tom): haven't looked at this, needs testing. used in client-server mode - async _toggle_editable() { - // Need to await the table and get the instance - // separately as load() only takes a promise - // to a table and not the instance itself. - const table = await this.luminoWidget.getTable(); - - // Setup ports in advance - if (!this._client_edit_port) { - this._client_edit_port = await this.luminoWidget.getEditPort(); - } - - // if (!this._kernel_edit_port) { - // this._kernel_edit_port = await this._kernel_table.make_port(); - // } - - const { plugin_config } = await this.luminoWidget.viewer.save(); - if (plugin_config?.editable) { - // TODO only evaluated during initial load. - // Toggling from python after initial load won't - // cause edits to propagate - - // Allow edits from the client Perspective to - // feed back to the kernel. - - // When the client updates, if the update - // comes through the edit port then forward - // it to the server. - this._client_view_update_callback = (updated) => { - if (updated.port_id === this._client_edit_port) { - this._kernel_table.update(updated.delta, { - port_id: this._kernel_edit_port, - }); - } - }; - - // If the server updates, and the edit is - // not coming from the server edit port, - // then synchronize state with the client. - this._kernel_view_update_callback = (updated) => { - if (updated.port_id !== this._kernel_edit_port) { - table.update(updated.delta); // any port, we dont care - } - }; - } else { - // ignore - this._client_view_update_callback = () => {}; - - // Load the table and mirror updates from the - // kernel. - this._kernel_view_update_callback = (updated) => - table.update(updated.delta); - } - - if (this._client_view) { - // NOTE: if `plugin_config_changed` called before - // `_handle_load_message`, this will be undefined - // Ignore, as `_handle_load_message` is sure to - // follow. - this._client_view.on_update( - (updated) => this._client_view_update_callback(updated), - { mode: "row" }, - ); - } - - // this._kernel_view.on_update( - // (updated) => this._kernel_view_update_callback(updated), - // { mode: "row" } - // ); - } - - /** - * When the View is removed after the widget terminates, clean up the - * client viewer and Web Worker. - */ - - remove() { - // Delete the but do not terminate the shared - // worker as it is shared across other widgets. - this.perspective_client.terminate(); // invokes the close callback we wired up in constructor - this.luminoWidget.delete(); - this.luminoWidget.viewer.removeEventListener( - "perspective-config-update", - this._synchronize_state_dbg, - ); - } - - /** - * When traitlets are updated in python, update the corresponding value on - * the front-end viewer. `client` and `server` are not included, as they - * are not properties in ``. - */ - - plugin_changed() { - this.luminoWidget.restore({ - plugin: this.model.get("plugin"), - }); - } - - columns_changed() { - this.luminoWidget.restore({ - columns: this.model.get("columns"), - }); - } - - group_by_changed() { - this.luminoWidget.restore({ - group_by: this.model.get("group_by"), - }); - } - - split_by_changed() { - this.luminoWidget.restore({ - split_by: this.model.get("split_by"), - }); - } - - aggregates_changed() { - this.luminoWidget.restore({ - aggregates: this.model.get("aggregates"), - }); - } - - sort_changed() { - this.luminoWidget.restore({ - sort: this.model.get("sort"), - }); - } - - filter_changed() { - this.luminoWidget.restore({ - filter: this.model.get("filter"), - }); - } - - expressions_changed() { - this.luminoWidget.restore({ - expressions: this.model.get("expressions"), - }); - } - - plugin_config_changed() { - this.luminoWidget.restore({ - plugin_config: this.model.get("plugin_config"), - }); - this._toggle_editable(); - } - - theme_changed() { - this.luminoWidget.restore({ - theme: this.model.get("theme"), - }); - } - - settings_changed() { - this.luminoWidget.restore({ - settings: this.model.get("settings"), - }); - } - - title_changed() { - this.luminoWidget.restore({ - title: this.model.get("title"), - }); - } - - version_changed() { - this.luminoWidget.restore({ - version: this.model.get("version"), - }); - } - - table_name_changed() { - // nop - // XXX(tom): we may want to re-load the viewer in this instance - } -} diff --git a/packages/jupyterlab/src/js/widget.js b/packages/jupyterlab/src/js/widget.js deleted file mode 100644 index afb24f0137..0000000000 --- a/packages/jupyterlab/src/js/widget.js +++ /dev/null @@ -1,54 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { PerspectiveWidget } from "./psp_widget"; - -/** - * PerspectiveJupyterWidget is the ipywidgets front-end for the Perspective Jupyterlab plugin. - */ -export class PerspectiveJupyterWidget extends PerspectiveWidget { - constructor(name = "Perspective", view, bindingMode) { - super(name, view.el, bindingMode); - this._view = view; - } - - /** - * Process the lumino message. - * - * Any custom lumino widget used inside a Jupyter widget should override - * the processMessage function like this. - */ - - processMessage(msg) { - super.processMessage(msg); - this._view.processLuminoMessage(msg); - } - - /** - * Dispose the widget. - * - * This causes the view to be destroyed as well with 'remove' - */ - - dispose() { - if (this.isDisposed) { - return; - } - - super.dispose(); - if (this._view) { - this._view.remove(); - } - - this._view = null; - } -} diff --git a/packages/jupyterlab/test/html/resize.html b/packages/jupyterlab/test/html/resize.html deleted file mode 100644 index 70aacd95dc..0000000000 --- a/packages/jupyterlab/test/html/resize.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - -
hello
- - diff --git a/packages/jupyterlab/test/js/resize.spec.mjs b/packages/jupyterlab/test/js/resize.spec.mjs deleted file mode 100644 index 7d85434dbc..0000000000 --- a/packages/jupyterlab/test/js/resize.spec.mjs +++ /dev/null @@ -1,114 +0,0 @@ -// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ -// ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ -// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ -// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ -// ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ -// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ -// ┃ Copyright (c) 2017, the Perspective Authors. ┃ -// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ -// ┃ This file is part of the Perspective library, distributed under the terms ┃ -// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ -// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ - -import { test } from "@perspective-dev/test"; -import { compareContentsToSnapshot } from "@perspective-dev/test"; - -test.beforeEach(async ({ page }) => { - await page.goto( - "/node_modules/@perspective-dev/jupyterlab/test/html/resize.html", - ); - await page.evaluate(async () => { - while (!window["__TEST_PERSPECTIVE_READY__"]) { - await new Promise((x) => setTimeout(x, 10)); - } - }); -}); - -test.describe("JupyterLab resize", () => { - test("Config should be hidden by default", async ({ page }) => { - // Snapshot is viewer contents - const contents = await page.evaluate(async () => { - await window.__WIDGET__.viewer.getTable(); - await window.__WIDGET__.viewer.flush(); - - // Linux returns ever-so-slightly different auto width - // column values so we need to strip these. - for (const elem of document.querySelectorAll( - "perspective-viewer *", - )) { - elem.removeAttribute("style"); - } - - return window.__WIDGET__.viewer.innerHTML; - }); - - await compareContentsToSnapshot(contents, [ - "jupyterlab-resize-config-hidden.txt", - ]); - }); - - test("Resize the container causes the widget to resize", async ({ - page, - }) => { - await page.evaluate(async () => { - await document.querySelector("perspective-viewer").toggleConfig(); - await document.querySelector("perspective-viewer").getTable(); - }); - - await page.evaluate(async () => { - document - .querySelector(".PSPContainer") - .setAttribute( - "style", - "position:absolute;top:0;left:0;width:300px;height:300px", - ); - - await document.querySelector("perspective-viewer").resize(); - }); - - // Snapshot is viewer contents - const contents = await page.evaluate(async () => { - document.querySelector(".PSPContainer").style = - "position:absolute;top:0;left:0;width:800px;height:600px"; - await document.querySelector("perspective-viewer").resize(); - - for (const elem of document.querySelectorAll( - "perspective-viewer *", - )) { - elem.removeAttribute("style"); - } - - return window.__WIDGET__.viewer.innerHTML; - }); - - await compareContentsToSnapshot(contents, [ - "jupyterlab-resize-config-shown.txt", - ]); - }); - - test("group_by traitlet works", async ({ page }) => { - await page.evaluate(async () => { - await document.querySelector("perspective-viewer").toggleConfig(); - await document.querySelector("perspective-viewer").getTable(); - await document.querySelector("perspective-viewer").flush(); - }); - - // Snapshot is datagrid contents - const contents = await page.evaluate(async () => { - await window.__WIDGET__.restore({ group_by: ["State"] }); - for (const elem of document.querySelectorAll( - "perspective-viewer *", - )) { - elem.removeAttribute("style"); - } - const datagrid = window.__WIDGET__.viewer.querySelector( - "perspective-viewer-datagrid", - ); - return datagrid.shadowRoot.innerHTML; - }); - - await compareContentsToSnapshot(contents, [ - "jupyterlab-resize-group-by-traitlet.txt", - ]); - }); -}); diff --git a/packages/jupyterlab/webpack.config.js b/packages/jupyterlab/webpack.config.js index 8cb8152ac7..79762765d4 100644 --- a/packages/jupyterlab/webpack.config.js +++ b/packages/jupyterlab/webpack.config.js @@ -10,15 +10,8 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -const CopyPlugin = require("copy-webpack-plugin"); - module.exports = { experiments: { topLevelAwait: true, }, - plugins: [ - new CopyPlugin({ - patterns: [{ from: "./install.json", to: "../install.json" }], - }), - ], }; diff --git a/packages/viewer-charts/src/ts/index.ts b/packages/viewer-charts/src/ts/index.ts index 2fba320c3c..a722695d45 100644 --- a/packages/viewer-charts/src/ts/index.ts +++ b/packages/viewer-charts/src/ts/index.ts @@ -23,17 +23,17 @@ export function register(...plugin_names: string[]) { : CHARTS.map((chart) => chart.name), ); + const already_registered: string[] = []; + CHARTS.forEach((chart) => { if (plugins.has(chart.name)) { const tagName = `perspective-viewer-charts-${chart.tag}`; - // Each registered tag is a thin subclass that pins - // `_chartType` so `draw()` / `save()` / etc. know which - // `ChartTypeConfig` they're driving. The chart impl - // class itself lives in the worker bundle — only - // `ChartTypeConfig.tag` crosses the host/renderer - // boundary, and the renderer constructs the impl from - // its own `CHART_IMPLS` registry. + if (customElements.get(tagName)) { + already_registered.push(tagName); + return; + } + customElements.define( tagName, class extends HTMLPerspectiveViewerWebGLPluginElement { @@ -48,6 +48,12 @@ export function register(...plugin_names: string[]) { }); } }); + + if (already_registered.length > 0) { + console.warn( + `viewer-charts plugins already registered (${already_registered.length}); skipping duplicate registration (Perspective was loaded more than once on this page).`, + ); + } } register(); diff --git a/packages/viewer-datagrid/src/ts/index.ts b/packages/viewer-datagrid/src/ts/index.ts index e13416f500..7f0cce944d 100644 --- a/packages/viewer-datagrid/src/ts/index.ts +++ b/packages/viewer-datagrid/src/ts/index.ts @@ -25,6 +25,13 @@ interface PerspectiveViewerConstructor { } async function _register_element(): Promise { + if (customElements.get("perspective-viewer-datagrid")) { + console.warn( + 'Custom element "perspective-viewer-datagrid" is already registered; skipping duplicate registration (Perspective was loaded more than once on this page).', + ); + return; + } + customElements.define( "perspective-viewer-datagrid-toolbar", HTMLPerspectiveViewerDatagridToolbarElement, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d9f82b7d17..0b7729719e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -656,6 +656,31 @@ importers: specifier: 'catalog:' version: 5.1.4(webpack@5.102.1) + packages/anywidget: + dependencies: + '@perspective-dev/client': + specifier: 'workspace:' + version: link:../../rust/perspective-js + '@perspective-dev/server': + specifier: 'workspace:' + version: link:../../rust/perspective-server + '@perspective-dev/viewer': + specifier: 'workspace:' + version: link:../../rust/perspective-viewer + '@perspective-dev/viewer-charts': + specifier: 'workspace:' + version: link:../viewer-charts + '@perspective-dev/viewer-datagrid': + specifier: 'workspace:' + version: link:../viewer-datagrid + devDependencies: + '@perspective-dev/esbuild-plugin': + specifier: 'workspace:' + version: link:../../tools/esbuild-plugin + lightningcss: + specifier: 'catalog:' + version: 1.32.0 + packages/cli: dependencies: '@perspective-dev/client': @@ -686,15 +711,9 @@ importers: packages/jupyterlab: dependencies: - '@jupyter-widgets/base': - specifier: '>2 <5' - version: 4.1.7(react@18.3.1) '@jupyterlab/application': specifier: '>2 <5' version: 4.4.10(react@18.3.1) - '@lumino/application': - specifier: <3 - version: 2.4.4 '@lumino/widgets': specifier: <3 version: 2.7.1 @@ -717,21 +736,18 @@ importers: '@jupyterlab/builder': specifier: ^4 version: 4.4.10 + '@perspective-dev/anywidget': + specifier: 'workspace:' + version: link:../anywidget '@perspective-dev/esbuild-plugin': specifier: 'workspace:' version: link:../../tools/esbuild-plugin '@perspective-dev/test': specifier: 'workspace:' version: link:../../tools/test - copy-webpack-plugin: - specifier: ~12 - version: 12.0.2(webpack@5.102.1(webpack-cli@5.1.4)) lightningcss: specifier: 'catalog:' version: 1.32.0 - zx: - specifier: ^8.1.8 - version: 8.8.5 packages/react: dependencies: @@ -1796,9 +1812,6 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jupyter-widgets/base@4.1.7': - resolution: {integrity: sha512-cWg0Bb+QKmyHPnCpvF+/3u+ZU0jkTQ62qGr56ReujzCCIIRoXo3GP81TdzzrDTGM9tTZP/i91sUyC+7Od8b4Ow==} - '@jupyter/react-components@0.16.7': resolution: {integrity: sha512-BKIPkJ9V011uhtdq1xBOu2M3up59CqsRbDS4aq8XhnHR4pwqfRV6k6irE5YBOETCoIwWZZ5RZO+cJcZ3DcsT5A==} @@ -1921,18 +1934,6 @@ packages: '@microsoft/fast-web-utilities@5.4.1': resolution: {integrity: sha512-ReWYncndjV3c8D8iq9tp7NcFNc1vbVHvcBFPME2nNFKNbS1XCesYZGlIlf3ot5EmuOXPlrzUHOWzQ2vFpIkqDg==} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@octokit/app@12.0.7': resolution: {integrity: sha512-NqgLlaaf7Yy1s5ghhiiBRGzstICpBYnVX5ce3Klk3iKaGeXJDBLVyrJ6e6sYOiTXolFK56Nx5QWS6oUBgP6rSw==} @@ -2235,10 +2236,6 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} - '@sindresorhus/merge-streams@2.3.0': - resolution: {integrity: sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==} - engines: {node: '>=18'} - '@swc/helpers@0.5.17': resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} @@ -2260,9 +2257,6 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/backbone@1.4.23': - resolution: {integrity: sha512-B/hN/DAJdWFOusEkEoa5xgfVuxJJPOR/6JQ2uwURPDyKL24PuC76IR6EcSRJ6lvGWtxHRQYMJQhJYm6KpMDtGQ==} - '@types/btoa-lite@1.0.2': resolution: {integrity: sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==} @@ -2287,9 +2281,6 @@ packages: '@types/html-minifier-terser@6.1.0': resolution: {integrity: sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==} - '@types/jquery@3.5.33': - resolution: {integrity: sha512-SeyVJXlCZpEki5F0ghuYe+L+PprQta6nRZqhONt9F13dWBtR/ftoaIbdRQ7cis7womE+X2LKhsDdDtkkDhJS6g==} - '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} @@ -2328,18 +2319,12 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} - '@types/sizzle@2.3.10': - resolution: {integrity: sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==} - '@types/source-list-map@0.1.6': resolution: {integrity: sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==} '@types/stoppable@1.1.3': resolution: {integrity: sha512-7wGKIBJGE4ZxFjk9NkjAxZMLlIXroETqP1FJCdoSvKmEznwmBxQFmTB1dsCkAvVcNemuSZM5qkkd9HE/NL2JTw==} - '@types/underscore@1.13.0': - resolution: {integrity: sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==} - '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -2634,9 +2619,6 @@ packages: react-native-b4a: optional: true - backbone@1.2.3: - resolution: {integrity: sha512-1/eXj4agG79UDN7TWnZXcGD6BJrBwLZKCX7zYcBIy9jWf4mrtVkw7IE1VOYFnrKahsmPF9L55Tib9IQRvk027w==} - balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -2723,10 +2705,6 @@ packages: resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} engines: {node: 18 || 20 || >=22} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - browserslist@4.27.0: resolution: {integrity: sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2889,12 +2867,6 @@ packages: copy-anything@2.0.6: resolution: {integrity: sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==} - copy-webpack-plugin@12.0.2: - resolution: {integrity: sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==} - engines: {node: '>= 18.12.0'} - peerDependencies: - webpack: ^5.1.0 - core-js@3.46.0: resolution: {integrity: sha512-vDMm9B0xnqqZ8uSBpZ8sNtRtOdmfShrvT6h2TuQGLs0Is+cR0DYbj/KWP6ALVNbWPpqA/qPLoOuppJN07humpA==} @@ -3245,10 +3217,6 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -3262,9 +3230,6 @@ packages: resolution: {integrity: sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==} engines: {node: '>= 4.9.1'} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fd-slicer@1.1.0: resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} @@ -3284,10 +3249,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - find-replace@3.0.0: resolution: {integrity: sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==} engines: {node: '>=4.0.0'} @@ -3409,10 +3370,6 @@ packages: resolution: {integrity: sha512-22pvDWt2hMPfL3UF6lWcZpP+VIwBekJyj6xyb1DpeSALJm+n/0gI9lWD30kvA/h3bgPqYeAX7xGONzmyHrSfqQ==} engines: {node: '>= 6'} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -3436,10 +3393,6 @@ packages: resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} engines: {node: '>= 0.4'} - globby@14.1.0: - resolution: {integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA==} - engines: {node: '>=18'} - gopd@1.2.0: resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} engines: {node: '>= 0.4'} @@ -3700,10 +3653,6 @@ packages: resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} engines: {node: '>= 0.4'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-plain-object@2.0.4: resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} engines: {node: '>=0.10.0'} @@ -3779,9 +3728,6 @@ packages: resolution: {integrity: sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==} engines: {node: '>= 0.6.0'} - jquery@3.7.1: - resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} - js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -4081,14 +4027,6 @@ packages: merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - microtime@3.1.1: resolution: {integrity: sha512-to1r7o24cDsud9IhN6/8wGmMx5R2kT0w2Xwm5okbYI3d1dk6Xv0m+Z+jg2vS9pt+ocgQHTCtgs/YuyJhySzxNg==} engines: {node: '>= 14.13.0'} @@ -4202,10 +4140,6 @@ packages: normalize-package-data@2.5.0: resolution: {integrity: sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - npm-run-all@4.1.5: resolution: {integrity: sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==} engines: {node: '>= 4'} @@ -4332,20 +4266,12 @@ packages: resolution: {integrity: sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==} engines: {node: '>=4'} - path-type@6.0.0: - resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} - engines: {node: '>=18'} - pend@1.2.0: resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} @@ -4500,9 +4426,6 @@ packages: querystringify@2.2.0: resolution: {integrity: sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - randombytes@2.1.0: resolution: {integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==} @@ -4583,10 +4506,6 @@ packages: engines: {node: '>= 0.4'} hasBin: true - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.52.5: resolution: {integrity: sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -4596,9 +4515,6 @@ packages: resolution: {integrity: sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==} engines: {node: '>=0.12.0'} - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} - rxjs@7.8.2: resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} @@ -4722,10 +4638,6 @@ packages: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} - slash@5.1.0: - resolution: {integrity: sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==} - engines: {node: '>=14.16'} - smart-buffer@4.2.0: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} @@ -4900,10 +4812,6 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} @@ -4993,19 +4901,12 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} - underscore@1.13.7: - resolution: {integrity: sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==} - undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - unicorn-magic@0.3.0: - resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} - engines: {node: '>=18'} - union@0.5.0: resolution: {integrity: sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==} engines: {node: '>= 0.8.0'} @@ -5979,23 +5880,6 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jupyter-widgets/base@4.1.7(react@18.3.1)': - dependencies: - '@jupyterlab/services': 7.4.10(react@18.3.1) - '@lumino/coreutils': 2.2.1 - '@lumino/messaging': 2.0.3 - '@lumino/widgets': 2.7.1 - '@types/backbone': 1.4.23 - '@types/lodash': 4.17.20 - backbone: 1.2.3 - base64-js: 1.5.1 - jquery: 3.7.1 - lodash: 4.17.21 - transitivePeerDependencies: - - bufferutil - - react - - utf-8-validate - '@jupyter/react-components@0.16.7': dependencies: '@jupyter/web-components': 0.16.7 @@ -6087,23 +5971,23 @@ snapshots: '@lumino/widgets': 2.7.1 ajv: 8.17.1 commander: 9.5.0 - css-loader: 6.11.0(webpack@5.102.1) + css-loader: 6.11.0(webpack@5.102.1(webpack-cli@5.1.4)) duplicate-package-checker-webpack-plugin: 3.0.0 fs-extra: 10.1.0 glob: 7.1.7 - license-webpack-plugin: 2.3.21(webpack@5.102.1) - mini-css-extract-plugin: 2.9.4(webpack@5.102.1) + license-webpack-plugin: 2.3.21(webpack@5.102.1(webpack-cli@5.1.4)) + mini-css-extract-plugin: 2.9.4(webpack@5.102.1(webpack-cli@5.1.4)) mini-svg-data-uri: 1.4.4 path-browserify: 1.0.1 process: 0.11.10 - source-map-loader: 1.0.2(webpack@5.102.1) - style-loader: 3.3.4(webpack@5.102.1) + source-map-loader: 1.0.2(webpack@5.102.1(webpack-cli@5.1.4)) + style-loader: 3.3.4(webpack@5.102.1(webpack-cli@5.1.4)) supports-color: 7.2.0 terser-webpack-plugin: 5.3.14(webpack@5.102.1) webpack: 5.102.1(webpack-cli@5.1.4) webpack-cli: 5.1.4(webpack@5.102.1) webpack-merge: 5.10.0 - worker-loader: 3.0.8(webpack@5.102.1) + worker-loader: 3.0.8(webpack@5.102.1(webpack-cli@5.1.4)) transitivePeerDependencies: - '@rspack/core' - '@swc/core' @@ -6388,18 +6272,6 @@ snapshots: dependencies: exenv-es6: 1.1.1 - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@octokit/app@12.0.7': dependencies: '@octokit/auth-app': 3.6.1 @@ -6891,8 +6763,6 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} - '@sindresorhus/merge-streams@2.3.0': {} - '@swc/helpers@0.5.17': dependencies: tslib: 2.8.1 @@ -6922,11 +6792,6 @@ snapshots: dependencies: '@babel/types': 7.28.5 - '@types/backbone@1.4.23': - dependencies: - '@types/jquery': 3.5.33 - '@types/underscore': 1.13.0 - '@types/btoa-lite@1.0.2': {} '@types/command-line-args@5.2.3': {} @@ -6951,10 +6816,6 @@ snapshots: '@types/html-minifier-terser@6.1.0': {} - '@types/jquery@3.5.33': - dependencies: - '@types/sizzle': 2.3.10 - '@types/json-schema@7.0.15': {} '@types/jsonwebtoken@9.0.10': @@ -6993,16 +6854,12 @@ snapshots: dependencies: csstype: 3.1.3 - '@types/sizzle@2.3.10': {} - '@types/source-list-map@0.1.6': {} '@types/stoppable@1.1.3': dependencies: '@types/node': 24.9.1 - '@types/underscore@1.13.0': {} - '@types/unist@3.0.3': {} '@types/webpack-sources@0.1.12': @@ -7374,10 +7231,6 @@ snapshots: b4a@1.7.3: {} - backbone@1.2.3: - dependencies: - underscore: 1.13.7 - balanced-match@1.0.2: {} balanced-match@4.0.4: {} @@ -7419,7 +7272,8 @@ snapshots: bare-path: 3.0.0 optional: true - base64-js@1.5.1: {} + base64-js@1.5.1: + optional: true baseline-browser-mapping@2.8.20: {} @@ -7452,10 +7306,6 @@ snapshots: dependencies: balanced-match: 4.0.4 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - browserslist@4.27.0: dependencies: baseline-browser-mapping: 2.8.20 @@ -7623,16 +7473,6 @@ snapshots: is-what: 3.14.1 optional: true - copy-webpack-plugin@12.0.2(webpack@5.102.1(webpack-cli@5.1.4)): - dependencies: - fast-glob: 3.3.3 - glob-parent: 6.0.2 - globby: 14.1.0 - normalize-path: 3.0.0 - schema-utils: 4.3.3 - serialize-javascript: 6.0.2 - webpack: 5.102.1(webpack-cli@5.1.4) - core-js@3.46.0: {} corser@2.0.1: {} @@ -7660,7 +7500,7 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 - css-loader@6.11.0(webpack@5.102.1): + css-loader@6.11.0(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) postcss: 8.5.6 @@ -8091,14 +7931,6 @@ snapshots: fast-fifo@1.3.2: {} - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} @@ -8107,10 +7939,6 @@ snapshots: fastest-levenshtein@1.0.16: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - fd-slicer@1.1.0: dependencies: pend: 1.2.0 @@ -8125,10 +7953,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - find-replace@3.0.0: dependencies: array-back: 3.1.0 @@ -8255,10 +8079,6 @@ snapshots: make-array: 1.0.5 util.inherits: 1.0.3 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -8290,15 +8110,6 @@ snapshots: define-properties: 1.2.1 gopd: 1.2.0 - globby@14.1.0: - dependencies: - '@sindresorhus/merge-streams': 2.3.0 - fast-glob: 3.3.3 - ignore: 7.0.5 - path-type: 6.0.0 - slash: 5.1.0 - unicorn-magic: 0.3.0 - gopd@1.2.0: {} graceful-fs@4.2.11: {} @@ -8576,8 +8387,6 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 - is-number@7.0.0: {} - is-plain-object@2.0.4: dependencies: isobject: 3.0.1 @@ -8649,8 +8458,6 @@ snapshots: jmespath@0.16.0: optional: true - jquery@3.7.1: {} - js-tokens@4.0.0: {} js-yaml@3.14.1: @@ -8766,7 +8573,7 @@ snapshots: dependencies: isomorphic.js: 0.2.5 - license-webpack-plugin@2.3.21(webpack@5.102.1): + license-webpack-plugin@2.3.21(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: '@types/webpack-sources': 0.1.12 webpack-sources: 1.4.3 @@ -8930,13 +8737,6 @@ snapshots: merge-stream@2.0.0: {} - merge2@1.4.1: {} - - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - microtime@3.1.1: dependencies: node-addon-api: 5.1.0 @@ -8950,7 +8750,7 @@ snapshots: mime@1.6.0: {} - mini-css-extract-plugin@2.9.4(webpack@5.102.1): + mini-css-extract-plugin@2.9.4(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: schema-utils: 4.3.3 tapable: 2.3.0 @@ -9028,8 +8828,6 @@ snapshots: semver: 5.7.2 validate-npm-package-license: 3.0.4 - normalize-path@3.0.0: {} - npm-run-all@4.1.5: dependencies: ansi-styles: 3.2.1 @@ -9180,14 +8978,10 @@ snapshots: dependencies: pify: 3.0.0 - path-type@6.0.0: {} - pend@1.2.0: {} picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.3: {} pidtree@0.3.1: {} @@ -9359,8 +9153,6 @@ snapshots: querystringify@2.2.0: {} - queue-microtask@1.2.3: {} - randombytes@2.1.0: dependencies: safe-buffer: 5.2.1 @@ -9445,8 +9237,6 @@ snapshots: path-parse: 1.0.7 supports-preserve-symlinks-flag: 1.0.0 - reusify@1.1.0: {} - rollup@4.52.5: dependencies: '@types/estree': 1.0.8 @@ -9477,10 +9267,6 @@ snapshots: run-async@4.0.6: {} - run-parallel@1.2.0: - dependencies: - queue-microtask: 1.2.3 - rxjs@7.8.2: dependencies: tslib: 2.8.1 @@ -9630,8 +9416,6 @@ snapshots: signal-exit@4.1.0: {} - slash@5.1.0: {} - smart-buffer@4.2.0: {} socks-proxy-agent@8.0.5: @@ -9651,7 +9435,7 @@ snapshots: source-map-js@1.2.1: {} - source-map-loader@1.0.2(webpack@5.102.1): + source-map-loader@1.0.2(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: data-urls: 2.0.0 iconv-lite: 0.6.3 @@ -9744,7 +9528,7 @@ snapshots: strip-json-comments@3.1.1: {} - style-loader@3.3.4(webpack@5.102.1): + style-loader@3.3.4(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: webpack: 5.102.1(webpack-cli@5.1.4) @@ -9835,10 +9619,6 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - tr46@0.0.3: {} tr46@2.1.0: @@ -9942,14 +9722,10 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 - underscore@1.13.7: {} - undici-types@6.21.0: {} undici-types@7.16.0: {} - unicorn-magic@0.3.0: {} - union@0.5.0: dependencies: qs: 6.14.0 @@ -10198,7 +9974,7 @@ snapshots: wordwrapjs@5.1.1: {} - worker-loader@3.0.8(webpack@5.102.1): + worker-loader@3.0.8(webpack@5.102.1(webpack-cli@5.1.4)): dependencies: loader-utils: 2.0.4 schema-utils: 3.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8cef746ab5..30f66b6304 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -7,6 +7,7 @@ packages: - "packages/viewer-datagrid" - "packages/viewer-charts" - "packages/workspace" + - "packages/anywidget" - "packages/jupyterlab" - "packages/react" - "packages/cli" diff --git a/rust/perspective-python/Cargo.toml b/rust/perspective-python/Cargo.toml index 5a1796f20f..964eeab9f8 100644 --- a/rust/perspective-python/Cargo.toml +++ b/rust/perspective-python/Cargo.toml @@ -29,7 +29,6 @@ include = [ "README.md", "./package.json", "perspective/**/*.py", - "perspective/extension", "perspective/templates", "perspective/tests/**/*.arrow", "./pyproject.toml", diff --git a/rust/perspective-python/build.mjs b/rust/perspective-python/build.mjs index a4befb5d79..bafc91cf36 100644 --- a/rust/perspective-python/build.mjs +++ b/rust/perspective-python/build.mjs @@ -99,7 +99,10 @@ if (build_wheel) { features.push(...standard_features); } - execSync(`${emsdk_prefix}maturin build ${flags} --features=${features.join(",")} ${target}`, { stdio: "inherit", env }); + execSync( + `${emsdk_prefix}maturin build ${flags} --features=${features.join(",")} ${target}`, + { stdio: "inherit", env }, + ); } if (build_sdist) { @@ -109,20 +112,22 @@ if (build_sdist) { const pyproject_toml = fs .readFileSync("./pyproject.toml") .toString("utf-8"); + const cargo = toml.parse(cargo_toml); const pyproject = toml.parse(pyproject_toml); - const version = cargo["package"]["version"]; const data_dir = `perspective_python-${version}.data`; const testfile = path.join( data_dir, "data/share/jupyter/labextensions/@perspective-dev/jupyterlab/package.json", ); + if (!fs.existsSync(testfile)) { throw new Error( "labextension is not present in data directory, please build `perspective-jupyterlab`", ); } + const readme_md = fs.readFileSync("./README.md"); const pkg_info = generatePkgInfo(pyproject, cargo, readme_md); fs.writeFileSync("./PKG-INFO", pkg_info); diff --git a/rust/perspective-python/clean.mjs b/rust/perspective-python/clean.mjs index d971b54527..505ecddfab 100644 --- a/rust/perspective-python/clean.mjs +++ b/rust/perspective-python/clean.mjs @@ -25,6 +25,7 @@ fs.rmSync(`perspective_python-${version}.data`, { fs.rmSync("LICENSE.md", { recursive: true, force: true }); fs.rmSync("LICENSE_THIRDPARTY_cargo.yml", { recursive: true, force: true }); fs.rmSync("perspective/*.so", { recursive: true, force: true }); +fs.rmSync("perspective/widget/static", { recursive: true, force: true }); // for (const path in glob("*.data")) { // fs.rmSync(path, { recursive: true, force: true }); diff --git a/rust/perspective-python/perspective/extension/finos-perspective-nbextension.json b/rust/perspective-python/perspective/extension/finos-perspective-nbextension.json deleted file mode 100644 index e47ddb2b8f..0000000000 --- a/rust/perspective-python/perspective/extension/finos-perspective-nbextension.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "load_extensions": { - "@perspective-dev/jupyterlab/extension": true - } -} diff --git a/rust/perspective-python/perspective/tests/widget/test_anywidget_smoke.py b/rust/perspective-python/perspective/tests/widget/test_anywidget_smoke.py new file mode 100644 index 0000000000..6690baffab --- /dev/null +++ b/rust/perspective-python/perspective/tests/widget/test_anywidget_smoke.py @@ -0,0 +1,74 @@ +# ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓ +# ┃ ██████ ██████ ██████ █ █ █ █ █ █▄ ▀███ █ ┃ +# ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█ ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄ ▀█ █ ▀▀▀▀▀ ┃ +# ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄ █ ▄▄▄▄▄ ┃ +# ┃ █ ██████ █ ▀█▄ █ ██████ █ ███▌▐███ ███████▄ █ ┃ +# ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫ +# ┃ Copyright (c) 2017, the Perspective Authors. ┃ +# ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃ +# ┃ This file is part of the Perspective library, distributed under the terms ┃ +# ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ +# ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ + +import pathlib + +import anywidget +import pytest + +import perspective.widget as widget_module +from perspective.widget import PerspectiveWidget + +pytestmark = pytest.mark.filterwarnings("ignore::DeprecationWarning") + +_STATIC = pathlib.Path(widget_module.__file__).parent / "static" + + +class TestAnyWidgetSmoke: + def test_instantiates_outside_jupyterlab(self): + widget = PerspectiveWidget({"a": [1, 2, 3, 4, 5]}, plugin="X Bar") + assert isinstance(widget, anywidget.AnyWidget) + assert widget.plugin == "X Bar" + + def test_traits_round_trip(self): + widget = PerspectiveWidget( + {"a": [1, 2, 3]}, group_by=["a"], sort=[["a", "desc"]] + ) + assert widget.group_by == ["a"] + assert widget.sort == [["a", "desc"]] + widget.split_by = ["a"] + assert widget.split_by == ["a"] + + def test_not_a_labextension_widget(self): + widget = PerspectiveWidget({"a": [1, 2, 3]}) + assert widget._model_module == "anywidget" + assert widget._view_module == "anywidget" + for attr in ( + "_model_module", + "_view_module", + "_model_name", + "_view_name", + ): + assert getattr(widget, attr, "") != "@perspective-dev/jupyterlab" + + def test_esm_css_configured(self): + assert PerspectiveWidget._esm is not None + assert PerspectiveWidget._css is not None + + def test_bundle_is_packaged(self): + esm = _STATIC / "perspective-anywidget.js" + css = _STATIC / "perspective-anywidget.css" + if not esm.exists(): + pytest.skip("anywidget bundle not built in this environment") + assert esm.stat().st_size > 0 + assert css.exists() and css.stat().st_size > 0 + + def test_proxy_session_lifecycle(self): + widget = PerspectiveWidget({"a": [1, 2, 3]}) + widget.handle_message(widget, {"type": "connect", "client_id": "c1"}, []) + assert "c1" in widget._sessions + widget.handle_message(widget, {"type": "hangup", "client_id": "c1"}, []) + assert "c1" not in widget._sessions + + def test_deferred_load_without_data(self): + widget = PerspectiveWidget(None, group_by=["a"]) + assert widget.group_by == ["a"] diff --git a/rust/perspective-python/perspective/widget/__init__.py b/rust/perspective-python/perspective/widget/__init__.py index adf2bca858..e87dc1bef9 100644 --- a/rust/perspective-python/perspective/widget/__init__.py +++ b/rust/perspective-python/perspective/widget/__init__.py @@ -13,19 +13,22 @@ import base64 import logging import os +import pathlib import re import importlib.metadata import inspect from string import Template -from ipywidgets import DOMWidget -from traitlets import Unicode, observe +import anywidget +from traitlets import observe from .viewer import PerspectiveViewer __version__ = re.sub(".dev[0-9]+", "", importlib.metadata.version("perspective-python")) __all__ = ["PerspectiveWidget"] +_STATIC = pathlib.Path(__file__).parent / "static" + __doc__ = """ `PerspectiveWidget` is a JupyterLab widget that implements the same API as ``, allowing for fast, intuitive @@ -90,7 +93,7 @@ """ -class PerspectiveWidget(DOMWidget, PerspectiveViewer): +class PerspectiveWidget(anywidget.AnyWidget, PerspectiveViewer): """`PerspectiveWidget` allows for Perspective to be used as a Jupyter widget. @@ -123,13 +126,8 @@ class PerspectiveWidget(DOMWidget, PerspectiveViewer): >>> widget.table.update({"a": [4, 5]}) # Browser UI updates """ - # Required by ipywidgets for proper registration of the backend - _model_name = Unicode("PerspectiveModel").tag(sync=True) - _model_module = Unicode("@perspective-dev/jupyterlab").tag(sync=True) - _model_module_version = Unicode("~{}".format(__version__)).tag(sync=True) - _view_name = Unicode("PerspectiveView").tag(sync=True) - _view_module = Unicode("@perspective-dev/jupyterlab").tag(sync=True) - _view_module_version = Unicode("~{}".format(__version__)).tag(sync=True) + _esm = _STATIC / "perspective-anywidget.js" + _css = _STATIC / "perspective-anywidget.css" def __init__( self, @@ -297,7 +295,7 @@ def send_response(msg): session.close() def _repr_mimebundle_(self, **kwargs): - super_bundle = super(DOMWidget, self)._repr_mimebundle_(**kwargs) + super_bundle = super()._repr_mimebundle_(**kwargs) if not _jupyter_html_export_enabled(): return super_bundle @@ -319,7 +317,7 @@ def psp_cdn(module, path=None): # return f"http://localhost:8080/node_modules/@perspective-dev/{module}/dist/{path}" return f"https://cdn.jsdelivr.net/npm/@perspective-dev/{module}@{__version__}/dist/{path}" - return super(DOMWidget, self)._repr_mimebundle_(**kwargs) | { + return super()._repr_mimebundle_(**kwargs) | { "text/html": template.substitute( psp_cdn_perspective=psp_cdn("perspective"), psp_cdn_perspective_viewer=psp_cdn("perspective-viewer"), diff --git a/rust/perspective-python/pyproject.toml b/rust/perspective-python/pyproject.toml index d5edcb67d0..ef4153cd87 100644 --- a/rust/perspective-python/pyproject.toml +++ b/rust/perspective-python/pyproject.toml @@ -37,7 +37,7 @@ classifiers = [ ] [project.optional-dependencies] -jupyter = ["ipywidgets>=7.5.1,<9"] +jupyter = ["anywidget>=0.9"] tornado = ["tornado>=5,<7"] aiohttp = ["aiohttp>=2,<4"] starlette = ["starlette<1"] @@ -49,6 +49,8 @@ features = ["pyo3/extension-module"] include = [ { path = "perspective/*libpsp.so", format = "wheel" }, { path = "perspective/*libpsp.dll", format = "wheel" }, + { path = "perspective/widget/static/perspective-anywidget.js", format = "wheel" }, + { path = "perspective/widget/static/perspective-anywidget.css", format = "wheel" }, ] [tool.pytest.ini_options] diff --git a/rust/perspective-python/requirements.txt b/rust/perspective-python/requirements.txt index 6bd19d03a3..58264f0527 100644 --- a/rust/perspective-python/requirements.txt +++ b/rust/perspective-python/requirements.txt @@ -1,3 +1,4 @@ +anywidget==0.11.0 clickhouse_connect==0.11.0 duckdb==1.4.4 Faker==26.0.0 diff --git a/rust/perspective-viewer/src/rust/utils/custom_element.rs b/rust/perspective-viewer/src/rust/utils/custom_element.rs index 5503738a41..5bd84b37ed 100644 --- a/rust/perspective-viewer/src/rust/utils/custom_element.rs +++ b/rust/perspective-viewer/src/rust/utils/custom_element.rs @@ -16,6 +16,11 @@ use wasm_bindgen::prelude::*; #[wasm_bindgen(inline_js = r#" export function bootstrap(psp, name, clsname, statics) { + if (customElements.get(name)) { + console.warn(`Custom element "${name}" is already registered; skipping duplicate registration (Perspective was loaded more than once on this page).`); + return; + } + const cls = psp[clsname]; const proto = cls.prototype; class x extends HTMLElement { diff --git a/tools/scripts/setup.mjs b/tools/scripts/setup.mjs index 33c7427c92..a1adf307ee 100644 --- a/tools/scripts/setup.mjs +++ b/tools/scripts/setup.mjs @@ -178,6 +178,11 @@ async function focus_package() { name: "@perspective-dev/jupyterlab", value: "jupyterlab", }, + { + key: "a", + name: "@perspective-dev/anywidget", + value: "anywidget", + }, { key: "w", name: "@perspective-dev/workspace",