From 0d3f24ce54782c06ce0ba534786eb0e2e90ac2e6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 30 Apr 2018 17:25:25 +0200 Subject: [PATCH] Explore: time selector * time selector for explore section * mostly ported the angular time selector, but left out the timepicker (3rd-party angular component) * can be initialised via url parameters (jump from panels to explore) * refreshing not implemented for now * moved the forward/backward nav buttons around the time selector --- public/app/containers/Explore/ElapsedTime.tsx | 2 +- public/app/containers/Explore/Explore.tsx | 73 ++++--- public/app/containers/Explore/Graph.tsx | 13 +- public/app/containers/Explore/TimePicker.tsx | 192 ++++++++++++++++++ public/app/containers/Explore/utils/query.ts | 9 +- public/app/core/services/keybindingSrv.ts | 9 +- .../app/features/panel/metrics_panel_ctrl.ts | 7 +- public/sass/pages/_explore.scss | 14 ++ 8 files changed, 273 insertions(+), 46 deletions(-) create mode 100644 public/app/containers/Explore/TimePicker.tsx diff --git a/public/app/containers/Explore/ElapsedTime.tsx b/public/app/containers/Explore/ElapsedTime.tsx index 9cd8f674186..a2d941515cd 100644 --- a/public/app/containers/Explore/ElapsedTime.tsx +++ b/public/app/containers/Explore/ElapsedTime.tsx @@ -41,6 +41,6 @@ export default class ElapsedTime extends PureComponent { const { elapsed } = this.state; const { className, time } = this.props; const value = (time || elapsed) / 1000; - return {value.toFixed(1)}s; + return {value.toFixed(1)}s; } } diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 40261ee635a..66500353812 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -8,6 +8,7 @@ import Legend from './Legend'; import QueryRows from './QueryRows'; import Graph from './Graph'; import Table from './Table'; +import TimePicker, { DEFAULT_RANGE } from './TimePicker'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { decodePathComponent } from 'app/core/utils/location_util'; @@ -15,40 +16,33 @@ import { decodePathComponent } from 'app/core/utils/location_util'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; - const alias = seriesData.target; - + const responseAlias = seriesData.target; + const query = options.targets[index].expr; + const alias = responseAlias && responseAlias !== '{}' ? responseAlias : query; const colorIndex = index % colors.length; const color = colors[colorIndex]; const series = new TimeSeries({ - datapoints: datapoints, - alias: alias, - color: color, + datapoints, + alias, + color, unit: seriesData.unit, }); - if (datapoints && datapoints.length > 0) { - const last = datapoints[datapoints.length - 1][1]; - const from = options.range.from; - if (last - from < -10000) { - series.isOutsideRange = true; - } - } - return series; }); } -function parseInitialQueries(initial) { - if (!initial) { - return []; - } +function parseInitialState(initial) { try { const parsed = JSON.parse(decodePathComponent(initial)); - return parsed.queries.map(q => q.query); + return { + queries: parsed.queries.map(q => q.query), + range: parsed.range, + }; } catch (e) { console.error(e); - return []; + return { queries: [], range: DEFAULT_RANGE }; } } @@ -60,6 +54,7 @@ interface IExploreState { latency: number; loading: any; queries: any; + range: any; requestOptions: any; showingGraph: boolean; showingTable: boolean; @@ -72,7 +67,7 @@ export class Explore extends React.Component { constructor(props) { super(props); - const initialQueries = parseInitialQueries(props.routeParams.initial); + const { range, queries } = parseInitialState(props.routeParams.initial); this.state = { datasource: null, datasourceError: null, @@ -80,7 +75,8 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, - queries: ensureQueries(initialQueries), + queries: ensureQueries(queries), + range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, showingTable: true, @@ -119,6 +115,14 @@ export class Explore extends React.Component { this.setState({ queries: nextQueries }); }; + handleChangeTime = nextRange => { + const range = { + from: nextRange.from, + to: nextRange.to, + }; + this.setState({ range }, () => this.handleSubmit()); + }; + handleClickGraphButton = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; @@ -147,7 +151,7 @@ export class Explore extends React.Component { }; async runGraphQuery() { - const { datasource, queries } = this.state; + const { datasource, queries, range } = this.state; if (!hasQuery(queries)) { return; } @@ -157,7 +161,7 @@ export class Explore extends React.Component { format: 'time_series', interval: datasource.interval, instant: false, - now, + range, queries: queries.map(q => q.query), }); try { @@ -172,7 +176,7 @@ export class Explore extends React.Component { } async runTableQuery() { - const { datasource, queries } = this.state; + const { datasource, queries, range } = this.state; if (!hasQuery(queries)) { return; } @@ -182,7 +186,7 @@ export class Explore extends React.Component { format: 'table', interval: datasource.interval, instant: true, - now, + range, queries: queries.map(q => q.query), }); try { @@ -210,6 +214,7 @@ export class Explore extends React.Component { latency, loading, queries, + range, requestOptions, showingGraph, showingTable, @@ -229,14 +234,8 @@ export class Explore extends React.Component { {datasource ? (
-
-
- {loading || latency ? : null} - -
-
+
+
@@ -244,6 +243,14 @@ export class Explore extends React.Component { Table
+
+ +
+ +
+ {loading || latency ? : null}
{ const $el = $(`#${this.props.id}`); const ticks = $el.width() / 100; - const min = userOptions.range.from.valueOf(); - const max = userOptions.range.to.valueOf(); + let { from, to } = userOptions.range; + if (!moment.isMoment(from)) { + from = dateMath.parse(from, false); + } + if (!moment.isMoment(to)) { + to = dateMath.parse(to, true); + } + const min = from.valueOf(); + const max = to.valueOf(); const dynamicOptions = { xaxis: { mode: 'time', diff --git a/public/app/containers/Explore/TimePicker.tsx b/public/app/containers/Explore/TimePicker.tsx new file mode 100644 index 00000000000..b67cd532019 --- /dev/null +++ b/public/app/containers/Explore/TimePicker.tsx @@ -0,0 +1,192 @@ +import React, { PureComponent } from 'react'; +import moment from 'moment'; + +import * as dateMath from 'app/core/utils/datemath'; +import * as rangeUtil from 'app/core/utils/rangeutil'; + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; + +export default class TimePicker extends PureComponent { + dropdownEl: any; + constructor(props) { + super(props); + this.state = { + fromRaw: props.range ? props.range.from : DEFAULT_RANGE.from, + isOpen: false, + isUtc: false, + rangeString: rangeUtil.describeTimeRange(props.range || DEFAULT_RANGE), + refreshInterval: '', + toRaw: props.range ? props.range.to : DEFAULT_RANGE.to, + }; + } + + move(direction) { + const { onChangeTime } = this.props; + const { fromRaw, toRaw } = this.state; + const range = { + from: dateMath.parse(fromRaw, false), + to: dateMath.parse(toRaw, true), + }; + + 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(); + } + + const rangeString = rangeUtil.describeTimeRange(range); + to = moment.utc(to); + from = moment.utc(from); + + this.setState( + { + rangeString, + fromRaw: from, + toRaw: to, + }, + () => { + onChangeTime({ to, from }); + } + ); + } + + handleChangeFrom = e => { + this.setState({ + fromRaw: e.target.value, + }); + }; + + handleChangeTo = e => { + this.setState({ + toRaw: e.target.value, + }); + }; + + 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); + this.setState( + { + toRaw: range.to, + fromRaw: range.from, + isOpen: false, + rangeString, + }, + () => { + if (onChangeTime) { + onChangeTime(range); + } + } + ); + }; + + 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 ( +
+
+

Custom range

+ + +
+
+ +
+
+ + +
+
+ +
+
+ + {/* +
+
+ +
+
*/} +
+ +
+

Quick ranges

+ {Object.keys(timeOptions).map(section => { + const group = timeOptions[section]; + return ( + + ); + })} +
+
+ ); + } + + render() { + const { isUtc, rangeString, refreshInterval } = this.state; + return ( +
+
+ + + +
+ {this.renderDropdown()} +
+ ); + } +} diff --git a/public/app/containers/Explore/utils/query.ts b/public/app/containers/Explore/utils/query.ts index d51c7339944..3aa0cc5b357 100644 --- a/public/app/containers/Explore/utils/query.ts +++ b/public/app/containers/Explore/utils/query.ts @@ -1,12 +1,7 @@ -export function buildQueryOptions({ format, interval, instant, now, queries }) { - const to = now; - const from = to - 1000 * 60 * 60 * 3; +export function buildQueryOptions({ format, interval, instant, range, queries }) { return { interval, - range: { - from, - to, - }, + range, targets: queries.map(expr => ({ expr, format, diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 94bf9efb31b..25d00ab37f1 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -14,7 +14,7 @@ export class KeybindingSrv { timepickerOpen = false; /** @ngInject */ - constructor(private $rootScope, private $location, private datasourceSrv) { + constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv) { // clear out all shortcuts on route change $rootScope.$on('$routeChangeSuccess', () => { Mousetrap.reset(); @@ -182,7 +182,12 @@ export class KeybindingSrv { const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const datasource = await this.datasourceSrv.get(panel.datasource); if (datasource && datasource.supportsExplore) { - const exploreState = encodePathComponent(JSON.stringify(datasource.getExploreState(panel))); + const range = this.timeSrv.timeRangeForUrl(); + const state = { + ...datasource.getExploreState(panel), + range, + }; + const exploreState = encodePathComponent(JSON.stringify(state)); this.$location.url(`/explore/${exploreState}`); } } diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index d460b27a679..3c48119ba3a 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -324,7 +324,12 @@ class MetricsPanelCtrl extends PanelCtrl { } explore() { - const exploreState = encodePathComponent(JSON.stringify(this.datasource.getExploreState(this.panel))); + const range = this.timeSrv.timeRangeForUrl(); + const state = { + ...this.datasource.getExploreState(this.panel), + range, + }; + const exploreState = encodePathComponent(JSON.stringify(state)); this.$location.url(`/explore/${exploreState}`); } diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 855d11cb859..200af40341e 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,7 +1,21 @@ .explore { + .navbar { + padding-left: 0; + padding-right: 0; + } + + .elapsed-time { + position: absolute; + right: -2.4rem; + top: 1.2rem; + } .graph-legend { flex-wrap: wrap; } + + .timepicker { + display: flex; + } } .query-row {