This is an automated email from the ASF dual-hosted git repository.

rusackas pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new dc41c45bec feat(pivot-table-chart): Download as pivoted excel (#33569)
dc41c45bec is described below

commit dc41c45bec385435b163bb1c53fdcfcd450b8441
Author: mdusmanalvi <[email protected]>
AuthorDate: Fri Jul 18 21:12:14 2025 +0300

    feat(pivot-table-chart): Download as pivoted excel (#33569)
    
    Co-authored-by: Mehmet Salih Yavuz <[email protected]>
---
 superset-frontend/package-lock.json                | 13 ++++++++++
 superset-frontend/package.json                     |  3 ++-
 .../src/dashboard/components/SliceHeader/index.tsx |  3 +++
 .../SliceHeaderControls.test.tsx                   | 15 ++++++++++++
 .../components/SliceHeaderControls/index.tsx       | 14 +++++++++++
 .../dashboard/components/gridComponents/Chart.jsx  |  2 ++
 superset-frontend/src/dashboard/types.ts           |  1 +
 .../useExploreAdditionalActionsMenu/index.jsx      | 19 +++++++++++++++
 .../src/utils/downloadAsPivotExcel.ts              | 28 ++++++++++++++++++++++
 9 files changed, 97 insertions(+), 1 deletion(-)

diff --git a/superset-frontend/package-lock.json 
b/superset-frontend/package-lock.json
index 63e0acd400..0df9b607a8 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 39d4167348..479eb4ece5 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 cbe548691f..127a0f374c 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<HTMLDivElement, 
SliceHeaderProps>(
       formData,
       width,
       height,
+      exportPivotExcel = () => ({}),
     },
     ref,
   ) => {
@@ -344,6 +346,7 @@ const SliceHeader = forwardRef<HTMLDivElement, 
SliceHeaderProps>(
                   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 0ef74cf227..aa5e413927 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 5d9293a1c4..312b20ec86 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')}
           </Menu.Item>
 
+          {isPivotTable && (
+            <Menu.Item
+              key={MenuKeys.ExportPivotXlsx}
+              icon={<Icons.FileOutlined css={dropdownIconsStyles} />}
+            >
+              {t('Export to Pivoted Excel')}
+            </Menu.Item>
+          )}
+
           {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 299f329b67..942ed4179f 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 43d67bd351..867cf0b0bf 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 373727548d..38d43caf96 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')}
           </Menu.Item>
+          <Menu.Item
+            key={MENU_KEYS.EXPORT_TO_PIVOT_XLSX}
+            icon={<Icons.FileOutlined />}
+            disabled={!canDownloadCSV}
+          >
+            {t('Export to Pivoted Excel')}
+          </Menu.Item>
         </Menu.SubMenu>
         <Menu.SubMenu title={t('Share')} key={MENU_KEYS.SHARE_SUBMENU}>
           <Menu.Item key={MENU_KEYS.COPY_PERMALINK}>
diff --git a/superset-frontend/src/utils/downloadAsPivotExcel.ts 
b/superset-frontend/src/utils/downloadAsPivotExcel.ts
new file mode 100644
index 0000000000..40d10fe349
--- /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`);
+}

Reply via email to