feat: [dashboard] notification and warning for auto force refresh (#9886)

* feat: [dashboard] notification and warning for auto force refresh

* fix review comments
This commit is contained in:
Grace Guo 2020-06-03 10:20:56 -07:00 committed by GitHub
parent ee777acd57
commit dcac860f3e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 156 additions and 6 deletions

View File

@ -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,

View File

@ -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(<RefreshIntervalModal {...mockedProps} />);
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(<RefreshIntervalModal {...props} />);
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);
});
});

View File

@ -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}
/>
</div>
</div>

View File

@ -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 {
<RefreshIntervalModal
refreshFrequency={refreshFrequency}
refreshLimit={refreshLimit}
refreshWarning={refreshWarning}
onChange={this.changeRefreshInterval}
editMode={editMode}
triggerNode={

View File

@ -20,6 +20,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import Select from 'src/components/Select';
import { t } from '@superset-ui/translation';
import { Alert, Button } from 'react-bootstrap';
import ModalTrigger from '../../components/ModalTrigger';
@ -28,9 +29,16 @@ const propTypes = {
refreshFrequency: PropTypes.number.isRequired,
onChange: PropTypes.func.isRequired,
editMode: PropTypes.bool.isRequired,
refreshLimit: PropTypes.number,
refreshWarning: PropTypes.string,
};
const options = [
const defaultProps = {
refreshLimit: 0,
refreshWarning: null,
};
export const options = [
[0, t("Don't refresh")],
[10, t('10 seconds')],
[30, t('30 seconds')],
@ -46,10 +54,25 @@ const options = [
class RefreshIntervalModal extends React.PureComponent {
constructor(props) {
super(props);
this.modalRef = React.createRef();
this.state = {
refreshFrequency: props.refreshFrequency,
};
this.handleFrequencyChange = this.handleFrequencyChange.bind(this);
this.onSave = this.onSave.bind(this);
this.onCancel = this.onCancel.bind(this);
}
onSave() {
this.props.onChange(this.state.refreshFrequency, this.props.editMode);
this.modalRef.current.close();
}
onCancel() {
this.setState({
refreshFrequency: this.props.refreshFrequency,
});
this.modalRef.current.close();
}
handleFrequencyChange(opt) {
@ -57,12 +80,17 @@ class RefreshIntervalModal extends React.PureComponent {
this.setState({
refreshFrequency: value,
});
this.props.onChange(value, this.props.editMode);
}
render() {
const { refreshLimit = 0, refreshWarning, editMode } = this.props;
const { refreshFrequency = 0 } = this.state;
const showRefreshWarning =
!!refreshFrequency && !!refreshWarning && refreshFrequency < refreshLimit;
return (
<ModalTrigger
ref={this.modalRef}
triggerNode={this.props.triggerNode}
isMenuItem
modalTitle={t('Refresh Interval')}
@ -74,12 +102,30 @@ class RefreshIntervalModal extends React.PureComponent {
value={this.state.refreshFrequency}
onChange={this.handleFrequencyChange}
/>
{showRefreshWarning && (
<div className="refresh-warning-container">
<Alert bsStyle="warning">
<div>{refreshWarning}</div>
<br />
<strong>{t('Are you sure you want to proceed?')}</strong>
</Alert>
</div>
)}
</div>
}
modalFooter={
<>
<Button bsStyle="primary" onClick={this.onSave}>
{editMode ? t('Save') : t('Save for this session')}
</Button>
<Button onClick={this.onCancel}>{t('Cancel')}</Button>
</>
}
/>
);
}
}
RefreshIntervalModal.propTypes = propTypes;
RefreshIntervalModal.defaultProps = defaultProps;
export default RefreshIntervalModal;

View File

@ -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,

View File

@ -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,

View File

@ -120,6 +120,7 @@ export default function dashboardStateReducer(state = {}, action) {
return {
...state,
refreshFrequency: action.refreshFrequency,
shouldPersistRefreshFrequency: action.isPersistent,
hasUnsavedChanges: action.isPersistent,
};
},

View File

@ -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,

View File

@ -165,6 +165,10 @@ body {
width: 80%;
}
.refresh-warning-container {
margin-top: 24px;
}
.dashboard-modal-actions-container {
margin-top: 24px;
text-align: right;

View File

@ -731,7 +731,7 @@ export class DatasourceEditor extends React.PureComponent {
<div>
<div className="m-t-10">
<Alert bsStyle="warning">
<span className="bold">{t('Be careful.')} </span>
<strong>{t('Be careful.')} </strong>
{t(
'Changing these settings will affect all charts using this datasource, including charts owned by other people.',
)}

View File

@ -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

View File

@ -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",