diff --git a/superset-frontend/package-lock.json b/superset-frontend/package-lock.json index 63e0acd400b..0df9b607a8f 100644 --- a/superset-frontend/package-lock.json +++ b/superset-frontend/package-lock.json @@ -135,6 +135,7 @@ "use-event-callback": "^0.1.0", "use-immer": "^0.9.0", "use-query-params": "^1.1.9", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "^17.7.2" }, "devDependencies": { @@ -56626,6 +56627,18 @@ } } }, + "node_modules/xlsx": { + "version": "0.20.3", + "resolved": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", + "integrity": "sha512-oLDq3jw7AcLqKWH2AhCpVTZl8mf6X2YReP+Neh0SJUzV/BdZYjth94tG5toiMB1PPrYtxOCfaoUCkvtuH+3AJA==", + "license": "Apache-2.0", + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/xml-name-validator": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", diff --git a/superset-frontend/package.json b/superset-frontend/package.json index 39d41673482..479eb4ece52 100644 --- a/superset-frontend/package.json +++ b/superset-frontend/package.json @@ -104,12 +104,12 @@ "@superset-ui/legacy-plugin-chart-world-map": "file:./plugins/legacy-plugin-chart-world-map", "@superset-ui/legacy-preset-chart-deckgl": "file:./plugins/legacy-preset-chart-deckgl", "@superset-ui/legacy-preset-chart-nvd3": "file:./plugins/legacy-preset-chart-nvd3", + "@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table", "@superset-ui/plugin-chart-cartodiagram": "file:./plugins/plugin-chart-cartodiagram", "@superset-ui/plugin-chart-echarts": "file:./plugins/plugin-chart-echarts", "@superset-ui/plugin-chart-handlebars": "file:./plugins/plugin-chart-handlebars", "@superset-ui/plugin-chart-pivot-table": "file:./plugins/plugin-chart-pivot-table", "@superset-ui/plugin-chart-table": "file:./plugins/plugin-chart-table", - "@superset-ui/plugin-chart-ag-grid-table": "file:./plugins/plugin-chart-ag-grid-table", "@superset-ui/plugin-chart-word-cloud": "file:./plugins/plugin-chart-word-cloud", "@superset-ui/switchboard": "file:./packages/superset-ui-switchboard", "@types/d3-format": "^3.0.1", @@ -203,6 +203,7 @@ "use-event-callback": "^0.1.0", "use-immer": "^0.9.0", "use-query-params": "^1.1.9", + "xlsx": "https://cdn.sheetjs.com/xlsx-0.20.3/xlsx-0.20.3.tgz", "yargs": "^17.7.2" }, "devDependencies": { diff --git a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx index cbe548691f0..127a0f374c5 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeader/index.tsx @@ -59,6 +59,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & { formData: object; width: number; height: number; + exportPivotExcel?: (arg0: string) => void; }; const annotationsLoading = t('Annotation layers are still loading.'); @@ -167,6 +168,7 @@ const SliceHeader = forwardRef( formData, width, height, + exportPivotExcel = () => ({}), }, ref, ) => { @@ -344,6 +346,7 @@ const SliceHeader = forwardRef( formData={formData} exploreUrl={exploreUrl} crossFiltersEnabled={isCrossFiltersEnabled} + exportPivotExcel={exportPivotExcel} /> )} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx index 0ef74cf227a..aa5e413927c 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/SliceHeaderControls.test.tsx @@ -33,6 +33,7 @@ const createProps = (viz_type = VizType.Sunburst) => exportFullCSV: jest.fn(), exportXLSX: jest.fn(), exportFullXLSX: jest.fn(), + exportPivotExcel: jest.fn(), forceRefresh: jest.fn(), handleToggleFullSize: jest.fn(), toggleExpandSlice: jest.fn(), @@ -254,6 +255,20 @@ test('Should not show export full Excel if report is not table', async () => { expect(screen.queryByText('Export to full Excel')).not.toBeInTheDocument(); }); +test('Should export to pivoted Excel if report is pivot table', async () => { + const props = createProps(VizType.PivotTable); + renderWrapper(props); + openMenu(); + expect(props.exportPivotExcel).toHaveBeenCalledTimes(0); + userEvent.hover(screen.getByText('Download')); + userEvent.click(await screen.findByText('Export to Pivoted Excel')); + expect(props.exportPivotExcel).toHaveBeenCalledTimes(1); + expect(props.exportPivotExcel).toHaveBeenCalledWith( + '.pvtTable', + props.slice.slice_name, + ); +}); + test('Should "Show chart description"', () => { const props = createProps(); renderWrapper(props); diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 5d9293a1c46..312b20ec869 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -128,6 +128,7 @@ export interface SliceHeaderControlsProps { exportXLSX?: (sliceId: number) => void; exportFullXLSX?: (sliceId: number) => void; handleToggleFullSize: () => void; + exportPivotExcel?: (tableSelector: string, sliceName: string) => void; addDangerToast: (message: string) => void; addSuccessToast: (message: string) => void; @@ -255,6 +256,10 @@ const SliceHeaderControls = ( }); break; } + case MenuKeys.ExportPivotXlsx: { + props.exportPivotExcel?.('.pvtTable', props.slice.slice_name); + break; + } case MenuKeys.CrossFilterScoping: { openScopingModal(); break; @@ -468,6 +473,15 @@ const SliceHeaderControls = ( {t('Export to Excel')} + {isPivotTable && ( + } + > + {t('Export to Pivoted Excel')} + + )} + {isFeatureEnabled(FeatureFlag.AllowFullCsvExport) && props.supersetCanCSV && isTable && ( diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 299f329b675..942ed4179f1 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -37,6 +37,7 @@ import { import { postFormData } from 'src/explore/exploreUtils/formData'; import { URL_PARAMS } from 'src/constants'; import { enforceSharedLabelsColorsArray } from 'src/utils/colorScheme'; +import exportPivotExcel from 'src/utils/downloadAsPivotExcel'; import SliceHeader from '../SliceHeader'; import MissingChart from '../MissingChart'; @@ -471,6 +472,7 @@ const Chart = props => { formData={formData} width={width} height={getHeaderHeight()} + exportPivotExcel={exportPivotExcel} /> {/* diff --git a/superset-frontend/src/dashboard/types.ts b/superset-frontend/src/dashboard/types.ts index 43d67bd351f..867cf0b0bf6 100644 --- a/superset-frontend/src/dashboard/types.ts +++ b/superset-frontend/src/dashboard/types.ts @@ -289,4 +289,5 @@ export enum MenuKeys { ToggleFullscreen = 'toggle_fullscreen', ManageEmbedded = 'manage_embedded', ManageEmailReports = 'manage_email_reports', + ExportPivotXlsx = 'export_pivot_xlsx', } diff --git a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx index 373727548d4..38d43caf964 100644 --- a/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx +++ b/superset-frontend/src/explore/components/useExploreAdditionalActionsMenu/index.jsx @@ -43,6 +43,7 @@ import { LOG_ACTIONS_CHART_DOWNLOAD_AS_CSV_PIVOTED, LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, } from 'src/logger/LogUtils'; +import exportPivotExcel from 'src/utils/downloadAsPivotExcel'; import ViewQueryModal from '../controls/ViewQueryModal'; import EmbedCodeContent from '../EmbedCodeContent'; import DashboardsSubMenu from './DashboardsSubMenu'; @@ -67,6 +68,7 @@ const MENU_KEYS = { DELETE_REPORT: 'delete_report', VIEW_QUERY: 'view_query', RUN_IN_SQL_LAB: 'run_in_sql_lab', + EXPORT_TO_PIVOT_XLSX: 'export_to_pivot_xlsx', }; const VIZ_TYPES_PIVOTABLE = [VizType.PivotTable]; @@ -248,6 +250,16 @@ export const useExploreAdditionalActionsMenu = ( }), ); break; + case MENU_KEYS.EXPORT_TO_PIVOT_XLSX: + exportPivotExcel('.pvtTable', slice?.slice_name ?? t('pivoted_xlsx')); + setIsDropdownVisible(false); + dispatch( + logEvent(LOG_ACTIONS_CHART_DOWNLOAD_AS_XLS, { + chartId: slice?.slice_id, + chartName: slice?.slice_name, + }), + ); + break; case MENU_KEYS.DOWNLOAD_AS_IMAGE: downloadAsImage( '.panel-body .chart-container', @@ -365,6 +377,13 @@ export const useExploreAdditionalActionsMenu = ( > {t('Export to Excel')} + } + disabled={!canDownloadCSV} + > + {t('Export to Pivoted Excel')} + diff --git a/superset-frontend/src/utils/downloadAsPivotExcel.ts b/superset-frontend/src/utils/downloadAsPivotExcel.ts new file mode 100644 index 00000000000..40d10fe3492 --- /dev/null +++ b/superset-frontend/src/utils/downloadAsPivotExcel.ts @@ -0,0 +1,28 @@ +/** + * 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 { utils, writeFile } from 'xlsx'; + +export default function exportPivotExcel( + tableSelector: string, + fileName: string, +) { + const table = document.querySelector(tableSelector); + const workbook = utils.table_to_book(table); + writeFile(workbook, `${fileName}.xlsx`); +}