Commit 0d3f24ce by David Kaltschmidt

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
parent 7a30f729
...@@ -41,6 +41,6 @@ export default class ElapsedTime extends PureComponent<any, any> { ...@@ -41,6 +41,6 @@ export default class ElapsedTime extends PureComponent<any, any> {
const { elapsed } = this.state; const { elapsed } = this.state;
const { className, time } = this.props; const { className, time } = this.props;
const value = (time || elapsed) / 1000; const value = (time || elapsed) / 1000;
return <span className={className}>{value.toFixed(1)}s</span>; return <span className={`elapsed-time ${className}`}>{value.toFixed(1)}s</span>;
} }
} }
...@@ -8,6 +8,7 @@ import Legend from './Legend'; ...@@ -8,6 +8,7 @@ import Legend from './Legend';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import Graph from './Graph'; import Graph from './Graph';
import Table from './Table'; import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
import { decodePathComponent } from 'app/core/utils/location_util'; import { decodePathComponent } from 'app/core/utils/location_util';
...@@ -15,40 +16,33 @@ import { decodePathComponent } from 'app/core/utils/location_util'; ...@@ -15,40 +16,33 @@ import { decodePathComponent } from 'app/core/utils/location_util';
function makeTimeSeriesList(dataList, options) { function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => { return dataList.map((seriesData, index) => {
const datapoints = seriesData.datapoints || []; 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 colorIndex = index % colors.length;
const color = colors[colorIndex]; const color = colors[colorIndex];
const series = new TimeSeries({ const series = new TimeSeries({
datapoints: datapoints, datapoints,
alias: alias, alias,
color: color, color,
unit: seriesData.unit, 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; return series;
}); });
} }
function parseInitialQueries(initial) { function parseInitialState(initial) {
if (!initial) {
return [];
}
try { try {
const parsed = JSON.parse(decodePathComponent(initial)); 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) { } catch (e) {
console.error(e); console.error(e);
return []; return { queries: [], range: DEFAULT_RANGE };
} }
} }
...@@ -60,6 +54,7 @@ interface IExploreState { ...@@ -60,6 +54,7 @@ interface IExploreState {
latency: number; latency: number;
loading: any; loading: any;
queries: any; queries: any;
range: any;
requestOptions: any; requestOptions: any;
showingGraph: boolean; showingGraph: boolean;
showingTable: boolean; showingTable: boolean;
...@@ -72,7 +67,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -72,7 +67,7 @@ export class Explore extends React.Component<any, IExploreState> {
constructor(props) { constructor(props) {
super(props); super(props);
const initialQueries = parseInitialQueries(props.routeParams.initial); const { range, queries } = parseInitialState(props.routeParams.initial);
this.state = { this.state = {
datasource: null, datasource: null,
datasourceError: null, datasourceError: null,
...@@ -80,7 +75,8 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -80,7 +75,8 @@ export class Explore extends React.Component<any, IExploreState> {
graphResult: null, graphResult: null,
latency: 0, latency: 0,
loading: false, loading: false,
queries: ensureQueries(initialQueries), queries: ensureQueries(queries),
range: range || { ...DEFAULT_RANGE },
requestOptions: null, requestOptions: null,
showingGraph: true, showingGraph: true,
showingTable: true, showingTable: true,
...@@ -119,6 +115,14 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -119,6 +115,14 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState({ queries: nextQueries }); this.setState({ queries: nextQueries });
}; };
handleChangeTime = nextRange => {
const range = {
from: nextRange.from,
to: nextRange.to,
};
this.setState({ range }, () => this.handleSubmit());
};
handleClickGraphButton = () => { handleClickGraphButton = () => {
this.setState(state => ({ showingGraph: !state.showingGraph })); this.setState(state => ({ showingGraph: !state.showingGraph }));
}; };
...@@ -147,7 +151,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -147,7 +151,7 @@ export class Explore extends React.Component<any, IExploreState> {
}; };
async runGraphQuery() { async runGraphQuery() {
const { datasource, queries } = this.state; const { datasource, queries, range } = this.state;
if (!hasQuery(queries)) { if (!hasQuery(queries)) {
return; return;
} }
...@@ -157,7 +161,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -157,7 +161,7 @@ export class Explore extends React.Component<any, IExploreState> {
format: 'time_series', format: 'time_series',
interval: datasource.interval, interval: datasource.interval,
instant: false, instant: false,
now, range,
queries: queries.map(q => q.query), queries: queries.map(q => q.query),
}); });
try { try {
...@@ -172,7 +176,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -172,7 +176,7 @@ export class Explore extends React.Component<any, IExploreState> {
} }
async runTableQuery() { async runTableQuery() {
const { datasource, queries } = this.state; const { datasource, queries, range } = this.state;
if (!hasQuery(queries)) { if (!hasQuery(queries)) {
return; return;
} }
...@@ -182,7 +186,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -182,7 +186,7 @@ export class Explore extends React.Component<any, IExploreState> {
format: 'table', format: 'table',
interval: datasource.interval, interval: datasource.interval,
instant: true, instant: true,
now, range,
queries: queries.map(q => q.query), queries: queries.map(q => q.query),
}); });
try { try {
...@@ -210,6 +214,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -210,6 +214,7 @@ export class Explore extends React.Component<any, IExploreState> {
latency, latency,
loading, loading,
queries, queries,
range,
requestOptions, requestOptions,
showingGraph, showingGraph,
showingTable, showingTable,
...@@ -229,14 +234,8 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -229,14 +234,8 @@ export class Explore extends React.Component<any, IExploreState> {
{datasource ? ( {datasource ? (
<div className="m-r-3"> <div className="m-r-3">
<div className="nav m-b-1"> <div className="nav m-b-1 navbar">
<div className="pull-right"> <div className="navbar-buttons">
{loading || latency ? <ElapsedTime time={latency} className="" /> : null}
<button type="submit" className="m-l-1 btn btn-primary" onClick={this.handleSubmit}>
<i className="fa fa-return" /> Run Query
</button>
</div>
<div>
<button className={graphButtonClassName} onClick={this.handleClickGraphButton}> <button className={graphButtonClassName} onClick={this.handleClickGraphButton}>
Graph Graph
</button> </button>
...@@ -244,6 +243,14 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -244,6 +243,14 @@ export class Explore extends React.Component<any, IExploreState> {
Table Table
</button> </button>
</div> </div>
<div className="navbar__spacer" />
<TimePicker range={range} onChangeTime={this.handleChangeTime} />
<div className="navbar-buttons">
<button type="submit" className="btn btn-primary" onClick={this.handleSubmit}>
<i className="fa fa-return" /> Run Query
</button>
</div>
{loading || latency ? <ElapsedTime time={latency} className="" /> : null}
</div> </div>
<QueryRows <QueryRows
queries={queries} queries={queries}
......
import $ from 'jquery'; import $ from 'jquery';
import React, { Component } from 'react'; import React, { Component } from 'react';
import moment from 'moment';
import * as dateMath from 'app/core/utils/datemath';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot';
...@@ -90,8 +92,15 @@ class Graph extends Component<any, any> { ...@@ -90,8 +92,15 @@ class Graph extends Component<any, any> {
const $el = $(`#${this.props.id}`); const $el = $(`#${this.props.id}`);
const ticks = $el.width() / 100; const ticks = $el.width() / 100;
const min = userOptions.range.from.valueOf(); let { from, to } = userOptions.range;
const max = userOptions.range.to.valueOf(); 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 = { const dynamicOptions = {
xaxis: { xaxis: {
mode: 'time', mode: 'time',
......
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<any, any> {
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 (
<div ref={this.dropdownRef} className="gf-timepicker-dropdown">
<form name="timeForm" className="gf-timepicker-absolute-section">
<h3 className="section-heading">Custom range</h3>
<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"
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" value={toRaw} onChange={this.handleChangeTo} />
</div>
</div>
{/* <label className="small">Refreshing every:</label>
<div className="gf-form-inline">
<div className="gf-form max-width-28">
<select className="gf-form-input input-medium" ng-options="f.value as f.text for f in ctrl.refresh.options"></select>
</div>
</div> */}
</form>
<div className="gf-timepicker-relative-section">
<h3 className="section-heading">Quick ranges</h3>
{Object.keys(timeOptions).map(section => {
const group = timeOptions[section];
return (
<ul key={section}>
{group.map(option => (
<li className={option.active ? 'active' : ''} key={option.display}>
<a onClick={() => this.handleClickRelativeOption(option)}>{option.display}</a>
</li>
))}
</ul>
);
})}
</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" 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> {rangeString}</span>
{isUtc ? <span className="gf-timepicker-utc">UTC</span> : null}
{refreshInterval ? <span className="text-warning">&nbsp; Refresh every {refreshInterval}</span> : null}
</button>
<button className="btn navbar-button navbar-button--tight" onClick={this.handleClickRight}>
<i className="fa fa-chevron-right" />
</button>
</div>
{this.renderDropdown()}
</div>
);
}
}
export function buildQueryOptions({ format, interval, instant, now, queries }) { export function buildQueryOptions({ format, interval, instant, range, queries }) {
const to = now;
const from = to - 1000 * 60 * 60 * 3;
return { return {
interval, interval,
range: { range,
from,
to,
},
targets: queries.map(expr => ({ targets: queries.map(expr => ({
expr, expr,
format, format,
......
...@@ -14,7 +14,7 @@ export class KeybindingSrv { ...@@ -14,7 +14,7 @@ export class KeybindingSrv {
timepickerOpen = false; timepickerOpen = false;
/** @ngInject */ /** @ngInject */
constructor(private $rootScope, private $location, private datasourceSrv) { constructor(private $rootScope, private $location, private datasourceSrv, private timeSrv) {
// clear out all shortcuts on route change // clear out all shortcuts on route change
$rootScope.$on('$routeChangeSuccess', () => { $rootScope.$on('$routeChangeSuccess', () => {
Mousetrap.reset(); Mousetrap.reset();
...@@ -182,7 +182,12 @@ export class KeybindingSrv { ...@@ -182,7 +182,12 @@ export class KeybindingSrv {
const panel = dashboard.getPanelById(dashboard.meta.focusPanelId); const panel = dashboard.getPanelById(dashboard.meta.focusPanelId);
const datasource = await this.datasourceSrv.get(panel.datasource); const datasource = await this.datasourceSrv.get(panel.datasource);
if (datasource && datasource.supportsExplore) { 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}`); this.$location.url(`/explore/${exploreState}`);
} }
} }
......
...@@ -324,7 +324,12 @@ class MetricsPanelCtrl extends PanelCtrl { ...@@ -324,7 +324,12 @@ class MetricsPanelCtrl extends PanelCtrl {
} }
explore() { 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}`); this.$location.url(`/explore/${exploreState}`);
} }
......
.explore { .explore {
.navbar {
padding-left: 0;
padding-right: 0;
}
.elapsed-time {
position: absolute;
right: -2.4rem;
top: 1.2rem;
}
.graph-legend { .graph-legend {
flex-wrap: wrap; flex-wrap: wrap;
} }
.timepicker {
display: flex;
}
} }
.query-row { .query-row {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment