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/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/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 && (
+
+ )}
({}),
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 {
)}
+ {this.props.supersetCanExcel && (
+
+ )}
+
{this.props.supersetCanExplore && (