diff --git a/CHANGELOG.md b/CHANGELOG.md index a60096e6add..54df1d50265 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## v2.0.0-alpha.9 [unreleased] ### Features +1. [13423](https://github.com/influxdata/influxdb/pull/13423): Set autorefresh of dashboard to pause if absolute time range is selected ### Bug Fixes diff --git a/ui/package-lock.json b/ui/package-lock.json index 35862b3e18d..0fcd955657a 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -6185,8 +6185,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6210,15 +6209,13 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6235,22 +6232,19 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6381,8 +6375,7 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6396,7 +6389,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6413,7 +6405,6 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6422,15 +6413,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6451,7 +6440,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6540,8 +6528,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6555,7 +6542,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6651,8 +6637,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6694,7 +6679,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6716,7 +6700,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6765,15 +6748,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/ui/src/dashboards/components/Dashboard.tsx b/ui/src/dashboards/components/Dashboard.tsx index 98e6a4d668c..d53e3870e79 100644 --- a/ui/src/dashboards/components/Dashboard.tsx +++ b/ui/src/dashboards/components/Dashboard.tsx @@ -13,7 +13,6 @@ import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { dashboard: Dashboard timeRange: TimeRange - autoRefresh: number manualRefresh: number inPresentationMode: boolean inView: (cell: Cell) => boolean @@ -34,7 +33,6 @@ class DashboardComponent extends PureComponent { onZoom, dashboard, timeRange, - autoRefresh, manualRefresh, onDeleteCell, onCloneCell, @@ -58,7 +56,6 @@ class DashboardComponent extends PureComponent { void - handleChooseAutoRefresh: AppActions.SetAutoRefreshActionCreator + handleChooseAutoRefresh: (autoRefreshInterval: number) => void + onSetAutoRefreshStatus: (status: AutoRefreshStatus) => void onManualRefresh: () => void handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher onAddCell: () => void @@ -55,8 +59,6 @@ export default class DashboardHeader extends Component { const { handleChooseAutoRefresh, onManualRefresh, - autoRefresh, - handleChooseTimeRange, timeRange: {upper, lower}, zoomedTimeRange: {upper: zoomedUpper, lower: zoomedLower}, isHidden, @@ -65,6 +67,7 @@ export default class DashboardHeader extends Component { onAddCell, onRenameDashboard, activeDashboard, + autoRefresh, } = this.props return ( @@ -97,7 +100,7 @@ export default class DashboardHeader extends Component { selected={autoRefresh} /> { private handleClickPresentationButton = (): void => { this.props.handleClickPresentationButton() } + + private handleChooseTimeRange = ( + timeRange: QueriesModels.TimeRange, + rangeType: RangeType = RangeType.Relative + ) => { + const { + autoRefresh, + onSetAutoRefreshStatus, + handleChooseTimeRange, + } = this.props + + handleChooseTimeRange(timeRange) + + if (rangeType === RangeType.Absolute) { + onSetAutoRefreshStatus(AutoRefreshStatus.Disabled) + return + } + + if (autoRefresh.status === AutoRefreshStatus.Disabled) { + if (autoRefresh.interval === 0) { + onSetAutoRefreshStatus(AutoRefreshStatus.Paused) + return + } + + onSetAutoRefreshStatus(AutoRefreshStatus.Active) + } + } } diff --git a/ui/src/dashboards/components/DashboardPage.tsx b/ui/src/dashboards/components/DashboardPage.tsx index f50f79cde35..ab177dbc948 100644 --- a/ui/src/dashboards/components/DashboardPage.tsx +++ b/ui/src/dashboards/components/DashboardPage.tsx @@ -19,17 +19,33 @@ import * as rangesActions from 'src/dashboards/actions/ranges' import * as appActions from 'src/shared/actions/app' import * as notifyActions from 'src/shared/actions/notifications' import {setActiveTimeMachine} from 'src/timeMachine/actions' +import { + setAutoRefreshInterval, + setAutoRefreshStatus, +} from 'src/shared/actions/autoRefresh' // Utils import {getDeep} from 'src/utils/wrappers' import {GlobalAutoRefresher} from 'src/utils/AutoRefresher' // Constants -import {DASHBOARD_LAYOUT_ROW_HEIGHT} from 'src/shared/constants' +import { + DASHBOARD_LAYOUT_ROW_HEIGHT, + AUTOREFRESH_DEFAULT, +} from 'src/shared/constants' import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges' // Types -import {Links, Dashboard, Cell, View, TimeRange, AppState} from 'src/types' +import { + Links, + Dashboard, + Cell, + View, + TimeRange, + AppState, + AutoRefresh, + AutoRefreshStatus, +} from 'src/types' import {RemoteDataState} from 'src/types' import {WithRouterProps} from 'react-router' import {ManualRefreshProps} from 'src/shared/components/ManualRefresh' @@ -44,7 +60,7 @@ interface StateProps { zoomedTimeRange: TimeRange timeRange: TimeRange dashboard: Dashboard - autoRefresh: number + autoRefresh: AutoRefresh inPresentationMode: boolean showVariablesControls: boolean views: {[cellID: string]: {view: View; status: RemoteDataState}} @@ -59,7 +75,8 @@ interface DispatchProps { updateQueryParams: typeof rangesActions.updateQueryParams setDashTimeV1: typeof rangesActions.setDashTimeV1 setZoomedTimeRange: typeof rangesActions.setZoomedTimeRange - handleChooseAutoRefresh: AppActions.SetAutoRefreshActionCreator + handleChooseAutoRefresh: typeof setAutoRefreshInterval + onSetAutoRefreshStatus: typeof setAutoRefreshStatus handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher notify: NotificationsActions.PublishNotificationActionCreator onCreateCellWithView: typeof dashboardActions.createCellWithView @@ -108,7 +125,9 @@ class DashboardPage extends Component { public async componentDidMount() { const {autoRefresh} = this.props - GlobalAutoRefresher.poll(autoRefresh) + if (autoRefresh.status === AutoRefreshStatus.Active) { + GlobalAutoRefresher.poll(autoRefresh.interval) + } window.addEventListener('resize', this.handleWindowResize, true) @@ -125,8 +144,13 @@ class DashboardPage extends Component { this.getDashboard() } - if (autoRefresh !== prevProps.autoRefresh) { - GlobalAutoRefresher.poll(autoRefresh) + if (!_.isEqual(autoRefresh, prevProps.autoRefresh)) { + if (autoRefresh.status === AutoRefreshStatus.Active) { + GlobalAutoRefresher.poll(autoRefresh.interval) + return + } + + GlobalAutoRefresher.stopPolling() } } @@ -146,7 +170,6 @@ class DashboardPage extends Component { onManualRefresh, inPresentationMode, showVariablesControls, - handleChooseAutoRefresh, handleClickPresentationButton, onToggleShowVariablesControls, children, @@ -166,7 +189,8 @@ class DashboardPage extends Component { zoomedTimeRange={zoomedTimeRange} onRenameDashboard={this.handleRenameDashboard} activeDashboard={dashboard ? dashboard.name : ''} - handleChooseAutoRefresh={handleChooseAutoRefresh} + handleChooseAutoRefresh={this.handleChooseAutoRefresh} + onSetAutoRefreshStatus={this.handleSetAutoRefreshStatus} handleChooseTimeRange={this.handleChooseTimeRange} handleClickPresentationButton={handleClickPresentationButton} toggleVariablesControlBar={onToggleShowVariablesControls} @@ -180,7 +204,6 @@ class DashboardPage extends Component { inView={this.inView} dashboard={dashboard} timeRange={timeRange} - autoRefresh={autoRefresh} manualRefresh={manualRefresh} setScrollTop={this.setScrollTop} onCloneCell={this.handleCloneCell} @@ -228,6 +251,30 @@ class DashboardPage extends Component { }) } + private handleSetAutoRefreshStatus = ( + autoRefreshStatus: AutoRefreshStatus + ) => { + const { + onSetAutoRefreshStatus, + params: {dashboardID}, + } = this.props + + onSetAutoRefreshStatus(dashboardID, autoRefreshStatus) + } + + private handleChooseAutoRefresh = (autoRefreshInterval: number) => { + const { + handleChooseAutoRefresh, + params: {dashboardID}, + } = this.props + + if (autoRefreshInterval === 0) { + this.handleSetAutoRefreshStatus(AutoRefreshStatus.Paused) + } + + handleChooseAutoRefresh(dashboardID, autoRefreshInterval) + } + private handlePositionChange = async (cells: Cell[]): Promise => { const {dashboard, updateCells} = this.props await updateCells(dashboard, cells) @@ -301,7 +348,6 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { links, app: { ephemeral: {inPresentationMode}, - persisted: {autoRefresh}, }, ranges, dashboards, @@ -312,6 +358,8 @@ const mstp = (state: AppState, {params: {dashboardID}}): StateProps => { const timeRange = ranges.find(r => r.dashboardID === dashboardID) || DEFAULT_TIME_RANGE + const autoRefresh = state.autoRefresh[dashboardID] || AUTOREFRESH_DEFAULT + const dashboard = dashboards.list.find(d => d.id === dashboardID) return { @@ -332,7 +380,8 @@ const mdtp: DispatchProps = { copyCell: dashboardActions.copyDashboardCellAsync, deleteCell: dashboardActions.deleteCellAsync, updateCells: dashboardActions.updateCellsAsync, - handleChooseAutoRefresh: appActions.setAutoRefresh, + handleChooseAutoRefresh: setAutoRefreshInterval, + onSetAutoRefreshStatus: setAutoRefreshStatus, handleClickPresentationButton: appActions.delayEnablePresentationMode, notify: notifyActions.notify, setDashTimeV1: rangesActions.setDashTimeV1, diff --git a/ui/src/localStorage.ts b/ui/src/localStorage.ts index 06ba10db9f3..1526c97c87d 100644 --- a/ui/src/localStorage.ts +++ b/ui/src/localStorage.ts @@ -31,6 +31,7 @@ export const loadLocalStorage = (): LocalStorage => { export const saveToLocalStorage = ({ app: {persisted}, ranges, + autoRefresh, variables, userSettings, orgs: {org}, @@ -43,6 +44,7 @@ export const saveToLocalStorage = ({ ...appPersisted, VERSION, ranges: normalizer(ranges), + autoRefresh, variables, userSettings, orgs: { diff --git a/ui/src/mockState.tsx b/ui/src/mockState.tsx index e7fbddadc3e..4f3212792f2 100644 --- a/ui/src/mockState.tsx +++ b/ui/src/mockState.tsx @@ -31,6 +31,7 @@ const localState = { duration: '15m', }, ], + autoRefresh: {}, variables: initialVariablesState(), userSettings: initialUserSettingsState(), } diff --git a/ui/src/shared/actions/autoRefresh.ts b/ui/src/shared/actions/autoRefresh.ts new file mode 100644 index 00000000000..34152af2edd --- /dev/null +++ b/ui/src/shared/actions/autoRefresh.ts @@ -0,0 +1,29 @@ +import {AutoRefreshStatus} from 'src/types' + +export type Action = SetAutoRefresh | SetAutoRefreshStatus + +interface SetAutoRefresh { + type: 'SET_AUTO_REFRESH_INTERVAL' + payload: {dashboardID: string; milliseconds: number} +} + +export const setAutoRefreshInterval = ( + dashboardID: string, + milliseconds: number +): SetAutoRefresh => ({ + type: 'SET_AUTO_REFRESH_INTERVAL', + payload: {dashboardID, milliseconds}, +}) + +interface SetAutoRefreshStatus { + type: 'SET_AUTO_REFRESH_STATUS' + payload: {dashboardID: string; status: AutoRefreshStatus} +} + +export const setAutoRefreshStatus = ( + dashboardID: string, + status: AutoRefreshStatus +): SetAutoRefreshStatus => ({ + type: 'SET_AUTO_REFRESH_STATUS', + payload: {dashboardID, status}, +}) diff --git a/ui/src/shared/components/TimeRangeDropdown.tsx b/ui/src/shared/components/TimeRangeDropdown.tsx index ed04828685b..1e6c8b64af5 100644 --- a/ui/src/shared/components/TimeRangeDropdown.tsx +++ b/ui/src/shared/components/TimeRangeDropdown.tsx @@ -18,9 +18,14 @@ import { // Types import {TimeRange} from 'src/types' +export enum RangeType { + Absolute = 'absolute', + Relative = 'relative', +} + interface Props { timeRange: TimeRange - onSetTimeRange: (timeRange: TimeRange) => void + onSetTimeRange: (timeRange: TimeRange, rangeType?: RangeType) => void } interface State { @@ -140,7 +145,7 @@ class TimeRangeDropdown extends PureComponent { } private handleApplyTimeRange = (timeRange: TimeRange) => { - this.props.onSetTimeRange(timeRange) + this.props.onSetTimeRange(timeRange, RangeType.Absolute) this.handleHideDatePicker() } diff --git a/ui/src/shared/components/cells/Cell.tsx b/ui/src/shared/components/cells/Cell.tsx index 471107215c2..7c9bf3c6396 100644 --- a/ui/src/shared/components/cells/Cell.tsx +++ b/ui/src/shared/components/cells/Cell.tsx @@ -31,7 +31,6 @@ interface StateProps { interface OwnProps { cell: Cell timeRange: TimeRange - autoRefresh: number manualRefresh: number onDeleteCell: (cell: Cell) => void onCloneCell: (cell: Cell) => void @@ -105,7 +104,6 @@ class CellComponent extends Component { private get view(): JSX.Element { const { timeRange, - autoRefresh, manualRefresh, onZoom, view, @@ -122,7 +120,6 @@ class CellComponent extends Component { view={view} onZoom={onZoom} timeRange={timeRange} - autoRefresh={autoRefresh} manualRefresh={manualRefresh} onEditCell={onEditCell} /> diff --git a/ui/src/shared/components/cells/Cells.tsx b/ui/src/shared/components/cells/Cells.tsx index 9dcf28336f3..53d82fbfc79 100644 --- a/ui/src/shared/components/cells/Cells.tsx +++ b/ui/src/shared/components/cells/Cells.tsx @@ -29,7 +29,6 @@ import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { cells: Cell[] timeRange: TimeRange - autoRefresh: number manualRefresh: number onZoom: (range: TimeRange) => void onCloneCell?: (cell: Cell) => void @@ -60,7 +59,6 @@ class Cells extends Component { onDeleteCell, onCloneCell, timeRange, - autoRefresh, manualRefresh, onEditNote, } = this.props @@ -84,7 +82,6 @@ class Cells extends Component { void onEditCell: () => void diff --git a/ui/src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown.tsx b/ui/src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown.tsx index 0b283e888f0..867294d2bab 100644 --- a/ui/src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown.tsx +++ b/ui/src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown.tsx @@ -3,28 +3,37 @@ import React, {Component} from 'react' import classnames from 'classnames' // Components -import {Dropdown} from 'src/clockface' -import {Button, ButtonShape, IconFont} from '@influxdata/clockface' +import { + Button, + ButtonShape, + IconFont, + ComponentStatus, + Dropdown, +} from '@influxdata/clockface' // Constants import autoRefreshOptions, { AutoRefreshOption, AutoRefreshOptionType, } from 'src/shared/data/autoRefreshes' + +// Types +import {AutoRefresh, AutoRefreshStatus} from 'src/types' + const DROPDOWN_WIDTH_COLLAPSED = 50 const DROPDOWN_WIDTH_FULL = 84 import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - selected: number + selected: AutoRefresh onChoose: (milliseconds: number) => void showManualRefresh: boolean onManualRefresh?: () => void } @ErrorHandling -class AutoRefreshDropdown extends Component { +export default class AutoRefreshDropdown extends Component { public static defaultProps = { showManualRefresh: true, } @@ -46,6 +55,7 @@ class AutoRefreshDropdown extends Component { menuWidthPixels={DROPDOWN_WIDTH_FULL} onChange={this.handleDropdownChange} selectedID={this.selectedID} + status={this.dropdownStatus} > {autoRefreshOptions.map(option => { if (option.type === AutoRefreshOptionType.Header) { @@ -73,16 +83,27 @@ class AutoRefreshDropdown extends Component { public handleDropdownChange = ( autoRefreshOption: AutoRefreshOption ): void => { - const {onChoose} = this.props - const {milliseconds} = autoRefreshOption + this.props.onChoose(autoRefreshOption.milliseconds) + } - onChoose(milliseconds) + private get dropdownStatus(): ComponentStatus { + if (this.isDisabled) { + return ComponentStatus.Disabled + } + + return ComponentStatus.Default + } + + private get isDisabled(): boolean { + const {selected} = this.props + + return selected.status === AutoRefreshStatus.Disabled } private get isPaused(): boolean { const {selected} = this.props - return selected === 0 + return selected.status === AutoRefreshStatus.Paused || this.isDisabled } private get className(): string { @@ -107,8 +128,9 @@ class AutoRefreshDropdown extends Component { private get selectedID(): string { const {selected} = this.props + const selectedOption = autoRefreshOptions.find( - option => option.milliseconds === selected + option => option.milliseconds === selected.interval ) return selectedOption.id @@ -135,5 +157,3 @@ class AutoRefreshDropdown extends Component { return null } } - -export default AutoRefreshDropdown diff --git a/ui/src/shared/constants/index.ts b/ui/src/shared/constants/index.ts index 1b611ada0d5..b71cfdb23b2 100644 --- a/ui/src/shared/constants/index.ts +++ b/ui/src/shared/constants/index.ts @@ -1,4 +1,5 @@ import _ from 'lodash' +import {AutoRefreshStatus} from 'src/types' export const DEFAULT_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss' @@ -393,7 +394,12 @@ export const HTTP_UNAUTHORIZED = 401 export const HTTP_FORBIDDEN = 403 export const HTTP_NOT_FOUND = 404 -export const AUTOREFRESH_DEFAULT = 0 // in milliseconds +export const AUTOREFRESH_DEFAULT_INTERVAL = 0 // in milliseconds +export const AUTOREFRESH_DEFAULT_STATUS = AutoRefreshStatus.Paused +export const AUTOREFRESH_DEFAULT = { + status: AUTOREFRESH_DEFAULT_STATUS, + interval: AUTOREFRESH_DEFAULT_INTERVAL, +} export const GRAPH = 'graph' export const TABLE = 'table' diff --git a/ui/src/shared/reducers/app.ts b/ui/src/shared/reducers/app.ts index 4a6a8db9222..3e90eb41a49 100644 --- a/ui/src/shared/reducers/app.ts +++ b/ui/src/shared/reducers/app.ts @@ -1,6 +1,6 @@ import {combineReducers} from 'redux' -import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' +import {AUTOREFRESH_DEFAULT_INTERVAL} from 'src/shared/constants' import {ActionTypes, Action} from 'src/types/actions/app' export interface AppState { @@ -18,7 +18,7 @@ const initialState: AppState = { inPresentationMode: false, }, persisted: { - autoRefresh: AUTOREFRESH_DEFAULT, + autoRefresh: AUTOREFRESH_DEFAULT_INTERVAL, showTemplateControlBar: false, }, } diff --git a/ui/src/shared/reducers/autoRefresh.test.ts b/ui/src/shared/reducers/autoRefresh.test.ts new file mode 100644 index 00000000000..df10d9593b5 --- /dev/null +++ b/ui/src/shared/reducers/autoRefresh.test.ts @@ -0,0 +1,82 @@ +import {autoRefreshReducer, initialState} from 'src/shared/reducers/autoRefresh' + +import { + setAutoRefreshInterval, + setAutoRefreshStatus, +} from 'src/shared/actions/autoRefresh' +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' +import {AutoRefreshStatus} from 'src/types' + +describe('autoRefresh reducer', () => { + it('can set interval with empty state', () => { + const interval = 1000 + const actual = autoRefreshReducer( + initialState(), + setAutoRefreshInterval('dashID', interval) + ) + + const expected = {['dashID']: {...AUTOREFRESH_DEFAULT, interval}} + + expect(actual).toEqual(expected) + }) + + it('can set interval ', () => { + const interval = 5000 + const dashboardID = 'anotherDash' + const startingAutoRefresh = { + interval: 1000, + status: AutoRefreshStatus.Active, + } + const startingState = { + [dashboardID]: startingAutoRefresh, + ['dash']: AUTOREFRESH_DEFAULT, + } + const actual = autoRefreshReducer( + startingState, + setAutoRefreshInterval(dashboardID, interval) + ) + + const expected = { + [dashboardID]: {...startingAutoRefresh, interval}, + ['dash']: AUTOREFRESH_DEFAULT, + } + + expect(actual).toEqual(expected) + }) + + it('can set status with empty state', () => { + const status = AutoRefreshStatus.Active + const actual = autoRefreshReducer( + initialState(), + setAutoRefreshStatus('dashID', status) + ) + + const expected = {['dashID']: {...AUTOREFRESH_DEFAULT, status}} + + expect(actual).toEqual(expected) + }) + + it('can set status ', () => { + const status = AutoRefreshStatus.Disabled + const dashboardID = 'anotherDash' + const startingAutoRefresh = { + interval: 1000, + status: AutoRefreshStatus.Active, + } + const startingState = { + [dashboardID]: startingAutoRefresh, + ['dash']: AUTOREFRESH_DEFAULT, + } + const actual = autoRefreshReducer( + startingState, + setAutoRefreshStatus(dashboardID, status) + ) + + const expected = { + [dashboardID]: {...startingAutoRefresh, status}, + ['dash']: AUTOREFRESH_DEFAULT, + } + + expect(actual).toEqual(expected) + }) +}) diff --git a/ui/src/shared/reducers/autoRefresh.ts b/ui/src/shared/reducers/autoRefresh.ts new file mode 100644 index 00000000000..1446e0fc523 --- /dev/null +++ b/ui/src/shared/reducers/autoRefresh.ts @@ -0,0 +1,46 @@ +// Libraries +import {produce} from 'immer' + +// Constants +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' + +// Types +import {Action} from 'src/shared/actions/autoRefresh' +import {AutoRefresh} from 'src/types' + +export interface AutoRefreshState { + [dashboardID: string]: AutoRefresh +} + +export const initialState = (): AutoRefreshState => { + return {} +} + +export const autoRefreshReducer = (state = initialState(), action: Action) => + produce(state, draftState => { + switch (action.type) { + case 'SET_AUTO_REFRESH_INTERVAL': { + const {dashboardID, milliseconds} = action.payload + + if (!draftState[dashboardID]) { + draftState[dashboardID] = AUTOREFRESH_DEFAULT + } + + draftState[dashboardID].interval = milliseconds + + return + } + + case 'SET_AUTO_REFRESH_STATUS': { + const {dashboardID, status} = action.payload + + if (!draftState[dashboardID]) { + draftState[dashboardID] = AUTOREFRESH_DEFAULT + } + + draftState[dashboardID].status = status + + return + } + } + }) diff --git a/ui/src/store/configureStore.ts b/ui/src/store/configureStore.ts index db9dd29870b..d0ca043b415 100644 --- a/ui/src/store/configureStore.ts +++ b/ui/src/store/configureStore.ts @@ -29,6 +29,7 @@ import templatesReducer from 'src/templates/reducers' import {scrapersReducer} from 'src/scrapers/reducers' import {userSettingsReducer} from 'src/userSettings/reducers' import {membersReducer} from 'src/members/reducers' +import {autoRefreshReducer} from 'src/shared/reducers/autoRefresh' // Types import {LocalStorage} from 'src/types/localStorage' @@ -39,6 +40,7 @@ type ReducerState = Pick> export const rootReducer = combineReducers({ ...sharedReducers, ranges: rangesReducer, + autoRefresh: autoRefreshReducer, dashboards: dashboardsReducer, timeMachines: timeMachinesReducer, routing: routerReducer, diff --git a/ui/src/timeMachine/actions/index.ts b/ui/src/timeMachine/actions/index.ts index 77c30a6f6f2..e05228aaeee 100644 --- a/ui/src/timeMachine/actions/index.ts +++ b/ui/src/timeMachine/actions/index.ts @@ -19,6 +19,7 @@ import { FieldOption, TableOptions, TimeMachineTab, + AutoRefresh, } from 'src/types' import {Color} from 'src/types/colors' import {HistogramPosition} from '@influxdata/vis' @@ -30,6 +31,7 @@ export type Action = | SetActiveTabAction | SetNameAction | SetTimeRangeAction + | SetAutoRefreshAction | SetTypeAction | SetActiveQueryText | SetIsViewingRawDataAction @@ -120,6 +122,18 @@ export const setTimeRange = (timeRange: TimeRange) => dispatch => { dispatch(reloadTagSelectors()) } +interface SetAutoRefreshAction { + type: 'SET_AUTO_REFRESH' + payload: {autoRefresh: AutoRefresh} +} + +export const setAutoRefresh = ( + autoRefresh: AutoRefresh +): SetAutoRefreshAction => ({ + type: 'SET_AUTO_REFRESH', + payload: {autoRefresh}, +}) + interface SetTypeAction { type: 'SET_VIEW_TYPE' payload: {type: ViewType} diff --git a/ui/src/timeMachine/components/Queries.tsx b/ui/src/timeMachine/components/Queries.tsx index 5f8cbbbc5e2..e030451eae2 100644 --- a/ui/src/timeMachine/components/Queries.tsx +++ b/ui/src/timeMachine/components/Queries.tsx @@ -7,7 +7,9 @@ import TimeMachineFluxEditor from 'src/timeMachine/components/TimeMachineFluxEdi import CSVExportButton from 'src/shared/components/CSVExportButton' import TimeMachineQueriesSwitcher from 'src/timeMachine/components/QueriesSwitcher' import TimeMachineRefreshDropdown from 'src/timeMachine/components/RefreshDropdown' -import TimeRangeDropdown from 'src/shared/components/TimeRangeDropdown' +import TimeRangeDropdown, { + RangeType, +} from 'src/shared/components/TimeRangeDropdown' import TimeMachineQueryTab from 'src/timeMachine/components/QueryTab' import TimeMachineQueryBuilder from 'src/timeMachine/components/QueryBuilder' import SubmitQueryButton from 'src/timeMachine/components/SubmitQueryButton' @@ -24,32 +26,41 @@ import { } from '@influxdata/clockface' // Actions -import {addQuery} from 'src/timeMachine/actions' +import {addQuery, setAutoRefresh} from 'src/timeMachine/actions' import {setTimeRange} from 'src/timeMachine/actions' // Utils import {getActiveTimeMachine, getActiveQuery} from 'src/timeMachine/selectors' // Types -import {AppState, DashboardQuery, QueryEditMode, TimeRange} from 'src/types' +import { + AppState, + DashboardQuery, + QueryEditMode, + TimeRange, + AutoRefresh, + AutoRefreshStatus, +} from 'src/types' import {DashboardDraftQuery} from 'src/types/dashboards' interface StateProps { activeQuery: DashboardQuery draftQueries: DashboardDraftQuery[] timeRange: TimeRange + autoRefresh: AutoRefresh } interface DispatchProps { onAddQuery: typeof addQuery onSetTimeRange: typeof setTimeRange + onSetAutoRefresh: typeof setAutoRefresh } type Props = StateProps & DispatchProps class TimeMachineQueries extends PureComponent { public render() { - const {draftQueries, onAddQuery, timeRange, onSetTimeRange} = this.props + const {draftQueries, onAddQuery, timeRange} = this.props return (
@@ -82,7 +93,7 @@ class TimeMachineQueries extends PureComponent { @@ -94,6 +105,29 @@ class TimeMachineQueries extends PureComponent { ) } + private handleSetTimeRange = ( + timeRange: TimeRange, + rangeType: RangeType = RangeType.Relative + ) => { + const {autoRefresh, onSetAutoRefresh, onSetTimeRange} = this.props + + onSetTimeRange(timeRange) + + if (rangeType === RangeType.Absolute) { + onSetAutoRefresh({...autoRefresh, status: AutoRefreshStatus.Disabled}) + return + } + + if (autoRefresh.status === AutoRefreshStatus.Disabled) { + if (autoRefresh.interval === 0) { + onSetAutoRefresh({...autoRefresh, status: AutoRefreshStatus.Paused}) + return + } + + onSetAutoRefresh({...autoRefresh, status: AutoRefreshStatus.Active}) + } + } + private get queryEditor(): JSX.Element { const {activeQuery} = this.props @@ -108,16 +142,17 @@ class TimeMachineQueries extends PureComponent { } const mstp = (state: AppState) => { - const {draftQueries, timeRange} = getActiveTimeMachine(state) + const {draftQueries, timeRange, autoRefresh} = getActiveTimeMachine(state) const activeQuery = getActiveQuery(state) - return {timeRange, activeQuery, draftQueries} + return {timeRange, activeQuery, draftQueries, autoRefresh} } const mdtp = { onAddQuery: addQuery, onSetTimeRange: setTimeRange, + onSetAutoRefresh: setAutoRefresh, } export default connect( diff --git a/ui/src/timeMachine/components/RefreshDropdown.tsx b/ui/src/timeMachine/components/RefreshDropdown.tsx index f27c4446599..7e83a91e3fc 100644 --- a/ui/src/timeMachine/components/RefreshDropdown.tsx +++ b/ui/src/timeMachine/components/RefreshDropdown.tsx @@ -1,6 +1,7 @@ // Libraries import React, {PureComponent} from 'react' import {connect} from 'react-redux' +import {isEqual} from 'lodash' // Components import AutoRefreshDropdown from 'src/shared/components/dropdown_auto_refresh/AutoRefreshDropdown' @@ -10,31 +11,43 @@ import {AutoRefresher} from 'src/utils/AutoRefresher' // Actions import {executeQueries} from 'src/timeMachine/actions/queries' +import {AutoRefreshStatus, AutoRefresh, AppState} from 'src/types' +import {setAutoRefresh} from 'src/timeMachine/actions' +import {getActiveTimeMachine} from 'src/timeMachine/selectors' interface DispatchProps { onExecuteQueries: typeof executeQueries + onSetAutoRefresh: typeof setAutoRefresh } -interface State { - autoRefreshInterval: number +interface StateProps { + autoRefresh: AutoRefresh } -class TimeMachineRefreshDropdown extends PureComponent { - public state: State = {autoRefreshInterval: 0} +type Props = StateProps & DispatchProps + +class TimeMachineRefreshDropdown extends PureComponent { private autoRefresher = new AutoRefresher() public componentDidMount() { - const {autoRefreshInterval} = this.state + const {autoRefresh} = this.props + if (autoRefresh.status === AutoRefreshStatus.Active) { + this.autoRefresher.poll(autoRefresh.interval) + } - this.autoRefresher.poll(autoRefreshInterval) this.autoRefresher.subscribe(this.executeQueries) } - public componentDidUpdate(__, prevState) { - const {autoRefreshInterval} = this.state + public componentDidUpdate(prevProps) { + const {autoRefresh} = this.props + + if (!isEqual(autoRefresh, prevProps.autoRefresh)) { + if (autoRefresh.status === AutoRefreshStatus.Active) { + this.autoRefresher.poll(autoRefresh.interval) + return + } - if (autoRefreshInterval !== prevState.autoRefreshInterval) { - this.autoRefresher.poll(autoRefreshInterval) + this.autoRefresher.stopPolling() } } @@ -44,19 +57,34 @@ class TimeMachineRefreshDropdown extends PureComponent { } public render() { - const {autoRefreshInterval} = this.state + const {autoRefresh} = this.props return ( ) } - private handleChooseInterval = (autoRefreshInterval: number) => { - this.setState({autoRefreshInterval}) + private handleChooseAutoRefresh = (interval: number) => { + const {onSetAutoRefresh, autoRefresh} = this.props + + if (interval === 0) { + onSetAutoRefresh({ + ...autoRefresh, + status: AutoRefreshStatus.Paused, + interval, + }) + return + } + + onSetAutoRefresh({ + ...autoRefresh, + interval, + status: AutoRefreshStatus.Active, + }) } private executeQueries = () => { @@ -64,11 +92,18 @@ class TimeMachineRefreshDropdown extends PureComponent { } } +const mstp = (state: AppState): StateProps => { + const {autoRefresh} = getActiveTimeMachine(state) + + return {autoRefresh} +} + const mdtp: DispatchProps = { onExecuteQueries: executeQueries, + onSetAutoRefresh: setAutoRefresh, } -export default connect<{}, DispatchProps>( - null, +export default connect( + mstp, mdtp )(TimeMachineRefreshDropdown) diff --git a/ui/src/timeMachine/reducers/index.ts b/ui/src/timeMachine/reducers/index.ts index 952dc88319d..990bde46700 100644 --- a/ui/src/timeMachine/reducers/index.ts +++ b/ui/src/timeMachine/reducers/index.ts @@ -14,7 +14,7 @@ import { } from 'src/timeMachine/constants' // Types -import {TimeRange, View} from 'src/types' +import {TimeRange, View, AutoRefresh} from 'src/types' import { ViewType, DashboardDraftQuery, @@ -28,6 +28,7 @@ import {Action} from 'src/timeMachine/actions' import {TimeMachineTab} from 'src/types/timeMachine' import {RemoteDataState} from 'src/types' import {Color} from 'src/types/colors' +import {AUTOREFRESH_DEFAULT} from 'src/shared/constants' interface QueryBuilderState { buckets: string[] @@ -53,6 +54,7 @@ interface QueryResultsState { export interface TimeMachineState { view: QueryView timeRange: TimeRange + autoRefresh: AutoRefresh draftQueries: DashboardDraftQuery[] isViewingRawData: boolean activeTab: TimeMachineTab @@ -70,6 +72,7 @@ export interface TimeMachinesState { export const initialStateHelper = (): TimeMachineState => ({ timeRange: {lower: 'now() - 1h'}, + autoRefresh: AUTOREFRESH_DEFAULT, view: createView(), draftQueries: [{...defaultViewQuery(), hidden: false}], isViewingRawData: false, @@ -177,6 +180,14 @@ export const timeMachineReducer = ( }) } + case 'SET_AUTO_REFRESH': { + return produce(state, draftState => { + draftState.autoRefresh = action.payload.autoRefresh + + buildAllQueries(draftState) + }) + } + case 'SET_VIEW_TYPE': { const {type} = action.payload const view = convertView(state.view, type) diff --git a/ui/src/types/autoRefresh.ts b/ui/src/types/autoRefresh.ts new file mode 100644 index 00000000000..250addd1015 --- /dev/null +++ b/ui/src/types/autoRefresh.ts @@ -0,0 +1,10 @@ +export enum AutoRefreshStatus { + Active = 'active', + Disabled = 'disabled', + Paused = 'paused', +} + +export interface AutoRefresh { + status: AutoRefreshStatus + interval: number +} diff --git a/ui/src/types/index.ts b/ui/src/types/index.ts index d4b0ffc5a6e..2968e8f1009 100644 --- a/ui/src/types/index.ts +++ b/ui/src/types/index.ts @@ -27,3 +27,4 @@ export * from './tasks' export * from './templates' export * from './timeMachine' export * from './members' +export * from './autoRefresh' diff --git a/ui/src/types/localStorage.ts b/ui/src/types/localStorage.ts index c5159d89ce2..c7f6cbc3e1e 100644 --- a/ui/src/types/localStorage.ts +++ b/ui/src/types/localStorage.ts @@ -2,11 +2,13 @@ import {AppState} from 'src/shared/reducers/app' import {VariablesState} from 'src/variables/reducers' import {UserSettingsState} from 'src/userSettings/reducers' import {OrgsState} from 'src/organizations/reducers/orgs' +import {AutoRefreshState} from 'src/shared/reducers/autoRefresh' export interface LocalStorage { VERSION: string app: AppState ranges: any[] + autoRefresh: AutoRefreshState variables: VariablesState userSettings: UserSettingsState orgs: OrgsState diff --git a/ui/src/types/stores.ts b/ui/src/types/stores.ts index e192daebb6b..d02b076ee88 100644 --- a/ui/src/types/stores.ts +++ b/ui/src/types/stores.ts @@ -22,6 +22,7 @@ import {UserSettingsState} from 'src/userSettings/reducers' import {DashboardsState} from 'src/dashboards/reducers/dashboards' import {OrgsState} from 'src/organizations/reducers/orgs' import {MembersState} from 'src/members/reducers' +import {AutoRefreshState} from 'src/shared/reducers/autoRefresh' export interface AppState { VERSION: string @@ -31,6 +32,7 @@ export interface AppState { links: Links app: AppPresentationState ranges: RangeState + autoRefresh: AutoRefreshState views: ViewsState dashboards: DashboardsState notifications: Notification[]