diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 4252730338d..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, @@ -10,7 +18,7 @@ const DEFAULT_EXPLORE_STATE: ExploreState = { exploreDatasources: [], graphRange: DEFAULT_RANGE, history: [], - queries: [], + initialQueries: [], queryTransactions: [], range: DEFAULT_RANGE, showingGraph: true, @@ -33,10 +41,10 @@ describe('state functions', () => { 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","queries":%5B%7B"expr":"metric"%7D%5D,"range":%7B"from":"now-1h","to":"now"%7D%7D'; expect(parseUrlState(paramValue)).toMatchObject({ datasource: 'Local', - queries: [{ query: 'metric' }], + queries: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -45,10 +53,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' }], + queries: [{ expr: 'metric' }], range: { from: 'now-1h', to: 'now', @@ -66,18 +74,20 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - queries: [ + initialQueries: [ { - 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","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' ); }); @@ -89,17 +99,19 @@ describe('state functions', () => { from: 'now-5h', to: 'now', }, - queries: [ + initialQueries: [ { - 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 +125,14 @@ describe('state functions', () => { from: 'now - 5h', to: 'now', }, - queries: [ + initialQueries: [ { - 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 +140,50 @@ describe('state functions', () => { const parsed = parseUrlState(serialized); // Account for datasource vs datasourceName - const { datasource, ...rest } = parsed; - const sameState = { + const { datasource, queries, ...rest } = parsed; + const resultState = { ...rest, datasource: DEFAULT_EXPLORE_STATE.datasource, datasourceName: datasource, + initialQueries: queries, }; - expect(state).toMatchObject(sameState); + expect(state).toMatchObject(resultState); }); }); }); + +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 ecd11a495ad..9ecc36a192f 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. * @@ -23,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 @@ -57,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 { @@ -70,7 +81,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - const queries = parsed.slice(3).map(query => ({ query })); + const queries = parsed.slice(3); return { datasource, queries, range }; } return parsed; @@ -84,16 +95,97 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { export function serializeStateToUrlParam(state: ExploreState, compact?: boolean): string { const urlState: ExploreUrlState = { datasource: state.datasourceName, - queries: state.queries.map(q => ({ query: q.query })), + queries: state.initialQueries.map(clearQueryKeys), 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.queries]); } 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 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 ensureQueries(queries?: DataQuery[]): DataQuery[] { + if (queries && typeof queries === 'object' && queries.length > 0) { + return queries.map((query, i) => ({ ...query, ...generateQueryKeys(i) })); + } + return [{ ...generateQueryKeys() }]; +} + +/** + * A target is non-empty when it has keys other than refId and key. + */ +export function hasNonEmptyQuery(queries: DataQuery[]): boolean { + return queries.some(query => Object.keys(query).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) { + 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, queries: DataQuery[]): 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; +} + +export function clearHistory(datasourceId: string) { + const historyKey = `grafana.explore.history.${datasourceId}`; + store.delete(historyKey); +} diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 5d39992c4a2..d4e9b689495 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, + ensureQueries, + getIntervals, + generateKey, + generateQueryKeys, + hasNonEmptyQuery, + 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,49 @@ 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. + * + * 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; /** * 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[]; + modifiedQueries: DataQuery[]; /** * Local ID cache to compare requested vs selected datasource */ @@ -105,11 +101,11 @@ export class Explore extends React.PureComponent { constructor(props) { super(props); const splitState: ExploreState = props.splitState; - let initialQueries: Query[]; + let initialQueries: DataQuery[]; if (splitState) { // Split state overrides everything this.state = splitState; - initialQueries = splitState.queries; + initialQueries = splitState.initialQueries; } else { const { datasource, queries, range } = props.urlState as ExploreUrlState; initialQueries = ensureQueries(queries); @@ -122,8 +118,8 @@ export class Explore extends React.PureComponent { datasourceName: datasource, exploreDatasources: [], graphRange: initialRange, + initialQueries, history: [], - queries: initialQueries, queryTransactions: [], range: initialRange, showingGraph: true, @@ -135,7 +131,7 @@ export class Explore extends React.PureComponent { supportsTable: null, }; } - this.queryExpressions = initialQueries.map(q => q.query); + this.modifiedQueries = initialQueries.slice(); } async componentDidMount() { @@ -198,32 +194,26 @@ export class Explore extends React.PureComponent { } // Check if queries can be imported from previously selected datasource - let queryExpressions = this.queryExpressions; + let modifiedQueries = this.modifiedQueries; if (origin) { if (origin.meta.id === datasource.meta.id) { // Keep same queries if same type of datasource - queryExpressions = [...this.queryExpressions]; + modifiedQueries = [...this.modifiedQueries]; } 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 + modifiedQueries = await datasource.importQueries(this.modifiedQueries, origin.meta); } else { // Default is blank queries - queryExpressions = this.queryExpressions.map(() => ''); + modifiedQueries = ensureQueries(); } } // Reset edit state with new queries - const nextQueries = this.state.queries.map((q, i) => ({ - ...q, - key: generateQueryKey(i), - query: queryExpressions[i], + const nextQueries = this.state.initialQueries.map((q, i) => ({ + ...modifiedQueries[i], + ...generateQueryKeys(i), })); - this.queryExpressions = queryExpressions; + this.modifiedQueries = modifiedQueries; // Custom components const StartPage = datasource.pluginExports.ExploreStartPage; @@ -239,7 +229,7 @@ export class Explore extends React.PureComponent { supportsTable, datasourceLoading: false, datasourceName: datasource.name, - queries: nextQueries, + initialQueries: nextQueries, showingStartPage: Boolean(StartPage), }, () => { @@ -256,16 +246,15 @@ export class Explore extends React.PureComponent { onAddQueryRow = index => { // Local cache - this.queryExpressions[index + 1] = ''; + this.modifiedQueries[index + 1] = { ...generateQueryKeys(index + 1) }; this.setState(state => { - const { queries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; - // Add row by generating new react key const nextQueries = [ - ...queries.slice(0, index + 1), - { query: '', key: generateQueryKey() }, - ...queries.slice(index + 1), + ...initialQueries.slice(0, index + 1), + { ...this.modifiedQueries[index + 1] }, + ...initialQueries.slice(index + 1), ]; // Ongoing transactions need to update their row indices @@ -279,7 +268,7 @@ export class Explore extends React.PureComponent { return qt; }); - return { queries: nextQueries, queryTransactions: nextQueryTransactions }; + return { initialQueries: nextQueries, queryTransactions: nextQueryTransactions }; }); }; @@ -296,26 +285,32 @@ 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) => { + // Null value means reset + if (value === null) { + value = { ...generateQueryKeys(index) }; + } + // Keep current value in local cache - this.queryExpressions[index] = value; + this.modifiedQueries[index] = value; if (override) { this.setState(state => { - // Replace query row - const { queries, queryTransactions } = state; - const nextQuery: Query = { - key: generateQueryKey(index), - query: value, + // Replace query row by injecting new key + const { initialQueries, queryTransactions } = state; + const query: DataQuery = { + ...value, + ...generateQueryKeys(index), }; - const nextQueries = [...queries]; - nextQueries[index] = nextQuery; + const nextQueries = [...initialQueries]; + nextQueries[index] = query; + this.modifiedQueries = [...nextQueries]; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); return { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, this.onSubmit); @@ -330,10 +325,10 @@ export class Explore extends React.PureComponent { }; onClickClear = () => { - this.queryExpressions = ['']; + this.modifiedQueries = ensureQueries(); this.setState( prevState => ({ - queries: ensureQueries(), + initialQueries: [...this.modifiedQueries], queryTransactions: [], showingStartPage: Boolean(prevState.StartPage), }), @@ -387,10 +382,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 = (query: DataQuery) => { + const nextQueries = [{ ...query, ...generateQueryKeys() }]; + this.modifiedQueries = [...nextQueries]; + this.setState({ initialQueries: nextQueries }, this.onSubmit); }; onClickSplit = () => { @@ -430,28 +425,28 @@ export class Explore extends React.PureComponent { const preventSubmit = action.preventSubmit; this.setState( state => { - const { queries, queryTransactions } = state; - let nextQueries; + const { initialQueries, queryTransactions } = state; + let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextQueries = queries.map((q, i) => ({ - key: generateQueryKey(i), - query: datasource.modifyQuery(this.queryExpressions[i], action), + nextQueries = initialQueries.map((query, i) => ({ + ...datasource.modifyQuery(this.modifiedQueries[i], action), + ...generateQueryKeys(i), })); // Discard all ongoing transactions nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = queries.map((q, i) => { + nextQueries = initialQueries.map((query, 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.modifiedQueries[i], action), + ...generateQueryKeys(i), } - : q; + : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -464,9 +459,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.modifiedQueries = [...nextQueries]; return { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -478,22 +473,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.modifiedQueries = [...this.modifiedQueries.slice(0, index), ...this.modifiedQueries.slice(index + 1)]; this.setState( state => { - const { queries, queryTransactions } = state; - if (queries.length <= 1) { + const { initialQueries, queryTransactions } = state; + if (initialQueries.length <= 1) { return null; } // Remove row from react state - const nextQueries = [...queries.slice(0, index), ...queries.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 { - queries: nextQueries, + initialQueries: nextQueries, queryTransactions: nextQueryTransactions, }; }, @@ -503,52 +498,68 @@ 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(); }; - buildQueryOptions( - query: string, - rowIndex: number, - 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 configuredQueries = [ { - ...targetOptions, - // Target identifier is needed for table transformations - refId: rowIndex + 1, - expr: query, + ...queryOptions, + ...query, }, ]; // Clone range for query request const queryRange: RawTimeRange = { ...range }; + // 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 = queryOptions.format; + return { interval, intervalMs, - targets, + panelId, + targets: configuredQueries, // Datasources rely on DataQueries being passed under the targets key. range: queryRange, }; } - startQueryTransaction(query: string, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { - const queryOptions = this.buildQueryOptions(query, rowIndex, options); + startQueryTransaction(query: DataQuery, rowIndex: number, resultType: ResultType, options: any): QueryTransaction { + const queryOptions = this.buildQueryOptions(query, options); const transaction: QueryTransaction = { query, resultType, rowIndex, - id: generateQueryKey(), + id: generateKey(), // reusing for unique ID done: false, latency: 0, options: queryOptions, @@ -578,7 +589,7 @@ export class Explore extends React.PureComponent { transactionId: string, result: any, latency: number, - queries: string[], + queries: DataQuery[], datasourceId: string ) { const { datasource } = this.state; @@ -597,8 +608,8 @@ export class Explore extends React.PureComponent { } // Get query hints - let hints; - if (datasource.getQueryHints) { + let hints: QueryHint[]; + if (datasource.getQueryHints as QueryHintGetter) { hints = datasource.getQueryHints(transaction.query, result); } @@ -634,7 +645,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; } @@ -678,88 +689,25 @@ export class Explore extends React.PureComponent { }); } - async runGraphQueries() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { + async runQueries(resultType: ResultType, queryOptions: any, resultGetter?: any) { + 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) => { - 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); - } - }); - } - - async runTableQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { - 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); - } - }); - } - - async runLogsQuery() { - const queries = [...this.queryExpressions]; - if (!hasQuery(queries)) { - 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); + 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 = resultGetter ? resultGetter(res.data) : res.data; + this.completeQueryTransaction(transaction.id, results, latency, queries, datasourceId); + this.setState({ graphRange: transaction.options.range }); + } catch (response) { + this.failQueryTransaction(transaction.id, response, datasourceId); } }); } @@ -769,7 +717,7 @@ export class Explore extends React.PureComponent { return { ...this.state, queryTransactions: [], - queries: ensureQueries(this.queryExpressions.map(query => ({ query }))), + initialQueries: [...this.modifiedQueries], }; } @@ -789,7 +737,7 @@ export class Explore extends React.PureComponent { exploreDatasources, graphRange, history, - queries, + initialQueries, queryTransactions, range, showingGraph, @@ -903,7 +851,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; + initialQuery: 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, 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} - initialQuery={query} + initialQuery={initialQuery} history={history} onClickHintFix={this.onClickHintFix} onPressEnter={this.onPressEnter} @@ -116,19 +116,19 @@ class QueryRow extends PureComponent { type QueryRowsProps = QueryRowCommonProps & QueryRowEventHandlers & { - queries: Query[]; + initialQueries: DataQuery[]; }; export default class QueryRows extends PureComponent { render() { - const { className = '', queries, transactions, ...handlers } = this.props; + const { className = '', initialQueries, transactions, ...handlers } = this.props; return (
- {queries.map((q, index) => ( + {initialQueries.map((query, 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..5667bd9a20d 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; + initialQuery?: 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 { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const query = { + ...initialQuery, + expr: value, + }; + onQueryChange(query, override); } }; @@ -196,15 +201,15 @@ 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/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 00745d2eee8..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(); @@ -177,6 +178,10 @@ export default class LoggingLanguageProvider extends LanguageProvider { } async importPrometheusQuery(query: string): Promise { + if (!query) { + return ''; + } + // Consider only first selector in query const selectorMatch = query.match(selectorRegexp); if (selectorMatch) { @@ -192,7 +197,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..6171c662127 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'; @@ -87,13 +88,13 @@ interface CascaderOption { interface PromQueryFieldProps { datasource: any; error?: string | JSX.Element; + initialQuery: 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 { @@ -163,9 +164,13 @@ class PromQueryField extends React.PureComponent { // Send text change to parent - const { onQueryChange } = this.props; + const { initialQuery, onQueryChange } = this.props; if (onQueryChange) { - onQueryChange(value, override); + const query: DataQuery = { + ...initialQuery, + expr: value, + }; + onQueryChange(query, override); } }; @@ -230,7 +235,7 @@ 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..41f78ec7421 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 = {}; - if (targets && targets.length > 0) { - const queries = targets.map(t => ({ - query: this.templateSrv.replace(t.expr, {}, this.interpolateQueryExpr), - format: t.format, + getExploreState(queries: DataQuery[]): Partial { + let state: Partial = { datasource: this.name }; + if (queries && queries.length > 0) { + const expandedQueries = queries.map(query => ({ + ...query, + expr: this.templateSrv.replace(query.expr, {}, this.interpolateQueryExpr), })); state = { ...state, - queries, - datasource: this.name, + queries: expandedQueries, }; } return state; } - getQueryHints(query: string, result: any[]) { - return getQueryHints(query, result, this); + getQueryHints(query: DataQuery, result: any[]) { + return getQueryHints(query.expr, result, this); } loadRules() { @@ -454,28 +455,35 @@ export class PrometheusDatasource { }); } - modifyQuery(query: string, action: any): string { + modifyQuery(query: DataQuery, action: any): DataQuery { + let expression = query.expr || ''; switch (action.type) { case 'ADD_FILTER': { - return addLabelToQuery(query, action.key, action.value); + expression = addLabelToQuery(expression, action.key, action.value); + break; } case 'ADD_HISTOGRAM_QUANTILE': { - return `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': { - return `rate(${query}[5m])`; + expression = `rate(${expression}[5m])`; + break; } case 'ADD_SUM': { - return `sum(${query.trim()}) by ($1)`; + expression = `sum(${expression.trim()}) by ($1)`; + break; } case 'EXPAND_RULES': { if (action.mapping) { - return expandRecordingRules(query, action.mapping); + expression = expandRecordingRules(expression, action.mapping); } + break; } default: - return query; + break; } + return { ...query, expr: expression }; } getPrometheusTime(date, roundUp) { 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', () => { diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 662835633db..f80a485fc29 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; + query: 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 { + (query: DataQuery, results: any[], ...rest: any): QueryHint[]; +} + export interface QueryTransaction { id: string; done: boolean; @@ -137,7 +136,7 @@ export interface QueryTransaction { hints?: QueryHint[]; latency: number; options: any; - query: string; + query: DataQuery; result?: any; // Table model / Timeseries[] / Logs resultType: ResultType; rowIndex: number; @@ -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. - */ + initialQueries: DataQuery[]; queryTransactions: QueryTransaction[]; range: RawTimeRange; showingGraph: boolean; @@ -182,7 +173,7 @@ export interface ExploreState { export interface ExploreUrlState { datasource: string; - queries: Query[]; + queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; }