diff --git a/public/app/core/utils/timePicker.test.ts b/public/app/core/utils/timePicker.test.ts new file mode 100644 index 00000000000..b9c54a92d8a --- /dev/null +++ b/public/app/core/utils/timePicker.test.ts @@ -0,0 +1,79 @@ +import { toUtc, AbsoluteTimeRange } from '@grafana/ui'; + +import { getShiftedTimeRange, getZoomedTimeRange } from './timePicker'; + +export const setup = (options?: any) => { + const defaultOptions = { + range: { + from: toUtc('2019-01-01 10:00:00'), + to: toUtc('2019-01-01 16:00:00'), + raw: { + from: 'now-6h', + to: 'now', + }, + }, + direction: 0, + }; + + return { ...defaultOptions, ...options }; +}; + +describe('getShiftedTimeRange', () => { + describe('when called with a direction of -1', () => { + it('then it should return correct result', () => { + const { range, direction } = setup({ direction: -1 }); + const expectedRange: AbsoluteTimeRange = { + from: toUtc('2019-01-01 07:00:00').valueOf(), + to: toUtc('2019-01-01 13:00:00').valueOf(), + }; + + const result = getShiftedTimeRange(direction, range); + + expect(result).toEqual(expectedRange); + }); + }); + + describe('when called with a direction of 1', () => { + it('then it should return correct result', () => { + const { range, direction } = setup({ direction: 1 }); + const expectedRange: AbsoluteTimeRange = { + from: toUtc('2019-01-01 13:00:00').valueOf(), + to: toUtc('2019-01-01 19:00:00').valueOf(), + }; + + const result = getShiftedTimeRange(direction, range); + + expect(result).toEqual(expectedRange); + }); + }); + + describe('when called with any other direction', () => { + it('then it should return correct result', () => { + const { range, direction } = setup({ direction: 0 }); + const expectedRange: AbsoluteTimeRange = { + from: toUtc('2019-01-01 10:00:00').valueOf(), + to: toUtc('2019-01-01 16:00:00').valueOf(), + }; + + const result = getShiftedTimeRange(direction, range); + + expect(result).toEqual(expectedRange); + }); + }); +}); + +describe('getZoomedTimeRange', () => { + describe('when called', () => { + it('then it should return correct result', () => { + const { range } = setup(); + const expectedRange: AbsoluteTimeRange = { + from: toUtc('2019-01-01 07:00:00').valueOf(), + to: toUtc('2019-01-01 19:00:00').valueOf(), + }; + + const result = getZoomedTimeRange(range, 2); + + expect(result).toEqual(expectedRange); + }); + }); +}); diff --git a/public/app/core/utils/timePicker.ts b/public/app/core/utils/timePicker.ts new file mode 100644 index 00000000000..974588857a5 --- /dev/null +++ b/public/app/core/utils/timePicker.ts @@ -0,0 +1,38 @@ +import { TimeRange, toUtc, AbsoluteTimeRange } from '@grafana/ui'; + +export const getShiftedTimeRange = (direction: number, origRange: TimeRange): AbsoluteTimeRange => { + const range = { + from: toUtc(origRange.from), + to: toUtc(origRange.to), + }; + + const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; + let to: number, from: number; + + if (direction === -1) { + to = range.to.valueOf() - timespan; + from = range.from.valueOf() - timespan; + } else if (direction === 1) { + to = range.to.valueOf() + timespan; + from = range.from.valueOf() + timespan; + if (to > Date.now() && range.to.valueOf() < Date.now()) { + to = Date.now(); + from = range.from.valueOf(); + } + } else { + to = range.to.valueOf(); + from = range.from.valueOf(); + } + + return { from, to }; +}; + +export const getZoomedTimeRange = (range: TimeRange, factor: number): AbsoluteTimeRange => { + const timespan = range.to.valueOf() - range.from.valueOf(); + const center = range.to.valueOf() - timespan / 2; + + const to = center + (timespan * factor) / 2; + const from = center - (timespan * factor) / 2; + + return { from, to }; +}; diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 065596d73df..401e9347ec5 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -16,6 +16,7 @@ import { TimePicker, RefreshPicker, RawTimeRange } from '@grafana/ui'; // Utils & Services import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; +import { getShiftedTimeRange } from 'app/core/utils/timePicker'; export interface Props { $injector: any; @@ -44,23 +45,7 @@ export class DashNavTimeControls extends Component { onMoveTimePicker = (direction: number) => { const range = this.timeSrv.timeRange(); - const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; - let to: number, from: number; - - if (direction === -1) { - to = range.to.valueOf() - timespan; - from = range.from.valueOf() - timespan; - } else if (direction === 1) { - to = range.to.valueOf() + timespan; - from = range.from.valueOf() + timespan; - if (to > Date.now() && range.to.valueOf() < Date.now()) { - to = Date.now(); - from = range.from.valueOf(); - } - } else { - to = range.to.valueOf(); - from = range.from.valueOf(); - } + const { from, to } = getShiftedTimeRange(direction, range); this.timeSrv.setTime({ from: toUtc(from), diff --git a/public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts b/public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts deleted file mode 100644 index f1d626e9e34..00000000000 --- a/public/app/features/dashboard/components/TimePicker/TimePickerCtrl.ts +++ /dev/null @@ -1,189 +0,0 @@ -import _ from 'lodash'; -import angular from 'angular'; - -import * as rangeUtil from '@grafana/ui/src/utils/rangeutil'; - -export class TimePickerCtrl { - static tooltipFormat = 'MMM D, YYYY HH:mm:ss'; - static defaults = { - time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'], - refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], - }; - - dashboard: any; - panel: any; - absolute: any; - timeRaw: any; - editTimeRaw: any; - tooltip: string; - rangeString: string; - timeOptions: any; - refresh: any; - isUtc: boolean; - firstDayOfWeek: number; - isOpen: boolean; - isAbsolute: boolean; - - /** @ngInject */ - constructor(private $scope, private $rootScope, private timeSrv) { - this.$scope.ctrl = this; - - $rootScope.onAppEvent('shift-time-forward', () => this.move(1), $scope); - $rootScope.onAppEvent('shift-time-backward', () => this.move(-1), $scope); - $rootScope.onAppEvent('closeTimepicker', this.openDropdown.bind(this), $scope); - - this.dashboard.on('refresh', this.onRefresh.bind(this), $scope); - - // init options - this.panel = this.dashboard.timepicker; - _.defaults(this.panel, TimePickerCtrl.defaults); - this.firstDayOfWeek = getLocaleData().firstDayOfWeek(); - - // init time stuff - this.onRefresh(); - } - - onRefresh() { - const time = angular.copy(this.timeSrv.timeRange()); - const timeRaw = angular.copy(time.raw); - - if (!this.dashboard.isTimezoneUtc()) { - time.from.local(); - time.to.local(); - if (isDateTime(timeRaw.from)) { - timeRaw.from.local(); - } - if (isDateTime(timeRaw.to)) { - timeRaw.to.local(); - } - this.isUtc = false; - } else { - this.isUtc = true; - } - - this.rangeString = rangeUtil.describeTimeRange(timeRaw); - this.absolute = { fromJs: time.from.toDate(), toJs: time.to.toDate() }; - this.tooltip = this.dashboard.formatDate(time.from) + '
to
'; - this.tooltip += this.dashboard.formatDate(time.to); - this.timeRaw = timeRaw; - this.isAbsolute = isDateTime(this.timeRaw.to); - } - - zoom(factor) { - this.$rootScope.appEvent('zoom-out', 2); - } - - move(direction) { - const range = this.timeSrv.timeRange(); - - const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; - let to, from; - if (direction === -1) { - to = range.to.valueOf() - timespan; - from = range.from.valueOf() - timespan; - } else if (direction === 1) { - to = range.to.valueOf() + timespan; - from = range.from.valueOf() + timespan; - if (to > Date.now() && range.to < Date.now()) { - to = Date.now(); - from = range.from.valueOf(); - } - } else { - to = range.to.valueOf(); - from = range.from.valueOf(); - } - - this.timeSrv.setTime({ from: toUtc(from), to: toUtc(to) }); - } - - openDropdown() { - if (this.isOpen) { - this.closeDropdown(); - return; - } - - this.onRefresh(); - this.editTimeRaw = this.timeRaw; - this.timeOptions = rangeUtil.getRelativeTimesList(this.panel, this.rangeString); - this.refresh = { - value: this.dashboard.refresh, - options: this.panel.refresh_intervals.map((interval: any) => { - return { text: interval, value: interval }; - }), - }; - - this.refresh.options.unshift({ text: 'off' }); - this.isOpen = true; - this.$rootScope.appEvent('timepickerOpen'); - } - - closeDropdown() { - this.isOpen = false; - this.$rootScope.appEvent('timepickerClosed'); - } - - applyCustom() { - if (this.refresh.value !== this.dashboard.refresh) { - this.timeSrv.setAutoRefresh(this.refresh.value); - } - - this.timeSrv.setTime(this.editTimeRaw); - this.closeDropdown(); - } - - absoluteFromChanged() { - this.editTimeRaw.from = this.getAbsoluteMomentForTimezone(this.absolute.fromJs); - } - - absoluteToChanged() { - this.editTimeRaw.to = this.getAbsoluteMomentForTimezone(this.absolute.toJs); - } - - getAbsoluteMomentForTimezone(jsDate) { - return this.dashboard.isTimezoneUtc() ? dateTime(jsDate).utc() : dateTime(jsDate); - } - - setRelativeFilter(timespan) { - const range = { from: timespan.from, to: timespan.to }; - - if (this.panel.nowDelay && range.to === 'now') { - range.to = 'now-' + this.panel.nowDelay; - } - - this.timeSrv.setTime(range); - this.closeDropdown(); - } -} - -export function settingsDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/components/TimePicker/settings.html', - controller: TimePickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - dashboard: '=', - }, - }; -} - -export function timePickerDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/components/TimePicker/template.html', - controller: TimePickerCtrl, - bindToController: true, - controllerAs: 'ctrl', - scope: { - dashboard: '=', - }, - }; -} - -angular.module('grafana.directives').directive('gfTimePickerSettings', settingsDirective); -angular.module('grafana.directives').directive('gfTimePicker', timePickerDirective); - -import { inputDateDirective } from './validation'; -import { toUtc, getLocaleData, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; -angular.module('grafana.directives').directive('inputDatetime', inputDateDirective); diff --git a/public/app/features/dashboard/components/TimePicker/index.ts b/public/app/features/dashboard/components/TimePicker/index.ts deleted file mode 100644 index ca6e2792c43..00000000000 --- a/public/app/features/dashboard/components/TimePicker/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { TimePickerCtrl } from './TimePickerCtrl'; diff --git a/public/app/features/dashboard/components/TimePicker/settings.html b/public/app/features/dashboard/components/TimePicker/settings.html deleted file mode 100644 index fd5170013c2..00000000000 --- a/public/app/features/dashboard/components/TimePicker/settings.html +++ /dev/null @@ -1,24 +0,0 @@ -
-
Time Options
- -
-
- -
- -
-
- -
- Auto-refresh - -
-
- Now delay now- - -
- - -
-
diff --git a/public/app/features/dashboard/components/TimePicker/template.html b/public/app/features/dashboard/components/TimePicker/template.html deleted file mode 100644 index 2821dd0ced5..00000000000 --- a/public/app/features/dashboard/components/TimePicker/template.html +++ /dev/null @@ -1,84 +0,0 @@ - - - - - - - - - - -
-
-
- Quick ranges -
-
-
    -
  • - -
  • -
