From b3161bea5a8040906c86ba2b919998cfb388f255 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 21 Nov 2018 14:45:57 +0100 Subject: [PATCH 1/6] 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; } From 22987ee75f5f68ead64f33a729e6f2eea3d16996 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 21 Nov 2018 16:28:30 +0100 Subject: [PATCH 2/6] Renamed targets to queries --- public/app/core/utils/explore.test.ts | 22 +-- public/app/core/utils/explore.ts | 34 ++-- public/app/features/explore/Explore.tsx | 174 +++++++++--------- public/app/features/explore/QueryRows.tsx | 16 +- .../logging/components/LoggingQueryField.tsx | 14 +- .../plugins/datasource/logging/datasource.ts | 4 +- .../datasource/logging/language_provider.ts | 12 +- .../prometheus/components/PromQueryField.tsx | 18 +- .../datasource/prometheus/datasource.ts | 32 ++-- public/app/types/explore.ts | 10 +- 10 files changed, 168 insertions(+), 168 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index aaa7e4d4281..e12f7782a12 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: [], - initialTargets: [], + initialQueries: [], 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, - targets: [], + queries: [], range: DEFAULT_RANGE, }); }); it('returns a valid Explore state from URL parameter', () => { const paramValue = - '%7B"datasource":"Local","targets":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D'; + '%7B"datasource":"Local","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D'; expect(parseUrlState(paramValue)).toMatchObject({ datasource: 'Local', - targets: [{ expr: 'metric' }], + queries: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -48,7 +48,7 @@ describe('state functions', () => { const paramValue = '%5B"now-1h","now","Local",%7B"expr":"metric"%7D%5D'; expect(parseUrlState(paramValue)).toMatchObject({ datasource: 'Local', - targets: [{ expr: 'metric' }], + queries: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -66,7 +66,7 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - initialTargets: [ + initialQueries: [ { refId: '1', expr: 'metric{test="a/b"}', @@ -78,7 +78,7 @@ describe('state functions', () => { ], }; expect(serializeStateToUrlParam(state)).toBe( - '{"datasource":"foo","targets":[{"expr":"metric{test=\\"a/b\\"}"},' + + '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' ); }); @@ -91,7 +91,7 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - initialTargets: [ + initialQueries: [ { refId: '1', expr: 'metric{test="a/b"}', @@ -117,7 +117,7 @@ describe('state functions', () => { from: 'now - 5h', to: 'now', }, - initialTargets: [ + initialQueries: [ { refId: '1', expr: 'metric{test="a/b"}', @@ -132,12 +132,12 @@ describe('state functions', () => { const parsed = parseUrlState(serialized); // Account for datasource vs datasourceName - const { datasource, targets, ...rest } = parsed; + const { datasource, queries, ...rest } = parsed; const resultState = { ...rest, datasource: DEFAULT_EXPLORE_STATE.datasource, datasourceName: datasource, - initialTargets: targets, + initialQueries: queries, }; expect(state).toMatchObject(resultState); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index bb1d1c9bbf1..f0d5c6f83d5 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -32,7 +32,7 @@ export async function getExploreUrl( timeSrv: any ) { let exploreDatasource = panelDatasource; - let exploreTargets = panelTargets; + let exploreTargets: DataQuery[] = panelTargets; let url; // Mixed datasources need to choose only one datasource @@ -79,25 +79,25 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - const targets = parsed.slice(3); - return { datasource, targets, range }; + const queries = parsed.slice(3); + return { datasource, queries, range }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, targets: [], range: DEFAULT_RANGE }; + return { datasource: null, queries: [], range: DEFAULT_RANGE }; } export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { datasource: state.datasourceName, - targets: state.initialTargets.map(({ key, refId, ...rest }) => rest), + queries: state.initialQueries.map(({ key, refId, ...rest }) => rest), range: state.range, }; if (compact) { - return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.targets]); + return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); } return JSON.stringify(urlState); } @@ -110,25 +110,25 @@ export function generateRefId(index = 0): string { return `${index + 1}`; } -export function generateTargetKeys(index = 0): { refId: string; key: string } { +export function generateQueryKeys(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) })); +export function ensureQueries(queries?: DataQuery[]): DataQuery[] { + if (queries && typeof queries === 'object' && queries.length > 0) { + return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) })); } - return [{ ...generateTargetKeys() }]; + return [{ ...generateQueryKeys() }]; } /** * 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 hasNonEmptyQuery(queries: DataQuery[]): boolean { + return queries.some(query => Object.keys(query).length > 2); } export function getIntervals( @@ -146,7 +146,7 @@ export function getIntervals( return kbn.calculateInterval(absoluteRange, resolution, datasource.interval); } -export function makeTimeSeriesList(dataList, options) { +export function makeTimeSeriesList(dataList) { return dataList.map((seriesData, index) => { const datapoints = seriesData.datapoints || []; const alias = seriesData.target; @@ -167,10 +167,10 @@ export function makeTimeSeriesList(dataList, options) { /** * Update the query history. Side-effect: store history in local storage */ -export function updateHistory(history: HistoryItem[], datasourceId: string, targets: DataQuery[]): HistoryItem[] { +export function updateHistory(history: HistoryItem[], datasourceId: string, queries: DataQuery[]): HistoryItem[] { const ts = Date.now(); - targets.forEach(target => { - history = [{ target, ts }, ...history]; + queries.forEach(query => { + history = [{ query, ts }, ...history]; }); if (history.length > MAX_HISTORY_ITEMS) { diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index e88e6417fcf..313ebcfa147 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -16,11 +16,11 @@ import { RawTimeRange, DataQuery } from 'app/types/series'; import store from 'app/core/store'; import { DEFAULT_RANGE, - ensureTargets, + ensureQueries, getIntervals, generateKey, - generateTargetKeys, - hasNonEmptyTarget, + generateQueryKeys, + hasNonEmptyQuery, makeTimeSeriesList, updateHistory, } from 'app/core/utils/explore'; @@ -63,7 +63,7 @@ export class Explore extends React.PureComponent { * Current query expressions of the rows including their modifications, used for running queries. * Not kept in component state to prevent edit-render roundtrips. */ - modifiedTargets: DataQuery[]; + modifiedQueries: DataQuery[]; /** * Local ID cache to compare requested vs selected datasource */ @@ -72,14 +72,14 @@ export class Explore extends React.PureComponent { constructor(props) { super(props); const splitState: ExploreState = props.splitState; - let initialTargets: DataQuery[]; + let initialQueries: DataQuery[]; if (splitState) { // Split state overrides everything this.state = splitState; - initialTargets = splitState.initialTargets; + initialQueries = splitState.initialQueries; } else { - const { datasource, targets, range } = props.urlState as ExploreUrlState; - initialTargets = ensureTargets(targets); + const { datasource, queries, range } = props.urlState as ExploreUrlState; + initialQueries = ensureQueries(queries); const initialRange = range || { ...DEFAULT_RANGE }; this.state = { datasource: null, @@ -89,7 +89,7 @@ export class Explore extends React.PureComponent { datasourceName: datasource, exploreDatasources: [], graphRange: initialRange, - initialTargets, + initialQueries, history: [], queryTransactions: [], range: initialRange, @@ -102,7 +102,7 @@ export class Explore extends React.PureComponent { supportsTable: null, }; } - this.modifiedTargets = initialTargets.slice(); + this.modifiedQueries = initialQueries.slice(); } async componentDidMount() { @@ -165,26 +165,26 @@ export class Explore extends React.PureComponent { } // Check if queries can be imported from previously selected datasource - let modifiedTargets = this.modifiedTargets; + let modifiedQueries = this.modifiedQueries; if (origin) { if (origin.meta.id === datasource.meta.id) { // Keep same queries if same type of datasource - modifiedTargets = [...this.modifiedTargets]; + modifiedQueries = [...this.modifiedQueries]; } else if (datasource.importQueries) { // Datasource-specific importers - modifiedTargets = await datasource.importQueries(this.modifiedTargets, origin.meta); + modifiedQueries = await datasource.importQueries(this.modifiedQueries, origin.meta); } else { // Default is blank queries - modifiedTargets = ensureTargets(); + modifiedQueries = ensureQueries(); } } // Reset edit state with new queries - const nextTargets = this.state.initialTargets.map((q, i) => ({ - ...modifiedTargets[i], - ...generateTargetKeys(i), + const nextQueries = this.state.initialQueries.map((q, i) => ({ + ...modifiedQueries[i], + ...generateQueryKeys(i), })); - this.modifiedTargets = modifiedTargets; + this.modifiedQueries = modifiedQueries; // Custom components const StartPage = datasource.pluginExports.ExploreStartPage; @@ -200,7 +200,7 @@ export class Explore extends React.PureComponent { supportsTable, datasourceLoading: false, datasourceName: datasource.name, - initialTargets: nextTargets, + initialQueries: nextQueries, showingStartPage: Boolean(StartPage), }, () => { @@ -217,15 +217,15 @@ export class Explore extends React.PureComponent { onAddQueryRow = index => { // Local cache - this.modifiedTargets[index + 1] = { ...generateTargetKeys(index + 1) }; + this.modifiedQueries[index + 1] = { ...generateQueryKeys(index + 1) }; this.setState(state => { - const { initialTargets, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; - const nextTargets = [ - ...initialTargets.slice(0, index + 1), - { ...this.modifiedTargets[index + 1] }, - ...initialTargets.slice(index + 1), + const nextQueries = [ + ...initialQueries.slice(0, index + 1), + { ...this.modifiedQueries[index + 1] }, + ...initialQueries.slice(index + 1), ]; // Ongoing transactions need to update their row indices @@ -239,7 +239,7 @@ export class Explore extends React.PureComponent { return qt; }); - return { initialTargets: nextTargets, queryTransactions: nextQueryTransactions }; + return { initialQueries: nextQueries, queryTransactions: nextQueryTransactions }; }); }; @@ -258,24 +258,24 @@ export class Explore extends React.PureComponent { onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { // Keep current value in local cache - this.modifiedTargets[index] = value; + this.modifiedQueries[index] = value; if (override) { this.setState(state => { // Replace query row - const { initialTargets, queryTransactions } = state; - const target: DataQuery = { + const { initialQueries, queryTransactions } = state; + const query: DataQuery = { ...value, - ...generateTargetKeys(index), + ...generateQueryKeys(index), }; - const nextTargets = [...initialTargets]; - nextTargets[index] = target; + const nextQueries = [...initialQueries]; + nextQueries[index] = query; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - initialTargets: nextTargets, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, this.onSubmit); @@ -290,10 +290,10 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.modifiedTargets = ensureTargets(); + this.modifiedQueries = ensureQueries(); this.setState( prevState => ({ - initialTargets: [...this.modifiedTargets], + initialQueries: [...this.modifiedQueries], queryTransactions: [], showingStartPage: Boolean(prevState.StartPage), }), @@ -347,10 +347,10 @@ export class Explore extends React.PureComponent { }; // Use this in help pages to set page to a single query - onClickExample = (target: DataQuery) => { - const nextTargets = [{ ...target, ...generateTargetKeys() }]; - this.modifiedTargets = [...nextTargets]; - this.setState({ initialTargets: nextTargets }, this.onSubmit); + onClickExample = (query: DataQuery) => { + const nextQueries = [{ ...query, ...generateQueryKeys() }]; + this.modifiedQueries = [...nextQueries]; + this.setState({ initialQueries: nextQueries }, this.onSubmit); }; onClickSplit = () => { @@ -390,28 +390,28 @@ export class Explore extends React.PureComponent { const preventSubmit = action.preventSubmit; this.setState( state => { - const { initialTargets, queryTransactions } = state; - let nextTargets: DataQuery[]; + const { initialQueries, queryTransactions } = state; + let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextTargets = initialTargets.map((target, i) => ({ - ...datasource.modifyQuery(this.modifiedTargets[i], action), - ...generateTargetKeys(i), + nextQueries = initialQueries.map((query, i) => ({ + ...datasource.modifyQuery(this.modifiedQueries[i], action), + ...generateQueryKeys(i), })); // Discard all ongoing transactions nextQueryTransactions = []; } else { // Modify query only at index - nextTargets = initialTargets.map((target, i) => { + nextQueries = initialQueries.map((query, i) => { // Synchronise all queries with local query cache to ensure consistency // TODO still needed? return i === index ? { - ...datasource.modifyQuery(this.modifiedTargets[i], action), - ...generateTargetKeys(i), + ...datasource.modifyQuery(this.modifiedQueries[i], action), + ...generateQueryKeys(i), } - : target; + : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -424,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.modifiedTargets = [...nextTargets]; + this.modifiedQueries = [...nextQueries]; return { - initialTargets: nextTargets, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -438,22 +438,22 @@ export class Explore extends React.PureComponent { onRemoveQueryRow = index => { // Remove from local cache - this.modifiedTargets = [...this.modifiedTargets.slice(0, index), ...this.modifiedTargets.slice(index + 1)]; + this.modifiedQueries = [...this.modifiedQueries.slice(0, index), ...this.modifiedQueries.slice(index + 1)]; this.setState( state => { - const { initialTargets, queryTransactions } = state; - if (initialTargets.length <= 1) { + const { initialQueries, queryTransactions } = state; + if (initialQueries.length <= 1) { return null; } // Remove row from react state - const nextTargets = [...initialTargets.slice(0, index), ...initialTargets.slice(index + 1)]; + const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; // Discard transactions related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - initialTargets: nextTargets, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -475,36 +475,36 @@ export class Explore extends React.PureComponent { this.saveState(); }; - buildQueryOptions(target: DataQuery, targetOptions: { format: string; hinting?: boolean; instant?: boolean }) { + buildQueryOptions(query: DataQuery, queryOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, range } = this.state; const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); - const targets = [ + const queries = [ { - ...targetOptions, - ...target, + ...queryOptions, + ...query, }, ]; // Clone range for query request const queryRange: RawTimeRange = { ...range }; - // Datasource is using `panelId + target.refId` for cancellation logic. + // Datasource is using `panelId + query.refId` for cancellation logic. // Using `format` here because it relates to the view panel that the request is for. - const panelId = targetOptions.format; + const panelId = queryOptions.format; return { interval, intervalMs, panelId, - targets, + queries, range: queryRange, }; } - startQueryTransaction(target: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(target, options); + startQueryTransaction(query: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { + const queryOptions = this.buildQueryOptions(query, options); const transaction: QueryTransaction = { - target, + query, resultType, rowIndex, id: generateKey(), // reusing for unique ID @@ -537,7 +537,7 @@ export class Explore extends React.PureComponent { transactionId: string, result: any, latency: number, - targets: DataQuery[], + queries: DataQuery[], datasourceId: string ) { const { datasource } = this.state; @@ -558,7 +558,7 @@ export class Explore extends React.PureComponent { // Get query hints let hints: QueryHint[]; if (datasource.getQueryHints as QueryHintGetter) { - hints = datasource.getQueryHints(transaction.target, result); + hints = datasource.getQueryHints(transaction.query, result); } // Mark transactions as complete @@ -575,7 +575,7 @@ export class Explore extends React.PureComponent { return qt; }); - const nextHistory = updateHistory(history, datasourceId, targets); + const nextHistory = updateHistory(history, datasourceId, queries); return { history: nextHistory, @@ -638,15 +638,15 @@ export class Explore extends React.PureComponent { } async runGraphQueries() { - const targets = [...this.modifiedTargets]; - if (!hasNonEmptyTarget(targets)) { + const queries = [...this.modifiedQueries]; + if (!hasNonEmptyQuery(queries)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - targets.forEach(async (target, rowIndex) => { - const transaction = this.startQueryTransaction(target, rowIndex, 'Graph', { + queries.forEach(async (query, rowIndex) => { + const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', { format: 'time_series', instant: false, }); @@ -654,8 +654,8 @@ export class Explore extends React.PureComponent { 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); + const results = makeTimeSeriesList(res.data); + this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); this.setState({ graphRange: transaction.options.range }); } catch (response) { this.failQueryTransaction(transaction.id, response, datasourceId); @@ -664,15 +664,15 @@ export class Explore extends React.PureComponent { } async runTableQuery() { - const targets = [...this.modifiedTargets]; - if (!hasNonEmptyTarget(targets)) { + const queries = [...this.modifiedQueries]; + if (!hasNonEmptyQuery(queries)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - targets.forEach(async (target, rowIndex) => { - const transaction = this.startQueryTransaction(target, rowIndex, 'Table', { + queries.forEach(async (query, rowIndex) => { + const transaction = this.startQueryTransaction(query, rowIndex, 'Table', { format: 'table', instant: true, valueWithRefId: true, @@ -682,7 +682,7 @@ export class Explore extends React.PureComponent { 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); + this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); } catch (response) { this.failQueryTransaction(transaction.id, response, datasourceId); } @@ -690,21 +690,21 @@ export class Explore extends React.PureComponent { } async runLogsQuery() { - const targets = [...this.modifiedTargets]; - if (!hasNonEmptyTarget(targets)) { + const queries = [...this.modifiedQueries]; + if (!hasNonEmptyQuery(queries)) { return; } const { datasource } = this.state; const datasourceId = datasource.meta.id; // Run all queries concurrently - targets.forEach(async (target, rowIndex) => { - const transaction = this.startQueryTransaction(target, rowIndex, 'Logs', { format: 'logs' }); + queries.forEach(async (query, rowIndex) => { + 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, targets, datasourceId); + this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); } catch (response) { this.failQueryTransaction(transaction.id, response, datasourceId); } @@ -716,7 +716,7 @@ export class Explore extends React.PureComponent { return { ...this.state, queryTransactions: [], - initialTargets: [...this.modifiedTargets], + initialQueries: [...this.modifiedQueries], }; } @@ -736,7 +736,7 @@ export class Explore extends React.PureComponent { exploreDatasources, graphRange, history, - initialTargets, + initialQueries, queryTransactions, range, showingGraph, @@ -850,7 +850,7 @@ export class Explore extends React.PureComponent { { @@ -76,7 +76,7 @@ class QueryRow extends PureComponent { }; render() { - const { datasource, history, initialTarget, transactions } = this.props; + const { datasource, history, initialQuery, 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} - initialTarget={initialTarget} + initialQuery={initialQuery} history={history} onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} @@ -116,19 +116,19 @@ class QueryRow extends PureComponent { type QueryRowsProps = QueryRowCommonProps & QueryRowEventHandlers & { - initialTargets: DataQuery[]; + initialQueries: DataQuery[]; }; export default class QueryRows extends PureComponent { render() { - const { className = '', initialTargets, transactions, ...handlers } = this.props; + const { className = '', initialQueries, transactions, ...handlers } = this.props; return (
- {initialTargets.map((target, index) => ( + {initialQueries.map((query, index) => ( t.rowIndex === index)} {...handlers} /> diff --git a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx index 78bdf271a3e..5667bd9a20d 100644 --- a/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx +++ b/public/app/plugins/datasource/logging/components/LoggingQueryField.tsx @@ -54,7 +54,7 @@ interface LoggingQueryFieldProps { error?: string | JSX.Element; hint?: any; history?: any[]; - initialTarget?: DataQuery; + initialQuery?: DataQuery; onClickHintFix?: (action: any) => void; onPressEnter?: () => void; onQueryChange?: (value: DataQuery, override?: boolean) => void; @@ -135,13 +135,13 @@ class LoggingQueryField extends React.PureComponent { // Send text change to parent - const { initialTarget, onQueryChange } = this.props; + const { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - const target = { - ...initialTarget, + const query = { + ...initialQuery, expr: value, }; - onQueryChange(target, override); + onQueryChange(query, override); } }; @@ -186,7 +186,7 @@ class LoggingQueryField extends React.PureComponent { - return this.languageProvider.importQueries(targets, originMeta.id); + async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { + return this.languageProvider.importQueries(queries, 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 419470c7f06..253e2ba097f 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -158,20 +158,20 @@ export default class LoggingLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } - async importQueries(targets: DataQuery[], datasourceType: string): Promise { + async importQueries(queries: DataQuery[], datasourceType: string): Promise { if (datasourceType === 'prometheus') { return Promise.all( - targets.map(async target => { - const expr = await this.importPrometheusQuery(target.expr); + queries.map(async query => { + const expr = await this.importPrometheusQuery(query.expr); return { - ...target, + ...query, expr, }; }) ); } - return targets.map(target => ({ - ...target, + return queries.map(query => ({ + ...query, expr: '', })); } diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index cefd44eba64..5f8a49b0bad 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -88,7 +88,7 @@ interface CascaderOption { type PromQueryFieldProps = { datasource: any; error?: string | JSX.Element; - initialTarget: DataQuery; + initialQuery: DataQuery; hint?: any; history?: any[]; metricsByPrefix?: CascaderOption[]; @@ -162,15 +162,15 @@ class PromQueryField extends React.PureComponent { + onChangeQuery = (value: string, override?: boolean) => { // Send text change to parent - const { initialTarget, onQueryChange } = this.props; + const { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - const target: DataQuery = { - ...initialTarget, - expr: query, + const query: DataQuery = { + ...initialQuery, + expr: value, }; - onQueryChange(target, override); + onQueryChange(query, override); } }; @@ -232,7 +232,7 @@ class PromQueryField extends React.PureComponent { + getExploreState(queries: DataQuery[]): Partial { let state: Partial = { datasource: this.name }; - if (targets && targets.length > 0) { - const expandedTargets = targets.map(target => ({ - ...target, - expr: this.templateSrv.replace(target.expr, {}, this.interpolateQueryExpr), + if (queries && queries.length > 0) { + const expandedQueries = queries.map(query => ({ + ...query, + expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr), })); state = { ...state, - targets: expandedTargets, + queries: expandedQueries, }; } return state; } - getQueryHints(target: DataQuery, result: any[]) { - return getQueryHints(target.expr, result, this); + getQueryHints(query: DataQuery, result: any[]) { + return getQueryHints(query.expr, result, this); } loadRules() { @@ -455,35 +455,35 @@ export class PrometheusDatasource { }); } - modifyQuery(target: DataQuery, action: any): DataQuery { - let query = target.expr; + modifyQuery(query: DataQuery, action: any): DataQuery { + let expression = query.expr || ''; switch (action.type) { case 'ADD_FILTER': { - query = addLabelToQuery(query, action.key, action.value); + expression = addLabelToQuery(expression, action.key, action.value); break; } case 'ADD_HISTOGRAM_QUANTILE': { - query = `histogram_quantile(0.95, sum(rate(${query}[5m])) by (le))`; + expression = `histogram_quantile(0.95, sum(rate(${expression}[5m])) by (le))`; break; } case 'ADD_RATE': { - query = `rate(${query}[5m])`; + expression = `rate(${expression}[5m])`; break; } case 'ADD_SUM': { - query = `sum(${query.trim()}) by ($1)`; + expression = `sum(${expression.trim()}) by ($1)`; break; } case 'EXPAND_RULES': { if (action.mapping) { - query = expandRecordingRules(query, action.mapping); + expression = expandRecordingRules(expression, action.mapping); } break; } default: break; } - return { ...target, expr: query }; + return { ...query, expr: expression }; } getPrometheusTime(date, roundUp) { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index bbfbc88c1e5..f80a485fc29 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -79,7 +79,7 @@ interface ExploreDatasource { export interface HistoryItem { ts: number; - target: DataQuery; + query: DataQuery; } export abstract class LanguageProvider { @@ -126,7 +126,7 @@ export interface QueryHint { } export interface QueryHintGetter { - (target: DataQuery, results: any[], ...rest: any): QueryHint[]; + (query: DataQuery, results: any[], ...rest: any): QueryHint[]; } export interface QueryTransaction { @@ -136,10 +136,10 @@ export interface QueryTransaction { hints?: QueryHint[]; latency: number; options: any; + query: DataQuery; result?: any; // Table model / Timeseries[] / Logs resultType: ResultType; rowIndex: number; - target: DataQuery; } export interface TextMatch { @@ -159,7 +159,7 @@ export interface ExploreState { exploreDatasources: ExploreDatasource[]; graphRange: RawTimeRange; history: HistoryItem[]; - initialTargets: DataQuery[]; + initialQueries: DataQuery[]; queryTransactions: QueryTransaction[]; range: RawTimeRange; showingGraph: boolean; @@ -173,7 +173,7 @@ export interface ExploreState { export interface ExploreUrlState { datasource: string; - targets: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; } From 331d419d4fd314753f34650795cfccaab1fd8642 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Wed, 21 Nov 2018 17:21:13 +0100 Subject: [PATCH 3/6] Combine query functions --- public/app/features/explore/Explore.tsx | 93 ++++++------------- .../prometheus/components/PromQueryField.tsx | 4 +- 2 files changed, 32 insertions(+), 65 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 313ebcfa147..0c4795d4d2f 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -257,12 +257,9 @@ export class Explore extends React.PureComponent { }; onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { - // Keep current value in local cache - this.modifiedQueries[index] = value; - if (override) { this.setState(state => { - // Replace query row + // Replace query row by injecting new key const { initialQueries, queryTransactions } = state; const query: DataQuery = { ...value, @@ -270,6 +267,7 @@ export class Explore extends React.PureComponent { }; const nextQueries = [...initialQueries]; nextQueries[index] = query; + this.modifiedQueries = [...nextQueries]; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); @@ -279,6 +277,9 @@ export class Explore extends React.PureComponent { queryTransactions: nextQueryTransactions, }; }, this.onSubmit); + } else if (value) { + // Keep current value in local cache + this.modifiedQueries[index] = value; } }; @@ -463,14 +464,30 @@ export class Explore extends React.PureComponent { onSubmit = () => { const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state; + // Keep table queries first since they need to return quickly if (showingTable && supportsTable) { - this.runTableQuery(); + this.runQueries( + 'Table', + { + format: 'table', + instant: true, + valueWithRefId: true, + }, + data => data[0] + ); } if (showingGraph && supportsGraph) { - this.runGraphQueries(); + this.runQueries( + 'Graph', + { + format: 'time_series', + instant: false, + }, + makeTimeSeriesList + ); } if (showingLogs && supportsLogs) { - this.runLogsQuery(); + this.runQueries('Logs', { format: 'logs' }); } this.saveState(); }; @@ -478,7 +495,8 @@ export class Explore extends React.PureComponent { buildQueryOptions(query: DataQuery, queryOptions: { format: string; hinting?: boolean; instant?: boolean }) { const { datasource, range } = this.state; const { interval, intervalMs } = getIntervals(range, datasource, this.el.offsetWidth); - const queries = [ + + const configuredQueries = [ { ...queryOptions, ...query, @@ -496,7 +514,7 @@ export class Explore extends React.PureComponent { interval, intervalMs, panelId, - queries, + targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. range: queryRange, }; } @@ -637,7 +655,7 @@ export class Explore extends React.PureComponent { }); } - async runGraphQueries() { + async runQueries(resultType: ResultType, queryOptions: any, resultGetter?: any) { const queries = [...this.modifiedQueries]; if (!hasNonEmptyQuery(queries)) { return; @@ -646,15 +664,12 @@ export class Explore extends React.PureComponent { const datasourceId = datasource.meta.id; // Run all queries concurrently queries.forEach(async (query, rowIndex) => { - const transaction = this.startQueryTransaction(query, rowIndex, 'Graph', { - format: 'time_series', - instant: false, - }); + const transaction = this.startQueryTransaction(query, rowIndex, resultType, queryOptions); try { const now = Date.now(); const res = await datasource.query(transaction.options); const latency = Date.now() - now; - const results = makeTimeSeriesList(res.data); + const results = resultGetter ? resultGetter(res.data) : res.data; this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); this.setState({ graphRange: transaction.options.range }); } catch (response) { @@ -663,54 +678,6 @@ export class Explore extends React.PureComponent { }); } - async runTableQuery() { - const queries = [...this.modifiedQueries]; - if (!hasNonEmptyQuery(queries)) { - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - 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); - } - }); - } - - async runLogsQuery() { - const queries = [...this.modifiedQueries]; - if (!hasNonEmptyQuery(queries)) { - return; - } - const { datasource } = this.state; - const datasourceId = datasource.meta.id; - // Run all queries concurrently - queries.forEach(async (query, rowIndex) => { - 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); - } - }); - } - cloneState(): ExploreState { // Copy state, but copy queries including modifications return { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 5f8a49b0bad..6171c662127 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -85,7 +85,7 @@ interface CascaderOption { disabled?: boolean; } -type PromQueryFieldProps = { +interface PromQueryFieldProps { datasource: any; error?: string | JSX.Element; initialQuery: DataQuery; @@ -95,7 +95,7 @@ type PromQueryFieldProps = { onClickHintFix?: (action: any) => void; onPressEnter?: () => void; onQueryChange?: (value: DataQuery, override?: boolean) => void; -}; +} interface PromQueryFieldState { metricsOptions: any[]; From bbaa5b63c8d49479a4b08ac2780c6ce649f706c6 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 22 Nov 2018 11:02:53 +0100 Subject: [PATCH 4/6] Fix history rendering for DataQuery --- public/app/core/utils/explore.test.ts | 45 ++++++++++++++++++- public/app/core/utils/explore.ts | 9 +++- .../logging/language_provider.test.ts | 37 ++++++++++++--- .../datasource/logging/language_provider.ts | 9 ++-- .../prometheus/language_provider.ts | 4 +- .../specs/language_provider.test.ts | 26 +++++++++++ 6 files changed, 116 insertions(+), 14 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index e12f7782a12..37d3d3bfac9 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -1,5 +1,13 @@ -import { DEFAULT_RANGE, serializeStateToUrlParam, parseUrlState } from './explore'; +import { + DEFAULT_RANGE, + serializeStateToUrlParam, + parseUrlState, + updateHistory, + clearHistory, + hasNonEmptyQuery, +} from './explore'; import { ExploreState } from 'app/types/explore'; +import store from 'app/core/store'; const DEFAULT_EXPLORE_STATE: ExploreState = { datasource: null, @@ -144,3 +152,38 @@ describe('state functions', () => { }); }); }); + +describe('updateHistory()', () => { + const datasourceId = 'myDatasource'; + const key = `grafana.explore.history.${datasourceId}`; + + beforeEach(() => { + clearHistory(datasourceId); + expect(store.exists(key)).toBeFalsy(); + }); + + test('should save history item to localStorage', () => { + const expected = [ + { + query: { refId: '1', expr: 'metric' }, + }, + ]; + expect(updateHistory([], datasourceId, [{ refId: '1', expr: 'metric' }])).toMatchObject(expected); + expect(store.exists(key)).toBeTruthy(); + expect(store.getObject(key)).toMatchObject(expected); + }); +}); + +describe('hasNonEmptyQuery', () => { + test('should return true if one query is non-empty', () => { + expect(hasNonEmptyQuery([{ refId: '1', key: '2', expr: 'foo' }])).toBeTruthy(); + }); + + test('should return false if query is empty', () => { + expect(hasNonEmptyQuery([{ refId: '1', key: '2' }])).toBeFalsy(); + }); + + test('should return false if no queries exist', () => { + expect(hasNonEmptyQuery([])).toBeFalsy(); + }); +}); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index f0d5c6f83d5..9ecc36a192f 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -66,6 +66,8 @@ export async function getExploreUrl( return url; } +const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; + export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { @@ -93,7 +95,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { datasource: state.datasourceName, - queries: state.initialQueries.map(({ key, refId, ...rest }) => rest), + queries: state.initialQueries.map(clearQueryKeys), range: state.range, }; if (compact) { @@ -182,3 +184,8 @@ export function updateHistory(history: HistoryItem[], datasourceId: string, quer store.setObject(historyKey, history); return history; } + +export function clearHistory(datasourceId: string) { + const historyKey = `grafana.explore.history.${datasourceId}`; + store.delete(historyKey); +} diff --git a/public/app/plugins/datasource/logging/language_provider.test.ts b/public/app/plugins/datasource/logging/language_provider.test.ts index e0844cf0c7a..79f696843bb 100644 --- a/public/app/plugins/datasource/logging/language_provider.test.ts +++ b/public/app/plugins/datasource/logging/language_provider.test.ts @@ -7,12 +7,37 @@ describe('Language completion provider', () => { metadataRequest: () => ({ data: { data: [] } }), }; - it('returns default suggestions on emtpty context', () => { - const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(0); + describe('empty query suggestions', () => { + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); + }); + + it('returns default suggestions with history on emtpty context when history was provided', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const history = [ + { + query: { refId: '1', expr: '{app="foo"}' }, + }, + ]; + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'History', + items: [ + { + label: '{app="foo"}', + }, + ], + }, + ]); + }); }); describe('label suggestions', () => { diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 253e2ba097f..eb47b3b1e27 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -7,6 +7,7 @@ import { LanguageProvider, TypeaheadInput, TypeaheadOutput, + HistoryItem, } from 'app/types/explore'; import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; import PromqlSyntax from 'app/plugins/datasource/prometheus/promql'; @@ -19,9 +20,9 @@ const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const wrapLabel = (label: string) => ({ label }); -export function addHistoryMetadata(item: CompletionItem, history: any[]): CompletionItem { +export function addHistoryMetadata(item: CompletionItem, history: HistoryItem[]): CompletionItem { const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF; - const historyForItem = history.filter(h => h.ts > cutoffTs && h.query === item.label); + const historyForItem = history.filter(h => h.ts > cutoffTs && (h.query.expr as string) === item.label); const count = historyForItem.length; const recent = historyForItem[0]; let hint = `Queried ${count} times in the last 24h.`; @@ -96,9 +97,9 @@ export default class LoggingLanguageProvider extends LanguageProvider { if (history && history.length > 0) { const historyItems = _.chain(history) - .uniqBy('query') + .uniqBy('query.expr') .take(HISTORY_ITEM_COUNT) - .map(h => h.query) + .map(h => h.query.expr) .map(wrapLabel) .map(item => addHistoryMetadata(item, history)) .value(); diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 6e6f461d341..5fd8fcebaaf 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -125,9 +125,9 @@ export default class PromQlLanguageProvider extends LanguageProvider { if (history && history.length > 0) { const historyItems = _.chain(history) - .uniqBy('query') + .uniqBy('query.expr') .take(HISTORY_ITEM_COUNT) - .map(h => h.query) + .map(h => h.query.expr) .map(wrapLabel) .map(item => addHistoryMetadata(item, history)) .value(); diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index bcb8cb34082..d3eb6de3087 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -36,6 +36,32 @@ describe('Language completion provider', () => { }, ]); }); + + it('returns default suggestions with history on emtpty context when history was provided', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const history = [ + { + query: { refId: '1', expr: 'metric' }, + }, + ]; + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }, { history }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'History', + items: [ + { + label: 'metric', + }, + ], + }, + { + label: 'Functions', + }, + ]); + }); }); describe('range suggestions', () => { From 21ee85b1e23db5f5067f00747a4c606a325e22a7 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 22 Nov 2018 11:14:46 +0100 Subject: [PATCH 5/6] Fix issue with deleting a query (empty string not updating) --- public/app/features/explore/Explore.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 0c4795d4d2f..73756cd861f 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -257,6 +257,14 @@ export class Explore extends React.PureComponent { }; onChangeQuery = (value: DataQuery, index: number, override?: boolean) => { + // Null value means reset + if (value === null) { + value = { ...generateQueryKeys(index) }; + } + + // Keep current value in local cache + this.modifiedQueries[index] = value; + if (override) { this.setState(state => { // Replace query row by injecting new key @@ -277,9 +285,6 @@ export class Explore extends React.PureComponent { queryTransactions: nextQueryTransactions, }; }, this.onSubmit); - } else if (value) { - // Keep current value in local cache - this.modifiedQueries[index] = value; } }; From 9e94d22401e77b3884a8a6f685ed1e63b53b86e1 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 22 Nov 2018 12:00:41 +0100 Subject: [PATCH 6/6] Added comments --- public/app/features/explore/Explore.tsx | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 73756cd861f..d4e9b689495 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -56,6 +56,35 @@ interface ExploreProps { * 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. + * + * QUERY HANDLING + * + * TLDR: to not re-render Explore during edits, query editing is not "controlled" + * in a React sense: values need to be pushed down via `initialQueries`, while + * edits travel up via `this.modifiedQueries`. + * + * By default the query rows start without prior state: `initialQueries` will + * contain one empty DataQuery. While the user modifies the DataQuery, the + * modifications are being tracked in `this.modifiedQueries`, which need to be + * used whenever a query is sent to the datasource to reflect what the user sees + * on the screen. Query rows can be initialized or reset using `initialQueries`, + * by giving the respective row a new key. This wipes the old row and its state. + * This property is also used to govern how many query rows there are (minimum 1). + * + * This flow makes sure that a query row can be arbitrarily complex without the + * fear of being wiped or re-initialized via props. The query row is free to keep + * its own state while the user edits or builds a query. Valid queries can be sent + * up to Explore via the `onChangeQuery` prop. + * + * DATASOURCE REQUESTS + * + * A click on Run Query creates transactions for all DataQueries for all expanded + * result viewers. New runs are discarding previous runs. Upon completion a transaction + * saves the result. The result viewers construct their data from the currently existing + * transactions. + * + * The result viewers determine some of the query options sent to the datasource, e.g., + * `format`, to indicate eventual transformations by the datasources' result transformers. */ export class Explore extends React.PureComponent { el: any;