mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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
This commit is contained in:
parent
7a30f72902
commit
0d3f24ce54
@ -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';
|
|||||||
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';
|
|||||||
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 {
|
|||||||
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> {
|
|||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
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> {
|
|||||||
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> {
|
|||||||
|
|
||||||
{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> {
|
|||||||
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}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
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> {
|
|||||||
|
|
||||||
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',
|
||||||
|
192
public/app/containers/Explore/TimePicker.tsx
Normal file
192
public/app/containers/Explore/TimePicker.tsx
Normal file
@ -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"> 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,7 @@
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,21 @@
|
|||||||
.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 {
|
||||||
|
Loading…
Reference in New Issue
Block a user