diff --git a/UPDATING.md b/UPDATING.md
index fcd69c18fd4..0680e44fb61 100644
--- a/UPDATING.md
+++ b/UPDATING.md
@@ -23,6 +23,10 @@ This file documents any backwards-incompatible changes in Superset and
assists people when migrating to a new version.
## Next
+- [34536](https://github.com/apache/superset/pull/34536): The `ENVIRONMENT_TAG_CONFIG` color values have changed to support only Ant Design semantic colors. Update your `superset_config.py`:
+ - Change `"error.base"` to just `"error"` after this PR
+ - Change any hex color values to one of: `"success"`, `"processing"`, `"error"`, `"warning"`, `"default"`
+ - Custom colors are no longer supported to maintain consistency with Ant Design components
- [34561](https://github.com/apache/superset/pull/34561) Added tiled screenshot functionality for Playwright-based reports to handle large dashboards more efficiently. When enabled (default: `SCREENSHOT_TILED_ENABLED = True`), dashboards with 20+ charts or height exceeding 5000px will be captured using multiple viewport-sized tiles and combined into a single image. This improves report generation performance and reliability for large dashboards.
Note: Pillow is now a required dependency (previously optional) to support image processing for tiled screenshots.
`thumbnails` optional dependency is now deprecated and will be removed in the next major release (7.0).
diff --git a/superset-frontend/packages/superset-ui-core/src/components/index.ts b/superset-frontend/packages/superset-ui-core/src/components/index.ts
index d1ead96dc3d..578405574c0 100644
--- a/superset-frontend/packages/superset-ui-core/src/components/index.ts
+++ b/superset-frontend/packages/superset-ui-core/src/components/index.ts
@@ -166,7 +166,6 @@ export * from './Table';
export * from './TableView';
export * from './Tag';
export * from './TelemetryPixel';
-export * from './ThemeSubMenu';
export * from './UnsavedChangesModal';
export * from './constants';
export * from './Result';
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
deleted file mode 100644
index c015f263567..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.test.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-/**
- * 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 { SyntheticEvent } from 'react';
-import {
- render,
- screen,
- userEvent,
- waitFor,
-} from 'spec/helpers/testing-library';
-import { Menu } from '@superset-ui/core/components/Menu';
-import downloadAsImage from 'src/utils/downloadAsImage';
-import DownloadAsImage from './DownloadAsImage';
-
-const mockAddDangerToast = jest.fn();
-
-jest.mock('src/utils/downloadAsImage', () => ({
- __esModule: true,
- default: jest.fn(() => (_e: SyntheticEvent) => console.log(_e)),
-}));
-
-jest.mock('src/components/MessageToasts/withToasts', () => ({
- useToasts: () => ({
- addDangerToast: mockAddDangerToast,
- }),
-}));
-
-const createProps = () => ({
- text: 'Download as Image',
- dashboardTitle: 'Test Dashboard',
- logEvent: jest.fn(),
-});
-
-const renderComponent = () => {
- render(
-
,
- {
- useRedux: true,
- },
- );
-};
-
-test('Should call download image on click', async () => {
- renderComponent();
- await waitFor(() => {
- expect(downloadAsImage).toHaveBeenCalledTimes(0);
- expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
- });
-
- userEvent.click(screen.getByRole('menuitem', { name: 'Download as Image' }));
-
- await waitFor(() => {
- expect(downloadAsImage).toHaveBeenCalledTimes(1);
- expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
- });
-});
-
-test('Component is rendered with role="menuitem"', async () => {
- renderComponent();
- const button = screen.getByRole('menuitem', { name: 'Download as Image' });
- expect(button).toBeInTheDocument();
-});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
deleted file mode 100644
index a992bbb5d4a..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsImage.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * 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 { SyntheticEvent } from 'react';
-import { logging, t } from '@superset-ui/core';
-import { Menu } from '@superset-ui/core/components/Menu';
-import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE } from 'src/logger/LogUtils';
-import downloadAsImage from 'src/utils/downloadAsImage';
-import { useToasts } from 'src/components/MessageToasts/withToasts';
-
-export default function DownloadAsImage({
- text,
- logEvent,
- dashboardTitle,
- ...props
-}: {
- text: string;
- dashboardTitle: string;
- logEvent?: Function;
-}) {
- const SCREENSHOT_NODE_SELECTOR = '.dashboard';
- const { addDangerToast } = useToasts();
- 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 (
- {
- onDownloadImage(e.domEvent);
- }}
- {...props}
- >
- {text}
-
- );
-}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
deleted file mode 100644
index cead7cf4473..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.test.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * 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 { SyntheticEvent } from 'react';
-import {
- render,
- screen,
- userEvent,
- waitFor,
-} from 'spec/helpers/testing-library';
-import { Menu } from '@superset-ui/core/components/Menu';
-import downloadAsPdf from 'src/utils/downloadAsPdf';
-import DownloadAsPdf from './DownloadAsPdf';
-
-const mockAddDangerToast = jest.fn();
-
-jest.mock('src/utils/downloadAsPdf', () => ({
- __esModule: true,
- default: jest.fn(() => (_e: SyntheticEvent) => console.log(_e)),
-}));
-
-jest.mock('src/components/MessageToasts/withToasts', () => ({
- useToasts: () => ({
- addDangerToast: mockAddDangerToast,
- }),
-}));
-
-const createProps = () => ({
- text: 'Export as PDF',
- dashboardTitle: 'Test Dashboard',
- logEvent: jest.fn(),
-});
-
-const renderComponent = () => {
- render(
- ,
- { useRedux: true },
- );
-};
-
-test('Should call download pdf on click', async () => {
- renderComponent();
- await waitFor(() => {
- expect(downloadAsPdf).toHaveBeenCalledTimes(0);
- expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
- });
-
- userEvent.click(screen.getByRole('menuitem', { name: 'Export as PDF' }));
-
- await waitFor(() => {
- expect(downloadAsPdf).toHaveBeenCalledTimes(1);
- expect(mockAddDangerToast).toHaveBeenCalledTimes(0);
- });
-});
-
-test('Component is rendered with role="menuitem"', async () => {
- renderComponent();
- const button = screen.getByRole('menuitem', { name: 'Export as PDF' });
- expect(button).toBeInTheDocument();
-});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
deleted file mode 100644
index 40fe21e0221..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadAsPdf.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-/**
- * 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 { SyntheticEvent } from 'react';
-import { logging, t } from '@superset-ui/core';
-import { Menu } from '@superset-ui/core/components/Menu';
-import downloadAsPdf from 'src/utils/downloadAsPdf';
-import { LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF } from 'src/logger/LogUtils';
-import { useToasts } from 'src/components/MessageToasts/withToasts';
-
-export default function DownloadAsPdf({
- text,
- logEvent,
- dashboardTitle,
- ...props
-}: {
- text: string;
- dashboardTitle: string;
- logEvent?: Function;
-}) {
- const SCREENSHOT_NODE_SELECTOR = '.dashboard';
- const { addDangerToast } = useToasts();
- 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 (
- {
- onDownloadPdf(e.domEvent);
- }}
- {...props}
- >
- {text}
-
- );
-}
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
deleted file mode 100644
index 5707c6734a8..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.test.tsx
+++ /dev/null
@@ -1,216 +0,0 @@
-/**
- * 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 {
- render,
- screen,
- userEvent,
- waitFor,
-} from 'spec/helpers/testing-library';
-import { Menu } from '@superset-ui/core/components/Menu';
-import fetchMock from 'fetch-mock';
-import { logging } from '@superset-ui/core';
-import { DownloadScreenshotFormat } from './types';
-import DownloadScreenshot from './DownloadScreenshot';
-
-const mockAddDangerToast = jest.fn();
-const mockLogEvent = jest.fn();
-const mockAddSuccessToast = jest.fn();
-const mockAddInfoToast = jest.fn();
-
-jest.spyOn(logging, 'error').mockImplementation(() => {});
-
-jest.mock('src/components/MessageToasts/withToasts', () => ({
- useToasts: () => ({
- addDangerToast: mockAddDangerToast,
- addSuccessToast: mockAddSuccessToast,
- addInfoToast: mockAddInfoToast,
- }),
-}));
-
-const defaultProps = () => ({
- text: 'Download',
- dashboardId: 123,
- format: DownloadScreenshotFormat.PDF,
- logEvent: mockLogEvent,
-});
-
-const renderComponent = () => {
- render(
- ,
- {
- useRedux: true,
- },
- );
-};
-
-describe('DownloadScreenshot component', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- jest.useRealTimers();
- fetchMock.restore();
- });
-
- afterAll(() => {
- jest.restoreAllMocks();
- });
-
- test('renders correctly with the given text', () => {
- renderComponent();
- expect(screen.getByText('Download')).toBeInTheDocument();
- });
-
- test('button renders with role="button"', async () => {
- renderComponent();
- const button = screen.getByRole('button', { name: 'Download' });
- expect(button).toBeInTheDocument();
- });
-
- test('displays error message when API call fails', async () => {
- const props = defaultProps();
-
- fetchMock.post(
- `glob:*/api/v1/dashboard/${props.dashboardId}/cache_dashboard_screenshot/`,
- {
- status: 400,
- body: {},
- },
- );
-
- renderComponent();
-
- userEvent.click(screen.getByRole('button', { name: 'Download' }));
-
- await waitFor(() => {
- expect(mockAddDangerToast).toHaveBeenCalledWith(
- 'The screenshot could not be downloaded. Please, try again later.',
- );
- });
- });
-
- test('displays success message when API call succeeds', async () => {
- const props = defaultProps();
- fetchMock.post(
- `glob:*/api/v1/dashboard/${props.dashboardId}/cache_dashboard_screenshot/`,
- {
- status: 200,
- body: {
- image_url: 'mocked_image_url',
- cache_key: 'mocked_cache_key',
- },
- },
- );
-
- fetchMock.get(
- `glob:*/api/v1/dashboard/${props.dashboardId}/screenshot/mocked_cache_key/?download_format=pdf`,
- {
- status: 200,
- body: {},
- },
- );
-
- renderComponent();
-
- userEvent.click(screen.getByRole('button', { name: 'Download' }));
-
- await waitFor(() => {
- expect(mockAddInfoToast).toHaveBeenCalledWith(
- 'The screenshot is being generated. Please, do not leave the page.',
- {
- noDuplicate: true,
- },
- );
- });
- });
-
- test('throws error when no image cache key is provided', async () => {
- const props = defaultProps();
- fetchMock.post(
- `glob:*/api/v1/dashboard/${props.dashboardId}/cache_dashboard_screenshot/`,
- {
- status: 200,
- body: {
- cache_key: '',
- },
- },
- );
-
- renderComponent();
-
- // Simulate the user clicking the download button
- userEvent.click(screen.getByRole('button', { name: 'Download' }));
-
- await waitFor(() => {
- expect(mockAddDangerToast).toHaveBeenCalledWith(
- 'The screenshot could not be downloaded. Please, try again later.',
- );
- });
- });
-
- test('displays success message when image retrieval succeeds', async () => {
- const props = defaultProps();
- fetchMock.post(
- `glob:*/api/v1/dashboard/${props.dashboardId}/cache_dashboard_screenshot/`,
- {
- status: 200,
- body: {
- image_url: 'mocked_image_url',
- cache_key: 'mocked_cache_key',
- },
- },
- );
-
- fetchMock.get(
- `glob:*/api/v1/dashboard/${props.dashboardId}/screenshot/mocked_cache_key/?download_format=pdf`,
- {
- status: 200,
- headers: {
- 'Content-Type': 'application/pdf',
- },
- body: new Blob([], { type: 'application/pdf' }),
- },
- );
-
- global.URL.createObjectURL = jest.fn(() => 'mockedObjectURL');
- global.URL.revokeObjectURL = jest.fn();
-
- // Render the component
- renderComponent();
-
- // Simulate the user clicking the download button
- userEvent.click(screen.getByRole('button', { name: 'Download' }));
-
- await waitFor(() => {
- expect(
- fetchMock.calls(
- `glob:*/api/v1/dashboard/${props.dashboardId}/screenshot/mocked_cache_key/?download_format=pdf`,
- ).length,
- ).toBe(1);
- });
-
- // Wait for the successful image retrieval message
- await waitFor(() => {
- expect(mockAddSuccessToast).toHaveBeenCalledWith(
- 'The screenshot has been downloaded.',
- );
- });
- });
-});
diff --git a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx b/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
deleted file mode 100644
index 8a47e042223..00000000000
--- a/superset-frontend/src/dashboard/components/menu/DownloadMenuItems/DownloadScreenshot.tsx
+++ /dev/null
@@ -1,203 +0,0 @@
-/**
- * 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 {
- logging,
- t,
- SupersetClient,
- SupersetApiError,
-} from '@superset-ui/core';
-import { Menu } from '@superset-ui/core/components/Menu';
-import {
- LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE,
- LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
-} from 'src/logger/LogUtils';
-import { RootState } from 'src/dashboard/types';
-import { useSelector } from 'react-redux';
-import { useToasts } from 'src/components/MessageToasts/withToasts';
-import { last } from 'lodash';
-import { getDashboardUrlParams } from 'src/utils/urlUtils';
-import { useCallback, useEffect, useRef } from 'react';
-import { DownloadScreenshotFormat } from './types';
-
-const RETRY_INTERVAL = 3000;
-const MAX_RETRIES = 30;
-
-export default function DownloadScreenshot({
- text,
- logEvent,
- dashboardId,
- format,
- ...rest
-}: {
- text: string;
- dashboardId: number;
- logEvent?: Function;
- format: string;
-}) {
- const activeTabs = useSelector(
- (state: RootState) => state.dashboardState.activeTabs || undefined,
- );
- const anchor = useSelector(
- (state: RootState) =>
- last(state.dashboardState.directPathToChild) || undefined,
- );
- const dataMask = useSelector(
- (state: RootState) => state.dataMask || undefined,
- );
- const { addDangerToast, addSuccessToast, addInfoToast } = useToasts();
- const currentIntervalIds = useRef([]);
-
- const printLoadingToast = () =>
- addInfoToast(
- t('The screenshot is being generated. Please, do not leave the page.'),
- {
- noDuplicate: true,
- },
- );
-
- const printFailureToast = useCallback(
- () =>
- addDangerToast(
- t('The screenshot could not be downloaded. Please, try again later.'),
- ),
- [addDangerToast],
- );
-
- const printSuccessToast = useCallback(
- () => addSuccessToast(t('The screenshot has been downloaded.')),
- [addSuccessToast],
- );
-
- const stopIntervals = useCallback(
- (message?: 'success' | 'failure') => {
- currentIntervalIds.current.forEach(clearInterval);
-
- if (message === 'failure') {
- printFailureToast();
- }
- if (message === 'success') {
- printSuccessToast();
- }
- },
- [printFailureToast, printSuccessToast],
- );
-
- const onDownloadScreenshot = () => {
- let retries = 0;
-
- const toastIntervalId = setInterval(
- () => printLoadingToast(),
- RETRY_INTERVAL,
- );
-
- currentIntervalIds.current = [
- ...(currentIntervalIds.current || []),
- toastIntervalId,
- ];
-
- printLoadingToast();
-
- // this function checks if the image is ready
- const checkImageReady = (cacheKey: string) =>
- SupersetClient.get({
- endpoint: `/api/v1/dashboard/${dashboardId}/screenshot/${cacheKey}/?download_format=${format}`,
- headers: { Accept: 'application/pdf, image/png' },
- parseMethod: 'raw',
- })
- .then((response: Response) => response.blob())
- .then(blob => {
- const url = window.URL.createObjectURL(blob);
- const a = document.createElement('a');
- a.href = url;
- a.download = `screenshot.${format}`;
- document.body.appendChild(a);
- a.click();
- document.body.removeChild(a);
- window.URL.revokeObjectURL(url);
- stopIntervals('success');
- })
- .catch(err => {
- if ((err as SupersetApiError).status === 404) {
- throw new Error('Image not ready');
- }
- });
-
- const fetchImageWithRetry = (cacheKey: string) => {
- if (retries >= MAX_RETRIES) {
- stopIntervals('failure');
- logging.error('Max retries reached');
- return;
- }
- checkImageReady(cacheKey).catch(() => {
- retries += 1;
- });
- };
-
- SupersetClient.post({
- endpoint: `/api/v1/dashboard/${dashboardId}/cache_dashboard_screenshot/`,
- jsonPayload: {
- anchor,
- activeTabs,
- dataMask,
- urlParams: getDashboardUrlParams(['edit']),
- },
- })
- .then(({ json }) => {
- const cacheKey = json?.cache_key;
- if (!cacheKey) {
- throw new Error('No image URL in response');
- }
- const retryIntervalId = setInterval(() => {
- fetchImageWithRetry(cacheKey);
- }, RETRY_INTERVAL);
- currentIntervalIds.current.push(retryIntervalId);
- fetchImageWithRetry(cacheKey);
- })
- .catch(error => {
- logging.error(error);
- stopIntervals('failure');
- })
- .finally(() => {
- logEvent?.(
- format === DownloadScreenshotFormat.PNG
- ? LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_IMAGE
- : LOG_ACTIONS_DASHBOARD_DOWNLOAD_AS_PDF,
- );
- });
- };
-
- useEffect(
- () => () => {
- if (currentIntervalIds.current.length > 0) {
- stopIntervals();
- }
- currentIntervalIds.current = [];
- },
- [stopIntervals],
- );
-
- return (
-
-