diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d030602af..4352c9b1df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# [v3.1.8](https://github.com/finos/perspective/releases/tag/v3.1.8) + +_2 December 2024_ ([Full changelog](https://github.com/finos/perspective/compare/v3.1.7...v3.1.8)) + +Fixes + +- fix treemap rendering issue in firefox [#2813](https://github.com/finos/perspective/pull/2813) +- Allow for passthrough of event loop in psp webserver handlers [#2829](https://github.com/finos/perspective/pull/2829) +- Fix workspace dark theme, theme render bug [#2864](https://github.com/finos/perspective/pull/2864) + +# [v3.1.7](https://github.com/finos/perspective/releases/tag/v3.1.7) + +_30 November 2024_ ([Full changelog](https://github.com/finos/perspective/compare/v3.1.6...v3.1.7)) + +Fixes + +- Remove `workspace-layout-update` event debounce [#2862](https://github.com/finos/perspective/pull/2862) + +Misc + +- Add some information on JSON input formats [#2856](https://github.com/finos/perspective/pull/2856) + # [v3.1.6](https://github.com/finos/perspective/releases/tag/v3.1.6) _18 November 2024_ ([Full changelog](https://github.com/finos/perspective/compare/v3.1.5...v3.1.6)) diff --git a/Cargo.lock b/Cargo.lock index d292903a1b..ab3fdc80b7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1961,7 +1961,7 @@ checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "perspective" -version = "3.1.6" +version = "3.1.8" dependencies = [ "async-lock", "axum", @@ -2001,7 +2001,7 @@ dependencies = [ [[package]] name = "perspective-client" -version = "3.1.6" +version = "3.1.8" dependencies = [ "async-lock", "futures", @@ -2023,7 +2023,7 @@ dependencies = [ [[package]] name = "perspective-js" -version = "3.1.6" +version = "3.1.8" dependencies = [ "anyhow", "base64 0.13.1", @@ -2052,7 +2052,7 @@ dependencies = [ [[package]] name = "perspective-lint" -version = "3.1.6" +version = "3.1.8" dependencies = [ "glob", "yew-fmt", @@ -2072,7 +2072,7 @@ dependencies = [ [[package]] name = "perspective-python" -version = "3.1.6" +version = "3.1.8" dependencies = [ "async-lock", "cmake", @@ -2092,7 +2092,7 @@ dependencies = [ [[package]] name = "perspective-server" -version = "3.1.6" +version = "3.1.8" dependencies = [ "async-lock", "base64 0.22.1", @@ -2108,7 +2108,7 @@ dependencies = [ [[package]] name = "perspective-viewer" -version = "3.1.6" +version = "3.1.8" dependencies = [ "anyhow", "async-lock", diff --git a/cpp/perspective/package.json b/cpp/perspective/package.json index 8ed89e516e..e4c570fb21 100644 --- a/cpp/perspective/package.json +++ b/cpp/perspective/package.json @@ -3,7 +3,7 @@ "private": true, "author": "The Perspective Authors", "license": "Apache-2.0", - "version": "3.1.6", + "version": "3.1.8", "main": "./dist/esm/perspective.cpp.js", "files": [ "dist/esm/**/*", diff --git a/docs/package.json b/docs/package.json index b000c643ae..9790288837 100644 --- a/docs/package.json +++ b/docs/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-docs", - "version": "3.1.6", + "version": "3.1.8", "private": true, "scripts": { "build": "node build.js && docusaurus build", diff --git a/examples/blocks/package.json b/examples/blocks/package.json index bad510f2b7..e44e3316cd 100644 --- a/examples/blocks/package.json +++ b/examples/blocks/package.json @@ -1,7 +1,7 @@ { "name": "blocks", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "A collection of simple client-side Perspective examples for `http://bl.ocks.org`.", "scripts": { "start": "mkdirp dist && node --experimental-modules server.mjs", diff --git a/examples/blocks/src/superstore/index.html b/examples/blocks/src/superstore/index.html index 32b1127c8b..28c0bcbe37 100644 --- a/examples/blocks/src/superstore/index.html +++ b/examples/blocks/src/superstore/index.html @@ -6,17 +6,12 @@ diff --git a/examples/blocks/src/superstore/index.json b/examples/blocks/src/superstore/index.json new file mode 100644 index 0000000000..78e3391b0d --- /dev/null +++ b/examples/blocks/src/superstore/index.json @@ -0,0 +1,169 @@ +index.html: 48 { + "sizes": [ + 1 + ], + "detail": { + "main": { + "type": "split-area", + "orientation": "horizontal", + "children": [ + { + "type": "tab-area", + "widgets": [ + "PERSPECTIVE_GENERATED_ID_0", + "PERSPECTIVE_GENERATED_ID_1" + ], + "currentIndex": 0 + }, + { + "type": "tab-area", + "widgets": [ + "PERSPECTIVE_GENERATED_ID_3" + ], + "currentIndex": 0 + }, + { + "type": "tab-area", + "widgets": [ + "PERSPECTIVE_GENERATED_ID_2" + ], + "currentIndex": 0 + } + ], + "sizes": [ + 0.3765432098765432, + 0.24691358024691357, + 0.3765432098765432 + ] + } + }, + "viewers": { + "PERSPECTIVE_GENERATED_ID_0": { + "version": "3.1.8", + "plugin": "Datagrid", + "plugin_config": { + "columns": {}, + "edit_mode": "READ_ONLY", + "scroll_lock": false + }, + "columns_config": {}, + "title": "Sales Report 2", + "group_by": [ + "Region", + "State" + ], + "split_by": [ + "Category", + "Sub-Category" + ], + "columns": [ + "Sales", + "Profit" + ], + "filter": [], + "sort": [], + "expressions": {}, + "aggregates": {}, + "table": "superstore", + "settings": false + }, + "PERSPECTIVE_GENERATED_ID_1": { + "version": "3.1.8", + "plugin": "Datagrid", + "plugin_config": { + "columns": {}, + "edit_mode": "READ_ONLY", + "scroll_lock": false + }, + "columns_config": {}, + "title": null, + "group_by": [], + "split_by": [], + "columns": [ + "Row ID", + "Order ID", + "Ship Mode", + "Customer ID", + "Customer Name", + "Segment", + "Country", + "City", + "State", + "Postal Code", + "Region", + "Product ID", + "Category", + "Sub-Category", + "Product Name", + "Sales", + "Quantity", + "Discount", + "Profit", + "Order Date", + "Ship Date" + ], + "filter": [], + "sort": [], + "expressions": {}, + "aggregates": {}, + "table": "superstore", + "settings": false + }, + "PERSPECTIVE_GENERATED_ID_3": { + "version": "3.1.8", + "plugin": "Datagrid", + "plugin_config": { + "columns": {}, + "edit_mode": "READ_ONLY", + "scroll_lock": false + }, + "columns_config": {}, + "title": "Sales Report 2 (*)", + "group_by": [ + "Region", + "State" + ], + "split_by": [ + "Category", + "Sub-Category" + ], + "columns": [ + "Sales", + "Profit" + ], + "filter": [], + "sort": [], + "expressions": {}, + "aggregates": {}, + "table": "superstore", + "settings": false + }, + "PERSPECTIVE_GENERATED_ID_2": { + "version": "3.1.8", + "plugin": "Treemap", + "plugin_config": {}, + "columns_config": {}, + "title": "Sales Report (by State)", + "group_by": [ + "State" + ], + "split_by": [], + "columns": [ + "Sales", + "Profit", + null + ], + "filter": [], + "sort": [ + [ + "Profit", + "desc" + ] + ], + "expressions": {}, + "aggregates": {}, + "table": "superstore", + "settings": false + } + } +} \ No newline at end of file diff --git a/examples/esbuild-example/package.json b/examples/esbuild-example/package.json index ad8f18e1de..a3163995fa 100644 --- a/examples/esbuild-example/package.json +++ b/examples/esbuild-example/package.json @@ -1,7 +1,7 @@ { "name": "esbuild-example", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An esbuild example app built using `@finos/perspective-viewer`.", "scripts": { "build": "node build.js", diff --git a/examples/esbuild-remote/package.json b/examples/esbuild-remote/package.json index c36cea0f4a..7ecb2d86ce 100644 --- a/examples/esbuild-remote/package.json +++ b/examples/esbuild-remote/package.json @@ -1,7 +1,7 @@ { "name": "esbuild-remote", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of 2 Perspectives, one client and one server, streaming via Apache Arrow.", "scripts": { "start": "node build.js && node server/index.mjs" diff --git a/examples/python-aiohttp/package.json b/examples/python-aiohttp/package.json index 84c2056504..5a7a9519b3 100644 --- a/examples/python-aiohttp/package.json +++ b/examples/python-aiohttp/package.json @@ -1,7 +1,7 @@ { "name": "python-aiohttp", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of editing a `perspective-python` server from the browser.", "scripts": { "start": "PYTHONPATH=../../python/perspective python3 server.py" diff --git a/examples/python-starlette/package.json b/examples/python-starlette/package.json index daf4804274..9b6fa71fea 100644 --- a/examples/python-starlette/package.json +++ b/examples/python-starlette/package.json @@ -1,7 +1,7 @@ { "name": "python-starlette", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of editing a `perspective-python` server from the browser.", "scripts": { "start": "PYTHONPATH=../../python/perspective python3 server.py" diff --git a/examples/python-starlette/server.py b/examples/python-starlette/server.py index b1bc6f602b..f1d3baa0a0 100644 --- a/examples/python-starlette/server.py +++ b/examples/python-starlette/server.py @@ -18,7 +18,6 @@ import uvicorn from fastapi import FastAPI, WebSocket -from fastapi.middleware.cors import CORSMiddleware from starlette.responses import FileResponse from starlette.staticfiles import StaticFiles @@ -64,21 +63,12 @@ async def websocket_handler(websocket: WebSocket): ) await handler.run() - # static_html_files = StaticFiles(directory="../python-tornado", html=True) static_html_files = StaticFiles(directory="../python-tornado", html=True) app = FastAPI() app.add_api_websocket_route("/websocket", websocket_handler) app.get("/node_modules/{rest_of_path:path}")(static_node_modules_handler) app.mount("/", static_html_files) - - app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], - ) return app diff --git a/examples/python-tornado-streaming/package.json b/examples/python-tornado-streaming/package.json index d525a02e45..c443f6a5cf 100644 --- a/examples/python-tornado-streaming/package.json +++ b/examples/python-tornado-streaming/package.json @@ -1,7 +1,7 @@ { "name": "python-tornado-streaming", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of streaming a `perspective-python` server to the browser.", "scripts": { "start": "PYTHONPATH=../../python/perspective python3 server.py" diff --git a/examples/python-tornado/package.json b/examples/python-tornado/package.json index 97b1d38c8c..7776d79491 100644 --- a/examples/python-tornado/package.json +++ b/examples/python-tornado/package.json @@ -1,7 +1,7 @@ { "name": "python-tornado", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of editing a `perspective-python` server from the browser.", "scripts": { "start": "PYTHONPATH=../../python/perspective python3 server.py" diff --git a/examples/react-example/package.json b/examples/react-example/package.json index 066f3fdc79..5d74a74d66 100644 --- a/examples/react-example/package.json +++ b/examples/react-example/package.json @@ -1,7 +1,7 @@ { "name": "react-example", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example app built using `@finos/perspective-viewer`.", "scripts": { "start": "webpack serve --open", diff --git a/examples/rust-axum/Cargo.toml b/examples/rust-axum/Cargo.toml index de91ddd56a..08fc9cf9ee 100644 --- a/examples/rust-axum/Cargo.toml +++ b/examples/rust-axum/Cargo.toml @@ -17,7 +17,7 @@ edition = "2021" publish = false [dependencies] -perspective = { version = "3.1.6", path = "../../rust/perspective" } +perspective = { version = "3.1.8", path = "../../rust/perspective" } axum = { version = ">=0.7,<2", features = ["ws"] } futures = "0.3" tokio = { version = "1.0", features = ["full"] } diff --git a/examples/rust-axum/package.json b/examples/rust-axum/package.json index f9f112619d..ce5993294d 100644 --- a/examples/rust-axum/package.json +++ b/examples/rust-axum/package.json @@ -1,7 +1,7 @@ { "name": "rust-axum", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example of a Rust/Axum virtual Perspective server", "scripts": { "start": "cargo run" diff --git a/examples/webpack-example/package.json b/examples/webpack-example/package.json index 958a12fb97..98eaf0e790 100644 --- a/examples/webpack-example/package.json +++ b/examples/webpack-example/package.json @@ -1,7 +1,7 @@ { "name": "webpack-example", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example app built using `@finos/perspective-viewer`.", "scripts": { "webpack_build": "webpack", diff --git a/examples/workspace/package.json b/examples/workspace/package.json index 6d9e5e4409..01b0e5b2e5 100644 --- a/examples/workspace/package.json +++ b/examples/workspace/package.json @@ -1,7 +1,7 @@ { "name": "workspace", "private": true, - "version": "3.1.6", + "version": "3.1.8", "description": "An example app built using `@finos/perspective-workspace`.", "scripts": { "start": "webpack serve --open", diff --git a/package.json b/package.json index c65e0a3853..7a1273a18b 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "https://github.com/finos/perspective" }, - "version": "3.1.6", + "version": "3.1.8", "changelog": { "labels": { "enhancement": "Added", diff --git a/packages/perspective-cli/package.json b/packages/perspective-cli/package.json index c6465aec1d..35d33a769f 100644 --- a/packages/perspective-cli/package.json +++ b/packages/perspective-cli/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-cli", - "version": "3.1.6", + "version": "3.1.8", "description": "Perspective.js CLI", "main": "src/js/index.js", "publishConfig": { diff --git a/packages/perspective-esbuild-plugin/package.json b/packages/perspective-esbuild-plugin/package.json index 98195e25c1..070745cc34 100644 --- a/packages/perspective-esbuild-plugin/package.json +++ b/packages/perspective-esbuild-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-esbuild-plugin", - "version": "3.1.6", + "version": "3.1.8", "description": "esbuild plugin for Perspective", "author": "", "license": "Apache-2.0", diff --git a/packages/perspective-jupyterlab/package.json b/packages/perspective-jupyterlab/package.json index 20926f1684..f6e248fb65 100644 --- a/packages/perspective-jupyterlab/package.json +++ b/packages/perspective-jupyterlab/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-jupyterlab", - "version": "3.1.6", + "version": "3.1.8", "description": "A Jupyterlab extension for the Perspective library, designed to be used with perspective-python.", "files": [ "dist/**/*", diff --git a/packages/perspective-viewer-d3fc/package.json b/packages/perspective-viewer-d3fc/package.json index 039b328153..f87ae850c0 100644 --- a/packages/perspective-viewer-d3fc/package.json +++ b/packages/perspective-viewer-d3fc/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-viewer-d3fc", - "version": "3.1.6", + "version": "3.1.8", "description": "Perspective.js D3FC Plugin", "unpkg": "./dist/cdn/perspective-viewer-d3fc.js", "jsdelivr": "./dist/cdn/perspective-viewer-d3fc.js", diff --git a/packages/perspective-viewer-d3fc/src/ts/series/treemap/treemapSeries.ts b/packages/perspective-viewer-d3fc/src/ts/series/treemap/treemapSeries.ts index 8715c0cb23..1358ac0513 100644 --- a/packages/perspective-viewer-d3fc/src/ts/series/treemap/treemapSeries.ts +++ b/packages/perspective-viewer-d3fc/src/ts/series/treemap/treemapSeries.ts @@ -70,10 +70,10 @@ export function treemapSeries() { const rects = nodesMerge .select("rect") .attr("class", (d) => `treerect ${nodeLevelHelper(maxDepth, d)}`) - .style("x", (d) => d.x0) - .style("y", (d) => d.y0) - .style("width", (d) => calcWidth(d)) - .style("height", (d) => calcHeight(d)); + .style("x", (d) => `${d.x0}px`) + .style("y", (d) => `${d.y0}px`) + .style("width", (d) => `${calcWidth(d)}px`) + .style("height", (d) => `${calcHeight(d)}px`); rects.style("fill", (d) => { if (nodeLevelHelper(maxDepth, d) === nodeLevel.leaf) { diff --git a/packages/perspective-viewer-datagrid/package.json b/packages/perspective-viewer-datagrid/package.json index 9153b46a18..75f58723c5 100644 --- a/packages/perspective-viewer-datagrid/package.json +++ b/packages/perspective-viewer-datagrid/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-viewer-datagrid", - "version": "3.1.6", + "version": "3.1.8", "description": "Perspective datagrid plugin based on `regular-table`", "unpkg": "dist/cdn/perspective-viewer-datagrid.js", "jsdelivr": "dist/cdn/perspective-viewer-datagrid.js", diff --git a/packages/perspective-viewer-datagrid/src/less/row-hover.less b/packages/perspective-viewer-datagrid/src/less/row-hover.less index 616130f6a3..8d60e95475 100644 --- a/packages/perspective-viewer-datagrid/src/less/row-hover.less +++ b/packages/perspective-viewer-datagrid/src/less/row-hover.less @@ -20,15 +20,12 @@ regular-table { tr:hover:after { border-color: var(--rt-hover--border-color, #c5c9d080) !important; background-color: transparent; - box-shadow: 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), 0px 5px 0px rgba(0, 0, 0, 0.01); + box-shadow: 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); &.psp-menu-open { box-shadow: inset -2px 0px 0px var(--icon--color), inset 2px 0px 0px var(--icon--color), - 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), - 0px 5px 0px rgba(0, 0, 0, 0.01); + 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); } } @@ -37,8 +34,7 @@ regular-table { box-shadow: inset -2px 0px 0px var(--icon--color), inset 2px 0px 0px var(--icon--color), inset 0px -2px 0px var(--icon--color), - 0px 1px 0px var(--rt-hover--border-color, #c5c9d080), - 0px 3px 0px rgba(0, 0, 0, 0.05), 0px 5px 0px rgba(0, 0, 0, 0.01); + 0px 1px 0px var(--rt-hover--border-color, #c5c9d080); } tr:hover diff --git a/packages/perspective-viewer-openlayers/package.json b/packages/perspective-viewer-openlayers/package.json index 8eaa78348e..25fcbf5dd0 100644 --- a/packages/perspective-viewer-openlayers/package.json +++ b/packages/perspective-viewer-openlayers/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-viewer-openlayers", - "version": "3.1.6", + "version": "3.1.8", "unpkg": "dist/cdn/perspective-viewer-openlayers.js", "jsdelivr": "dist/cdn/perspective-viewer-openlayers.js", "exports": { diff --git a/packages/perspective-webpack-plugin/package.json b/packages/perspective-webpack-plugin/package.json index 9fa13b861b..d3fcd4e7c8 100644 --- a/packages/perspective-webpack-plugin/package.json +++ b/packages/perspective-webpack-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-webpack-plugin", - "version": "3.1.6", + "version": "3.1.8", "description": "Perspective.js Webpack Plugin", "main": "index.js", "publishConfig": { diff --git a/packages/perspective-workspace/build.js b/packages/perspective-workspace/build.js index ad7d4375a5..f39bce6223 100644 --- a/packages/perspective-workspace/build.js +++ b/packages/perspective-workspace/build.js @@ -143,6 +143,8 @@ async function build_all() { pro_dark.compile().get("output.css") ); + await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); + try { await $`npx tsc --project ./tsconfig.json`.stdio( "inherit", @@ -152,8 +154,6 @@ async function build_all() { } catch (e) { process.exit(1); } - - await Promise.all(BUILD.map(build)).catch(() => process.exit(1)); } build_all(); diff --git a/packages/perspective-workspace/package.json b/packages/perspective-workspace/package.json index 0be97e9540..a5826e811d 100644 --- a/packages/perspective-workspace/package.json +++ b/packages/perspective-workspace/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-workspace", - "version": "3.1.6", + "version": "3.1.8", "description": "Perspective Workspace", "files": [ "dist/**/*", diff --git a/packages/perspective-workspace/src/less/menu.less b/packages/perspective-workspace/src/less/menu.less index f184081323..2f7d138079 100644 --- a/packages/perspective-workspace/src/less/menu.less +++ b/packages/perspective-workspace/src/less/menu.less @@ -12,11 +12,15 @@ @import "@lumino/widgets/style/menu.css"; +:host { + position: absolute; +} + .lm-Menu { font-size: 12px; padding: 8px; - background: white; - color: #666; + background-color: var(--plugin--background); + color: var(--icon--color); border: 1px solid var(--inactive--color); border-radius: 3px; max-width: 350px; diff --git a/packages/perspective-workspace/src/less/viewer.less b/packages/perspective-workspace/src/less/viewer.less index 7aa9897dac..ae4fdf8113 100644 --- a/packages/perspective-workspace/src/less/viewer.less +++ b/packages/perspective-workspace/src/less/viewer.less @@ -64,7 +64,7 @@ } } -perspective-viewer { +::slotted(perspective-viewer) { flex: 1; position: relative; display: block; @@ -76,10 +76,11 @@ perspective-viewer { overflow: visible !important; } -.lm-mod-override-cursor { +:host-context(.lm-mod-override-cursor) { cursor: grabbing !important; } -.lm-mod-override-cursor perspective-viewer > * { - pointer-events: none; +:host-context(.lm-mod-override-cursor) ::slotted(perspective-viewer), +.context-menu ::slotted(perspective-viewer) { + --override-content-pointer-events: none; } diff --git a/packages/perspective-workspace/src/less/workspace.less b/packages/perspective-workspace/src/less/workspace.less index 6cf4f4d217..7b8c283ce9 100644 --- a/packages/perspective-workspace/src/less/workspace.less +++ b/packages/perspective-workspace/src/less/workspace.less @@ -21,6 +21,8 @@ @import "@lumino/widgets/style/tabbar.css"; @import "@lumino/widgets/style/tabpanel.css"; +@import "./injected.less"; + :host { @import "./tabbar.less"; @import "./dockpanel.less"; @@ -28,8 +30,8 @@ background-color: hsl(210deg 18% 90%); - width: 100%; - height: 100%; + // width: 100%; + // height: 100%; .workspace { width: 100%; diff --git a/packages/perspective-workspace/src/themes/pro-dark.less b/packages/perspective-workspace/src/themes/pro-dark.less index ce2b459b64..6d9f372a63 100644 --- a/packages/perspective-workspace/src/themes/pro-dark.less +++ b/packages/perspective-workspace/src/themes/pro-dark.less @@ -22,9 +22,6 @@ perspective-indicator[theme="Pro Dark"] { --theme-name: "Pro Dark"; } -.lm-Menu { - @include perspective-viewer-pro-dark--colors; -} perspective-workspace perspective-viewer { --status-bar--height: 38px; @@ -62,7 +59,7 @@ perspective-viewer[theme="Pro Dark"].workspace-master-widget { --plugin--background: @grey800; } -.lm-Menu { +perspective-workspace-menu { font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace; font-weight: 300; diff --git a/packages/perspective-workspace/src/themes/pro.less b/packages/perspective-workspace/src/themes/pro.less index 6c8296ac47..5fca260321 100644 --- a/packages/perspective-workspace/src/themes/pro.less +++ b/packages/perspective-workspace/src/themes/pro.less @@ -24,11 +24,6 @@ perspective-workspace { background-color: #dadada; } -.lm-Menu { - @include perspective-viewer-pro--colors; - background-color: #ffffff; -} - perspective-workspace perspective-viewer[settings] { --modal-panel--margin: -4px 0 -4px 0; --status-bar--border-radius: 6px 0 0 0; @@ -84,7 +79,7 @@ perspective-viewer[theme="Pro Light"].workspace-master-widget { --workspace-tabbar-tab--border-width: 1px 1px 0px 1px; } -.lm-Menu { +perspective-workspace-menu { font-family: "ui-monospace", "SFMono-Regular", "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace; font-weight: 300; diff --git a/packages/perspective-workspace/src/ts/perspective-workspace.ts b/packages/perspective-workspace/src/ts/perspective-workspace.ts index 6753e69407..0254226b4b 100644 --- a/packages/perspective-workspace/src/ts/perspective-workspace.ts +++ b/packages/perspective-workspace/src/ts/perspective-workspace.ts @@ -27,7 +27,6 @@ import { import { bindTemplate, CustomElementProto } from "./utils/custom_elements"; import style from "../../build/css/workspace.css"; import template from "../html/workspace.html"; -import injectedStyles from "../../build/css/injected.css"; /** * A Custom Element for coordinating a set of `` light DOM @@ -52,13 +51,10 @@ import injectedStyles from "../../build/css/injected.css"; * name="View One" * table="superstore"> * - * * * - * * * ``` * @@ -295,11 +291,6 @@ export class HTMLPerspectiveWorkspaceElement extends HTMLElement { } } -const _injectStyle = document.createElement("style"); -_injectStyle.toggleAttribute("injected", true); -_injectStyle.innerHTML = injectedStyles; -document.head.appendChild(_injectStyle); - bindTemplate( template, style diff --git a/packages/perspective-workspace/src/ts/utils/custom_elements.ts b/packages/perspective-workspace/src/ts/utils/custom_elements.ts index 13b631daa3..a3264fb0c7 100644 --- a/packages/perspective-workspace/src/ts/utils/custom_elements.ts +++ b/packages/perspective-workspace/src/ts/utils/custom_elements.ts @@ -51,14 +51,9 @@ export function registerElement( `` + template.innerHTML; } - template.innerHTML = - `` + template.innerHTML; - const _perspective_element = class extends proto { - _initialized: boolean; - _initializing: boolean; + private _initialized: boolean; + private _initializing: boolean; constructor() { super(); diff --git a/packages/perspective-workspace/src/ts/workspace/commands.ts b/packages/perspective-workspace/src/ts/workspace/commands.ts index 09cc6ea761..95192f349a 100644 --- a/packages/perspective-workspace/src/ts/workspace/commands.ts +++ b/packages/perspective-workspace/src/ts/workspace/commands.ts @@ -20,6 +20,7 @@ import type { } from "@finos/perspective-viewer"; import type { PerspectiveWorkspace } from "./workspace"; +import { WorkspaceMenu } from "./menu"; export const createCommands = ( workspace: PerspectiveWorkspace, @@ -43,7 +44,10 @@ export const createCommands = ( workspace.get_context_menu()?.init_overlay?.(); menu.addEventListener("blur", () => { const context_menu = workspace.get_context_menu()!; - const signal = context_menu.aboutToClose as Signal; + const signal = context_menu.aboutToClose as Signal< + WorkspaceMenu, + any + >; signal.emit({}); }); }, @@ -83,7 +87,7 @@ export const createCommands = ( menu.addEventListener("blur", () => { ( workspace.get_context_menu()?.aboutToClose as - | Signal + | Signal | undefined )?.emit({}); }); @@ -230,11 +234,11 @@ export const createCommands = ( workspace.toggleMasterDetail( workspace.getWidgetByName(args.widget_name as string)! ), - isVisible: () => true, - // iconClass: (args) => - // args.widget.parent === workspace.dockpanel - // ? "menu-master" - // : "menu-detail", + isVisible: (args) => { + return !!workspace.getWidgetByName(args.widget_name as string) + ?._is_pivoted; + }, + label: (args) => { return workspace.getWidgetByName(args.widget_name as string)! .parent === workspace.get_dock_panel() diff --git a/packages/perspective-workspace/src/ts/workspace/dockpanel.ts b/packages/perspective-workspace/src/ts/workspace/dockpanel.ts index f34994b97a..25e25faa98 100644 --- a/packages/perspective-workspace/src/ts/workspace/dockpanel.ts +++ b/packages/perspective-workspace/src/ts/workspace/dockpanel.ts @@ -11,7 +11,6 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ import { DockLayout, DockPanel, TabBar, Widget } from "@lumino/widgets"; -import { toArray } from "@lumino/algorithm"; import { PerspectiveTabBar } from "./tabbar"; import { PerspectiveTabBarRenderer } from "./tabbarrenderer"; import { PerspectiveWorkspace } from "./workspace"; @@ -55,7 +54,7 @@ export class PerspectiveDockPanel extends DockPanel { const widget = sender.titles[args.index].owner; const layout = this.layout as DockLayout; const old = layout.saveLayout(); - if (toArray(layout.widgets()).length > 1) { + if (Array.from(layout.widgets()).length > 1) { layout.removeWidget(widget); } @@ -65,6 +64,8 @@ export class PerspectiveDockPanel extends DockPanel { // @ts-ignore: accessing a private member `_drag` const drag = this._drag; if (drag) { + drag.dragImage?.parentElement.removeChild(drag.dragImage); + drag.dragImage = null; drag._promise.then(() => { if (!widget.node.isConnected) { layout.restoreLayout(old); diff --git a/packages/perspective-workspace/src/ts/workspace/menu.ts b/packages/perspective-workspace/src/ts/workspace/menu.ts index b7d1b27c4a..6e942130fe 100644 --- a/packages/perspective-workspace/src/ts/workspace/menu.ts +++ b/packages/perspective-workspace/src/ts/workspace/menu.ts @@ -10,10 +10,61 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +import { ElementExt } from "@lumino/domutils"; +import { MessageLoop } from "@lumino/messaging"; import { h } from "@lumino/virtualdom"; -import { Menu } from "@lumino/widgets"; +import { Menu, Widget } from "@lumino/widgets"; -export class MenuRenderer extends Menu.Renderer { +export class WorkspaceMenu extends Menu { + private _host: ShadowRoot; + init_overlay?: () => void; + + constructor(host: ShadowRoot, options: Menu.IOptions) { + options.renderer = new MenuRenderer(); + super(options); + this._host = host; + (this as any)._openChildMenu = this._overrideOpenChildMenu.bind(this); + } + + open(x: number, y: number, options?: Menu.IOpenOptions) { + options ||= {}; + options.host = this._host as any as HTMLElement; + super.open(x, y, options); + } + + // Override this lumino private method because it will otherwise always + // attach to `document.body`. + private _overrideOpenChildMenu(activateFirst = false) { + const self = this as any; + let item = this.activeItem; + if (!item || item.type !== "submenu" || !item.submenu) { + self._closeChildMenu(); + return; + } + + let submenu = item.submenu; + if (submenu === self._childMenu) { + return; + } + + Menu.saveWindowData(); + self._closeChildMenu(); + self._childMenu = submenu; + self._childIndex = self._activeIndex; + (submenu as any)._parentMenu = this; + MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest); + let itemNode = this.contentNode.children[self._activeIndex]; + openSubmenu(submenu, itemNode as HTMLElement, self._host); + if (activateFirst) { + submenu.activeIndex = -1; + submenu.activateNextItem(); + } + + submenu.activate(); + } +} + +class MenuRenderer extends Menu.Renderer { formatLabel(data: Menu.IRenderData) { let { label, mnemonic } = data.item; if (mnemonic < 0 || mnemonic >= label.length) { @@ -67,3 +118,72 @@ export class MenuRenderer extends Menu.Renderer { ); } } + +// Prevent submenus from attaching outside the Shadow DOM. +// Forked from [Lumino](https://github.com/jupyterlab/lumino/blob/main/packages/widgets/src/menu.ts). +// [License](https://github.com/jupyterlab/lumino/blob/main/LICENSE) +export function openSubmenu( + submenu: Menu, + itemNode: HTMLElement, + host: HTMLElement +): void { + const windowData = getWindowData(); + let px = windowData.pageXOffset; + let py = windowData.pageYOffset; + let cw = windowData.clientWidth; + let ch = windowData.clientHeight; + const hostData = (host as any).host.getBoundingClientRect(); + let hx = hostData.x; + let hy = hostData.y; + MessageLoop.sendMessage(submenu, Widget.Msg.UpdateRequest); + let maxHeight = ch; + let node = submenu.node; + let style = node.style; + style.opacity = "0"; + style.maxHeight = `${maxHeight}px`; + Widget.attach(submenu, host); + let { width, height } = node.getBoundingClientRect(); + let box = ElementExt.boxSizing(submenu.node); + let itemRect = itemNode.getBoundingClientRect(); + let x = itemRect.right - SUBMENU_OVERLAP - hx; + if (x + width > px + cw + hx) { + x = itemRect.left + SUBMENU_OVERLAP - width; + } + + let y = itemRect.top - box.borderTop - box.paddingTop - hy; + if (y + height > py + ch + hy) { + y = itemRect.bottom + box.borderBottom + box.paddingBottom - height; + } + + style.transform = `translate(${Math.max(0, x)}px, ${Math.max(0, y)}px`; + style.opacity = "1"; +} + +export const SUBMENU_OVERLAP = 3; + +let transientWindowDataCache: IWindowData | null = null; +let transientCacheCounter: number = 0; + +function getWindowData(): IWindowData { + if (transientCacheCounter > 0) { + transientCacheCounter--; + return transientWindowDataCache!; + } + return _getWindowData(); +} + +function _getWindowData(): IWindowData { + return { + pageXOffset: window.pageXOffset, + pageYOffset: window.pageYOffset, + clientWidth: document.documentElement.clientWidth, + clientHeight: document.documentElement.clientHeight, + }; +} + +interface IWindowData { + pageXOffset: number; + pageYOffset: number; + clientWidth: number; + clientHeight: number; +} diff --git a/packages/perspective-workspace/src/ts/workspace/tabbar.ts b/packages/perspective-workspace/src/ts/workspace/tabbar.ts index 363b78c962..263c5dece6 100644 --- a/packages/perspective-workspace/src/ts/workspace/tabbar.ts +++ b/packages/perspective-workspace/src/ts/workspace/tabbar.ts @@ -13,20 +13,16 @@ import { ArrayExt } from "@lumino/algorithm"; import { ElementExt } from "@lumino/domutils"; import { TabBar } from "@lumino/widgets"; -import { - TabBarItems, - DEFAULT_TITLE, - PerspectiveTabBarRenderer, -} from "./tabbarrenderer"; -import { VirtualDOM, VirtualElement } from "@lumino/virtualdom"; +import { TabBarItems, PerspectiveTabBarRenderer } from "./tabbarrenderer"; +import { VirtualDOM } from "@lumino/virtualdom"; import { CommandRegistry } from "@lumino/commands"; -import { MenuRenderer } from "./menu"; import { Menu } from "@lumino/widgets"; import { PerspectiveWorkspace } from "./workspace"; import { Message } from "@lumino/messaging"; import { Title } from "@lumino/widgets"; import { Signal } from "@lumino/signaling"; import { ReadonlyJSONObject, ReadonlyJSONValue } from "@lumino/coreutils"; +import { WorkspaceMenu } from "./menu"; export class PerspectiveTabBar extends TabBar { _workspace: PerspectiveWorkspace; @@ -97,8 +93,10 @@ export class PerspectiveTabBar extends TabBar { onClick(otherTitles: Title[], index: number, event: MouseEvent) { const commands = new CommandRegistry(); - const renderer = new MenuRenderer(); - this._menu = new Menu({ commands, renderer }); + this._menu = new WorkspaceMenu(this._workspace.menu_elem.shadowRoot!, { + commands, + }); + this._menu.addClass("perspective-workspace-menu"); this._menu.dataset.minwidth = this.__titles[index]; for (const title of otherTitles) { @@ -131,10 +129,12 @@ export class PerspectiveTabBar extends TabBar { }); const box = (event.target as HTMLElement).getBoundingClientRect(); - this._menu.open(box.x, box.y + box.height); + const outer_box = this._workspace.element.getBoundingClientRect(); + this._menu.open(box.x - outer_box.x, box.y + box.height - outer_box.y); this._menu.aboutToClose.connect(() => { this._menu = undefined; }); + event.preventDefault(); event.stopPropagation(); } diff --git a/packages/perspective-workspace/src/ts/workspace/widget.ts b/packages/perspective-workspace/src/ts/workspace/widget.ts index 0f9e12e3ae..59a615370f 100644 --- a/packages/perspective-workspace/src/ts/workspace/widget.ts +++ b/packages/perspective-workspace/src/ts/workspace/widget.ts @@ -25,6 +25,7 @@ export class PerspectiveViewerWidget extends Widget { viewer: psp_viewer.HTMLPerspectiveViewerElement; _title: string; _is_table_loaded: boolean; + _is_pivoted: boolean; _restore_config?: () => Promise; task?: Promise; @@ -33,6 +34,7 @@ export class PerspectiveViewerWidget extends Widget { this.viewer = viewer; this._title = ""; this._is_table_loaded = false; + this._is_pivoted = false; } get name(): string { @@ -65,16 +67,7 @@ export class PerspectiveViewerWidget extends Widget { this.viewer.setAttribute("table", table); } - // if (selectable) { - // this.viewer.setAttribute("selectable", selectable); - // } - - // if (editable) { - // this.viewer.setAttribute("editable", editable); - // } - const restore_config = () => this.viewer.restore({ ...viewerConfig }); - if (this._is_table_loaded) { return restore_config(); } else { @@ -88,14 +81,8 @@ export class PerspectiveViewerWidget extends Widget { table: this.viewer.getAttribute("table"), }; - if (this.viewer.hasAttribute("selectable")) { - config.selectable = this.viewer.getAttribute("selectable"); - } - - if (this.viewer.hasAttribute("editable")) { - config.editable = this.viewer.getAttribute("editable"); - } - + delete config["theme"]; + delete config["settings"]; return config; } diff --git a/packages/perspective-workspace/src/ts/workspace/workspace.ts b/packages/perspective-workspace/src/ts/workspace/workspace.ts index e2d256d01d..e617c150f7 100644 --- a/packages/perspective-workspace/src/ts/workspace/workspace.ts +++ b/packages/perspective-workspace/src/ts/workspace/workspace.ts @@ -12,30 +12,21 @@ import { find, toArray } from "@lumino/algorithm"; import { CommandRegistry } from "@lumino/commands"; -import { - SplitPanel, - Panel, - Menu, - DockPanel, - Title, - Widget, -} from "@lumino/widgets"; -import { Slot } from "@lumino/signaling"; +import { SplitPanel, Panel, DockPanel } from "@lumino/widgets"; import uniqBy from "lodash/uniqBy"; -import { DebouncedFunc } from "lodash"; +import { DebouncedFunc, isEqual } from "lodash"; import debounce from "lodash/debounce"; import type { HTMLPerspectiveViewerElement, ViewerConfigUpdate, } from "@finos/perspective-viewer"; import type * as psp from "@finos/perspective"; - +import injectedStyles from "../../../build/css/injected.css"; import { PerspectiveDockPanel } from "./dockpanel"; -import { MenuRenderer } from "./menu"; +import { WorkspaceMenu } from "./menu"; import { createCommands } from "./commands"; import { PerspectiveViewerWidget } from "./widget"; import { ObservableMap } from "../utils/observable_map"; -import { ReadonlyJSONObject } from "@lumino/coreutils"; const DEFAULT_WORKSPACE_SIZE = [1, 3]; @@ -62,19 +53,20 @@ export class PerspectiveWorkspace extends SplitPanel { private dockpanel: PerspectiveDockPanel; private detailPanel: Panel; private masterPanel: SplitPanel; - private element: HTMLElement; + element: HTMLElement; + menu_elem: HTMLElement; private _tables: ObservableMap>; private listeners: WeakMap void>; private indicator: HTMLElement; private commands: CommandRegistry; - private menuRenderer: MenuRenderer; + private _menu?: WorkspaceMenu; private _minimizedLayoutSlots?: DockPanel.ILayoutConfig; private _minimizedLayout?: DockPanel.ILayoutConfig; private _maximizedWidget?: PerspectiveViewerWidget; - private _save?: DebouncedFunc<() => false | Promise>; - private _context_menu?: Menu & { init_overlay?: () => void }; + private _last_updated_state?: PerspectiveWorkspaceConfig; + // private _context_menu?: Menu & { init_overlay?: () => void }; - constructor(element: HTMLElement, options = {}) { + constructor(element: HTMLElement) { super({ orientation: "horizontal" }); this.addClass("perspective-workspace"); this.dockpanel = new PerspectiveDockPanel(this); @@ -84,7 +76,10 @@ export class PerspectiveWorkspace extends SplitPanel { this.detailPanel.addWidget(this.dockpanel); this.masterPanel = new SplitPanel({ orientation: "vertical" }); this.masterPanel.addClass("master-panel"); - this.dockpanel.layoutModified.connect(() => this.workspaceUpdated()); + this.dockpanel.layoutModified.connect(() => { + this.workspaceUpdated(); + }); + this.addWidget(this.detailPanel); this.spacing = 6; this.element = element; @@ -94,14 +89,22 @@ export class PerspectiveWorkspace extends SplitPanel { this._tables.addDeleteListener(this._delete_listener.bind(this)); this.indicator = this.init_indicator(); this.commands = createCommands(this, this.indicator); - this.menuRenderer = new MenuRenderer(); + this.menu_elem = document.createElement("perspective-workspace-menu"); + this.menu_elem.attachShadow({ mode: "open" }); + this.menu_elem.shadowRoot!.innerHTML = ``; + + this.element.shadowRoot!.insertBefore( + this.menu_elem, + this.element.shadowRoot!.lastElementChild! + ); + element.addEventListener("contextmenu", (event) => this.showContextMenu(null, event) ); } - get_context_menu(): (Menu & { init_overlay?: () => void }) | undefined { - return this._context_menu; + get_context_menu(): WorkspaceMenu | undefined { + return this._menu; } get_dock_panel(): PerspectiveDockPanel { @@ -638,19 +641,16 @@ export class PerspectiveWorkspace extends SplitPanel { */ createContextMenu(widget: PerspectiveViewerWidget | null) { - const contextMenu: Menu & { init_overlay?: () => void } = new Menu({ + this._menu = new WorkspaceMenu(this.menu_elem.shadowRoot!, { commands: this.commands, - renderer: this.menuRenderer, }); - this._context_menu = contextMenu; - const tabbar = find( this.dockpanel.tabBars(), (bar) => bar.currentTitle?.owner === widget ); - const init_overlay = () => { + this._menu.init_overlay = () => { if (widget) { widget.addClass("context-focus"); widget.viewer.classList.add("context-focus"); @@ -660,23 +660,24 @@ export class PerspectiveWorkspace extends SplitPanel { if ( widget.viewer.classList.contains("workspace-master-widget") ) { - contextMenu.node.classList.add("workspace-master-menu"); + this._menu!.node.classList.add("workspace-master-menu"); } else { - contextMenu.node.classList.remove("workspace-master-menu"); + this._menu!.node.classList.remove("workspace-master-menu"); } } }; - contextMenu.init_overlay = init_overlay; if (widget?.parent === this.dockpanel || widget === null) { - contextMenu.addItem({ + this._menu.addItem({ type: "submenu", command: "workspace:newmenu", submenu: (() => { - const submenu = new Menu({ - commands: this.commands, - renderer: this.menuRenderer, - }); + const submenu = new WorkspaceMenu( + this.menu_elem.shadowRoot!, + { + commands: this.commands, + } + ); for (const table of this.tables.keys()) { let args; @@ -725,7 +726,6 @@ export class PerspectiveWorkspace extends SplitPanel { } submenu.title.label = "New Table"; - return submenu; })(), }); @@ -733,51 +733,51 @@ export class PerspectiveWorkspace extends SplitPanel { if (widget) { if (widget?.parent === this.dockpanel) { - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); } - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:duplicate", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:master", args: { widget_name: widget.name }, }); - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:settings", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:reset", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:export", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:copy", args: { widget_name: widget.name }, }); - contextMenu.addItem({ type: "separator" }); + this._menu.addItem({ type: "separator" }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:close", args: { widget_name: widget.name }, }); - contextMenu.addItem({ + this._menu.addItem({ command: "workspace:help", }); } - contextMenu.aboutToClose.connect(() => { + this._menu.aboutToClose.connect(() => { if (widget) { this.element.classList.remove("context-menu"); this.removeClass("context-menu"); @@ -786,14 +786,18 @@ export class PerspectiveWorkspace extends SplitPanel { } }); - return contextMenu; + return this._menu; } showContextMenu(widget: PerspectiveViewerWidget | null, event: MouseEvent) { if (!event.shiftKey) { const menu = this.createContextMenu(widget); menu.init_overlay?.(); - menu.open(event.clientX, event.clientY); + const rect = this.element.getBoundingClientRect(); + menu.open(event.clientX - rect.x, event.clientY - rect.y, { + host: this.menu_elem.shadowRoot as unknown as HTMLElement, + }); + event.preventDefault(); event.stopPropagation(); } @@ -823,11 +827,12 @@ export class PerspectiveWorkspace extends SplitPanel { } addViewer(config: ViewerConfigUpdateExt, is_global_filter?: boolean) { - const widget = this._createWidgetAndNode({ config }); if (this.dockpanel.mode === "single-document") { + const _task = this._maximizedWidget!.viewer.toggleConfig(false); this._unmaximize(); } + const widget = this._createWidgetAndNode({ config }); if (is_global_filter) { if (!this.masterPanel.isAttached) { this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE); @@ -961,6 +966,7 @@ export class PerspectiveWorkspace extends SplitPanel { const updated = async (event: CustomEvent) => { this.workspaceUpdated(); widget.title.label = event.detail.title; + widget._is_pivoted = event.detail.group_by?.length > 0; }; widget.node.addEventListener("contextmenu", contextMenu); @@ -968,10 +974,6 @@ export class PerspectiveWorkspace extends SplitPanel { // @ts-ignore widget.viewer.addEventListener("perspective-config-update", updated); - widget.viewer.addEventListener( - "perspective-plugin-update", - this.workspaceUpdated.bind(this) - ); this.listeners.set(widget, () => { widget.node.removeEventListener("contextmenu", contextMenu); @@ -985,11 +987,6 @@ export class PerspectiveWorkspace extends SplitPanel { "perspective-config-update", updated ); - - widget.viewer.removeEventListener( - "perspective-plugin-update", - this.workspaceUpdated.bind(this) - ); }); } @@ -1010,9 +1007,18 @@ export class PerspectiveWorkspace extends SplitPanel { * */ - async _fireUpdateEvent() { + async workspaceUpdated() { const layout = await this.save(); if (layout) { + if (this._last_updated_state) { + if (isEqual(this._last_updated_state, layout)) { + return; + } + } + + this._last_updated_state = + layout as any as PerspectiveWorkspaceConfig; + const tables: Record> = {}; this.tables.forEach((value, key) => { tables[key] = value; @@ -1025,17 +1031,4 @@ export class PerspectiveWorkspace extends SplitPanel { ); } } - - async workspaceUpdated() { - if (!this._save) { - this._save = debounce( - () => - this.dockpanel.mode !== "single-document" && - this._fireUpdateEvent(), - 500 - ); - } - - this._save(); - } } diff --git a/packages/perspective-workspace/test/js/restore.spec.js b/packages/perspective-workspace/test/js/restore.spec.js index 125abe83eb..a5d92caf7d 100644 --- a/packages/perspective-workspace/test/js/restore.spec.js +++ b/packages/perspective-workspace/test/js/restore.spec.js @@ -84,6 +84,7 @@ function tests(context, compare) { }, config); await page.evaluate(async () => { + const workspace = document.getElementById("workspace"); await workspace.flush(); }); diff --git a/rust/bundle/main.rs b/rust/bundle/main.rs index c57749678a..770338d01f 100644 --- a/rust/bundle/main.rs +++ b/rust/bundle/main.rs @@ -67,6 +67,7 @@ fn bindgen(outdir: &Path, artifact: &str, is_release: bool) { Bindgen::new() .web(true) .unwrap() + .keep_debug(!is_release) .input_path(input) .typescript(true) .out_name(&format!("{}.wasm", artifact.replace('_', "-"))) diff --git a/rust/lint/Cargo.toml b/rust/lint/Cargo.toml index 2d138b8efd..36b4e76597 100644 --- a/rust/lint/Cargo.toml +++ b/rust/lint/Cargo.toml @@ -13,7 +13,7 @@ [package] name = "perspective-lint" description = "A CLI utility to lint rust code" -version = "3.1.6" +version = "3.1.8" edition = "2021" publish = false diff --git a/rust/perspective-client/Cargo.toml b/rust/perspective-client/Cargo.toml index e8a9cb32dd..f1b58ac6c2 100644 --- a/rust/perspective-client/Cargo.toml +++ b/rust/perspective-client/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective-client" -version = "3.1.6" +version = "3.1.8" authors = ["Andrew Stein "] edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." diff --git a/rust/perspective-client/docs/table.md b/rust/perspective-client/docs/table.md index 2217a0abe6..06f8cc5c15 100644 --- a/rust/perspective-client/docs/table.md +++ b/rust/perspective-client/docs/table.md @@ -279,3 +279,35 @@ table.replace(df)
`limit` cannot be used in conjunction with `index`.
+ +# JSON Input Data + +Perspective supports many kinds of input data, including two formats of JSON +data: row-oriented and column-oriented data. + +## Row Oriented JSON + +Row-oriented JSON is in the form of a list of objects. Each object in the list +corresponds to a row in the table. For example: + +```json +[ + { "a": 86, "b": false, "c": "words" }, + { "a": 0, "b": true, "c": "" }, + { "a": 12345, "b": false, "c": "here" } +] +``` + +## Column Oriented JSON + +Column-Oriented JSON comes in the form of an object of lists. Each key of the +object is a column name, and each element of the list is the corresponding value +in the row. + +```json +{ + "a": [86, 0, 12345], + "b": [false, true, false], + "c": ["words", "", "here"] +} +``` diff --git a/rust/perspective-client/package.json b/rust/perspective-client/package.json index 2e2d0268f3..b181aceecb 100644 --- a/rust/perspective-client/package.json +++ b/rust/perspective-client/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-client", - "version": "3.1.6", + "version": "3.1.8", "description": "", "private": true, "repository": { diff --git a/rust/perspective-js/Cargo.toml b/rust/perspective-js/Cargo.toml index 7bf31adf8e..97ca80b799 100644 --- a/rust/perspective-js/Cargo.toml +++ b/rust/perspective-js/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective-js" -version = "3.1.6" +version = "3.1.8" authors = ["Andrew Stein "] edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." @@ -47,7 +47,7 @@ anyhow = "1.0.66" wasm-bindgen-test = "0.3.13" [dependencies] -perspective-client = { path = "../perspective-client", version = "3.1.6" } +perspective-client = { path = "../perspective-client", version = "3.1.8" } base64 = "0.13.0" chrono = "0.4" extend = "1.1.2" diff --git a/rust/perspective-js/package.json b/rust/perspective-js/package.json index 414b2883db..d052bd72f1 100644 --- a/rust/perspective-js/package.json +++ b/rust/perspective-js/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective", - "version": "3.1.6", + "version": "3.1.8", "description": "", "repository": { "type": "git", diff --git a/rust/perspective-python/Cargo.toml b/rust/perspective-python/Cargo.toml index 66d6055a21..6b4307d0e9 100644 --- a/rust/perspective-python/Cargo.toml +++ b/rust/perspective-python/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective-python" -version = "3.1.6" +version = "3.1.8" edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." repository = "https://github.com/finos/perspective" @@ -61,8 +61,8 @@ python-config-rs = "0.1.2" # NOTE: when building from the git repo, these perspective-* dependencies are # overridden with path dependencies in .cargo/config.toml. This is done to # support the sdist, which doesn't include these packages. -perspective-client = { version = "3.1.6" } -perspective-server = { version = "3.1.6" } +perspective-client = { version = "3.1.8" } +perspective-server = { version = "3.1.8" } async-lock = "2.5.0" pollster = "0.3.0" extend = "1.1.2" diff --git a/rust/perspective-python/package.json b/rust/perspective-python/package.json index 9ebea4c1cb..8381ddc4a0 100644 --- a/rust/perspective-python/package.json +++ b/rust/perspective-python/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-python", - "version": "3.1.6", + "version": "3.1.8", "description": "", "private": true, "repository": { diff --git a/rust/perspective-python/perspective/__init__.py b/rust/perspective-python/perspective/__init__.py index fec5959597..f408157acd 100644 --- a/rust/perspective-python/perspective/__init__.py +++ b/rust/perspective-python/perspective/__init__.py @@ -10,7 +10,7 @@ # ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ # ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -__version__ = "3.1.6" +__version__ = "3.1.8" __all__ = [ "_jupyter_labextension_paths", "Server", diff --git a/rust/perspective-python/perspective/handlers/aiohttp.py b/rust/perspective-python/perspective/handlers/aiohttp.py index 421889720d..f4d459a217 100644 --- a/rust/perspective-python/perspective/handlers/aiohttp.py +++ b/rust/perspective-python/perspective/handlers/aiohttp.py @@ -37,11 +37,12 @@ class PerspectiveAIOHTTPHandler(object): def __init__(self, **kwargs): self.server = kwargs.pop("perspective_server", perspective.GLOBAL_SERVER) self._request = kwargs.pop("request") + self._loop = kwargs.pop("loop", asyncio.get_event_loop()) super().__init__(**kwargs) async def run(self) -> web.WebSocketResponse: def inner(msg): - asyncio.get_running_loop().create_task(self._ws.send_bytes(msg)) + self._loop.create_task(self._ws.send_bytes(msg)) self.session = self.server.new_session(inner) try: diff --git a/rust/perspective-python/perspective/handlers/starlette.py b/rust/perspective-python/perspective/handlers/starlette.py index 4a12fd0194..963ba1c3ef 100644 --- a/rust/perspective-python/perspective/handlers/starlette.py +++ b/rust/perspective-python/perspective/handlers/starlette.py @@ -34,11 +34,12 @@ class PerspectiveStarletteHandler(object): def __init__(self, **kwargs): self._server = kwargs.pop("perspective_server", perspective.GLOBAL_SERVER) self._websocket = kwargs.pop("websocket") + self._loop = kwargs.pop("loop", asyncio.get_event_loop()) super().__init__(**kwargs) async def run(self) -> None: def inner(msg): - asyncio.get_running_loop().create_task(self._websocket.send_bytes(msg)) + self._loop.create_task(self._websocket.send_bytes(msg)) self.session = self._server.new_session(inner) diff --git a/rust/perspective-python/perspective/handlers/tornado.py b/rust/perspective-python/perspective/handlers/tornado.py index 4abae77c73..a2adf35f5b 100644 --- a/rust/perspective-python/perspective/handlers/tornado.py +++ b/rust/perspective-python/perspective/handlers/tornado.py @@ -45,8 +45,9 @@ class PerspectiveTornadoHandler(WebSocketHandler): def check_origin(self, origin): return True - def initialize(self, perspective_server=perspective.GLOBAL_SERVER): + def initialize(self, perspective_server=perspective.GLOBAL_SERVER, loop=None): self.server = perspective_server + self.loop = loop or IOLoop.current() def open(self): def inner(msg): @@ -63,4 +64,4 @@ def on_message(self, msg: bytes): return self.session.handle_request(msg) - IOLoop.current().call_later(0, self.session.poll) + self.loop.call_later(0, self.session.poll) diff --git a/rust/perspective-python/pyproject.toml b/rust/perspective-python/pyproject.toml index a32edf9fa9..ec591f2148 100644 --- a/rust/perspective-python/pyproject.toml +++ b/rust/perspective-python/pyproject.toml @@ -44,7 +44,7 @@ starlette = ["starlette<1"] [tool.maturin] module-name = "perspective" -data = "perspective_python-3.1.6.data" +data = "perspective_python-3.1.8.data" features = ["pyo3/extension-module"] include = [ { path = "perspective/*libpsp.so", format = "wheel" }, diff --git a/rust/perspective-server/Cargo.toml b/rust/perspective-server/Cargo.toml index 1138589cea..15e8b5581b 100644 --- a/rust/perspective-server/Cargo.toml +++ b/rust/perspective-server/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective-server" -version = "3.1.6" +version = "3.1.8" authors = ["Andrew Stein "] edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." @@ -47,7 +47,7 @@ shlex = "1.3.0" [dependencies] link-cplusplus = "1.0.9" -perspective-client = { version = "3.1.6", path = "../perspective-client" } +perspective-client = { version = "3.1.8", path = "../perspective-client" } async-lock = "2.5.0" tracing = { version = ">=0.1.36" } futures = "0.3" diff --git a/rust/perspective-viewer/Cargo.toml b/rust/perspective-viewer/Cargo.toml index 01ff8c9233..5178bd4d73 100644 --- a/rust/perspective-viewer/Cargo.toml +++ b/rust/perspective-viewer/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective-viewer" -version = "3.1.6" +version = "3.1.8" authors = ["Andrew Stein "] edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." @@ -46,8 +46,8 @@ anyhow = "1.0.66" wasm-bindgen-test = "0.3.13" [dependencies] -perspective-client = { path = "../perspective-client", version = "3.1.6" } -perspective-js = { path = "../perspective-js", version = "3.1.6" } +perspective-client = { path = "../perspective-client", version = "3.1.8" } +perspective-js = { path = "../perspective-js", version = "3.1.8" } # Provides async `Mutex` for locked sections such as `render` async-lock = "2.5.0" @@ -83,7 +83,7 @@ js-sys = "0.3.64" nom = "7.1.1" # MessagePack serialization -rmp-serde = "1.1.1" +rmp-serde = "1.3.0" # Serialization for tokens and JS APIs serde = { version = "1.0", features = ["derive"] } diff --git a/rust/perspective-viewer/package.json b/rust/perspective-viewer/package.json index 24cd17b3ff..b777879436 100644 --- a/rust/perspective-viewer/package.json +++ b/rust/perspective-viewer/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-viewer", - "version": "3.1.6", + "version": "3.1.8", "description": "The `` Custom Element, frontend for Perspective.js", "repository": { "type": "git", diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 75db84eec1..80a61259ce 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -14,6 +14,10 @@ --settings-panel-z-index: 10; } +::slotted(*) { + pointer-events: var(--override-content-pointer-events); +} + :host .sidebar_close_button { position: absolute; top: 0; diff --git a/rust/perspective-viewer/src/rust/config/viewer_config.rs b/rust/perspective-viewer/src/rust/config/viewer_config.rs index 4991f4ca48..66b7b73402 100644 --- a/rust/perspective-viewer/src/rust/config/viewer_config.rs +++ b/rust/perspective-viewer/src/rust/config/viewer_config.rs @@ -120,14 +120,14 @@ impl ViewerConfig { match format { Some(ViewerConfigEncoding::String) => { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); - let bytes = rmp_serde::to_vec(&self.token())?; + let bytes = rmp_serde::to_vec_named(&self.token())?; encoder.write_all(&bytes)?; let encoded = encoder.finish()?; Ok(JsValue::from(base64::encode(encoded))) }, Some(ViewerConfigEncoding::ArrayBuffer) => { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::default()); - let bytes = rmp_serde::to_vec(&self.token())?; + let bytes = rmp_serde::to_vec_named(&self.token())?; encoder.write_all(&bytes)?; let encoded = encoder.finish()?; let array = js_sys::Uint8Array::from(&encoded[..]); diff --git a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs index df49f5ad5b..a117fef0b5 100644 --- a/rust/perspective-viewer/src/rust/custom_elements/viewer.rs +++ b/rust/perspective-viewer/src/rust/custom_elements/viewer.rs @@ -664,7 +664,7 @@ impl PerspectiveViewerElement { .collect(); let theme_name = presentation.get_selected_theme_name().await; - presentation.reset_available_themes(themes).await; + let mut changed = presentation.reset_available_themes(themes).await; let reset_theme = presentation .get_available_themes() .await? @@ -672,12 +672,14 @@ impl PerspectiveViewerElement { .find(|y| theme_name.as_ref() == Some(y)) .cloned(); - presentation.set_theme_name(reset_theme.as_deref()).await?; - if let Some(view) = session.get_view() { - renderer.restyle_all(&view).await - } else { - Ok(JsValue::UNDEFINED) + changed = presentation.set_theme_name(reset_theme.as_deref()).await? || changed; + if changed { + if let Some(view) = session.get_view() { + return renderer.restyle_all(&view).await; + } } + + Ok(JsValue::UNDEFINED) }) } diff --git a/rust/perspective-viewer/src/rust/presentation.rs b/rust/perspective-viewer/src/rust/presentation.rs index 07f7b4c78c..c1675d0985 100644 --- a/rust/perspective-viewer/src/rust/presentation.rs +++ b/rust/perspective-viewer/src/rust/presentation.rs @@ -11,7 +11,7 @@ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ use std::cell::RefCell; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::ops::Deref; use std::rc::Rc; @@ -186,9 +186,20 @@ impl Presentation { /// Reset the state. `styleSheets` will be re-parsed next time /// `get_themes()` is called if the `themes` argument is `None`. - pub async fn reset_available_themes(&self, themes: Option>) { + /// + /// # Returns + /// A `bool` indicating whether the internal state changed. + pub async fn reset_available_themes(&self, themes: Option>) -> bool { + fn as_set(x: &Option>) -> HashSet<&'_ String> { + x.as_ref() + .map(|x| x.iter().collect::>()) + .unwrap_or_default() + } + let mut mutex = self.0.theme_data.lock().await; + let changed = as_set(&mutex.themes) != as_set(&themes); mutex.themes = themes; + changed } pub async fn get_selected_theme_config(&self) -> ApiResult<(Vec, Option)> { @@ -218,8 +229,17 @@ impl Presentation { } /// Set the theme by name, or `None` for the default theme. - pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult<()> { - let (themes, _) = self.get_selected_theme_config().await?; + /// + /// # Returns + /// A `bool` indicating whether the internal state changed. + pub async fn set_theme_name(&self, theme: Option<&str>) -> ApiResult { + let (themes, selected) = self.get_selected_theme_config().await?; + if let Some(x) = selected { + if themes.get(x).map(|x| x.as_str()) == theme { + return Ok(false); + } + } + let index = if let Some(theme) = theme { self.set_theme_attribute(Some(theme))?; themes.iter().position(|x| x == theme) @@ -232,7 +252,7 @@ impl Presentation { }; self.theme_config_updated.emit((themes, index)); - Ok(()) + Ok(true) } /// Returns an owned copy of the curent column configuration map. diff --git a/rust/perspective-viewer/test/js/save_restore.spec.js b/rust/perspective-viewer/test/js/save_restore.spec.js index c51ceb44dd..5ea57413b7 100644 --- a/rust/perspective-viewer/test/js/save_restore.spec.js +++ b/rust/perspective-viewer/test/js/save_restore.spec.js @@ -142,4 +142,69 @@ test.describe("Save/Restore", async () => { "restore-restores-config-from-save.txt", ]); }); + + test("save/restore works in string format", async ({ page }) => { + const config = await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ + settings: true, + group_by: ["State"], + columns: ["Profit", "Sales"], + }); + return await viewer.save("string"); + }); + + const config3 = await page.evaluate(async (config) => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.reset(); + await viewer.restore(config); + return await viewer.save(); + }, config); + + expect(config3).toEqual({ + ...DEFAULT_CONFIG, + columns: ["Profit", "Sales"], + plugin: "Debug", + group_by: ["State"], + settings: true, + theme: "Pro Light", + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents, [ + "save-restore-works-in-string-format.txt", + ]); + }); + + test("save/restore works in arraybuffer format", async ({ page }) => { + const config3 = await page.evaluate(async () => { + const viewer = document.querySelector("perspective-viewer"); + await viewer.getTable(); + await viewer.restore({ + settings: true, + group_by: ["State"], + columns: ["Profit", "Sales"], + }); + + const config = await viewer.save("arraybuffer"); + await viewer.reset(); + await viewer.restore(config); + return await viewer.save(); + }); + + expect(config3).toEqual({ + ...DEFAULT_CONFIG, + columns: ["Profit", "Sales"], + plugin: "Debug", + group_by: ["State"], + settings: true, + theme: "Pro Light", + }); + + const contents = await get_contents(page); + await compareContentsToSnapshot(contents, [ + "save-restore-works-in-arraybuffer-format.txt", + ]); + }); }); diff --git a/rust/perspective/Cargo.toml b/rust/perspective/Cargo.toml index 34f53ae339..387a86ddd7 100644 --- a/rust/perspective/Cargo.toml +++ b/rust/perspective/Cargo.toml @@ -12,7 +12,7 @@ [package] name = "perspective" -version = "3.1.6" +version = "3.1.8" authors = ["Andrew Stein "] edition = "2021" description = "A data visualization and analytics component, especially well-suited for large and/or streaming datasets." @@ -38,8 +38,8 @@ external-cpp = [ [dependencies] async-lock = "2.5.0" -perspective-client = { version = "3.1.6", path = "../perspective-client" } -perspective-server = { version = "3.1.6", path = "../perspective-server" } +perspective-client = { version = "3.1.8", path = "../perspective-client" } +perspective-server = { version = "3.1.8", path = "../perspective-server" } tracing = { version = ">=0.1.36" } axum = { version = ">=0.7,<2", features = ["ws"], optional = true } diff --git a/rust/perspective/package.json b/rust/perspective/package.json index 6ec4f04f01..076984e820 100644 --- a/rust/perspective/package.json +++ b/rust/perspective/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-rs", - "version": "3.1.6", + "version": "3.1.8", "description": "", "private": true, "repository": { diff --git a/tools/perspective-bench/basic_suite.mjs b/tools/perspective-bench/basic_suite.mjs index 7fc94a86f4..1f8e5cf2db 100644 --- a/tools/perspective-bench/basic_suite.mjs +++ b/tools/perspective-bench/basic_suite.mjs @@ -43,7 +43,7 @@ perspective_bench.suite( const { default: perspective } = await import("@finos/perspective"); client = await perspective.websocket(path); metadata = { - version: "3.1.6", + version: "3.1.8", version_idx, }; } else { diff --git a/tools/perspective-scripts/package.json b/tools/perspective-scripts/package.json index 77f9cfff2a..f7905fc8ed 100644 --- a/tools/perspective-scripts/package.json +++ b/tools/perspective-scripts/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-scripts", - "version": "3.1.6", + "version": "3.1.8", "description": "Build scripts based on perspective", "private": true, "files": [ diff --git a/tools/perspective-test/package.json b/tools/perspective-test/package.json index b689bea916..17b56c8730 100644 --- a/tools/perspective-test/package.json +++ b/tools/perspective-test/package.json @@ -1,6 +1,6 @@ { "name": "@finos/perspective-test", - "version": "3.1.6", + "version": "3.1.8", "description": "Test utility based on perspective", "private": true, "main": "src/js/index.ts", diff --git a/tools/perspective-test/results.tar.gz b/tools/perspective-test/results.tar.gz index 2f686a40d1..671ec179c3 100644 Binary files a/tools/perspective-test/results.tar.gz and b/tools/perspective-test/results.tar.gz differ diff --git a/tools/perspective-test/src/html/workspace-test.html b/tools/perspective-test/src/html/workspace-test.html index 3dda82c296..eabcbd27f4 100644 --- a/tools/perspective-test/src/html/workspace-test.html +++ b/tools/perspective-test/src/html/workspace-test.html @@ -23,11 +23,17 @@ padding: 0; overflow: hidden; } - #container { width: 100%; height: 100%; } + perspective-workspace { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } perspective-workspace, perspective-viewer { font-family: "Roboto Mono" !important;