Explore: Uses new TimePicker from Grafana/UI (#17793)

* Wip: Intiail commit

* Refactor: Replaces TimePicker in Explore

* Refactor: Removes Angular TimePicker folder

* Refactor: Adds tests for getShiftedTimeRange

* Fix: Fixes invalid import to removed TimePicker

* Fix: Fixes dateTime tests

* Refactor: Reuses getShiftedTimeRange for both Explore and Dashboards

* Refactor: Shares getZoomedTimeRange between Explore and Dashboard
This commit is contained in:
Hugo Häggmark 2019-06-28 12:07:55 +02:00 committed by GitHub
parent 2379de53c4
commit ead4b1f5c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 302 additions and 1032 deletions

View File

@ -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);
});
});
});

View File

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

View File

@ -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<Props> {
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),

View File

@ -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) + ' <br>to<br>';
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);

View File

@ -1 +0,0 @@
export { TimePickerCtrl } from './TimePickerCtrl';

View File

@ -1,24 +0,0 @@
<div class="editor-row">
<h5 class="section-heading">Time Options</h5>
<div class="gf-form-group">
<div class="gf-form">
<label class="gf-form-label width-10">Timezone</label>
<div class="gf-form-select-wrapper">
<select ng-model="ctrl.dashboard.timezone" class='gf-form-input' ng-options="f.value as f.text for f in [{value: '', text: 'Default'}, {value: 'browser', text: 'Local browser time'},{value: 'utc', text: 'UTC'}]"></select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Auto-refresh</span>
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.refresh_intervals" array-join>
</div>
<div class="gf-form">
<span class="gf-form-label width-10">Now delay now-</span>
<input type="text" class="gf-form-input max-width-25" ng-model="ctrl.panel.nowDelay" placeholder="0m" valid-time-span bs-tooltip="'Enter 1m to ignore the last minute (because it can contain incomplete metrics)'"
data-placement="right">
</div>
<gf-form-switch class="gf-form" label="Hide time picker" checked="ctrl.panel.hidden" label-class="width-10"></gf-form-switch>
</div>
</div>

View File

@ -1,84 +0,0 @@
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(-1)' ng-if="ctrl.isAbsolute">
<i class="fa fa-chevron-left"></i>
</button>
<button bs-tooltip="ctrl.tooltip" data-placement="bottom" ng-click="ctrl.openDropdown()" class="btn navbar-button gf-timepicker-nav-btn">
<i class="fa fa-clock-o"></i>
<span ng-bind="ctrl.rangeString"></span>
<span ng-show="ctrl.isUtc" class="gf-timepicker-utc">UTC</span>
<!-- <span ng-show="ctrl.dashboard.refresh" class="text-warning">&nbsp; Refresh every {{ctrl.dashboard.refresh}}</span> -->
</button>
<button class="btn navbar-button navbar-button--tight" ng-click='ctrl.move(1)' ng-if="ctrl.isAbsolute">
<i class="fa fa-chevron-right"></i>
</button>
<button class="btn navbar-button navbar-button--zoom" bs-tooltip="'Time range zoom out <br> CTRL+Z'" data-placement="bottom" ng-click='ctrl.zoom(2)'>
<i class="fa fa-search-minus"></i>
</button>
<!-- <button class="btn navbar-button navbar-button--refresh" ng-click="ctrl.timeSrv.refreshDashboard()">
<i class="fa fa-refresh"></i>
</button> -->
<div ng-if="ctrl.isOpen" class="gf-timepicker-dropdown">
<div class="popover-box">
<div class="popover-box__header">
<span class="popover-box__title">Quick ranges</span>
</div>
<div class="popover-box__body gf-timepicker-relative-section">
<ul ng-repeat="group in ctrl.timeOptions">
<li bindonce ng-repeat='option in group' ng-class="{active: option.active}">
<a ng-click="ctrl.setRelativeFilter(option)" bo-text="option.display"></a>
</li>
</ul>
</div>
</div>
<div class="popover-box">
<div class="popover-box__header">
<span class="popover-box__title">Custom range</span>
</div>
<form name="timeForm" class="popover-box__body gf-timepicker-absolute-section max-width-28">
<label class="small">From:</label>
<div class="gf-form-inline">
<div class="gf-form max-width-28">
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.from" input-datetime>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-secondary" type="button" ng-click="openFromPicker=!openFromPicker">
<i class="fa fa-calendar"></i>
</button>
</div>
</div>
<div ng-if="openFromPicker">
<datepicker ng-model="ctrl.absolute.fromJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteFromChanged()"></datepicker>
</div>
<label class="small">To:</label>
<div class="gf-form-inline">
<div class="gf-form max-width-28">
<input type="text" class="gf-form-input input-large" ng-model="ctrl.editTimeRaw.to" input-datetime>
</div>
<div class="gf-form">
<button class="btn gf-form-btn btn-secondary" type="button" ng-click="openToPicker=!openToPicker">
<i class="fa fa-calendar"></i>
</button>
</div>
</div>
<div ng-if="openToPicker">
<datepicker ng-model="ctrl.absolute.toJs" class="gf-timepicker-component" show-weeks="false" starting-day="ctrl.firstDayOfWeek" ng-change="ctrl.absoluteToChanged()"></datepicker>
</div>
<div class="gf-form gf-form--flex-end m-t-1">
<div class="gf-form">
<button type="submit" class="btn gf-form-btn btn-primary" ng-click="ctrl.applyCustom();" ng-disabled="!timeForm.$valid">Apply</button>
</div>
</div>
</form>
</div>
</div>

