From 0d3f24ce54782c06ce0ba534786eb0e2e90ac2e6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 30 Apr 2018 17:25:25 +0200 Subject: [PATCH 1/5] 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 { From eadaff619157b12ba697b9f7cf34aff7b83823b9 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 1 May 2018 13:27:25 +0200 Subject: [PATCH 2/5] Explore: Design integration * style header like other grafana components * use panel container for graph and same styles for query field * fix typeahead CSS selector (was created outside of .explore) * use navbar buttons for +/- of rows * moved elapsed time under run query button * fix JS error on multiple timeseries being returned * fix color for graph lines * show prometheus query errors --- public/app/containers/Explore/Explore.tsx | 130 ++--- public/app/containers/Explore/Graph.tsx | 19 +- public/app/containers/Explore/QueryField.tsx | 2 +- public/app/containers/Explore/QueryRows.tsx | 5 +- .../datasource/prometheus/datasource.ts | 1 + .../prometheus/result_transformer.ts | 11 +- public/sass/pages/_explore.scss | 471 +++++++++--------- 7 files changed, 345 insertions(+), 294 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 66500353812..cf8cb41a593 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -4,7 +4,6 @@ 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'; @@ -16,9 +15,7 @@ import { decodePathComponent } from 'app/core/utils/location_util'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; - const responseAlias = seriesData.target; - const query = options.targets[index].expr; - const alias = responseAlias && responseAlias !== '{}' ? responseAlias : query; + const alias = seriesData.target; const colorIndex = index % colors.length; const color = colors[colorIndex]; @@ -54,6 +51,7 @@ interface IExploreState { latency: number; loading: any; queries: any; + queryError: any; range: any; requestOptions: any; showingGraph: boolean; @@ -76,6 +74,7 @@ export class Explore extends React.Component { latency: 0, loading: false, queries: ensureQueries(queries), + queryError: null, range: range || { ...DEFAULT_RANGE }, requestOptions: null, showingGraph: true, @@ -94,6 +93,10 @@ export class Explore extends React.Component { } } + componentDidCatch(error) { + console.error(error); + } + handleAddQueryRow = index => { const { queries } = this.state; const nextQueries = [ @@ -155,7 +158,7 @@ export class Explore extends React.Component { 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', @@ -169,9 +172,10 @@ 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 }); } } @@ -180,7 +184,7 @@ export class Explore extends React.Component { 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', @@ -194,9 +198,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 }); } } @@ -214,6 +219,7 @@ export class Explore extends React.Component { latency, loading, queries, + queryError, range, requestOptions, showingGraph, @@ -221,55 +227,63 @@ export class Explore extends React.Component { 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' : ''; return (
-
-

Explore

- {datasourceLoading ?
Loading datasource...
: null} - - {datasourceError ?
Error connecting to datasource.
: null} - - {datasource ? ( -
-
-
- - -
-
- -
- -
- {loading || latency ? : null} -
- -
- {showingGraph ? ( - - ) : null} - {showingGraph ? : null} - {showingTable ? : null} - - - ) : 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 b8bda8696bc..d797a579512 100644 --- a/public/app/containers/Explore/Graph.tsx +++ b/public/app/containers/Explore/Graph.tsx @@ -2,11 +2,12 @@ import $ from 'jquery'; import React, { Component } from 'react'; 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 'vendor/flot/jquery.flot'; -import 'vendor/flot/jquery.flot.time'; +import Legend from './Legend'; // Copied from graph.ts function time_format(ticks, min, max) { @@ -86,6 +87,7 @@ class Graph extends Component { return; } const series = data.map((ts: TimeSeries) => ({ + color: ts.color, label: ts.label, data: ts.getFlotPairs('null'), })); @@ -120,12 +122,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/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index a52f3aefa2e..7470885177b 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/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 200af40341e..541477877bc 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,14 +1,35 @@ .explore { - .navbar { - padding-left: 0; - padding-right: 0; + .explore-container { + padding: 2rem; + } + + .explore-graph { + width: 100%; + height: 100%; + } + + .panel-container { + padding: 10px 10px 5px 10px; + } + + .navbar-page-btn .fa { + position: relative; + top: -1px; + font-size: 19px; + line-height: 8px; + opacity: 0.75; + margin-right: 8px; } .elapsed-time { position: absolute; - right: -2.4rem; - top: 1.2rem; + left: 0; + right: 0; + top: 3.5rem; + text-align: center; + font-size: 0.8rem; } + .graph-legend { flex-wrap: wrap; } @@ -16,10 +37,19 @@ .timepicker { display: flex; } + + .run-icon { + margin-left: 0.5em; + transform: rotate(90deg); + } + + .relative { + position: relative; + } } .query-row { - position: relative; + display: flex; & + & { margin-top: 0.5rem; @@ -27,12 +57,7 @@ } .query-row-tools { - position: absolute; - left: -4rem; - top: 0.33rem; - > * { - margin-right: 0.25rem; - } + width: 4rem; } .query-field { @@ -49,14 +74,14 @@ cursor: text; line-height: 1.5; color: rgba(0, 0, 0, 0.65); - background-color: #fff; + background-color: $panel-bg; background-image: none; - border: 1px solid lightgray; + border: $panel-border; border-radius: 3px; transition: all 0.3s; } -.explore { +.explore-typeahead { .typeahead { position: absolute; z-index: auto; @@ -117,221 +142,223 @@ * @author Tim Shedor */ -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; +.explore { + 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; - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; -} + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + } -/* 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; -} + /* 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; + } -code[class*='language'] { - max-height: inherit; - height: inherit; - padding: 0 1em; - display: block; - overflow: auto; -} + code[class*='language'] { + max-height: inherit; + height: inherit; + padding: 0 1em; + display: block; + overflow: auto; + } -/* 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; -} + /* 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; + } -/* 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; -} + /* 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; + } -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); -} - -: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.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; + 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); + } + + :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.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; + } + } + + /* 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; } } - -/* 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; -} From 23c9da6162f268e1134023f2aba3d1b52c5310de Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 May 2018 15:37:44 +0200 Subject: [PATCH 3/5] Fixed custom dates for react timepicker * added jest tests for timepicker component --- .../containers/Explore/TimePicker.jest.tsx | 74 ++++++++++++++++ public/app/containers/Explore/TimePicker.tsx | 85 +++++++++++++++---- public/sass/pages/_explore.scss | 4 + 3 files changed, 147 insertions(+), 16 deletions(-) create mode 100644 public/app/containers/Explore/TimePicker.jest.tsx 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 index b67cd532019..3ae4ea4a83c 100644 --- a/public/app/containers/Explore/TimePicker.tsx +++ b/public/app/containers/Explore/TimePicker.tsx @@ -4,22 +4,43 @@ 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: props.range ? props.range.from : DEFAULT_RANGE.from, - isOpen: false, - isUtc: false, - rangeString: rangeUtil.describeTimeRange(props.range || DEFAULT_RANGE), + fromRaw: parseTime(fromRaw, props.isUtc, true), + isOpen: props.isOpen, + isUtc: props.isUtc, + rangeString: rangeUtil.describeTimeRange(range), refreshInterval: '', - toRaw: props.range ? props.range.to : DEFAULT_RANGE.to, + toRaw: parseTime(toRaw, props.isUtc, true), }; } @@ -49,14 +70,15 @@ export default class TimePicker extends PureComponent { } const rangeString = rangeUtil.describeTimeRange(range); - to = moment.utc(to); - from = moment.utc(from); + // No need to convert to UTC again + to = moment(to); + from = moment(from); this.setState( { rangeString, - fromRaw: from, - toRaw: to, + fromRaw: from.format(DATE_FORMAT), + toRaw: to.format(DATE_FORMAT), }, () => { onChangeTime({ to, from }); @@ -76,6 +98,27 @@ export default class TimePicker extends PureComponent { }); }; + 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 => ({ @@ -118,7 +161,7 @@ export default class TimePicker extends PureComponent { const timeOptions = this.getTimeOptions(); return (
-
+

Custom range

@@ -126,7 +169,7 @@ export default class TimePicker extends PureComponent {
@@ -136,7 +179,12 @@ export default class TimePicker extends PureComponent {
- +
@@ -146,7 +194,12 @@ export default class TimePicker extends PureComponent {
*/} - +
+ +
+

Quick ranges

@@ -172,16 +225,16 @@ export default class TimePicker extends PureComponent { return (
- -
diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 541477877bc..47fb3857225 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -36,6 +36,10 @@ .timepicker { display: flex; + + &-rangestring { + margin-left: 0.5em; + } } .run-icon { From f5e351af8b7ebd72800502b65aa7e6f741c08106 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 15 May 2018 17:07:38 +0200 Subject: [PATCH 4/5] Explore split view * button to bring a up a duplicate explore area to compare * side by side rendering of two explore components * right component has close button * left component has page title --- public/app/containers/Explore/Explore.tsx | 54 +++++++++++++++++++---- public/app/containers/Explore/Graph.tsx | 1 + public/app/containers/Explore/Wrapper.tsx | 33 ++++++++++++++ public/app/routes/routes.ts | 3 +- public/sass/pages/_explore.scss | 48 ++++++++++++++++---- 5 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 public/app/containers/Explore/Wrapper.tsx diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index cf8cb41a593..deebe84f2c8 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -80,6 +80,7 @@ export class Explore extends React.Component { showingGraph: true, showingTable: true, tableResult: null, + ...props.initialState, }; } @@ -126,10 +127,24 @@ export class Explore extends React.Component { 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 })); }; @@ -211,6 +226,7 @@ export class Explore extends React.Component { }; render() { + const { position, split } = this.props; const { datasource, datasourceError, @@ -230,16 +246,32 @@ export class Explore extends React.Component { const graphHeight = showingBoth ? '200px' : '400px'; const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; const tableButtonActive = showingBoth || showingTable ? 'active' : ''; + const exploreClass = split ? 'explore explore-split' : 'explore'; return ( -
+
- + {position === 'left' ? ( + + ) : ( +
+ +
+ )}
+ {position === 'left' && !split ? ( +
+ +
+ ) : null}
: null} diff --git a/public/app/containers/Explore/Graph.tsx b/public/app/containers/Explore/Graph.tsx index d797a579512..a43ddfb2aa5 100644 --- a/public/app/containers/Explore/Graph.tsx +++ b/public/app/containers/Explore/Graph.tsx @@ -75,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(); 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/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/pages/_explore.scss b/public/sass/pages/_explore.scss index 47fb3857225..b618b8eb5b7 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -1,8 +1,22 @@ .explore { - .explore-container { + width: 100%; + + &-container { padding: 2rem; } + &-wrapper { + display: flex; + + > .explore-split { + width: 50%; + } + } + + .explore-first-button { + margin-left: 15px; + } + .explore-graph { width: 100%; height: 100%; @@ -12,13 +26,27 @@ padding: 10px 10px 5px 10px; } - .navbar-page-btn .fa { - position: relative; - top: -1px; - font-size: 19px; - line-height: 8px; - opacity: 0.75; - margin-right: 8px; + .navbar { + flex-wrap: wrap; + height: auto; + } + + .navbar-page-btn { + margin-right: 1rem; + + .fa { + position: relative; + top: -1px; + font-size: 19px; + line-height: 8px; + opacity: 0.75; + margin-right: 8px; + } + } + + .navbar-button.active { + color: #0083b3; + background-color: white; } .elapsed-time { @@ -52,6 +80,10 @@ } } +.explore + .explore { + border-left: 1px dotted #aaa; +} + .query-row { display: flex; From 448b1cbc155f611083e1b99e8ac7fe2816d4893e Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 25 May 2018 12:51:27 +0200 Subject: [PATCH 5/5] Integrated dark theme for explore UI --- public/sass/_variables.dark.scss | 12 ++ public/sass/_variables.light.scss | 12 ++ public/sass/pages/_explore.scss | 236 +++++------------------------- 3 files changed, 63 insertions(+), 197 deletions(-) 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 b618b8eb5b7..7dacccf6a87 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -13,19 +13,18 @@ } } + // Push split button a bit .explore-first-button { margin-left: 15px; } - .explore-graph { - width: 100%; - height: 100%; - } - + // Graph panel needs a bit extra padding at top .panel-container { - padding: 10px 10px 5px 10px; + padding: $panel-padding; + padding-top: 10px; } + // Make sure wrap buttons around on small screens .navbar { flex-wrap: wrap; height: auto; @@ -34,19 +33,18 @@ .navbar-page-btn { margin-right: 1rem; + // Explore icon in header .fa { - position: relative; - top: -1px; - font-size: 19px; - line-height: 8px; + font-size: 100%; opacity: 0.75; - margin-right: 8px; + margin-right: 0.5em; } } + // Toggle mode .navbar-button.active { - color: #0083b3; - background-color: white; + color: $btn-active-text-color; + background-color: $btn-active-bg; } .elapsed-time { @@ -81,7 +79,7 @@ } .explore + .explore { - border-left: 1px dotted #aaa; + border-left: 1px dotted $table-border; } .query-row { @@ -97,8 +95,8 @@ } .query-field { - font-size: 14px; - font-family: Consolas, Menlo, Courier, monospace; + font-size: $font-size-root; + font-family: $font-family-monospace; height: auto; } @@ -108,12 +106,12 @@ padding: 6px 7px 4px; width: 100%; cursor: text; - line-height: 1.5; - color: rgba(0, 0, 0, 0.65); + line-height: $line-height-base; + color: $text-color-weak; background-color: $panel-bg; background-image: none; border: $panel-border; - border-radius: 3px; + border-radius: $border-radius; transition: all 0.3s; } @@ -124,38 +122,36 @@ 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; @@ -165,129 +161,24 @@ } .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 { - 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; - - -moz-tab-size: 4; - -o-tab-size: 4; - tab-size: 4; - - -webkit-hyphens: none; - -moz-hyphens: none; - -ms-hyphens: none; - hyphens: none; - } - - /* 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; - } - - code[class*='language'] { - max-height: inherit; - height: inherit; - padding: 0 1em; - display: block; - overflow: auto; - } - - /* 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; - } - - /* 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; - } - - 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); - } - - :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.comment, .token.block-comment, .token.prolog, .token.doctype, .token.cdata { - color: #7d8b99; + color: $text-color-weak; } .token.punctuation { - color: #5f6364; + color: $text-color-weak; } .token.property, @@ -298,7 +189,7 @@ .token.constant, .token.symbol, .token.deleted { - color: #c92c2c; + color: $query-red; } .token.selector, @@ -308,33 +199,26 @@ .token.function, .token.builtin, .token.inserted { - color: #2f9c0a; + color: $query-green; } .token.operator, .token.entity, .token.url, .token.variable { - color: #a67f59; - background: rgba(255, 255, 255, 0.5); + color: $query-purple; } .token.atrule, .token.attr-value, .token.keyword, .token.class-name { - color: #1990b8; + color: $query-blue; } .token.regex, .token.important { - color: #e90; - } - - .language-css .token.string, - .style .token.string { - color: #a67f59; - background: rgba(255, 255, 255, 0.5); + color: $query-orange; } .token.important { @@ -355,46 +239,4 @@ .namespace { opacity: 0.7; } - - @media screen and (max-width: 767px) { - pre[class*='language-']:before, - pre[class*='language-']:after { - bottom: 14px; - box-shadow: none; - } - } - - /* 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; - } }