Skip to content

Commit

Permalink
feat(Export as PDF - rasterized): Adding rasterized pdf functionality…
Browse files Browse the repository at this point in the history
… to dashboard (apache#25696)
  • Loading branch information
fisjac authored Oct 31, 2023
1 parent 5659c87 commit 74dbada
Show file tree
Hide file tree
Showing 14 changed files with 633 additions and 36 deletions.
270 changes: 270 additions & 0 deletions superset-frontend/package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions superset-frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@
"d3-color": "^3.1.0",
"d3-scale": "^2.1.2",
"dom-to-image-more": "^2.10.1",
"dom-to-pdf": "^0.3.2",
"emotion-rgba": "0.0.9",
"fast-glob": "^3.2.7",
"fontsource-fira-code": "^4.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,10 +109,10 @@ test('should render', () => {
expect(container).toBeInTheDocument();
});

test('should render the dropdown button', () => {
test('should render the Download dropdown button when not in edit mode', () => {
const mockedProps = createProps();
setup(mockedProps);
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Download' })).toBeInTheDocument();
});

test('should render the menu items', async () => {
Expand All @@ -121,16 +121,17 @@ test('should render the menu items', async () => {
expect(screen.getAllByRole('menuitem')).toHaveLength(4);
expect(screen.getByText('Refresh dashboard')).toBeInTheDocument();
expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
expect(screen.getByText('Download as image')).toBeInTheDocument();
expect(screen.getByText('Enter fullscreen')).toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
});

test('should render the menu items in edit mode', async () => {
setup(editModeOnProps);
expect(screen.getAllByRole('menuitem')).toHaveLength(4);
expect(screen.getAllByRole('menuitem')).toHaveLength(5);
expect(screen.getByText('Set auto-refresh interval')).toBeInTheDocument();
expect(screen.getByText('Edit properties')).toBeInTheDocument();
expect(screen.getByText('Edit CSS')).toBeInTheDocument();
expect(screen.getByText('Download')).toBeInTheDocument();
});

describe('with native filters feature flag disabled', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,17 @@ import {
import { Menu } from 'src/components/Menu';
import { URL_PARAMS } from 'src/constants';
import ShareMenuItems from 'src/dashboard/components/menu/ShareMenuItems';
import DownloadMenuItems from 'src/dashboard/components/menu/DownloadMenuItems';
import CssEditor from 'src/dashboard/components/CssEditor';
import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal';
import SaveModal from 'src/dashboard/components/SaveModal';
import HeaderReportDropdown from 'src/features/reports/ReportModal/HeaderReportDropdown';
import injectCustomCss from 'src/dashboard/util/injectCustomCss';
import { SAVE_TYPE_NEWDASHBOARD } from 'src/dashboard/util/constants';
import FilterScopeModal from 'src/dashboard/components/filterscope/FilterScopeModal';
import downloadAsImage from 'src/utils/downloadAsImage';
import getDashboardUrl from 'src/dashboard/util/getDashboardUrl';
import { getActiveFilters } from 'src/dashboard/util/activeDashboardFilters';
import { getUrlParam } from 'src/utils/urlUtils';
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';

const propTypes = {
addSuccessToast: PropTypes.func.isRequired,
Expand Down Expand Up @@ -90,14 +89,12 @@ const MENU_KEYS = {
SET_FILTER_MAPPING: 'set-filter-mapping',
EDIT_PROPERTIES: 'edit-properties',
EDIT_CSS: 'edit-css',
DOWNLOAD_AS_IMAGE: 'download-as-image',
DOWNLOAD_DASHBOARD: 'download-dashboard',
TOGGLE_FULLSCREEN: 'toggle-fullscreen',
MANAGE_EMBEDDED: 'manage-embedded',
MANAGE_EMAIL_REPORT: 'manage-email-report',
};

const SCREENSHOT_NODE_SELECTOR = '.dashboard';

class HeaderActionsDropdown extends React.PureComponent {
static discardChanges() {
window.location.reload();
Expand Down Expand Up @@ -158,7 +155,7 @@ class HeaderActionsDropdown extends React.PureComponent {
this.props.startPeriodicRender(refreshInterval * 1000);
}

handleMenuClick({ key, domEvent }) {
handleMenuClick({ key }) {
switch (key) {
case MENU_KEYS.REFRESH_DASHBOARD:
this.props.forceRefreshAllCharts();
Expand All @@ -167,23 +164,6 @@ class HeaderActionsDropdown extends React.PureComponent {
case MENU_KEYS.EDIT_PROPERTIES:
this.props.showPropertiesModal();
break;
case MENU_KEYS.DOWNLOAD_AS_IMAGE: {
// menu closes with a delay, we need to hide it manually,
// so that we don't capture it on the screenshot
const menu = document.querySelector(
'.ant-dropdown:not(.ant-dropdown-hidden)',
);
menu.style.visibility = 'hidden';
downloadAsImage(
SCREENSHOT_NODE_SELECTOR,
this.props.dashboardTitle,
true,
)(domEvent).then(() => {
menu.style.visibility = 'visible';
});
this.props.logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE);
break;
}
case MENU_KEYS.TOGGLE_FULLSCREEN: {
const url = getDashboardUrl({
pathname: window.location.pathname,
Expand Down Expand Up @@ -311,14 +291,19 @@ class HeaderActionsDropdown extends React.PureComponent {
/>
</Menu.Item>
)}
{!editMode && (
<Menu.Item
key={MENU_KEYS.DOWNLOAD_AS_IMAGE}
onClick={this.handleMenuClick}
>
{t('Download as image')}
</Menu.Item>
)}
<Menu.SubMenu
key={MENU_KEYS.DOWNLOAD_DASHBOARD}
disabled={isLoading}
title={t('Download')}
logEvent={this.props.logEvent}
>
<DownloadMenuItems
pdfMenuItemTitle={t('Export to PDF')}
imageMenuItemTitle={t('Download as Image')}
dashboardTitle={dashboardTitle}
addDangerToast={addDangerToast}
/>
</Menu.SubMenu>
{userCanShare && (
<Menu.SubMenu
key={MENU_KEYS.SHARE_DASHBOARD}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { SyntheticEvent } from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Menu } from 'src/components/Menu';
import downloadAsImage from 'src/utils/downloadAsImage';
import DownloadAsImage from './DownloadAsImage';

jest.mock('src/utils/downloadAsImage', () => ({
__esModule: true,
default: jest.fn(() => (_e: SyntheticEvent) => {}),
}));

const createProps = () => ({
addDangerToast: jest.fn(),
text: 'Download as Image',
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
});

const renderComponent = () => {
render(
<Menu>
<DownloadAsImage {...createProps()} />
</Menu>,
);
};

test('Should call download image on click', async () => {
const props = createProps();
renderComponent();
await waitFor(() => {
expect(downloadAsImage).toBeCalledTimes(0);
expect(props.addDangerToast).toBeCalledTimes(0);
});

userEvent.click(screen.getByRole('button', { name: 'Download as Image' }));

await waitFor(() => {
expect(downloadAsImage).toBeCalledTimes(1);
expect(props.addDangerToast).toBeCalledTimes(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { SyntheticEvent } from 'react';
import { logging, t } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
import downloadAsImage from 'src/utils/downloadAsImage';

export default function DownloadAsImage({
text,
logEvent,
dashboardTitle,
addDangerToast,
...rest
}: {
text: string;
addDangerToast: Function;
dashboardTitle: string;
logEvent?: Function;
}) {
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
const onDownloadImage = async (e: SyntheticEvent) => {
try {
downloadAsImage(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
} catch (error) {
logging.error(error);
addDangerToast(t('Sorry, something went wrong. Try again later.'));
}
logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE);
};

return (
<Menu.Item key="download-image" {...rest}>
<div onClick={onDownloadImage} role="button" tabIndex={0}>
{text}
</div>
</Menu.Item>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { SyntheticEvent } from 'react';
import { render, screen, waitFor } from 'spec/helpers/testing-library';
import userEvent from '@testing-library/user-event';
import { Menu } from 'src/components/Menu';
import downloadAsPdf from 'src/utils/downloadAsPdf';
import DownloadAsPdf from './DownloadAsPdf';

jest.mock('src/utils/downloadAsPdf', () => ({
__esModule: true,
default: jest.fn(() => (_e: SyntheticEvent) => {}),
}));

const createProps = () => ({
addDangerToast: jest.fn(),
text: 'Export as PDF',
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
});

const renderComponent = () => {
render(
<Menu>
<DownloadAsPdf {...createProps()} />
</Menu>,
);
};

test('Should call download pdf on click', async () => {
const props = createProps();
renderComponent();
await waitFor(() => {
expect(downloadAsPdf).toBeCalledTimes(0);
expect(props.addDangerToast).toBeCalledTimes(0);
});

userEvent.click(screen.getByRole('button', { name: 'Export as PDF' }));

await waitFor(() => {
expect(downloadAsPdf).toBeCalledTimes(1);
expect(props.addDangerToast).toBeCalledTimes(0);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React, { SyntheticEvent } from 'react';
import { logging, t } from '@superset-ui/core';
import { Menu } from 'src/components/Menu';
import downloadAsPdf from 'src/utils/downloadAsPdf';
import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF } from 'src/logger/LogUtils';

export default function DownloadAsPdf({
text,
logEvent,
dashboardTitle,
addDangerToast,
...rest
}: {
text: string;
addDangerToast: Function;
dashboardTitle: string;
logEvent?: Function;
}) {
const SCREENSHOT_NODE_SELECTOR = '.dashboard';
const onDownloadPdf = async (e: SyntheticEvent) => {
try {
downloadAsPdf(SCREENSHOT_NODE_SELECTOR, dashboardTitle, true)(e);
} catch (error) {
logging.error(error);
addDangerToast(t('Sorry, something went wrong. Try again later.'));
}
logEvent?.(LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF);
};

return (
<Menu.Item key="download-pdf" {...rest}>
<div onClick={onDownloadPdf} role="button" tabIndex={0}>
{text}
</div>
</Menu.Item>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import { render, screen } from 'spec/helpers/testing-library';
import DownloadMenuItems from '.';

const createProps = () => ({
addDangerToast: jest.fn(),
pdfMenuItemTitle: 'Export to PDF',
imageMenuItemTitle: 'Download as Image',
dashboardTitle: 'Test Dashboard',
logEvent: jest.fn(),
});

const renderComponent = () => {
render(<DownloadMenuItems {...createProps()} />);
};

test('Should render menu items', () => {
renderComponent();
expect(
screen.getByRole('menuitem', { name: 'Export to PDF' }),
).toBeInTheDocument();
expect(
screen.getByRole('menuitem', { name: 'Download as Image' }),
).toBeInTheDocument();
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import { Menu } from 'src/components/Menu';
import DownloadAsImage from './DownloadAsImage';
import DownloadAsPdf from './DownloadAsPdf';

export interface DownloadMenuItemProps {
pdfMenuItemTitle: string;
imageMenuItemTitle: string;
addDangerToast: Function;
dashboardTitle: string;
logEvent?: Function;
}

const DownloadMenuItems = (props: DownloadMenuItemProps) => {
const {
pdfMenuItemTitle,
imageMenuItemTitle,
addDangerToast,
dashboardTitle,
logEvent,
...rest
} = props;

return (
<Menu selectable={false}>
<DownloadAsPdf
text={pdfMenuItemTitle}
addDangerToast={addDangerToast}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
{...rest}
/>
<DownloadAsImage
text={imageMenuItemTitle}
addDangerToast={addDangerToast}
dashboardTitle={dashboardTitle}
logEvent={logEvent}
{...rest}
/>
</Menu>
);
};

export default DownloadMenuItems;
Loading

0 comments on commit 74dbada

Please sign in to comment.