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
|
// Utils & Services
|
||||||
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
|
import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker';
|
||||||
|
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
$injector: any;
|
$injector: any;
|
||||||
@ -44,23 +45,7 @@ export class DashNavTimeControls extends Component<Props> {
|
|||||||
|
|
||||||
onMoveTimePicker = (direction: number) => {
|
onMoveTimePicker = (direction: number) => {
|
||||||
const range = this.timeSrv.timeRange();
|
const range = this.timeSrv.timeRange();
|
||||||
const timespan = (range.to.valueOf() - range.from.valueOf()) / 2;
|
const { from, to } = getShiftedTimeRange(direction, range);
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.timeSrv.setTime({
|
this.timeSrv.setTime({
|
||||||
from: toUtc(from),
|
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/VersionHistory';
|
||||||
import './components/DashboardSettings';
|
import './components/DashboardSettings';
|
||||||
import './components/SubMenu';
|
import './components/SubMenu';
|
||||||
import './components/TimePicker';
|
|
||||||
import './components/UnsavedChangesModal';
|
import './components/UnsavedChangesModal';
|
||||||
import './components/SaveModals';
|
import './components/SaveModals';
|
||||||
import './components/ShareModal';
|
import './components/ShareModal';
|
||||||
|
@ -12,6 +12,7 @@ import { ITimeoutService, ILocationService } from 'angular';
|
|||||||
import { ContextSrv } from 'app/core/services/context_srv';
|
import { ContextSrv } from 'app/core/services/context_srv';
|
||||||
import { DashboardModel } from '../state/DashboardModel';
|
import { DashboardModel } from '../state/DashboardModel';
|
||||||
import { toUtc, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
|
import { toUtc, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper';
|
||||||
|
import { getZoomedTimeRange } from 'app/core/utils/timePicker';
|
||||||
|
|
||||||
export class TimeSrv {
|
export class TimeSrv {
|
||||||
time: any;
|
time: any;
|
||||||
@ -238,12 +239,7 @@ export class TimeSrv {
|
|||||||
|
|
||||||
zoomOut(e: any, factor: number) {
|
zoomOut(e: any, factor: number) {
|
||||||
const range = this.timeRange();
|
const range = this.timeRange();
|
||||||
|
const { from, to } = getZoomedTimeRange(range, factor);
|
||||||
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;
|
|
||||||
|
|
||||||
this.setTime({ from: toUtc(from), to: toUtc(to) });
|
this.setTime({ from: toUtc(from), to: toUtc(to) });
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import GraphContainer from './GraphContainer';
|
|||||||
import LogsContainer from './LogsContainer';
|
import LogsContainer from './LogsContainer';
|
||||||
import QueryRows from './QueryRows';
|
import QueryRows from './QueryRows';
|
||||||
import TableContainer from './TableContainer';
|
import TableContainer from './TableContainer';
|
||||||
import TimePicker from './TimePicker';
|
|
||||||
|
|
||||||
// Actions
|
// Actions
|
||||||
import {
|
import {
|
||||||
@ -35,7 +34,6 @@ import { RawTimeRange, DataQuery, ExploreStartPageProps, DataSourceApi, DataQuer
|
|||||||
import {
|
import {
|
||||||
ExploreItemState,
|
ExploreItemState,
|
||||||
ExploreUrlState,
|
ExploreUrlState,
|
||||||
RangeScanner,
|
|
||||||
ExploreId,
|
ExploreId,
|
||||||
ExploreUpdateState,
|
ExploreUpdateState,
|
||||||
ExploreUIState,
|
ExploreUIState,
|
||||||
@ -71,7 +69,6 @@ interface ExploreProps {
|
|||||||
update: ExploreUpdateState;
|
update: ExploreUpdateState;
|
||||||
reconnectDatasource: typeof reconnectDatasource;
|
reconnectDatasource: typeof reconnectDatasource;
|
||||||
refreshExplore: typeof refreshExplore;
|
refreshExplore: typeof refreshExplore;
|
||||||
scanner?: RangeScanner;
|
|
||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
scanRange?: RawTimeRange;
|
scanRange?: RawTimeRange;
|
||||||
scanStart: typeof scanStart;
|
scanStart: typeof scanStart;
|
||||||
@ -117,15 +114,10 @@ interface ExploreProps {
|
|||||||
export class Explore extends React.PureComponent<ExploreProps> {
|
export class Explore extends React.PureComponent<ExploreProps> {
|
||||||
el: any;
|
el: any;
|
||||||
exploreEvents: Emitter;
|
exploreEvents: Emitter;
|
||||||
/**
|
|
||||||
* Timepicker to control scanning
|
|
||||||
*/
|
|
||||||
timepickerRef: React.RefObject<TimePicker>;
|
|
||||||
|
|
||||||
constructor(props: ExploreProps) {
|
constructor(props: ExploreProps) {
|
||||||
super(props);
|
super(props);
|
||||||
this.exploreEvents = new Emitter();
|
this.exploreEvents = new Emitter();
|
||||||
this.timepickerRef = React.createRef();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
@ -159,11 +151,9 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
this.el = el;
|
this.el = el;
|
||||||
};
|
};
|
||||||
|
|
||||||
onChangeTime = (rawRange: RawTimeRange, changedByScanner?: boolean) => {
|
onChangeTime = (rawRange: RawTimeRange) => {
|
||||||
const { updateTimeRange, exploreId, scanning } = this.props;
|
const { updateTimeRange, exploreId } = this.props;
|
||||||
if (scanning && !changedByScanner) {
|
|
||||||
this.onStopScanning();
|
|
||||||
}
|
|
||||||
updateTimeRange({ exploreId, rawRange });
|
updateTimeRange({ exploreId, rawRange });
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -190,13 +180,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
|
|
||||||
onStartScanning = () => {
|
onStartScanning = () => {
|
||||||
// Scanner will trigger a query
|
// Scanner will trigger a query
|
||||||
const scanner = this.scanPreviousRange;
|
this.props.scanStart(this.props.exploreId);
|
||||||
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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onStopScanning = () => {
|
onStopScanning = () => {
|
||||||
@ -244,7 +228,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={exploreClass} ref={this.getRef}>
|
<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}
|
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
|
||||||
{datasourceMissing ? this.renderEmptyState() : 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 { hot } from 'react-hot-loader';
|
||||||
|
|
||||||
import { ExploreId, ExploreMode } from 'app/types/explore';
|
import { ExploreId, ExploreMode } from 'app/types/explore';
|
||||||
import {
|
import { DataSourceSelectItem, RawTimeRange, TimeZone, TimeRange, SelectOptionItem, LoadingState } from '@grafana/ui';
|
||||||
DataSourceSelectItem,
|
|
||||||
RawTimeRange,
|
|
||||||
ClickOutsideWrapper,
|
|
||||||
TimeZone,
|
|
||||||
TimeRange,
|
|
||||||
SelectOptionItem,
|
|
||||||
LoadingState,
|
|
||||||
} from '@grafana/ui';
|
|
||||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||||
import { StoreState } from 'app/types/store';
|
import { StoreState } from 'app/types/store';
|
||||||
import {
|
import {
|
||||||
@ -23,10 +15,9 @@ import {
|
|||||||
changeRefreshInterval,
|
changeRefreshInterval,
|
||||||
changeMode,
|
changeMode,
|
||||||
} from './state/actions';
|
} from './state/actions';
|
||||||
import TimePicker from './TimePicker';
|
|
||||||
import { getTimeZone } from '../profile/state/selectors';
|
import { getTimeZone } from '../profile/state/selectors';
|
||||||
import { RefreshPicker, SetInterval } from '@grafana/ui';
|
|
||||||
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
|
import ToggleButtonGroup, { ToggleButton } from 'app/core/components/ToggleButtonGroup/ToggleButtonGroup';
|
||||||
|
import { ExploreTimeControls } from './ExploreTimeControls';
|
||||||
|
|
||||||
enum IconSide {
|
enum IconSide {
|
||||||
left = 'left',
|
left = 'left',
|
||||||
@ -63,7 +54,6 @@ const createResponsiveButton = (options: {
|
|||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
timepickerRef: React.RefObject<TimePicker>;
|
|
||||||
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
onChangeTime: (range: RawTimeRange, changedByScanner?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -111,10 +101,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
return this.props.runQueries(this.props.exploreId);
|
return this.props.runQueries(this.props.exploreId);
|
||||||
};
|
};
|
||||||
|
|
||||||
onCloseTimePicker = () => {
|
|
||||||
this.props.timepickerRef.current.setState({ isOpen: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeRefreshInterval = (item: string) => {
|
onChangeRefreshInterval = (item: string) => {
|
||||||
const { changeRefreshInterval, exploreId } = this.props;
|
const { changeRefreshInterval, exploreId } = this.props;
|
||||||
changeRefreshInterval(exploreId, item);
|
changeRefreshInterval(exploreId, item);
|
||||||
@ -136,7 +122,6 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
timeZone,
|
timeZone,
|
||||||
selectedDatasource,
|
selectedDatasource,
|
||||||
splitted,
|
splitted,
|
||||||
timepickerRef,
|
|
||||||
refreshInterval,
|
refreshInterval,
|
||||||
onChangeTime,
|
onChangeTime,
|
||||||
split,
|
split,
|
||||||
@ -214,20 +199,18 @@ export class UnConnectedExploreToolbar extends PureComponent<Props, {}> {
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
<div className="explore-toolbar-content-item timepicker">
|
<div className="explore-toolbar-content-item timepicker">
|
||||||
{!isLive && (
|
<ExploreTimeControls
|
||||||
<ClickOutsideWrapper onClick={this.onCloseTimePicker}>
|
exploreId={exploreId}
|
||||||
<TimePicker ref={timepickerRef} range={range} timeZone={timeZone} onChangeTime={onChangeTime} />
|
|
||||||
</ClickOutsideWrapper>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<RefreshPicker
|
|
||||||
onIntervalChanged={this.onChangeRefreshInterval}
|
|
||||||
onRefresh={this.onRunQuery}
|
|
||||||
value={refreshInterval}
|
|
||||||
tooltip="Refresh"
|
|
||||||
hasLiveOption={hasLiveOption}
|
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>
|
||||||
|
|
||||||
<div className="explore-toolbar-content-item">
|
<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
|
// Staged rendering
|
||||||
if (prevState.deferLogs && !this.state.deferLogs && !this.state.renderAll) {
|
if (prevState.deferLogs && !this.state.deferLogs && !this.state.renderAll) {
|
||||||
this.renderAllTimer = setTimeout(() => this.setState({ renderAll: true }), 2000);
|
this.renderAllTimer = setTimeout(() => this.setState({ renderAll: true }), 2000);
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
LogRowModel,
|
LogRowModel,
|
||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
LoadingState,
|
LoadingState,
|
||||||
|
TimeRange,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
import { ExploreId, ExploreItemState } from 'app/types/explore';
|
||||||
@ -47,6 +48,7 @@ interface LogsContainerProps {
|
|||||||
isLive: boolean;
|
isLive: boolean;
|
||||||
stopLive: typeof changeRefreshIntervalAction;
|
stopLive: typeof changeRefreshIntervalAction;
|
||||||
updateTimeRange: typeof updateTimeRange;
|
updateTimeRange: typeof updateTimeRange;
|
||||||
|
range: TimeRange;
|
||||||
absoluteRange: AbsoluteTimeRange;
|
absoluteRange: AbsoluteTimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,7 +92,9 @@ export class LogsContainer extends Component<LogsContainerProps> {
|
|||||||
return (
|
return (
|
||||||
nextProps.loading !== this.props.loading ||
|
nextProps.loading !== this.props.loading ||
|
||||||
nextProps.dedupStrategy !== this.props.dedupStrategy ||
|
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,
|
absoluteRange,
|
||||||
timeZone,
|
timeZone,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
range,
|
||||||
width,
|
width,
|
||||||
hiddenLogLevels,
|
hiddenLogLevels,
|
||||||
isLive,
|
isLive,
|
||||||
@ -139,7 +143,7 @@ export class LogsContainer extends Component<LogsContainerProps> {
|
|||||||
absoluteRange={absoluteRange}
|
absoluteRange={absoluteRange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
scanning={scanning}
|
scanning={scanning}
|
||||||
scanRange={scanRange}
|
scanRange={range.raw}
|
||||||
width={width}
|
width={width}
|
||||||
hiddenLogLevels={hiddenLogLevels}
|
hiddenLogLevels={hiddenLogLevels}
|
||||||
getRowContext={this.getLogRowContext}
|
getRowContext={this.getLogRowContext}
|
||||||
@ -157,9 +161,9 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
logsResult,
|
logsResult,
|
||||||
loadingState,
|
loadingState,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
isLive,
|
isLive,
|
||||||
|
range,
|
||||||
absoluteRange,
|
absoluteRange,
|
||||||
} = item;
|
} = item;
|
||||||
const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
|
const loading = loadingState === LoadingState.Loading || loadingState === LoadingState.Streaming;
|
||||||
@ -173,13 +177,13 @@ function mapStateToProps(state: StoreState, { exploreId }) {
|
|||||||
logsHighlighterExpressions,
|
logsHighlighterExpressions,
|
||||||
logsResult,
|
logsResult,
|
||||||
scanning,
|
scanning,
|
||||||
scanRange,
|
|
||||||
timeZone,
|
timeZone,
|
||||||
dedupStrategy,
|
dedupStrategy,
|
||||||
hiddenLogLevels,
|
hiddenLogLevels,
|
||||||
dedupedResult,
|
dedupedResult,
|
||||||
datasourceInstance,
|
datasourceInstance,
|
||||||
isLive,
|
isLive,
|
||||||
|
range,
|
||||||
absoluteRange,
|
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,
|
LoadingState,
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
} from '@grafana/ui/src/types';
|
} from '@grafana/ui/src/types';
|
||||||
import {
|
import { ExploreId, ExploreItemState, HistoryItem, ExploreUIState, ExploreMode, QueryOptions } from 'app/types/explore';
|
||||||
ExploreId,
|
|
||||||
ExploreItemState,
|
|
||||||
HistoryItem,
|
|
||||||
RangeScanner,
|
|
||||||
ExploreUIState,
|
|
||||||
ExploreMode,
|
|
||||||
QueryOptions,
|
|
||||||
} from 'app/types/explore';
|
|
||||||
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory';
|
||||||
import TableModel from 'app/core/table_model';
|
import TableModel from 'app/core/table_model';
|
||||||
|
|
||||||
@ -171,12 +163,6 @@ export interface RemoveQueryRowPayload {
|
|||||||
|
|
||||||
export interface ScanStartPayload {
|
export interface ScanStartPayload {
|
||||||
exploreId: ExploreId;
|
exploreId: ExploreId;
|
||||||
scanner: RangeScanner;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ScanRangePayload {
|
|
||||||
exploreId: ExploreId;
|
|
||||||
range: RawTimeRange;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScanStopPayload {
|
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
|
* @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 scanStartAction = actionCreatorFactory<ScanStartPayload>('explore/SCAN_START').create();
|
||||||
export const scanRangeAction = actionCreatorFactory<ScanRangePayload>('explore/SCAN_RANGE').create();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stop any scanning for more results.
|
* Stop any scanning for more results.
|
||||||
|
@ -26,7 +26,7 @@ import {
|
|||||||
LogsDedupStrategy,
|
LogsDedupStrategy,
|
||||||
AbsoluteTimeRange,
|
AbsoluteTimeRange,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { ExploreId, RangeScanner, ExploreUIState, QueryTransaction, ExploreMode } from 'app/types/explore';
|
import { ExploreId, ExploreUIState, QueryTransaction, ExploreMode } from 'app/types/explore';
|
||||||
import {
|
import {
|
||||||
updateDatasourceInstanceAction,
|
updateDatasourceInstanceAction,
|
||||||
changeQueryAction,
|
changeQueryAction,
|
||||||
@ -58,7 +58,6 @@ import {
|
|||||||
loadExploreDatasources,
|
loadExploreDatasources,
|
||||||
changeModeAction,
|
changeModeAction,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
scanRangeAction,
|
|
||||||
runQueriesAction,
|
runQueriesAction,
|
||||||
stateSaveAction,
|
stateSaveAction,
|
||||||
updateTimeRangeAction,
|
updateTimeRangeAction,
|
||||||
@ -66,6 +65,7 @@ import {
|
|||||||
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
import { ActionOf, ActionCreator } from 'app/core/redux/actionCreatorFactory';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
import { offOption } from '@grafana/ui/src/components/RefreshPicker/RefreshPicker';
|
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
|
* 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 exploreId Explore area
|
||||||
* @param scanner Function that a) returns a new time range and b) triggers a query run for the new range
|
* @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> {
|
export function scanStart(exploreId: ExploreId): ThunkResult<void> {
|
||||||
return dispatch => {
|
return (dispatch, getState) => {
|
||||||
// Register the scanner
|
// Register the scanner
|
||||||
dispatch(scanStartAction({ exploreId, scanner }));
|
dispatch(scanStartAction({ exploreId }));
|
||||||
// Scanning must trigger query run, and return the new range
|
// 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
|
// 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 { mockExploreState } from 'test/mocks/mockExploreState';
|
||||||
import { epicTester } from 'test/core/redux/epicTester';
|
import { epicTester, MOCKED_ABSOLUTE_RANGE } from 'test/core/redux/epicTester';
|
||||||
import {
|
import {
|
||||||
processQueryResultsAction,
|
processQueryResultsAction,
|
||||||
resetQueryErrorAction,
|
resetQueryErrorAction,
|
||||||
querySuccessAction,
|
querySuccessAction,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
scanRangeAction,
|
updateTimeRangeAction,
|
||||||
|
runQueriesAction,
|
||||||
} from '../actionTypes';
|
} from '../actionTypes';
|
||||||
import { SeriesData, LoadingState } from '@grafana/ui';
|
import { SeriesData, LoadingState } from '@grafana/ui';
|
||||||
import { processQueryResultsEpic } from './processQueryResultsEpic';
|
import { processQueryResultsEpic } from './processQueryResultsEpic';
|
||||||
@ -81,7 +82,7 @@ describe('processQueryResultsEpic', () => {
|
|||||||
|
|
||||||
describe('and we do not have a result', () => {
|
describe('and we do not have a result', () => {
|
||||||
it('then correct actions are dispatched', () => {
|
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 { latency, loadingState } = testContext();
|
||||||
const graphResult = [];
|
const graphResult = [];
|
||||||
const tableResult = new TableModel();
|
const tableResult = new TableModel();
|
||||||
@ -94,7 +95,8 @@ describe('processQueryResultsEpic', () => {
|
|||||||
.thenResultingActionsEqual(
|
.thenResultingActionsEqual(
|
||||||
resetQueryErrorAction({ exploreId, refIds: [] }),
|
resetQueryErrorAction({ exploreId, refIds: [] }),
|
||||||
querySuccessAction({ exploreId, loadingState, graphResult, tableResult, logsResult, latency }),
|
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,
|
processQueryResultsAction,
|
||||||
ProcessQueryResultsPayload,
|
ProcessQueryResultsPayload,
|
||||||
querySuccessAction,
|
querySuccessAction,
|
||||||
scanRangeAction,
|
|
||||||
resetQueryErrorAction,
|
resetQueryErrorAction,
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
|
updateTimeRangeAction,
|
||||||
|
runQueriesAction,
|
||||||
} from '../actionTypes';
|
} from '../actionTypes';
|
||||||
import { ResultProcessor } from '../../utils/ResultProcessor';
|
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(
|
return action$.ofType(processQueryResultsAction.type).pipe(
|
||||||
mergeMap((action: ActionOf<ProcessQueryResultsPayload>) => {
|
mergeMap((action: ActionOf<ProcessQueryResultsPayload>) => {
|
||||||
const { exploreId, datasourceId, latency, loadingState, series, delta } = action.payload;
|
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 datasource already changed, results do not matter
|
||||||
if (datasourceInstance.meta.id !== datasourceId) {
|
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
|
// Keep scanning for results if this was the last scanning transaction
|
||||||
if (scanning) {
|
if (scanning) {
|
||||||
if (_.size(result) === 0) {
|
if (_.size(result) === 0) {
|
||||||
const range = scanner();
|
const range = getShiftedTimeRange(-1, state$.value.explore[exploreId].range, getTimeZone(state$.value.user));
|
||||||
actions.push(scanRangeAction({ exploreId, range }));
|
actions.push(updateTimeRangeAction({ exploreId, absoluteRange: range }));
|
||||||
|
actions.push(runQueriesAction({ exploreId }));
|
||||||
} else {
|
} else {
|
||||||
// We can stop scanning if we have a result
|
// We can stop scanning if we have a result
|
||||||
actions.push(scanStopAction({ exploreId }));
|
actions.push(scanStopAction({ exploreId }));
|
||||||
|
@ -5,14 +5,7 @@ import {
|
|||||||
makeInitialUpdateState,
|
makeInitialUpdateState,
|
||||||
initialExploreState,
|
initialExploreState,
|
||||||
} from './reducers';
|
} from './reducers';
|
||||||
import {
|
import { ExploreId, ExploreItemState, ExploreUrlState, ExploreState, ExploreMode } from 'app/types/explore';
|
||||||
ExploreId,
|
|
||||||
ExploreItemState,
|
|
||||||
ExploreUrlState,
|
|
||||||
ExploreState,
|
|
||||||
RangeScanner,
|
|
||||||
ExploreMode,
|
|
||||||
} from 'app/types/explore';
|
|
||||||
import { reducerTester } from 'test/core/redux/reducerTester';
|
import { reducerTester } from 'test/core/redux/reducerTester';
|
||||||
import {
|
import {
|
||||||
scanStartAction,
|
scanStartAction,
|
||||||
@ -36,28 +29,23 @@ import { DataSourceApi, DataQuery, LogsModel, LogsDedupStrategy, LoadingState }
|
|||||||
describe('Explore item reducer', () => {
|
describe('Explore item reducer', () => {
|
||||||
describe('scanning', () => {
|
describe('scanning', () => {
|
||||||
it('should start scanning', () => {
|
it('should start scanning', () => {
|
||||||
const scanner = jest.fn();
|
|
||||||
const initalState = {
|
const initalState = {
|
||||||
...makeExploreItemState(),
|
...makeExploreItemState(),
|
||||||
scanning: false,
|
scanning: false,
|
||||||
scanner: undefined as RangeScanner,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
reducerTester()
|
reducerTester()
|
||||||
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
|
.givenReducer(itemReducer as Reducer<ExploreItemState, ActionOf<any>>, initalState)
|
||||||
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner }))
|
.whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left }))
|
||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...makeExploreItemState(),
|
...makeExploreItemState(),
|
||||||
scanning: true,
|
scanning: true,
|
||||||
scanner,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('should stop scanning', () => {
|
it('should stop scanning', () => {
|
||||||
const scanner = jest.fn();
|
|
||||||
const initalState = {
|
const initalState = {
|
||||||
...makeExploreItemState(),
|
...makeExploreItemState(),
|
||||||
scanning: true,
|
scanning: true,
|
||||||
scanner,
|
|
||||||
scanRange: {},
|
scanRange: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,7 +55,6 @@ describe('Explore item reducer', () => {
|
|||||||
.thenStateShouldEqual({
|
.thenStateShouldEqual({
|
||||||
...makeExploreItemState(),
|
...makeExploreItemState(),
|
||||||
scanning: false,
|
scanning: false,
|
||||||
scanner: undefined,
|
|
||||||
scanRange: undefined,
|
scanRange: undefined,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,6 @@ import {
|
|||||||
queryFailureAction,
|
queryFailureAction,
|
||||||
setUrlReplacedAction,
|
setUrlReplacedAction,
|
||||||
querySuccessAction,
|
querySuccessAction,
|
||||||
scanRangeAction,
|
|
||||||
scanStopAction,
|
scanStopAction,
|
||||||
resetQueryErrorAction,
|
resetQueryErrorAction,
|
||||||
queryStartAction,
|
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({
|
.addMapper({
|
||||||
filter: scanStartAction,
|
filter: scanStartAction,
|
||||||
mapper: (state, action): ExploreItemState => {
|
mapper: (state, action): ExploreItemState => {
|
||||||
return { ...state, scanning: true, scanner: action.payload.scanner };
|
return { ...state, scanning: true };
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.addMapper({
|
.addMapper({
|
||||||
@ -423,7 +416,6 @@ export const itemReducer = reducerFactory<ExploreItemState>({} as ExploreItemSta
|
|||||||
...state,
|
...state,
|
||||||
scanning: false,
|
scanning: false,
|
||||||
scanRange: undefined,
|
scanRange: undefined,
|
||||||
scanner: undefined,
|
|
||||||
update: makeInitialUpdateState(),
|
update: makeInitialUpdateState(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -36,6 +36,7 @@ import {
|
|||||||
DateTime,
|
DateTime,
|
||||||
toUtc,
|
toUtc,
|
||||||
dateTime,
|
dateTime,
|
||||||
|
AbsoluteTimeRange,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { getQueryResponse } from 'app/core/utils/explore';
|
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 { UserState } from 'app/types/user';
|
||||||
import { getTimeRange } from 'app/core/utils/explore';
|
import { getTimeRange } from 'app/core/utils/explore';
|
||||||
import { getTimeZone } from 'app/features/profile/state/selectors';
|
import { getTimeZone } from 'app/features/profile/state/selectors';
|
||||||
|
import { getShiftedTimeRange } from 'app/core/utils/timePicker';
|
||||||
|
|
||||||
const rootReducers = {
|
const rootReducers = {
|
||||||
...sharedReducers,
|
...sharedReducers,
|
||||||
@ -87,6 +89,7 @@ export interface EpicDependencies {
|
|||||||
getTimeZone: (state: UserState) => TimeZone;
|
getTimeZone: (state: UserState) => TimeZone;
|
||||||
toUtc: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
|
toUtc: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
|
||||||
dateTime: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
|
dateTime: (input?: DateTimeInput, formatInput?: FormatInput) => DateTime;
|
||||||
|
getShiftedTimeRange: (direction: number, origRange: TimeRange, timeZone: TimeZone) => AbsoluteTimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
const dependencies: EpicDependencies = {
|
const dependencies: EpicDependencies = {
|
||||||
@ -96,6 +99,7 @@ const dependencies: EpicDependencies = {
|
|||||||
getTimeZone,
|
getTimeZone,
|
||||||
toUtc,
|
toUtc,
|
||||||
dateTime,
|
dateTime,
|
||||||
|
getShiftedTimeRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const epicMiddleware = createEpicMiddleware({ dependencies });
|
const epicMiddleware = createEpicMiddleware({ dependencies });
|
||||||
|
@ -192,10 +192,6 @@ export interface ExploreItemState {
|
|||||||
range: TimeRange;
|
range: TimeRange;
|
||||||
|
|
||||||
absoluteRange: AbsoluteTimeRange;
|
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.
|
* True if scanning for more results is active.
|
||||||
*/
|
*/
|
||||||
@ -334,8 +330,6 @@ export interface QueryTransaction {
|
|||||||
scanning?: boolean;
|
scanning?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type RangeScanner = () => RawTimeRange;
|
|
||||||
|
|
||||||
export interface TextMatch {
|
export interface TextMatch {
|
||||||
text: string;
|
text: string;
|
||||||
start: number;
|
start: number;
|
||||||
|
@ -98,6 +98,10 @@
|
|||||||
padding: 10px 2px;
|
padding: 10px 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.explore-toolbar-content-item.timepicker {
|
||||||
|
z-index: $zindex-timepicker-popover;
|
||||||
|
}
|
||||||
|
|
||||||
.explore-toolbar-content-item:first-child {
|
.explore-toolbar-content-item:first-child {
|
||||||
padding-left: $dashboard-padding;
|
padding-left: $dashboard-padding;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
|
@ -17,6 +17,8 @@ import { EpicDependencies } from 'app/store/configureStore';
|
|||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { DEFAULT_RANGE } from 'app/core/utils/explore';
|
import { DEFAULT_RANGE } from 'app/core/utils/explore';
|
||||||
|
|
||||||
|
export const MOCKED_ABSOLUTE_RANGE = { from: 1, to: 2 };
|
||||||
|
|
||||||
export const epicTester = (
|
export const epicTester = (
|
||||||
epic: Epic<ActionOf<any>, ActionOf<any>, StoreState, EpicDependencies>,
|
epic: Epic<ActionOf<any>, ActionOf<any>, StoreState, EpicDependencies>,
|
||||||
state?: Partial<StoreState>,
|
state?: Partial<StoreState>,
|
||||||
@ -48,6 +50,8 @@ export const epicTester = (
|
|||||||
|
|
||||||
const getTimeRange = jest.fn().mockReturnValue(DEFAULT_RANGE);
|
const getTimeRange = jest.fn().mockReturnValue(DEFAULT_RANGE);
|
||||||
|
|
||||||
|
const getShiftedTimeRange = jest.fn().mockReturnValue(MOCKED_ABSOLUTE_RANGE);
|
||||||
|
|
||||||
const getTimeZone = jest.fn().mockReturnValue(DefaultTimeZone);
|
const getTimeZone = jest.fn().mockReturnValue(DefaultTimeZone);
|
||||||
|
|
||||||
const toUtc = jest.fn().mockReturnValue(null);
|
const toUtc = jest.fn().mockReturnValue(null);
|
||||||
@ -61,6 +65,7 @@ export const epicTester = (
|
|||||||
getTimeZone,
|
getTimeZone,
|
||||||
toUtc,
|
toUtc,
|
||||||
dateTime,
|
dateTime,
|
||||||
|
getShiftedTimeRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
const theDependencies: EpicDependencies = { ...defaultDependencies, ...dependencies };
|
const theDependencies: EpicDependencies = { ...defaultDependencies, ...dependencies };
|
||||||
|
Loading…
Reference in New Issue
Block a user