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`);
+}