diff --git a/.gitignore b/.gitignore index 5823b03643..3130ad18c3 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ __pycache__/ .clangd .DS_Store .emsdk +.pnpm-store .ipynb_checkpoints .perspectiverc .vscode/* diff --git a/docs/src/components/Demo/layouts.js b/docs/src/components/Demo/layouts.js index 4c4f82f758..c2fb3c9518 100644 --- a/docs/src/components/Demo/layouts.js +++ b/docs/src/components/Demo/layouts.js @@ -31,6 +31,7 @@ export const LAYOUTS = { fg_gradient: 17.4, }, }, + group_rollup_mode: "rollup", settings: true, title: "Market Monitor", group_by: ["name"], @@ -52,6 +53,7 @@ export const LAYOUTS = { plugin: "datagrid", title: "Blotter", columns: ["ask", "bid", "chg"], + group_rollup_mode: "rollup", sort: [ ["name", "desc"], ["lastUpdate", "desc"], @@ -63,6 +65,7 @@ export const LAYOUTS = { }, "x bar": { title: "Px (Δ)", + group_rollup_mode: "flat", columns: ["chg"], plugin: "X Bar", sort: [["chg", "asc"]], @@ -71,6 +74,7 @@ export const LAYOUTS = { }, "y line": { title: "Time Series (Px)", + group_rollup_mode: "flat", plugin: "Y Line", group_by: ["lastUpdate"], split_by: [], @@ -81,6 +85,7 @@ export const LAYOUTS = { }, "xy scatter": { title: "Spread Scatter", + group_rollup_mode: "flat", plugin: "X/Y Scatter", group_by: ["name"], split_by: [], @@ -90,6 +95,7 @@ export const LAYOUTS = { }, treemap: { plugin: "Treemap", + group_rollup_mode: "flat", title: "Volume Map", group_by: ["name", "client"], split_by: [], @@ -101,6 +107,7 @@ export const LAYOUTS = { ], }, heatmap: { + group_rollup_mode: "flat", title: "Spread Heatmap", columns: ["name"], plugin: "Heatmap", diff --git a/examples/blocks/src/dataset/index.html b/examples/blocks/src/dataset/index.html index 720a92da1b..11c949f92f 100644 --- a/examples/blocks/src/dataset/index.html +++ b/examples/blocks/src/dataset/index.html @@ -175,13 +175,13 @@ const make_run_click_callback = (state) => async () => { state.table?.delete?.({ lazy: true }); state.table = gen_data(); - await window.psp_workspace.addTable("superstore", state.table); + // await window.psp_workspace.addTable("superstore", state.table); }; const make_del_click_callback = (state) => async () => { if (state.table) { // await viewer.eject(); - await window.psp_workspace.removeTable("superstore"); + // await window.psp_workspace.removeTable("superstore"); await state.table.then((x) => x.delete({ lazy: true })); state.table = undefined; } diff --git a/examples/blocks/src/fractal/index.js b/examples/blocks/src/fractal/index.js index 4ff4b48e8d..01309ac6ec 100644 --- a/examples/blocks/src/fractal/index.js +++ b/examples/blocks/src/fractal/index.js @@ -56,6 +56,7 @@ c`; function generate_layout(params) { return { plugin: "Heatmap", + table: "raw_data", settings: true, group_by: [`floor("index" / ${params.resolution})`], split_by: [`"index" % ${params.resolution}`], @@ -116,10 +117,16 @@ const make_run_click_callback = (worker, state) => async () => { window.run.disabled = true; if (!state.table) { - state.table = await worker.table({ - index: "integer", - }); - window.viewer.load(Promise.resolve(state.table)); + state.table = await worker.table( + { + index: "integer", + }, + { + name: "raw_data", + }, + ); + + window.viewer.load(worker); } const run = document.getElementById("run"); @@ -154,4 +161,5 @@ run.addEventListener( "click", make_run_click_callback(await perspective.worker(), {}), ); + run.dispatchEvent(new Event("click")); diff --git a/packages/viewer-d3fc/test/js/axisLabel.spec.ts b/packages/viewer-d3fc/test/js/axisLabel.spec.ts index 849d425dab..050e378c72 100644 --- a/packages/viewer-d3fc/test/js/axisLabel.spec.ts +++ b/packages/viewer-d3fc/test/js/axisLabel.spec.ts @@ -60,7 +60,7 @@ function confirmDataIsNotEpochForm(dateValues: any[]) { test.describe("Axis Values With Grouped Data With A Date Field In The Group", () => { test("X Bar y-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="X Bar"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-xbar"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -93,7 +93,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Bar x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Bar"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-ybar"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -126,7 +126,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("OHLC x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="OHLC"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-ohlc"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -159,40 +159,44 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Heatmap x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Heatmap"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-heatmap"); - const dateValues = await page.evaluate(async () => { - let viewer = document.querySelector("perspective-viewer"); + const dateValues = await page + .evaluate(async () => { + let viewer = document.querySelector("perspective-viewer"); - if (!viewer) { - return Error("Invalid Viewer"); - } + if (!viewer) { + return Error("Invalid Viewer"); + } - const plugin_element = viewer.querySelector( - `perspective-viewer-d3fc-heatmap`, - ); + const plugin_element = viewer.querySelector( + `perspective-viewer-d3fc-heatmap`, + ); - if (!plugin_element) { - throw Error("Invalid Plugin Element"); - } + if (!plugin_element) { + throw Error("Invalid Plugin Element"); + } - const shadowRoot = plugin_element.shadowRoot; - const dateTextElements = shadowRoot.querySelectorAll( - "div d3fc-group d3fc-svg.x-axis.bottom-axis svg g.group:last-child g.tick text", - ); + const shadowRoot = plugin_element.shadowRoot; + const dateTextElements = shadowRoot.querySelectorAll( + "div d3fc-group d3fc-svg.x-axis.bottom-axis svg g.group:last-child g.tick text", + ); - // collect and return the actual date data to be used. - return Array.from(dateTextElements).map((el) => - el.textContent?.trim(), - ); - }); + // collect and return the actual date data to be used. + return Array.from(dateTextElements).map((el) => + el.textContent?.trim(), + ); + }) + .catch((e) => e); + + await page.pause(); confirmDataIsNotEpochForm(dateValues); }); test("Y Line x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Line"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yline"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -225,7 +229,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Area x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Area"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yarea"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -258,7 +262,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("Y Scatter x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Y Scatter"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-yscatter"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); @@ -291,7 +295,7 @@ test.describe("Axis Values With Grouped Data With A Date Field In The Group", () test("CandleStick x-axis label with grouped data", async ({ page }) => { await page.click('div[data-plugin="Candlestick"]'); - await page.waitForSelector("perspective-viewer"); + await page.waitForSelector("perspective-viewer-d3fc-candlestick"); const dateValues = await page.evaluate(async () => { let viewer = document.querySelector("perspective-viewer"); diff --git a/packages/viewer-d3fc/test/js/line.spec.ts b/packages/viewer-d3fc/test/js/line.spec.ts index 7258a4e16b..1023b60762 100644 --- a/packages/viewer-d3fc/test/js/line.spec.ts +++ b/packages/viewer-d3fc/test/js/line.spec.ts @@ -72,9 +72,7 @@ test.describe("Line regressions", () => { ?.shadowRoot?.innerHTML; }); - compareContentsToSnapshot(out!, [ - "line-charts-denser-than-one-second-regression.txt", - ]); + await compareContentsToSnapshot(out!); }); test("Zoom on a chart with split Y axis renders the right axis", async ({ diff --git a/packages/viewer-datagrid/src/ts/style_handlers/body.ts b/packages/viewer-datagrid/src/ts/style_handlers/body.ts index c5b0d614e2..97462ae06e 100644 --- a/packages/viewer-datagrid/src/ts/style_handlers/body.ts +++ b/packages/viewer-datagrid/src/ts/style_handlers/body.ts @@ -65,18 +65,20 @@ export function applyBodyCellStyles( // Calculate aggregate depth visibility // @ts-ignore - metadata._is_hidden_by_aggregate_depth = ((x?: number) => - x === 0 || x === undefined - ? false - : x - 1 < - Math.min( - this._config.group_by.length, - plugin?.aggregate_depth || 0, - ))( - (metadata.row_header as unknown[] | undefined)?.filter( - (x) => x !== undefined, - )?.length, - ); + metadata._is_hidden_by_aggregate_depth = + this._config.group_rollup_mode === "rollup" && + ((x?: number) => + x === 0 || x === undefined + ? false + : x - 1 < + Math.min( + this._config.group_by.length, + plugin?.aggregate_depth || 0, + ))( + (metadata.row_header as unknown[] | undefined)?.filter( + (x) => x !== undefined, + )?.length, + ); // Apply type-specific cell styling if (is_numeric) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f6682d7e94..a1f72d589f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,11 +25,11 @@ catalogs: specifier: 3.0.0 version: 3.0.0 '@playwright/experimental-ct-react': - specifier: '=1.52.0' - version: 1.52.0 + specifier: '=1.58.0' + version: 1.58.0 '@playwright/test': - specifier: '=1.52.0' - version: 1.52.0 + specifier: '=1.58.0' + version: 1.58.0 '@prospective.co/procss': specifier: 0.1.18 version: 0.1.18 @@ -246,10 +246,10 @@ importers: version: link:packages/workspace '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 chalk: specifier: 'catalog:' version: 5.6.2 @@ -1050,10 +1050,10 @@ importers: version: link:../../tools/test '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 '@types/node': specifier: 'catalog:' version: 24.9.1 @@ -1142,13 +1142,16 @@ importers: version: link:../../tools/test '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 '@prospective.co/procss': specifier: 'catalog:' version: 0.1.18 + '@types/node': + specifier: 'catalog:' + version: 24.9.1 '@types/react': specifier: 'catalog:' version: 19.2.2 @@ -1266,10 +1269,10 @@ importers: version: 3.0.0 '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 auto-changelog: specifier: 'catalog:' version: 2.5.0 @@ -1299,10 +1302,10 @@ importers: dependencies: '@playwright/experimental-ct-react': specifier: 'catalog:' - version: 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) + version: 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1) '@playwright/test': specifier: 'catalog:' - version: 1.52.0 + version: 1.58.0 prettier: specifier: 'catalog:' version: 3.6.2 @@ -3331,17 +3334,17 @@ packages: resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} engines: {node: '>=8.0.0'} - '@playwright/experimental-ct-core@1.52.0': - resolution: {integrity: sha512-DiDEammXxt8OIFDfoNitoOZyHFJAu6aYi0abmHl0IZgOQHxccP6UX50aTEnSTTUWCfwUWB0Vd8TKJ6w122WJEw==} + '@playwright/experimental-ct-core@1.58.0': + resolution: {integrity: sha512-YZsjApZmRE78Kp2E6OtAvFFVheUyZDfrlZMf+lfnSshmYHrrJUy3bhdCe7EPCWsE12XfCVVAv6G0btiyAx8d0w==} engines: {node: '>=18'} - '@playwright/experimental-ct-react@1.52.0': - resolution: {integrity: sha512-r9gREinfeCAgnMp2Kpr6MnXSnKE06HlM0qWkortrtOHhD1xdGAT+mBBBP0YvPN2f169wGNIRuSOxp05MFZ+XaQ==} + '@playwright/experimental-ct-react@1.58.0': + resolution: {integrity: sha512-hm3Vddy1zNrTFfh2qwjuKvz8lY9oJm2iQkSITSMat4tztK5KxfwJrRyLGeZNCuzAy3bCWGk80Rb0ZfQ3Vitw+A==} engines: {node: '>=18'} hasBin: true - '@playwright/test@1.52.0': - resolution: {integrity: sha512-uh6W7sb55hl7D6vsAeA+V2p5JnlAqzhqFyF0VcJkKZXkgnFcVG9PziERRHQfPLfNGx1C292a4JqbWzhR8L4R1g==} + '@playwright/test@1.58.0': + resolution: {integrity: sha512-fWza+Lpbj6SkQKCrU6si4iu+fD2dD3gxNHFhUPxsfXBPhnv3rRSQVd0NtBUT9Z/RhF/boCBcuUaMUSTRTopjZg==} engines: {node: '>=18'} hasBin: true @@ -7224,13 +7227,13 @@ packages: resolution: {integrity: sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==} engines: {node: '>=14.16'} - playwright-core@1.52.0: - resolution: {integrity: sha512-l2osTgLXSMeuLZOML9qYODUQoPPnUsKsb5/P6LJ2e6uPKXUdPK5WYhN4z03G+YNbWmGDY4YENauNu4ZKczreHg==} + playwright-core@1.58.0: + resolution: {integrity: sha512-aaoB1RWrdNi3//rOeKuMiS65UCcgOVljU46At6eFcOFPFHWtd2weHRRow6z/n+Lec0Lvu0k9ZPKJSjPugikirw==} engines: {node: '>=18'} hasBin: true - playwright@1.52.0: - resolution: {integrity: sha512-JAwMNMBlxJ2oD1kce4KPtMkDeKGHQstdpFPcPH3maElAXon/QZeTvtsfXmTMRyO9TslfoYOXkSsvao2nE1ilTw==} + playwright@1.58.0: + resolution: {integrity: sha512-2SVA0sbPktiIY/MCOPX8e86ehA/e+tDNq+e5Y8qjKYti2Z/JG7xnronT/TXTIkKbYGWlCbuucZ6dziEgkoEjQQ==} engines: {node: '>=18'} hasBin: true @@ -12470,10 +12473,10 @@ snapshots: '@opentelemetry/api@1.9.0': {} - '@playwright/experimental-ct-core@1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': + '@playwright/experimental-ct-core@1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)': dependencies: - playwright: 1.52.0 - playwright-core: 1.52.0 + playwright: 1.58.0 + playwright-core: 1.58.0 vite: 6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' @@ -12488,9 +12491,9 @@ snapshots: - tsx - yaml - '@playwright/experimental-ct-react@1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1)': + '@playwright/experimental-ct-react@1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1))(yaml@2.8.1)': dependencies: - '@playwright/experimental-ct-core': 1.52.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) + '@playwright/experimental-ct-core': 1.58.0(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1) '@vitejs/plugin-react': 4.7.0(vite@6.4.1(@types/node@24.9.1)(jiti@1.21.7)(less@4.4.2)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.1)) transitivePeerDependencies: - '@types/node' @@ -12507,9 +12510,9 @@ snapshots: - vite - yaml - '@playwright/test@1.52.0': + '@playwright/test@1.58.0': dependencies: - playwright: 1.52.0 + playwright: 1.58.0 '@pnpm/config.env-replace@1.1.0': {} @@ -17048,11 +17051,11 @@ snapshots: dependencies: find-up: 6.3.0 - playwright-core@1.52.0: {} + playwright-core@1.58.0: {} - playwright@1.52.0: + playwright@1.58.0: dependencies: - playwright-core: 1.52.0 + playwright-core: 1.58.0 optionalDependencies: fsevents: 2.3.2 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cf3f70e6bd..d9a1b37ddf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -50,8 +50,8 @@ catalog: "@fontsource/roboto-mono": "4.5.10" "@iarna/toml": "3.0.0" "@jupyterlab/builder": "^4" - "@playwright/experimental-ct-react": "=1.52.0" - "@playwright/test": "=1.52.0" + "@playwright/experimental-ct-react": "=1.58.0" + "@playwright/test": "=1.58.0" "@prospective.co/procss": "0.1.18" "@types/d3": "^7.4.3" "@types/lodash": "^4.17.20" diff --git a/rust/perspective-client/src/rust/client.rs b/rust/perspective-client/src/rust/client.rs index 53a443ffa1..041c9edcc3 100644 --- a/rust/perspective-client/src/rust/client.rs +++ b/rust/perspective-client/src/rust/client.rs @@ -88,7 +88,7 @@ impl SystemInfo { /// Metadata about what features are supported by the `Server` to which this /// [`Client`] connects. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Features(Arc); impl Features { diff --git a/rust/perspective-client/src/rust/table.rs b/rust/perspective-client/src/rust/table.rs index 4070900383..f499e6d9e5 100644 --- a/rust/perspective-client/src/rust/table.rs +++ b/rust/perspective-client/src/rust/table.rs @@ -152,7 +152,7 @@ pub struct UpdateOptions { /// Result of a call to [`Table::validate_expressions`], containing a schema /// for valid expressions and error messages for invalid ones. -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct ExprValidationResult { pub expression_schema: Schema, pub errors: HashMap, diff --git a/rust/perspective-client/src/rust/virtual_server/features.rs b/rust/perspective-client/src/rust/virtual_server/features.rs index 97e1e952ad..03399dd556 100644 --- a/rust/perspective-client/src/rust/virtual_server/features.rs +++ b/rust/perspective-client/src/rust/virtual_server/features.rs @@ -24,7 +24,7 @@ use crate::proto::{ColumnType, GetFeaturesResp}; /// This struct is returned by /// [`VirtualServerHandler::get_features`](super::VirtualServerHandler::get_features) /// to inform clients about which operations are available. -#[derive(Debug, Default, Deserialize, Serialize)] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] pub struct Features<'a> { /// Whether group-by aggregation is supported. #[serde(default)] @@ -63,7 +63,7 @@ pub struct Features<'a> { /// /// Aggregates can either take no additional arguments ([`AggSpec::Single`]) /// or require column type arguments ([`AggSpec::Multiple`]). -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)] #[serde(untagged)] pub enum AggSpec<'a> { /// An aggregate function with no additional arguments. diff --git a/rust/perspective-js/src/rust/utils/futures.rs b/rust/perspective-js/src/rust/utils/futures.rs index 7b8beed3c6..50fc6001d1 100644 --- a/rust/perspective-js/src/rust/utils/futures.rs +++ b/rust/perspective-js/src/rust/utils/futures.rs @@ -64,6 +64,10 @@ where pub fn spawn> + 'static>(x: U) { drop(js_sys::Promise::from(Self::new(x))) } + + pub fn spawn_throttled> + 'static>(x: U) { + drop(js_sys::Promise::from(Self::new_throttled(x))) + } } impl Default for ApiFuture diff --git a/rust/perspective-viewer/build.mjs b/rust/perspective-viewer/build.mjs index 45736c34d9..a6ccba30ee 100644 --- a/rust/perspective-viewer/build.mjs +++ b/rust/perspective-viewer/build.mjs @@ -12,7 +12,6 @@ import { execSync } from "child_process"; import { build } from "@perspective-dev/esbuild-plugin/build.js"; -import { PerspectiveEsbuildPlugin } from "@perspective-dev/esbuild-plugin"; import { NodeModulesExternal } from "@perspective-dev/esbuild-plugin/external.js"; import * as fs from "node:fs"; import { BuildCss } from "@prospective.co/procss/target/cjs/procss.js"; @@ -31,9 +30,7 @@ function get_host() { } async function build_all() { execSync( - `cargo bundle --target=${get_host()} -- perspective_viewer ${ - IS_DEBUG ? "" : "--release" - }`, + `cargo bundle --target=${get_host()} -- perspective_viewer ${IS_DEBUG ? "" : "--release"}`, INHERIT, ); @@ -56,7 +53,6 @@ async function build_all() { { entryPoints: ["src/ts/perspective-viewer.inline.ts"], format: "esm", - plugins: [PerspectiveEsbuildPlugin()], loader: { ".wasm": "binary" }, outfile: "dist/esm/perspective-viewer.inline.js", }, @@ -85,7 +81,6 @@ async function build_all() { { entryPoints: ["src/ts/perspective-viewer.cdn.ts"], format: "esm", - plugins: [PerspectiveEsbuildPlugin()], loader: { ".wasm": "file" }, outfile: "dist/cdn/perspective-viewer.js", }, diff --git a/rust/perspective-viewer/package.json b/rust/perspective-viewer/package.json index 8c6c40b307..a4171b688f 100644 --- a/rust/perspective-viewer/package.json +++ b/rust/perspective-viewer/package.json @@ -47,6 +47,7 @@ "@playwright/test": "catalog:", "@playwright/experimental-ct-react": "catalog:", "@types/react": "catalog:", + "@types/node": "catalog:", "@types/react-dom": "catalog:", "prettier": "catalog:", "typedoc": "catalog:", diff --git a/rust/perspective-viewer/src/less/column-selector.less b/rust/perspective-viewer/src/less/column-selector.less index e0d14c523a..ac1de261ef 100644 --- a/rust/perspective-viewer/src/less/column-selector.less +++ b/rust/perspective-viewer/src/less/column-selector.less @@ -211,7 +211,6 @@ } span.expression-delete-button { - // padding-right: 1.5px; padding-left: 5px; margin-right: 8px; margin-left: auto; @@ -384,9 +383,7 @@ .is_column_active { &.required { - // color: var(--inactive--color, #ccc); opacity: 0.3; - cursor: not-allowed; cursor: initial; } @@ -396,30 +393,6 @@ } } - // .column-selector-column[data-index="0"]:before { - // content: var(--column-selector-column-0--label, attr(data-label)); - // } - - // .column-selector-column[data-index="1"]:before { - // content: var(--column-selector-column-1--label, attr(data-label)); - // } - - // .column-selector-column[data-index="2"]:before { - // content: var(--column-selector-column-2--label, attr(data-label)); - // } - - // .column-selector-column[data-index="3"]:before { - // content: var(--column-selector-column-3--label, attr(data-label)); - // } - - // .column-selector-column[data-index="4"]:before { - // content: var(--column-selector-column-4--label, attr(data-label)); - // } - - // .column-selector-column[data-index="5"]:before { - // content: var(--column-selector-column-5--label, attr(data-label)); - // } - .column-selector-column { position: relative; @@ -502,9 +475,6 @@ // selected for the `columns` field of the `ViewConfig`. #sub-columns { padding-bottom: 8px; - // & > div:not(:empty) { - // margin-bottom: 6px; - // } &:empty { display: none; diff --git a/rust/perspective-viewer/src/less/column-settings-panel.less b/rust/perspective-viewer/src/less/column-settings-panel.less index 8564a89f0b..6425261f91 100644 --- a/rust/perspective-viewer/src/less/column-settings-panel.less +++ b/rust/perspective-viewer/src/less/column-settings-panel.less @@ -43,10 +43,6 @@ font-size: var(--label--font-size, 0.75em); } - .radio-list-item label { - margin-top: 6px; - } - input { &[type="text"], &[type="search"] { @@ -86,7 +82,7 @@ cursor: text; } } - &::focus { + &:focus { outline-style: solid; background: var(--plugin--background); } diff --git a/rust/perspective-viewer/src/less/column-style.less b/rust/perspective-viewer/src/less/column-style.less index fb5bd49bfd..eb879baf28 100644 --- a/rust/perspective-viewer/src/less/column-style.less +++ b/rust/perspective-viewer/src/less/column-style.less @@ -167,20 +167,6 @@ flex: 1 1 100%; } - div.bool-field { - display: flex; - label { - padding: 0 8px; - } - } - - div.inner_section { - margin-top: 4px; - width: 0px; - margin-bottom: 8px; - flex: 1 1 100%; - } - div.row { display: flex; align-items: center; diff --git a/rust/perspective-viewer/src/less/config-selector.less b/rust/perspective-viewer/src/less/config-selector.less index 7d1b07048b..bdf0f37db2 100644 --- a/rust/perspective-viewer/src/less/config-selector.less +++ b/rust/perspective-viewer/src/less/config-selector.less @@ -215,10 +215,6 @@ content: var(--filter-label--content, "Where"); } - .highlight-drop { - background-color: rgba(0, 0, 0, 0.5); - } - .rrow { display: flex; min-height: 24px; @@ -270,18 +266,6 @@ margin: 0; padding: 0 0 0 0; min-height: 26px; - // margin-bottom: -1px; - - // // Comma separators - // &> :not(.config-drop):after { - // width: 0px; - // content: ","; - // } - - // &> :last-child:after { - // display: none; - // content: "" !important; - // } } .psp-text-field__input + label { @@ -289,7 +273,6 @@ height: 20px; line-height: 17px; box-sizing: border-box; - // color: var(--inactive--color); white-space: nowrap; padding: var(--column-drop-container--padding, 0px); font-size: var(--label--font-size, 0.75em); diff --git a/rust/perspective-viewer/src/less/dom/select.less b/rust/perspective-viewer/src/less/dom/select.less index 37a2936dc5..d2e06f489d 100644 --- a/rust/perspective-viewer/src/less/dom/select.less +++ b/rust/perspective-viewer/src/less/dom/select.less @@ -38,8 +38,6 @@ border-width: 0px; outline: none; -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; appearance: none; color: inherit; padding: 0px 12px 0px 0px; diff --git a/rust/perspective-viewer/src/less/status-bar.less b/rust/perspective-viewer/src/less/status-bar.less index d9eac8d38d..2a5d967998 100644 --- a/rust/perspective-viewer/src/less/status-bar.less +++ b/rust/perspective-viewer/src/less/status-bar.less @@ -21,8 +21,6 @@ // When settings open ... #main_column #status_bar, #main_column #status_bar.titled { - // padding-right: 10px; - input::placeholder { color: var(--inactive--color); } @@ -95,7 +93,6 @@ input, textarea { - // grid-area: 1 / 1; position: absolute; left: 0; right: 0; @@ -123,11 +120,6 @@ } } - .app-title { - margin-left: 12px; - font-size: 16px; - } - .section { display: flex; align-items: center; @@ -171,32 +163,16 @@ align-self: stretch; } - #counter-arrow:before { - content: var(--status-bar-counter--content, "arrow_back"); - } - span { white-space: nowrap; - - // font-size: var(--label--font-size, 0.75em); margin: 0px 14px; user-select: none; - // height: 100%; - // line-height: 36px; - - // &:before { - // position: relative; - // } &:hover { color: inherit; } } - // span#rows { - // margin-left: 2px; - // } - span.icon { height: 100%; line-height: 36px; @@ -279,7 +255,7 @@ align-items: center; border-radius: 3px; margin: 0; - height: auto; //var(--status-bar--height, 48px); + height: auto; border: 1px solid var(--status-ok-icon--border-color, transparent); cursor: var(--status-ok-icon--cursor); pointer-events: var(--status-indicator--pointer-events, none); @@ -298,7 +274,6 @@ var(--icon--color) ); } - // color: var(--status-ok-icon--hover--background-color); } &.errored { @@ -317,12 +292,8 @@ align-items: center; justify-content: center; height: 20px; - height: 20px; border-radius: 10px; color: var(--plugin--background); - // pointer-events: all; - // mask-image: url(../svg/status_error.svg); - // -webkit-mask-image: url(../svg/status_error.svg); &:before { content: "!"; } @@ -354,10 +325,6 @@ -webkit-mask-image: var(--updating-icon--mask-image); } - // span#status.uninitialized { - - // } - span#status.updating { animation-name: status-bar-updating-inverse; animation-fill-mode: forwards; diff --git a/rust/perspective-viewer/src/less/viewer.less b/rust/perspective-viewer/src/less/viewer.less index 3487b8e8df..4d0ae1bcf4 100644 --- a/rust/perspective-viewer/src/less/viewer.less +++ b/rust/perspective-viewer/src/less/viewer.less @@ -251,17 +251,8 @@ .noselect { -webkit-touch-callout: none; - /* iOS Safari */ -webkit-user-select: none; - /* Safari */ - -khtml-user-select: none; - /* Konqueror HTML */ - -moz-user-select: none; - /* Firefox */ - -ms-user-select: none; - /* Internet Explorer/Edge */ user-select: none; - /* Non-prefixed version, currently supported by Chrome and Opera */ } .sidebar_column { @@ -297,10 +288,6 @@ padding: 0; } - // #settings_button.titled { - // opacity: 0.2; - // } - #close_button { background-color: var(--plugin--background); padding: 0px; @@ -325,14 +312,12 @@ } &:before { - // font-feature-settings: "liga"; - // content: var(--config-button-icon--content, "Configure"); display: inline-block; height: 20px; width: 20px; content: ""; mask-size: cover; - -webkit-mask-size: cover; //40px 35px; + -webkit-mask-size: cover; background-repeat: no-repeat; background-color: var(--icon--color); mask-image: var(--close-icon--mask-image); @@ -347,12 +332,11 @@ display: flex; align-items: center; justify-content: center; - z-index: 10000; + z-index: 3; border: 1px solid var(--inactive--color); border-radius: 5px; font-size: 10px; font-weight: normal; - z-index: 3; &:hover { color: var(--plugin--background, inherit); @@ -365,14 +349,12 @@ } &:before { - // font-feature-settings: "liga"; - // content: var(--config-button-icon--content, "Configure"); display: inline-block; height: 20px; width: 20px; content: ""; mask-size: cover; - -webkit-mask-size: cover; //40px 35px; + -webkit-mask-size: cover; background-repeat: no-repeat; background-color: var(--icon--color); mask-image: var(--drawer-tab-inverted-icon--mask-image); @@ -380,34 +362,11 @@ } } - // #settings_button.floating { - // border: none; - // opacity: 1; - // background-color: transparent; - // margin-right: 0px; - // padding: 0; - // border-radius: 0px; - - // &:before { - // display: inline-block; - // height: 18px; - // width: 26px; - // content: ""; - // mask-size: cover; - // -webkit-mask-size: cover; //40px 35px; - // background-repeat: no-repeat; - // background-color: var(--icon--color); - // mask-image: var(--drawer-tab-icon--mask-image); - // -webkit-mask-image: var(--drawer-tab-icon--mask-image); - // } - // } - .split-panel.orient-reverse > .split-panel-child:not(:last-child):not(.is-width-override) { max-width: 300px; } - #expr_panel_header, .sidebar_header { min-height: var( --plugin-selector--height, @@ -420,7 +379,6 @@ border-bottom: 1px solid var(--inactive--color, #6e6e6e); } - #expr_panel_header_title, .sidebar_header_title { padding-left: 9px; overflow: hidden; @@ -429,7 +387,6 @@ margin-right: 30px; } - #expr_panel_border, .sidebar_border { height: 2px; width: 100%; @@ -438,13 +395,4 @@ flex-shrink: 0; flex-grow: 0; } - - .expr_editor_column { - z-index: 2; - width: 100%; - } - - .is-width-override > .expr_editor_column { - min-width: unset; - } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector.rs index 6911442bff..508dab014b 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector.rs @@ -40,16 +40,19 @@ use super::containers::scroll_panel::*; use super::containers::split_panel::{Orientation, SplitPanel}; use super::style::LocalStyle; use crate::components::containers::scroll_panel_item::ScrollPanelItem; +use crate::css; use crate::custom_elements::ColumnDropDownElement; use crate::dragdrop::*; -use crate::model::*; use crate::presentation::ColumnLocator; use crate::renderer::*; +use crate::session::drag_drop_update::*; use crate::session::*; +use crate::tasks::{ + ActiveColumnState, ActiveColumnStateData, ColumnsIteratorSet, can_render_column_styles, +}; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct ColumnSelectorProps { /// Fires when the expression/config column is open. pub on_open_expr_panel: Callback, @@ -61,6 +64,15 @@ pub struct ColumnSelectorProps { #[prop_or_default] pub on_resize: Option>>, + /// Value props threaded from root's `SessionProps` / `RendererProps`. + pub has_table: bool, + pub named_column_count: usize, + pub view_config: Rc, + pub drag_column: Option, + /// Cloned session metadata snapshot — threaded from `SessionProps` + /// so that metadata changes trigger re-renders via prop diffing. + pub metadata: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -70,17 +82,21 @@ pub struct ColumnSelectorProps { impl PartialEq for ColumnSelectorProps { fn eq(&self, rhs: &Self) -> bool { self.selected_column == rhs.selected_column + && self.has_table == rhs.has_table + && self.named_column_count == rhs.named_column_count + && self.view_config == rhs.view_config + && self.drag_column == rhs.drag_column + && self.metadata == rhs.metadata } } #[derive(Debug)] pub enum ColumnSelectorMsg { - TableLoaded, - ViewCreated, + /// Triggers a plain re-render; used as `onselect`/`ondragenter` callbacks + /// from `ConfigSelector` after it mutates the view config. + Redraw, HoverActiveIndex(Option), SetWidth(f64), - Drag(DragEffect), - DragEnd, Drop((String, DragTarget, DragEffect, usize)), } @@ -89,8 +105,7 @@ use ColumnSelectorMsg::*; /// A `ColumnSelector` controls the `columns` field of the `ViewConfig`, /// deriving its options from the table columns and `ViewConfig` expressions. pub struct ColumnSelector { - _subscriptions: [Subscription; 5], - named_row_count: usize, + _subscriptions: [Subscription; 1], drag_container: DragDropContainer, column_dropdown: ColumnDropDownElement, viewport_width: f64, @@ -103,44 +118,14 @@ impl Component for ColumnSelector { fn create(ctx: &Context) -> Self { let ColumnSelectorProps { - dragdrop, - renderer, - session, - .. + dragdrop, session, .. } = ctx.props(); - let table_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::TableLoaded); - session.table_loaded.add_listener(cb) - }; - - let view_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::ViewCreated); - session.view_created.add_listener(cb) - }; let drop_sub = { let cb = ctx.link().callback(ColumnSelectorMsg::Drop); dragdrop.drop_received.add_listener(cb) }; - let drag_sub = { - let cb = ctx.link().callback(ColumnSelectorMsg::Drag); - dragdrop.dragstart_received.add_listener(cb) - }; - - let dragend_sub = { - let cb = ctx.link().callback(|_| ColumnSelectorMsg::DragEnd); - dragdrop.dragend_received.add_listener(cb) - }; - - let named = maybe! { - let plugin = - renderer.get_active_plugin().ok()?; - - Some(plugin.config_column_names()?.length() as usize) - }; - - let named_row_count = named.unwrap_or_default(); let drag_container = DragDropContainer::new(|| {}, { let link = ctx.link().clone(); move || link.send_message(ColumnSelectorMsg::HoverActiveIndex(None)) @@ -148,8 +133,7 @@ impl Component for ColumnSelector { let column_dropdown = ColumnDropDownElement::new(session.clone()); Self { - _subscriptions: [table_sub, view_sub, drop_sub, drag_sub, dragend_sub], - named_row_count, + _subscriptions: [drop_sub], viewport_width: 0f64, drag_container, column_dropdown, @@ -159,23 +143,11 @@ impl Component for ColumnSelector { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - Drag(DragEffect::Move(DragTarget::Active)) => false, - Drag(_) | DragEnd | TableLoaded => true, + Redraw => true, SetWidth(w) => { self.viewport_width = w; false }, - ViewCreated => { - let named = maybe! { - let plugin = - ctx.props().renderer.get_active_plugin().ok()?; - - Some(plugin.config_column_names()?.length() as usize) - }; - - self.named_row_count = named.unwrap_or_default(); - true - }, HoverActiveIndex(Some(to_index)) => ctx .props() .dragdrop @@ -185,33 +157,74 @@ impl Component for ColumnSelector { true }, Drop((column, DragTarget::Active, DragEffect::Move(DragTarget::Active), index)) => { - if !ctx.props().is_invalid_columns_column(&column, index) { - let update = ctx.props().session.create_drag_drop_update( + let is_invalid = { + let config = &ctx.props().view_config; + let from_index = config + .columns + .iter() + .position(|x| x.as_ref() == Some(&column)); + let min_cols = ctx.props().renderer.metadata().min; + let is_to_empty = !config + .columns + .get(index) + .map(|x| x.is_some()) + .unwrap_or_default(); + min_cols + .and_then(|x| from_index.map(|fi| fi < x)) + .unwrap_or_default() + && is_to_empty + }; + if !is_invalid { + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, DragTarget::Active, DragEffect::Move(DragTarget::Active), &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); } } true }, Drop((column, DragTarget::Active, effect, index)) => { - let update = ctx.props().session.create_drag_drop_update( + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, DragTarget::Active, effect, &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); } true @@ -228,10 +241,26 @@ impl Component for ColumnSelector { dragdrop, .. } = ctx.props(); - let config = session.get_view_config(); + let metadata = &ctx.props().metadata; + // When `config.columns` is empty but the table has columns (transient + // state during `load()` after `reset()` clears the config), fill in + // all table columns as active — matching `validate_view_config()`. + let prop_config = &ctx.props().view_config; + let config: Rc = if prop_config.columns.is_empty() { + if let Some(table_cols) = metadata.get_table_columns() { + Rc::new(perspective_client::config::ViewConfig { + columns: table_cols.iter().map(|c| Some(c.clone())).collect(), + ..(**prop_config).clone() + }) + } else { + prop_config.clone() + } + } else { + prop_config.clone() + }; let is_aggregated = config.is_aggregated(); - let columns_iter = ctx.props().column_selector_iter_set(&config); - let onselect = ctx.link().callback(|()| ViewCreated); + let columns_iter = ColumnsIteratorSet::new(&config, metadata, renderer, dragdrop); + let onselect = ctx.link().callback(|()| Redraw); let ondragenter = ctx.link().callback(HoverActiveIndex); let ondragover = Callback::from(|_event: DragEvent| _event.prevent_default()); let ondrop = Callback::from({ @@ -245,7 +274,7 @@ impl Component for ColumnSelector { }); let mut active_classes = classes!(); - if ctx.props().dragdrop.get_drag_column().is_some() { + if ctx.props().drag_column.is_some() { active_classes.push("dragdrop-highlight"); }; @@ -258,8 +287,7 @@ impl Component for ColumnSelector { + config.split_by.len() + config.filter.len() + config.sort.len()) as f64, - session - .metadata() + metadata .get_features() .map(|x| { let mut y = 0.0; @@ -288,7 +316,10 @@ impl Component for ColumnSelector { }; - let mut named_count = self.named_row_count; + let mut named_count = ctx.props().named_column_count; let mut active_columns: Vec<_> = columns_iter .active() .enumerate() - .map(|(idx, name)| { + .map(|(idx, name): (usize, ActiveColumnState)| { let ondragenter = ondragenter.reform(move |_| Some(idx)); let size_hint = if named_count > 0 { 50.0 } else { 28.0 }; named_count = named_count.saturating_sub(1); @@ -315,6 +346,34 @@ impl Component for ColumnSelector { Some(ColumnLocator::Table(x)) | Some(ColumnLocator::Expression(x)) if x == &key ); + // Compute metadata-derived props here so that changes to + // session metadata propagate via prop diffing. + // For DragOver placeholders, resolve the type from the + // dragged column (since `get_name()` returns `None`). + let col_type = name + .get_name() + .and_then(|n| metadata.get_column_table_type(n)) + .or_else(|| { + if matches!(name.state, ActiveColumnStateData::DragOver) { + dragdrop + .get_drag_column() + .and_then(|c| metadata.get_column_table_type(&c)) + } else { + None + } + }); + + let is_expression = name + .get_name() + .map(|n| metadata.is_column_expression(n)) + .unwrap_or(false); + + let can_render_styles = name + .get_name() + .and_then(|n| can_render_column_styles(renderer, &config, metadata, n).ok()) + .unwrap_or(false); + + let show_edit_btn = is_expression || can_render_styles; let on_open_expr_panel = &ctx.props().on_open_expr_panel; html_nested! { @@ -323,6 +382,11 @@ impl Component for ColumnSelector { {idx} {is_aggregated} {is_editing} + {is_expression} + {show_edit_btn} + {col_type} + view_config={config.clone()} + metadata={metadata.clone()} {name} {on_open_expr_panel} {ondragenter} @@ -344,6 +408,7 @@ impl Component for ColumnSelector { .map(|(idx, vc)| { let selected_column = ctx.props().selected_column.as_ref(); let is_editing = matches!(selected_column, Some(ColumnLocator::Expression(x)) if x.as_str() == vc.name); + let is_expression = metadata.is_column_expression(vc.name); html_nested! { , + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent as a value prop. + pub view_config: Rc, + /// State pub session: Session, pub dragdrop: DragDrop, @@ -71,8 +91,16 @@ pub struct ActiveColumnProps { } impl PartialEq for ActiveColumnProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.idx == rhs.idx + && self.name == rhs.name + && self.is_aggregated == rhs.is_aggregated + && self.is_editing == rhs.is_editing + && self.is_expression == rhs.is_expression + && self.show_edit_btn == rhs.show_edit_btn + && self.col_type == rhs.col_type + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -121,7 +149,7 @@ impl Component for ActiveColumn { is_render }, New(InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); if ctx.props().idx >= view_config.columns.len() { view_config.columns.push(Some(col)); } else { @@ -133,15 +161,21 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } true }, New(InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); if ctx.props().idx >= view_config.columns.len() { view_config.columns.push(Some(col.name.as_ref().to_owned())); } else { @@ -155,10 +189,16 @@ impl Component for ActiveColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } true }, @@ -234,7 +274,7 @@ impl Component for ActiveColumn { }) .collect(); - let col_type = ctx.props().get_table_type(&ctx.props().name); + let col_type = ctx.props().col_type; match (name, col_type) { ((label, ColumnState::Empty), _) => { classes.push("empty-named"); @@ -242,8 +282,7 @@ impl Component for ActiveColumn { let on_select = ctx.link().callback(ActiveColumnMsg::New); let exclude = ctx .props() - .session - .get_view_config() + .view_config .columns .iter() .flatten() @@ -292,11 +331,12 @@ impl Component for ActiveColumn { })) }; - let ondragend = &ctx.props().ondragend.reform(|_| {}); + let ondragend = &ctx.props().ondragend.reform(|_| tracing::error!("dragend")); let ondragstart = ctx.link().callback({ let event_name = name.to_owned(); let dragdrop = ctx.props().dragdrop.clone(); move |event: DragEvent| { + tracing::error!("dragstart"); dragdrop.set_drag_image(&event).unwrap(); dragdrop.notify_drag_start( event_name.to_string(), @@ -312,17 +352,12 @@ impl Component for ActiveColumn { .link() .callback(|event: MouseEvent| MouseEnter(event.which() == 0)); - let is_expression = ctx.props().session.metadata().is_column_expression(&name); + let is_expression = ctx.props().is_expression; + let show_edit_btn = ctx.props().show_edit_btn; let mut class = ctx.props().renderer.metadata().mode.css(); if is_required { class.push("required"); }; - - let can_render_styles = ctx - .props() - .can_render_column_styles(&name) - .unwrap_or_default(); - let show_edit_btn = is_expression || can_render_styles; html! {
@@ -390,27 +427,6 @@ impl Component for ActiveColumn { } impl ActiveColumnProps { - fn get_name(&self, defn: &ActiveColumnState) -> Option { - match &defn.state { - ActiveColumnStateData::DragOver => Some(self.dragdrop.get_drag_column().unwrap()), - ActiveColumnStateData::Column(name) => Some(name.to_owned()), - ActiveColumnStateData::Required => None, - ActiveColumnStateData::Invalid => None, - } - } - - fn get_table_type(&self, defn: &ActiveColumnState) -> Option { - self.get_name(defn) - .as_ref() - .and_then(|x| self.session.metadata().get_column_table_type(x)) - } - - fn _get_view_type(&self, defn: &ActiveColumnState) -> Option { - self.get_name(defn) - .as_ref() - .and_then(|x| self.session.metadata().get_column_view_type(x)) - } - /// Remove an active column from `columns`, or alternatively make this /// column the only column in `columns` if the shift key is set (via the /// `shift` flag). @@ -420,7 +436,7 @@ impl ActiveColumnProps { /// with respect to `columns`. /// - `shift` whether to toggle or select this column. fn deactivate_column(&self, name: String, shift: bool) { - let mut columns = self.session.get_view_config().columns.clone(); + let mut columns = self.view_config.columns.clone(); let max_cols = self .renderer .metadata() @@ -457,7 +473,7 @@ impl ActiveColumnProps { } fn get_aggregate(&self, name: &str) -> Option { - self.session.get_view_config().aggregates.get(name).cloned() + self.view_config.aggregates.get(name).cloned() } fn apply_columns(&self, columns: Vec>) { @@ -466,8 +482,13 @@ impl ActiveColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(config).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs index c53f145759..2aceef4f18 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/aggregate_selector.rs @@ -14,18 +14,16 @@ use std::collections::HashSet; use std::rc::Rc; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use yew::prelude::*; use crate::components::containers::select::*; use crate::components::style::LocalStyle; -use crate::model::*; +use crate::css; use crate::renderer::*; use crate::session::*; -use crate::{PerspectiveProperties, css}; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct AggregateSelectorProps { /// The name of this aggregate. pub column: String, @@ -33,6 +31,12 @@ pub struct AggregateSelectorProps { /// Which aggregate is currently selected. pub aggregate: Option, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent as a value prop. + pub view_config: Rc, + // State pub renderer: Renderer, pub session: Session, @@ -40,7 +44,10 @@ pub struct AggregateSelectorProps { impl PartialEq for AggregateSelectorProps { fn eq(&self, rhs: &Self) -> bool { - self.column == rhs.column && self.aggregate == rhs.aggregate + self.column == rhs.column + && self.aggregate == rhs.aggregate + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -89,11 +96,10 @@ impl Component for AggregateSelector { .clone() .or_else(|| { ctx.props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().column) .and_then(|x| { - ctx.props().session.metadata().get_features().and_then(|y| { + ctx.props().metadata.get_features().and_then(|y| { y.aggregates.get(&(x as u32)).and_then(|z| { z.aggregates .first() @@ -130,24 +136,27 @@ impl Component for AggregateSelector { impl AggregateSelector { pub fn set_aggregate(&mut self, ctx: &Context, aggregate: Aggregate) { self.aggregate = Some(aggregate.clone()); - let mut aggregates = ctx.props().session.get_view_config().aggregates.clone(); + let mut aggregates = ctx.props().view_config.aggregates.clone(); aggregates.insert(ctx.props().column.clone(), aggregate); let config = ViewConfigUpdate { aggregates: Some(aggregates), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } pub fn get_dropdown_aggregates(&self, ctx: &Context) -> Vec> { let aggregates = ctx .props() - .session - .metadata() + .metadata .get_column_aggregates(&ctx.props().column) .map(|x| x.collect::>()) .unwrap_or_default(); diff --git a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs index 654fcc7211..8c291a0a5d 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/config_selector.rs @@ -13,8 +13,7 @@ use std::collections::HashSet; use std::rc::Rc; -use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; +use perspective_client::config::{ViewConfig, *}; use perspective_js::utils::ApiFuture; use yew::prelude::*; @@ -25,21 +24,31 @@ use super::sort_column::*; use crate::components::containers::dragdrop_list::*; use crate::components::containers::select::{Select, SelectItem}; use crate::components::style::LocalStyle; +use crate::css; use crate::custom_elements::{ColumnDropDownElement, FilterDropDownElement}; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; +use crate::session::drag_drop_update::*; use crate::session::*; use crate::utils::*; -use crate::{PerspectiveProperties, css}; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct ConfigSelectorProps { pub onselect: Callback<()>, #[prop_or_default] pub ondragenter: Callback<()>, + /// Current view config threaded as a value prop so that config changes + /// (group_by, sort, filter, etc.) trigger re-renders via normal prop + /// diffing rather than a PubSub `view_created` subscription. + pub view_config: Rc, + /// Column currently being dragged — threaded to show `dragdrop-highlight` + /// without subscribing to `dragstart_received`/`dragend_received`. + pub drag_column: Option, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -47,22 +56,21 @@ pub struct ConfigSelectorProps { } impl PartialEq for ConfigSelectorProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.view_config == other.view_config + && self.drag_column == other.drag_column + && self.metadata == other.metadata } } #[derive(Debug)] pub enum ConfigSelectorMsg { - DragStart, - DragEnd, DragOver(usize, DragTarget), DragLeave(DragTarget), Drop(String, DragTarget, DragEffect, usize), Close(usize, DragTarget), SetFilterValue(usize, String), TransposePivots, - ViewCreated, New(DragTarget, InPlaceColumn), UpdateGroupRollupMode(GroupRollupMode), } @@ -71,7 +79,7 @@ pub enum ConfigSelectorMsg { pub struct ConfigSelector { filter_dropdown: FilterDropDownElement, column_dropdown: ColumnDropDownElement, - _subscriptions: [Rc; 4], + _subscriptions: [Rc; 1], } impl Component for ConfigSelector { @@ -79,12 +87,6 @@ impl Component for ConfigSelector { type Properties = ConfigSelectorProps; fn create(ctx: &Context) -> Self { - let cb = ctx.link().callback(|_| ConfigSelectorMsg::DragStart); - let drag_sub = Rc::new(ctx.props().dragdrop.dragstart_received.add_listener(cb)); - - let cb = ctx.link().callback(|_| ConfigSelectorMsg::DragEnd); - let dragend_sub = Rc::new(ctx.props().dragdrop.dragend_received.add_listener(cb)); - let cb = ctx .link() .callback(|x: (String, DragTarget, DragEffect, usize)| { @@ -92,12 +94,9 @@ impl Component for ConfigSelector { }); let drop_sub = Rc::new(ctx.props().dragdrop.drop_received.add_listener(cb)); - let cb = ctx.link().callback(|_| ConfigSelectorMsg::ViewCreated); - let view_sub = Rc::new(ctx.props().session.view_created.add_listener(cb)); - let filter_dropdown = FilterDropDownElement::new(ctx.props().session.clone()); let column_dropdown = ColumnDropDownElement::new(ctx.props().session.clone()); - let _subscriptions = [drop_sub, view_sub, drag_sub, dragend_sub]; + let _subscriptions = [drop_sub]; Self { filter_dropdown, column_dropdown, @@ -107,8 +106,6 @@ impl Component for ConfigSelector { fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - ConfigSelectorMsg::DragStart | ConfigSelectorMsg::ViewCreated => true, - ConfigSelectorMsg::DragEnd => true, ConfigSelectorMsg::DragOver(index, action) => { let should_render = ctx.props().dragdrop.notify_drag_enter(action, index); if should_render { @@ -121,7 +118,7 @@ impl Component for ConfigSelector { true }, ConfigSelectorMsg::Close(index, DragTarget::Sort) => { - let mut sort = ctx.props().session.get_view_config().sort.clone(); + let mut sort = ctx.props().view_config.sort.clone(); sort.remove(index); let sort = Some(sort); let config = ViewConfigUpdate { @@ -129,10 +126,16 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -143,22 +146,26 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } false }, ConfigSelectorMsg::Close(index, DragTarget::GroupBy) => { - if ctx.props().session.get_view_config().group_rollup_mode == GroupRollupMode::Total - { + if ctx.props().view_config.group_rollup_mode == GroupRollupMode::Total { let requirements = ctx.props().renderer.metadata(); let rollup_features = ctx .props() - .session - .metadata() + .metadata .get_features() .map(|x| x.get_group_rollup_modes()) .unwrap(); @@ -171,51 +178,69 @@ impl Component for ConfigSelector { )); false } else { - let mut group_by = ctx.props().session.get_view_config().group_by.clone(); + let mut group_by = ctx.props().view_config.group_by.clone(); group_by.remove(index); let config = ViewConfigUpdate { group_by: Some(group_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false } }, ConfigSelectorMsg::Close(index, DragTarget::SplitBy) => { - let mut split_by = ctx.props().session.get_view_config().split_by.clone(); + let mut split_by = ctx.props().view_config.split_by.clone(); split_by.remove(index); let config = ViewConfigUpdate { split_by: Some(split_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::Close(index, DragTarget::Filter) => { self.filter_dropdown.hide().unwrap(); - let mut filter = ctx.props().session.get_view_config().filter.clone(); + let mut filter = ctx.props().view_config.filter.clone(); filter.remove(index); let config = ViewConfigUpdate { filter: Some(filter), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(config).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -224,18 +249,31 @@ impl Component for ConfigSelector { ConfigSelectorMsg::Drop(column, action, effect, index) if action != DragTarget::Active => { - let update = ctx.props().session.create_drag_drop_update( + let col_type = ctx + .props() + .metadata + .get_column_table_type(column.as_str()) + .unwrap(); + let update = ctx.props().view_config.create_drag_drop_update( column, + col_type, index, action, effect, &ctx.props().renderer.metadata(), + ctx.props().metadata.get_features().unwrap(), ); - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -247,7 +285,7 @@ impl Component for ConfigSelector { }, ConfigSelectorMsg::Drop(..) => false, ConfigSelectorMsg::TransposePivots => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); std::mem::swap(&mut view_config.group_by, &mut view_config.split_by); let update = ViewConfigUpdate { @@ -256,16 +294,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::SetFilterValue(index, input) => { - let mut filter = ctx.props().session.get_view_config().filter.clone(); + let mut filter = ctx.props().view_config.filter.clone(); // TODO Can't special case these - need to make this part of the // Features API. @@ -295,47 +339,65 @@ impl Component for ConfigSelector { } }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } false }, ConfigSelectorMsg::New(DragTarget::GroupBy, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.group_by.push(col); let update = ViewConfigUpdate { group_by: Some(view_config.group_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::SplitBy, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.split_by.push(col); let update = ViewConfigUpdate { split_by: Some(view_config.split_by), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Filter, InPlaceColumn::Column(column)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); let op = ctx.props().default_op(column.as_str()).unwrap_or_default(); view_config.filter.push(Filter::new( &column, @@ -348,32 +410,44 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Sort, InPlaceColumn::Column(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.sort.push(Sort(col, SortDir::Asc)); let update = ViewConfigUpdate { sort: Some(view_config.sort), ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::GroupBy, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.group_by.push(col.name.as_ref().to_owned()); view_config.expressions.insert(&col); let update = ViewConfigUpdate { @@ -382,16 +456,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::SplitBy, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config.split_by.push(col.name.as_ref().to_owned()); view_config.expressions.insert(&col); let update = ViewConfigUpdate { @@ -400,16 +480,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Filter, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); let column = col.name.as_ref(); view_config.filter.push(Filter::new( column, @@ -426,16 +512,22 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false }, ConfigSelectorMsg::New(DragTarget::Sort, InPlaceColumn::Expression(col)) => { - let mut view_config = ctx.props().session.get_view_config().clone(); + let mut view_config = (*ctx.props().view_config).clone(); view_config .sort .push(Sort(col.name.as_ref().to_owned(), SortDir::Asc)); @@ -446,10 +538,16 @@ impl Component for ConfigSelector { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + { + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + } ctx.props().onselect.emit(()); false @@ -471,12 +569,12 @@ impl Component for ConfigSelector { session, .. } = ctx.props(); - let config = session.get_view_config(); + let config = &ctx.props().view_config; let transpose = ctx.link().callback(|_| ConfigSelectorMsg::TransposePivots); let column_dropdown = self.column_dropdown.clone(); let mut class = classes!(); - if dragdrop.get_drag_column().is_some() { + if ctx.props().drag_column.is_some() { class.push("dragdrop-highlight"); } @@ -489,17 +587,14 @@ impl Component for ConfigSelector { move |_event| dragdrop.notify_drag_end() }); - let metadata = session.metadata(); + let metadata = &ctx.props().metadata; let features = metadata.get_features().unwrap(); let requirements = renderer.metadata(); let on_group_rollup_mode = ctx .link() .callback(ConfigSelectorMsg::UpdateGroupRollupMode); - let rollup_features = ctx - .props() - .session - .metadata() + let rollup_features = metadata .get_features() .map(|x| x.get_group_rollup_modes()) .unwrap(); @@ -548,6 +643,7 @@ impl Component for ConfigSelector { @@ -580,6 +676,7 @@ impl Component for ConfigSelector { @@ -604,6 +701,8 @@ impl Component for ConfigSelector { @@ -634,6 +733,8 @@ impl Component for ConfigSelector { filter_dropdown={ &self.filter_dropdown } filter={ filter.clone() } on_keydown={ filter_keydown } + view_config={config.clone()} + metadata={metadata.clone()} {dragdrop} {renderer} {session}> @@ -649,9 +750,8 @@ impl Component for ConfigSelector { impl ConfigSelectorProps { fn default_op(&self, column: &str) -> Option { - let metadata = self.session.metadata(); - let features = metadata.get_features()?; - let col_type = metadata.get_column_table_type(column)?; + let features = self.metadata.get_features()?; + let col_type = self.metadata.get_column_table_type(column)?; let first = features.default_op(col_type)?; Some(first.to_string()) } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs index a0808d255c..70c9cc2ae6 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/filter_column.rs @@ -15,7 +15,6 @@ use std::rc::Rc; use chrono::{Datelike, NaiveDate, TimeZone, Utc}; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use wasm_bindgen::JsCast; use web_sys::*; @@ -27,19 +26,23 @@ use crate::components::style::LocalStyle; use crate::components::type_icon::TypeIcon; use crate::custom_elements::*; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; +use crate::{css, maybe}; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct FilterColumnProps { pub filter: Filter, pub idx: usize, pub filter_dropdown: FilterDropDownElement, pub on_keydown: Callback, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + /// Current view config threaded as a value prop. + pub view_config: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -48,7 +51,11 @@ pub struct FilterColumnProps { impl PartialEq for FilterColumnProps { fn eq(&self, rhs: &Self) -> bool { - self.idx == rhs.idx && self.filter == rhs.filter && self.on_keydown == rhs.on_keydown + self.idx == rhs.idx + && self.filter == rhs.filter + && self.on_keydown == rhs.on_keydown + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -95,7 +102,7 @@ impl Component for FilterColumn { this.filter_ops = Rc::new( maybe! { - Some(get_filter_ops(ctx.props().session(), col_type?)? + Some(get_filter_ops(&ctx.props().metadata, col_type?)? .into_iter() .map(SelectItem::Option) .collect::>()) @@ -183,7 +190,7 @@ impl Component for FilterColumn { changed = true; self.filter_ops = Rc::new( maybe! { - Some(get_filter_ops(&ctx.props().session, col_type?)? + Some(get_filter_ops(&ctx.props().metadata, col_type?)? .into_iter() .map(SelectItem::Option) .collect::>()) @@ -204,11 +211,7 @@ impl Component for FilterColumn { let idx = ctx.props().idx; let filter = ctx.props().filter.clone(); let column = filter.column().to_owned(); - let col_type = ctx - .props() - .session - .metadata() - .get_column_table_type(&column); + let col_type = ctx.props().metadata.get_column_table_type(&column); let select = ctx.link().callback(FilterColumnMsg::FilterOpSelect); let noderef = &self.input_ref; let input = ctx.link().callback({ @@ -377,8 +380,7 @@ impl Component for FilterColumn { } /// Get the allowed `FilterOp`s for this filter. -fn get_filter_ops(session: &Session, col_type: ColumnType) -> Option> { - let metadata = session.metadata(); +fn get_filter_ops(metadata: &SessionMetadata, col_type: ColumnType) -> Option> { let features = metadata.get_features()?; features .filter_ops @@ -404,9 +406,7 @@ impl FilterColumnProps { /// Get this filter's type, e.g. the type of the column. fn get_filter_type(&self, filter: &Filter) -> Option { - self.session - .metadata() - .get_column_table_type(filter.column()) + self.metadata.get_column_table_type(filter.column()) } // Get the string value, suitable for the `value` field of a `FilterColumns`'s @@ -442,7 +442,7 @@ impl FilterColumnProps { /// # Arguments /// - `op` The new `FilterOp`. fn update_filter_op(&self, idx: usize, op: String) { - let mut filter = self.session.get_view_config().filter.clone(); + let mut filter = self.view_config.filter.clone(); let filter_column = &mut filter.get_mut(idx).expect("Filter on no column"); *filter_column.op_mut() = op; let update = ViewConfigUpdate { @@ -450,9 +450,14 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(update).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } /// Update the filter value from the string input read from the DOM. @@ -460,7 +465,7 @@ impl FilterColumnProps { /// # Arguments /// - `val` The new filter value. fn update_filter_input(&self, val: String) { - let mut filters = self.session.get_view_config().filter.clone(); + let mut filters = self.view_config.filter.clone(); let filter_column = &mut filters.get_mut(self.idx).expect("Filter on no column"); // TODO This belongs in the Features API. @@ -523,9 +528,14 @@ impl FilterColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(update).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs index e8d8a9bf4c..38b2d689a2 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/inactive_column.rs @@ -10,9 +10,10 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use itertools::Itertools; use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use web_sys::*; use yew::prelude::*; @@ -21,14 +22,12 @@ use super::expr_edit_button::*; use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; use crate::js::plugin::*; -use crate::model::*; use crate::presentation::ColumnLocator; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct InactiveColumnProps { /// This column's index in its list. pub idx: usize, @@ -42,6 +41,18 @@ pub struct InactiveColumnProps { /// Is the expression/config panel open for this column? pub is_editing: bool, + /// Whether this column is an expression column. Computed by the parent + /// so that changes to session metadata trigger a re-render via prop diff. + #[prop_or_default] + pub is_expression: bool, + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from parent so we avoid + /// `session.get_view_config()` calls. + pub view_config: Rc, + /// `dragend` event`. pub ondragend: Callback<()>, @@ -58,11 +69,14 @@ pub struct InactiveColumnProps { } impl PartialEq for InactiveColumnProps { - /// Equality for `InactiveColumnProps` determines when it should re-render, - /// which is only when it has changed. - /// TODO Aggregates - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.idx == rhs.idx + && self.visible == rhs.visible + && self.name == rhs.name + && self.is_editing == rhs.is_editing + && self.is_expression == rhs.is_expression + && self.metadata == rhs.metadata + && self.view_config == rhs.view_config } } @@ -112,8 +126,7 @@ impl Component for InactiveColumn { fn view(&self, ctx: &Context) -> Html { let col_type = ctx .props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().name) .unwrap_or(ColumnType::String); @@ -128,7 +141,7 @@ impl Component for InactiveColumn { move |event: DragEvent| { dragdrop.set_drag_image(&event).unwrap(); dragdrop.notify_drag_start(event_name.to_string(), DragEffect::Copy); - MouseLeave(false) + MouseLeave(true) } }); @@ -137,11 +150,7 @@ impl Component for InactiveColumn { .link() .callback(|event: MouseEvent| MouseEnter(event.which() == 0)); - let is_expression = ctx - .props() - .session - .metadata() - .is_column_expression(&ctx.props().name); + let is_expression = ctx.props().is_expression; let is_active_class = ctx.props().renderer.metadata().mode.css(); let mut class = classes!("column-selector-column"); @@ -191,7 +200,7 @@ impl InactiveColumnProps { /// with respect to `columns`. /// - `shift` whether to toggle or select this column. pub fn activate_column(&self, name: String, shift: bool) { - let mut columns = self.session.get_view_config().columns.clone(); + let mut columns = self.view_config.columns.clone(); let max_cols = self .renderer .metadata() @@ -229,8 +238,13 @@ impl InactiveColumnProps { ..ViewConfigUpdate::default() }; - self.update_and_render(config) - .map(ApiFuture::spawn) - .unwrap_or_log(); + if self.session.update_view_config(config).is_ok() { + let session = self.session.clone(); + let renderer = self.renderer.clone(); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } } } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs index dfcfe96ced..ea26c28d69 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/invalid_column.rs @@ -15,23 +15,12 @@ use yew::prelude::*; use crate::components::style::LocalStyle; use crate::css; -#[derive(Default)] -pub struct InvalidColumn {} - -impl Component for InvalidColumn { - type Message = (); - type Properties = (); - - fn create(_ctx: &Context) -> Self { - Self::default() - } - - fn view(&self, _ctx: &Context) -> Html { - html! { -
- -
-
- } +#[function_component(InvalidColumn)] +pub fn invalid_column() -> Html { + html! { +
+ +
+
} } diff --git a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs index 71ecedab51..27ccc9bda9 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/pivot_column.rs @@ -10,6 +10,8 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use perspective_client::config::ColumnType; use web_sys::*; use yew::prelude::*; @@ -19,9 +21,8 @@ use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct PivotColumnProps { /// Column name. pub column: String, @@ -32,6 +33,10 @@ pub struct PivotColumnProps { /// The drag starte of this column, if applicable. pub action: DragTarget, + /// Session metadata snapshot — threaded from `SessionProps`. + #[prop_or_default] + pub metadata: Option>, + // State #[prop_or_default] pub opt_session: Option, @@ -40,7 +45,9 @@ pub struct PivotColumnProps { impl PartialEq for PivotColumnProps { fn eq(&self, other: &Self) -> bool { - self.column == other.column && self.action == other.action + self.column == other.column + && self.action == other.action + && self.metadata == other.metadata } } @@ -80,9 +87,9 @@ impl Component for PivotColumn { let col_type = ctx.props().column_type.unwrap_or_else(|| { ctx.props() - .opt_session + .metadata .as_ref() - .and_then(|x| x.metadata().get_column_table_type(&ctx.props().column)) + .and_then(|x| x.get_column_table_type(&ctx.props().column)) .unwrap_or(ColumnType::Integer) }); diff --git a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs index 02f7abea05..40ba61d4eb 100644 --- a/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs +++ b/rust/perspective-viewer/src/rust/components/column_selector/sort_column.rs @@ -10,8 +10,9 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ +use std::rc::Rc; + use perspective_client::config::*; -use perspective_client::utils::PerspectiveResultExt; use perspective_js::utils::ApiFuture; use web_sys::*; use yew::prelude::*; @@ -19,17 +20,21 @@ use yew::prelude::*; use crate::components::containers::dragdrop_list::*; use crate::components::type_icon::TypeIcon; use crate::dragdrop::*; -use crate::model::*; use crate::renderer::*; use crate::session::*; use crate::utils::*; -use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties)] pub struct SortColumnProps { pub sort: Sort, pub idx: usize, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// Current view config — threaded as a value prop. + pub view_config: Rc, + // State pub session: Session, pub renderer: Renderer, @@ -38,7 +43,10 @@ pub struct SortColumnProps { impl PartialEq for SortColumnProps { fn eq(&self, other: &Self) -> bool { - self.sort == other.sort && self.idx == other.idx + self.sort == other.sort + && self.idx == other.idx + && self.metadata == other.metadata + && self.view_config == other.view_config } } @@ -69,8 +77,8 @@ impl Component for SortColumn { fn update(&mut self, ctx: &Context, msg: SortColumnMsg) -> bool { match msg { SortColumnMsg::SortDirClick(shift_key) => { - let is_split = ctx.props().session.get_view_config().split_by.is_empty(); - let mut sort = ctx.props().session.get_view_config().sort.clone(); + let is_split = ctx.props().view_config.split_by.is_empty(); + let mut sort = ctx.props().view_config.sort.clone(); let sort_column = &mut sort.get_mut(ctx.props().idx).expect("Sort on no column"); sort_column.1 = sort_column.1.cycle(!is_split, shift_key); let update = ViewConfigUpdate { @@ -78,10 +86,14 @@ impl Component for SortColumn { ..ViewConfigUpdate::default() }; - ctx.props() - .update_and_render(update) - .map(ApiFuture::spawn) - .unwrap_or_log(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); + if session.update_view_config(update).is_ok() { + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } false }, @@ -110,8 +122,7 @@ impl Component for SortColumn { let col_type = ctx .props() - .session - .metadata() + .metadata .get_column_table_type(&ctx.props().sort.0.to_owned()) .unwrap_or(ColumnType::Integer); diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs index ab96691ee3..3c7b57a6a9 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar.rs @@ -18,7 +18,7 @@ use std::rc::Rc; use derivative::Derivative; use itertools::Itertools; -use perspective_client::config::{ColumnType, Expression}; +use perspective_client::config::{ColumnType, Expression, ViewConfig}; use perspective_client::utils::PerspectiveResultExt; use yew::{Callback, Component, Html, Properties, html, props}; @@ -34,14 +34,16 @@ use crate::components::expression_editor::ExpressionEditorProps; use crate::components::style::LocalStyle; use crate::components::type_icon::TypeIconType; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::{ColumnLocator, ColumnSettingsTab, Presentation}; use crate::renderer::Renderer; -use crate::session::Session; -use crate::utils::*; +use crate::session::{Session, SessionMetadata}; +use crate::tasks::{ + EditExpression, HasCustomEvents, HasPresentation, HasRenderer, HasSession, + can_render_column_styles, locator_name_or_default, locator_view_type, +}; use crate::*; -#[derive(Clone, Derivative, Properties, PerspectiveProperties!)] +#[derive(Clone, Derivative, Properties)] #[derivative(Debug)] pub struct ColumnSettingsPanelProps { pub selected_column: ColumnLocator, @@ -50,6 +52,17 @@ pub struct ColumnSettingsPanelProps { pub width_override: Option, pub on_select_tab: Callback, + /// Active plugin name threaded as a value prop so that plugin changes + /// trigger re-initialization via `changed()` rather than a PubSub + /// `render_limits_changed` subscription. + pub plugin_name: Option, + + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + + /// View config snapshot — threaded from `SessionProps`. + pub view_config: Rc, + // State #[derivative(Debug = "ignore")] pub custom_events: CustomEvents, @@ -66,7 +79,35 @@ pub struct ColumnSettingsPanelProps { impl PartialEq for ColumnSettingsPanelProps { fn eq(&self, other: &Self) -> bool { - self.selected_column == other.selected_column && self.selected_tab == other.selected_tab + self.selected_column == other.selected_column + && self.selected_tab == other.selected_tab + && self.plugin_name == other.plugin_name + && self.metadata == other.metadata + && self.view_config == other.view_config + } +} + +impl HasCustomEvents for ColumnSettingsPanelProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for ColumnSettingsPanelProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for ColumnSettingsPanelProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for ColumnSettingsPanelProps { + fn session(&self) -> &Session { + &self.session } } @@ -80,7 +121,6 @@ pub enum ColumnSettingsPanelMsg { OnSaveAttributes(()), OnResetAttributes(()), OnDelete(()), - SessionUpdated(bool), } #[derive(Derivative)] @@ -102,9 +142,6 @@ pub struct ColumnSettingsPanel { save_count: u8, save_enabled: bool, tabs: Vec, - - #[derivative(Debug = "ignore")] - _session_sub: Option, } impl Component for ColumnSettingsPanel { @@ -112,18 +149,7 @@ impl Component for ColumnSettingsPanel { type Properties = ColumnSettingsPanelProps; fn create(ctx: &yew::prelude::Context) -> Self { - let session_cb = ctx - .link() - .callback(|(is_update, _)| ColumnSettingsPanelMsg::SessionUpdated(is_update)); - - let session_sub = ctx - .props() - .renderer - .render_limits_changed - .add_listener(session_cb); - let mut this = Self { - _session_sub: Some(session_sub), initial_expr_value: Rc::default(), expr_value: Rc::default(), expr_valid: false, @@ -233,14 +259,6 @@ impl Component for ColumnSettingsPanel { ctx.props().on_close.emit(()); true }, - ColumnSettingsPanelMsg::SessionUpdated(is_update) => { - if !is_update { - self.initialize(ctx); - true - } else { - false - } - }, } } @@ -264,6 +282,7 @@ impl Component for ColumnSettingsPanel { ColumnSettingsPanelMsg::SetHeaderValid(valid), ] }), + metadata: ctx.props().metadata.clone(), session: &ctx.props().session }); @@ -274,13 +293,27 @@ impl Component for ColumnSettingsPanel { alias: ctx.props().selected_column.name().cloned(), disabled: !ctx.props().selected_column.is_expr(), reset_count: self.reset_count, + metadata: ctx.props().metadata.clone(), session: &ctx.props().session }); let disable_delete = ctx .props() - .session - .is_locator_active(&ctx.props().selected_column); + .selected_column + .name() + .map(|name| { + let config = &ctx.props().view_config; + config.columns.iter().any(|maybe_col| { + maybe_col + .as_ref() + .map(|col| col == name) + .unwrap_or_default() + }) || config.group_by.iter().any(|col| col == name) + || config.split_by.iter().any(|col| col == name) + || config.filter.iter().any(|col| col.column() == name) + || config.sort.iter().any(|col| &col.0 == name) + }) + .unwrap_or_default(); let save_section = SaveSettingsProps { save_enabled: self.save_enabled, @@ -302,15 +335,17 @@ impl Component for ColumnSettingsPanel { save_section, }; - let style_tab = props!(StyleTabProps { + let style_tab = StyleTabProps { ty: self.maybe_ty, column_name: self.column_name.clone(), - group_by_depth: ctx.props().session.get_view_config().group_by.len() as u32, - custom_events: ctx.props().custom_events(), - presentation: ctx.props().presentation(), - renderer: ctx.props().renderer(), - session: ctx.props().session() - }); + group_by_depth: ctx.props().view_config.group_by.len() as u32, + view_config: ctx.props().view_config.clone(), + metadata: ctx.props().metadata.clone(), + custom_events: ctx.props().custom_events.clone(), + presentation: ctx.props().presentation.clone(), + renderer: ctx.props().renderer.clone(), + session: ctx.props().session.clone(), + }; let tab_children = self.tabs.iter().map(|tab| match tab { ColumnSettingsTab::Attributes => html! { }, @@ -356,15 +391,12 @@ impl ColumnSettingsPanel { } fn initialize(&mut self, ctx: &yew::prelude::Context) { - let column_name = ctx - .props() - .session - .locator_name_or_default(&ctx.props().selected_column); + let column_name = + locator_name_or_default(&ctx.props().metadata, &ctx.props().selected_column); let initial_expr_value = ctx .props() - .session - .metadata() + .metadata .get_expression_by_alias(&column_name) .unwrap_or_default(); @@ -372,19 +404,19 @@ impl ColumnSettingsPanel { let initial_header_value = (*initial_expr_value != column_name).then_some(column_name.clone()); - let maybe_ty = ctx - .props() - .session() - .locator_view_type(&ctx.props().selected_column); + let maybe_ty = locator_view_type(&ctx.props().metadata, &ctx.props().selected_column); let tabs = { let mut tabs = vec![]; let is_new_expr = ctx.props().selected_column.is_new_expr(); let show_styles = !is_new_expr - && ctx - .props() - .can_render_column_styles(&column_name) - .unwrap_or_default(); + && can_render_column_styles( + &ctx.props().renderer, + &ctx.props().view_config, + &ctx.props().metadata, + &column_name, + ) + .unwrap_or_default(); if !is_new_expr && show_styles { tabs.push(ColumnSettingsTab::Style); @@ -414,7 +446,6 @@ impl ColumnSettingsPanel { on_input, on_save, on_validate, - _session_sub: self._session_sub.take(), ..*self } } diff --git a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs index a64e8562ef..0322ef40f8 100644 --- a/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs +++ b/rust/perspective-viewer/src/rust/components/column_settings_sidebar/style_tab.rs @@ -14,6 +14,8 @@ mod agg_depth_selector; mod stub; mod symbol; +use std::rc::Rc; + use itertools::Itertools; use perspective_client::config::ColumnType; use yew::{Html, Properties, function_component, html}; @@ -26,18 +28,26 @@ use crate::components::number_column_style::NumberColumnStyle; use crate::components::string_column_style::StringColumnStyle; use crate::components::style_controls::CustomNumberFormat; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::Presentation; use crate::renderer::Renderer; use crate::session::Session; -use crate::*; +use crate::tasks::{ + HasCustomEvents, HasPresentation, HasRenderer, HasSession, SendPluginConfig, + get_column_style_control_options, +}; -#[derive(Clone, PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct StyleTabProps { pub ty: Option, pub column_name: String, pub group_by_depth: u32, + /// View config snapshot — threaded from parent. + pub view_config: Rc, + + /// Session metadata snapshot — threaded from parent. + pub metadata: Rc, + // State pub custom_events: CustomEvents, pub presentation: Presentation, @@ -45,123 +55,151 @@ pub struct StyleTabProps { pub session: Session, } +impl HasCustomEvents for StyleTabProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for StyleTabProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for StyleTabProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for StyleTabProps { + fn session(&self) -> &Session { + &self.session + } +} + #[function_component] pub fn StyleTab(props: &StyleTabProps) -> Html { - let config = props.presentation().get_columns_config(&props.column_name); + let config = props.presentation.get_columns_config(&props.column_name); let on_change = yew::use_callback( - (props.clone_state(), props.column_name.clone()), + (props.clone(), props.column_name.clone()), |config, (state, column_name)| { state.send_plugin_config(column_name, config); }, ); - let components = props - .get_column_style_control_options(&props.column_name) - .map(|opts| { - let mut components = vec![]; - if !props.session().get_view_config().group_by.is_empty() { - let aggregate_depth = config.as_ref().map(|x| x.aggregate_depth as f64); - components.push(("Aggregate Depth", html! { - - })); - } - - if let Some(default_config) = opts.datagrid_number_style { - let config = config - .as_ref() - .map(|config| config.datagrid_number_style.clone()); - - components.push(("Number Styles", html! { - - })); - } - if let Some(default_config) = opts.datagrid_string_style { - let config = config - .as_ref() - .map(|config| config.datagrid_string_style.clone()); - - components.push(("String Styles", html! { - - })); - } - - if let Some(default_config) = opts.datagrid_datetime_style { - let config = config - .as_ref() - .map(|config| config.datagrid_datetime_style.clone()); - - let enable_time_config = props.ty.unwrap() == ColumnType::Datetime; - components.push(("Datetime Styles", html! { - - })) - } - - if let Some(default_config) = opts.symbols { - let restored_config = config - .as_ref() - .map(|config| config.symbols.clone()) - .unwrap_or_default(); - - components.push(("Symbols", html! { - - })) - } - - if opts.number_string_format.unwrap_or_default() { - let restored_config = config - .as_ref() - .and_then(|config| config.number_format.clone()) - .unwrap_or_default(); - - components.push(("Number Formatting", html! { - - })); - } - - components - .into_iter() - .map(|(_title, component)| { - html! { -
- // { title } - { component } -
- } - }) - .collect_vec() - }) - .unwrap_or_else(|error| { - vec![html! { - - }] - }); + let components = get_column_style_control_options( + &props.renderer, + &props.view_config, + &props.metadata, + &props.column_name, + ) + .map(|opts| { + let mut components = vec![]; + if !props.view_config.group_by.is_empty() { + let aggregate_depth = config.as_ref().map(|x| x.aggregate_depth as f64); + components.push(("Aggregate Depth", html! { + + })); + } + + if let Some(default_config) = opts.datagrid_number_style { + let config = config + .as_ref() + .map(|config| config.datagrid_number_style.clone()); + + components.push(("Number Styles", html! { + + })); + } + if let Some(default_config) = opts.datagrid_string_style { + let config = config + .as_ref() + .map(|config| config.datagrid_string_style.clone()); + + components.push(("String Styles", html! { + + })); + } + + if let Some(default_config) = opts.datagrid_datetime_style { + let config = config + .as_ref() + .map(|config| config.datagrid_datetime_style.clone()); + + let enable_time_config = props.ty.unwrap() == ColumnType::Datetime; + components.push(("Datetime Styles", html! { + + })) + } + + if let Some(default_config) = opts.symbols { + let restored_config = config + .as_ref() + .map(|config| config.symbols.clone()) + .unwrap_or_default(); + + components.push(("Symbols", html! { + + })) + } + + if opts.number_string_format.unwrap_or_default() { + let restored_config = config + .as_ref() + .and_then(|config| config.number_format.clone()) + .unwrap_or_default(); + + components.push(("Number Formatting", html! { + + })); + } + + components + .into_iter() + .map(|(_title, component)| { + html! { +
+ // { title } + { component } +
+ } + }) + .collect_vec() + }) + .unwrap_or_else(|error| { + vec![html! { + + }] + }); html! {
diff --git a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs index 10f1d4a9ce..f87cbfacbb 100644 --- a/rust/perspective-viewer/src/rust/components/copy_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/copy_dropdown.rs @@ -17,8 +17,8 @@ use yew::prelude::*; use super::containers::dropdown_menu::*; use super::modal::*; use super::style::StyleProvider; -use crate::model::*; use crate::renderer::*; +use crate::tasks::*; use crate::utils::*; type CopyDropDownMenuItem = DropDownMenuItem; @@ -39,9 +39,7 @@ impl ModalLink for CopyDropDownMenuProps { } } -pub struct CopyDropDownMenu { - _sub: Subscription, -} +pub struct CopyDropDownMenu {} impl Component for CopyDropDownMenu { type Message = (); @@ -49,13 +47,7 @@ impl Component for CopyDropDownMenu { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); - let _sub = ctx - .props() - .renderer - .plugin_changed - .add_listener(ctx.link().callback(|_| ())); - - Self { _sub } + Self {} } fn update(&mut self, _ctx: &Context, _msg: Self::Message) -> bool { diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs index 348b4a21ed..52494d5ba3 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style.rs @@ -56,8 +56,10 @@ impl ModalLink for DatetimeColumnStyleProps { } impl PartialEq for DatetimeColumnStyleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config + && self.config == other.config + && self.default_config == other.default_config } } diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs index f1407551e9..5bc823b589 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/custom.rs @@ -40,8 +40,8 @@ impl ModalLink for DatetimeStyleCustomProps { } impl PartialEq for DatetimeStyleCustomProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config && self.config == other.config } } diff --git a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs index a2ebedacc3..a0c6ae944b 100644 --- a/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs +++ b/rust/perspective-viewer/src/rust/components/datetime_column_style/simple.rs @@ -36,8 +36,8 @@ impl ModalLink for DatetimeStyleSimpleProps { } impl PartialEq for DatetimeStyleSimpleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.enable_time_config == other.enable_time_config && self.config == other.config } } diff --git a/rust/perspective-viewer/src/rust/components/editable_header.rs b/rust/perspective-viewer/src/rust/components/editable_header.rs index 2280af03ca..7839cc82d9 100644 --- a/rust/perspective-viewer/src/rust/components/editable_header.rs +++ b/rust/perspective-viewer/src/rust/components/editable_header.rs @@ -18,10 +18,10 @@ use yew::{Callback, Component, Html, NodeRef, Properties, TargetCast, classes, h use super::type_icon::TypeIconType; use crate::components::type_icon::TypeIcon; -use crate::session::Session; -use crate::*; +use crate::maybe; +use crate::session::{Session, SessionMetadata}; -#[derive(Clone, PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct EditableHeaderProps { pub icon_type: Option, pub on_change: Callback<(Option, bool)>, @@ -32,6 +32,9 @@ pub struct EditableHeaderProps { #[prop_or_default] pub reset_count: u8, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, } @@ -111,7 +114,7 @@ impl Component for EditableHeader { if !self.edited { return Some(true); } - let metadata = ctx.props().session.metadata(); + let metadata = &ctx.props().metadata; let expressions = metadata.get_expression_columns(); let found = metadata .get_table_columns()? diff --git a/rust/perspective-viewer/src/rust/components/export_dropdown.rs b/rust/perspective-viewer/src/rust/components/export_dropdown.rs index 4fe642a681..547c953190 100644 --- a/rust/perspective-viewer/src/rust/components/export_dropdown.rs +++ b/rust/perspective-viewer/src/rust/components/export_dropdown.rs @@ -18,8 +18,8 @@ use yew::prelude::*; use super::containers::dropdown_menu::*; use super::modal::{ModalLink, SetModalLink}; use super::style::StyleProvider; -use crate::model::*; use crate::renderer::*; +use crate::tasks::*; use crate::utils::*; use crate::*; @@ -49,7 +49,6 @@ pub enum ExportDropDownMenuMsg { #[derive(Default)] pub struct ExportDropDownMenu { title: String, - _sub: Option, input_ref: NodeRef, invalid: bool, } @@ -98,20 +97,12 @@ impl Component for ExportDropDownMenu { fn create(ctx: &Context) -> Self { ctx.set_modal_link(); - let _sub = Some( - ctx.props() - .renderer - .plugin_changed - .add_listener(ctx.link().callback(|_| ExportDropDownMenuMsg::TitleChange)), - ); - Self { title: ctx .props() .session .get_title() .unwrap_or_else(|| "untitled".to_owned()), - _sub, ..Default::default() } } diff --git a/rust/perspective-viewer/src/rust/components/expression_editor.rs b/rust/perspective-viewer/src/rust/components/expression_editor.rs index 66389a2ad9..aa37a04bbe 100644 --- a/rust/perspective-viewer/src/rust/components/expression_editor.rs +++ b/rust/perspective-viewer/src/rust/components/expression_editor.rs @@ -17,11 +17,10 @@ use yew::prelude::*; use super::form::code_editor::*; use super::style::LocalStyle; -use crate::model::*; -use crate::session::Session; +use crate::session::{Session, SessionMetadata}; use crate::*; -#[derive(Properties, PartialEq, PerspectiveProperties!, Clone)] +#[derive(Properties, PartialEq, Clone)] pub struct ExpressionEditorProps { pub on_save: Callback<()>, pub on_validate: Callback, @@ -32,6 +31,9 @@ pub struct ExpressionEditorProps { #[prop_or_default] pub reset_count: u8, + /// Session metadata snapshot — threaded from `SessionProps`. + pub metadata: Rc, + // State pub session: Session, } @@ -55,7 +57,7 @@ impl Component for ExpressionEditor { fn create(ctx: &Context) -> Self { let oninput = ctx.link().callback(ExpressionEditorMsg::SetExpr); - let expr = initial_expr(&ctx.props().session, &ctx.props().alias); + let expr = initial_expr(&ctx.props().metadata, &ctx.props().alias); ctx.link() .send_message(Self::Message::SetExpr(expr.clone())); @@ -89,8 +91,8 @@ impl Component for ExpressionEditor { if self.error.is_none() { maybe!({ let alias = ctx.props().alias.as_ref()?; - let session = ctx.props().session(); - let old = session.metadata().get_expression_by_alias(alias)?; + let session = &ctx.props().session; + let old = ctx.props().metadata.get_expression_by_alias(alias)?; let is_edited = *self.expr != old; session .metadata_mut() @@ -140,7 +142,7 @@ impl Component for ExpressionEditor { { ctx.link() .send_message(ExpressionEditorMsg::SetExpr(initial_expr( - &ctx.props().session, + &ctx.props().metadata, &ctx.props().alias, ))); false @@ -150,10 +152,10 @@ impl Component for ExpressionEditor { } } -fn initial_expr(session: &Session, alias: &Option) -> Rc { +fn initial_expr(metadata: &SessionMetadata, alias: &Option) -> Rc { alias .as_ref() - .and_then(|alias| session.metadata().get_expression_by_alias(alias)) + .and_then(|alias| metadata.get_expression_by_alias(alias)) .unwrap_or_default() .into() } diff --git a/rust/perspective-viewer/src/rust/components/font_loader.rs b/rust/perspective-viewer/src/rust/components/font_loader.rs index 5fcab0878d..39a6a6bcd1 100644 --- a/rust/perspective-viewer/src/rust/components/font_loader.rs +++ b/rust/perspective-viewer/src/rust/components/font_loader.rs @@ -47,35 +47,18 @@ impl PartialEq for FontLoaderProps { } } -/// The `FontLoader` component ensures that fonts are loaded before they are -/// visible. -pub struct FontLoader {} - -impl Component for FontLoader { - type Message = (); - type Properties = FontLoaderProps; - - fn create(_ctx: &Context) -> Self { - Self {} - } - - fn update(&mut self, _ctx: &Context, _msg: ()) -> bool { - false - } +#[function_component(FontLoader)] +pub fn font_loader(props: &FontLoaderProps) -> Html { + if matches!(props.get_status(), FontLoaderStatus::Finished) { + html! {} + } else { + let inner = props + .get_fonts() + .iter() + .map(font_test_html) + .collect::(); - fn view(&self, ctx: &Context) -> yew::virtual_dom::VNode { - if matches!(ctx.props().get_status(), FontLoaderStatus::Finished) { - html! {} - } else { - let inner = ctx - .props() - .get_fonts() - .iter() - .map(font_test_html) - .collect::(); - - html! { <>{ inner } } - } + html! { <>{ inner } } } } diff --git a/rust/perspective-viewer/src/rust/components/form/debug.rs b/rust/perspective-viewer/src/rust/components/form/debug.rs index ae58c94bba..2e1c7bf7b2 100644 --- a/rust/perspective-viewer/src/rust/components/form/debug.rs +++ b/rust/perspective-viewer/src/rust/components/form/debug.rs @@ -20,21 +20,39 @@ use yew::prelude::*; use crate::components::containers::trap_door_panel::TrapDoorPanel; use crate::components::form::code_editor::CodeEditor; use crate::components::style::LocalStyle; +use crate::css; use crate::js::{MimeType, copy_to_clipboard, paste_from_clipboard}; -use crate::model::*; use crate::presentation::*; use crate::renderer::*; use crate::session::*; +use crate::tasks::*; use crate::utils::*; -use crate::{PerspectiveProperties, css}; -#[derive(PartialEq, Properties, PerspectiveProperties!)] +#[derive(Clone, PartialEq, Properties)] pub struct DebugPanelProps { pub presentation: Presentation, pub renderer: Renderer, pub session: Session, } +impl HasPresentation for DebugPanelProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for DebugPanelProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for DebugPanelProps { + fn session(&self) -> &Session { + &self.session + } +} + #[function_component(DebugPanel)] pub fn debug_panel(props: &DebugPanelProps) -> Html { let expr = use_state_eq(|| Rc::new("".to_string())); @@ -42,7 +60,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { let select_all = use_memo((), |()| PubSub::default()); let modified = use_state_eq(|| false); - use_effect_with((expr.setter(), props.clone_state()), { + use_effect_with((expr.setter(), props.clone()), { clone!(error, modified); move |(text, state)| { state.set_text(text.clone()); @@ -57,7 +75,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { )); let sub2 = state - .renderer() + .renderer .reset_changed .add_listener(state.reset_callback( text.clone(), @@ -66,7 +84,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { )); let sub3 = state - .session() + .session .view_config_changed .add_listener(state.reset_callback( text.clone(), @@ -90,7 +108,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } }); - let onsave = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onsave = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| props.on_save(text, error, &modified) }); @@ -111,12 +129,12 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { }, ); - let onapply = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onapply = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| props.on_save(text, error, &modified) }); - let onreset = use_callback((expr.setter(), error.clone(), props.clone_state()), { + let onreset = use_callback((expr.setter(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| { props.set_text(text.clone()); @@ -125,7 +143,7 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } }); - let onpaste = use_callback((expr.clone(), error.clone(), props.clone_state()), { + let onpaste = use_callback((expr.clone(), error.clone(), props.clone()), { clone!(modified); move |_, (text, error, props)| { clone!(text, error, props, modified); @@ -181,12 +199,11 @@ pub fn debug_panel(props: &DebugPanelProps) -> Html { } } -impl DebugPanelPropsState { +impl DebugPanelProps { fn set_text(&self, setter: UseStateSetter>) { let props = self.clone(); ApiFuture::spawn(async move { - let task = props.get_viewer_config(); - let config = task.await?; + let config = props.get_viewer_config().await?; let json = JsValue::from_serde_ext(&config)?; let js_string = js_sys::JSON::stringify_with_replacer_and_space(&json, &JsValue::NULL, &2.into())?; diff --git a/rust/perspective-viewer/src/rust/components/main_panel.rs b/rust/perspective-viewer/src/rust/components/main_panel.rs index 5997edd3ea..54fc5be1d8 100644 --- a/rust/perspective-viewer/src/rust/components/main_panel.rs +++ b/rust/perspective-viewer/src/rust/components/main_panel.rs @@ -10,24 +10,50 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use futures::channel::oneshot::*; +use std::rc::Rc; + use perspective_js::utils::*; use wasm_bindgen::prelude::*; use yew::prelude::*; use super::render_warning::RenderWarning; use super::status_bar::StatusBar; -use crate::PerspectiveProperties; use crate::custom_events::CustomEvents; use crate::presentation::Presentation; +use crate::renderer::limits::RenderLimits; use crate::renderer::*; -use crate::session::*; +use crate::session::{Session, TableErrorState, ViewStats}; use crate::utils::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct MainPanelProps { pub on_settings: Callback<()>, + /// Reset callback forwarded from the root component. Fired when the user + /// clicks the reset button; `bool` is `true` for a full reset (expressions + /// + column configs), `false` for config-only. + pub on_reset: Callback, + + /// Render-limit dimensions forwarded from the root's `RendererProps`. + /// `Some` when the active plugin is capping the rendered row/column count; + /// `None` when no limits are active (e.g. after a plugin change). + pub render_limits: Option, + + /// Value props from root's `SessionProps`, threaded to `StatusBar` / + /// `StatusIndicator`. + pub has_table: bool, + pub is_errored: bool, + pub stats: Option, + pub update_count: u32, + pub error: Option, + pub title: Option, + + /// Value props from root's `PresentationProps`, threaded to `StatusBar`. + pub is_settings_open: bool, + pub selected_theme: Option, + pub available_themes: Rc>, + pub is_workspace: bool, + /// State pub custom_events: CustomEvents, pub session: Session, @@ -36,28 +62,33 @@ pub struct MainPanelProps { } impl PartialEq for MainPanelProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.has_table == rhs.has_table + && self.is_errored == rhs.is_errored + && self.stats == rhs.stats + && self.update_count == rhs.update_count + && self.error == rhs.error + && self.title == rhs.title + && self.is_settings_open == rhs.is_settings_open + && self.selected_theme == rhs.selected_theme + && self.available_themes == rhs.available_themes + && self.is_workspace == rhs.is_workspace + && self.render_limits == rhs.render_limits } } impl MainPanelProps { fn is_title(&self) -> bool { - self.session.get_title().is_some() + self.title.is_some() } } #[derive(Debug)] pub enum MainPanelMsg { - Reset(bool, Option>), - RenderLimits(Option<(usize, usize, Option, Option)>), PointerEvent(web_sys::PointerEvent), - Error, } pub struct MainPanel { - _subscriptions: [Subscription; 2], - dimensions: Option<(usize, usize, Option, Option)>, main_panel_ref: NodeRef, } @@ -65,85 +96,14 @@ impl Component for MainPanel { type Message = MainPanelMsg; type Properties = MainPanelProps; - fn create(ctx: &Context) -> Self { - let session_sub = { - let callback = ctx.link().callback(move |(_, render_limits)| { - MainPanelMsg::RenderLimits(Some(render_limits)) - }); - - ctx.props() - .renderer - .render_limits_changed - .add_listener(callback) - }; - - let error_sub = ctx - .props() - .session - .table_errored - .add_listener(ctx.link().callback(|_| MainPanelMsg::Error)); - + fn create(_ctx: &Context) -> Self { Self { - _subscriptions: [session_sub, error_sub], - dimensions: None, main_panel_ref: NodeRef::default(), } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { match msg { - MainPanelMsg::Error => true, - MainPanelMsg::Reset(all, sender) => { - ctx.props().presentation.set_open_column_settings(None); - - clone!( - ctx.props().renderer, - ctx.props().session, - ctx.props().presentation - ); - - ApiFuture::spawn(async move { - session - .reset(ResetOptions { - config: true, - expressions: all, - ..ResetOptions::default() - }) - .await?; - let columns_config = if all { - presentation.reset_columns_configs(); - None - } else { - Some(presentation.all_columns_configs()) - }; - - renderer.reset(columns_config.as_ref()).await?; - presentation.reset_available_themes(None).await; - if all { - presentation.reset_theme().await?; - } - - let result = renderer.draw(session.validate().await?.create_view()).await; - if let Some(sender) = sender { - sender.send(()).unwrap(); - } - - renderer.reset_changed.emit(()); - result - }); - - false - }, - - MainPanelMsg::RenderLimits(dimensions) => { - if self.dimensions != dimensions { - self.dimensions = dimensions; - true - } else { - false - } - }, - MainPanelMsg::PointerEvent(event) => { if event.target().map(JsValue::from) == self @@ -175,8 +135,7 @@ impl Component for MainPanel { .. } = ctx.props(); - let is_settings_open = - ctx.props().presentation.is_settings_open() && ctx.props().session.has_table(); + let is_settings_open = ctx.props().is_settings_open && ctx.props().has_table; let on_settings = (!is_settings_open).then(|| ctx.props().on_settings.clone()); @@ -189,14 +148,34 @@ impl Component for MainPanel { class.push("titled"); } - let on_reset = ctx.link().callback(|all| MainPanelMsg::Reset(all, None)); let pointerdown = ctx.link().callback(MainPanelMsg::PointerEvent); + let on_dismiss_warning = { + clone!(renderer, session); + Callback::from(move |_: ()| { + clone!(renderer, session); + ApiFuture::spawn(async move { + renderer.disable_active_plugin_render_warning(); + let view_task = session.get_view(); + renderer.update(view_task).await + }); + }) + }; html! {
- +
diff --git a/rust/perspective-viewer/src/rust/components/mod.rs b/rust/perspective-viewer/src/rust/components/mod.rs index a309e26168..dfea92f649 100644 --- a/rust/perspective-viewer/src/rust/components/mod.rs +++ b/rust/perspective-viewer/src/rust/components/mod.rs @@ -22,7 +22,6 @@ pub mod copy_dropdown; pub mod datetime_column_style; pub mod editable_header; pub mod empty_row; -pub mod error_message; pub mod export_dropdown; pub mod expression_editor; pub mod filter_dropdown; diff --git a/rust/perspective-viewer/src/rust/components/number_column_style.rs b/rust/perspective-viewer/src/rust/components/number_column_style.rs index a0fd1a9d58..6a2ba9b13c 100644 --- a/rust/perspective-viewer/src/rust/components/number_column_style.rs +++ b/rust/perspective-viewer/src/rust/components/number_column_style.rs @@ -65,8 +65,10 @@ impl ModalLink for NumberColumnStyleProps { } impl PartialEq for NumberColumnStyleProps { - fn eq(&self, _other: &Self) -> bool { - false + fn eq(&self, other: &Self) -> bool { + self.config == other.config + && self.default_config == other.default_config + && self.column_name == other.column_name } } diff --git a/rust/perspective-viewer/src/rust/components/plugin_selector.rs b/rust/perspective-viewer/src/rust/components/plugin_selector.rs index cb44d0d2b5..e22bfd56c7 100644 --- a/rust/perspective-viewer/src/rust/components/plugin_selector.rs +++ b/rust/perspective-viewer/src/rust/components/plugin_selector.rs @@ -10,106 +10,53 @@ // ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃ // ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛ -use perspective_client::config::ViewConfigUpdate; -use perspective_js::utils::ApiFuture; +use std::rc::Rc; + use yew::prelude::*; -use super::containers::select::*; use super::style::LocalStyle; -use crate::config::*; -use crate::js::*; -use crate::model::*; -use crate::presentation::Presentation; -use crate::renderer::*; -use crate::session::*; -use crate::utils::*; -use crate::{css, *}; - -#[derive(Properties, PartialEq, PerspectiveProperties!)] +use crate::css; + +/// Pure value props — no engine handles, no PubSub subscriptions. +/// The parent passes updated values whenever the renderer state changes. +#[derive(Properties, PartialEq)] pub struct PluginSelectorProps { - pub presentation: Presentation, - pub renderer: Renderer, - pub session: Session, + /// Name of the currently active plugin. + pub plugin_name: Option, + + /// Flat list of all registered plugin names (all categories merged). + pub available_plugins: Rc>, + + /// Called when the user selects a different plugin. + pub on_select_plugin: Callback, } #[derive(Debug)] pub enum PluginSelectorMsg { ComponentSelectPlugin(String), - RendererSelectPlugin(String), OpenMenu, } use PluginSelectorMsg::*; pub struct PluginSelector { - options: Vec>, is_open: bool, - _plugin_sub: Subscription, } impl Component for PluginSelector { type Message = PluginSelectorMsg; type Properties = PluginSelectorProps; - fn create(ctx: &Context) -> Self { - let PluginSelectorProps { renderer, .. } = ctx.props(); - let options = generate_plugin_optgroups(renderer); - let _plugin_sub = renderer.plugin_changed.add_listener({ - let link = ctx.link().clone(); - move |plugin: JsPerspectiveViewerPlugin| { - let name = plugin.name(); - link.send_message(PluginSelectorMsg::RendererSelectPlugin(name)) - } - }); - - Self { - options, - is_open: false, - _plugin_sub, - } + fn create(_ctx: &Context) -> Self { + Self { is_open: false } } fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - let PluginSelectorProps { - presentation, - renderer, - session, - .. - } = ctx.props(); match msg { - RendererSelectPlugin(_plugin_name) => true, ComponentSelectPlugin(plugin_name) => { - if !session.is_errored() { - let metadata = - renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); - - let prev_metadata = renderer.metadata(); - let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); - let rollup_features = session - .metadata() - .get_features() - .map(|x| x.get_group_rollup_modes()) - .unwrap(); - - let group_rollups = requirements.get_group_rollups(&rollup_features); - let mut update = ViewConfigUpdate { - group_rollup_mode: group_rollups.first().cloned(), - ..ViewConfigUpdate::default() - }; - - session.set_update_column_defaults(&mut update, requirements); - - if let Ok(task) = ctx.props().update_and_render(update) { - ApiFuture::spawn(task); - } - - presentation.set_open_column_settings(None); - self.is_open = false; - false - } else { - self.is_open = false; - true - } + ctx.props().on_select_plugin.emit(plugin_name); + self.is_open = false; + false }, OpenMenu => { self.is_open = !self.is_open; @@ -124,22 +71,18 @@ impl Component for PluginSelector { fn view(&self, ctx: &Context) -> Html { let callback = ctx.link().callback(|_| OpenMenu); - let plugin_name = ctx.props().renderer.get_active_plugin().unwrap().name(); + let plugin_name = ctx.props().plugin_name.clone().unwrap_or_default(); let plugin_name2 = plugin_name.clone(); let class = if self.is_open { "open" } else { "" }; - let items = self.options.iter().map(|item| match item { - SelectItem::OptGroup(_cat, items) => html! { - items.iter().filter(|x| *x != &plugin_name2).map(|x| { - let callback = ctx.link().callback(ComponentSelectPlugin); - html! { - - } - }).collect::() - }, - SelectItem::Option(_item) => html! {}, - }); + let items = ctx + .props() + .available_plugins + .iter() + .filter(|x| x.as_str() != plugin_name2.as_str()) + .map(|x| { + let callback = ctx.link().callback(ComponentSelectPlugin); + html! { } + }); html! { <> @@ -156,19 +99,6 @@ impl Component for PluginSelector { } } -/// Generate the opt groups for the plugin selector by collecting by category -/// then sorting. -fn generate_plugin_optgroups(renderer: &Renderer) -> Vec> { - let mut options = renderer - .get_all_plugin_categories() - .into_iter() - .map(|(category, value)| SelectItem::OptGroup(category.into(), value)) - .collect::>(); - - options.sort_by_key(|x| x.name()); - options -} - #[derive(Properties, PartialEq)] struct PluginSelectProps { name: String, diff --git a/rust/perspective-viewer/src/rust/components/render_warning.rs b/rust/perspective-viewer/src/rust/components/render_warning.rs index 10cae11d61..4301cc68a6 100644 --- a/rust/perspective-viewer/src/rust/components/render_warning.rs +++ b/rust/perspective-viewer/src/rust/components/render_warning.rs @@ -13,144 +13,93 @@ use yew::prelude::*; use super::style::LocalStyle; -use crate::model::*; -use crate::renderer::*; -use crate::session::*; -use crate::*; +use crate::css; +use crate::renderer::limits::RenderLimits; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Properties, PartialEq)] pub struct RenderWarningProps { - // Current dimensions - pub dimensions: Option<(usize, usize, Option, Option)>, + pub dimensions: Option, - // State - pub renderer: Renderer, - pub session: Session, + /// Called when the user clicks "Render all points". The parent disables + /// the render warning on the active plugin and re-draws. + pub on_dismiss: Callback<()>, } -impl PartialEq for RenderWarningProps { - fn eq(&self, other: &Self) -> bool { - self.dimensions == other.dimensions - } -} - -pub enum RenderWarningMsg { - DismissWarning, -} - -pub struct RenderWarning { - col_warn: Option<(usize, usize)>, - row_warn: Option<(usize, usize)>, -} - -impl RenderWarning { - fn update_warnings(&mut self, ctx: &Context) { - if let Some((num_cols, num_rows, max_cols, max_rows)) = ctx.props().dimensions { - let count = num_cols * num_rows; - if max_cols.is_some_and(|x| x < num_cols) { - self.col_warn = Some((max_cols.unwrap(), num_cols)); - } else { - self.col_warn = None; - } - - if max_rows.is_some_and(|x| x < num_rows) { - self.row_warn = Some((num_cols * max_rows.unwrap(), count)); - } else { - self.row_warn = None; - } +#[function_component(RenderWarning)] +pub fn render_warning(props: &RenderWarningProps) -> Html { + let dimensions = props.dimensions; + let (col_warn, row_warn) = if let Some(limits) = dimensions { + let col_warn = if limits.max_cols.is_some_and(|x| x < limits.num_cols) { + Some((limits.max_cols.unwrap(), limits.num_cols)) } else { - self.col_warn = None; - self.row_warn = None; - } - } -} - -impl Component for RenderWarning { - type Message = RenderWarningMsg; - type Properties = RenderWarningProps; - - fn create(ctx: &Context) -> Self { - let mut elem = Self { - col_warn: None, - row_warn: None, + None }; - elem.update_warnings(ctx); - elem - } + let row_warn = if limits.max_rows.is_some_and(|x| x < limits.num_rows) { + Some(( + limits.num_cols * limits.max_rows.unwrap(), + limits.num_cols * limits.num_rows, + )) + } else { + None + }; - fn update(&mut self, ctx: &Context, msg: Self::Message) -> bool { - match msg { - RenderWarningMsg::DismissWarning => { - let state = ctx.props().clone_state(); - ApiFuture::spawn(async move { - state.renderer().disable_active_plugin_render_warning(); - let view_task = state.session().get_view(); - state.renderer().update(view_task).await - }); + (col_warn, row_warn) + } else { + (None, None) + }; + + if col_warn.is_some() || row_warn.is_some() { + let warning = match (col_warn, row_warn) { + (Some((x, y)), Some((a, b))) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of columns and" } + { render_pair(a, b) } + { "of points." } + + }, + (Some((x, y)), None) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of columns." } + + }, + (None, Some((x, y))) => html! { + + { "Rendering" } + { render_pair(x, y) } + { "of points." } + }, + _ => html! {
}, }; - true - } - - fn changed(&mut self, ctx: &Context, _old: &Self::Properties) -> bool { - self.update_warnings(ctx); - true - } - fn view(&self, ctx: &Context) -> Html { - if self.col_warn.is_some() || self.row_warn.is_some() { - let warning = match (self.col_warn, self.row_warn) { - (Some((x, y)), Some((a, b))) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of columns and" } - { render_pair(a, b) } - { "of points." } + let on_dismiss = props.on_dismiss.clone(); + let onclick = Callback::from(move |_: MouseEvent| on_dismiss.emit(())); + html! { + <> + +
+ + + { warning } - }, - (Some((x, y)), None) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of columns." } - - }, - (None, Some((x, y))) => html! { - - { "Rendering" } - { render_pair(x, y) } - { "of points." } - - }, - _ => html! {
}, - }; - - let onclick = ctx.link().callback(|_| RenderWarningMsg::DismissWarning); - - html! { - <> - -
- - - { warning } + + + { "Render all points" } - - - { "Render all points" } - - -
- - } - } else { - html! {} + +
+ } + } else { + html! {} } } diff --git a/rust/perspective-viewer/src/rust/components/settings_panel.rs b/rust/perspective-viewer/src/rust/components/settings_panel.rs index 137325c522..4cc7418ed3 100644 --- a/rust/perspective-viewer/src/rust/components/settings_panel.rs +++ b/rust/perspective-viewer/src/rust/components/settings_panel.rs @@ -12,20 +12,23 @@ use std::rc::Rc; +use perspective_client::config::{ViewConfig, ViewConfigUpdate}; +use perspective_js::utils::ApiFuture; use yew::prelude::*; use super::column_selector::ColumnSelector; use super::plugin_selector::PluginSelector; -use crate::PerspectiveProperties; use crate::components::containers::sidebar_close_button::SidebarCloseButton; +use crate::config::PluginUpdate; use crate::dragdrop::*; -use crate::model::*; -use crate::presentation::{ColumnLocator, Presentation}; +use crate::presentation::{ColumnLocator, OpenColumnSettings, Presentation}; use crate::renderer::*; +use crate::session::column_defaults_update::*; use crate::session::*; +use crate::tasks::can_render_column_styles; use crate::utils::*; -#[derive(Clone, Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct SettingsPanelProps { pub on_close: Callback<()>, pub on_resize: Rc>, @@ -33,6 +36,22 @@ pub struct SettingsPanelProps { pub on_debug: Callback<()>, pub is_debug: bool, + /// Value props threaded from the root's `RendererProps` / `SessionProps`. + pub plugin_name: Option, + pub available_plugins: Rc>, + pub has_table: bool, + pub named_column_count: usize, + pub view_config: Rc, + /// Column currently being dragged (if any) — threaded to show drag + /// highlights without per-component `DragDrop` PubSub subscriptions. + pub drag_column: Option, + /// Cloned session metadata snapshot — threaded from `SessionProps` + /// so that metadata changes trigger re-renders via prop diffing. + pub metadata: Rc, + /// Snapshot of the column-settings sidebar state — threaded from + /// `PresentationProps` so that open/close triggers re-renders. + pub open_column_settings: OpenColumnSettings, + /// State pub dragdrop: DragDrop, pub session: Session, @@ -41,8 +60,16 @@ pub struct SettingsPanelProps { } impl PartialEq for SettingsPanelProps { - fn eq(&self, _rhs: &Self) -> bool { - false + fn eq(&self, rhs: &Self) -> bool { + self.is_debug == rhs.is_debug + && self.plugin_name == rhs.plugin_name + && self.available_plugins == rhs.available_plugins + && self.has_table == rhs.has_table + && self.named_column_count == rhs.named_column_count + && self.view_config == rhs.view_config + && self.drag_column == rhs.drag_column + && self.metadata == rhs.metadata + && self.open_column_settings == rhs.open_column_settings } } @@ -55,7 +82,72 @@ pub fn SettingsPanel(props: &SettingsPanelProps) -> Html { session, .. } = &props; - let selected_column = props.get_current_column_locator(); + + let selected_column = { + let locator = props.open_column_settings.locator.clone(); + let config = &props.view_config; + locator.filter(|locator| match locator { + ColumnLocator::Table(name) => { + locator + .name() + .map(|n| { + config.columns.iter().any(|maybe_col| { + maybe_col.as_ref().map(|col| col == n).unwrap_or_default() + }) || config.group_by.iter().any(|col| col == n) + || config.split_by.iter().any(|col| col == n) + || config.filter.iter().any(|col| col.column() == n) + || config.sort.iter().any(|col| &col.0 == n) + }) + .unwrap_or_default() + && can_render_column_styles(&props.renderer, config, &props.metadata, name) + .unwrap_or_default() + }, + _ => true, + }) + }; + + let plugin_name = props.plugin_name.clone(); + let available_plugins = props.available_plugins.clone(); + + // Dispatch callback: captures engine handles, constructs config update, renders + let on_select_plugin = { + clone!(renderer, session, presentation); + let session_metadata = props.metadata.clone(); + Callback::from(move |plugin_name: String| { + if !session.is_errored() { + let metadata = + renderer.get_next_plugin_metadata(&PluginUpdate::Update(plugin_name)); + let prev_metadata = renderer.metadata(); + let requirements = metadata.as_ref().unwrap_or(&*prev_metadata); + let rollup_features = session_metadata + .get_features() + .map(|x| x.get_group_rollup_modes()) + .unwrap(); + let group_rollups = requirements.get_group_rollups(&rollup_features); + let all_columns: Vec<_> = session_metadata + .get_table_columns() + .into_iter() + .flatten() + .cloned() + .map(Some) + .collect(); + let mut update = ViewConfigUpdate { + group_rollup_mode: group_rollups.first().cloned(), + ..ViewConfigUpdate::default() + }; + update.set_update_column_defaults(&session_metadata, &all_columns, requirements); + if session.update_view_config(update).is_ok() { + clone!(renderer, session); + ApiFuture::spawn(async move { + renderer.apply_pending_plugin()?; + renderer.draw(session.validate().await?.create_view()).await + }); + } + presentation.set_open_column_settings(None); + } + }) + }; + html! { } diff --git a/rust/perspective-viewer/src/rust/components/status_bar.rs b/rust/perspective-viewer/src/rust/components/status_bar.rs index dfe52b896d..e741b6fe9b 100644 --- a/rust/perspective-viewer/src/rust/components/status_bar.rs +++ b/rust/perspective-viewer/src/rust/components/status_bar.rs @@ -22,14 +22,13 @@ use crate::components::status_bar_counter::StatusBarRowsCounter; use crate::custom_elements::copy_dropdown::*; use crate::custom_elements::export_dropdown::*; use crate::custom_events::CustomEvents; -use crate::model::*; use crate::presentation::Presentation; use crate::renderer::*; use crate::session::*; -use crate::utils::*; +use crate::tasks::*; use crate::*; -#[derive(Properties, PerspectiveProperties!)] +#[derive(Clone, Properties)] pub struct StatusBarProps { // DOM Attribute pub id: String, @@ -41,6 +40,25 @@ pub struct StatusBarProps { #[prop_or_default] pub on_settings: Option>, + // Value props threaded from the root's `SessionProps`. + // Using these avoids PubSub subscriptions for table_loaded / table_errored. + pub has_table: bool, + pub is_errored: bool, + pub stats: Option, + /// In-flight render counter and full error, threaded to `StatusIndicator`. + pub update_count: u32, + pub error: Option, + /// Title string from session — threaded to avoid title_changed + /// subscription. + pub title: Option, + /// Theme state from presentation — threaded to avoid theme_config_updated / + /// visibility_changed subscriptions. + pub is_settings_open: bool, + pub selected_theme: Option, + pub available_themes: Rc>, + /// Whether this viewer is hosted inside a ``. + pub is_workspace: bool, + // State pub custom_events: CustomEvents, pub session: Session, @@ -51,6 +69,48 @@ pub struct StatusBarProps { impl PartialEq for StatusBarProps { fn eq(&self, other: &Self) -> bool { self.id == other.id + && self.has_table == other.has_table + && self.is_errored == other.is_errored + && self.stats == other.stats + && self.update_count == other.update_count + && self.error == other.error + && self.title == other.title + && self.is_settings_open == other.is_settings_open + && self.selected_theme == other.selected_theme + && self.available_themes == other.available_themes + && self.is_workspace == other.is_workspace + } +} + +impl HasCustomEvents for StatusBarProps { + fn custom_events(&self) -> &CustomEvents { + &self.custom_events + } +} + +impl HasPresentation for StatusBarProps { + fn presentation(&self) -> &Presentation { + &self.presentation + } +} + +impl HasRenderer for StatusBarProps { + fn renderer(&self) -> &Renderer { + &self.renderer + } +} + +impl HasSession for StatusBarProps { + fn session(&self) -> &Session { + &self.session + } +} + +impl StateProvider for StatusBarProps { + type State = StatusBarProps; + + fn clone_state(&self) -> Self::State { + self.clone() } } @@ -60,7 +120,6 @@ pub enum StatusBarMsg { Copy, Noop, Eject, - SetThemeConfig((Rc>, Option)), SetTheme(String), ResetTheme, PointerEvent(web_sys::PointerEvent), @@ -70,13 +129,13 @@ pub enum StatusBarMsg { /// A toolbar with buttons, and `Table` & `View` status information. pub struct StatusBar { - _subscriptions: [Subscription; 5], copy_ref: NodeRef, export_ref: NodeRef, input_ref: NodeRef, statusbar_ref: NodeRef, - theme: Option, - themes: Rc>, + /// Local title tracks the live `` value before the user commits the + /// change (blur / Enter). Reset to the prop value whenever the prop + /// changes. title: Option, } @@ -85,21 +144,24 @@ impl Component for StatusBar { type Properties = StatusBarProps; fn create(ctx: &Context) -> Self { - fetch_initial_theme(ctx); Self { - _subscriptions: register_listeners(ctx), copy_ref: NodeRef::default(), export_ref: NodeRef::default(), input_ref: NodeRef::default(), statusbar_ref: NodeRef::default(), - theme: None, - themes: vec![].into(), - title: ctx.props().session().get_title().clone(), + title: ctx.props().title.clone(), } } - fn changed(&mut self, ctx: &Context, _old_props: &Self::Properties) -> bool { - self._subscriptions = register_listeners(ctx); + fn changed(&mut self, ctx: &Context, old_props: &Self::Properties) -> bool { + // Keep the local title in sync with the prop whenever the session title + // changes externally (e.g. restore() call) or the settings panel opens / + // closes (which resets the input element). + if ctx.props().title != old_props.title + || ctx.props().is_settings_open != old_props.is_settings_open + { + self.title = ctx.props().title.clone(); + } true } @@ -111,27 +173,24 @@ impl Component for StatusBar { false }, StatusBarMsg::ResetTheme => { - let state = ctx.props().clone_state(); + let presentation = ctx.props().presentation.clone(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); ApiFuture::spawn(async move { - state.presentation.reset_theme().await?; - let view = state.session.get_view().into_apierror()?; - state.renderer.restyle_all(&view).await + presentation.reset_theme().await?; + let view = session.get_view().into_apierror()?; + renderer.restyle_all(&view).await }); true }, - StatusBarMsg::SetThemeConfig((themes, index)) => { - let new_theme = index.and_then(|x| themes.get(x)).cloned(); - let should_render = new_theme != self.theme || self.themes != themes; - self.theme = new_theme; - self.themes = themes; - should_render - }, StatusBarMsg::SetTheme(theme_name) => { - let state = ctx.props().clone_state(); + let presentation = ctx.props().presentation.clone(); + let session = ctx.props().session.clone(); + let renderer = ctx.props().renderer.clone(); ApiFuture::spawn(async move { - state.presentation.set_theme_name(Some(&theme_name)).await?; - let view = state.session.get_view().into_apierror()?; - state.renderer.restyle_all(&view).await + presentation.set_theme_name(Some(&theme_name)).await?; + let view = session.get_view().into_apierror()?; + renderer.restyle_all(&view).await }); false @@ -151,7 +210,7 @@ impl Component for StatusBar { false }, StatusBarMsg::Noop => { - self.title = ctx.props().session().get_title(); + self.title = ctx.props().title.clone(); true }, StatusBarMsg::TitleInputEvent => { @@ -201,16 +260,21 @@ impl Component for StatusBar { .. } = ctx.props(); + let has_table = ctx.props().has_table; + let is_errored = ctx.props().is_errored; + let is_settings_open = ctx.props().is_settings_open; + let title = &ctx.props().title; + let mut is_updating_class_name = classes!(); - if session.get_title().is_some() { + if title.is_some() { is_updating_class_name.push("titled"); }; - if !presentation.is_settings_open() { + if !is_settings_open { is_updating_class_name.push(["settings-closed", "titled"]); }; - if !session.has_table() { + if !has_table { is_updating_class_name.push("updating"); } @@ -229,18 +293,18 @@ impl Component for StatusBar { .link() .callback(|_: InputEvent| StatusBarMsg::TitleInputEvent); - let is_menu = session.has_table() && ctx.props().on_settings.as_ref().is_none(); + let is_menu = has_table && ctx.props().on_settings.as_ref().is_none(); let is_title = is_menu - || presentation.get_is_workspace() - || session.get_title().is_some() - || session.is_errored() + || ctx.props().is_workspace + || title.is_some() + || is_errored || presentation.is_active(&self.input_ref.cast::()); - let is_settings = session.get_title().is_some() - || presentation.get_is_workspace() - || !session.has_table() - || session.is_errored() - || presentation.is_settings_open() + let is_settings = title.is_some() + || ctx.props().is_workspace + || !has_table + || is_errored + || is_settings_open || presentation.is_active(&self.input_ref.cast::()); if is_settings { @@ -253,7 +317,15 @@ impl Component for StatusBar { class={is_updating_class_name} {onpointerdown} > - + if is_title {