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..deebe84f2c8 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -4,10 +4,10 @@ import colors from 'app/core/utils/colors'; import TimeSeries from 'app/core/time_series2'; import ElapsedTime from './ElapsedTime'; -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'; @@ -16,39 +16,30 @@ function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; const alias = seriesData.target; - 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 +51,8 @@ interface IExploreState { latency: number; loading: any; queries: any; + queryError: any; + range: any; requestOptions: any; showingGraph: boolean; showingTable: boolean; @@ -72,7 +65,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,11 +73,14 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, - queries: ensureQueries(initialQueries), + queries: ensureQueries(queries), + queryError: null, + range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, showingTable: true, tableResult: null, + ...props.initialState, }; } @@ -98,6 +94,10 @@ export class Explore extends React.Component { } } + componentDidCatch(error) { + console.error(error); + } + handleAddQueryRow = index => { const { queries } = this.state; const nextQueries = [ @@ -119,10 +119,32 @@ 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()); + }; + + handleClickCloseSplit = () => { + const { onChangeSplit } = this.props; + if (onChangeSplit) { + onChangeSplit(false); + } + }; + handleClickGraphButton = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; + handleClickSplit = () => { + const { onChangeSplit } = this.props; + if (onChangeSplit) { + onChangeSplit(true, this.state); + } + }; + handleClickTableButton = () => { this.setState(state => ({ showingTable: !state.showingTable })); }; @@ -147,17 +169,17 @@ export class Explore extends React.Component { }; async runGraphQuery() { - const { datasource, queries } = this.state; + const { datasource, queries, range } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, graphResult: null }); + this.setState({ latency: 0, loading: true, graphResult: null, queryError: null }); const now = Date.now(); const options = buildQueryOptions({ format: 'time_series', interval: datasource.interval, instant: false, - now, + range, queries: queries.map(q => q.query), }); try { @@ -165,24 +187,25 @@ export class Explore extends React.Component { const result = makeTimeSeriesList(res.data, options); const latency = Date.now() - now; this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); - } catch (error) { - console.error(error); - this.setState({ loading: false, graphResult: error }); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.setState({ loading: false, queryError }); } } async runTableQuery() { - const { datasource, queries } = this.state; + const { datasource, queries, range } = this.state; if (!hasQuery(queries)) { return; } - this.setState({ latency: 0, loading: true, tableResult: null }); + this.setState({ latency: 0, loading: true, queryError: null, tableResult: null }); const now = Date.now(); const options = buildQueryOptions({ format: 'table', interval: datasource.interval, instant: true, - now, + range, queries: queries.map(q => q.query), }); try { @@ -190,9 +213,10 @@ export class Explore extends React.Component { const tableModel = res.data[0]; const latency = Date.now() - now; this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); - } catch (error) { - console.error(error); - this.setState({ loading: false, tableResult: null }); + } catch (response) { + console.error(response); + const queryError = response.data ? response.data.error : response; + this.setState({ loading: false, queryError }); } } @@ -202,6 +226,7 @@ export class Explore extends React.Component { }; render() { + const { position, split } = this.props; const { datasource, datasourceError, @@ -210,59 +235,93 @@ export class Explore extends React.Component { latency, loading, queries, + queryError, + range, requestOptions, showingGraph, showingTable, tableResult, } = this.state; const showingBoth = showingGraph && showingTable; - const graphHeight = showingBoth ? '200px' : null; - const graphButtonClassName = showingBoth || showingGraph ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; - const tableButtonClassName = showingBoth || showingTable ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; + const graphHeight = showingBoth ? '200px' : '400px'; + const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; + const tableButtonActive = showingBoth || showingTable ? 'active' : ''; + const exploreClass = split ? 'explore explore-split' : 'explore'; return ( -
-
-

Explore

- {datasourceLoading ?
Loading datasource...
: null} - - {datasourceError ?
Error connecting to datasource.
: null} - - {datasource ? ( -
-
-
- {loading || latency ? : null} - -
-
- - -
-
- -
- {showingGraph ? ( - - ) : null} - {showingGraph ? : null} - {showingTable ? : null} - +
+
+ {position === 'left' ? ( + + ) : ( +
+ +
+ )} +
+ {position === 'left' && !split ? ( +
+
) : null} +
+ + +
+ +
+ + {loading || latency ? : null} +
+ + {datasourceLoading ?
Loading datasource...
: null} + + {datasourceError ? ( +
+ Error connecting to datasource. +
+ ) : null} + + {datasource ? ( +
+ + {queryError ?
{queryError}
: null} +
+ {showingGraph ? ( + + ) : null} + {showingTable ?
: null} + + + ) : null} ); } diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx index 0a13b39619d..a43ddfb2aa5 100644 --- a/public/app/containers/Explore/Graph.tsx +++ b/public/app/containers/Explore/Graph.tsx @@ -1,10 +1,13 @@ import $ from 'jquery'; import React, { Component } from 'react'; - -import TimeSeries from 'app/core/time_series2'; +import moment from 'moment'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; +import * as dateMath from 'app/core/utils/datemath'; +import TimeSeries from 'app/core/time_series2'; + +import Legend from './Legend'; // Copied from graph.ts function time_format(ticks, min, max) { @@ -72,6 +75,7 @@ class Graph extends Component { if ( prevProps.data !== this.props.data || prevProps.options !== this.props.options || + prevProps.split !== this.props.split || prevProps.height !== this.props.height ) { this.draw(); @@ -84,14 +88,22 @@ class Graph extends Component { return; } const series = data.map((ts: TimeSeries) => ({ + color: ts.color, label: ts.label, data: ts.getFlotPairs('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', @@ -111,12 +123,13 @@ class Graph extends Component { } render() { - const style = { - height: this.props.height || '400px', - width: this.props.width || '100%', - }; - - return
; + const { data, height } = this.props; + return ( +
+
+ +
+ ); } } diff --git a/public/app/containers/Explore/QueryField.tsx b/public/app/containers/Explore/QueryField.tsx index 816473619fd..53354584fea 100644 --- a/public/app/containers/Explore/QueryField.tsx +++ b/public/app/containers/Explore/QueryField.tsx @@ -50,7 +50,7 @@ class Portal extends React.Component { constructor(props) { super(props); this.node = document.createElement('div'); - this.node.classList.add(`query-field-portal-${props.index}`); + this.node.classList.add('explore-typeahead', `explore-typeahead-${props.index}`); document.body.appendChild(this.node); } diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index 3940d16b2f6..74f6c28d41b 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -48,10 +48,10 @@ class QueryRow extends PureComponent { return (
- -
@@ -60,6 +60,7 @@ class QueryRow extends PureComponent { initialQuery={edited ? null : query} onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} + placeholder="Enter a PromQL query" request={request} />
diff --git a/public/app/containers/Explore/TimePicker.jest.tsx b/public/app/containers/Explore/TimePicker.jest.tsx new file mode 100644 index 00000000000..afe6b092901 --- /dev/null +++ b/public/app/containers/Explore/TimePicker.jest.tsx @@ -0,0 +1,74 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import sinon from 'sinon'; + +import * as rangeUtil from 'app/core/utils/rangeutil'; +import TimePicker, { DEFAULT_RANGE, parseTime } from './TimePicker'; + +describe('', () => { + it('renders closed with default values', () => { + const rangeString = rangeUtil.describeTimeRange(DEFAULT_RANGE); + const wrapper = shallow(); + expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString); + expect(wrapper.find('.gf-timepicker-dropdown').exists()).toBe(false); + }); + + it('renders with relative range', () => { + const range = { + from: 'now-7h', + to: 'now', + }; + const rangeString = rangeUtil.describeTimeRange(range); + const wrapper = shallow(); + expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString); + expect(wrapper.state('fromRaw')).toBe(range.from); + expect(wrapper.state('toRaw')).toBe(range.to); + expect(wrapper.find('.timepicker-from').props().value).toBe(range.from); + expect(wrapper.find('.timepicker-to').props().value).toBe(range.to); + }); + + it('renders with epoch (millies) range converted to ISO-ish', () => { + const range = { + from: '1', + to: '1000', + }; + const rangeString = rangeUtil.describeTimeRange({ + from: parseTime(range.from), + to: parseTime(range.to), + }); + const wrapper = shallow(); + expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00'); + expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01'); + expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString); + 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'); + }); + + it('moves ranges forward and backward by half the range on arrow click', () => { + const range = { + from: '2000', + to: '4000', + }; + const rangeString = rangeUtil.describeTimeRange({ + from: parseTime(range.from), + to: parseTime(range.to), + }); + + const onChangeTime = sinon.spy(); + const wrapper = shallow(); + expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02'); + expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04'); + expect(wrapper.find('.timepicker-rangestring').text()).toBe(rangeString); + expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:02'); + expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:04'); + + wrapper.find('.timepicker-left').simulate('click'); + expect(onChangeTime.calledOnce).toBe(true); + 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(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02'); + expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04'); + }); +}); diff --git a/public/app/containers/Explore/TimePicker.tsx b/public/app/containers/Explore/TimePicker.tsx new file mode 100644 index 00000000000..3ae4ea4a83c --- /dev/null +++ b/public/app/containers/Explore/TimePicker.tsx @@ -0,0 +1,245 @@ +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'; + +const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss'; + +export const DEFAULT_RANGE = { + from: 'now-6h', + to: 'now', +}; + +export function parseTime(value, isUtc = false, asString = false) { + if (value.indexOf('now') !== -1) { + return value; + } + if (!isNaN(value)) { + const epoch = parseInt(value); + const m = isUtc ? moment.utc(epoch) : moment(epoch); + return asString ? m.format(DATE_FORMAT) : m; + } + return undefined; +} + +export default class TimePicker extends PureComponent { + dropdownEl: any; + constructor(props) { + super(props); + + const fromRaw = props.range ? props.range.from : DEFAULT_RANGE.from; + const toRaw = props.range ? props.range.to : DEFAULT_RANGE.to; + const range = { + from: parseTime(fromRaw), + to: parseTime(toRaw), + }; + this.state = { + fromRaw: parseTime(fromRaw, props.isUtc, true), + isOpen: props.isOpen, + isUtc: props.isUtc, + rangeString: rangeUtil.describeTimeRange(range), + refreshInterval: '', + toRaw: parseTime(toRaw, props.isUtc, true), + }; + } + + 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); + // No need to convert to UTC again + to = moment(to); + from = moment(from); + + this.setState( + { + rangeString, + fromRaw: from.format(DATE_FORMAT), + toRaw: to.format(DATE_FORMAT), + }, + () => { + onChangeTime({ to, from }); + } + ); + } + + handleChangeFrom = e => { + this.setState({ + fromRaw: e.target.value, + }); + }; + + handleChangeTo = e => { + this.setState({ + toRaw: e.target.value, + }); + }; + + handleClickApply = () => { + const { onChangeTime } = this.props; + const { toRaw, fromRaw } = this.state; + const range = { + from: dateMath.parse(fromRaw, false), + to: dateMath.parse(toRaw, true), + }; + const rangeString = rangeUtil.describeTimeRange(range); + this.setState( + { + isOpen: false, + rangeString, + }, + () => { + if (onChangeTime) { + onChangeTime(range); + } + } + ); + }; + + 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/Wrapper.tsx b/public/app/containers/Explore/Wrapper.tsx new file mode 100644 index 00000000000..6bdbd7cc42f --- /dev/null +++ b/public/app/containers/Explore/Wrapper.tsx @@ -0,0 +1,33 @@ +import React, { PureComponent } from 'react'; + +import Explore from './Explore'; + +export default class Wrapper extends PureComponent { + state = { + initialState: null, + split: false, + }; + + handleChangeSplit = (split, initialState) => { + this.setState({ split, initialState }); + }; + + render() { + // State overrides for props from first Explore + const { initialState, split } = this.state; + return ( +
+ + {split ? ( + + ) : null} +
+ ); + } +} 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/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index ff7dc1b150c..46431a08ab1 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -164,6 +164,7 @@ export class PrometheusDatasource { legendFormat: activeTargets[index].legendFormat, start: start, end: end, + query: queries[index].expr, responseListLength: responseList.length, responseIndex: index, refId: activeTargets[index].refId, diff --git a/public/app/plugins/datasource/prometheus/result_transformer.ts b/public/app/plugins/datasource/prometheus/result_transformer.ts index d5feda7d28c..7f5430bf7d6 100644 --- a/public/app/plugins/datasource/prometheus/result_transformer.ts +++ b/public/app/plugins/datasource/prometheus/result_transformer.ts @@ -123,11 +123,16 @@ export class ResultTransformer { } createMetricLabel(labelData, options) { + let label = ''; if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { - return this.getOriginalMetricName(labelData); + label = this.getOriginalMetricName(labelData); + } else { + label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData); } - - return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}'; + if (!label || label === '{}') { + label = options.query; + } + return label; } renderTemplate(aliasPattern, aliasData) { diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 6a61315f956..b10084d1941 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -3,7 +3,6 @@ import './ReactContainer'; import ServerStats from 'app/containers/ServerStats/ServerStats'; import AlertRuleList from 'app/containers/AlertRuleList/AlertRuleList'; -// import Explore from 'app/containers/Explore/Explore'; import FolderSettings from 'app/containers/ManageDashboards/FolderSettings'; import FolderPermissions from 'app/containers/ManageDashboards/FolderPermissions'; @@ -114,7 +113,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/explore/:initial?', { template: '', resolve: { - component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Explore'), + component: () => import(/* webpackChunkName: "explore" */ 'app/containers/Explore/Wrapper'), }, }) .when('/org', { diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 6e86aa1872e..4907540815d 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -45,6 +45,10 @@ $brand-warning: $brand-primary; $brand-danger: $red; $query-blue: $blue; +$query-red: $red; +$query-green: $green; +$query-purple: $purple; +$query-orange: $orange; // Status colors // ------------------------- @@ -176,6 +180,9 @@ $btn-inverse-bg-hl: lighten($dark-3, 4%); $btn-inverse-text-color: $link-color; $btn-inverse-text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1); +$btn-active-bg: $gray-4; +$btn-active-text-color: $blue-dark; + $btn-link-color: $gray-3; $iconContainerBackground: $black; @@ -204,6 +211,11 @@ $input-invalid-border-color: lighten($red, 5%); $search-shadow: 0 0 30px 0 $black; $search-filter-box-bg: $gray-blue; +// Typeahead +$typeahead-shadow: 0 5px 10px 0 $black; +$typeahead-selected-bg: $dark-4; +$typeahead-selected-color: $blue; + // Dropdowns // ------------------------- $dropdownBackground: $dark-3; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index bb8f93dbe69..14716f6dfef 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -46,6 +46,10 @@ $brand-warning: $orange; $brand-danger: $red; $query-blue: $blue-dark; +$query-red: $red; +$query-green: $green; +$query-purple: $purple; +$query-orange: $orange; // Status colors // ------------------------- @@ -173,6 +177,9 @@ $btn-inverse-bg-hl: darken($gray-6, 5%); $btn-inverse-text-color: $gray-1; $btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); +$btn-active-bg: $white; +$btn-active-text-color: $blue-dark; + $btn-link-color: $gray-1; $btn-divider-left: $gray-4; @@ -226,6 +233,11 @@ $tab-border-color: $gray-5; $search-shadow: 0 5px 30px 0 $gray-4; $search-filter-box-bg: $gray-7; +// Typeahead +$typeahead-shadow: 0 5px 10px 0 $gray-5; +$typeahead-selected-bg: lighten($blue, 25%); +$typeahead-selected-color: $blue-dark; + // Dropdowns // ------------------------- $dropdownBackground: $white; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 855d11cb859..7dacccf6a87 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,11 +1,89 @@ .explore { + width: 100%; + + &-container { + padding: 2rem; + } + + &-wrapper { + display: flex; + + > .explore-split { + width: 50%; + } + } + + // Push split button a bit + .explore-first-button { + margin-left: 15px; + } + + // Graph panel needs a bit extra padding at top + .panel-container { + padding: $panel-padding; + padding-top: 10px; + } + + // Make sure wrap buttons around on small screens + .navbar { + flex-wrap: wrap; + height: auto; + } + + .navbar-page-btn { + margin-right: 1rem; + + // Explore icon in header + .fa { + font-size: 100%; + opacity: 0.75; + margin-right: 0.5em; + } + } + + // Toggle mode + .navbar-button.active { + color: $btn-active-text-color; + background-color: $btn-active-bg; + } + + .elapsed-time { + position: absolute; + left: 0; + right: 0; + top: 3.5rem; + text-align: center; + font-size: 0.8rem; + } + .graph-legend { flex-wrap: wrap; } + + .timepicker { + display: flex; + + &-rangestring { + margin-left: 0.5em; + } + } + + .run-icon { + margin-left: 0.5em; + transform: rotate(90deg); + } + + .relative { + position: relative; + } +} + +.explore + .explore { + border-left: 1px dotted $table-border; } .query-row { - position: relative; + display: flex; & + & { margin-top: 0.5rem; @@ -13,17 +91,12 @@ } .query-row-tools { - position: absolute; - left: -4rem; - top: 0.33rem; - > * { - margin-right: 0.25rem; - } + width: 4rem; } .query-field { - font-size: 14px; - font-family: Consolas, Menlo, Courier, monospace; + font-size: $font-size-root; + font-family: $font-family-monospace; height: auto; } @@ -33,54 +106,52 @@ padding: 6px 7px 4px; width: 100%; cursor: text; - line-height: 1.5; - color: rgba(0, 0, 0, 0.65); - background-color: #fff; + line-height: $line-height-base; + color: $text-color-weak; + background-color: $panel-bg; background-image: none; - border: 1px solid lightgray; - border-radius: 3px; + border: $panel-border; + border-radius: $border-radius; transition: all 0.3s; } -.explore { +.explore-typeahead { .typeahead { position: absolute; z-index: auto; top: -10000px; left: -10000px; opacity: 0; - border-radius: 4px; + border-radius: $border-radius; transition: opacity 0.75s; - border: 1px solid #e4e4e4; + border: $panel-border; max-height: calc(66vh); overflow-y: scroll; max-width: calc(66%); overflow-x: hidden; outline: none; list-style: none; - background: #fff; - color: rgba(0, 0, 0, 0.65); + background: $panel-bg; + color: $text-color; transition: opacity 0.4s ease-out; + box-shadow: $typeahead-shadow; } .typeahead-group__title { - color: rgba(0, 0, 0, 0.43); - font-size: 12px; - line-height: 1.5; - padding: 8px 16px; + color: $text-color-weak; + font-size: $font-size-sm; + line-height: $line-height-base; + padding: $input-padding-y $input-padding-x; } .typeahead-item { - line-height: 200%; height: auto; - font-family: Consolas, Menlo, Courier, monospace; - padding: 0 16px 0 28px; - font-size: 12px; + font-family: $font-family-monospace; + padding: $input-padding-y $input-padding-x; + padding-left: $input-padding-x-lg; + font-size: $font-size-sm; text-overflow: ellipsis; overflow: hidden; - margin-left: -1px; - left: 1px; - position: relative; z-index: 1; display: block; white-space: nowrap; @@ -90,234 +161,82 @@ } .typeahead-item__selected { - background-color: #ecf6fd; - color: #108ee9; + background-color: $typeahead-selected-bg; + color: $typeahead-selected-color; } } /* SYNTAX */ -/** - * prism.js Coy theme for JavaScript, CoffeeScript, CSS and HTML - * Based on https://github.com/tshedor/workshop-wp-theme (Example: http://workshop.kansan.com/category/sessions/basics or http://workshop.timshedor.com/category/sessions/basics); - * @author Tim Shedor - */ +.explore { + .token.comment, + .token.block-comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: $text-color-weak; + } -code[class*='language-'], -pre[class*='language-'] { - color: black; - background: none; - font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; - text-align: left; - white-space: pre; - word-spacing: normal; - word-break: normal; - word-wrap: normal; - line-height: 1.5; + .token.punctuation { + color: $text-color-weak; + } - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; + .token.property, + .token.tag, + .token.boolean, + .token.number, + .token.function-name, + .token.constant, + .token.symbol, + .token.deleted { + color: $query-red; + } - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} + .token.selector, + .token.attr-name, + .token.string, + .token.char, + .token.function, + .token.builtin, + .token.inserted { + color: $query-green; + } -/* Code blocks */ -pre[class*='language-'] { - position: relative; - margin: 0.5em 0; - overflow: visible; - padding: 0; -} -pre[class*='language-'] > code { - position: relative; - border-left: 10px solid #358ccb; - box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf; - background-color: #fdfdfd; - background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); - background-size: 3em 3em; - background-origin: content-box; - background-attachment: local; -} + .token.operator, + .token.entity, + .token.url, + .token.variable { + color: $query-purple; + } -code[class*='language'] { - max-height: inherit; - height: inherit; - padding: 0 1em; - display: block; - overflow: auto; -} + .token.atrule, + .token.attr-value, + .token.keyword, + .token.class-name { + color: $query-blue; + } -/* Margin bottom to accomodate shadow */ -:not(pre) > code[class*='language-'], -pre[class*='language-'] { - background-color: #fdfdfd; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; - margin-bottom: 1em; -} + .token.regex, + .token.important { + color: $query-orange; + } -/* Inline code */ -:not(pre) > code[class*='language-'] { - position: relative; - padding: 0.2em; - border-radius: 0.3em; - color: #c92c2c; - border: 1px solid rgba(0, 0, 0, 0.1); - display: inline; - white-space: normal; -} + .token.important { + font-weight: normal; + } -pre[class*='language-']:before, -pre[class*='language-']:after { - content: ''; - z-index: -2; - display: block; - position: absolute; - bottom: 0.75em; - left: 0.18em; - width: 40%; - height: 20%; - max-height: 13em; - box-shadow: 0px 13px 8px #979797; - -webkit-transform: rotate(-2deg); - -moz-transform: rotate(-2deg); - -ms-transform: rotate(-2deg); - -o-transform: rotate(-2deg); - transform: rotate(-2deg); -} + .token.bold { + font-weight: bold; + } + .token.italic { + font-style: italic; + } -:not(pre) > code[class*='language-']:after, -pre[class*='language-']:after { - right: 0.75em; - left: auto; - -webkit-transform: rotate(2deg); - -moz-transform: rotate(2deg); - -ms-transform: rotate(2deg); - -o-transform: rotate(2deg); - transform: rotate(2deg); -} + .token.entity { + cursor: help; + } -.token.comment, -.token.block-comment, -.token.prolog, -.token.doctype, -.token.cdata { - color: #7d8b99; -} - -.token.punctuation { - color: #5f6364; -} - -.token.property, -.token.tag, -.token.boolean, -.token.number, -.token.function-name, -.token.constant, -.token.symbol, -.token.deleted { - color: #c92c2c; -} - -.token.selector, -.token.attr-name, -.token.string, -.token.char, -.token.function, -.token.builtin, -.token.inserted { - color: #2f9c0a; -} - -.token.operator, -.token.entity, -.token.url, -.token.variable { - color: #a67f59; - background: rgba(255, 255, 255, 0.5); -} - -.token.atrule, -.token.attr-value, -.token.keyword, -.token.class-name { - color: #1990b8; -} - -.token.regex, -.token.important { - color: #e90; -} - -.language-css .token.string, -.style .token.string { - color: #a67f59; - background: rgba(255, 255, 255, 0.5); -} - -.token.important { - font-weight: normal; -} - -.token.bold { - font-weight: bold; -} -.token.italic { - font-style: italic; -} - -.token.entity { - cursor: help; -} - -.namespace { - opacity: 0.7; -} - -@media screen and (max-width: 767px) { - pre[class*='language-']:before, - pre[class*='language-']:after { - bottom: 14px; - box-shadow: none; + .namespace { + opacity: 0.7; } } - -/* Plugin styles */ -.token.tab:not(:empty):before, -.token.cr:before, -.token.lf:before { - color: #e0d7d1; -} - -/* Plugin styles: Line Numbers */ -pre[class*='language-'].line-numbers { - padding-left: 0; -} - -pre[class*='language-'].line-numbers code { - padding-left: 3.8em; -} - -pre[class*='language-'].line-numbers .line-numbers-rows { - left: 0; -} - -/* Plugin styles: Line Highlight */ -pre[class*='language-'][data-line] { - padding-top: 0; - padding-bottom: 0; - padding-left: 0; -} -pre[data-line] code { - position: relative; - padding-left: 4em; -} -pre .line-highlight { - margin-top: 0; -}