From b30f6a5db1c9d79d4837529b671090f4a908c056 Mon Sep 17 00:00:00 2001 From: Kamil Gabryjelski Date: Thu, 30 Jun 2022 21:06:51 +0200 Subject: [PATCH] chore(explore): Get Explore data from endpoint instead of bootstrap_data (#20519) * feat(explore): Use v1/explore endpoint data instead of bootstrapData * Add tests * Fix ci * Remove redundant dependency * Use form_data_key in cypress tests * Add auth headers to for data request * Address comments * Remove displaying danger toast * Conditionally add auth headers * Address comments * Fix typing bug * fix * Fix opening dataset * Fix sqllab chart create * Run queries in parallel * Fix dashboard id autofill * Fix lint * Fix test --- .../cypress/integration/explore/chart.test.js | 2 +- .../integration/explore/control.test.ts | 10 +- .../explore/explore.applitools.test.ts | 2 +- .../integration/explore/filter_box.test.js | 2 +- .../cypress/integration/explore/link.test.ts | 2 +- .../explore/visualizations/area.test.js | 34 ++-- .../explore/visualizations/big_number.test.js | 6 +- .../explore/visualizations/box_plot.test.js | 2 +- .../explore/visualizations/bubble.test.js | 8 +- .../explore/visualizations/compare.test.js | 2 +- .../explore/visualizations/dist_bar.test.js | 8 +- .../visualizations/download_chart.test.js | 2 +- .../explore/visualizations/dual_line.test.js | 4 +- .../explore/visualizations/gauge.test.js | 2 +- .../explore/visualizations/graph.test.ts | 2 +- .../explore/visualizations/histogram.test.ts | 2 +- .../explore/visualizations/line.test.ts | 38 ++--- .../explore/visualizations/pie.test.js | 2 +- .../visualizations/pivot_table.test.js | 4 +- .../explore/visualizations/sankey.test.js | 6 +- .../explore/visualizations/shared.helper.js | 4 +- .../explore/visualizations/sunburst.test.js | 2 +- .../explore/visualizations/table.test.ts | 8 +- .../explore/visualizations/time_table.js | 10 +- .../explore/visualizations/treemap.test.js | 2 +- .../explore/visualizations/world_map.test.js | 2 +- .../cypress-base/cypress/support/index.ts | 46 +++++- .../superset-ui-chart-controls/src/types.ts | 10 ++ .../components/SaveDatasetModal/index.tsx | 75 +++++---- superset-frontend/src/SqlLab/types.ts | 10 +- .../src/addSlice/AddSliceContainer.test.tsx | 9 +- .../src/addSlice/AddSliceContainer.tsx | 31 ++-- .../src/components/Chart/chartReducer.ts | 3 +- superset-frontend/src/constants.ts | 4 + superset-frontend/src/explore/App.jsx | 6 +- superset-frontend/src/explore/ExplorePage.tsx | 68 ++++++++ .../src/explore/actions/datasourcesActions.ts | 2 +- .../explore/actions/hydrateExplore.test.ts | 92 +++++++++++ .../src/explore/actions/hydrateExplore.ts | 146 ++++++++++++++++++ .../components/ControlPanelsContainer.tsx | 3 +- .../DatasourcePanel/DatasourcePanel.test.tsx | 4 +- .../components/ExploreViewContainer/index.jsx | 16 +- .../controls/DatasourceControl/index.jsx | 2 +- .../VizTypeControl/FastVizSwitcher.tsx | 2 +- .../getParsedExploreURLParams.test.ts | 62 ++++++++ .../exploreUtils/getParsedExploreURLParams.ts | 117 ++++++++++++++ .../src/explore/exploreUtils/index.js | 3 +- superset-frontend/src/explore/fixtures.tsx | 59 ++++++- superset-frontend/src/explore/index.jsx | 22 ++- .../explore/reducers/datasourcesReducer.ts | 6 +- .../src/explore/reducers/exploreReducer.js | 7 +- .../src/explore/reducers/getInitialState.ts | 146 ------------------ .../src/explore/reducers/saveModalReducer.js | 4 + superset-frontend/src/explore/store.js | 2 +- superset-frontend/src/explore/types.ts | 47 +++++- 55 files changed, 845 insertions(+), 327 deletions(-) create mode 100644 superset-frontend/src/explore/ExplorePage.tsx create mode 100644 superset-frontend/src/explore/actions/hydrateExplore.test.ts create mode 100644 superset-frontend/src/explore/actions/hydrateExplore.ts create mode 100644 superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts create mode 100644 superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.ts delete mode 100644 superset-frontend/src/explore/reducers/getInitialState.ts diff --git a/superset-frontend/cypress-base/cypress/integration/explore/chart.test.js b/superset-frontend/cypress-base/cypress/integration/explore/chart.test.js index c9f4a1c9f58bf..ca37bf9690a9f 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/chart.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/chart.test.js @@ -41,7 +41,7 @@ describe('No Results', () => { ], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.wait('@getJson').its('response.statusCode').should('eq', 200); cy.get('div.chart-container').contains( 'No results were returned for this query', diff --git a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts index 18bf8859a8ef9..95bdd514bf5b0 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/control.test.ts @@ -148,7 +148,7 @@ describe('Time range filter', () => { metrics: [NUM_METRIC], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') @@ -172,7 +172,7 @@ describe('Time range filter', () => { time_range: 'Last year', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') @@ -192,7 +192,7 @@ describe('Time range filter', () => { time_range: 'previous calendar month', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') @@ -212,7 +212,7 @@ describe('Time range filter', () => { time_range: 'DATEADD(DATETIME("today"), -7, day) : today', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') @@ -235,7 +235,7 @@ describe('Time range filter', () => { time_range: 'No filter', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData' }); cy.get('[data-test=time-range-trigger]') diff --git a/superset-frontend/cypress-base/cypress/integration/explore/explore.applitools.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/explore.applitools.test.ts index 96b0d6684768a..64d77b4d6854a 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/explore.applitools.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/explore.applitools.test.ts @@ -31,7 +31,7 @@ describe('explore view', () => { it('should load Explore', () => { const LINE_CHART_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'line' }; const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); cy.eyesOpen({ testName: 'Explore page', diff --git a/superset-frontend/cypress-base/cypress/integration/explore/filter_box.test.js b/superset-frontend/cypress-base/cypress/integration/explore/filter_box.test.js index 921377c45fa4b..b9844274e2d04 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/filter_box.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/filter_box.test.js @@ -22,7 +22,7 @@ describe('Edit FilterBox Chart', () => { const VIZ_DEFAULTS = { ...FORM_DATA_DEFAULTS, viz_type: 'filter_box' }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts index 9f07e9c10b859..fb3445fc6366f 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/link.test.ts @@ -74,7 +74,7 @@ describe('Test explore links', () => { }; const newChartName = `Test chart [${shortid.generate()}]`; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@tableChartData' }); cy.url().then(() => { cy.get('[data-test="query-save-button"]').click(); diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/area.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/area.test.js index 86b5a789c2474..59b8beabe38b9 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/area.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/area.test.js @@ -51,7 +51,7 @@ describe('Visualization > Area', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } @@ -75,23 +75,21 @@ describe('Visualization > Area', () => { }); it('should work with groupby and filter', () => { - cy.visitChartByParams( - JSON.stringify({ - ...AREA_FORM_DATA, - groupby: ['region'], - adhoc_filters: [ - { - expressionType: 'SIMPLE', - subject: 'region', - operator: 'IN', - comparator: ['South Asia', 'North America'], - clause: 'WHERE', - sqlExpression: null, - filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo', - }, - ], - }), - ); + cy.visitChartByParams({ + ...AREA_FORM_DATA, + groupby: ['region'], + adhoc_filters: [ + { + expressionType: 'SIMPLE', + subject: 'region', + operator: 'IN', + comparator: ['South Asia', 'North America'], + clause: 'WHERE', + sqlExpression: null, + filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo', + }, + ], + }); cy.wait('@getJson').then(async ({ response }) => { const responseBody = response?.body; diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js index 30e7716b730c2..ede7ed47f9a79 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/big_number.test.js @@ -25,11 +25,11 @@ describe('Visualization > Big Number with Trendline', () => { slice_id: 42, granularity_sqla: 'year', time_grain_sqla: 'P1D', - time_range: '2000+:+2014-01-02', + time_range: '2000 : 2014-01-02', metric: 'sum__SP_POP_TOTL', adhoc_filters: [], compare_lag: '10', - compare_suffix: 'over+10Y', + compare_suffix: 'over 10Y', y_axis_format: '.3s', show_trend_line: true, start_y_axis_at_zero: true, @@ -42,7 +42,7 @@ describe('Visualization > Big Number with Trendline', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: '.superset-legacy-chart-big-number', diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/box_plot.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/box_plot.test.js index 432815b8692c1..6a4afba97a91f 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/box_plot.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/box_plot.test.js @@ -33,7 +33,7 @@ describe('Visualization > Box Plot', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/bubble.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/bubble.test.js index 9bd91f37c5eed..9c824e5d9ff40 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/bubble.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/bubble.test.js @@ -23,7 +23,7 @@ describe('Visualization > Bubble', () => { slice_id: 46, granularity_sqla: 'year', time_grain_sqla: 'P1D', - time_range: '2011-01-01+:+2011-01-02', + time_range: '2011-01-01 : 2011-01-02', series: 'region', entity: 'country_name', x: 'sum__SP_RUR_TOTL_ZS', @@ -47,7 +47,7 @@ describe('Visualization > Bubble', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } @@ -60,7 +60,7 @@ describe('Visualization > Bubble', () => { // Since main functionality is already covered in filter test below, // skip this test until we find a solution. it.skip('should work', () => { - cy.visitChartByParams(JSON.stringify(BUBBLE_FORM_DATA)).then(() => { + cy.visitChartByParams(BUBBLE_FORM_DATA).then(() => { cy.wait('@getJson').then(xhr => { let expectedBubblesNumber = 0; xhr.responseBody.data.forEach(element => { @@ -86,7 +86,7 @@ describe('Visualization > Bubble', () => { expressionType: 'SIMPLE', subject: 'region', operator: '==', - comparator: 'South+Asia', + comparator: 'South Asia', clause: 'WHERE', sqlExpression: null, filterOptionName: 'filter_b2tfg1rs8y_8kmrcyxvsqd', diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/compare.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/compare.test.js index 83b37f889f77c..35f56754a849f 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/compare.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/compare.test.js @@ -47,7 +47,7 @@ describe('Visualization > Compare', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dist_bar.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dist_bar.test.js index bec718367ef92..6bd7c82f430b4 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dist_bar.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dist_bar.test.js @@ -33,7 +33,7 @@ describe('Visualization > Distribution bar chart', () => { groupby: ['state'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', querySubstring: NUM_METRIC.label, @@ -49,7 +49,7 @@ describe('Visualization > Distribution bar chart', () => { columns: ['gender'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -61,7 +61,7 @@ describe('Visualization > Distribution bar chart', () => { row_limit: 10, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -74,7 +74,7 @@ describe('Visualization > Distribution bar chart', () => { contribution: true, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); }); diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js index ce4a871f8e2de..029ead311088e 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/download_chart.test.js @@ -33,7 +33,7 @@ describe('Download Chart > Distribution bar chart', () => { groupby: ['state'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.get('.header-with-actions .ant-dropdown-trigger').click(); cy.get(':nth-child(1) > .ant-dropdown-menu-submenu-title').click(); cy.get( diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dual_line.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dual_line.test.js index 641b2925d77ec..3a3eb334fb9fb 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dual_line.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/dual_line.test.js @@ -23,7 +23,7 @@ describe('Visualization > Dual Line', () => { slice_id: 58, granularity_sqla: 'ds', time_grain_sqla: 'P1D', - time_range: '100+years+ago+:+now', + time_range: '100 years ago : now', color_scheme: 'bnbColors', x_axis_format: 'smart_date', metric: 'sum__num', @@ -35,7 +35,7 @@ describe('Visualization > Dual Line', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/gauge.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/gauge.test.js index 8b5b2ffd0b72c..c4735b7b8a652 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/gauge.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/gauge.test.js @@ -27,7 +27,7 @@ describe('Visualization > Gauge', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/graph.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/graph.test.ts index 47adb075bdd91..c01d9c5099a15 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/graph.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/graph.test.ts @@ -46,7 +46,7 @@ describe('Visualization > Graph', () => { function verify(formData: { [name: string]: string | boolean | number | Array; }): void { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/histogram.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/histogram.test.ts index 67cbba3f9699d..ff6355319ae4a 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/histogram.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/histogram.test.ts @@ -39,7 +39,7 @@ describe('Visualization > Histogram', () => { }; function verify(formData: QueryFormData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts index e8998b4bef86e..da20cfab85a95 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/line.test.ts @@ -28,7 +28,7 @@ describe('Visualization > Line', () => { it('should show validator error when no metric', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [] }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.get('.panel-body').contains( `Add required control values to preview chart`, ); @@ -36,7 +36,7 @@ describe('Visualization > Line', () => { it('should not show validator error when metric added', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [] }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.get('.panel-body').contains( `Add required control values to preview chart`, ); @@ -61,7 +61,7 @@ describe('Visualization > Line', () => { it('should allow negative values in Y bounds', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.get('#controlSections-tab-display').click(); cy.get('span').contains('Y Axis Bounds').scrollIntoView(); cy.get('input[placeholder="Min"]').type('-0.1', { delay: 100 }); @@ -81,7 +81,7 @@ describe('Visualization > Line', () => { it('should work with adhoc metric', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics: [NUM_METRIC] }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -89,7 +89,7 @@ describe('Visualization > Line', () => { const metrics = ['count']; const groupby = ['gender']; const formData = { ...LINE_CHART_DEFAULTS, metrics, groupby }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -101,7 +101,7 @@ describe('Visualization > Line', () => { metrics, adhoc_filters: filters, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -113,7 +113,7 @@ describe('Visualization > Line', () => { groupby: ['name'], timeseries_limit_metric: NUM_METRIC, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -126,7 +126,7 @@ describe('Visualization > Line', () => { timeseries_limit_metric: NUM_METRIC, order_desc: true, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -138,7 +138,7 @@ describe('Visualization > Line', () => { rolling_type: 'mean', rolling_periods: 10, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -147,12 +147,12 @@ describe('Visualization > Line', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics, - time_compare: ['1+year'], + time_compare: ['1 year'], comparison_type: 'values', groupby: ['gender'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); // Offset color should match original line color @@ -190,10 +190,10 @@ describe('Visualization > Line', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics, - time_compare: ['1+year'], + time_compare: ['1 year'], comparison_type: 'ratio', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -202,10 +202,10 @@ describe('Visualization > Line', () => { const formData = { ...LINE_CHART_DEFAULTS, metrics, - time_compare: ['1+year'], + time_compare: ['1 year'], comparison_type: 'percentage', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); }); @@ -214,7 +214,7 @@ describe('Visualization > Line', () => { ...LINE_CHART_DEFAULTS, metrics: ['count'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); cy.get('text.nv-legend-text').contains('COUNT(*)'); }); @@ -225,7 +225,7 @@ describe('Visualization > Line', () => { metrics: ['count'], annotation_layers: [ { - name: 'Goal+line', + name: 'Goal line', annotationType: 'FORMULA', sourceType: '', value: 'y=140000', @@ -245,7 +245,7 @@ describe('Visualization > Line', () => { }, ], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); cy.get('.slice_container').within(() => { // Goal line annotation doesn't show up in legend @@ -281,7 +281,7 @@ describe('Visualization > Line', () => { }, ], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); }, ); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pie.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pie.test.js index fb083de615e9a..3b28128e68c00 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pie.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pie.test.js @@ -37,7 +37,7 @@ describe('Visualization > Pie', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js index 14de08da79360..ef62beb381056 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/pivot_table.test.js @@ -23,7 +23,7 @@ describe('Visualization > Pivot Table', () => { slice_id: 61, granularity_sqla: 'ds', time_grain_sqla: 'P1D', - time_range: '100+years+ago+:+now', + time_range: '100 years ago : now', metrics: ['sum__num'], adhoc_filters: [], groupby: ['name'], @@ -54,7 +54,7 @@ describe('Visualization > Pivot Table', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'table' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sankey.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sankey.test.js index 257ec00c1f05d..747e8a06f25b6 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sankey.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sankey.test.js @@ -24,7 +24,7 @@ describe('Visualization > Sankey', () => { url_params: {}, granularity_sqla: null, time_grain_sqla: 'P1D', - time_range: 'Last+week', + time_range: 'Last week', groupby: ['source', 'target'], metric: 'sum__value', adhoc_filters: [], @@ -33,7 +33,7 @@ describe('Visualization > Sankey', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } @@ -53,7 +53,7 @@ describe('Visualization > Sankey', () => { adhoc_filters: [ { expressionType: 'SQL', - sqlExpression: 'SUM(value)+>+0', + sqlExpression: 'SUM(value) > 0', clause: 'HAVING', subject: null, operator: null, diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/shared.helper.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/shared.helper.js index 78a659fc91f31..bfd50e66d3df2 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/shared.helper.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/shared.helper.js @@ -24,7 +24,7 @@ export const FORM_DATA_DEFAULTS = { datasource: '3__table', granularity_sqla: 'ds', time_grain_sqla: null, - time_range: '100+years+ago+:+now', + time_range: '100 years ago : now', adhoc_filters: [], groupby: [], limit: null, @@ -37,7 +37,7 @@ export const HEALTH_POP_FORM_DATA_DEFAULTS = { datasource: '2__table', granularity_sqla: 'ds', time_grain_sqla: 'P1D', - time_range: '1960-01-01+:+2014-01-02', + time_range: '1960-01-01 : 2014-01-02', }; export const NUM_METRIC = { diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sunburst.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sunburst.test.js index 99cbb1e407e4c..e7ccacac03baa 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sunburst.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/sunburst.test.js @@ -32,7 +32,7 @@ describe('Visualization > Sunburst', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts index 6361d93d1809a..f11b23d595a1d 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/table.test.ts @@ -174,7 +174,7 @@ describe('Visualization > Table', () => { groupby: ['name'], row_limit: limit, }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.wait('@chartData').then(({ response }) => { cy.verifySliceContainer('table'); expect(response?.body.result[0].data.length).to.eq(limit); @@ -219,7 +219,7 @@ describe('Visualization > Table', () => { order_by_cols: ['["num", false]'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.wait('@chartData').then(({ response }) => { cy.verifySliceContainer('table'); const records = response?.body.result[0].data; @@ -233,7 +233,7 @@ describe('Visualization > Table', () => { const formData = { ...VIZ_DEFAULTS, metrics, adhoc_filters: filters }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData', chartSelector: 'table' }); }); @@ -244,7 +244,7 @@ describe('Visualization > Table', () => { groupby: ['state'], }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@chartData', querySubstring: /group by.*state/i, diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/time_table.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/time_table.js index 7da90027856f6..eb81d17a3ba1e 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/time_table.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/time_table.js @@ -33,7 +33,7 @@ describe('Visualization > Time TableViz', () => { column_collection: [ { key: '9g4K-B-YL', - label: 'Last+Year', + label: 'Last Year', colType: 'time', timeLag: '1', comparisonType: 'value', @@ -42,7 +42,7 @@ describe('Visualization > Time TableViz', () => { url: '', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', querySubstring: NUM_METRIC.label, @@ -61,7 +61,7 @@ describe('Visualization > Time TableViz', () => { column_collection: [ { key: '9g4K-B-YL', - label: 'Last+Year', + label: 'Last Year', colType: 'time', timeLag: '1', comparisonType: 'value', @@ -70,7 +70,7 @@ describe('Visualization > Time TableViz', () => { url: '', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', querySubstring: NUM_METRIC.label, @@ -107,7 +107,7 @@ describe('Visualization > Time TableViz', () => { url: '', }; - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', querySubstring: NUM_METRIC.label, diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/treemap.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/treemap.test.js index 6ebe06274fbd5..1be85e9e4cdfb 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/treemap.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/treemap.test.js @@ -38,7 +38,7 @@ describe('Visualization > Treemap', () => { const level2 = '.chart-container rect[style="fill: rgb(0, 122, 135);"]'; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/world_map.test.js b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/world_map.test.js index ed9d3e4214987..0fab65519d8dd 100644 --- a/superset-frontend/cypress-base/cypress/integration/explore/visualizations/world_map.test.js +++ b/superset-frontend/cypress-base/cypress/integration/explore/visualizations/world_map.test.js @@ -35,7 +35,7 @@ describe('Visualization > World Map', () => { }; function verify(formData) { - cy.visitChartByParams(JSON.stringify(formData)); + cy.visitChartByParams(formData); cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' }); } diff --git a/superset-frontend/cypress-base/cypress/support/index.ts b/superset-frontend/cypress-base/cypress/support/index.ts index 38e1ad5f6c88a..9d77f98accad9 100644 --- a/superset-frontend/cypress-base/cypress/support/index.ts +++ b/superset-frontend/cypress-base/cypress/support/index.ts @@ -55,12 +55,46 @@ Cypress.Commands.add('visitChartById', chartId => cy.visit(`${BASE_EXPLORE_URL}{"slice_id": ${chartId}}`), ); -Cypress.Commands.add('visitChartByParams', params => { - const queryString = - typeof params === 'string' ? params : JSON.stringify(params); - const url = `${BASE_EXPLORE_URL}${queryString}`; - return cy.visit(url); -}); +Cypress.Commands.add( + 'visitChartByParams', + (formData: { + datasource?: string; + datasource_id?: number; + datasource_type?: string; + [key: string]: unknown; + }) => { + let datasource_id; + let datasource_type; + if (formData.datasource_id && formData.datasource_type) { + ({ datasource_id, datasource_type } = formData); + } else { + [datasource_id, datasource_type] = formData.datasource?.split('__') || []; + } + const accessToken = window.localStorage.getItem('access_token'); + cy.request({ + method: 'POST', + url: 'api/v1/explore/form_data', + body: { + datasource_id, + datasource_type, + form_data: JSON.stringify(formData), + }, + headers: { + ...(accessToken && { + Cookie: `csrf_access_token=${accessToken}`, + 'X-CSRFToken': accessToken, + }), + ...(TokenName && { Authorization: `Bearer ${TokenName}` }), + 'Content-Type': 'application/json', + Referer: `${Cypress.config().baseUrl}/`, + }, + }).then(response => { + const formDataKey = response.body.key; + const url = `/superset/explore/?form_data_key=${formDataKey}`; + cy.visit(url); + }); + }, +); Cypress.Commands.add('verifySliceContainer', chartSelector => { // After a wait response check for valid slice container diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 1de492d518a0c..5ae2f26c4f776 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -50,6 +50,14 @@ export type SharedControlComponents = typeof sharedControlComponents; /** ---------------------------------------------- * Input data/props while rendering * ---------------------------------------------*/ +export interface Owner { + first_name: string; + id: number; + last_name: string; + username: string; + email?: string; +} + export type ColumnMeta = Omit & { id?: number; } & AnyDict; @@ -67,8 +75,10 @@ export interface Dataset { time_grain_sqla?: string; granularity_sqla?: string; datasource_name: string | null; + name?: string; description: string | null; uid?: string; + owners?: Owner[]; } export interface ControlPanelState { diff --git a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx index 9a8366ba56bb2..4a87d52c186b8 100644 --- a/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx +++ b/superset-frontend/src/SqlLab/components/SaveDatasetModal/index.tsx @@ -46,8 +46,11 @@ import { SqlLabExploreRootState, getInitialState, ExploreDatasource, + SqlLabRootState, } from 'src/SqlLab/types'; -import { exploreChart } from 'src/explore/exploreUtils'; +import { mountExploreUrl } from 'src/explore/exploreUtils'; +import { postFormData } from 'src/explore/exploreUtils/formData'; +import { URL_PARAMS } from 'src/constants'; interface SaveDatasetModalProps { visible: boolean; @@ -115,6 +118,9 @@ export const SaveDatasetModal: FunctionComponent = ({ modalDescription, datasource, }) => { + const defaultVizType = useSelector( + state => state.common?.conf?.DEFAULT_VIZ_TYPE || 'table', + ); const query = datasource as QueryResponse; const getDefaultDatasetName = () => `${query.tab} ${moment().format('MM/DD/YYYY HH:mm:ss')}`; @@ -137,30 +143,40 @@ export const SaveDatasetModal: FunctionComponent = ({ const dispatch = useDispatch<(dispatch: any) => Promise>(); const handleOverwriteDataset = async () => { - await updateDataset( - query.dbId, - datasetToOverwrite.datasetId, - query.sql, - query.results.selected_columns.map( - (d: { name: string; type: string; is_dttm: boolean }) => ({ - column_name: d.name, - type: d.type, - is_dttm: d.is_dttm, - }), + const [, key] = await Promise.all([ + updateDataset( + query.dbId, + datasetToOverwrite.datasetId, + query.sql, + query.results.selected_columns.map( + (d: { name: string; type: string; is_dttm: boolean }) => ({ + column_name: d.name, + type: d.type, + is_dttm: d.is_dttm, + }), + ), + datasetToOverwrite.owners.map((o: DatasetOwner) => o.id), + true, ), - datasetToOverwrite.owners.map((o: DatasetOwner) => o.id), - true, - ); + postFormData(datasetToOverwrite.datasetId, 'table', { + ...EXPLORE_CHART_DEFAULT, + datasource: `${datasetToOverwrite.datasetId}__table`, + ...(defaultVizType === 'table' && { + all_columns: query.results.selected_columns.map( + column => column.name, + ), + }), + }), + ]); + + const url = mountExploreUrl(null, { + [URL_PARAMS.formDataKey.name]: key, + }); + window.open(url, '_blank', 'noreferrer'); setShouldOverwriteDataset(false); setDatasetToOverwrite({}); setDatasetName(getDefaultDatasetName()); - - exploreChart({ - ...EXPLORE_CHART_DEFAULT, - datasource: `${datasetToOverwrite.datasetId}__table`, - selected_columns: query.results.selected_columns, - }); }; const getUserDatasets = async (searchText = '') => { @@ -235,15 +251,20 @@ export const SaveDatasetModal: FunctionComponent = ({ columns: selectedColumns, }), ) - .then((data: { table_id: number }) => { - exploreChart({ + .then((data: { table_id: number }) => + postFormData(data.table_id, 'table', { + ...EXPLORE_CHART_DEFAULT, datasource: `${data.table_id}__table`, - metrics: [], - groupby: [], - time_range: 'No filter', - selectedColumns, - row_limit: 1000, + ...(defaultVizType === 'table' && { + all_columns: selectedColumns.map(column => column.name), + }), + }), + ) + .then((key: string) => { + const url = mountExploreUrl(null, { + [URL_PARAMS.formDataKey.name]: key, }); + window.open(url, '_blank', 'noreferrer'); }) .catch(() => { addDangerToast(t('An error occurred saving dataset')); diff --git a/superset-frontend/src/SqlLab/types.ts b/superset-frontend/src/SqlLab/types.ts index 9a6198b864b1f..1b7b1d495cd8c 100644 --- a/superset-frontend/src/SqlLab/types.ts +++ b/superset-frontend/src/SqlLab/types.ts @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ +import { Dataset } from '@superset-ui/chart-controls'; +import { JsonObject, Query, QueryResponse } from '@superset-ui/core'; import { SupersetError } from 'src/components/ErrorMessage/types'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { ToastType } from 'src/components/MessageToasts/types'; -import { Dataset } from '@superset-ui/chart-controls'; -import { Query, QueryResponse } from '@superset-ui/core'; import { ExploreRootState } from 'src/explore/types'; export type ExploreDatasource = Dataset | QueryResponse; @@ -68,7 +68,10 @@ export type SqlLabRootState = { }; localStorageUsageInKilobytes: number; messageToasts: toastState[]; - common: {}; + common: { + flash_messages: string[]; + conf: JsonObject; + }; }; export type SqlLabExploreRootState = SqlLabRootState | ExploreRootState; @@ -96,6 +99,7 @@ export const EXPLORE_CHART_DEFAULT = { metrics: [], groupby: [], time_range: 'No filter', + row_limit: 1000, }; export interface DatasetOwner { diff --git a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx index f05a349ad7b5a..bc58e925e43d5 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.test.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.test.tsx @@ -104,7 +104,7 @@ test('renders an enabled button if datasource and viz type are selected', async const wrapper = await getWrapper(); wrapper.setState({ datasource, - visType: 'table', + vizType: 'table', }); expect( wrapper.find(Button).find({ disabled: true }).hostNodes(), @@ -125,7 +125,7 @@ test('double-click viz type submits if datasource is selected', async () => { wrapper.update(); wrapper.setState({ datasource, - visType: 'table', + vizType: 'table', }); wrapper.instance().onVizTypeDoubleClick(); @@ -136,9 +136,8 @@ test('formats Explore url', async () => { const wrapper = await getWrapper(); wrapper.setState({ datasource, - visType: 'table', + vizType: 'table', }); - const formattedUrl = - '/superset/explore/?form_data=%7B%22viz_type%22%3A%22table%22%2C%22datasource%22%3A%221%22%7D'; + const formattedUrl = '/superset/explore/?viz_type=table&datasource=1'; expect(wrapper.instance().exploreUrl()).toBe(formattedUrl); }); diff --git a/superset-frontend/src/addSlice/AddSliceContainer.tsx b/superset-frontend/src/addSlice/AddSliceContainer.tsx index a99c75e2af4a5..71981fdef9a11 100644 --- a/superset-frontend/src/addSlice/AddSliceContainer.tsx +++ b/superset-frontend/src/addSlice/AddSliceContainer.tsx @@ -45,7 +45,7 @@ export type AddSliceContainerProps = { export type AddSliceContainerState = { datasource?: { label: string; value: string }; - visType: string | null; + vizType: string | null; canCreateDataset: boolean; }; @@ -208,7 +208,7 @@ export default class AddSliceContainer extends React.PureComponent< constructor(props: AddSliceContainerProps) { super(props); this.state = { - visType: null, + vizType: null, canCreateDataset: findPermission( 'can_write', 'Dataset', @@ -217,7 +217,7 @@ export default class AddSliceContainer extends React.PureComponent< }; this.changeDatasource = this.changeDatasource.bind(this); - this.changeVisType = this.changeVisType.bind(this); + this.changeVizType = this.changeVizType.bind(this); this.gotoSlice = this.gotoSlice.bind(this); this.newLabel = this.newLabel.bind(this); this.loadDatasources = this.loadDatasources.bind(this); @@ -226,14 +226,11 @@ export default class AddSliceContainer extends React.PureComponent< exploreUrl() { const dashboardId = getUrlParam(URL_PARAMS.dashboardId); - const formData = encodeURIComponent( - JSON.stringify({ - viz_type: this.state.visType, - datasource: this.state.datasource?.value, - ...(!isNullish(dashboardId) && { dashboardId }), - }), - ); - return `/superset/explore/?form_data=${formData}`; + let url = `/superset/explore/?viz_type=${this.state.vizType}&datasource=${this.state.datasource?.value}`; + if (!isNullish(dashboardId)) { + url += `&dashboard_id=${dashboardId}`; + } + return url; } gotoSlice() { @@ -244,12 +241,12 @@ export default class AddSliceContainer extends React.PureComponent< this.setState({ datasource }); } - changeVisType(visType: string | null) { - this.setState({ visType }); + changeVizType(vizType: string | null) { + this.setState({ vizType }); } isBtnDisabled() { - return !(this.state.datasource?.value && this.state.visType); + return !(this.state.datasource?.value && this.state.vizType); } onVizTypeDoubleClick() { @@ -369,14 +366,14 @@ export default class AddSliceContainer extends React.PureComponent< /> {t('Choose chart type')}} - status={this.state.visType ? 'finish' : 'process'} + status={this.state.vizType ? 'finish' : 'process'} description={ } diff --git a/superset-frontend/src/components/Chart/chartReducer.ts b/superset-frontend/src/components/Chart/chartReducer.ts index 010140584c2b1..11b498290f279 100644 --- a/superset-frontend/src/components/Chart/chartReducer.ts +++ b/superset-frontend/src/components/Chart/chartReducer.ts @@ -22,6 +22,7 @@ import { HYDRATE_DASHBOARD } from 'src/dashboard/actions/hydrate'; import { DatasourcesAction } from 'src/dashboard/actions/datasources'; import { ChartState } from 'src/explore/types'; import { getFormDataFromControls } from 'src/explore/controlUtils'; +import { HYDRATE_EXPLORE } from 'src/explore/actions/hydrateExplore'; import { now } from 'src/utils/dates'; import * as actions from './chartAction'; @@ -194,7 +195,7 @@ export default function chartReducer( delete charts[key]; return charts; } - if (action.type === HYDRATE_DASHBOARD) { + if (action.type === HYDRATE_DASHBOARD || action.type === HYDRATE_EXPLORE) { return { ...action.data.charts }; } if (action.type === DatasourcesAction.SET_DATASOURCES) { diff --git a/superset-frontend/src/constants.ts b/superset-frontend/src/constants.ts index 60668ddcb865d..84e809532ec7a 100644 --- a/superset-frontend/src/constants.ts +++ b/superset-frontend/src/constants.ts @@ -91,6 +91,10 @@ export const URL_PARAMS = { name: 'permalink_key', type: 'string', }, + vizType: { + name: 'viz_type', + type: 'string', + }, } as const; export const RESERVED_CHART_URL_PARAMS: string[] = [ diff --git a/superset-frontend/src/explore/App.jsx b/superset-frontend/src/explore/App.jsx index c440b784e7def..995cf3d9e91c8 100644 --- a/superset-frontend/src/explore/App.jsx +++ b/superset-frontend/src/explore/App.jsx @@ -27,10 +27,10 @@ import { DynamicPluginProvider } from 'src/components/DynamicPlugins'; import ToastContainer from 'src/components/MessageToasts/ToastContainer'; import setupApp from 'src/setup/setupApp'; import setupPlugins from 'src/setup/setupPlugins'; +import { theme } from 'src/preamble'; +import { ExplorePage } from './ExplorePage'; import './main.less'; import '../assets/stylesheets/reactable-pagination.less'; -import { theme } from 'src/preamble'; -import ExploreViewContainer from './components/ExploreViewContainer'; setupApp(); setupPlugins(); @@ -41,7 +41,7 @@ const App = ({ store }) => ( - + diff --git a/superset-frontend/src/explore/ExplorePage.tsx b/superset-frontend/src/explore/ExplorePage.tsx new file mode 100644 index 0000000000000..50982924eedff --- /dev/null +++ b/superset-frontend/src/explore/ExplorePage.tsx @@ -0,0 +1,68 @@ +/** + * 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 React, { useEffect, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { makeApi, t } from '@superset-ui/core'; +import Loading from 'src/components/Loading'; +import { getParsedExploreURLParams } from './exploreUtils/getParsedExploreURLParams'; +import { hydrateExplore } from './actions/hydrateExplore'; +import ExploreViewContainer from './components/ExploreViewContainer'; +import { ExploreResponsePayload } from './types'; +import { fallbackExploreInitialData } from './fixtures'; +import { addDangerToast } from '../components/MessageToasts/actions'; +import { isNullish } from '../utils/common'; + +const loadErrorMessage = t('Failed to load chart data.'); + +const fetchExploreData = () => { + const exploreUrlParams = getParsedExploreURLParams(); + return makeApi<{}, ExploreResponsePayload>({ + method: 'GET', + endpoint: 'api/v1/explore/', + })(exploreUrlParams); +}; + +export const ExplorePage = () => { + const [isLoaded, setIsLoaded] = useState(false); + const dispatch = useDispatch(); + + useEffect(() => { + fetchExploreData() + .then(({ result }) => { + if (isNullish(result.dataset?.id) && isNullish(result.dataset?.uid)) { + dispatch(hydrateExplore(fallbackExploreInitialData)); + dispatch(addDangerToast(loadErrorMessage)); + } else { + dispatch(hydrateExplore(result)); + } + }) + .catch(() => { + dispatch(hydrateExplore(fallbackExploreInitialData)); + dispatch(addDangerToast(loadErrorMessage)); + }) + .finally(() => { + setIsLoaded(true); + }); + }, [dispatch]); + + if (!isLoaded) { + return ; + } + return ; +}; diff --git a/superset-frontend/src/explore/actions/datasourcesActions.ts b/superset-frontend/src/explore/actions/datasourcesActions.ts index baf6f75ccc4c4..4fc3bce96a130 100644 --- a/superset-frontend/src/explore/actions/datasourcesActions.ts +++ b/superset-frontend/src/explore/actions/datasourcesActions.ts @@ -20,7 +20,7 @@ import { Dispatch } from 'redux'; import { Dataset } from '@superset-ui/chart-controls'; import { updateFormDataByDatasource } from './exploreActions'; -import { ExplorePageState } from '../reducers/getInitialState'; +import { ExplorePageState } from '../types'; export const SET_DATASOURCE = 'SET_DATASOURCE'; export interface SetDatasource { diff --git a/superset-frontend/src/explore/actions/hydrateExplore.test.ts b/superset-frontend/src/explore/actions/hydrateExplore.test.ts new file mode 100644 index 0000000000000..9cc7b883e928f --- /dev/null +++ b/superset-frontend/src/explore/actions/hydrateExplore.test.ts @@ -0,0 +1,92 @@ +/** + * 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 { hydrateExplore, HYDRATE_EXPLORE } from './hydrateExplore'; +import { exploreInitialData } from '../fixtures'; + +test('creates hydrate action from initial data', () => { + const dispatch = jest.fn(); + const getState = jest.fn(() => ({ + user: {}, + charts: {}, + datasources: {}, + common: {}, + explore: {}, + })); + // ignore type check - we dont need exact explore state for this test + // @ts-ignore + hydrateExplore(exploreInitialData)(dispatch, getState); + expect(dispatch).toHaveBeenCalledWith( + expect.objectContaining({ + type: HYDRATE_EXPLORE, + data: { + charts: { + 371: { + id: 371, + chartAlert: null, + chartStatus: null, + chartStackTrace: null, + chartUpdateEndTime: null, + chartUpdateStartTime: 0, + latestQueryFormData: { + cache_timeout: undefined, + datasource: '8__table', + slice_id: 371, + url_params: undefined, + viz_type: 'table', + }, + sliceFormData: { + cache_timeout: undefined, + datasource: '8__table', + slice_id: 371, + url_params: undefined, + viz_type: 'table', + }, + queryController: null, + queriesResponse: null, + triggerQuery: false, + lastRendered: 0, + }, + }, + datasources: { + '8__table': exploreInitialData.dataset, + }, + saveModal: { + dashboards: [], + saveModalAlert: null, + }, + explore: { + can_add: false, + can_download: false, + can_overwrite: false, + isDatasourceMetaLoading: false, + isStarred: false, + triggerRender: false, + datasource: exploreInitialData.dataset, + controls: expect.any(Object), + form_data: exploreInitialData.form_data, + slice: exploreInitialData.slice, + controlsTransferred: [], + standalone: null, + force: null, + }, + }, + }), + ); +}); diff --git a/superset-frontend/src/explore/actions/hydrateExplore.ts b/superset-frontend/src/explore/actions/hydrateExplore.ts new file mode 100644 index 0000000000000..ec49c0cdd5b05 --- /dev/null +++ b/superset-frontend/src/explore/actions/hydrateExplore.ts @@ -0,0 +1,146 @@ +/** + * 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 { ControlStateMapping } from '@superset-ui/chart-controls'; + +import { + ChartState, + ExplorePageInitialData, + ExplorePageState, +} from 'src/explore/types'; +import { getChartKey } from 'src/explore/exploreUtils'; +import { getControlsState } from 'src/explore/store'; +import { Dispatch } from 'redux'; +import { ensureIsArray } from '@superset-ui/core'; +import { + getFormDataFromControls, + applyMapStateToPropsToControl, +} from 'src/explore/controlUtils'; +import { getDatasourceUid } from 'src/utils/getDatasourceUid'; +import { getUrlParam } from 'src/utils/urlUtils'; +import { URL_PARAMS } from 'src/constants'; +import { findPermission } from 'src/utils/findPermission'; + +export const HYDRATE_EXPLORE = 'HYDRATE_EXPLORE'; +export const hydrateExplore = + ({ form_data, slice, dataset }: ExplorePageInitialData) => + (dispatch: Dispatch, getState: () => ExplorePageState) => { + const { user, datasources, charts, sliceEntities, common } = getState(); + + const sliceId = getUrlParam(URL_PARAMS.sliceId); + const dashboardId = getUrlParam(URL_PARAMS.dashboardId); + const fallbackSlice = sliceId ? sliceEntities?.slices?.[sliceId] : null; + const initialSlice = slice ?? fallbackSlice; + const initialFormData = form_data ?? initialSlice?.form_data; + if (!initialFormData.viz_type) { + const defaultVizType = common?.conf.DEFAULT_VIZ_TYPE || 'table'; + initialFormData.viz_type = + getUrlParam(URL_PARAMS.vizType) || defaultVizType; + } + if (dashboardId) { + initialFormData.dashboardId = dashboardId; + } + const initialDatasource = + datasources?.[initialFormData.datasource] ?? dataset; + + const initialExploreState = { + form_data: initialFormData, + slice: initialSlice, + datasource: initialDatasource, + }; + const initialControls = getControlsState( + initialExploreState, + initialFormData, + ) as ControlStateMapping; + + const exploreState = { + // note this will add `form_data` to state, + // which will be manipulable by future reducers. + can_add: findPermission('can_write', 'Chart', user?.roles), + can_download: findPermission('can_csv', 'Superset', user?.roles), + can_overwrite: ensureIsArray(slice?.owners).includes( + user?.userId as number, + ), + isDatasourceMetaLoading: false, + isStarred: false, + triggerRender: false, + // duplicate datasource in exploreState - it's needed by getControlsState + datasource: initialDatasource, + // Initial control state will skip `control.mapStateToProps` + // because `bootstrapData.controls` is undefined. + controls: initialControls, + form_data: initialFormData, + slice: initialSlice, + controlsTransferred: [], + standalone: getUrlParam(URL_PARAMS.standalone), + force: getUrlParam(URL_PARAMS.force), + }; + + // apply initial mapStateToProps for all controls, must execute AFTER + // bootstrapState has initialized `controls`. Order of execution is not + // guaranteed, so controls shouldn't rely on each other's mapped state. + Object.entries(exploreState.controls).forEach(([key, controlState]) => { + exploreState.controls[key] = applyMapStateToPropsToControl( + controlState, + exploreState, + ); + }); + const sliceFormData = initialSlice + ? getFormDataFromControls(initialControls) + : null; + + const chartKey: number = getChartKey(initialExploreState); + const chart: ChartState = { + id: chartKey, + chartAlert: null, + chartStatus: null, + chartStackTrace: null, + chartUpdateEndTime: null, + chartUpdateStartTime: 0, + latestQueryFormData: getFormDataFromControls(exploreState.controls), + sliceFormData, + queryController: null, + queriesResponse: null, + triggerQuery: false, + lastRendered: 0, + }; + + return dispatch({ + type: HYDRATE_EXPLORE, + data: { + charts: { + ...charts, + [chartKey]: chart, + }, + datasources: { + ...datasources, + [getDatasourceUid(initialDatasource)]: initialDatasource, + }, + saveModal: { + dashboards: [], + saveModalAlert: null, + }, + explore: exploreState, + }, + }); + }; + +export type HydrateExplore = { + type: typeof HYDRATE_EXPLORE; + data: ExplorePageState; +}; diff --git a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx index a02917e2fcc5c..4160fbbd891cb 100644 --- a/superset-frontend/src/explore/components/ControlPanelsContainer.tsx +++ b/superset-frontend/src/explore/components/ControlPanelsContainer.tsx @@ -54,8 +54,7 @@ import Loading from 'src/components/Loading'; import { usePrevious } from 'src/hooks/usePrevious'; import { getSectionsToRender } from 'src/explore/controlUtils'; import { ExploreActions } from 'src/explore/actions/exploreActions'; -import { ExplorePageState } from 'src/explore/reducers/getInitialState'; -import { ChartState } from 'src/explore/types'; +import { ChartState, ExplorePageState } from 'src/explore/types'; import { Tooltip } from 'src/components/Tooltip'; import { rgba } from 'emotion-rgba'; diff --git a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx index 4b19c5b2f3818..a9495175d9b91 100644 --- a/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx +++ b/superset-frontend/src/explore/components/DatasourcePanel/DatasourcePanel.test.tsx @@ -47,7 +47,9 @@ const datasource = { main_dttm_col: 'None', datasource_name: 'table1', description: 'desc', - owners: [{ username: 'admin', userId: 1 }], + owners: [ + { first_name: 'admin', last_name: 'admin', username: 'admin', id: 1 }, + ], }; const mockUser = { diff --git a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx index d8d2662371587..0155e3c37822b 100644 --- a/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx +++ b/superset-frontend/src/explore/components/ExploreViewContainer/index.jsx @@ -694,17 +694,9 @@ function ExploreViewContainer(props) { ExploreViewContainer.propTypes = propTypes; function mapStateToProps(state) { - const { - explore, - charts, - common, - impressionId, - dataMask, - reports, - datasources, - user, - } = state; - const { controls, slice } = explore; + const { explore, charts, common, impressionId, dataMask, reports, user } = + state; + const { controls, slice, datasource } = explore; const form_data = getFormDataFromControls(controls); const slice_id = form_data.slice_id ?? slice?.slice_id ?? 0; // 0 - unsaved chart form_data.extra_form_data = mergeExtraFormData( @@ -720,8 +712,6 @@ function mapStateToProps(state) { dashboardId = undefined; } - const datasource = datasources[form_data.datasource]; - return { isDatasourceMetaLoading: explore.isDatasourceMetaLoading, datasource, diff --git a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx index 3e559c4bbfb04..254ebe090254b 100644 --- a/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx +++ b/superset-frontend/src/explore/components/controls/DatasourceControl/index.jsx @@ -237,7 +237,7 @@ class DatasourceControl extends React.PureComponent { const isSqlSupported = datasource.type === 'table'; const { user } = this.props; const allowEdit = datasource.owners - .map(o => o.id || o.value) + ?.map(o => o.id || o.value) .includes(user.userId); isUserAdmin(user); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx index ce19ba2fa47d8..c31b632b5fd14 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/FastVizSwitcher.tsx @@ -29,8 +29,8 @@ import { css, SupersetTheme, t, useTheme } from '@superset-ui/core'; import { usePluginContext } from 'src/components/DynamicPlugins'; import { Tooltip } from 'src/components/Tooltip'; import Icons from 'src/components/Icons'; -import { ExplorePageState } from 'src/explore/reducers/getInitialState'; import { getChartKey } from 'src/explore/exploreUtils'; +import { ExplorePageState } from 'src/explore/types'; export interface VizMeta { icon: ReactElement; diff --git a/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts new file mode 100644 index 0000000000000..8d5a8ee09c16e --- /dev/null +++ b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.test.ts @@ -0,0 +1,62 @@ +/** + * 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 { getParsedExploreURLParams } from './getParsedExploreURLParams'; + +const EXPLORE_BASE_URL = 'http://localhost:9000/superset/explore/'; +const setupLocation = (newUrl: string) => { + delete (window as any).location; + // @ts-ignore + window.location = new URL(newUrl); +}; + +test('get form_data_key and slice_id from search params - url when moving from dashboard to explore', () => { + setupLocation( + `${EXPLORE_BASE_URL}?form_data_key=yrLXmyE9fmhQ11lM1KgaD1PoPSBpuLZIJfqdyIdw9GoBwhPFRZHeIgeFiNZljbpd&slice_id=56`, + ); + expect(getParsedExploreURLParams().toString()).toEqual( + 'slice_id=56&form_data_key=yrLXmyE9fmhQ11lM1KgaD1PoPSBpuLZIJfqdyIdw9GoBwhPFRZHeIgeFiNZljbpd', + ); +}); + +test('get slice_id from form_data search param - url on Chart List', () => { + setupLocation(`${EXPLORE_BASE_URL}?form_data=%7B%22slice_id%22%3A%2056%7D`); + expect(getParsedExploreURLParams().toString()).toEqual('slice_id=56'); +}); + +test('get datasource and viz type from form_data search param - url when creating new chart', () => { + setupLocation( + `${EXPLORE_BASE_URL}?form_data=%7B%22viz_type%22%3A%22big_number%22%2C%22datasource%22%3A%222__table%22%7D`, + ); + expect(getParsedExploreURLParams().toString()).toEqual( + 'viz_type=big_number&dataset_id=2&dataset_type=table', + ); +}); + +test('get permalink key from path params', () => { + setupLocation(`${EXPLORE_BASE_URL}p/kpOqweaMY9R/`); + expect(getParsedExploreURLParams().toString()).toEqual( + 'permalink_key=kpOqweaMY9R', + ); +}); + +test('get dataset id from path params', () => { + setupLocation(`${EXPLORE_BASE_URL}table/42/`); + expect(getParsedExploreURLParams().toString()).toEqual('dataset_id=42'); +}); diff --git a/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.ts b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.ts new file mode 100644 index 0000000000000..042b8f7f88766 --- /dev/null +++ b/superset-frontend/src/explore/exploreUtils/getParsedExploreURLParams.ts @@ -0,0 +1,117 @@ +/** + * 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. + */ + +// mapping { url_param: v1_explore_request_param } +const EXPLORE_URL_SEARCH_PARAMS = { + form_data: { + name: 'form_data', + parser: (formData: string) => { + const formDataObject = JSON.parse(formData); + if (formDataObject.datasource) { + const [dataset_id, dataset_type] = + formDataObject.datasource.split('__'); + formDataObject.dataset_id = dataset_id; + formDataObject.dataset_type = dataset_type; + delete formDataObject.datasource; + } + return formDataObject; + }, + }, + slice_id: { + name: 'slice_id', + }, + dataset_id: { + name: 'dataset_id', + }, + dataset_type: { + name: 'dataset_type', + }, + datasource: { + name: 'datasource', + parser: (datasource: string) => { + const [dataset_id, dataset_type] = datasource.split('__'); + return { dataset_id, dataset_type }; + }, + }, + form_data_key: { + name: 'form_data_key', + }, + permalink_key: { + name: 'permalink_key', + }, + viz_type: { + name: 'viz_type', + }, + dashboard_id: { + name: 'dashboard_id', + }, +}; + +const EXPLORE_URL_PATH_PARAMS = { + p: 'permalink_key', // permalink + table: 'dataset_id', +}; + +// search params can be placed in form_data object +// we need to "flatten" the search params to use them with /v1/explore endpoint +const getParsedExploreURLSearchParams = () => { + const urlSearchParams = new URLSearchParams(window.location.search); + return Object.keys(EXPLORE_URL_SEARCH_PARAMS).reduce((acc, currentParam) => { + const paramValue = urlSearchParams.get(currentParam); + if (paramValue === null) { + return acc; + } + let parsedParamValue; + try { + parsedParamValue = + EXPLORE_URL_SEARCH_PARAMS[currentParam].parser?.(paramValue) ?? + paramValue; + } catch { + parsedParamValue = paramValue; + } + if (typeof parsedParamValue === 'object') { + return { ...acc, ...parsedParamValue }; + } + return { + ...acc, + [EXPLORE_URL_SEARCH_PARAMS[currentParam].name]: parsedParamValue, + }; + }, {}); +}; + +// path params need to be transformed to search params to use them with /v1/explore endpoint +const getParsedExploreURLPathParams = () => + Object.keys(EXPLORE_URL_PATH_PARAMS).reduce((acc, currentParam) => { + const re = new RegExp(`/(${currentParam})/(\\w+)`); + const pathGroups = window.location.pathname.match(re); + if (pathGroups && pathGroups[2]) { + return { ...acc, [EXPLORE_URL_PATH_PARAMS[currentParam]]: pathGroups[2] }; + } + return acc; + }, {}); + +export const getParsedExploreURLParams = () => + new URLSearchParams( + Object.entries({ + ...getParsedExploreURLSearchParams(), + ...getParsedExploreURLPathParams(), + }) + .map(entry => entry.join('=')) + .join('&'), + ); diff --git a/superset-frontend/src/explore/exploreUtils/index.js b/superset-frontend/src/explore/exploreUtils/index.js index 73d1fe088baaa..506de032e5d73 100644 --- a/superset-frontend/src/explore/exploreUtils/index.js +++ b/superset-frontend/src/explore/exploreUtils/index.js @@ -267,11 +267,12 @@ export const exportChart = ({ SupersetClient.postForm(url, { form_data: safeStringify(payload) }); }; -export const exploreChart = formData => { +export const exploreChart = (formData, requestParams) => { const url = getExploreUrl({ formData, endpointType: 'base', allowDomainSharding: false, + requestParams, }); SupersetClient.postForm(url, { form_data: safeStringify(formData) }); }; diff --git a/superset-frontend/src/explore/fixtures.tsx b/superset-frontend/src/explore/fixtures.tsx index 78579b82e49c3..755985be2bda9 100644 --- a/superset-frontend/src/explore/fixtures.tsx +++ b/superset-frontend/src/explore/fixtures.tsx @@ -18,13 +18,14 @@ */ import React from 'react'; -import { t } from '@superset-ui/core'; +import { DatasourceType, t } from '@superset-ui/core'; import { ColumnMeta, ColumnOption, ControlConfig, ControlPanelSectionConfig, } from '@superset-ui/chart-controls'; +import { ExplorePageInitialData } from './types'; export const controlPanelSectionsChartOptions: (ControlPanelSectionConfig | null)[] = [ @@ -108,3 +109,59 @@ export const controlPanelSectionsChartOptionsTable: ControlPanelSectionConfig[] ], }, ]; + +export const exploreInitialData: ExplorePageInitialData = { + form_data: { + datasource: '8__table', + metric: 'count', + slice_id: 371, + time_range: 'No filter', + viz_type: 'table', + }, + slice: { + cache_timeout: null, + description: null, + slice_id: 371, + slice_name: 'Age distribution of respondents', + is_managed_externally: false, + form_data: { + datasource: '8__table', + metric: 'count', + slice_id: 371, + time_range: 'No filter', + viz_type: 'table', + }, + }, + dataset: { + id: 8, + type: DatasourceType.Table, + columns: [{ column_name: 'a' }], + metrics: [{ metric_name: 'first' }, { metric_name: 'second' }], + column_format: {}, + verbose_map: {}, + main_dttm_col: '', + datasource_name: '8__table', + description: null, + }, +}; + +export const fallbackExploreInitialData: ExplorePageInitialData = { + form_data: { + datasource: '0__table', + viz_type: 'table', + }, + dataset: { + id: 0, + type: DatasourceType.Table, + columns: [], + metrics: [], + column_format: {}, + verbose_map: {}, + main_dttm_col: '', + owners: [], + datasource_name: 'missing_datasource', + name: 'missing_datasource', + description: null, + }, + slice: null, +}; diff --git a/superset-frontend/src/explore/index.jsx b/superset-frontend/src/explore/index.jsx index cc99666e02f5b..0af6b02747e5f 100644 --- a/superset-frontend/src/explore/index.jsx +++ b/superset-frontend/src/explore/index.jsx @@ -20,10 +20,11 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { createStore, applyMiddleware, compose } from 'redux'; import thunk from 'redux-thunk'; -import logger from '../middleware/loggerMiddleware'; -import { initFeatureFlags } from '../featureFlags'; -import { initEnhancer } from '../reduxUtils'; -import getInitialState from './reducers/getInitialState'; +import shortid from 'shortid'; +import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages'; +import logger from 'src/middleware/loggerMiddleware'; +import { initFeatureFlags } from 'src/featureFlags'; +import { initEnhancer } from 'src/reduxUtils'; import rootReducer from './reducers/index'; import App from './App'; @@ -31,11 +32,18 @@ const exploreViewContainer = document.getElementById('app'); const bootstrapData = JSON.parse( exploreViewContainer.getAttribute('data-bootstrap'), ); -initFeatureFlags(bootstrapData.common.feature_flags); -const initState = getInitialState(bootstrapData); + +const user = { ...bootstrapData.user }; +const common = { ...bootstrapData.common }; +initFeatureFlags(common.feature_flags); const store = createStore( rootReducer, - initState, + { + user, + common, + impressionId: shortid.generate(), + messageToasts: getToastsFromPyFlashMessages(common?.flash_messages || []), + }, compose(applyMiddleware(thunk, logger), initEnhancer(false)), ); diff --git a/superset-frontend/src/explore/reducers/datasourcesReducer.ts b/superset-frontend/src/explore/reducers/datasourcesReducer.ts index e3050feea016d..50393dbd24507 100644 --- a/superset-frontend/src/explore/reducers/datasourcesReducer.ts +++ b/superset-frontend/src/explore/reducers/datasourcesReducer.ts @@ -22,11 +22,12 @@ import { AnyDatasourcesAction, SET_DATASOURCE, } from '../actions/datasourcesActions'; +import { HYDRATE_EXPLORE, HydrateExplore } from '../actions/hydrateExplore'; export default function datasourcesReducer( // TODO: change type to include other datasource types datasources: { [key: string]: Dataset }, - action: AnyDatasourcesAction, + action: AnyDatasourcesAction | HydrateExplore, ) { if (action.type === SET_DATASOURCE) { return { @@ -34,5 +35,8 @@ export default function datasourcesReducer( [getDatasourceUid(action.datasource)]: action.datasource, }; } + if (action.type === HYDRATE_EXPLORE) { + return { ...(action as HydrateExplore).data.datasources }; + } return datasources || {}; } diff --git a/superset-frontend/src/explore/reducers/exploreReducer.js b/superset-frontend/src/explore/reducers/exploreReducer.js index 706104b765ce7..f99ab9437c435 100644 --- a/superset-frontend/src/explore/reducers/exploreReducer.js +++ b/superset-frontend/src/explore/reducers/exploreReducer.js @@ -28,6 +28,7 @@ import { StandardizedFormData, } from 'src/explore/controlUtils'; import * as actions from 'src/explore/actions/exploreActions'; +import { HYDRATE_EXPLORE } from '../actions/hydrateExplore'; export default function exploreReducer(state = {}, action) { const actionHandlers = { @@ -247,8 +248,12 @@ export default function exploreReducer(state = {}, action) { force: action.force, }; }, + [HYDRATE_EXPLORE]() { + return { + ...action.data.explore, + }; + }, }; - if (action.type in actionHandlers) { return actionHandlers[action.type](); } diff --git a/superset-frontend/src/explore/reducers/getInitialState.ts b/superset-frontend/src/explore/reducers/getInitialState.ts deleted file mode 100644 index 4d4970237eaa4..0000000000000 --- a/superset-frontend/src/explore/reducers/getInitialState.ts +++ /dev/null @@ -1,146 +0,0 @@ -/** - * 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 shortid from 'shortid'; -import { - DatasourceType, - ensureIsArray, - JsonObject, - QueryFormData, -} from '@superset-ui/core'; -import { ControlStateMapping, Dataset } from '@superset-ui/chart-controls'; -import { - CommonBootstrapData, - UserWithPermissionsAndRoles, -} from 'src/types/bootstrapTypes'; -import getToastsFromPyFlashMessages from 'src/components/MessageToasts/getToastsFromPyFlashMessages'; - -import { ChartState, Slice } from 'src/explore/types'; -import { getChartKey } from 'src/explore/exploreUtils'; -import { getControlsState } from 'src/explore/store'; -import { - getFormDataFromControls, - applyMapStateToPropsToControl, -} from 'src/explore/controlUtils'; -import { findPermission } from 'src/utils/findPermission'; -import { getDatasourceUid } from 'src/utils/getDatasourceUid'; -import { getUrlParam } from 'src/utils/urlUtils'; -import { URL_PARAMS } from 'src/constants'; - -export interface ExplorePageBootstrapData extends JsonObject { - can_add: boolean; - can_download: boolean; - can_overwrite: boolean; - common: CommonBootstrapData; - datasource: Dataset; - datasource_id: number; - datasource_type: DatasourceType; - forced_height: string | null; - form_data: QueryFormData; - slice: Slice | null; - standalone: boolean; - force: boolean; - user: UserWithPermissionsAndRoles; -} - -export default function getInitialState( - bootstrapData: ExplorePageBootstrapData, -) { - const { - form_data: initialFormData, - common, - user, - datasource, - slice, - } = bootstrapData; - - const exploreState = { - // note this will add `form_data` to state, - // which will be manipulatable by future reducers. - can_add: findPermission('can_write', 'Chart', user?.roles), - can_download: findPermission('can_csv', 'Superset', user?.roles), - can_overwrite: ensureIsArray(slice?.owners).includes( - user?.userId as number, - ), - isDatasourceMetaLoading: false, - isStarred: false, - triggerRender: false, - // duplicate datasource in exploreState - it's needed by getControlsState - datasource, - // Initial control state will skip `control.mapStateToProps` - // because `bootstrapData.controls` is undefined. - controls: getControlsState( - bootstrapData, - initialFormData, - ) as ControlStateMapping, - form_data: initialFormData, - slice, - controlsTransferred: [], - standalone: getUrlParam(URL_PARAMS.standalone), - force: getUrlParam(URL_PARAMS.force), - }; - - // apply initial mapStateToProps for all controls, must execute AFTER - // bootstrapState has initialized `controls`. Order of execution is not - // guaranteed, so controls shouldn't rely on each other's mapped state. - Object.entries(exploreState.controls).forEach(([key, controlState]) => { - exploreState.controls[key] = applyMapStateToPropsToControl( - controlState, - exploreState, - ); - }); - const sliceFormData = slice - ? getFormDataFromControls(getControlsState(bootstrapData, slice.form_data)) - : null; - - const chartKey: number = getChartKey(bootstrapData); - const chart: ChartState = { - id: chartKey, - chartAlert: null, - chartStatus: null, - chartStackTrace: null, - chartUpdateEndTime: null, - chartUpdateStartTime: 0, - latestQueryFormData: getFormDataFromControls(exploreState.controls), - sliceFormData, - queryController: null, - queriesResponse: null, - triggerQuery: false, - lastRendered: 0, - }; - - return { - common: common || {}, - user: user || {}, - charts: { - [chartKey]: chart, - }, - datasources: { [getDatasourceUid(datasource)]: datasource }, - saveModal: { - dashboards: [], - saveModalAlert: null, - }, - explore: exploreState, - impressionId: shortid.generate(), - messageToasts: getToastsFromPyFlashMessages( - (bootstrapData.common || {}).flash_messages || [], - ), - }; -} - -export type ExplorePageState = ReturnType; diff --git a/superset-frontend/src/explore/reducers/saveModalReducer.js b/superset-frontend/src/explore/reducers/saveModalReducer.js index eee4197991189..85d4cf0e32468 100644 --- a/superset-frontend/src/explore/reducers/saveModalReducer.js +++ b/superset-frontend/src/explore/reducers/saveModalReducer.js @@ -18,6 +18,7 @@ */ /* eslint camelcase: 0 */ import * as actions from '../actions/saveModalActions'; +import { HYDRATE_EXPLORE } from '../actions/hydrateExplore'; export default function saveModalReducer(state = {}, action) { const actionHandlers = { @@ -39,6 +40,9 @@ export default function saveModalReducer(state = {}, action) { [actions.REMOVE_SAVE_MODAL_ALERT]() { return { ...state, saveModalAlert: null }; }, + [HYDRATE_EXPLORE]() { + return { ...action.data.saveModal }; + }, }; if (action.type in actionHandlers) { diff --git a/superset-frontend/src/explore/store.js b/superset-frontend/src/explore/store.js index 80ad75e3e5bdf..8bd0477087088 100644 --- a/superset-frontend/src/explore/store.js +++ b/superset-frontend/src/explore/store.js @@ -42,7 +42,7 @@ export function getControlsState(state, inputFormData) { // Getting a list of active control names for the current viz const formData = { ...inputFormData }; const vizType = - formData.viz_type || state.common.conf.DEFAULT_VIZ_TYPE || 'table'; + formData.viz_type || state.common?.conf.DEFAULT_VIZ_TYPE || 'table'; handleDeprecatedControls(formData); diff --git a/superset-frontend/src/explore/types.ts b/superset-frontend/src/explore/types.ts index 4d50b449c500f..85b161058aa6e 100644 --- a/superset-frontend/src/explore/types.ts +++ b/superset-frontend/src/explore/types.ts @@ -21,13 +21,17 @@ import { QueryFormData, AnnotationData, AdhocMetric, + JsonObject, } from '@superset-ui/core'; -import { ColumnMeta, Dataset } from '@superset-ui/chart-controls'; +import { + ColumnMeta, + ControlStateMapping, + Dataset, +} from '@superset-ui/chart-controls'; import { DatabaseObject } from 'src/views/CRUD/types'; import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; import { toastState } from 'src/SqlLab/types'; - -export { Slice, Chart } from 'src/types/Chart'; +import { Slice } from 'src/types/Chart'; export type ChartStatus = | 'loading' @@ -90,3 +94,40 @@ export type ExploreRootState = { messageToasts: toastState[]; common: {}; }; + +export interface ExplorePageInitialData { + dataset: Dataset; + form_data: QueryFormData; + slice: Slice | null; +} + +export interface ExploreResponsePayload { + result: ExplorePageInitialData & { message: string }; +} + +export interface ExplorePageState { + user: UserWithPermissionsAndRoles; + common: { + flash_messages: string[]; + conf: JsonObject; + }; + charts: { [key: number]: ChartState }; + datasources: { [key: string]: Dataset }; + explore: { + can_add: boolean; + can_download: boolean; + can_overwrite: boolean; + isDatasourceMetaLoading: boolean; + isStarred: boolean; + triggerRender: boolean; + // duplicate datasource in exploreState - it's needed by getControlsState + datasource: Dataset; + controls: ControlStateMapping; + form_data: QueryFormData; + slice: Slice; + controlsTransferred: string[]; + standalone: boolean; + force: boolean; + }; + sliceEntities?: JsonObject; // propagated from Dashboard view +}