View File

@ -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);
},
};
}

View File

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

View File

@ -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) });
}

View File

@ -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<ExploreProps> {
el: any;
exploreEvents: Emitter;
/**
* Timepicker to control scanning
*/
timepickerRef: React.RefObject<TimePicker>;
constructor(props: ExploreProps) {
super(props);
this.exploreEvents = new Emitter();
this.timepickerRef = React.createRef();
}
componentDidMount() {
@ -159,11 +151,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
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<ExploreProps> {
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<ExploreProps> {
return (
<div className={exploreClass} ref={this.getRef}>
<ExploreToolbar exploreId={exploreId} timepickerRef={this.timepickerRef} onChangeTime={this.onChangeTime} />
<ExploreToolbar exploreId={exploreId} onChangeTime={this.onChangeTime} />
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
{datasourceMissing ? this.renderEmptyState() : null}

View File

@ -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<Props> {
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 && (
<TimePicker
value={range}
onChange={this.onChangeTimePicker}
timeZone={timeZone}
onMoveBackward={this.onMoveBack}
onMoveForward={this.onMoveForward}
onZoom={this.onZoom}
selectOptions={this.setActiveTimeOption(defaultSelectOptions, range.raw)}
/>
)}
<RefreshPicker
onIntervalChanged={onChangeRefreshInterval}
onRefresh={onRunQuery}
value={refreshInterval}
tooltip="Refresh"
hasLiveOption={hasLiveOption}
/>
{refreshInterval && <SetInterval func={onRunQuery} interval={refreshInterval} loading={loading} />}
</>
);
}
}

View File

@ -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<TimePicker>;
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
}
@ -111,10 +101,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
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<Props, {}> {
timeZone,
selectedDatasource,
splitted,
timepickerRef,
refreshInterval,
onChangeTime,
split,
@ -214,20 +199,18 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
</div>
) : null}
<div className="explore-toolbar-content-item timepicker">
{!isLive && (
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
<TimePicker ref={timepickerRef} range={range} timeZone={timeZone} onChangeTime={onChangeTime} />
</ClickOutsideWrapper>
)}
<RefreshPicker
onIntervalChanged={this.onChangeRefreshInterval}
onRefresh={this.onRunQuery}
value={refreshInterval}
tooltip="Refresh"
<ExploreTimeControls
exploreId={exploreId}
hasLiveOption={hasLiveOption}
isLive={isLive}
loading={loading}
range={range}
refreshInterval={refreshInterval}
timeZone={timeZone}
onChangeTime={onChangeTime}
onChangeRefreshInterval={this.onChangeRefreshInterval}
onRunQuery={this.onRunQuery}
/>
{refreshInterval && <SetInterval func={this.onRunQuery} interval={refreshInterval} loading={loading} />}
</div>
<div className="explore-toolbar-content-item">

View File

@ -101,7 +101,7 @@ export default class Logs extends PureComponent<Props, State> {
}
}
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);

View File

