From 988a5614a706906af5efed1e694609a64572bd02 Mon Sep 17 00:00:00 2001 From: AmirMohammad Dadkhah Date: Sat, 16 May 2020 17:03:26 +0430 Subject: [PATCH 1/2] Front-end of exporting excel files --- .../components/gridComponents/Chart_spec.jsx | 1 + .../components/ExploreActionButtons_spec.jsx | 4 ++-- .../spec/javascripts/explore/utils_spec.jsx | 24 +++++++++++++++++++ .../spec/javascripts/profile/fixtures.tsx | 1 + .../src/SqlLab/components/SouthPane.jsx | 1 + .../src/dashboard/components/SliceHeader.jsx | 8 +++++++ .../components/SliceHeaderControls.jsx | 13 ++++++++++ .../components/gridComponents/Chart.jsx | 14 +++++++++++ .../src/dashboard/containers/Chart.jsx | 1 + .../src/dashboard/reducers/getInitialState.js | 1 + .../src/explore/actions/exploreActions.js | 15 ++++++++++-- .../components/ExploreActionButtons.jsx | 11 +++++++++ superset-frontend/src/explore/exploreUtils.js | 3 +++ superset-frontend/src/logger/LogUtils.js | 3 +++ 14 files changed, 96 insertions(+), 4 deletions(-) diff --git a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx index 955ade713a710..785df283496a8 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/gridComponents/Chart_spec.jsx @@ -56,6 +56,7 @@ describe('Chart', () => { isExpanded: false, supersetCanExplore: false, supersetCanCSV: false, + supersetCanExcel: false, sliceCanEdit: false, }; diff --git a/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx b/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx index 6a8b6e0993a72..2c6eb6654977b 100644 --- a/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/components/ExploreActionButtons_spec.jsx @@ -34,8 +34,8 @@ describe('ExploreActionButtons', () => { ).toBe(true); }); - it('should render 5 children/buttons', () => { + it('should render 6 children/buttons', () => { const wrapper = shallow(); - expect(wrapper.children()).toHaveLength(5); + expect(wrapper.children()).toHaveLength(6); }); }); diff --git a/superset-frontend/spec/javascripts/explore/utils_spec.jsx b/superset-frontend/spec/javascripts/explore/utils_spec.jsx index ce3b064be4eb4..5f315b9c60f79 100644 --- a/superset-frontend/spec/javascripts/explore/utils_spec.jsx +++ b/superset-frontend/spec/javascripts/explore/utils_spec.jsx @@ -89,6 +89,18 @@ describe('exploreUtils', () => { URI('/superset/explore_json/').search({ csv: 'true' }), ); }); + it('generates proper excel URL', () => { + const url = getExploreUrl({ + formData, + endpointType: 'excel', + force: false, + curUrl: 'superset.com', + }); + compareURI( + URI(url), + URI('/superset/explore_json/').search({ excel: 'true' }), + ); + }); it('generates proper standalone URL', () => { const url = getExploreUrl({ formData, @@ -183,11 +195,23 @@ describe('exploreUtils', () => { }); expect(csvURL).toMatch(availableDomains[0]); + let excelURL = getExploreUrl({ + formData, + endpointType: 'excel', + }); + expect(excelURL).toMatch(availableDomains[0]); + csvURL = getExploreUrl({ formData, endpointType: 'csv', }); expect(csvURL).toMatch(availableDomains[0]); + + excelURL = getExploreUrl({ + formData, + endpointType: 'excel', + }); + expect(excelURL).toMatch(availableDomains[0]); }); }); diff --git a/superset-frontend/spec/javascripts/profile/fixtures.tsx b/superset-frontend/spec/javascripts/profile/fixtures.tsx index cd434248cbb61..ff2b8d686dbf2 100644 --- a/superset-frontend/spec/javascripts/profile/fixtures.tsx +++ b/superset-frontend/spec/javascripts/profile/fixtures.tsx @@ -30,6 +30,7 @@ export const user = { ['can_sql_json', 'Superset'], ['can_search_queries', 'Superset'], ['can_csv', 'Superset'], + ['can_excel', 'Superset'], ], }, firstName: 'alpha', diff --git a/superset-frontend/src/SqlLab/components/SouthPane.jsx b/superset-frontend/src/SqlLab/components/SouthPane.jsx index 54c0dc3499536..584ba1857fadc 100644 --- a/superset-frontend/src/SqlLab/components/SouthPane.jsx +++ b/superset-frontend/src/SqlLab/components/SouthPane.jsx @@ -144,6 +144,7 @@ export class SouthPane extends React.PureComponent { query={query} visualize={false} csv={false} + excel={false} actions={props.actions} cache height={innerTabContentHeight} diff --git a/superset-frontend/src/dashboard/components/SliceHeader.jsx b/superset-frontend/src/dashboard/components/SliceHeader.jsx index 07fba49931408..ebea8a7ec7783 100644 --- a/superset-frontend/src/dashboard/components/SliceHeader.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeader.jsx @@ -36,12 +36,14 @@ const propTypes = { forceRefresh: PropTypes.func, exploreChart: PropTypes.func, exportCSV: PropTypes.func, + exportExcel: PropTypes.func, editMode: PropTypes.bool, annotationQuery: PropTypes.object, annotationError: PropTypes.object, sliceName: PropTypes.string, supersetCanExplore: PropTypes.bool, supersetCanCSV: PropTypes.bool, + supersetCanExcel: PropTypes.bool, sliceCanEdit: PropTypes.bool, componentId: PropTypes.string.isRequired, dashboardId: PropTypes.number.isRequired, @@ -58,6 +60,7 @@ const defaultProps = { toggleExpandSlice: () => ({}), exploreChart: () => ({}), exportCSV: () => ({}), + exportExcel: () => ({}), editMode: false, annotationQuery: {}, annotationError: {}, @@ -68,6 +71,7 @@ const defaultProps = { sliceName: '', supersetCanExplore: false, supersetCanCSV: false, + supersetCanExcel: false, sliceCanEdit: false, }; @@ -86,10 +90,12 @@ class SliceHeader extends React.PureComponent { forceRefresh, exploreChart, exportCSV, + exportExcel, innerRef, sliceName, supersetCanExplore, supersetCanCSV, + supersetCanExcel, sliceCanEdit, editMode, updateSliceName, @@ -146,8 +152,10 @@ class SliceHeader extends React.PureComponent { forceRefresh={forceRefresh} exploreChart={exploreChart} exportCSV={exportCSV} + exportExcel={exportExcel} supersetCanExplore={supersetCanExplore} supersetCanCSV={supersetCanCSV} + supersetCanExcel={supersetCanExcel} sliceCanEdit={sliceCanEdit} componentId={componentId} dashboardId={dashboardId} diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx index 9a820810ac8f7..2d9dfb67554dc 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls.jsx @@ -37,11 +37,13 @@ const propTypes = { updatedDttm: PropTypes.number, supersetCanExplore: PropTypes.bool, supersetCanCSV: PropTypes.bool, + supersetCanExcel: PropTypes.bool, sliceCanEdit: PropTypes.bool, toggleExpandSlice: PropTypes.func, forceRefresh: PropTypes.func, exploreChart: PropTypes.func, exportCSV: PropTypes.func, + exportExcel: PropTypes.func, }; const defaultProps = { @@ -49,12 +51,14 @@ const defaultProps = { toggleExpandSlice: () => ({}), exploreChart: () => ({}), exportCSV: () => ({}), + exportExcel: () => ({}), cachedDttm: null, updatedDttm: null, isCached: false, isExpanded: false, supersetCanExplore: false, supersetCanCSV: false, + supersetCanExcel: false, sliceCanEdit: false, }; @@ -70,6 +74,7 @@ class SliceHeaderControls extends React.PureComponent { constructor(props) { super(props); this.exportCSV = this.exportCSV.bind(this); + this.exportExcel = this.exportExcel.bind(this); this.exploreChart = this.exploreChart.bind(this); this.toggleControls = this.toggleControls.bind(this); this.refreshChart = this.refreshChart.bind(this); @@ -89,6 +94,10 @@ class SliceHeaderControls extends React.PureComponent { this.props.exportCSV(this.props.slice.slice_id); } + exportExcel() { + this.props.exportExcel(this.props.slice.slice_id); + } + exploreChart() { this.props.exploreChart(this.props.slice.slice_id); } @@ -168,6 +177,10 @@ class SliceHeaderControls extends React.PureComponent { {t('Export CSV')} )} + {this.props.supersetCanExcel && ( + {t('Export XLSX')} + )} + {this.props.supersetCanExplore && ( {t('Explore chart')} diff --git a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx index 96ea55770c485..b5d3424e35bcc 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Chart.jsx @@ -28,6 +28,7 @@ import { LOG_ACTIONS_CHANGE_DASHBOARD_FILTER, LOG_ACTIONS_EXPLORE_DASHBOARD_CHART, LOG_ACTIONS_EXPORT_CSV_DASHBOARD_CHART, + LOG_ACTIONS_EXPORT_EXCEL_DASHBOARD_CHART, LOG_ACTIONS_FORCE_REFRESH_CHART, } from '../../../logger/LogUtils'; import { isFilterBox } from '../../util/activeDashboardFilters'; @@ -63,6 +64,7 @@ const propTypes = { isCached: PropTypes.bool, supersetCanExplore: PropTypes.bool.isRequired, supersetCanCSV: PropTypes.bool.isRequired, + supersetCanExcel: PropTypes.bool.isRequired, sliceCanEdit: PropTypes.bool.isRequired, addDangerToast: PropTypes.func.isRequired, }; @@ -94,6 +96,7 @@ class Chart extends React.Component { this.handleFilterMenuClose = this.handleFilterMenuClose.bind(this); this.exploreChart = this.exploreChart.bind(this); this.exportCSV = this.exportCSV.bind(this); + this.exportExcel = this.exportExcel.bind(this); this.forceRefresh = this.forceRefresh.bind(this); this.resize = this.resize.bind(this); this.setDescriptionRef = this.setDescriptionRef.bind(this); @@ -213,6 +216,14 @@ class Chart extends React.Component { }); } + exportExcel() { + this.props.logEvent(LOG_ACTIONS_EXPORT_EXCEL_DASHBOARD_CHART, { + slice_id: this.props.slice.slice_id, + is_cached: this.props.isCached, + }); + exportChart(this.props.formData, 'excel'); + } + forceRefresh() { this.props.logEvent(LOG_ACTIONS_FORCE_REFRESH_CHART, { slice_id: this.props.slice.slice_id, @@ -243,6 +254,7 @@ class Chart extends React.Component { timeout, supersetCanExplore, supersetCanCSV, + supersetCanExcel, sliceCanEdit, addDangerToast, handleToggleFullSize, @@ -283,10 +295,12 @@ class Chart extends React.Component { annotationQuery={chart.annotationQuery} exploreChart={this.exploreChart} exportCSV={this.exportCSV} + exportExcel={this.exportExcel} updateSliceName={updateSliceName} sliceName={sliceName} supersetCanExplore={supersetCanExplore} supersetCanCSV={supersetCanCSV} + supersetCanExcel={supersetCanExcel} sliceCanEdit={sliceCanEdit} componentId={componentId} dashboardId={dashboardId} diff --git a/superset-frontend/src/dashboard/containers/Chart.jsx b/superset-frontend/src/dashboard/containers/Chart.jsx index 634a201d2cf14..4e80fce9b7f8c 100644 --- a/superset-frontend/src/dashboard/containers/Chart.jsx +++ b/superset-frontend/src/dashboard/containers/Chart.jsx @@ -72,6 +72,7 @@ function mapStateToProps( isExpanded: !!dashboardState.expandedSlices[id], supersetCanExplore: !!dashboardInfo.superset_can_explore, supersetCanCSV: !!dashboardInfo.superset_can_csv, + supersetCanExcel: !!dashboardInfo.superset_can_excel, sliceCanEdit: !!dashboardInfo.slice_can_edit, }; } diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index 4f77b070c71c4..e66d5ed8628c7 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -274,6 +274,7 @@ export default function (bootstrapData) { dash_save_perm: dashboard.dash_save_perm, superset_can_explore: dashboard.superset_can_explore, superset_can_csv: dashboard.superset_can_csv, + superset_can_excel: dashboard.superset_can_excel, slice_can_edit: dashboard.slice_can_edit, common: { flash_messages: common.flash_messages, diff --git a/superset-frontend/src/explore/actions/exploreActions.js b/superset-frontend/src/explore/actions/exploreActions.js index e4fd73265d8c4..9cca10006a473 100644 --- a/superset-frontend/src/explore/actions/exploreActions.js +++ b/superset-frontend/src/explore/actions/exploreActions.js @@ -109,8 +109,19 @@ export function setControlValue(controlName, value, validationErrors) { } export const UPDATE_EXPLORE_ENDPOINTS = 'UPDATE_EXPLORE_ENDPOINTS'; -export function updateExploreEndpoints(jsonUrl, csvUrl, standaloneUrl) { - return { type: UPDATE_EXPLORE_ENDPOINTS, jsonUrl, csvUrl, standaloneUrl }; +export function updateExploreEndpoints( + jsonUrl, + csvUrl, + excelUrl, + standaloneUrl, +) { + return { + type: UPDATE_EXPLORE_ENDPOINTS, + jsonUrl, + csvUrl, + excelUrl, + standaloneUrl, + }; } export const SET_EXPLORE_CONTROLS = 'UPDATE_EXPLORE_CONTROLS'; diff --git a/superset-frontend/src/explore/components/ExploreActionButtons.jsx b/superset-frontend/src/explore/components/ExploreActionButtons.jsx index 08757fe6e06cf..0513f8e9585b8 100644 --- a/superset-frontend/src/explore/components/ExploreActionButtons.jsx +++ b/superset-frontend/src/explore/components/ExploreActionButtons.jsx @@ -100,6 +100,17 @@ export default function ExploreActionButtons({ .csv )} + {latestQueryFormData && ( + + .xlsx + + )} Date: Wed, 27 May 2020 12:54:15 +0430 Subject: [PATCH 2/2] export excel --- requirements.txt | 1 + setup.py | 1 + .../src/SqlLab/components/ResultSet.tsx | 17 ++++++++++++++++- .../components/gridComponents/Chart.jsx | 6 +++++- .../explore/components/ExploreActionButtons.jsx | 10 ++++++++++ superset-frontend/src/explore/exploreUtils.js | 6 ++++-- superset/config.py | 1 + superset/utils/core.py | 1 + superset/views/base.py | 8 ++++++++ superset/viz.py | 9 +++++++++ superset/viz_sip38.py | 9 +++++++++ 11 files changed, 65 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index da33b8ae81743..aee86957f53e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,6 +63,7 @@ marshmallow==3.6.1 # via flask-appbuilder, marshmallow-enum, marshmallow- msgpack==1.0.0 # via apache-superset (setup.py) multidict==4.7.6 # via aiohttp, yarl numpy==1.18.4 # via pandas, pyarrow +openpyxl==3.0.3 packaging==20.3 # via bleach pandas==1.0.3 # via apache-superset (setup.py) parsedatetime==2.5 # via apache-superset (setup.py) diff --git a/setup.py b/setup.py index 2a219204487e2..04dc765951167 100644 --- a/setup.py +++ b/setup.py @@ -90,6 +90,7 @@ def get_git_sha(): "isodate", "markdown>=3.0", "msgpack>=1.0.0, <1.1", + "openpyxl>=3.0.3", "pandas>=1.0.3, <1.1", "parsedatetime", "pathlib2", diff --git a/superset-frontend/src/SqlLab/components/ResultSet.tsx b/superset-frontend/src/SqlLab/components/ResultSet.tsx index db154fba4312d..8af6fbb1b83b5 100644 --- a/superset-frontend/src/SqlLab/components/ResultSet.tsx +++ b/superset-frontend/src/SqlLab/components/ResultSet.tsx @@ -41,6 +41,7 @@ interface ResultSetProps { actions: Record; cache?: boolean; csv?: boolean; + excel?: boolean; database?: Record; displayLimit: number; height: number; @@ -63,6 +64,7 @@ export default class ResultSet extends React.PureComponent< static defaultProps = { cache: false, csv: true, + excel: true, database: {}, search: true, showSql: false, @@ -146,7 +148,12 @@ export default class ResultSet extends React.PureComponent< } } renderControls() { - if (this.props.search || this.props.visualize || this.props.csv) { + if ( + this.props.search || + this.props.visualize || + this.props.csv || + this.props.excel + ) { let data = this.props.query.results.data; if (this.props.cache && this.props.query.cached) { data = this.state.data; @@ -172,6 +179,14 @@ export default class ResultSet extends React.PureComponent< {t('.CSV')} )} + {this.props.excel && ( + + )} { - return resultFormat === 'csv' ? resultFormat : resultType; + return resultFormat === 'csv' || resultFormat === 'xlsx' + ? resultFormat + : resultType; }; export function postForm(url, payload, target = '_blank') { diff --git a/superset/config.py b/superset/config.py index 2c2b1802bf60f..031149ba8a1c7 100644 --- a/superset/config.py +++ b/superset/config.py @@ -376,6 +376,7 @@ def _try_json_readsha( # pylint: disable=unused-argument # method. # note: index option should not be overridden CSV_EXPORT = {"encoding": "utf-8"} +EXCEL_EXPORT = {"encoding": "utf-8"} # --------------------------------------------------- # Time grain configurations diff --git a/superset/utils/core.py b/superset/utils/core.py index 9c9fc8f7f3898..e5effb36cf0df 100644 --- a/superset/utils/core.py +++ b/superset/utils/core.py @@ -1476,6 +1476,7 @@ class ChartDataResultFormat(str, Enum): CSV = "csv" JSON = "json" + EXCEL = "excel" class TemporalType(str, Enum): diff --git a/superset/views/base.py b/superset/views/base.py index 58c4943d0fbfa..5185d53dee1c5 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -460,6 +460,14 @@ class CsvResponse(Response): # pylint: disable=too-many-ancestors charset = conf["CSV_EXPORT"].get("encoding", "utf-8") +class ExcelResponse(Response): # pylint: disable=too-many-ancestors + """ + Override Response to take into account xlsx encoding from config.py + """ + + charset = conf.get("EXCEL_EXPORT").get("encoding", "utf-8") + + def check_ownership(obj: Any, raise_if_false: bool = True) -> bool: """Meant to be used in `pre_update` hooks on models to enforce ownership diff --git a/superset/viz.py b/superset/viz.py index 3ce98e4b8d84e..401ac09009e38 100644 --- a/superset/viz.py +++ b/superset/viz.py @@ -22,6 +22,7 @@ """ import copy import inspect +import io import logging import math import re @@ -550,6 +551,14 @@ def get_csv(self) -> Optional[str]: include_index = not isinstance(df.index, pd.RangeIndex) return df.to_csv(index=include_index, **config["CSV_EXPORT"]) + def get_excel(self) -> bytes: + data = io.BytesIO() + df = self.get_df() + include_index = not isinstance(df.index, pd.RangeIndex) + df.to_excel(data, index=include_index, **config.get("EXCEL_EXPORT")) + data.seek(0) + return data.read() + def get_data(self, df: pd.DataFrame) -> VizData: return df.to_dict(orient="records") diff --git a/superset/viz_sip38.py b/superset/viz_sip38.py index aa7a4e1f7798a..c2995a94017f2 100644 --- a/superset/viz_sip38.py +++ b/superset/viz_sip38.py @@ -24,6 +24,7 @@ import copy import hashlib import inspect +import io import logging import math import re @@ -577,6 +578,14 @@ def get_csv(self) -> Optional[str]: include_index = not isinstance(df.index, pd.RangeIndex) return df.to_csv(index=include_index, **config["CSV_EXPORT"]) + def get_excel(self): + data = io.BytesIO() + df = self.get_df() + include_index = not isinstance(df.index, pd.RangeIndex) + df.to_excel(data, index=include_index, **config.get("EXCEL_EXPORT")) + data.seek(0) + return data.read() + def get_data(self, df: pd.DataFrame) -> VizData: return df.to_dict(orient="records")