You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2024/05/18 00:07:15 UTC
(superset) 01/01: add listener to repaint on visibility change for canvas
This is an automated email from the ASF dual-hosted git repository.
elizabeth pushed a commit to branch elizabeth/repaint-convas
in repository https://gitbox.apache.org/repos/asf/superset.git
commit 44715b7597e5b02fdff84a59211569fa23b7a45d
Author: Elizabeth Thompson <es...@gmail.com>
AuthorDate: Fri May 17 17:07:00 2024 -0700
add listener to repaint on visibility change for canvas
---
superset-frontend/package-lock.json | 65 ++++++++++
superset-frontend/package.json | 1 +
.../src/components/Chart/ChartRenderer.jsx | 1 +
.../src/dashboard/components/Dashboard.jsx | 23 ++++
.../src/dashboard/components/Dashboard.test.jsx | 136 +++++++++++++++++++++
5 files changed, 226 insertions(+)
diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json
index deaa3748f5..7ac405a55a 100644
--- a/superset-frontend/package-lock.json
+++ b/superset-frontend/package-lock.json
@@ -250,6 +250,7 @@
"ignore-styles": "^5.0.1",
"imports-loader": "^3.1.1",
"jest": "^26.6.3",
+ "jest-canvas-mock": "^2.5.2",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
"jest-websocket-mock": "^2.2.0",
@@ -31171,6 +31172,12 @@
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
+ "node_modules/cssfontparser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
+ "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
+ "dev": true
+ },
"node_modules/cssnano": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz",
@@ -41175,6 +41182,16 @@
"node": ">= 10.14.2"
}
},
+ "node_modules/jest-canvas-mock": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
+ "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
+ "dev": true,
+ "dependencies": {
+ "cssfontparser": "^1.2.1",
+ "moo-color": "^1.0.2"
+ }
+ },
"node_modules/jest-changed-files": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
@@ -52539,6 +52556,21 @@
"resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
"integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
},
+ "node_modules/moo-color": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
+ "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "^1.1.4"
+ }
+ },
+ "node_modules/moo-color/node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ },
"node_modules/mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
@@ -96422,6 +96454,12 @@
"resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz",
"integrity": "sha1-xtJnJjKi5cg+AT5oZKQs6N79IK4="
},
+ "cssfontparser": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/cssfontparser/-/cssfontparser-1.2.1.tgz",
+ "integrity": "sha512-6tun4LoZnj7VN6YeegOVb67KBX/7JJsqvj+pv3ZA7F878/eN33AbGa5b/S/wXxS/tcp8nc40xRUrsPlxIyNUPg==",
+ "dev": true
+ },
"cssnano": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.0.3.tgz",
@@ -104272,6 +104310,16 @@
}
}
},
+ "jest-canvas-mock": {
+ "version": "2.5.2",
+ "resolved": "https://registry.npmjs.org/jest-canvas-mock/-/jest-canvas-mock-2.5.2.tgz",
+ "integrity": "sha512-vgnpPupjOL6+L5oJXzxTxFrlGEIbHdZqFU+LFNdtLxZ3lRDCl17FlTMM7IatoRQkrcyOTMlDinjUguqmQ6bR2A==",
+ "dev": true,
+ "requires": {
+ "cssfontparser": "^1.2.1",
+ "moo-color": "^1.0.2"
+ }
+ },
"jest-changed-files": {
"version": "26.6.2",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz",
@@ -112794,6 +112842,23 @@
"resolved": "https://registry.npmjs.org/moo/-/moo-0.4.3.tgz",
"integrity": "sha512-gFD2xGCl8YFgGHsqJ9NKRVdwlioeW3mI1iqfLNYQOv0+6JRwG58Zk9DIGQgyIaffSYaO1xsKnMaYzzNr1KyIAw=="
},
+ "moo-color": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/moo-color/-/moo-color-1.0.3.tgz",
+ "integrity": "sha512-i/+ZKXMDf6aqYtBhuOcej71YSlbjT3wCO/4H1j8rPvxDJEifdwgg5MaFyu6iYAT8GBZJg2z0dkgK4YMzvURALQ==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.1.4"
+ },
+ "dependencies": {
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
+ },
"mousetrap": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz",
diff --git a/superset-frontend/package.json b/superset-frontend/package.json
index c526a9b067..70ca63de7e 100644
--- a/superset-frontend/package.json
+++ b/superset-frontend/package.json
@@ -316,6 +316,7 @@
"ignore-styles": "^5.0.1",
"imports-loader": "^3.1.1",
"jest": "^26.6.3",
+ "jest-canvas-mock": "^2.5.2",
"jest-environment-enzyme": "^7.1.2",
"jest-enzyme": "^7.1.2",
"jest-websocket-mock": "^2.2.0",
diff --git a/superset-frontend/src/components/Chart/ChartRenderer.jsx b/superset-frontend/src/components/Chart/ChartRenderer.jsx
index 5f29840dde..a7d908adfe 100644
--- a/superset-frontend/src/components/Chart/ChartRenderer.jsx
+++ b/superset-frontend/src/components/Chart/ChartRenderer.jsx
@@ -95,6 +95,7 @@ class ChartRenderer extends React.Component {
this.hasQueryResponseChange = false;
this.contextMenuRef = React.createRef();
+ this.chartRef = React.createRef();
this.handleAddFilter = this.handleAddFilter.bind(this);
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
diff --git a/superset-frontend/src/dashboard/components/Dashboard.jsx b/superset-frontend/src/dashboard/components/Dashboard.jsx
index 038ab148b9..9e48075132 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.jsx
@@ -90,6 +90,8 @@ class Dashboard extends React.PureComponent {
this.appliedFilters = props.activeFilters ?? {};
this.appliedOwnDataCharts = props.ownDataCharts ?? {};
this.onVisibilityChange = this.onVisibilityChange.bind(this);
+ this.handleVisibilityChange = this.handleVisibilityChange.bind(this);
+ this.repaintCanvas = this.repaintCanvas.bind(this);
}
componentDidMount() {
@@ -192,6 +194,24 @@ class Dashboard extends React.PureComponent {
this.props.actions.clearDataMaskState();
}
+ repaintCanvas(canvas, ctx, imageBitmap) {
+ // Clear the canvas
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+ // Draw the copied content
+ ctx.drawImage(imageBitmap, 0, 0);
+ }
+
+ handleVisibilityChange() {
+ this.canvases.forEach(canvas => {
+ const ctx = canvas.getContext('2d');
+ createImageBitmap(canvas).then(imageBitmap => {
+ // Call the repaintCanvas function with canvas, ctx, and imageBitmap
+ this.repaintCanvas(canvas, ctx, imageBitmap);
+ });
+ });
+ }
+
onVisibilityChange() {
if (document.visibilityState === 'hidden') {
// from visible to hidden
@@ -199,6 +219,7 @@ class Dashboard extends React.PureComponent {
start_offset: Logger.getTimestamp(),
ts: new Date().getTime(),
};
+ this.canvases = document.querySelectorAll('canvas');
} else if (document.visibilityState === 'visible') {
// from hidden to visible
const logStart = this.visibilityEventData.start_offset;
@@ -206,6 +227,8 @@ class Dashboard extends React.PureComponent {
...this.visibilityEventData,
duration: Logger.getTimestamp() - logStart,
});
+ // for chrome to ensure that the canvas doesn't disappear
+ this.handleVisibilityChange();
}
}
diff --git a/superset-frontend/src/dashboard/components/Dashboard.test.jsx b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
index d75bda27dc..7e1d06e0b4 100644
--- a/superset-frontend/src/dashboard/components/Dashboard.test.jsx
+++ b/superset-frontend/src/dashboard/components/Dashboard.test.jsx
@@ -19,11 +19,14 @@
import React from 'react';
import { shallow } from 'enzyme';
import sinon from 'sinon';
+import 'jest-canvas-mock';
import Dashboard from 'src/dashboard/components/Dashboard';
import { CHART_TYPE } from 'src/dashboard/util/componentTypes';
import newComponentFactory from 'src/dashboard/util/newComponentFactory';
+import { Logger, LOG_ACTIONS_HIDE_BROWSER_TAB } from '../../logger/LogUtils';
+
// mock data
import chartQueries from 'spec/fixtures/mockChartQueries';
import datasources from 'spec/fixtures/mockDatasource';
@@ -245,5 +248,138 @@ describe('Dashboard', () => {
expect(refreshSpy.callCount).toBe(1);
expect(refreshSpy.getCall(0).args[0]).toEqual([]);
});
+
+ // The canvas is cleared using the clearRect method.
+ it('should clear the canvas using clearRect method', () => {
+ // Arrange
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ const imageBitmap = new ImageBitmap(100, 100);
+
+ // Act
+ wrapper.instance().repaintCanvas(canvas, ctx, imageBitmap);
+
+ // Assert
+ expect(ctx.clearRect).toHaveBeenCalledWith(
+ 0,
+ 0,
+ canvas.width,
+ canvas.height,
+ );
+ });
+
+ // The canvas width and height are 0.
+ it('should recreate the canvas with the same dimensions', () => {
+ // Arrange
+ const canvas = document.createElement('canvas');
+ const ctx = canvas.getContext('2d');
+ const imageBitmap = new ImageBitmap(100, 100);
+
+ // Act
+ wrapper.instance().repaintCanvas(canvas, ctx, imageBitmap);
+
+ // Assert
+ const { width, height } = ctx.canvas;
+ expect(canvas.width).toBe(width);
+ expect(canvas.height).toBe(height);
+ });
+
+ // When the document visibility state changes to 'hidden', the method sets the 'visibilityEventData' object with a 'start_offset' and 'ts' properties, and queries all canvas elements on the page and stores them in the 'canvases' property.
+ it('should set visibilityEventData and canvases when document visibility state changes to "hidden"', () => {
+ // Initialize the class object with props
+ const props = {
+ activeFilters: {},
+ ownDataCharts: {},
+ actions: {
+ logEvent: jest.fn(),
+ },
+ layout: {},
+ dashboardInfo: {},
+ dashboardState: {
+ editMode: false,
+ isPublished: false,
+ hasUnsavedChanges: false,
+ },
+ activeFilters: {},
+ ownDataCharts: {},
+ chartConfiguration: {},
+ };
+
+ const DATE_TO_USE = new Date('2020');
+ const _Date = Date;
+ global.Date = jest.fn(() => DATE_TO_USE);
+ global.Date.UTC = _Date.UTC;
+ global.Date.parse = _Date.parse;
+ global.Date.now = _Date.now;
+
+ // Your test code here
+
+ const dashboard = new Dashboard(props);
+
+ // Mock the return value of document.visibilityState
+ jest.spyOn(document, 'visibilityState', 'get').mockReturnValue('hidden');
+ // mock Logger.getTimestamp() to return a fixed value
+ jest.spyOn(Logger, 'getTimestamp').mockReturnValue(1234567890);
+
+ // Invoke the method
+ dashboard.onVisibilityChange();
+
+ // Assert that visibilityEventData is set correctly
+ expect(dashboard.visibilityEventData).toEqual({
+ start_offset: 1234567890,
+ ts: DATE_TO_USE.getTime(),
+ });
+
+ // Assert that canvases are queried correctly
+ expect(dashboard.canvases).toEqual(expect.any(NodeList));
+
+ // Restore the original implementation of document.visibilityState
+ jest.restoreAllMocks();
+ // After your test
+ global.Date = _Date;
+ });
+
+ // When the document visibility state changes to 'visible', the method logs an event and calls the 'handleVisibilityChange' method.
+ it('should log an event and call handleVisibilityChange when document visibility state changes to "visible"', () => {
+ // Initialize the class object
+ const dashboard = new Dashboard({ activeFilters: {} });
+
+ // Mock the props and actions
+ dashboard.props = {
+ actions: {
+ logEvent: jest.fn(),
+ },
+ };
+
+ // Mock the visibilityEventData
+ dashboard.visibilityEventData = {
+ start_offset: 123,
+ ts: 456,
+ };
+
+ // Mock the handleVisibilityChange method
+ dashboard.handleVisibilityChange = jest.fn();
+
+ // Mock the document.visibilityState property
+ jest.spyOn(document, 'visibilityState', 'get').mockReturnValue('visible');
+
+ // Invoke the method
+ dashboard.onVisibilityChange();
+
+ // Assert that logEvent is called with the correct arguments
+ expect(dashboard.props.actions.logEvent).toHaveBeenCalledWith(
+ LOG_ACTIONS_HIDE_BROWSER_TAB,
+ {
+ ...dashboard.visibilityEventData,
+ duration: expect.any(Number),
+ },
+ );
+
+ // Assert that handleVisibilityChange is called
+ expect(dashboard.handleVisibilityChange).toHaveBeenCalled();
+
+ // Restore the original implementation of document.visibilityState
+ jest.restoreAllMocks();
+ });
});
});