-
-
- -
-
- Custom range -
-
- -
-
- -
-
- -
-
- -
- -
- - - -
-
- -
-
- -
-
- -
- -
- -
-
- -
-
-
-
-
- diff --git a/public/app/features/dashboard/components/TimePicker/validation.ts b/public/app/features/dashboard/components/TimePicker/validation.ts deleted file mode 100644 index a99409a0fb0..00000000000 --- a/public/app/features/dashboard/components/TimePicker/validation.ts +++ /dev/null @@ -1,49 +0,0 @@ -import * as dateMath from '@grafana/ui/src/utils/datemath'; -import { toUtc, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper'; - -export function inputDateDirective() { - return { - restrict: 'A', - require: 'ngModel', - link: ($scope, $elem, attrs, ngModel) => { - const format = 'YYYY-MM-DD HH:mm:ss'; - - const fromUser = text => { - if (text.indexOf('now') !== -1) { - if (!dateMath.isValid(text)) { - ngModel.$setValidity('error', false); - return undefined; - } - ngModel.$setValidity('error', true); - return text; - } - - let parsed; - if ($scope.ctrl.isUtc) { - parsed = toUtc(text, format); - } else { - parsed = dateTime(text, format); - } - - if (!parsed.isValid()) { - ngModel.$setValidity('error', false); - return undefined; - } - - ngModel.$setValidity('error', true); - return parsed; - }; - - const toUser = currentValue => { - if (isDateTime(currentValue)) { - return currentValue.format(format); - } else { - return currentValue; - } - }; - - ngModel.$parsers.push(fromUser); - ngModel.$formatters.push(toUser); - }, - }; -} diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index 1a326d73bd9..e2c042ef138 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -12,7 +12,6 @@ import './components/FolderPicker'; import './components/VersionHistory'; import './components/DashboardSettings'; import './components/SubMenu'; -import './components/TimePicker'; import './components/UnsavedChangesModal'; import './components/SaveModals'; import './components/ShareModal'; diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 1716563874e..5c1a305a2ba 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -12,6 +12,7 @@ import { ITimeoutService, ILocationService } from 'angular'; import { ContextSrv } from 'app/core/services/context_srv'; import { DashboardModel } from '../state/DashboardModel'; import { toUtc, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper'; +import { getZoomedTimeRange } from 'app/core/utils/timePicker'; export class TimeSrv { time: any; @@ -238,12 +239,7 @@ export class TimeSrv { zoomOut(e: any, factor: number) { const range = this.timeRange(); - - const timespan = range.to.valueOf() - range.from.valueOf(); - const center = range.to.valueOf() - timespan / 2; - - const to = center + (timespan * factor) / 2; - const from = center - (timespan * factor) / 2; + const { from, to } = getZoomedTimeRange(range, factor); this.setTime({ from: toUtc(from), to: toUtc(to) }); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b662d961158..ffd04e6a235 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -16,7 +16,6 @@ import GraphContainer from './GraphContainer'; import LogsContainer from './LogsContainer'; import QueryRows from './QueryRows'; import TableContainer from './TableContainer'; -import TimePicker from './TimePicker'; // Actions import { @@ -35,7 +34,6 @@ import { RawTimeRange, DataQuery, ExploreStartPageProps, DataSourceApi, DataQuer import { ExploreItemState, ExploreUrlState, - RangeScanner, ExploreId, ExploreUpdateState, ExploreUIState, @@ -71,7 +69,6 @@ interface ExploreProps { update: ExploreUpdateState; reconnectDatasource: typeof reconnectDatasource; refreshExplore: typeof refreshExplore; - scanner?: RangeScanner; scanning?: boolean; scanRange?: RawTimeRange; scanStart: typeof scanStart; @@ -117,15 +114,10 @@ interface ExploreProps { export class Explore extends React.PureComponent { el: any; exploreEvents: Emitter; - /** - * Timepicker to control scanning - */ - timepickerRef: React.RefObject; constructor(props: ExploreProps) { super(props); this.exploreEvents = new Emitter(); - this.timepickerRef = React.createRef(); } componentDidMount() { @@ -159,11 +151,9 @@ export class Explore extends React.PureComponent { this.el = el; }; - onChangeTime = (rawRange: RawTimeRange, changedByScanner?: boolean) => { - const { updateTimeRange, exploreId, scanning } = this.props; - if (scanning && !changedByScanner) { - this.onStopScanning(); - } + onChangeTime = (rawRange: RawTimeRange) => { + const { updateTimeRange, exploreId } = this.props; + updateTimeRange({ exploreId, rawRange }); }; @@ -190,13 +180,7 @@ export class Explore extends React.PureComponent { onStartScanning = () => { // Scanner will trigger a query - const scanner = this.scanPreviousRange; - this.props.scanStart(this.props.exploreId, scanner); - }; - - scanPreviousRange = (): RawTimeRange => { - // Calling move() on the timepicker will trigger this.onChangeTime() - return this.timepickerRef.current.move(-1, true); + this.props.scanStart(this.props.exploreId); }; onStopScanning = () => { @@ -244,7 +228,7 @@ export class Explore extends React.PureComponent { return (
- + {datasourceLoading ?
Loading datasource...
: null} {datasourceMissing ? this.renderEmptyState() : null} diff --git a/public/app/features/explore/ExploreTimeControls.tsx b/public/app/features/explore/ExploreTimeControls.tsx new file mode 100644 index 00000000000..7ae5bf10a7b --- /dev/null +++ b/public/app/features/explore/ExploreTimeControls.tsx @@ -0,0 +1,112 @@ +// Libaries +import React, { Component } from 'react'; + +// Types +import { ExploreId } from 'app/types'; +import { TimeRange, TimeOption, TimeZone, SetInterval, toUtc, dateTime } from '@grafana/ui'; + +// State + +// Components +import { TimePicker, RefreshPicker, RawTimeRange } from '@grafana/ui'; + +// Utils & Services +import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; +import { getShiftedTimeRange, getZoomedTimeRange } from 'app/core/utils/timePicker'; + +export interface Props { + exploreId: ExploreId; + hasLiveOption: boolean; + isLive: boolean; + loading: boolean; + range: TimeRange; + refreshInterval: string; + timeZone: TimeZone; + onRunQuery: () => void; + onChangeRefreshInterval: (interval: string) => void; + onChangeTime: (range: RawTimeRange) => void; +} + +export class ExploreTimeControls extends Component { + onMoveTimePicker = (direction: number) => { + const { range, onChangeTime, timeZone } = this.props; + const { from, to } = getShiftedTimeRange(direction, range); + const nextTimeRange = { + from: timeZone === 'utc' ? toUtc(from) : dateTime(from), + to: timeZone === 'utc' ? toUtc(to) : dateTime(to), + }; + + onChangeTime(nextTimeRange); + }; + + onMoveForward = () => this.onMoveTimePicker(1); + onMoveBack = () => this.onMoveTimePicker(-1); + + onChangeTimePicker = (timeRange: TimeRange) => { + this.props.onChangeTime(timeRange.raw); + }; + + onZoom = () => { + const { range, onChangeTime, timeZone } = this.props; + const { from, to } = getZoomedTimeRange(range, 2); + const nextTimeRange = { + from: timeZone === 'utc' ? toUtc(from) : dateTime(from), + to: timeZone === 'utc' ? toUtc(to) : dateTime(to), + }; + + onChangeTime(nextTimeRange); + }; + + setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => { + return timeOptions.map(option => { + if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) { + return { + ...option, + active: true, + }; + } + return { + ...option, + active: false, + }; + }); + }; + + render() { + const { + hasLiveOption, + isLive, + loading, + range, + refreshInterval, + timeZone, + onRunQuery, + onChangeRefreshInterval, + } = this.props; + + return ( + <> + {!isLive && ( + + )} + + + {refreshInterval && } + + ); + } +} diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 75f8cc75b7c..ed8f27d561f 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -3,15 +3,7 @@ import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { ExploreId, ExploreMode } from 'app/types/explore'; -import { - DataSourceSelectItem, - RawTimeRange, - ClickOutsideWrapper, - TimeZone, - TimeRange, - SelectOptionItem, - LoadingState, -} from '@grafana/ui'; +import { DataSourceSelectItem, RawTimeRange, TimeZone, TimeRange, SelectOptionItem, LoadingState } from '@grafana/ui'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { StoreState } from 'app/types/store'; import { @@ -23,10 +15,9 @@ import { changeRefreshInterval, changeMode, } from './state/actions'; -import TimePicker from './TimePicker'; import { getTimeZone } from '../profile/state/selectors'; -import { RefreshPicker, SetInterval } from '@grafana/ui'; import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup'; +import { ExploreTimeControls } from './ExploreTimeControls'; enum IconSide { left = 'left', @@ -63,7 +54,6 @@ const createResponsiveButton = (options: { interface OwnProps { exploreId: ExploreId; - timepickerRef: React.RefObject; onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void; } @@ -111,10 +101,6 @@ export class UnConnectedExploreToolbar extends PureComponent { return this.props.runQueries(this.props.exploreId); }; - onCloseTimePicker = () => { - this.props.timepickerRef.current.setState({ isOpen: false }); - }; - onChangeRefreshInterval = (item: string) => { const { changeRefreshInterval, exploreId } = this.props; changeRefreshInterval(exploreId, item); @@ -136,7 +122,6 @@ export class UnConnectedExploreToolbar extends PureComponent { timeZone, selectedDatasource, splitted, - timepickerRef, refreshInterval, onChangeTime, split, @@ -214,20 +199,18 @@ export class UnConnectedExploreToolbar extends PureComponent {
) : null}
- {!isLive && ( - - - - )} - - - {refreshInterval && }
diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 1d6d6460ff8..1632bc68405 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -101,7 +101,7 @@ export default class Logs extends PureComponent { } } - componentDidUpdate(prevProps, prevState) { + componentDidUpdate(prevProps: Props, prevState: State) { // Staged rendering if (prevState.deferLogs && !this.state.deferLogs && !this.state.renderAll) { this.renderAllTimer = setTimeout(() => this.setState({ renderAll: true }), 2000); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 93fb547eb87..a5ef33a6945 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -11,6 +11,7 @@ import { LogRowModel, LogsDedupStrategy, LoadingState, + TimeRange, } from '@grafana/ui'; import { ExploreId, ExploreItemState } from 'app/types/explore'; @@ -47,6 +48,7 @@ interface LogsContainerProps { isLive: boolean; stopLive: typeof changeRefreshIntervalAction; updateTimeRange: typeof updateTimeRange; + range: TimeRange; absoluteRange: AbsoluteTimeRange; } @@ -90,7 +92,9 @@ export class LogsContainer extends Component { return ( nextProps.loading !== this.props.loading || nextProps.dedupStrategy !== this.props.dedupStrategy || - nextProps.logsHighlighterExpressions !== this.props.logsHighlighterExpressions + nextProps.logsHighlighterExpressions !== this.props.logsHighlighterExpressions || + nextProps.scanning !== this.props.scanning || + nextProps.isLive !== this.props.isLive ); } @@ -107,7 +111,7 @@ export class LogsContainer extends Component { absoluteRange, timeZone, scanning, - scanRange, + range, width, hiddenLogLevels, isLive, @@ -139,7 +143,7 @@ export class LogsContainer extends Component { absoluteRange={absoluteRange} timeZone={timeZone} scanning={scanning} - scanRange={scanRange} + scanRange={range.raw} width={width} hiddenLogLevels={hiddenLogLevels} getRowContext={this.getLogRowContext} @@ -157,9 +161,9 @@ function mapStateToProps(state: StoreState, { exploreId }) { logsResult, loadingState, scanning, - scanRange, datasourceInstance, isLive, + range, absoluteRange, } = item; const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming; @@ -173,13 +177,13 @@ function mapStateToProps(state: StoreState, { exploreId }) { logsHighlighterExpressions, logsResult, scanning, - scanRange, timeZone, dedupStrategy, hiddenLogLevels, dedupedResult, datasourceInstance, isLive, + range, absoluteRange, }; } diff --git a/public/app/features/explore/TimePicker.test.tsx b/public/app/features/explore/TimePicker.test.tsx deleted file mode 100644 index ea793096374..00000000000 --- a/public/app/features/explore/TimePicker.test.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import React from 'react'; -import { shallow } from 'enzyme'; -import sinon from 'sinon'; - -import * as dateMath from '@grafana/ui/src/utils/datemath'; -import * as rangeUtil from '@grafana/ui/src/utils/rangeutil'; -import TimePicker from './TimePicker'; -import { RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui'; -import { toUtc, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; - -const DEFAULT_RANGE = { - from: 'now-6h', - to: 'now', -}; - -const fromRaw = (rawRange: RawTimeRange): TimeRange => { - const raw = { - from: isDateTime(rawRange.from) ? dateTime(rawRange.from) : rawRange.from, - to: isDateTime(rawRange.to) ? dateTime(rawRange.to) : rawRange.to, - }; - - return { - from: dateMath.parse(raw.from, false), - to: dateMath.parse(raw.to, true), - raw: rawRange, - }; -}; - -describe('', () => { - it('render default values when closed and relative time range', () => { - const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); - expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); - expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeFalsy(); - expect(wrapper.find('.gf-timepicker-utc').exists()).toBeFalsy(); - }); - - it('render default values when closed, utc and relative time range', () => { - const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); - expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); - expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeFalsy(); - expect(wrapper.find('.gf-timepicker-utc').exists()).toBeTruthy(); - }); - - it('renders default values when open and relative range', () => { - const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); - expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); - expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeTruthy(); - expect(wrapper.find('.gf-timepicker-utc').exists()).toBeFalsy(); - expect(wrapper.find('.timepicker-from').props().value).toBe(DEFAULT_RANGE.from); - expect(wrapper.find('.timepicker-to').props().value).toBe(DEFAULT_RANGE.to); - }); - - it('renders default values when open, utc and relative range', () => { - const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); - expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); - expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBeTruthy(); - expect(wrapper.find('.gf-timepicker-utc').exists()).toBeTruthy(); - expect(wrapper.find('.timepicker-from').props().value).toBe(DEFAULT_RANGE.from); - expect(wrapper.find('.timepicker-to').props().value).toBe(DEFAULT_RANGE.to); - }); - - it('apply with absolute range and non-utc', () => { - const range = { - from: toUtc(1), - to: toUtc(1000), - raw: { - from: toUtc(1), - to: toUtc(1000), - }, - }; - const localRange = { - from: dateTime(1), - to: dateTime(1000), - raw: { - from: dateTime(1), - to: dateTime(1000), - }, - }; - const expectedRangeString = rangeUtil.describeTimeRange(localRange); - - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); - expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); - expect(wrapper.state('initialRange')).toBe(range.raw); - expect(wrapper.find('.timepicker-rangestring').text()).toBe(expectedRangeString); - expect(wrapper.find('.timepicker-from').props().value).toBe(localRange.from.format(TIME_FORMAT)); - expect(wrapper.find('.timepicker-to').props().value).toBe(localRange.to.format(TIME_FORMAT)); - - wrapper.find('button.gf-form-btn').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(0); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000); - - expect(wrapper.state('isOpen')).toBeFalsy(); - expect(wrapper.state('rangeString')).toBe(expectedRangeString); - }); - - it('apply with absolute range and utc', () => { - const range = { - from: toUtc(1), - to: toUtc(1000), - raw: { - from: toUtc(1), - to: toUtc(1000), - }, - }; - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00'); - expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01'); - expect(wrapper.state('initialRange')).toBe(range.raw); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); - expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:00'); - expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01'); - - wrapper.find('button.gf-form-btn').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(0); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000); - - expect(wrapper.state('isOpen')).toBeFalsy(); - expect(wrapper.state('rangeString')).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); - }); - - it('moves ranges backward by half the range on left arrow click when utc', () => { - const rawRange = { - from: toUtc(2000), - to: toUtc(4000), - raw: { - from: toUtc(2000), - to: toUtc(4000), - }, - }; - const range = fromRaw(rawRange); - - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02'); - expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04'); - - wrapper.find('.timepicker-left').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(1000); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(3000); - }); - - it('moves ranges backward by half the range on left arrow click when not utc', () => { - const range = { - from: toUtc(2000), - to: toUtc(4000), - raw: { - from: toUtc(2000), - to: toUtc(4000), - }, - }; - const localRange = { - from: dateTime(2000), - to: dateTime(4000), - raw: { - from: dateTime(2000), - to: dateTime(4000), - }, - }; - - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); - expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); - - wrapper.find('.timepicker-left').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(1000); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(3000); - }); - - it('moves ranges forward by half the range on right arrow click when utc', () => { - const range = { - from: toUtc(1000), - to: toUtc(3000), - raw: { - from: toUtc(1000), - to: toUtc(3000), - }, - }; - - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01'); - expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03'); - - wrapper.find('.timepicker-right').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(2000); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(4000); - }); - - it('moves ranges forward by half the range on right arrow click when not utc', () => { - const range = { - from: toUtc(1000), - to: toUtc(3000), - raw: { - from: toUtc(1000), - to: toUtc(3000), - }, - }; - const localRange = { - from: dateTime(1000), - to: dateTime(3000), - raw: { - from: dateTime(1000), - to: dateTime(3000), - }, - }; - - const onChangeTime = sinon.spy(); - const wrapper = shallow(); - expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); - expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); - - wrapper.find('.timepicker-right').simulate('click'); - expect(onChangeTime.calledOnce).toBeTruthy(); - expect(onChangeTime.getCall(0).args[0].from.valueOf()).toBe(2000); - expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(4000); - }); -}); diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx deleted file mode 100644 index 510646dcfe4..00000000000 --- a/public/app/features/explore/TimePicker.tsx +++ /dev/null @@ -1,305 +0,0 @@ -import React, { PureComponent, ChangeEvent } from 'react'; -import * as rangeUtil from '@grafana/ui/src/utils/rangeutil'; -import { Input, RawTimeRange, TimeRange, TIME_FORMAT, TimeZone } from '@grafana/ui'; -import { toUtc, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; - -interface TimePickerProps { - isOpen?: boolean; - range: TimeRange; - timeZone: TimeZone; - onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void; -} - -interface TimePickerState { - isOpen: boolean; - isUtc: boolean; - rangeString: string; - refreshInterval?: string; - initialRange: RawTimeRange; - - // Input-controlled text, keep these in a shape that is human-editable - fromRaw: string; - toRaw: string; -} - -const getRaw = (range: any, timeZone: TimeZone) => { - const rawRange = { - from: range.raw.from, - to: range.raw.to, - }; - - if (isDateTime(rawRange.from)) { - if (timeZone === 'browser') { - rawRange.from = rawRange.from.local(); - } - rawRange.from = rawRange.from.format(TIME_FORMAT); - } - - if (isDateTime(rawRange.to)) { - if (timeZone === 'browser') { - rawRange.to = rawRange.to.local(); - } - rawRange.to = rawRange.to.format(TIME_FORMAT); - } - - return rawRange; -}; - -/** - * TimePicker with dropdown menu for relative dates. - * - * Initialize with a range that is either based on relative rawRange.strings, - * or on Moment objects. - * Internally the component needs to keep a string representation in `fromRaw` - * and `toRaw` for the controlled inputs. - * When a time is picked, `onChangeTime` is called with the new range that - * is again based on relative time strings or Moment objects. - */ -export default class TimePicker extends PureComponent { - dropdownEl: any; - - constructor(props) { - super(props); - - const { range, timeZone, isOpen } = props; - const rawRange = getRaw(range, timeZone); - - this.state = { - isOpen: isOpen, - isUtc: timeZone === 'utc', - rangeString: rangeUtil.describeTimeRange(range.raw), - fromRaw: rawRange.from, - toRaw: rawRange.to, - initialRange: range.raw, - refreshInterval: '', - }; - } - - static getDerivedStateFromProps(props: TimePickerProps, state: TimePickerState) { - if ( - state.initialRange && - state.initialRange.from === props.range.raw.from && - state.initialRange.to === props.range.raw.to - ) { - return state; - } - - const { range } = props; - const rawRange = getRaw(range, props.timeZone); - - return { - ...state, - fromRaw: rawRange.from, - toRaw: rawRange.to, - initialRange: range.raw, - rangeString: rangeUtil.describeTimeRange(range.raw), - }; - } - - move(direction: number, scanning?: boolean): RawTimeRange { - const { onChangeTime, range: origRange } = this.props; - const range = { - from: toUtc(origRange.from), - to: toUtc(origRange.to), - }; - - const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; - let to, from; - - if (direction === -1) { - to = range.to.valueOf() - timespan; - from = range.from.valueOf() - timespan; - } else if (direction === 1) { - to = range.to.valueOf() + timespan; - from = range.from.valueOf() + timespan; - } else { - to = range.to.valueOf(); - from = range.from.valueOf(); - } - - const nextTimeRange = { - from: this.props.timeZone === 'utc' ? toUtc(from) : dateTime(from), - to: this.props.timeZone === 'utc' ? toUtc(to) : dateTime(to), - }; - - if (onChangeTime) { - onChangeTime(nextTimeRange); - } - return nextTimeRange; - } - - handleChangeFrom = (event: ChangeEvent) => { - this.setState({ - fromRaw: event.target.value, - }); - }; - - handleChangeTo = (event: ChangeEvent) => { - this.setState({ - toRaw: event.target.value, - }); - }; - - handleClickApply = () => { - const { onChangeTime, timeZone } = this.props; - let rawRange; - - this.setState( - state => { - const { toRaw, fromRaw } = this.state; - rawRange = { - from: fromRaw, - to: toRaw, - }; - - if (rawRange.from.indexOf('now') === -1) { - rawRange.from = timeZone === 'utc' ? toUtc(rawRange.from, TIME_FORMAT) : dateTime(rawRange.from, TIME_FORMAT); - } - - if (rawRange.to.indexOf('now') === -1) { - rawRange.to = timeZone === 'utc' ? toUtc(rawRange.to, TIME_FORMAT) : dateTime(rawRange.to, TIME_FORMAT); - } - - const rangeString = rangeUtil.describeTimeRange(rawRange); - return { - isOpen: false, - rangeString, - }; - }, - () => { - if (onChangeTime) { - onChangeTime(rawRange); - } - } - ); - }; - - handleClickLeft = () => this.move(-1); - handleClickPicker = () => { - this.setState(state => ({ - isOpen: !state.isOpen, - })); - }; - handleClickRight = () => this.move(1); - handleClickRefresh = () => {}; - handleClickRelativeOption = range => { - const { onChangeTime } = this.props; - const rangeString = rangeUtil.describeTimeRange(range); - const rawRange = { - from: range.from, - to: range.to, - }; - this.setState( - { - toRaw: rawRange.to, - fromRaw: rawRange.from, - isOpen: false, - rangeString, - }, - () => { - if (onChangeTime) { - onChangeTime(rawRange); - } - } - ); - }; - - getTimeOptions() { - return rangeUtil.getRelativeTimesList({}, this.state.rangeString); - } - - dropdownRef = el => { - this.dropdownEl = el; - }; - - renderDropdown() { - const { fromRaw, isOpen, toRaw } = this.state; - if (!isOpen) { - return null; - } - const timeOptions = this.getTimeOptions(); - return ( -
-
-
- Quick ranges -
-
- {Object.keys(timeOptions).map(section => { - const group = timeOptions[section]; - return ( - - ); - })} -
-
- -
-
- Custom range -
-
- -
-
- -
-
- - -
-
- -
-
-
- -
-
-
-
- ); - } - - render() { - const { isUtc, rangeString, refreshInterval } = this.state; - - return ( -
-
- - - -
- {this.renderDropdown()} -
- ); - } -} diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 89fdc79dc52..cc230bc2d4f 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -16,15 +16,7 @@ import { LoadingState, AbsoluteTimeRange, } from '@grafana/ui/src/types'; -import { - ExploreId, - ExploreItemState, - HistoryItem, - RangeScanner, - ExploreUIState, - ExploreMode, - QueryOptions, -} from 'app/types/explore'; +import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore'; import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; import TableModel from 'app/core/table_model'; @@ -171,12 +163,6 @@ export interface RemoveQueryRowPayload { export interface ScanStartPayload { exploreId: ExploreId; - scanner: RangeScanner; -} - -export interface ScanRangePayload { - exploreId: ExploreId; - range: RawTimeRange; } export interface ScanStopPayload { @@ -397,7 +383,6 @@ export const runQueriesAction = actionCreatorFactory('explore * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range */ export const scanStartAction = actionCreatorFactory('explore/SCAN_START').create(); -export const scanRangeAction = actionCreatorFactory('explore/SCAN_RANGE').create(); /** * Stop any scanning for more results. diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 718f0475bf9..8d961a37c3e 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -26,7 +26,7 @@ import { LogsDedupStrategy, AbsoluteTimeRange, } from '@grafana/ui'; -import { ExploreId, RangeScanner, ExploreUIState, QueryTransaction, ExploreMode } from 'app/types/explore'; +import { ExploreId, ExploreUIState, QueryTransaction, ExploreMode } from 'app/types/explore'; import { updateDatasourceInstanceAction, changeQueryAction, @@ -58,7 +58,6 @@ import { loadExploreDatasources, changeModeAction, scanStopAction, - scanRangeAction, runQueriesAction, stateSaveAction, updateTimeRangeAction, @@ -66,6 +65,7 @@ import { import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory'; import { getTimeZone } from 'app/features/profile/state/selectors'; import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker'; +import { getShiftedTimeRange } from 'app/core/utils/timePicker'; /** * Updates UI state and save it to the URL @@ -413,14 +413,15 @@ export function runQueries(exploreId: ExploreId): ThunkResult { * @param exploreId Explore area * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range */ -export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { - return dispatch => { +export function scanStart(exploreId: ExploreId): ThunkResult { + return (dispatch, getState) => { // Register the scanner - dispatch(scanStartAction({ exploreId, scanner })); + dispatch(scanStartAction({ exploreId })); // Scanning must trigger query run, and return the new range - const range = scanner(); + const range = getShiftedTimeRange(-1, getState().explore[exploreId].range); // Set the new range to be displayed - dispatch(scanRangeAction({ exploreId, range })); + dispatch(updateTimeRangeAction({ exploreId, absoluteRange: range })); + dispatch(runQueriesAction({ exploreId })); }; } diff --git a/public/app/features/explore/state/epics/processQueryResultsEpic.test.ts b/public/app/features/explore/state/epics/processQueryResultsEpic.test.ts index c5da93081aa..9a427e97a22 100644 --- a/public/app/features/explore/state/epics/processQueryResultsEpic.test.ts +++ b/public/app/features/explore/state/epics/processQueryResultsEpic.test.ts @@ -1,11 +1,12 @@ import { mockExploreState } from 'test/mocks/mockExploreState'; -import { epicTester } from 'test/core/redux/epicTester'; +import { epicTester, MOCKED_ABSOLUTE_RANGE } from 'test/core/redux/epicTester'; import { processQueryResultsAction, resetQueryErrorAction, querySuccessAction, scanStopAction, - scanRangeAction, + updateTimeRangeAction, + runQueriesAction, } from '../actionTypes'; import { SeriesData, LoadingState } from '@grafana/ui'; import { processQueryResultsEpic } from './processQueryResultsEpic'; @@ -81,7 +82,7 @@ describe('processQueryResultsEpic', () => { describe('and we do not have a result', () => { it('then correct actions are dispatched', () => { - const { datasourceId, exploreId, state, scanner } = mockExploreState({ scanning: true }); + const { datasourceId, exploreId, state } = mockExploreState({ scanning: true }); const { latency, loadingState } = testContext(); const graphResult = []; const tableResult = new TableModel(); @@ -94,7 +95,8 @@ describe('processQueryResultsEpic', () => { .thenResultingActionsEqual( resetQueryErrorAction({ exploreId, refIds: [] }), querySuccessAction({ exploreId, loadingState, graphResult, tableResult, logsResult, latency }), - scanRangeAction({ exploreId, range: scanner() }) + updateTimeRangeAction({ exploreId, absoluteRange: MOCKED_ABSOLUTE_RANGE }), + runQueriesAction({ exploreId }) ); }); }); diff --git a/public/app/features/explore/state/epics/processQueryResultsEpic.ts b/public/app/features/explore/state/epics/processQueryResultsEpic.ts index 76e767c36a0..db46659eb6e 100644 --- a/public/app/features/explore/state/epics/processQueryResultsEpic.ts +++ b/public/app/features/explore/state/epics/processQueryResultsEpic.ts @@ -11,17 +11,22 @@ import { processQueryResultsAction, ProcessQueryResultsPayload, querySuccessAction, - scanRangeAction, resetQueryErrorAction, scanStopAction, + updateTimeRangeAction, + runQueriesAction, } from '../actionTypes'; import { ResultProcessor } from '../../utils/ResultProcessor'; -export const processQueryResultsEpic: Epic, ActionOf, StoreState> = (action$, state$) => { +export const processQueryResultsEpic: Epic, ActionOf, StoreState> = ( + action$, + state$, + { getTimeZone, getShiftedTimeRange } +) => { return action$.ofType(processQueryResultsAction.type).pipe( mergeMap((action: ActionOf) => { const { exploreId, datasourceId, latency, loadingState, series, delta } = action.payload; - const { datasourceInstance, scanning, scanner, eventBridge } = state$.value.explore[exploreId]; + const { datasourceInstance, scanning, eventBridge } = state$.value.explore[exploreId]; // If datasource already changed, results do not matter if (datasourceInstance.meta.id !== datasourceId) { @@ -62,8 +67,9 @@ export const processQueryResultsEpic: Epic, ActionOf, StoreSt // Keep scanning for results if this was the last scanning transaction if (scanning) { if (_.size(result) === 0) { - const range = scanner(); - actions.push(scanRangeAction({ exploreId, range })); + const range = getShiftedTimeRange(-1, state$.value.explore[exploreId].range, getTimeZone(state$.value.user)); + actions.push(updateTimeRangeAction({ exploreId, absoluteRange: range })); + actions.push(runQueriesAction({ exploreId })); } else { // We can stop scanning if we have a result actions.push(scanStopAction({ exploreId })); diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 40196df7639..9404e1591f4 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -5,14 +5,7 @@ import { makeInitialUpdateState, initialExploreState, } from './reducers'; -import { - ExploreId, - ExploreItemState, - ExploreUrlState, - ExploreState, - RangeScanner, - ExploreMode, -} from 'app/types/explore'; +import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState, ExploreMode } from 'app/types/explore'; import { reducerTester } from 'test/core/redux/reducerTester'; import { scanStartAction, @@ -36,28 +29,23 @@ import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy, LoadingState } describe('Explore item reducer', () => { describe('scanning', () => { it('should start scanning', () => { - const scanner = jest.fn(); const initalState = { ...makeExploreItemState(), scanning: false, - scanner: undefined as RangeScanner, }; reducerTester() .givenReducer(itemReducer as Reducer>, initalState) - .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner })) + .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left })) .thenStateShouldEqual({ ...makeExploreItemState(), scanning: true, - scanner, }); }); it('should stop scanning', () => { - const scanner = jest.fn(); const initalState = { ...makeExploreItemState(), scanning: true, - scanner, scanRange: {}, }; @@ -67,7 +55,6 @@ describe('Explore item reducer', () => { .thenStateShouldEqual({ ...makeExploreItemState(), scanning: false, - scanner: undefined, scanRange: undefined, }); }); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index b19a394ba8a..dcbd19bc1cf 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -24,7 +24,6 @@ import { queryFailureAction, setUrlReplacedAction, querySuccessAction, - scanRangeAction, scanStopAction, resetQueryErrorAction, queryStartAction, @@ -404,16 +403,10 @@ export const itemReducer = reducerFactory({} as ExploreItemSta }; }, }) - .addMapper({ - filter: scanRangeAction, - mapper: (state, action): ExploreItemState => { - return { ...state, scanRange: action.payload.range }; - }, - }) .addMapper({ filter: scanStartAction, mapper: (state, action): ExploreItemState => { - return { ...state, scanning: true, scanner: action.payload.scanner }; + return { ...state, scanning: true }; }, }) .addMapper({ @@ -423,7 +416,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...state, scanning: false, scanRange: undefined, - scanner: undefined, update: makeInitialUpdateState(), }; }, diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 2c4ad840271..6b3c3dafcbc 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -36,6 +36,7 @@ import { DateTime, toUtc, dateTime, + AbsoluteTimeRange, } from '@grafana/ui'; import { Observable } from 'rxjs'; import { getQueryResponse } from 'app/core/utils/explore'; @@ -46,6 +47,7 @@ import { TimeSrv, getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { UserState } from 'app/types/user'; import { getTimeRange } from 'app/core/utils/explore'; import { getTimeZone } from 'app/features/profile/state/selectors'; +import { getShiftedTimeRange } from 'app/core/utils/timePicker'; const rootReducers = { ...sharedReducers, @@ -87,6 +89,7 @@ export interface EpicDependencies { getTimeZone: (state: UserState) => TimeZone; toUtc: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime; dateTime: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime; + getShiftedTimeRange: (direction: number, origRange: TimeRange, timeZone: TimeZone) => AbsoluteTimeRange; } const dependencies: EpicDependencies = { @@ -96,6 +99,7 @@ const dependencies: EpicDependencies = { getTimeZone, toUtc, dateTime, + getShiftedTimeRange, }; const epicMiddleware = createEpicMiddleware({ dependencies }); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index a9cc5d71853..2ca3fa99b8d 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -192,10 +192,6 @@ export interface ExploreItemState { range: TimeRange; absoluteRange: AbsoluteTimeRange; - /** - * Scanner function that calculates a new range, triggers a query run, and returns the new range. - */ - scanner?: RangeScanner; /** * True if scanning for more results is active. */ @@ -334,8 +330,6 @@ export interface QueryTransaction { scanning?: boolean; } -export type RangeScanner = () => RawTimeRange; - export interface TextMatch { text: string; start: number; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index c06af5864c7..be33165ba37 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -98,6 +98,10 @@ padding: 10px 2px; } +.explore-toolbar-content-item.timepicker { + z-index: $zindex-timepicker-popover; +} + .explore-toolbar-content-item:first-child { padding-left: $dashboard-padding; margin-right: auto; diff --git a/public/test/core/redux/epicTester.ts b/public/test/core/redux/epicTester.ts index f92e786c3a0..7b5f0a47b76 100644 --- a/public/test/core/redux/epicTester.ts +++ b/public/test/core/redux/epicTester.ts @@ -17,6 +17,8 @@ import { EpicDependencies } from 'app/store/configureStore'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DEFAULT_RANGE } from 'app/core/utils/explore'; +export const MOCKED_ABSOLUTE_RANGE = { from: 1, to: 2 }; + export const epicTester = ( epic: Epic, ActionOf, StoreState, EpicDependencies>, state?: Partial, @@ -48,6 +50,8 @@ export const epicTester = ( const getTimeRange = jest.fn().mockReturnValue(DEFAULT_RANGE); + const getShiftedTimeRange = jest.fn().mockReturnValue(MOCKED_ABSOLUTE_RANGE); + const getTimeZone = jest.fn().mockReturnValue(DefaultTimeZone); const toUtc = jest.fn().mockReturnValue(null); @@ -61,6 +65,7 @@ export const epicTester = ( getTimeZone, toUtc, dateTime, + getShiftedTimeRange, }; const theDependencies: EpicDependencies = { ...defaultDependencies, ...dependencies };