mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
2379de53c4
commit
ead4b1f5c7
79
public/app/core/utils/timePicker.test.ts
Normal file
79
public/app/core/utils/timePicker.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
38
public/app/core/utils/timePicker.ts
Normal file
38
public/app/core/utils/timePicker.ts
Normal 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 };
|
||||
};
|
@ -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),
|
||||
|
@ -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);
|
@ -1 +0,0 @@
|
||||
export { TimePickerCtrl } from './TimePickerCtrl';
|
@ -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>
|
@ -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"> 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>
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
}
|
@ -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';
|
||||
|
@ -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) });
|
||||
}
|
||||
|
@ -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}
|
||||
|
||||
|
112
public/app/features/explore/ExploreTimeControls.tsx
Normal file
112
public/app/features/explore/ExploreTimeControls.tsx
Normal 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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
@ -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">
|
||||
|
@ -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);
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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"> 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>
|
||||
);
|
||||
}
|
||||
}
|
@ -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.
|
||||
|
@ -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 }));
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 })
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -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 }));
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
|
@ -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(),
|
||||
};
|
||||
},
|
||||
|
@ -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 });
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 };
|
||||
|
Loading…
Reference in New Issue
Block a user