From 1d4b3aea4e0e85533d2ad3d7c6c11dc7e27e5371 Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Fri, 11 Jun 2021 17:25:00 -0700 Subject: [PATCH] feat: show spinner on exports (#15107) * feat: show spinner on exports * Set cookie only if token is passed * Use iframe * Small fixes * Fix lint * Remove stale test * Add explicit type --- superset-frontend/src/utils/export.ts | 48 +++++++++++++++++++ .../src/views/CRUD/chart/ChartCard.tsx | 4 +- .../src/views/CRUD/chart/ChartList.tsx | 14 +++++- .../views/CRUD/dashboard/DashboardCard.tsx | 8 ++-- .../views/CRUD/dashboard/DashboardList.tsx | 14 +++++- .../views/CRUD/data/database/DatabaseList.tsx | 16 +++++-- .../views/CRUD/data/dataset/DatasetList.tsx | 17 ++++--- .../data/savedquery/SavedQueryList.test.jsx | 9 ---- .../CRUD/data/savedquery/SavedQueryList.tsx | 15 +++++- superset-frontend/src/views/CRUD/utils.tsx | 28 +---------- .../src/views/CRUD/welcome/ChartTable.tsx | 12 +++++ .../src/views/CRUD/welcome/DashboardTable.tsx | 12 +++++ superset/charts/api.py | 6 ++- superset/dashboards/api.py | 6 ++- superset/databases/api.py | 6 ++- superset/datasets/api.py | 6 ++- superset/queries/saved_queries/api.py | 6 ++- 17 files changed, 167 insertions(+), 60 deletions(-) create mode 100644 superset-frontend/src/utils/export.ts diff --git a/superset-frontend/src/utils/export.ts b/superset-frontend/src/utils/export.ts new file mode 100644 index 000000000000..4f3ea75d7675 --- /dev/null +++ b/superset-frontend/src/utils/export.ts @@ -0,0 +1,48 @@ +/** + * 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 parseCookie from 'src/utils/parseCookie'; +import rison from 'rison'; +import shortid from 'shortid'; + +export default function handleResourceExport( + resource: string, + ids: number[], + done: () => void, + interval = 200, +): void { + const token = shortid.generate(); + const url = `/api/v1/${resource}/export/?q=${rison.encode( + ids, + )}&token=${token}`; + + // create new iframe for export + const iframe = document.createElement('iframe'); + iframe.style.display = 'none'; + iframe.src = url; + document.body.appendChild(iframe); + + const timer = window.setInterval(() => { + const cookie: { [cookieId: string]: string } = parseCookie(); + if (cookie[token] === 'done') { + window.clearInterval(timer); + document.body.removeChild(iframe); + done(); + } + }, interval); +} diff --git a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx index cc4f066f95d1..4a04638794a6 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartCard.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartCard.tsx @@ -29,7 +29,7 @@ import Label from 'src/components/Label'; import { Dropdown, Menu } from 'src/common/components'; import FaveStar from 'src/components/FaveStar'; import FacePile from 'src/components/FacePile'; -import { handleChartDelete, handleBulkChartExport, CardStyles } from '../utils'; +import { handleChartDelete, CardStyles } from '../utils'; interface ChartCardProps { chart: Chart; @@ -45,6 +45,7 @@ interface ChartCardProps { chartFilter?: string; userId?: number; showThumbnails?: boolean; + handleBulkChartExport: (chartsToExport: Chart[]) => void; } export default function ChartCard({ @@ -61,6 +62,7 @@ export default function ChartCard({ favoriteStatus, chartFilter, userId, + handleBulkChartExport, }: ChartCardProps) { const canEdit = hasPerm('can_write'); const canDelete = hasPerm('can_write'); diff --git a/superset-frontend/src/views/CRUD/chart/ChartList.tsx b/superset-frontend/src/views/CRUD/chart/ChartList.tsx index 4f2211ebe728..7af94b706073 100644 --- a/superset-frontend/src/views/CRUD/chart/ChartList.tsx +++ b/superset-frontend/src/views/CRUD/chart/ChartList.tsx @@ -29,7 +29,6 @@ import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags'; import { createErrorHandler, createFetchRelated, - handleBulkChartExport, handleChartDelete, } from 'src/views/CRUD/utils'; import { @@ -37,6 +36,7 @@ import { useFavoriteStatus, useListViewResource, } from 'src/views/CRUD/hooks'; +import handleResourceExport from 'src/utils/export'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; import FaveStar from 'src/components/FaveStar'; @@ -47,6 +47,7 @@ import ListView, { ListViewProps, SelectOption, } from 'src/components/ListView'; +import Loading from 'src/components/Loading'; import { getFromLocalStorage } from 'src/utils/localStorageHelpers'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import PropertiesModal from 'src/explore/components/PropertiesModal'; @@ -156,6 +157,7 @@ function ChartList(props: ChartListProps) { const [importingChart, showImportModal] = useState(false); const [passwordFields, setPasswordFields] = useState([]); + const [preparingExport, setPreparingExport] = useState(false); const openChartImportModal = () => { showImportModal(true); @@ -177,6 +179,14 @@ function ChartList(props: ChartListProps) { hasPerm('can_read') && isFeatureEnabled(FeatureFlag.VERSIONED_EXPORT); const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; + const handleBulkChartExport = (chartsToExport: Chart[]) => { + const ids = chartsToExport.map(({ id }) => id); + handleResourceExport('chart', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + function handleBulkChartDelete(chartsToDelete: Chart[]) { SupersetClient.delete({ endpoint: `/api/v1/chart/?q=${rison.encode( @@ -540,6 +550,7 @@ function ChartList(props: ChartListProps) { loading={loading} favoriteStatus={favoriteStatus[chart.id]} saveFavoriteStatus={saveFavoriteStatus} + handleBulkChartExport={handleBulkChartExport} /> ); } @@ -653,6 +664,7 @@ function ChartList(props: ChartListProps) { passwordFields={passwordFields} setPasswordFields={setPasswordFields} /> + {preparingExport && } ); } diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx index 93de8ead611d..7b5d792ed4a1 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardCard.tsx @@ -19,11 +19,7 @@ import React from 'react'; import { Link, useHistory } from 'react-router-dom'; import { t } from '@superset-ui/core'; -import { - handleDashboardDelete, - handleBulkDashboardExport, - CardStyles, -} from 'src/views/CRUD/utils'; +import { handleDashboardDelete, CardStyles } from 'src/views/CRUD/utils'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { Dropdown, Menu } from 'src/common/components'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; @@ -49,6 +45,7 @@ interface DashboardCardProps { dashboardFilter?: string; userId?: number; showThumbnails?: boolean; + handleBulkDashboardExport: (dashboardsToExport: Dashboard[]) => void; } function DashboardCard({ @@ -64,6 +61,7 @@ function DashboardCard({ favoriteStatus, saveFavoriteStatus, showThumbnails, + handleBulkDashboardExport, }: DashboardCardProps) { const history = useHistory(); const canEdit = hasPerm('can_write'); diff --git a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx index 9831c2640590..daef59cb195b 100644 --- a/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx +++ b/superset-frontend/src/views/CRUD/dashboard/DashboardList.tsx @@ -25,10 +25,11 @@ import { createFetchRelated, createErrorHandler, handleDashboardDelete, - handleBulkDashboardExport, } from 'src/views/CRUD/utils'; import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; +import handleResourceExport from 'src/utils/export'; +import Loading from 'src/components/Loading'; import SubMenu, { SubMenuProps } from 'src/components/Menu/SubMenu'; import ListView, { ListViewProps, @@ -123,6 +124,7 @@ function DashboardList(props: DashboardListProps) { const [importingDashboard, showImportModal] = useState(false); const [passwordFields, setPasswordFields] = useState([]); + const [preparingExport, setPreparingExport] = useState(false); const openDashboardImportModal = () => { showImportModal(true); @@ -170,6 +172,14 @@ function DashboardList(props: DashboardListProps) { ); } + const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => { + const ids = dashboardsToExport.map(({ id }) => id); + handleResourceExport('dashboard', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + function handleBulkDashboardDelete(dashboardsToDelete: Dashboard[]) { return SupersetClient.delete({ endpoint: `/api/v1/dashboard/?q=${rison.encode( @@ -487,6 +497,7 @@ function DashboardList(props: DashboardListProps) { openDashboardEditModal={openDashboardEditModal} saveFavoriteStatus={saveFavoriteStatus} favoriteStatus={favoriteStatus[dashboard.id]} + handleBulkDashboardExport={handleBulkDashboardExport} /> ); } @@ -605,6 +616,7 @@ function DashboardList(props: DashboardListProps) { passwordFields={passwordFields} setPasswordFields={setPasswordFields} /> + {preparingExport && } ); } diff --git a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx index fda65baa2703..d3be18978ca3 100644 --- a/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx +++ b/superset-frontend/src/views/CRUD/data/database/DatabaseList.tsx @@ -18,7 +18,7 @@ */ import { SupersetClient, t, styled } from '@superset-ui/core'; import React, { useState, useMemo } from 'react'; -import rison from 'rison'; +import Loading from 'src/components/Loading'; import { isFeatureEnabled, FeatureFlag } from 'src/featureFlags'; import { useListViewResource } from 'src/views/CRUD/hooks'; import { createErrorHandler } from 'src/views/CRUD/utils'; @@ -30,6 +30,7 @@ import Icons from 'src/components/Icons'; import ListView, { FilterOperator, Filters } from 'src/components/ListView'; import { commonMenuData } from 'src/views/CRUD/data/common'; import ImportModelsModal from 'src/components/ImportModal/index'; +import handleResourceExport from 'src/utils/export'; import DatabaseModal from './DatabaseModal'; import { DatabaseObject } from './types'; @@ -97,6 +98,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { ); const [importingDatabase, showImportModal] = useState(false); const [passwordFields, setPasswordFields] = useState([]); + const [preparingExport, setPreparingExport] = useState(false); const openDatabaseImportModal = () => { showImportModal(true); @@ -203,9 +205,14 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { } function handleDatabaseExport(database: DatabaseObject) { - return window.location.assign( - `/api/v1/database/export/?q=${rison.encode([database.id])}`, - ); + if (database.id === undefined) { + return; + } + + handleResourceExport('database', [database.id], () => { + setPreparingExport(false); + }); + setPreparingExport(true); } const initialSort = [{ id: 'changed_on_delta_humanized', desc: true }]; @@ -469,6 +476,7 @@ function DatabaseList({ addDangerToast, addSuccessToast }: DatabaseListProps) { passwordFields={passwordFields} setPasswordFields={setPasswordFields} /> + {preparingExport && } ); } diff --git a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx index 3f3ab8d39714..7d5fbb2852be 100644 --- a/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx +++ b/superset-frontend/src/views/CRUD/data/dataset/DatasetList.tsx @@ -33,11 +33,13 @@ import { useListViewResource } from 'src/views/CRUD/hooks'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; import DatasourceModal from 'src/datasource/DatasourceModal'; import DeleteModal from 'src/components/DeleteModal'; +import handleResourceExport from 'src/utils/export'; import ListView, { ListViewProps, Filters, FilterOperator, } from 'src/components/ListView'; +import Loading from 'src/components/Loading'; import SubMenu, { SubMenuProps, ButtonProps, @@ -131,6 +133,7 @@ const DatasetList: FunctionComponent = ({ const [importingDataset, showImportModal] = useState(false); const [passwordFields, setPasswordFields] = useState([]); + const [preparingExport, setPreparingExport] = useState(false); const openDatasetImportModal = () => { showImportModal(true); @@ -547,12 +550,13 @@ const DatasetList: FunctionComponent = ({ ); }; - const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => - window.location.assign( - `/api/v1/dataset/export/?q=${rison.encode( - datasetsToExport.map(({ id }) => id), - )}`, - ); + const handleBulkDatasetExport = (datasetsToExport: Dataset[]) => { + const ids = datasetsToExport.map(({ id }) => id); + handleResourceExport('dataset', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; return ( <> @@ -682,6 +686,7 @@ const DatasetList: FunctionComponent = ({ passwordFields={passwordFields} setPasswordFields={setPasswordFields} /> + {preparingExport && } ); }; diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.test.jsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.test.jsx index 38ecb4aadc06..2e3a1045bcfa 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.test.jsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.test.jsx @@ -26,7 +26,6 @@ import { render, screen, cleanup, waitFor } from 'spec/helpers/testing-library'; import userEvent from '@testing-library/user-event'; import { QueryParamProvider } from 'use-query-params'; import { act } from 'react-dom/test-utils'; -import { handleBulkSavedQueryExport } from 'src/views/CRUD/utils'; import * as featureFlags from 'src/featureFlags'; import SavedQueryList from 'src/views/CRUD/data/savedquery/SavedQueryList'; import SubMenu from 'src/components/Menu/SubMenu'; @@ -285,14 +284,6 @@ describe('RTL', () => { expect(exportTooltip).toBeInTheDocument(); }); - it('runs handleBulkSavedQueryExport when export is clicked', () => { - // Grab Export action button and mock mouse clicking it - const exportActionButton = screen.getAllByRole('button')[18]; - userEvent.click(exportActionButton); - - expect(handleBulkSavedQueryExport).toHaveBeenCalled(); - }); - it('renders an import button in the submenu', () => { // Grab and assert that import saved query button is visible const importButton = screen.getByTestId('import-button'); diff --git a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx index 04a418b39dc5..9c18e9df19f3 100644 --- a/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx +++ b/superset-frontend/src/views/CRUD/data/savedquery/SavedQueryList.tsx @@ -25,12 +25,12 @@ import { createFetchRelated, createFetchDistinct, createErrorHandler, - handleBulkSavedQueryExport, } from 'src/views/CRUD/utils'; import Popover from 'src/components/Popover'; import withToasts from 'src/messageToasts/enhancers/withToasts'; import { useListViewResource } from 'src/views/CRUD/hooks'; import ConfirmStatusChange from 'src/components/ConfirmStatusChange'; +import handleResourceExport from 'src/utils/export'; import SubMenu, { SubMenuProps, ButtonProps, @@ -40,6 +40,7 @@ import ListView, { Filters, FilterOperator, } from 'src/components/ListView'; +import Loading from 'src/components/Loading'; import DeleteModal from 'src/components/DeleteModal'; import ActionsBar, { ActionProps } from 'src/components/ListView/ActionsBar'; import { Tooltip } from 'src/components/Tooltip'; @@ -117,6 +118,7 @@ function SavedQueryList({ ] = useState(null); const [importingSavedQuery, showImportModal] = useState(false); const [passwordFields, setPasswordFields] = useState([]); + const [preparingExport, setPreparingExport] = useState(false); const openSavedQueryImportModal = () => { showImportModal(true); @@ -238,6 +240,16 @@ function SavedQueryList({ ); }; + const handleBulkSavedQueryExport = ( + savedQueriesToExport: SavedQueryObject[], + ) => { + const ids = savedQueriesToExport.map(({ id }) => id); + handleResourceExport('saved_query', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const handleBulkQueryDelete = (queriesToDelete: SavedQueryObject[]) => { SupersetClient.delete({ endpoint: `/api/v1/saved_query/?q=${rison.encode( @@ -542,6 +554,7 @@ function SavedQueryList({ passwordFields={passwordFields} setPasswordFields={setPasswordFields} /> + {preparingExport && } ); } diff --git a/superset-frontend/src/views/CRUD/utils.tsx b/superset-frontend/src/views/CRUD/utils.tsx index 7e575c425aa8..316f534d681d 100644 --- a/superset-frontend/src/views/CRUD/utils.tsx +++ b/superset-frontend/src/views/CRUD/utils.tsx @@ -29,7 +29,7 @@ import rison from 'rison'; import { getClientErrorObject } from 'src/utils/getClientErrorObject'; import { FetchDataConfig } from 'src/components/ListView'; import SupersetText from 'src/utils/textUtils'; -import { Dashboard, Filters, SavedQueryObject } from './types'; +import { Dashboard, Filters } from './types'; const createFetchResourceMethod = (method: string) => ( resource: string, @@ -219,32 +219,6 @@ export function handleChartDelete( ); } -export function handleBulkChartExport(chartsToExport: Chart[]) { - return window.location.assign( - `/api/v1/chart/export/?q=${rison.encode( - chartsToExport.map(({ id }) => id), - )}`, - ); -} - -export function handleBulkDashboardExport(dashboardsToExport: Dashboard[]) { - return window.location.assign( - `/api/v1/dashboard/export/?q=${rison.encode( - dashboardsToExport.map(({ id }) => id), - )}`, - ); -} - -export function handleBulkSavedQueryExport( - savedQueriesToExport: SavedQueryObject[], -) { - return window.location.assign( - `/api/v1/saved_query/export/?q=${rison.encode( - savedQueriesToExport.map(({ id }) => id), - )}`, - ); -} - export function handleDashboardDelete( { id, dashboard_title: dashboardTitle }: Dashboard, refreshData: (config?: FetchDataConfig | null) => void, diff --git a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx index 0fefac675227..dd574413f539 100644 --- a/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/ChartTable.tsx @@ -33,6 +33,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal'; import { User } from 'src/types/bootstrapTypes'; import ChartCard from 'src/views/CRUD/chart/ChartCard'; import Chart from 'src/types/Chart'; +import handleResourceExport from 'src/utils/export'; import Loading from 'src/components/Loading'; import ErrorBoundary from 'src/components/ErrorBoundary'; import SubMenu from 'src/components/Menu/SubMenu'; @@ -90,6 +91,7 @@ function ChartTable({ } = useChartEditModal(setCharts, charts); const [chartFilter, setChartFilter] = useState('Mine'); + const [preparingExport, setPreparingExport] = useState(false); useEffect(() => { const filter = getFromLocalStorage('chart', null); @@ -98,6 +100,14 @@ function ChartTable({ } else setChartFilter(filter.tab); }, []); + const handleBulkChartExport = (chartsToExport: Chart[]) => { + const ids = chartsToExport.map(({ id }) => id); + handleResourceExport('chart', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const getFilters = (filterName: string) => { const filters = []; @@ -208,12 +218,14 @@ function ChartTable({ addSuccessToast={addSuccessToast} favoriteStatus={favoriteStatus[e.id]} saveFavoriteStatus={saveFavoriteStatus} + handleBulkChartExport={handleBulkChartExport} /> ))} ) : ( )} + {preparingExport && } ); } diff --git a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx index 4a84148a6029..f53408a0fd7d 100644 --- a/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx +++ b/superset-frontend/src/views/CRUD/welcome/DashboardTable.tsx @@ -20,6 +20,7 @@ import React, { useState, useMemo, useEffect } from 'react'; import { SupersetClient, t } from '@superset-ui/core'; import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks'; import { Dashboard, DashboardTableProps } from 'src/views/CRUD/types'; +import handleResourceExport from 'src/utils/export'; import { useHistory } from 'react-router-dom'; import { setInLocalStorage, @@ -72,6 +73,7 @@ function DashboardTable({ ); const [editModal, setEditModal] = useState(); const [dashboardFilter, setDashboardFilter] = useState('Mine'); + const [preparingExport, setPreparingExport] = useState(false); useEffect(() => { const filter = getFromLocalStorage('dashboard', null); @@ -80,6 +82,14 @@ function DashboardTable({ } else setDashboardFilter(filter.tab); }, []); + const handleBulkDashboardExport = (dashboardsToExport: Dashboard[]) => { + const ids = dashboardsToExport.map(({ id }) => id); + handleResourceExport('dashboard', ids, () => { + setPreparingExport(false); + }); + setPreparingExport(true); + }; + const handleDashboardEdit = (edits: Dashboard) => SupersetClient.get({ endpoint: `/api/v1/dashboard/${edits.id}`, @@ -221,6 +231,7 @@ function DashboardTable({ } saveFavoriteStatus={saveFavoriteStatus} favoriteStatus={favoriteStatus[e.id]} + handleBulkDashboardExport={handleBulkDashboardExport} /> ))} @@ -228,6 +239,7 @@ function DashboardTable({ {dashboards.length === 0 && ( )} + {preparingExport && } ); } diff --git a/superset/charts/api.py b/superset/charts/api.py index bb92b3da20ef..f248f161adc0 100644 --- a/superset/charts/api.py +++ b/superset/charts/api.py @@ -903,6 +903,7 @@ def export(self, **kwargs: Any) -> Response: 500: $ref: '#/components/responses/500' """ + token = request.args.get("token") requested_ids = kwargs["rison"] timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"chart_export_{timestamp}" @@ -918,12 +919,15 @@ def export(self, **kwargs: Any) -> Response: return self.response_404() buf.seek(0) - return send_file( + response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) + if token: + response.set_cookie(token, "done", max_age=600) + return response @expose("/favorite_status/", methods=["GET"]) @protect() diff --git a/superset/dashboards/api.py b/superset/dashboards/api.py index 632f7f2c6464..a581a2d5c9e0 100644 --- a/superset/dashboards/api.py +++ b/superset/dashboards/api.py @@ -706,6 +706,7 @@ def export(self, **kwargs: Any) -> Response: requested_ids = kwargs["rison"] if is_feature_enabled("VERSIONED_EXPORT"): + token = request.args.get("token") timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"dashboard_export_{timestamp}" filename = f"{root}.zip" @@ -722,12 +723,15 @@ def export(self, **kwargs: Any) -> Response: return self.response_404() buf.seek(0) - return send_file( + response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) + if token: + response.set_cookie(token, "done", max_age=600) + return response query = self.datamodel.session.query(Dashboard).filter( Dashboard.id.in_(requested_ids) diff --git a/superset/databases/api.py b/superset/databases/api.py index d64238baf4a3..a4c8f79a6cd2 100644 --- a/superset/databases/api.py +++ b/superset/databases/api.py @@ -722,6 +722,7 @@ def export(self, **kwargs: Any) -> Response: 500: $ref: '#/components/responses/500' """ + token = request.args.get("token") requested_ids = kwargs["rison"] timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"database_export_{timestamp}" @@ -739,12 +740,15 @@ def export(self, **kwargs: Any) -> Response: return self.response_404() buf.seek(0) - return send_file( + response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) + if token: + response.set_cookie(token, "done", max_age=600) + return response @expose("/import/", methods=["POST"]) @protect() diff --git a/superset/datasets/api.py b/superset/datasets/api.py index 312369c609e3..042ca929440f 100644 --- a/superset/datasets/api.py +++ b/superset/datasets/api.py @@ -432,6 +432,7 @@ def export(self, **kwargs: Any) -> Response: requested_ids = kwargs["rison"] if is_feature_enabled("VERSIONED_EXPORT"): + token = request.args.get("token") timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"dataset_export_{timestamp}" filename = f"{root}.zip" @@ -448,12 +449,15 @@ def export(self, **kwargs: Any) -> Response: return self.response_404() buf.seek(0) - return send_file( + response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) + if token: + response.set_cookie(token, "done", max_age=600) + return response query = self.datamodel.session.query(SqlaTable).filter( SqlaTable.id.in_(requested_ids) diff --git a/superset/queries/saved_queries/api.py b/superset/queries/saved_queries/api.py index 97b123e34442..eb484cc94ccd 100644 --- a/superset/queries/saved_queries/api.py +++ b/superset/queries/saved_queries/api.py @@ -237,6 +237,7 @@ def export(self, **kwargs: Any) -> Response: 500: $ref: '#/components/responses/500' """ + token = request.args.get("token") requested_ids = kwargs["rison"] timestamp = datetime.now().strftime("%Y%m%dT%H%M%S") root = f"saved_query_export_{timestamp}" @@ -254,12 +255,15 @@ def export(self, **kwargs: Any) -> Response: return self.response_404() buf.seek(0) - return send_file( + response = send_file( buf, mimetype="application/zip", as_attachment=True, attachment_filename=filename, ) + if token: + response.set_cookie(token, "done", max_age=600) + return response @expose("/import/", methods=["POST"]) @protect()