Explore: add support for multiple queries

* adds +/- buttons to query rows in the Explore section
* on Run Query all query expressions are submitted
* `generateQueryKey` and `ensureQueries` are helpers to ensure each
 query field has a unique key for react.
This commit is contained in:
David Kaltschmidt 2018-04-27 15:42:35 +02:00
parent 25d3ec5bbf
commit 949e3d29e8
4 changed files with 175 additions and 48 deletions

View File

@ -5,29 +5,11 @@ import TimeSeries from 'app/core/time_series2';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
import Legend from './Legend'; import Legend from './Legend';
import QueryField from './QueryField'; import QueryRows from './QueryRows';
import Graph from './Graph'; import Graph from './Graph';
import Table from './Table'; import Table from './Table';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query';
function buildQueryOptions({ format, interval, instant, now, query }) {
const to = now;
const from = to - 1000 * 60 * 60 * 3;
return {
interval,
range: {
from,
to,
},
targets: [
{
expr: query,
format,
instant,
},
],
};
}
function makeTimeSeriesList(dataList, options) { function makeTimeSeriesList(dataList, options) {
return dataList.map((seriesData, index) => { return dataList.map((seriesData, index) => {
@ -63,6 +45,7 @@ interface IExploreState {
graphResult: any; graphResult: any;
latency: number; latency: number;
loading: any; loading: any;
queries: any;
requestOptions: any; requestOptions: any;
showingGraph: boolean; showingGraph: boolean;
showingTable: boolean; showingTable: boolean;
@ -72,7 +55,6 @@ interface IExploreState {
// @observer // @observer
export class Explore extends React.Component<any, IExploreState> { export class Explore extends React.Component<any, IExploreState> {
datasourceSrv: DatasourceSrv; datasourceSrv: DatasourceSrv;
query: string;
constructor(props) { constructor(props) {
super(props); super(props);
@ -83,6 +65,7 @@ export class Explore extends React.Component<any, IExploreState> {
graphResult: null, graphResult: null,
latency: 0, latency: 0,
loading: false, loading: false,
queries: ensureQueries(),
requestOptions: null, requestOptions: null,
showingGraph: true, showingGraph: true,
showingTable: true, showingTable: true,
@ -100,6 +83,27 @@ export class Explore extends React.Component<any, IExploreState> {
} }
} }
handleAddQueryRow = index => {
const { queries } = this.state;
const nextQueries = [
...queries.slice(0, index + 1),
{ query: '', key: generateQueryKey() },
...queries.slice(index + 1),
];
this.setState({ queries: nextQueries });
};
handleChangeQuery = (query, index) => {
const { queries } = this.state;
const nextQuery = {
...queries[index],
query,
};
const nextQueries = [...queries];
nextQueries[index] = nextQuery;
this.setState({ queries: nextQueries });
};
handleClickGraphButton = () => { handleClickGraphButton = () => {
this.setState(state => ({ showingGraph: !state.showingGraph })); this.setState(state => ({ showingGraph: !state.showingGraph }));
}; };
@ -108,12 +112,13 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState(state => ({ showingTable: !state.showingTable })); this.setState(state => ({ showingTable: !state.showingTable }));
}; };
handleRequestError({ error }) { handleRemoveQueryRow = index => {
console.error(error); const { queries } = this.state;
} if (queries.length <= 1) {
return;
handleQueryChange = query => { }
this.query = query; const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)];
this.setState({ queries: nextQueries }, () => this.handleSubmit());
}; };
handleSubmit = () => { handleSubmit = () => {
@ -127,9 +132,8 @@ export class Explore extends React.Component<any, IExploreState> {
}; };
async runGraphQuery() { async runGraphQuery() {
const { query } = this; const { datasource, queries } = this.state;
const { datasource } = this.state; if (!hasQuery(queries)) {
if (!query) {
return; return;
} }
this.setState({ latency: 0, loading: true, graphResult: null }); this.setState({ latency: 0, loading: true, graphResult: null });
@ -139,7 +143,7 @@ export class Explore extends React.Component<any, IExploreState> {
interval: datasource.interval, interval: datasource.interval,
instant: false, instant: false,
now, now,
query, queries: queries.map(q => q.query),
}); });
try { try {
const res = await datasource.query(options); const res = await datasource.query(options);
@ -153,14 +157,19 @@ export class Explore extends React.Component<any, IExploreState> {
} }
async runTableQuery() { async runTableQuery() {
const { query } = this; const { datasource, queries } = this.state;
const { datasource } = this.state; if (!hasQuery(queries)) {
if (!query) {
return; return;
} }
this.setState({ latency: 0, loading: true, tableResult: null }); this.setState({ latency: 0, loading: true, tableResult: null });
const now = Date.now(); const now = Date.now();
const options = buildQueryOptions({ format: 'table', interval: datasource.interval, instant: true, now, query }); const options = buildQueryOptions({
format: 'table',
interval: datasource.interval,
instant: true,
now,
queries: queries.map(q => q.query),
});
try { try {
const res = await datasource.query(options); const res = await datasource.query(options);
const tableModel = res.data[0]; const tableModel = res.data[0];
@ -182,10 +191,11 @@ export class Explore extends React.Component<any, IExploreState> {
datasource, datasource,
datasourceError, datasourceError,
datasourceLoading, datasourceLoading,
graphResult,
latency, latency,
loading, loading,
queries,
requestOptions, requestOptions,
graphResult,
showingGraph, showingGraph,
showingTable, showingTable,
tableResult, tableResult,
@ -205,7 +215,8 @@ export class Explore extends React.Component<any, IExploreState> {
{datasource ? ( {datasource ? (
<div className="m-r-3"> <div className="m-r-3">
<div className="nav m-b-1"> <div className="nav m-b-1">
<div className="pull-right" style={{ paddingRight: '6rem' }}> <div className="pull-right">
{loading || latency ? <ElapsedTime time={latency} className="" /> : null}
<button type="submit" className="m-l-1 btn btn-primary" onClick={this.handleSubmit}> <button type="submit" className="m-l-1 btn btn-primary" onClick={this.handleSubmit}>
<i className="fa fa-return" /> Run Query <i className="fa fa-return" /> Run Query
</button> </button>
@ -219,15 +230,14 @@ export class Explore extends React.Component<any, IExploreState> {
</button> </button>
</div> </div>
</div> </div>
<div className="query-field-wrapper"> <QueryRows
<QueryField queries={queries}
request={this.request} request={this.request}
onPressEnter={this.handleSubmit} onAddQueryRow={this.handleAddQueryRow}
onQueryChange={this.handleQueryChange} onChangeQuery={this.handleChangeQuery}
onRequestError={this.handleRequestError} onExecuteQuery={this.handleSubmit}
/> onRemoveQueryRow={this.handleRemoveQueryRow}
</div> />
{loading || latency ? <ElapsedTime time={latency} className="m-l-1" /> : null}
<main className="m-t-2"> <main className="m-t-2">
{showingGraph ? ( {showingGraph ? (
<Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} /> <Graph data={graphResult} id="explore-1" options={requestOptions} height={graphHeight} />

View File

@ -0,0 +1,69 @@
import React, { PureComponent } from 'react';
import QueryField from './QueryField';
class QueryRow extends PureComponent<any, any> {
constructor(props) {
super(props);
this.state = {
query: '',
};
}
handleChangeQuery = value => {
const { index, onChangeQuery } = this.props;
this.setState({ query: value });
if (onChangeQuery) {
onChangeQuery(value, index);
}
};
handleClickAddButton = () => {
const { index, onAddQueryRow } = this.props;
if (onAddQueryRow) {
onAddQueryRow(index);
}
};
handleClickRemoveButton = () => {
const { index, onRemoveQueryRow } = this.props;
if (onRemoveQueryRow) {
onRemoveQueryRow(index);
}
};
handlePressEnter = () => {
const { onExecuteQuery } = this.props;
if (onExecuteQuery) {
onExecuteQuery();
}
};
render() {
const { request } = this.props;
return (
<div className="query-row">
<div className="query-row-tools">
<button className="btn btn-small btn-inverse" onClick={this.handleClickAddButton}>
<i className="fa fa-plus" />
</button>
<button className="btn btn-small btn-inverse" onClick={this.handleClickRemoveButton}>
<i className="fa fa-minus" />
</button>
</div>
<div className="query-field-wrapper">
<QueryField onPressEnter={this.handlePressEnter} onQueryChange={this.handleChangeQuery} request={request} />
</div>
</div>
);
}
}
export default class QueryRows extends PureComponent<any, any> {
render() {
const { className = '', queries, ...handlers } = this.props;
return (
<div className={className}>{queries.map((q, index) => <QueryRow key={q.key} index={index} {...handlers} />)}</div>
);
}
}

View File

@ -0,0 +1,31 @@
export function buildQueryOptions({ format, interval, instant, now, queries }) {
const to = now;
const from = to - 1000 * 60 * 60 * 3;
return {
interval,
range: {
from,
to,
},
targets: queries.map(expr => ({
expr,
format,
instant,
})),
};
}
export function generateQueryKey(index = 0) {
return `Q-${Date.now()}-${Math.random()}-${index}`;
}
export function ensureQueries(queries?) {
if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0] === 'string') {
return queries.map((query, i) => ({ key: generateQueryKey(i), query }));
}
return [{ key: generateQueryKey(), query: '' }];
}
export function hasQuery(queries) {
return queries.some(q => q.query);
}

View File

@ -4,6 +4,23 @@
} }
} }
.query-row {
position: relative;
& + & {
margin-top: 0.5rem;
}
}
.query-row-tools {
position: absolute;
left: -4rem;
top: 0.33rem;
> * {
margin-right: 0.25rem;
}
}
.query-field { .query-field {
font-size: 14px; font-size: 14px;
font-family: Consolas, Menlo, Courier, monospace; font-family: Consolas, Menlo, Courier, monospace;
@ -14,14 +31,14 @@
position: relative; position: relative;
display: inline-block; display: inline-block;
padding: 6px 7px 4px; padding: 6px 7px 4px;
width: calc(100% - 6rem); width: 100%;
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: #fff;
background-image: none; background-image: none;
border: 1px solid lightgray; border: 1px solid lightgray;
border-radius: 4px; border-radius: 3px;
transition: all 0.3s; transition: all 0.3s;
} }