diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 55c1d088ccc..eae3f4d0a1f 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -5,29 +5,11 @@ import TimeSeries from 'app/core/time_series2'; import ElapsedTime from './ElapsedTime'; import Legend from './Legend'; -import QueryField from './QueryField'; +import QueryRows from './QueryRows'; import Graph from './Graph'; import Table from './Table'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; - -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, - }, - ], - }; -} +import { buildQueryOptions, ensureQueries, generateQueryKey, hasQuery } from './utils/query'; function makeTimeSeriesList(dataList, options) { return dataList.map((seriesData, index) => { @@ -63,6 +45,7 @@ interface IExploreState { graphResult: any; latency: number; loading: any; + queries: any; requestOptions: any; showingGraph: boolean; showingTable: boolean; @@ -72,7 +55,6 @@ interface IExploreState { // @observer export class Explore extends React.Component { datasourceSrv: DatasourceSrv; - query: string; constructor(props) { super(props); @@ -83,6 +65,7 @@ export class Explore extends React.Component { graphResult: null, latency: 0, loading: false, + queries: ensureQueries(), requestOptions: null, showingGraph: true, showingTable: true, @@ -100,6 +83,27 @@ export class Explore extends React.Component { } } + 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 = () => { this.setState(state => ({ showingGraph: !state.showingGraph })); }; @@ -108,12 +112,13 @@ export class Explore extends React.Component { this.setState(state => ({ showingTable: !state.showingTable })); }; - handleRequestError({ error }) { - console.error(error); - } - - handleQueryChange = query => { - this.query = query; + handleRemoveQueryRow = index => { + const { queries } = this.state; + if (queries.length <= 1) { + return; + } + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + this.setState({ queries: nextQueries }, () => this.handleSubmit()); }; handleSubmit = () => { @@ -127,9 +132,8 @@ export class Explore extends React.Component { }; async runGraphQuery() { - const { query } = this; - const { datasource } = this.state; - if (!query) { + const { datasource, queries } = this.state; + if (!hasQuery(queries)) { return; } this.setState({ latency: 0, loading: true, graphResult: null }); @@ -139,7 +143,7 @@ export class Explore extends React.Component { interval: datasource.interval, instant: false, now, - query, + queries: queries.map(q => q.query), }); try { const res = await datasource.query(options); @@ -153,14 +157,19 @@ export class Explore extends React.Component { } async runTableQuery() { - const { query } = this; - const { datasource } = this.state; - if (!query) { + const { datasource, queries } = this.state; + if (!hasQuery(queries)) { return; } this.setState({ latency: 0, loading: true, tableResult: null }); 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 { const res = await datasource.query(options); const tableModel = res.data[0]; @@ -182,10 +191,11 @@ export class Explore extends React.Component { datasource, datasourceError, datasourceLoading, + graphResult, latency, loading, + queries, requestOptions, - graphResult, showingGraph, showingTable, tableResult, @@ -205,7 +215,8 @@ export class Explore extends React.Component { {datasource ? (
-
+
+ {loading || latency ? : null} @@ -219,15 +230,14 @@ export class Explore extends React.Component {
-
- -
- {loading || latency ? : null} +
{showingGraph ? ( diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx new file mode 100644 index 00000000000..a0a9981368d --- /dev/null +++ b/public/app/containers/Explore/QueryRows.tsx @@ -0,0 +1,69 @@ +import React, { PureComponent } from 'react'; + +import QueryField from './QueryField'; + +class QueryRow extends PureComponent { + 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 ( +
+
+ + +
+
+ +
+
+ ); + } +} + +export default class QueryRows extends PureComponent { + render() { + const { className = '', queries, ...handlers } = this.props; + return ( +
{queries.map((q, index) => )}
+ ); + } +} diff --git a/public/app/containers/Explore/utils/query.ts b/public/app/containers/Explore/utils/query.ts new file mode 100644 index 00000000000..d51c7339944 --- /dev/null +++ b/public/app/containers/Explore/utils/query.ts @@ -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); +} diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 4bd0162563b..74a19c1d2c2 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -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 { font-size: 14px; font-family: Consolas, Menlo, Courier, monospace; @@ -14,14 +31,14 @@ position: relative; display: inline-block; padding: 6px 7px 4px; - width: calc(100% - 6rem); + width: 100%; cursor: text; line-height: 1.5; color: rgba(0, 0, 0, 0.65); background-color: #fff; background-image: none; border: 1px solid lightgray; - border-radius: 4px; + border-radius: 3px; transition: all 0.3s; }