From b3161bea5a8040906c86ba2b919998cfb388f255 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 21 Nov 2018 14:45:57 +0100 Subject: [PATCH] Explore: Introduce DataQuery interface for query handling - Queries in Explore have been string based - This PR introduces the use of the DataQuery type to denote all queries handled in Explore - Within Explore all handling of DataQueries is transparent - Modifying DataQueries is left to the datasource - Using `target` as variable names for DataQueries to be consistent with the rest of Grafana --- public/app/core/utils/explore.test.ts | 49 +-- public/app/core/utils/explore.ts | 107 +++++- public/app/features/explore/Explore.tsx | 345 ++++++++---------- public/app/features/explore/QueryField.tsx | 18 +- public/app/features/explore/QueryRows.tsx | 26 +- .../explore/QueryTransactionStatus.tsx | 4 +- public/app/features/explore/utils/query.ts | 16 - .../logging/components/LoggingCheatSheet.tsx | 5 +- .../logging/components/LoggingQueryField.tsx | 25 +- .../logging/components/LoggingStartPage.tsx | 2 +- .../plugins/datasource/logging/datasource.ts | 4 +- .../datasource/logging/language_provider.ts | 18 +- .../prometheus/components/PromCheatSheet.tsx | 5 +- .../prometheus/components/PromQueryField.tsx | 29 +- .../prometheus/components/PromStart.tsx | 2 +- .../datasource/prometheus/datasource.ts | 40 +- public/app/types/explore.ts | 27 +- 17 files changed, 386 insertions(+), 336 deletions(-) delete mode 100644 public/app/features/explore/utils/query.ts diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 4252730338d..aaa7e4d4281 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -10,7 +10,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { exploreDatasources: [], graphRange: DEFAULT_RANGE, history: [], - queries: [], + initialTargets: [], queryTransactions: [], range: DEFAULT_RANGE, showingGraph: true, @@ -26,17 +26,17 @@ describe('state functions', () => { it('returns default state on empty string', () => { expect(parseUrlState('')).toMatchObject({ datasource: null, - queries: [], + targets: [], range: DEFAULT_RANGE, }); }); it('returns a valid Explore state from URL parameter', () => { const paramValue = - '%7B"datasource":"Local","queries":%5B%7B"query":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D'; + '%7B"datasource":"Local","targets":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D'; expect(parseUrlState(paramValue)).toMatchObject({ datasource: 'Local', - queries: [{ query: 'metric' }], + targets: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -45,10 +45,10 @@ describe('state functions', () => { }); it('returns a valid Explore state from a compact URL parameter', () => { - const paramValue = '%5B"now-1h","now","Local","metric"%5D'; + const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D'; expect(parseUrlState(paramValue)).toMatchObject({ datasource: 'Local', - queries: [{ query: 'metric' }], + targets: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -66,18 +66,20 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - queries: [ + initialTargets: [ { - query: 'metric{test="a/b"}', + refId: '1', + expr: 'metric{test="a/b"}', }, { - query: 'super{foo="x/z"}', + refId: '2', + expr: 'super{foo="x/z"}', }, ], }; expect(serializeStateToUrlParam(state)).toBe( - '{"datasource":"foo","queries":[{"query":"metric{test=\\"a/b\\"}"},' + - '{"query":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' + '{"datasource":"foo","targets":[{"expr":"metric{test=\\"a/b\\"}"},' + + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' ); }); @@ -89,17 +91,19 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - queries: [ + initialTargets: [ { - query: 'metric{test="a/b"}', + refId: '1', + expr: 'metric{test="a/b"}', }, { - query: 'super{foo="x/z"}', + refId: '2', + expr: 'super{foo="x/z"}', }, ], }; expect(serializeStateToUrlParam(state, true)).toBe( - '["now-5h","now","foo","metric{test=\\"a/b\\"}","super{foo=\\"x/z\\"}"]' + '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' ); }); }); @@ -113,12 +117,14 @@ describe('state functions', () => { from: 'now - 5h', to: 'now', }, - queries: [ + initialTargets: [ { - query: 'metric{test="a/b"}', + refId: '1', + expr: 'metric{test="a/b"}', }, { - query: 'super{foo="x/z"}', + refId: '2', + expr: 'super{foo="x/z"}', }, ], }; @@ -126,14 +132,15 @@ describe('state functions', () => { const parsed = parseUrlState(serialized); // Account for datasource vs datasourceName - const { datasource, ...rest } = parsed; - const sameState = { + const { datasource, targets, ...rest } = parsed; + const resultState = { ...rest, datasource: DEFAULT_EXPLORE_STATE.datasource, datasourceName: datasource, + initialTargets: targets, }; - expect(state).toMatchObject(sameState); + expect(state).toMatchObject(resultState); }); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index ecd11a495ad..bb1d1c9bbf1 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -1,11 +1,20 @@ import { renderUrl } from 'app/core/utils/url'; -import { ExploreState, ExploreUrlState } from 'app/types/explore'; +import { ExploreState, ExploreUrlState, HistoryItem } from 'app/types/explore'; +import { DataQuery, RawTimeRange } from 'app/types/series'; + +import kbn from 'app/core/utils/kbn'; +import colors from 'app/core/utils/colors'; +import TimeSeries from 'app/core/time_series2'; +import { parse as parseDate } from 'app/core/utils/datemath'; +import store from 'app/core/store'; export const DEFAULT_RANGE = { from: 'now-6h', to: 'now', }; +const MAX_HISTORY_ITEMS = 100; + /** * Returns an Explore-URL that contains a panel's queries and the dashboard time range. * @@ -70,30 +79,106 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - const queries = parsed.slice(3).map(query => ({ query })); - return { datasource, queries, range }; + const targets = parsed.slice(3); + return { datasource, targets, range }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, queries: [], range: DEFAULT_RANGE }; + return { datasource: null, targets: [], range: DEFAULT_RANGE }; } export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { datasource: state.datasourceName, - queries: state.queries.map(q => ({ query: q.query })), + targets: state.initialTargets.map(({ key, refId, ...rest }) => rest), range: state.range, }; if (compact) { - return JSON.stringify([ - urlState.range.from, - urlState.range.to, - urlState.datasource, - ...urlState.queries.map(q => q.query), - ]); + return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.targets]); } return JSON.stringify(urlState); } + +export function generateKey(index = 0): string { + return `Q-${Date.now()}-${Math.random()}-${index}`; +} + +export function generateRefId(index = 0): string { + return `${index + 1}`; +} + +export function generateTargetKeys(index = 0): { refId: string; key: string } { + return { refId: generateRefId(index), key: generateKey(index) }; +} + +/** + * Ensure at least one target exists and that targets have the necessary keys + */ +export function ensureTargets(targets?: DataQuery[]): DataQuery[] { + if (targets && typeof targets === 'object' && targets.length > 0) { + return targets.map((target, i) => ({ ...target, ...generateTargetKeys(i) })); + } + return [{ ...generateTargetKeys() }]; +} + +/** + * A target is non-empty when it has keys other than refId and key. + */ +export function hasNonEmptyTarget(targets: DataQuery[]): boolean { + return targets.some(target => Object.keys(target).length > 2); +} + +export function getIntervals( + range: RawTimeRange, + datasource, + resolution: number +): { interval: string; intervalMs: number } { + if (!datasource || !resolution) { + return { interval: '1s', intervalMs: 1000 }; + } + const absoluteRange: RawTimeRange = { + from: parseDate(range.from, false), + to: parseDate(range.to, true), + }; + return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); +} + +export function makeTimeSeriesList(dataList, options) { + return dataList.map((seriesData, index) => { + const datapoints = seriesData.datapoints || []; + const alias = seriesData.target; + const colorIndex = index % colors.length; + const color = colors[colorIndex]; + + const series = new TimeSeries({ + datapoints, + alias, + color, + unit: seriesData.unit, + }); + + return series; + }); +} + +/** + * Update the query history. Side-effect: store history in local storage + */ +export function updateHistory(history: HistoryItem[], datasourceId: string, targets: DataQuery[]): HistoryItem[] { + const ts = Date.now(); + targets.forEach(target => { + history = [{ target, ts }, ...history]; + }); + + if (history.length > MAX_HISTORY_ITEMS) { + history = history.slice(0, MAX_HISTORY_ITEMS); + } + + // Combine all queries of a datasource type into one history + const historyKey = `grafana.explore.history.${datasourceId}`; + store.setObject(historyKey, history); + return history; +} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 5d39992c4a2..e88e6417fcf 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -4,14 +4,26 @@ import Select from 'react-select'; import _ from 'lodash'; import { DataSource } from 'app/types/datasources'; -import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore'; +import { + ExploreState, + ExploreUrlState, + QueryTransaction, + ResultType, + QueryHintGetter, + QueryHint, +} from 'app/types/explore'; import { RawTimeRange, DataQuery } from 'app/types/series'; -import kbn from 'app/core/utils/kbn'; -import colors from 'app/core/utils/colors'; import store from 'app/core/store'; -import TimeSeries from 'app/core/time_series2'; -import { parse as parseDate } from 'app/core/utils/datemath'; -import { DEFAULT_RANGE } from 'app/core/utils/explore'; +import { + DEFAULT_RANGE, + ensureTargets, + getIntervals, + generateKey, + generateTargetKeys, + hasNonEmptyTarget, + makeTimeSeriesList, + updateHistory, +} from 'app/core/utils/explore'; import ResetStyles from 'app/core/components/Picker/ResetStyles'; import PickerOption from 'app/core/components/Picker/PickerOption'; import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'; @@ -26,57 +38,6 @@ import Logs from './Logs'; import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import TimePicker from './TimePicker'; -import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; - -const MAX_HISTORY_ITEMS = 100; - -function getIntervals(range: RawTimeRange, datasource, resolution: number): { interval: string; intervalMs: number } { - if (!datasource || !resolution) { - return { interval: '1s', intervalMs: 1000 }; - } - const absoluteRange: RawTimeRange = { - from: parseDate(range.from, false), - to: parseDate(range.to, true), - }; - return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); -} - -function makeTimeSeriesList(dataList, options) { - return dataList.map((seriesData, index) => { - const datapoints = seriesData.datapoints || []; - const alias = seriesData.target; - const colorIndex = index % colors.length; - const color = colors[colorIndex]; - - const series = new TimeSeries({ - datapoints, - alias, - color, - unit: seriesData.unit, - }); - - return series; - }); -} - -/** - * Update the query history. Side-effect: store history in local storage - */ -function updateHistory(history: HistoryItem[], datasourceId: string, queries: string[]): HistoryItem[] { - const ts = Date.now(); - queries.forEach(query => { - history = [{ query, ts }, ...history]; - }); - - if (history.length > MAX_HISTORY_ITEMS) { - history = history.slice(0, MAX_HISTORY_ITEMS); - } - - // Combine all queries of a datasource type into one history - const historyKey = `grafana.explore.history.${datasourceId}`; - store.setObject(historyKey, history); - return history; -} interface ExploreProps { datasourceSrv: DatasourceSrv; @@ -89,14 +50,20 @@ interface ExploreProps { urlState: ExploreUrlState; } +/** + * Explore provides an area for quick query iteration for a given datasource. + * Once a datasource is selected it populates the query section at the top. + * When queries are run, their results are being displayed in the main section. + * The datasource determines what kind of query editor it brings, and what kind + * of results viewers it supports. + */ export class Explore extends React.PureComponent { el: any; /** * Current query expressions of the rows including their modifications, used for running queries. * Not kept in component state to prevent edit-render roundtrips. - * TODO: make this generic (other datasources might not have string representations of current query state) */ - queryExpressions: string[]; + modifiedTargets: DataQuery[]; /** * Local ID cache to compare requested vs selected datasource */ @@ -105,14 +72,14 @@ export class Explore extends React.PureComponent { constructor(props) { super(props); const splitState: ExploreState = props.splitState; - let initialQueries: Query[]; + let initialTargets: DataQuery[]; if (splitState) { // Split state overrides everything this.state = splitState; - initialQueries = splitState.queries; + initialTargets = splitState.initialTargets; } else { - const { datasource, queries, range } = props.urlState as ExploreUrlState; - initialQueries = ensureQueries(queries); + const { datasource, targets, range } = props.urlState as ExploreUrlState; + initialTargets = ensureTargets(targets); const initialRange = range || { ...DEFAULT_RANGE }; this.state = { datasource: null, @@ -122,8 +89,8 @@ export class Explore extends React.PureComponent { datasourceName: datasource, exploreDatasources: [], graphRange: initialRange, + initialTargets, history: [], - queries: initialQueries, queryTransactions: [], range: initialRange, showingGraph: true, @@ -135,7 +102,7 @@ export class Explore extends React.PureComponent { supportsTable: null, }; } - this.queryExpressions = initialQueries.map(q => q.query); + this.modifiedTargets = initialTargets.slice(); } async componentDidMount() { @@ -198,32 +165,26 @@ export class Explore extends React.PureComponent { } // Check if queries can be imported from previously selected datasource - let queryExpressions = this.queryExpressions; + let modifiedTargets = this.modifiedTargets; if (origin) { if (origin.meta.id === datasource.meta.id) { // Keep same queries if same type of datasource - queryExpressions = [...this.queryExpressions]; + modifiedTargets = [...this.modifiedTargets]; } else if (datasource.importQueries) { - // Datasource-specific importers, wrapping to satisfy interface - const wrappedQueries: DataQuery[] = this.queryExpressions.map((query, index) => ({ - refId: String(index), - expr: query, - })); - const modifiedQueries: DataQuery[] = await datasource.importQueries(wrappedQueries, origin.meta); - queryExpressions = modifiedQueries.map(({ expr }) => expr); + // Datasource-specific importers + modifiedTargets = await datasource.importQueries(this.modifiedTargets, origin.meta); } else { // Default is blank queries - queryExpressions = this.queryExpressions.map(() => ''); + modifiedTargets = ensureTargets(); } } // Reset edit state with new queries - const nextQueries = this.state.queries.map((q, i) => ({ - ...q, - key: generateQueryKey(i), - query: queryExpressions[i], + const nextTargets = this.state.initialTargets.map((q, i) => ({ + ...modifiedTargets[i], + ...generateTargetKeys(i), })); - this.queryExpressions = queryExpressions; + this.modifiedTargets = modifiedTargets; // Custom components const StartPage = datasource.pluginExports.ExploreStartPage; @@ -239,7 +200,7 @@ export class Explore extends React.PureComponent { supportsTable, datasourceLoading: false, datasourceName: datasource.name, - queries: nextQueries, + initialTargets: nextTargets, showingStartPage: Boolean(StartPage), }, () => { @@ -256,16 +217,15 @@ export class Explore extends React.PureComponent { onAddQueryRow = index => { // Local cache - this.queryExpressions[index + 1] = ''; + this.modifiedTargets[index + 1] = { ...generateTargetKeys(index + 1) }; this.setState(state => { - const { queries, queryTransactions } = state; + const { initialTargets, queryTransactions } = state; - // Add row by generating new react key - const nextQueries = [ - ...queries.slice(0, index + 1), - { query: '', key: generateQueryKey() }, - ...queries.slice(index + 1), + const nextTargets = [ + ...initialTargets.slice(0, index + 1), + { ...this.modifiedTargets[index + 1] }, + ...initialTargets.slice(index + 1), ]; // Ongoing transactions need to update their row indices @@ -279,7 +239,7 @@ export class Explore extends React.PureComponent { return qt; }); - return { queries: nextQueries, queryTransactions: nextQueryTransactions }; + return { initialTargets: nextTargets, queryTransactions: nextQueryTransactions }; }); }; @@ -296,26 +256,26 @@ export class Explore extends React.PureComponent { this.setDatasource(datasource as any, origin); }; - onChangeQuery = (value: string, index: number, override?: boolean) => { + onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { // Keep current value in local cache - this.queryExpressions[index] = value; + this.modifiedTargets[index] = value; if (override) { this.setState(state => { // Replace query row - const { queries, queryTransactions } = state; - const nextQuery: Query = { - key: generateQueryKey(index), - query: value, + const { initialTargets, queryTransactions } = state; + const target: DataQuery = { + ...value, + ...generateTargetKeys(index), }; - const nextQueries = [...queries]; - nextQueries[index] = nextQuery; + const nextTargets = [...initialTargets]; + nextTargets[index] = target; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - queries: nextQueries, + initialTargets: nextTargets, queryTransactions: nextQueryTransactions, }; }, this.onSubmit); @@ -330,10 +290,10 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.queryExpressions = ['']; + this.modifiedTargets = ensureTargets(); this.setState( prevState => ({ - queries: ensureQueries(), + initialTargets: [...this.modifiedTargets], queryTransactions: [], showingStartPage: Boolean(prevState.StartPage), }), @@ -387,10 +347,10 @@ export class Explore extends React.PureComponent { }; // Use this in help pages to set page to a single query - onClickQuery = query => { - const nextQueries = [{ query, key: generateQueryKey() }]; - this.queryExpressions = nextQueries.map(q => q.query); - this.setState({ queries: nextQueries }, this.onSubmit); + onClickExample = (target: DataQuery) => { + const nextTargets = [{ ...target, ...generateTargetKeys() }]; + this.modifiedTargets = [...nextTargets]; + this.setState({ initialTargets: nextTargets }, this.onSubmit); }; onClickSplit = () => { @@ -430,28 +390,28 @@ export class Explore extends React.PureComponent { const preventSubmit = action.preventSubmit; this.setState( state => { - const { queries, queryTransactions } = state; - let nextQueries; + const { initialTargets, queryTransactions } = state; + let nextTargets: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextQueries = queries.map((q, i) => ({ - key: generateQueryKey(i), - query: datasource.modifyQuery(this.queryExpressions[i], action), + nextTargets = initialTargets.map((target, i) => ({ + ...datasource.modifyQuery(this.modifiedTargets[i], action), + ...generateTargetKeys(i), })); // Discard all ongoing transactions nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = queries.map((q, i) => { + nextTargets = initialTargets.map((target, i) => { // Synchronise all queries with local query cache to ensure consistency - q.query = this.queryExpressions[i]; + // TODO still needed? return i === index ? { - key: generateQueryKey(index), - query: datasource.modifyQuery(q.query, action), + ...datasource.modifyQuery(this.modifiedTargets[i], action), + ...generateTargetKeys(i), } - : q; + : target; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -464,9 +424,9 @@ export class Explore extends React.PureComponent { // Preserve previous row query transaction to keep results visible if next query is incomplete .filter(qt => preventSubmit || qt.rowIndex !== index); } - this.queryExpressions = nextQueries.map(q => q.query); + this.modifiedTargets = [...nextTargets]; return { - queries: nextQueries, + initialTargets: nextTargets, queryTransactions: nextQueryTransactions, }; }, @@ -478,22 +438,22 @@ export class Explore extends React.PureComponent { onRemoveQueryRow = index => { // Remove from local cache - this.queryExpressions = [...this.queryExpressions.slice(0, index), ...this.queryExpressions.slice(index + 1)]; + this.modifiedTargets = [...this.modifiedTargets.slice(0, index), ...this.modifiedTargets.slice(index + 1)]; this.setState( state => { - const { queries, queryTransactions } = state; - if (queries.length <= 1) { + const { initialTargets, queryTransactions } = state; + if (initialTargets.length <= 1) { return null; } // Remove row from react state - const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; + const nextTargets = [...initialTargets.slice(0, index), ...initialTargets.slice(index + 1)]; // Discard transactions related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - queries: nextQueries, + initialTargets: nextTargets, queryTransactions: nextQueryTransactions, }; }, @@ -515,40 +475,39 @@ export class Explore extends React.PureComponent { this.saveState(); }; - buildQueryOptions( - query: string, - rowIndex: number, - targetOptions: { format: string; hinting?: boolean; instant?: boolean } - ) { + buildQueryOptions(target: DataQuery, targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, range } = this.state; const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); const targets = [ { ...targetOptions, - // Target identifier is needed for table transformations - refId: rowIndex + 1, - expr: query, + ...target, }, ]; // Clone range for query request const queryRange: RawTimeRange = { ...range }; + // Datasource is using `panelId + target.refId` for cancellation logic. + // Using `format` here because it relates to the view panel that the request is for. + const panelId = targetOptions.format; + return { interval, intervalMs, + panelId, targets, range: queryRange, }; } - startQueryTransaction(query: string, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(query, rowIndex, options); + startQueryTransaction(target: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { + const queryOptions = this.buildQueryOptions(target, options); const transaction: QueryTransaction = { - query, + target, resultType, rowIndex, - id: generateQueryKey(), + id: generateKey(), // reusing for unique ID done: false, latency: 0, options: queryOptions, @@ -578,7 +537,7 @@ export class Explore extends React.PureComponent { transactionId: string, result: any, latency: number, - queries: string[], + targets: DataQuery[], datasourceId: string ) { const { datasource } = this.state; @@ -597,9 +556,9 @@ export class Explore extends React.PureComponent { } // Get query hints - let hints; - if (datasource.getQueryHints) { - hints = datasource.getQueryHints(transaction.query, result); + let hints: QueryHint[]; + if (datasource.getQueryHints as QueryHintGetter) { + hints = datasource.getQueryHints(transaction.target, result); } // Mark transactions as complete @@ -616,7 +575,7 @@ export class Explore extends React.PureComponent { return qt; }); - const nextHistory = updateHistory(history, datasourceId, queries); + const nextHistory = updateHistory(history, datasourceId, targets); return { history: nextHistory, @@ -634,7 +593,7 @@ export class Explore extends React.PureComponent { failQueryTransaction(transactionId: string, response: any, datasourceId: string) { const { datasource } = this.state; - if (datasource.meta.id !== datasourceId) { + if (datasource.meta.id !== datasourceId || response.cancelled) { // Navigated away, queries did not matter return; } @@ -679,87 +638,75 @@ export class Explore extends React.PureComponent { } async runGraphQueries() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { + const targets = [...this.modifiedTargets]; + if (!hasNonEmptyTarget(targets)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', { - format: 'time_series', - instant: false, - }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = makeTimeSeriesList(res.data, transaction.options); - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - this.setState({ graphRange: transaction.options.range }); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); + targets.forEach(async (target, rowIndex) => { + const transaction = this.startQueryTransaction(target, rowIndex, 'Graph', { + format: 'time_series', + instant: false, + }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = makeTimeSeriesList(res.data, transaction.options); + this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId); + this.setState({ graphRange: transaction.options.range }); + } catch (response) { + this.failQueryTransaction(transaction.id, response, datasourceId); } }); } async runTableQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { + const targets = [...this.modifiedTargets]; + if (!hasNonEmptyTarget(targets)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Table', { - format: 'table', - instant: true, - valueWithRefId: true, - }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = res.data[0]; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); + targets.forEach(async (target, rowIndex) => { + const transaction = this.startQueryTransaction(target, rowIndex, 'Table', { + format: 'table', + instant: true, + valueWithRefId: true, + }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = res.data[0]; + this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId); + } catch (response) { + this.failQueryTransaction(transaction.id, response, datasourceId); } }); } async runLogsQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { + const targets = [...this.modifiedTargets]; + if (!hasNonEmptyTarget(targets)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - if (query) { - const transaction = this.startQueryTransaction(query, rowIndex, 'Logs', { format: 'logs' }); - try { - const now = Date.now(); - const res = await datasource.query(transaction.options); - const latency = Date.now() - now; - const results = res.data; - this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); - } catch (response) { - this.failQueryTransaction(transaction.id, response, datasourceId); - } - } else { - this.discardTransactions(rowIndex); + targets.forEach(async (target, rowIndex) => { + const transaction = this.startQueryTransaction(target, rowIndex, 'Logs', { format: 'logs' }); + try { + const now = Date.now(); + const res = await datasource.query(transaction.options); + const latency = Date.now() - now; + const results = res.data; + this.completeQueryTransaction(transaction.id, results, latency, targets, datasourceId); + } catch (response) { + this.failQueryTransaction(transaction.id, response, datasourceId); } }); } @@ -769,7 +716,7 @@ export class Explore extends React.PureComponent { return { ...this.state, queryTransactions: [], - queries: ensureQueries(this.queryExpressions.map(query => ({ query }))), + initialTargets: [...this.modifiedTargets], }; } @@ -789,7 +736,7 @@ export class Explore extends React.PureComponent { exploreDatasources, graphRange, history, - queries, + initialTargets, queryTransactions, range, showingGraph, @@ -903,7 +850,7 @@ export class Explore extends React.PureComponent { { />
- {showingStartPage && } + {showingStartPage && } {!showingStartPage && ( <> {supportsGraph && ( diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 224d34574b8..d5cba981951 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -27,14 +27,14 @@ function hasSuggestions(suggestions: CompletionItemGroup[]): boolean { return suggestions && suggestions.length > 0; } -interface QueryFieldProps { +export interface QueryFieldProps { additionalPlugins?: any[]; cleanText?: (text: string) => string; - initialValue: string | null; + initialQuery: string | null; onBlur?: () => void; onFocus?: () => void; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; - onValueChanged?: (value: Value) => void; + onValueChanged?: (value: string) => void; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; portalOrigin?: string; @@ -60,16 +60,22 @@ export interface TypeaheadInput { wrapperNode: Element; } +/** + * Renders an editor field. + * Pass initial value as initialQuery and listen to changes in props.onValueChanged. + * This component can only process strings. Internally it uses Slate Value. + * Implement props.onTypeahead to use suggestions, see PromQueryField.tsx as an example. + */ export class QueryField extends React.PureComponent { menuEl: HTMLElement | null; placeholdersBuffer: PlaceholdersBuffer; plugins: any[]; resetTimer: any; - constructor(props, context) { + constructor(props: QueryFieldProps, context) { super(props, context); - this.placeholdersBuffer = new PlaceholdersBuffer(props.initialValue || ''); + this.placeholdersBuffer = new PlaceholdersBuffer(props.initialQuery || ''); // Base plugins this.plugins = [ClearPlugin(), NewlinePlugin(), ...props.additionalPlugins].filter(p => p); @@ -92,7 +98,7 @@ export class QueryField extends React.PureComponent qt.hints && qt.hints.length > 0); @@ -16,7 +16,7 @@ function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHi interface QueryRowEventHandlers { onAddQueryRow: (index: number) => void; - onChangeQuery: (value: string, index: number, override?: boolean) => void; + onChangeQuery: (value: DataQuery, index: number, override?: boolean) => void; onClickHintFix: (action: object, index?: number) => void; onExecuteQuery: () => void; onRemoveQueryRow: (index: number) => void; @@ -32,11 +32,11 @@ interface QueryRowCommonProps { type QueryRowProps = QueryRowCommonProps & QueryRowEventHandlers & { index: number; - query: string; + initialTarget: DataQuery; }; class QueryRow extends PureComponent { - onChangeQuery = (value, override?: boolean) => { + onChangeQuery = (value: DataQuery, override?: boolean) => { const { index, onChangeQuery } = this.props; if (onChangeQuery) { onChangeQuery(value, index, override); @@ -51,7 +51,7 @@ class QueryRow extends PureComponent { }; onClickClearButton = () => { - this.onChangeQuery('', true); + this.onChangeQuery(null, true); }; onClickHintFix = action => { @@ -76,7 +76,7 @@ class QueryRow extends PureComponent { }; render() { - const { datasource, history, query, transactions } = this.props; + const { datasource, history, initialTarget, transactions } = this.props; const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); const queryError = transactionWithError ? transactionWithError.error : null; @@ -91,7 +91,7 @@ class QueryRow extends PureComponent { datasource={datasource} error={queryError} hint={hint} - initialQuery={query} + initialTarget={initialTarget} history={history} onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} @@ -116,19 +116,19 @@ class QueryRow extends PureComponent { type QueryRowsProps = QueryRowCommonProps & QueryRowEventHandlers & { - queries: Query[]; + initialTargets: DataQuery[]; }; export default class QueryRows extends PureComponent { render() { - const { className = '', queries, transactions, ...handlers } = this.props; + const { className = '', initialTargets, transactions, ...handlers } = this.props; return (
- {queries.map((q, index) => ( + {initialTargets.map((target, index) => ( t.rowIndex === index)} {...handlers} /> diff --git a/public/app/features/explore/QueryTransactionStatus.tsx b/public/app/features/explore/QueryTransactionStatus.tsx index 77a50b7d2ca..6f47f147645 100644 --- a/public/app/features/explore/QueryTransactionStatus.tsx +++ b/public/app/features/explore/QueryTransactionStatus.tsx @@ -35,7 +35,9 @@ export default class QueryTransactionStatus extends PureComponent - {transactions.map((t, i) => )} + {transactions.map((t, i) => ( + + ))}
); } diff --git a/public/app/features/explore/utils/query.ts b/public/app/features/explore/utils/query.ts deleted file mode 100644 index 193ee2dbc52..00000000000 --- a/public/app/features/explore/utils/query.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Query } from 'app/types/explore'; - -export function generateQueryKey(index = 0): string { - return `Q-${Date.now()}-${Math.random()}-${index}`; -} - -export function ensureQueries(queries?: Query[]): Query[] { - if (queries && typeof queries === 'object' && queries.length > 0 && typeof queries[0].query === 'string') { - return queries.map(({ query }, i) => ({ key: generateQueryKey(i), query })); - } - return [{ key: generateQueryKey(), query: '' }]; -} - -export function hasQuery(queries: string[]): boolean { - return queries.some(q => Boolean(q)); -} diff --git a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx b/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx index a7af48b6eda..651c28783d9 100644 --- a/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingCheatSheet.tsx @@ -19,7 +19,10 @@ export default (props: any) => ( {CHEAT_SHEET_ITEMS.map(item => (
{item.title}
-
props.onClickQuery(item.expression)}> +
props.onClickExample({ refId: '1', expr: item.expression })} + > {item.expression}
{item.label}
diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx index ce79d38f9a8..78bdf271a3e 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx @@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import { DataQuery } from 'app/types'; const PRISM_SYNTAX = 'promql'; @@ -53,10 +54,10 @@ interface LoggingQueryFieldProps { error?: string | JSX.Element; hint?: any; history?: any[]; - initialQuery?: string | null; + initialTarget?: DataQuery; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: string, override?: boolean) => void; + onQueryChange?: (value: DataQuery, override?: boolean) => void; } interface LoggingQueryFieldState { @@ -134,9 +135,13 @@ class LoggingQueryField extends React.PureComponent { // Send text change to parent - const { onQueryChange } = this.props; + const { initialTarget, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const target = { + ...initialTarget, + expr: value, + }; + onQueryChange(target, override); } }; @@ -181,7 +186,7 @@ class LoggingQueryField extends React.PureComponent
- {error ?
{error}
: null} diff --git a/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx b/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx index 89262999637..2c25a248fa9 100644 --- a/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingStartPage.tsx @@ -52,7 +52,7 @@ export default class LoggingStartPage extends PureComponent
- {active === 'start' && } + {active === 'start' && }
); diff --git a/public/app/plugins/datasource/logging/datasource.ts b/public/app/plugins/datasource/logging/datasource.ts index 494dcd78d6c..5d27d3f1f5c 100644 --- a/public/app/plugins/datasource/logging/datasource.ts +++ b/public/app/plugins/datasource/logging/datasource.ts @@ -112,8 +112,8 @@ export default class LoggingDatasource { }); } - async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { - return this.languageProvider.importQueries(queries, originMeta.id); + async importQueries(targets: DataQuery[], originMeta: PluginMeta): Promise { + return this.languageProvider.importQueries(targets, originMeta.id); } metadataRequest(url) { diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 00745d2eee8..419470c7f06 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -158,25 +158,29 @@ export default class LoggingLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } - async importQueries(queries: DataQuery[], datasourceType: string): Promise { + async importQueries(targets: DataQuery[], datasourceType: string): Promise { if (datasourceType === 'prometheus') { return Promise.all( - queries.map(async query => { - const expr = await this.importPrometheusQuery(query.expr); + targets.map(async target => { + const expr = await this.importPrometheusQuery(target.expr); return { - ...query, + ...target, expr, }; }) ); } - return queries.map(query => ({ - ...query, + return targets.map(target => ({ + ...target, expr: '', })); } async importPrometheusQuery(query: string): Promise { + if (!query) { + return ''; + } + // Consider only first selector in query const selectorMatch = query.match(selectorRegexp); if (selectorMatch) { @@ -192,7 +196,7 @@ export default class LoggingLanguageProvider extends LanguageProvider { const commonLabels = {}; for (const key in labels) { const existingKeys = this.labelKeys[EMPTY_SELECTOR]; - if (existingKeys.indexOf(key) > -1) { + if (existingKeys && existingKeys.indexOf(key) > -1) { // Should we check for label value equality here? commonLabels[key] = labels[key]; } diff --git a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx index a2d3a03d794..ea9a373e67a 100644 --- a/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromCheatSheet.tsx @@ -25,7 +25,10 @@ export default (props: any) => ( {CHEAT_SHEET_ITEMS.map(item => (
{item.title}
-
props.onClickQuery(item.expression)}> +
props.onClickExample({ refId: '1', expr: item.expression })} + > {item.expression}
{item.label}
diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index a7787096d85..cefd44eba64 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -10,7 +10,8 @@ import { TypeaheadOutput } from 'app/types/explore'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; -import TypeaheadField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; +import { DataQuery } from 'app/types'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -84,17 +85,17 @@ interface CascaderOption { disabled?: boolean; } -interface PromQueryFieldProps { +type PromQueryFieldProps = { datasource: any; error?: string | JSX.Element; + initialTarget: DataQuery; hint?: any; history?: any[]; - initialQuery?: string | null; metricsByPrefix?: CascaderOption[]; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; - onQueryChange?: (value: string, override?: boolean) => void; -} + onQueryChange?: (value: DataQuery, override?: boolean) => void; +}; interface PromQueryFieldState { metricsOptions: any[]; @@ -161,11 +162,15 @@ class PromQueryField extends React.PureComponent { + onChangeQuery = (query: string, override?: boolean) => { // Send text change to parent - const { onQueryChange } = this.props; + const { initialTarget, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const target: DataQuery = { + ...initialTarget, + expr: query, + }; + onQueryChange(target, override); } }; @@ -227,10 +232,10 @@ class PromQueryField extends React.PureComponent @@ -242,10 +247,10 @@ class PromQueryField extends React.PureComponent
- {
- {active === 'start' && } + {active === 'start' && }
); diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 0cedafdff75..f845fc17106 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -11,6 +11,8 @@ import { BackendSrv } from 'app/core/services/backend_srv'; import addLabelToQuery from './add_label_to_query'; import { getQueryHints } from './query_hints'; import { expandRecordingRules } from './language_utils'; +import { DataQuery } from 'app/types'; +import { ExploreUrlState } from 'app/types/explore'; export function alignRange(start, end, step) { const alignedEnd = Math.ceil(end / step) * step; @@ -419,24 +421,23 @@ export class PrometheusDatasource { }); } - getExploreState(targets: any[]) { - let state = {}; + getExploreState(targets: DataQuery[]): Partial { + let state: Partial = { datasource: this.name }; if (targets && targets.length > 0) { - const queries = targets.map(t => ({ - query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), - format: t.format, + const expandedTargets = targets.map(target => ({ + ...target, + expr: this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr), })); state = { ...state, - queries, - datasource: this.name, + targets: expandedTargets, }; } return state; } - getQueryHints(query: string, result: any[]) { - return getQueryHints(query, result, this); + getQueryHints(target: DataQuery, result: any[]) { + return getQueryHints(target.expr, result, this); } loadRules() { @@ -454,28 +455,35 @@ export class PrometheusDatasource { }); } - modifyQuery(query: string, action: any): string { + modifyQuery(target: DataQuery, action: any): DataQuery { + let query = target.expr; switch (action.type) { case 'ADD_FILTER': { - return addLabelToQuery(query, action.key, action.value); + query = addLabelToQuery(query, action.key, action.value); + break; } case 'ADD_HISTOGRAM_QUANTILE': { - return `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`; + query = `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`; + break; } case 'ADD_RATE': { - return `rate(${query}[5m])`; + query = `rate(${query}[5m])`; + break; } case 'ADD_SUM': { - return `sum(${query.trim()}) by ($1)`; + query = `sum(${query.trim()}) by ($1)`; + break; } case 'EXPAND_RULES': { if (action.mapping) { - return expandRecordingRules(query, action.mapping); + query = expandRecordingRules(query, action.mapping); } + break; } default: - return query; + break; } + return { ...target, expr: query }; } getPrometheusTime(date, roundUp) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 662835633db..bbfbc88c1e5 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,6 +1,6 @@ import { Value } from 'slate'; -import { RawTimeRange } from './series'; +import { DataQuery, RawTimeRange } from './series'; export interface CompletionItem { /** @@ -79,7 +79,7 @@ interface ExploreDatasource { export interface HistoryItem { ts: number; - query: string; + target: DataQuery; } export abstract class LanguageProvider { @@ -107,11 +107,6 @@ export interface TypeaheadOutput { suggestions: CompletionItemGroup[]; } -export interface Query { - query: string; - key?: string; -} - export interface QueryFix { type: string; label: string; @@ -130,6 +125,10 @@ export interface QueryHint { fix?: QueryFix; } +export interface QueryHintGetter { + (target: DataQuery, results: any[], ...rest: any): QueryHint[]; +} + export interface QueryTransaction { id: string; done: boolean; @@ -137,10 +136,10 @@ export interface QueryTransaction { hints?: QueryHint[]; latency: number; options: any; - query: string; result?: any; // Table model / Timeseries[] / Logs resultType: ResultType; rowIndex: number; + target: DataQuery; } export interface TextMatch { @@ -160,15 +159,7 @@ export interface ExploreState { exploreDatasources: ExploreDatasource[]; graphRange: RawTimeRange; history: HistoryItem[]; - /** - * Initial rows of queries to push down the tree. - * Modifications do not end up here, but in `this.queryExpressions`. - * The only way to reset a query is to change its `key`. - */ - queries: Query[]; - /** - * Hints gathered for the query row. - */ + initialTargets: DataQuery[]; queryTransactions: QueryTransaction[]; range: RawTimeRange; showingGraph: boolean; @@ -182,7 +173,7 @@ export interface ExploreState { export interface ExploreUrlState { datasource: string; - queries: Query[]; + targets: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; }