@ -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<LogsContainerProps> {
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<LogsContainerProps> {
absoluteRange,
timeZone,
scanning,
scanRange,
range,
width,
hiddenLogLevels,
isLive,
@ -139,7 +143,7 @@ export class LogsContainer extends Component<LogsContainerProps> {
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,
};
}

View File

@ -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('<TimePicker />', () => {
it('render default values when closed and relative time range', () => {
const range = fromRaw(DEFAULT_RANGE);
const wrapper = shallow(<TimePicker range={range} timeZone="browser" />);
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(<TimePicker range={range} timeZone="utc" />);
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(<TimePicker range={range} isOpen timeZone="browser" />);
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(<TimePicker range={range} isOpen timeZone="utc" />);
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(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
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(<TimePicker range={range} timeZone="utc" isOpen onChangeTime={onChangeTime} />);
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(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
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(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
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(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="utc" />);
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(<TimePicker range={range} isOpen onChangeTime={onChangeTime} timeZone="browser" />);
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);
});
});

View File

@ -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<TimePickerProps, TimePickerState> {
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<HTMLInputElement>) => {
this.setState({
fromRaw: event.target.value,
});
};
handleChangeTo = (event: ChangeEvent<HTMLInputElement>) => {
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 (
<div ref={this.dropdownRef} className="gf-timepicker-dropdown">
<div className="popover-box">
<div className="popover-box__header">
<span className="popover-box__title">Quick ranges</span>
</div>
<div className="popover-box__body gf-timepicker-relative-section">
{Object.keys(timeOptions).map(section => {
const group = timeOptions[section];
return (
<ul key={section}>
{group.map((option: any) => (
<li className={option.active ? 'active' : ''} key={option.display}>
<a onClick={() => this.handleClickRelativeOption(option)}>{option.display}</a>
</li>
))}
</ul>
);
})}
</div>
</div>
<div className="popover-box">
<div className="popover-box__header">
<span className="popover-box__title">Custom range</span>
</div>
<div className="popover-box__body gf-timepicker-absolute-section">
<label className="small">From:</label>
<div className="gf-form-inline">
<div className="gf-form max-width-28">
<Input
type="text"
className="gf-form-input input-large timepicker-from"
value={fromRaw}
onChange={this.handleChangeFrom}
/>
</div>
</div>
<label className="small">To:</label>
<div className="gf-form-inline">
<div className="gf-form max-width-28">
<Input
type="text"
className="gf-form-input input-large timepicker-to"
value={toRaw}
onChange={this.handleChangeTo}
/>
</div>
</div>
<div className="gf-form">
<button className="btn gf-form-btn btn-secondary" onClick={this.handleClickApply}>
Apply
</button>
</div>
</div>
</div>
</div>
);
}
render() {
const { isUtc, rangeString, refreshInterval } = this.state;
return (
<div className="timepicker">
<div className="navbar-buttons">
<button className="btn navbar-button navbar-button--tight timepicker-left" onClick={this.handleClickLeft}>
<i className="fa fa-chevron-left" />
</button>
<button className="btn navbar-button gf-timepicker-nav-btn" onClick={this.handleClickPicker}>
<i className="fa fa-clock-o" />
<span className="timepicker-rangestring">{rangeString}</span>
{isUtc ? <span className="gf-timepicker-utc">UTC</span> : null}
{refreshInterval ? <span className="text-warning">&nbsp; Refresh every {refreshInterval}</span> : null}
</button>
<button className="btn navbar-button navbar-button--tight timepicker-right" onClick={this.handleClickRight}>
<i className="fa fa-chevron-right" />
</button>
</div>
{this.renderDropdown()}
</div>
);
}
}

View File

@ -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<RunQueriesPayload>('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<ScanStartPayload>('explore/SCAN_START').create();
export const scanRangeAction = actionCreatorFactory<ScanRangePayload>('explore/SCAN_RANGE').create();
/**
* Stop any scanning for more results.

View File

@ -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<void> {
* @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<void> {
return dispatch => {
export function scanStart(exploreId: ExploreId): ThunkResult<void> {
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 }));
};
}

View File

@ -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 })
);
});
});

View File

@ -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<any>, ActionOf<any>, StoreState> = (action$, state$) => {
export const processQueryResultsEpic: Epic<ActionOf<any>, ActionOf<any>, StoreState> = (
action$,
state$,
{ getTimeZone, getShiftedTimeRange }
) => {
return action$.ofType(processQueryResultsAction.type).pipe(
mergeMap((action: ActionOf<ProcessQueryResultsPayload>) => {
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<any>, ActionOf<any>, 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 }));

View File

@ -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<ExploreItemState, ActionOf<any>>, 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,
});
});

View File

@ -24,7 +24,6 @@ import {
queryFailureAction,
setUrlReplacedAction,
querySuccessAction,
scanRangeAction,
scanStopAction,
resetQueryErrorAction,
queryStartAction,
@ -404,16 +403,10 @@ export const itemReducer = reducerFactory<ExploreItemState>({} 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<ExploreItemState>({} as ExploreItemSta
...state,
scanning: false,
scanRange: undefined,
scanner: undefined,
update: makeInitialUpdateState(),
};
},

View File

@ -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 });

View File

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

View File

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

View File

@ -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<any>, ActionOf<any>, StoreState, EpicDependencies>,
state?: Partial<StoreState>,
@ -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 };