浏览代码

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
David Kaltschmidt 7 年之前
父节点
当前提交
0d3f24ce54

+ 1 - 1
public/app/containers/Explore/ElapsedTime.tsx

@@ -41,6 +41,6 @@ export default class ElapsedTime extends PureComponent<any, any> {
     const { elapsed } = this.state;
     const { className, time } = this.props;
     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>;
   }
 }

+ 40 - 33
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<any, IExploreState> {
 
   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<any, IExploreState> {
       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<any, IExploreState> {
     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<any, IExploreState> {
   };
 
   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<any, IExploreState> {
       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<any, IExploreState> {
   }
 
   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<any, IExploreState> {
       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<any, IExploreState> {
       latency,
       loading,
       queries,
+      range,
       requestOptions,
       showingGraph,
       showingTable,
@@ -229,14 +234,8 @@ export class Explore extends React.Component<any, IExploreState> {
 
           {datasource ? (
             <div className="m-r-3">
-              <div className="nav m-b-1">
-                <div className="pull-right">
-                  {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>
+              <div className="nav m-b-1 navbar">
+                <div className="navbar-buttons">
                   <button className={graphButtonClassName} onClick={this.handleClickGraphButton}>
                     Graph
                   </button>
@@ -244,6 +243,14 @@ export class Explore extends React.Component<any, IExploreState> {
                     Table
                   </button>
                 </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>
               <QueryRows
                 queries={queries}

+ 11 - 2
public/app/containers/Explore/Graph.tsx

@@ -1,6 +1,8 @@
 import $ from 'jquery';
 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 'vendor/flot/jquery.flot';
@@ -90,8 +92,15 @@ class Graph extends Component<any, any> {
 
     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',

+ 192 - 0
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<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>
+    );
+  }
+}

+ 2 - 7
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,

+ 7 - 2
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}`);
         }
       }

+ 6 - 1
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}`);
   }
 

+ 14 - 0
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 {