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
This commit is contained in:
David Kaltschmidt 2018-05-01 13:27:25 +02:00
parent 0d3f24ce54
commit eadaff6191
7 changed files with 345 additions and 294 deletions

View File

@ -4,7 +4,6 @@ import colors from 'app/core/utils/colors';
import TimeSeries from 'app/core/time_series2'; import TimeSeries from 'app/core/time_series2';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
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';
@ -16,9 +15,7 @@ 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 responseAlias = seriesData.target; const alias = 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];
@ -54,6 +51,7 @@ interface IExploreState {
latency: number; latency: number;
loading: any; loading: any;
queries: any; queries: any;
queryError: any;
range: any; range: any;
requestOptions: any; requestOptions: any;
showingGraph: boolean; showingGraph: boolean;
@ -76,6 +74,7 @@ export class Explore extends React.Component<any, IExploreState> {
latency: 0, latency: 0,
loading: false, loading: false,
queries: ensureQueries(queries), queries: ensureQueries(queries),
queryError: null,
range: range || { ...DEFAULT_RANGE }, range: range || { ...DEFAULT_RANGE },
requestOptions: null, requestOptions: null,
showingGraph: true, showingGraph: true,
@ -94,6 +93,10 @@ export class Explore extends React.Component<any, IExploreState> {
} }
} }
componentDidCatch(error) {
console.error(error);
}
handleAddQueryRow = index => { handleAddQueryRow = index => {
const { queries } = this.state; const { queries } = this.state;
const nextQueries = [ const nextQueries = [
@ -155,7 +158,7 @@ export class Explore extends React.Component<any, IExploreState> {
if (!hasQuery(queries)) { if (!hasQuery(queries)) {
return; return;
} }
this.setState({ latency: 0, loading: true, graphResult: null }); this.setState({ latency: 0, loading: true, graphResult: null, queryError: null });
const now = Date.now(); const now = Date.now();
const options = buildQueryOptions({ const options = buildQueryOptions({
format: 'time_series', format: 'time_series',
@ -169,9 +172,10 @@ export class Explore extends React.Component<any, IExploreState> {
const result = makeTimeSeriesList(res.data, options); const result = makeTimeSeriesList(res.data, options);
const latency = Date.now() - now; const latency = Date.now() - now;
this.setState({ latency, loading: false, graphResult: result, requestOptions: options }); this.setState({ latency, loading: false, graphResult: result, requestOptions: options });
} catch (error) { } catch (response) {
console.error(error); console.error(response);
this.setState({ loading: false, graphResult: error }); const queryError = response.data ? response.data.error : response;
this.setState({ loading: false, queryError });
} }
} }
@ -180,7 +184,7 @@ export class Explore extends React.Component<any, IExploreState> {
if (!hasQuery(queries)) { if (!hasQuery(queries)) {
return; return;
} }
this.setState({ latency: 0, loading: true, tableResult: null }); this.setState({ latency: 0, loading: true, queryError: null, tableResult: null });
const now = Date.now(); const now = Date.now();
const options = buildQueryOptions({ const options = buildQueryOptions({
format: 'table', format: 'table',
@ -194,9 +198,10 @@ export class Explore extends React.Component<any, IExploreState> {
const tableModel = res.data[0]; const tableModel = res.data[0];
const latency = Date.now() - now; const latency = Date.now() - now;
this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options }); this.setState({ latency, loading: false, tableResult: tableModel, requestOptions: options });
} catch (error) { } catch (response) {
console.error(error); console.error(response);
this.setState({ loading: false, tableResult: null }); const queryError = response.data ? response.data.error : response;
this.setState({ loading: false, queryError });
} }
} }
@ -214,6 +219,7 @@ export class Explore extends React.Component<any, IExploreState> {
latency, latency,
loading, loading,
queries, queries,
queryError,
range, range,
requestOptions, requestOptions,
showingGraph, showingGraph,
@ -221,55 +227,63 @@ export class Explore extends React.Component<any, IExploreState> {
tableResult, tableResult,
} = this.state; } = this.state;
const showingBoth = showingGraph && showingTable; const showingBoth = showingGraph && showingTable;
const graphHeight = showingBoth ? '200px' : null; const graphHeight = showingBoth ? '200px' : '400px';
const graphButtonClassName = showingBoth || showingGraph ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
const tableButtonClassName = showingBoth || showingTable ? 'btn m-r-1' : 'btn btn-inverse m-r-1'; const tableButtonActive = showingBoth || showingTable ? 'active' : '';
return ( return (
<div className="explore"> <div className="explore">
<div className="page-body page-full"> <div className="navbar">
<h2 className="page-sub-heading">Explore</h2> <div>
{datasourceLoading ? <div>Loading datasource...</div> : null} <a className="navbar-page-btn">
<i className="fa fa-rocket" />
{datasourceError ? <div title={datasourceError}>Error connecting to datasource.</div> : null} Explore
</a>
{datasource ? ( </div>
<div className="m-r-3"> <div className="navbar__spacer" />
<div className="nav m-b-1 navbar"> <div className="navbar-buttons">
<div className="navbar-buttons"> <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
<button className={graphButtonClassName} onClick={this.handleClickGraphButton}> Graph
Graph </button>
</button> <button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
<button className={tableButtonClassName} onClick={this.handleClickTableButton}> Table
Table </button>
</button> </div>
</div> <TimePicker range={range} onChangeTime={this.handleChangeTime} />
<div className="navbar__spacer" /> <div className="navbar-buttons relative">
<TimePicker range={range} onChangeTime={this.handleChangeTime} /> <button className="btn navbar-button--primary" onClick={this.handleSubmit}>
<div className="navbar-buttons"> Run Query <i className="fa fa-level-down run-icon" />
<button type="submit" className="btn btn-primary" onClick={this.handleSubmit}> </button>
<i className="fa fa-return" /> Run Query {loading || latency ? <ElapsedTime time={latency} className="text-info" /> : null}
</button> </div>
</div>
{loading || latency ? <ElapsedTime time={latency} className="" /> : null}
</div>
<QueryRows
queries={queries}
request={this.request}
onAddQueryRow={this.handleAddQueryRow}
onChangeQuery={this.handleChangeQuery}
onExecuteQuery={this.handleSubmit}
onRemoveQueryRow={this.handleRemoveQueryRow}
/>
<main className="m-t-2">
{showingGraph ? (
<Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />
) : null}
{showingGraph ? <Legend data={graphResult} /> : null}
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
</main>
</div>
) : null}
</div> </div>
{datasourceLoading ? <div className="explore-container">Loading datasource...</div> : null}
{datasourceError ? (
<div className="explore-container" title={datasourceError}>
Error connecting to datasource.
</div>
) : null}
{datasource ? (
<div className="explore-container">
<QueryRows
queries={queries}
request={this.request}
onAddQueryRow={this.handleAddQueryRow}
onChangeQuery={this.handleChangeQuery}
onExecuteQuery={this.handleSubmit}
onRemoveQueryRow={this.handleRemoveQueryRow}
/>
{queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
<main className="m-t-2">
{showingGraph ? (
<Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />
) : null}
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
</main>
</div>
) : null}
</div> </div>
); );
} }

View File

@ -2,11 +2,12 @@ import $ from 'jquery';
import React, { Component } from 'react'; import React, { Component } from 'react';
import moment from 'moment'; import moment from 'moment';
import 'vendor/flot/jquery.flot';
import 'vendor/flot/jquery.flot.time';
import * as dateMath from 'app/core/utils/datemath'; 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 Legend from './Legend';
import 'vendor/flot/jquery.flot.time';
// Copied from graph.ts // Copied from graph.ts
function time_format(ticks, min, max) { function time_format(ticks, min, max) {
@ -86,6 +87,7 @@ class Graph extends Component<any, any> {
return; return;
} }
const series = data.map((ts: TimeSeries) => ({ const series = data.map((ts: TimeSeries) => ({
color: ts.color,
label: ts.label, label: ts.label,
data: ts.getFlotPairs('null'), data: ts.getFlotPairs('null'),
})); }));
@ -120,12 +122,13 @@ class Graph extends Component<any, any> {
} }
render() { render() {
const style = { const { data, height } = this.props;
height: this.props.height || '400px', return (
width: this.props.width || '100%', <div className="panel-container">
}; <div id={this.props.id} className="explore-graph" style={{ height }} />
<Legend data={data} />
return <div id={this.props.id} style={style} />; </div>
);
} }
} }

View File

@ -50,7 +50,7 @@ class Portal extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
this.node = document.createElement('div'); 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); document.body.appendChild(this.node);
} }

