diff --git a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx index 336a1002c..b2d7bc0db 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/Header_spec.jsx @@ -36,6 +36,10 @@ describe('Header', () => { dash_edit_perm: true, dash_save_perm: true, userId: 1, + metadata: {}, + common: { + conf: {}, + }, }, dashboardTitle: 'title', charts: {}, @@ -79,6 +83,7 @@ describe('Header', () => { describe('read-only-user', () => { const overrideProps = { dashboardInfo: { + ...props.dashboardInfo, id: 1, dash_edit_perm: false, dash_save_perm: false, @@ -121,6 +126,7 @@ describe('Header', () => { const overrideProps = { editMode: false, dashboardInfo: { + ...props.dashboardInfo, id: 1, dash_edit_perm: true, dash_save_perm: true, @@ -163,6 +169,7 @@ describe('Header', () => { const overrideProps = { editMode: true, dashboardInfo: { + ...props.dashboardInfo, id: 1, dash_edit_perm: true, dash_save_perm: true, @@ -204,6 +211,7 @@ describe('Header', () => { describe('logged-out-user', () => { const overrideProps = { dashboardInfo: { + ...props.dashboardInfo, id: 1, dash_edit_perm: false, dash_save_perm: false, diff --git a/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx b/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx index 09cb78fab..92f34ba9d 100644 --- a/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx +++ b/superset-frontend/spec/javascripts/dashboard/components/RefreshIntervalModal_spec.jsx @@ -17,9 +17,11 @@ * under the License. */ import React from 'react'; -import { mount } from 'enzyme'; +import { mount, shallow } from 'enzyme'; +import ModalTrigger from 'src/components/ModalTrigger'; import RefreshIntervalModal from 'src/dashboard/components/RefreshIntervalModal'; +import { Modal, Alert } from 'react-bootstrap'; describe('RefreshIntervalModal', () => { const mockedProps = { @@ -44,7 +46,22 @@ describe('RefreshIntervalModal', () => { it('should change refreshFrequency with edit mode', () => { const wrapper = mount(); wrapper.instance().handleFrequencyChange({ value: 30 }); + wrapper.instance().onSave(); expect(mockedProps.onChange).toHaveBeenCalled(); expect(mockedProps.onChange).toHaveBeenCalledWith(30, mockedProps.editMode); }); + it('should show warning message', () => { + const props = { + ...mockedProps, + refreshLimit: 3600, + refreshWarning: 'Show warning', + }; + + const wrapper = shallow(); + wrapper.instance().handleFrequencyChange({ value: 30 }); + expect(wrapper.find(ModalTrigger).dive().find(Alert)).toHaveLength(1); + + wrapper.instance().handleFrequencyChange({ value: 3601 }); + expect(wrapper.find(ModalTrigger).dive().find(Alert)).toHaveLength(0); + }); }); diff --git a/superset-frontend/src/dashboard/components/Header.jsx b/superset-frontend/src/dashboard/components/Header.jsx index 89ba78545..45c67270c 100644 --- a/superset-frontend/src/dashboard/components/Header.jsx +++ b/superset-frontend/src/dashboard/components/Header.jsx @@ -17,6 +17,7 @@ * under the License. */ /* eslint-env browser */ +import moment from 'moment'; import React from 'react'; import PropTypes from 'prop-types'; import { CategoricalColorNamespace } from '@superset-ui/color'; @@ -46,6 +47,7 @@ import { } from '../../logger/LogUtils'; import PropertiesModal from './PropertiesModal'; import setPeriodicRunner from '../util/setPeriodicRunner'; +import { options as PeriodicRefreshOptions } from './RefreshIntervalModal'; const propTypes = { addSuccessToast: PropTypes.func.isRequired, @@ -86,6 +88,7 @@ const propTypes = { setMaxUndoHistoryExceeded: PropTypes.func.isRequired, maxUndoHistoryToast: PropTypes.func.isRequired, refreshFrequency: PropTypes.number.isRequired, + shouldPersistRefreshFrequency: PropTypes.bool.isRequired, setRefreshFrequency: PropTypes.func.isRequired, dashboardInfoChanged: PropTypes.func.isRequired, dashboardTitleChanged: PropTypes.func.isRequired, @@ -206,6 +209,18 @@ class Header extends React.PureComponent { } startPeriodicRender(interval) { + let intervalMessage; + if (interval) { + const predefinedValue = PeriodicRefreshOptions.find( + option => option.value === interval / 1000, + ); + if (predefinedValue) { + intervalMessage = predefinedValue.label; + } else { + intervalMessage = moment.duration(interval, 'millisecond').humanize(); + } + } + const periodicRender = () => { const { fetchCharts, logEvent, charts, dashboardInfo } = this.props; const { metadata } = dashboardInfo; @@ -218,6 +233,13 @@ class Header extends React.PureComponent { interval, chartCount: affectedCharts.length, }); + this.props.addWarningToast( + t( + `This dashboard is currently force refreshing; the next force refresh will be in %s.`, + intervalMessage, + ), + ); + return fetchCharts( affectedCharts, true, @@ -249,7 +271,8 @@ class Header extends React.PureComponent { colorNamespace, colorScheme, dashboardInfo, - refreshFrequency, + refreshFrequency: currentRefreshFrequency, + shouldPersistRefreshFrequency, } = this.props; const scale = CategoricalColorNamespace.getScale( @@ -257,6 +280,11 @@ class Header extends React.PureComponent { colorNamespace, ); const labelColors = colorScheme ? scale.getColorMap() : {}; + // check refresh frequency is for current session or persist + const refreshFrequency = shouldPersistRefreshFrequency + ? currentRefreshFrequency + : dashboardInfo.metadata.refresh_frequency; // eslint-disable camelcase + const data = { positions, expanded_slices: expandedSlices, @@ -318,11 +346,17 @@ class Header extends React.PureComponent { hasUnsavedChanges, isLoading, refreshFrequency, + shouldPersistRefreshFrequency, setRefreshFrequency, } = this.props; const userCanEdit = dashboardInfo.dash_edit_perm; const userCanSaveAs = dashboardInfo.dash_save_perm; + const refreshLimit = + dashboardInfo.common.conf.SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT; + const refreshWarning = + dashboardInfo.common.conf + .SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE; const popButton = hasUnsavedChanges; return ( @@ -474,6 +508,7 @@ class Header extends React.PureComponent { addDangerToast={this.props.addDangerToast} dashboardId={dashboardInfo.id} dashboardTitle={dashboardTitle} + dashboardInfo={dashboardInfo} layout={layout} expandedSlices={expandedSlices} customCss={customCss} @@ -484,6 +519,7 @@ class Header extends React.PureComponent { forceRefreshAllCharts={this.forceRefresh} startPeriodicRender={this.startPeriodicRender} refreshFrequency={refreshFrequency} + shouldPersistRefreshFrequency={shouldPersistRefreshFrequency} setRefreshFrequency={setRefreshFrequency} updateCss={updateCss} editMode={editMode} @@ -492,6 +528,8 @@ class Header extends React.PureComponent { userCanSave={userCanSaveAs} isLoading={isLoading} showPropertiesModal={this.showPropertiesModal} + refreshLimit={refreshLimit} + refreshWarning={refreshWarning} /> diff --git a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx index 0097c0ce4..95189a1bc 100644 --- a/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx +++ b/superset-frontend/src/dashboard/components/HeaderActionsDropdown.jsx @@ -35,6 +35,7 @@ import { getActiveFilters } from '../util/activeDashboardFilters'; const propTypes = { addSuccessToast: PropTypes.func.isRequired, addDangerToast: PropTypes.func.isRequired, + dashboardInfo: PropTypes.object.isRequired, dashboardId: PropTypes.number.isRequired, dashboardTitle: PropTypes.string.isRequired, hasUnsavedChanges: PropTypes.bool.isRequired, @@ -45,6 +46,7 @@ const propTypes = { updateCss: PropTypes.func.isRequired, forceRefreshAllCharts: PropTypes.func.isRequired, refreshFrequency: PropTypes.number.isRequired, + shouldPersistRefreshFrequency: PropTypes.bool.isRequired, setRefreshFrequency: PropTypes.func.isRequired, startPeriodicRender: PropTypes.func.isRequired, editMode: PropTypes.bool.isRequired, @@ -55,11 +57,15 @@ const propTypes = { expandedSlices: PropTypes.object.isRequired, onSave: PropTypes.func.isRequired, showPropertiesModal: PropTypes.func.isRequired, + refreshLimit: PropTypes.number, + refreshWarning: PropTypes.string, }; const defaultProps = { colorNamespace: undefined, colorScheme: undefined, + refreshLimit: 0, + refreshWarning: null, }; class HeaderActionsDropdown extends React.PureComponent { @@ -114,8 +120,10 @@ class HeaderActionsDropdown extends React.PureComponent { const { dashboardTitle, dashboardId, + dashboardInfo, forceRefreshAllCharts, refreshFrequency, + shouldPersistRefreshFrequency, editMode, customCss, colorNamespace, @@ -127,6 +135,8 @@ class HeaderActionsDropdown extends React.PureComponent { userCanEdit, userCanSave, isLoading, + refreshLimit, + refreshWarning, } = this.props; const emailTitle = t('Superset Dashboard'); @@ -147,10 +157,12 @@ class HeaderActionsDropdown extends React.PureComponent { addDangerToast={this.props.addDangerToast} dashboardId={dashboardId} dashboardTitle={dashboardTitle} + dashboardInfo={dashboardInfo} saveType={SAVE_TYPE_NEWDASHBOARD} layout={layout} expandedSlices={expandedSlices} refreshFrequency={refreshFrequency} + shouldPersistRefreshFrequency={shouldPersistRefreshFrequency} customCss={customCss} colorNamespace={colorNamespace} colorScheme={colorScheme} @@ -180,6 +192,8 @@ class HeaderActionsDropdown extends React.PureComponent { + {showRefreshWarning && ( +
+ +
{refreshWarning}
+
+ {t('Are you sure you want to proceed?')} +
+
+ )} } + modalFooter={ + <> + + + + } /> ); } } RefreshIntervalModal.propTypes = propTypes; +RefreshIntervalModal.defaultProps = defaultProps; export default RefreshIntervalModal; diff --git a/superset-frontend/src/dashboard/components/SaveModal.jsx b/superset-frontend/src/dashboard/components/SaveModal.jsx index e8dcb9171..16a5d1ed1 100644 --- a/superset-frontend/src/dashboard/components/SaveModal.jsx +++ b/superset-frontend/src/dashboard/components/SaveModal.jsx @@ -32,6 +32,7 @@ const propTypes = { addDangerToast: PropTypes.func.isRequired, dashboardId: PropTypes.number.isRequired, dashboardTitle: PropTypes.string.isRequired, + dashboardInfo: PropTypes.object.isRequired, expandedSlices: PropTypes.object.isRequired, layout: PropTypes.object.isRequired, saveType: PropTypes.oneOf([SAVE_TYPE_OVERWRITE, SAVE_TYPE_NEWDASHBOARD]), @@ -94,13 +95,15 @@ class SaveModal extends React.PureComponent { const { saveType, newDashName } = this.state; const { dashboardTitle, + dashboardInfo, layout: positions, customCss, colorNamespace, colorScheme, expandedSlices, dashboardId, - refreshFrequency, + refreshFrequency: currentRefreshFrequency, + shouldPersistRefreshFrequency, } = this.props; const scale = CategoricalColorNamespace.getScale( @@ -108,6 +111,11 @@ class SaveModal extends React.PureComponent { colorNamespace, ); const labelColors = colorScheme ? scale.getColorMap() : {}; + // check refresh frequency is for current session or persist + const refreshFrequency = shouldPersistRefreshFrequency + ? currentRefreshFrequency + : dashboardInfo.metadata.refresh_frequency; // eslint-disable camelcase + const data = { positions, css: customCss, diff --git a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx index 9516f245e..962fc9a45 100644 --- a/superset-frontend/src/dashboard/containers/DashboardHeader.jsx +++ b/superset-frontend/src/dashboard/containers/DashboardHeader.jsx @@ -71,6 +71,7 @@ function mapStateToProps({ ).text, expandedSlices: dashboardState.expandedSlices, refreshFrequency: dashboardState.refreshFrequency, + shouldPersistRefreshFrequency: !!dashboardState.shouldPersistRefreshFrequency, customCss: dashboardState.css, colorNamespace: dashboardState.colorNamespace, colorScheme: dashboardState.colorScheme, diff --git a/superset-frontend/src/dashboard/reducers/dashboardState.js b/superset-frontend/src/dashboard/reducers/dashboardState.js index 61c9a8b0c..fc7e079f4 100644 --- a/superset-frontend/src/dashboard/reducers/dashboardState.js +++ b/superset-frontend/src/dashboard/reducers/dashboardState.js @@ -120,6 +120,7 @@ export default function dashboardStateReducer(state = {}, action) { return { ...state, refreshFrequency: action.refreshFrequency, + shouldPersistRefreshFrequency: action.isPersistent, hasUnsavedChanges: action.isPersistent, }; }, diff --git a/superset-frontend/src/dashboard/reducers/getInitialState.js b/superset-frontend/src/dashboard/reducers/getInitialState.js index bd2761894..3d4e38a6c 100644 --- a/superset-frontend/src/dashboard/reducers/getInitialState.js +++ b/superset-frontend/src/dashboard/reducers/getInitialState.js @@ -292,6 +292,9 @@ export default function (bootstrapData) { focusedFilterField: [], expandedSlices: dashboard.metadata.expanded_slices || {}, refreshFrequency: dashboard.metadata.refresh_frequency || 0, + // dashboard viewers can set refresh frequency for the current visit, + // only persistent refreshFrequency will be saved to backend + shouldPersistRefreshFrequency: false, css: dashboard.css || '', colorNamespace: dashboard.metadata.color_namespace, colorScheme: dashboard.metadata.color_scheme, diff --git a/superset-frontend/src/dashboard/stylesheets/dashboard.less b/superset-frontend/src/dashboard/stylesheets/dashboard.less index fb7593f50..2d99763ae 100644 --- a/superset-frontend/src/dashboard/stylesheets/dashboard.less +++ b/superset-frontend/src/dashboard/stylesheets/dashboard.less @@ -165,6 +165,10 @@ body { width: 80%; } + .refresh-warning-container { + margin-top: 24px; + } + .dashboard-modal-actions-container { margin-top: 24px; text-align: right; diff --git a/superset-frontend/src/datasource/DatasourceEditor.jsx b/superset-frontend/src/datasource/DatasourceEditor.jsx index bb56fd253..114a36d4d 100644 --- a/superset-frontend/src/datasource/DatasourceEditor.jsx +++ b/superset-frontend/src/datasource/DatasourceEditor.jsx @@ -731,7 +731,7 @@ export class DatasourceEditor extends React.PureComponent {
- {t('Be careful.')} + {t('Be careful.')} {t( 'Changing these settings will affect all charts using this datasource, including charts owned by other people.', )} diff --git a/superset/config.py b/superset/config.py index 32ce4ae6d..738c251f6 100644 --- a/superset/config.py +++ b/superset/config.py @@ -126,6 +126,14 @@ SUPERSET_WEBSERVER_PORT = 8088 # (gunicorn, nginx, apache, ...) timeout setting to be <= to this setting SUPERSET_WEBSERVER_TIMEOUT = 60 +# this 2 settings are used by dashboard period force refresh feature +# When user choose auto force refresh frequency +# < SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT +# they will see warning message in the Refresh Interval Modal. +# please check PR #9886 +SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT = 0 +SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE = None + SUPERSET_DASHBOARD_POSITION_DATA_LIMIT = 65535 CUSTOM_SECURITY_MANAGER = None SQLALCHEMY_TRACK_MODIFICATIONS = False diff --git a/superset/views/base.py b/superset/views/base.py index 6995fd182..5821619c1 100644 --- a/superset/views/base.py +++ b/superset/views/base.py @@ -61,6 +61,8 @@ if TYPE_CHECKING: FRONTEND_CONF_KEYS = ( "SUPERSET_WEBSERVER_TIMEOUT", "SUPERSET_DASHBOARD_POSITION_DATA_LIMIT", + "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_LIMIT", + "SUPERSET_DASHBOARD_PERIODICAL_REFRESH_WARNING_MESSAGE", "ENABLE_JAVASCRIPT_CONTROLS", "DEFAULT_SQLLAB_LIMIT", "SQL_MAX_ROW",