This is an automated email from the ASF dual-hosted git repository. graceguo pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-superset.git
The following commit(s) were added to refs/heads/master by this push: new 6ec9e1a [Ready][Dashboard] disable force refresh when chart is still loading (#6034) 6ec9e1a is described below commit 6ec9e1a4e94beb1d6a1f49e25569473ba99715c1 Author: Grace Guo <grace....@airbnb.com> AuthorDate: Wed Oct 24 10:21:23 2018 -0700 [Ready][Dashboard] disable force refresh when chart is still loading (#6034) * [Dashboard] disable force refresh when chart is still loading - disable force refresh for chart, when chart is still loading - disable force refresh for dashboard, when any one of chart is loading * remove css change (introduced by mistake) * add integration tests for disable/enable force refresh --- .../cypress/integration/dashboard/controls.js | 99 ++++++++++++---------- .../cypress/integration/dashboard/fav_star.js | 35 ++++++++ .../cypress/integration/dashboard/index.test.js | 2 + superset/assets/src/chart/chartReducer.js | 2 +- .../assets/src/dashboard/components/Header.jsx | 8 +- .../dashboard/components/HeaderActionsDropdown.jsx | 4 +- .../dashboard/components/SliceHeaderControls.jsx | 14 +-- .../src/dashboard/containers/DashboardHeader.jsx | 2 + .../src/dashboard/util/isDashboardLoading.js | 5 ++ .../assets/src/explore/reducers/getInitialState.js | 3 +- 10 files changed, 120 insertions(+), 54 deletions(-) diff --git a/superset/assets/cypress/integration/dashboard/controls.js b/superset/assets/cypress/integration/dashboard/controls.js index b79abf8..0f163ee 100644 --- a/superset/assets/cypress/integration/dashboard/controls.js +++ b/superset/assets/cypress/integration/dashboard/controls.js @@ -1,71 +1,84 @@ -import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper'; +import { WORLD_HEALTH_DASHBOARD } from './dashboard.helper'; import readResponseBlob from '../../utils/readResponseBlob'; export default () => describe('top-level controls', () => { - let sliceIds = []; - let dashboard = {}; - let isFavoriteDashboard = false; + const sliceRequests = []; + const forceRefreshRequests = []; + let mapId; beforeEach(() => { cy.server(); cy.login(); - - cy.route(CHECK_DASHBOARD_FAVORITE_ENDPOINT).as('countFavStar'); cy.visit(WORLD_HEALTH_DASHBOARD); cy.get('#app').then((data) => { const bootstrapData = JSON.parse(data[0].dataset.bootstrap); - dashboard = bootstrapData.dashboard_data; - sliceIds = dashboard.slices.map(slice => (slice.slice_id)); - }); + const dashboard = bootstrapData.dashboard_data; + const sliceIds = dashboard.slices.map(slice => (slice.slice_id)); + mapId = dashboard.slices.find(slice => (slice.form_data.viz_type === 'world_map')).slice_id; - cy.wait('@countFavStar').then((xhr) => { - isFavoriteDashboard = xhr.response.body.count === 1; + sliceIds + .forEach((id) => { + const sliceRequest = `getJson_${id}`; + sliceRequests.push(`@${sliceRequest}`); + cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}`).as(sliceRequest); + + const forceRefresh = `getJson_${id}_force`; + forceRefreshRequests.push(`@${forceRefresh}`); + cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}&force=true`).as(forceRefresh); + }); }); }); + afterEach(() => { + sliceRequests.length = 0; + forceRefreshRequests.length = 0; + }); - it('should allow favor/unfavor', () => { - if (!isFavoriteDashboard) { - cy.get('.favstar').find('i').should('have.class', 'fa-star-o'); - cy.get('.favstar').trigger('click'); - cy.get('.favstar').find('i').should('have.class', 'fa-star') - .and('not.have.class', 'fa-star-o'); - } else { - cy.get('.favstar').find('i').should('have.class', 'fa-star') - .and('not.have.class', 'fa-star-o'); - cy.get('.favstar').trigger('click'); - cy.get('.fave-unfave-icon').find('i').should('have.class', 'fa-star-o') - .and('not.have.class', 'fa-star'); - } + it('should allow chart level refresh', () => { + cy.wait(sliceRequests); + cy.get('.grid-container .world_map').should('be.exist'); + cy.get(`#slice_${mapId}-controls`).click(); + cy.get(`#slice_${mapId}-controls`).next() + .find('.refresh-tooltip').trigger('click', { force: true }); - // reset to original fav state - cy.get('.favstar').trigger('click'); - }); + // not allow dashboard level force refresh when any chart is loading + cy.get('#save-dash-split-button').trigger('click', { forece: true }); + cy.contains('Force refresh dashboard').parent().should('have.class', 'disabled'); + // not allow chart level force refresh when it is loading + cy.get(`#slice_${mapId}-controls`).next() + .find('.refresh-tooltip') + .parent() + .parent() + .should('have.class', 'disabled'); - it('should allow auto refresh', () => { - const sliceRequests = []; - const forceRefreshRequests = []; - sliceIds - .forEach((id) => { - const sliceRequest = `getJson_${id}`; - sliceRequests.push(`@${sliceRequest}`); - cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}`).as(sliceRequest); + cy.wait(`@getJson_${mapId}_force`); + cy.get('#save-dash-split-button').trigger('click'); + cy.contains('Force refresh dashboard').parent().not('have.class', 'disabled'); + }); - const forceRefresh = `getJson_${id}_force`; - forceRefreshRequests.push(`@${forceRefresh}`); - cy.route('POST', `/superset/explore_json/?form_data={"slice_id":${id}}&force=true`).as(forceRefresh); - }); + it('should allow dashboard level force refresh', () => { + // should not show dashobard level force refresh + cy.get('#save-dash-split-button').trigger('click'); + cy.contains('Force refresh dashboard').parent().should('have.class', 'disabled'); + // wait the all dash finish loading. cy.wait(sliceRequests); cy.get('#save-dash-split-button').trigger('click'); - cy.contains('Force refresh dashboard').click(); + cy.contains('Force refresh dashboard').trigger('click', { force: true }); + cy.get('#save-dash-split-button').trigger('click'); + cy.contains('Force refresh dashboard').parent().should('have.class', 'disabled'); + // wait all charts force refreshed cy.wait(forceRefreshRequests).then((xhrs) => { // is_cached in response should be false - xhrs.forEach(async (xhr) => { - const responseBody = await readResponseBlob(xhr.response.body); - expect(responseBody.is_cached).to.equal(false); + xhrs.forEach((xhr) => { + readResponseBlob(xhr.response.body).then((responseBody) => { + expect(responseBody.is_cached).to.equal(false); + }); }); }); + + cy.get('#save-dash-split-button').trigger('click'); + cy.contains('Force refresh dashboard').parent().not('have.class', 'disabled'); }); }); diff --git a/superset/assets/cypress/integration/dashboard/fav_star.js b/superset/assets/cypress/integration/dashboard/fav_star.js new file mode 100644 index 0000000..bb5957a --- /dev/null +++ b/superset/assets/cypress/integration/dashboard/fav_star.js @@ -0,0 +1,35 @@ +import { WORLD_HEALTH_DASHBOARD, CHECK_DASHBOARD_FAVORITE_ENDPOINT } from './dashboard.helper'; + +export default () => describe('favorite dashboard', () => { + let isFavoriteDashboard = false; + + beforeEach(() => { + cy.server(); + cy.login(); + + cy.route(CHECK_DASHBOARD_FAVORITE_ENDPOINT).as('countFavStar'); + cy.visit(WORLD_HEALTH_DASHBOARD); + + cy.wait('@countFavStar').then((xhr) => { + isFavoriteDashboard = xhr.response.body.count === 1; + }); + }); + + it('should allow favor/unfavor', () => { + if (!isFavoriteDashboard) { + cy.get('.favstar').find('i').should('have.class', 'fa-star-o'); + cy.get('.favstar').trigger('click'); + cy.get('.favstar').find('i').should('have.class', 'fa-star') + .and('not.have.class', 'fa-star-o'); + } else { + cy.get('.favstar').find('i').should('have.class', 'fa-star') + .and('not.have.class', 'fa-star-o'); + cy.get('.favstar').trigger('click'); + cy.get('.fave-unfave-icon').find('i').should('have.class', 'fa-star-o') + .and('not.have.class', 'fa-star'); + } + + // reset to original fav state + cy.get('.favstar').trigger('click'); + }); +}); diff --git a/superset/assets/cypress/integration/dashboard/index.test.js b/superset/assets/cypress/integration/dashboard/index.test.js index 664e3ac..5db178d 100644 --- a/superset/assets/cypress/integration/dashboard/index.test.js +++ b/superset/assets/cypress/integration/dashboard/index.test.js @@ -1,11 +1,13 @@ import DashboardControlsTest from './controls'; import DashboardEditModeTest from './edit_mode'; +import DashboardFavStarTest from './fav_star'; import DashboardFilterTest from './filter'; import DashboardLoadTest from './load'; describe('Dashboard', () => { DashboardControlsTest(); DashboardEditModeTest(); + DashboardFavStarTest(); DashboardFilterTest(); DashboardLoadTest(); }); diff --git a/superset/assets/src/chart/chartReducer.js b/superset/assets/src/chart/chartReducer.js index 5563d5c..5f9a421 100644 --- a/superset/assets/src/chart/chartReducer.js +++ b/superset/assets/src/chart/chartReducer.js @@ -8,7 +8,7 @@ export const chart = { chartAlert: null, chartStatus: 'loading', chartUpdateEndTime: null, - chartUpdateStartTime: now(), + chartUpdateStartTime: 0, latestQueryFormData: {}, queryRequest: null, queryResponse: null, diff --git a/superset/assets/src/dashboard/components/Header.jsx b/superset/assets/src/dashboard/components/Header.jsx index dd2af7f..1dfe846 100644 --- a/superset/assets/src/dashboard/components/Header.jsx +++ b/superset/assets/src/dashboard/components/Header.jsx @@ -28,6 +28,7 @@ const propTypes = { expandedSlices: PropTypes.object.isRequired, css: PropTypes.string.isRequired, isStarred: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, onSave: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, fetchFaveStar: PropTypes.func.isRequired, @@ -94,7 +95,10 @@ class Header extends React.PureComponent { } forceRefresh() { - return this.props.fetchCharts(Object.values(this.props.charts), true); + if (!this.props.isLoading) { + return this.props.fetchCharts(Object.values(this.props.charts), true); + } + return false; } handleChangeText(nextText) { @@ -185,6 +189,7 @@ class Header extends React.PureComponent { showBuilderPane, dashboardInfo, hasUnsavedChanges, + isLoading, } = this.props; const userCanEdit = dashboardInfo.dash_edit_perm; @@ -305,6 +310,7 @@ class Header extends React.PureComponent { hasUnsavedChanges={hasUnsavedChanges} userCanEdit={userCanEdit} userCanSave={userCanSaveAs} + isLoading={isLoading} /> </div> </div> diff --git a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx index d012e34..9967ce0 100644 --- a/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset/assets/src/dashboard/components/HeaderActionsDropdown.jsx @@ -26,6 +26,7 @@ const propTypes = { editMode: PropTypes.bool.isRequired, userCanEdit: PropTypes.bool.isRequired, userCanSave: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, layout: PropTypes.object.isRequired, filters: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, @@ -91,6 +92,7 @@ class HeaderActionsDropdown extends React.PureComponent { onSave, userCanEdit, userCanSave, + isLoading, } = this.props; const emailTitle = t('Superset Dashboard'); @@ -137,7 +139,7 @@ class HeaderActionsDropdown extends React.PureComponent { {userCanSave && <MenuItem divider />} - <MenuItem onClick={forceRefreshAllCharts}> + <MenuItem onClick={forceRefreshAllCharts} disabled={isLoading}> {t('Force refresh dashboard')} </MenuItem> <RefreshIntervalModal diff --git a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx index 4116a34..b5ee0b8 100644 --- a/superset/assets/src/dashboard/components/SliceHeaderControls.jsx +++ b/superset/assets/src/dashboard/components/SliceHeaderControls.jsx @@ -88,11 +88,13 @@ class SliceHeaderControls extends React.PureComponent { } refreshChart() { - this.props.forceRefresh(this.props.slice.slice_id); - Logger.append(LOG_ACTIONS_REFRESH_CHART, { - slice_id: this.props.slice.slice_id, - is_cached: this.props.isCached, - }); + if (this.props.updatedDttm) { + this.props.forceRefresh(this.props.slice.slice_id); + Logger.append(LOG_ACTIONS_REFRESH_CHART, { + slice_id: this.props.slice.slice_id, + is_cached: this.props.isCached, + }); + } } toggleControls() { @@ -122,7 +124,7 @@ class SliceHeaderControls extends React.PureComponent { </Dropdown.Toggle> <Dropdown.Menu> - <MenuItem onClick={this.refreshChart}> + <MenuItem onClick={this.refreshChart} disabled={!updatedDttm}> {t('Force refresh')} <div className="refresh-tooltip">{refreshTooltip}</div> </MenuItem> diff --git a/superset/assets/src/dashboard/containers/DashboardHeader.jsx b/superset/assets/src/dashboard/containers/DashboardHeader.jsx index 3e54c62..459f0d8 100644 --- a/superset/assets/src/dashboard/containers/DashboardHeader.jsx +++ b/superset/assets/src/dashboard/containers/DashboardHeader.jsx @@ -2,6 +2,7 @@ import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import DashboardHeader from '../components/Header'; +import isDashboardLoading from '../util/isDashboardLoading'; import { setEditMode, @@ -51,6 +52,7 @@ function mapStateToProps({ charts, userId: dashboardInfo.userId, isStarred: !!dashboardState.isStarred, + isLoading: isDashboardLoading(charts), hasUnsavedChanges: !!dashboardState.hasUnsavedChanges, maxUndoHistoryExceeded: !!dashboardState.maxUndoHistoryExceeded, editMode: !!dashboardState.editMode, diff --git a/superset/assets/src/dashboard/util/isDashboardLoading.js b/superset/assets/src/dashboard/util/isDashboardLoading.js new file mode 100644 index 0000000..01054f5 --- /dev/null +++ b/superset/assets/src/dashboard/util/isDashboardLoading.js @@ -0,0 +1,5 @@ +export default function isDashboardLoading(charts) { + return Object.values(charts).some( + chart => chart.chartUpdateStartTime >= (chart.chartUpdateEndTime || 0), + ); +} diff --git a/superset/assets/src/explore/reducers/getInitialState.js b/superset/assets/src/explore/reducers/getInitialState.js index 9c6f921..21a6edd 100644 --- a/superset/assets/src/explore/reducers/getInitialState.js +++ b/superset/assets/src/explore/reducers/getInitialState.js @@ -1,7 +1,6 @@ import shortid from 'shortid'; import getToastsFromPyFlashMessages from '../../messageToasts/utils/getToastsFromPyFlashMessages'; -import { now } from '../../modules/dates'; import { getChartKey } from '../exploreUtils'; import { getControlsState, getFormDataFromControls } from '../store'; @@ -37,7 +36,7 @@ export default function getInitialState(bootstrapData) { chartAlert: null, chartStatus: 'loading', chartUpdateEndTime: null, - chartUpdateStartTime: now(), + chartUpdateStartTime: 0, latestQueryFormData: getFormDataFromControls(controls), sliceFormData, queryController: null,