View File

@ -48,10 +48,10 @@ class QueryRow extends PureComponent<any, any> {
return ( return (
<div className="query-row"> <div className="query-row">
<div className="query-row-tools"> <div className="query-row-tools">
<button className="btn btn-small btn-inverse" onClick={this.handleClickAddButton}> <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickAddButton}>
<i className="fa fa-plus" /> <i className="fa fa-plus" />
</button> </button>
<button className="btn btn-small btn-inverse" onClick={this.handleClickRemoveButton}> <button className="btn navbar-button navbar-button--tight" onClick={this.handleClickRemoveButton}>
<i className="fa fa-minus" /> <i className="fa fa-minus" />
</button> </button>
</div> </div>
@ -60,6 +60,7 @@ class QueryRow extends PureComponent<any, any> {
initialQuery={edited ? null : query} initialQuery={edited ? null : query}
onPressEnter={this.handlePressEnter} onPressEnter={this.handlePressEnter}
onQueryChange={this.handleChangeQuery} onQueryChange={this.handleChangeQuery}
placeholder="Enter a PromQL query"
request={request} request={request}
/> />
</div> </div>

View File

@ -164,6 +164,7 @@ export class PrometheusDatasource {
legendFormat: activeTargets[index].legendFormat, legendFormat: activeTargets[index].legendFormat,
start: start, start: start,
end: end, end: end,
query: queries[index].expr,
responseListLength: responseList.length, responseListLength: responseList.length,
responseIndex: index, responseIndex: index,
refId: activeTargets[index].refId, refId: activeTargets[index].refId,

View File

@ -123,11 +123,16 @@ export class ResultTransformer {
} }
createMetricLabel(labelData, options) { createMetricLabel(labelData, options) {
let label = '';
if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) { if (_.isUndefined(options) || _.isEmpty(options.legendFormat)) {
return this.getOriginalMetricName(labelData); label = this.getOriginalMetricName(labelData);
} else {
label = this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData);
} }
if (!label || label === '{}') {
return this.renderTemplate(this.templateSrv.replace(options.legendFormat), labelData) || '{}'; label = options.query;
}
return label;
} }
renderTemplate(aliasPattern, aliasData) { renderTemplate(aliasPattern, aliasData) {

View File

@ -1,14 +1,35 @@
.explore { .explore {
.navbar { .explore-container {
padding-left: 0; padding: 2rem;
padding-right: 0; }
.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 { .elapsed-time {
position: absolute; position: absolute;
right: -2.4rem; left: 0;
top: 1.2rem; right: 0;
top: 3.5rem;
text-align: center;
font-size: 0.8rem;
} }
.graph-legend { .graph-legend {
flex-wrap: wrap; flex-wrap: wrap;
} }
@ -16,10 +37,19 @@
.timepicker { .timepicker {
display: flex; display: flex;
} }
.run-icon {
margin-left: 0.5em;
transform: rotate(90deg);
}
.relative {
position: relative;
}
} }
.query-row { .query-row {
position: relative; display: flex;
& + & { & + & {
margin-top: 0.5rem; margin-top: 0.5rem;
@ -27,12 +57,7 @@
} }
.query-row-tools { .query-row-tools {
position: absolute; width: 4rem;
left: -4rem;
top: 0.33rem;
> * {
margin-right: 0.25rem;
}
} }
.query-field { .query-field {
@ -49,14 +74,14 @@
cursor: text; cursor: text;
line-height: 1.5; line-height: 1.5;
color: rgba(0, 0, 0, 0.65); color: rgba(0, 0, 0, 0.65);
background-color: #fff; background-color: $panel-bg;
background-image: none; background-image: none;
border: 1px solid lightgray; border: $panel-border;
border-radius: 3px; border-radius: 3px;
transition: all 0.3s; transition: all 0.3s;
} }
.explore { .explore-typeahead {
.typeahead { .typeahead {
position: absolute; position: absolute;
z-index: auto; z-index: auto;
@ -117,221 +142,223 @@
* @author Tim Shedor * @author Tim Shedor
*/ */
code[class*='language-'], .explore {
pre[class*='language-'] { code[class*='language-'],
color: black; pre[class*='language-'] {
background: none; color: black;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; background: none;
text-align: left; font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
white-space: pre; text-align: left;
word-spacing: normal; white-space: pre;
word-break: normal; word-spacing: normal;
word-wrap: normal; word-break: normal;
line-height: 1.5; word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4; -moz-tab-size: 4;
-o-tab-size: 4; -o-tab-size: 4;
tab-size: 4; tab-size: 4;
-webkit-hyphens: none; -webkit-hyphens: none;
-moz-hyphens: none; -moz-hyphens: none;
-ms-hyphens: none; -ms-hyphens: none;
hyphens: none; hyphens: none;
} }
/* Code blocks */ /* Code blocks */
pre[class*='language-'] { pre[class*='language-'] {
position: relative; position: relative;
margin: 0.5em 0; margin: 0.5em 0;
overflow: visible; overflow: visible;
padding: 0; padding: 0;
} }
pre[class*='language-'] > code { pre[class*='language-'] > code {
position: relative; position: relative;
border-left: 10px solid #358ccb; border-left: 10px solid #358ccb;
box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf; box-shadow: -1px 0px 0px 0px #358ccb, 0px 0px 0px 1px #dfdfdf;
background-color: #fdfdfd; background-color: #fdfdfd;
background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%);
background-size: 3em 3em; background-size: 3em 3em;
background-origin: content-box; background-origin: content-box;
background-attachment: local; background-attachment: local;
} }
code[class*='language'] { code[class*='language'] {
max-height: inherit; max-height: inherit;
height: inherit; height: inherit;
padding: 0 1em; padding: 0 1em;
display: block; display: block;
overflow: auto; overflow: auto;
} }
/* Margin bottom to accomodate shadow */ /* Margin bottom to accomodate shadow */
:not(pre) > code[class*='language-'], :not(pre) > code[class*='language-'],
pre[class*='language-'] { pre[class*='language-'] {
background-color: #fdfdfd; background-color: #fdfdfd;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
-moz-box-sizing: border-box; -moz-box-sizing: border-box;
box-sizing: border-box; box-sizing: border-box;
margin-bottom: 1em; margin-bottom: 1em;
} }
/* Inline code */ /* Inline code */
:not(pre) > code[class*='language-'] { :not(pre) > code[class*='language-'] {
position: relative; position: relative;
padding: 0.2em; padding: 0.2em;
border-radius: 0.3em; border-radius: 0.3em;
color: #c92c2c; color: #c92c2c;
border: 1px solid rgba(0, 0, 0, 0.1); border: 1px solid rgba(0, 0, 0, 0.1);
display: inline; display: inline;
white-space: normal; 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-']:before,
pre[class*='language-']:after { pre[class*='language-']:after {
bottom: 14px; content: '';
box-shadow: none; 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;
}