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:
parent
ee777acd57
commit
dcac860f3e
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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={
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ export default function dashboardStateReducer(state = {}, action) {
|
|||
return {
|
||||
...state,
|
||||
refreshFrequency: action.refreshFrequency,
|
||||
shouldPersistRefreshFrequency: action.isPersistent,
|
||||
hasUnsavedChanges: action.isPersistent,
|
||||
};
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -165,6 +165,10 @@ body {
|
|||
width: 80%;
|
||||
}
|
||||
|
||||
.refresh-warning-container {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.dashboard-modal-actions-container {
|
||||
margin-top: 24px;
|
||||
text-align: right;
|
||||
|
|
|
|||
|
|
@ -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.',
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in New Issue