From 4fd1d92332e0054761f42dd48040531945a49e20 Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 22 Nov 2023 14:35:15 +0100 Subject: [PATCH] Loki: Remove relying on timeSrv.timeRange in LanguageProvider (#78450) * Loki: Allow setting of timeRange when using languageProvider functions * Loki: Use timerange where available for start * Loki: Use timerange where available for fetchLabels * Loki: Use timerange where available for fetchSeriesLabels * Loki: Use timerange where available for fetchLabelValues * Loki: Use timerange where available for getParserAndLabelKeys * Loki: Update and add tests for fetchLabels * Loki: Update and add tests for fetchSeriesLabels * Loki: Update and add tests for fetchSeries * Loki: Update and add tests for fetchLabelValues * Loki: Update and add tests for fetchLabelValues * Loki: Update and add tests for getParserAndLabelKeys * Update public/app/plugins/datasource/loki/LanguageProvider.test.ts Co-authored-by: Matias Chomicki * Update public/app/plugins/datasource/loki/LanguageProvider.test.ts Co-authored-by: Matias Chomicki * Not needing to use languageProvider.getDefaultTime in Monaco * Update comment * Update getDefaultTimeRange to be ptivate --------- Co-authored-by: Matias Chomicki --- kinds/dashboard/dashboard_kind.cue | 4 +- .../datasource/loki/LanguageProvider.test.ts | 147 ++++++++- .../datasource/loki/LanguageProvider.ts | 73 +++-- .../loki/components/LokiLabelBrowser.tsx | 23 +- .../loki/components/LokiQueryEditor.tsx | 2 + .../loki/components/LokiQueryField.test.tsx | 1 + .../loki/components/LokiQueryField.tsx | 7 +- .../monaco-query-field/MonacoQueryField.tsx | 13 +- .../MonacoQueryFieldProps.ts | 3 +- .../CompletionDataProvider.test.ts | 19 +- .../CompletionDataProvider.ts | 13 +- .../completions.test.ts | 40 ++- .../app/plugins/datasource/loki/datasource.ts | 14 +- .../app_plugin_developer_documentation.md | 26 +- .../components/LabelBrowserModal.tsx | 10 +- .../components/LokiQueryBuilder.test.tsx | 15 + .../components/LokiQueryBuilder.tsx | 279 +++++++++--------- .../components/LokiQueryBuilderContainer.tsx | 6 +- 18 files changed, 470 insertions(+), 225 deletions(-) diff --git a/kinds/dashboard/dashboard_kind.cue b/kinds/dashboard/dashboard_kind.cue index 4964b029a54..51ba29bc246 100644 --- a/kinds/dashboard/dashboard_kind.cue +++ b/kinds/dashboard/dashboard_kind.cue @@ -243,8 +243,8 @@ lineage: schemas: [{ // `4`: Numerical DESC // `5`: Alphabetical Case Insensitive ASC // `6`: Alphabetical Case Insensitive DESC - // `7`: Natural ASC - // `8`: Natural DESC + // `7`: Natural ASC + // `8`: Natural DESC #VariableSort: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 @cuetsy(kind="enum",memberNames="disabled|alphabeticalAsc|alphabeticalDesc|numericalAsc|numericalDesc|alphabeticalCaseInsensitiveAsc|alphabeticalCaseInsensitiveDesc|naturalAsc|naturalDesc") // Ref to a DataSource instance diff --git a/public/app/plugins/datasource/loki/LanguageProvider.test.ts b/public/app/plugins/datasource/loki/LanguageProvider.test.ts index 040e2d48e2d..5041e5193d8 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.test.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.test.ts @@ -1,4 +1,4 @@ -import { AbstractLabelOperator, DataFrame } from '@grafana/data'; +import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange } from '@grafana/data'; import LanguageProvider from './LanguageProvider'; import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; @@ -24,6 +24,26 @@ jest.mock('app/store/store', () => ({ }, })); +const mockTimeRange = { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, +}; +jest.mock('@grafana/data', () => ({ + ...jest.requireActual('@grafana/data'), + getDefaultTimeRange: jest.fn().mockReturnValue({ + from: 0, + to: 1, + raw: { + from: 0, + to: 1, + }, + }), +})); + describe('Language completion provider', () => { describe('fetchSeries', () => { it('should use match[] parameter', () => { @@ -38,6 +58,25 @@ describe('Language completion provider', () => { start: 1560153109000, }); }); + + it('should use provided time range', () => { + const datasource = setup({}); + datasource.getTimeRangeParams = jest + .fn() + .mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() })); + const languageProvider = new LanguageProvider(datasource); + languageProvider.request = jest.fn(); + languageProvider.fetchSeries('{job="grafana"}', { timeRange: mockTimeRange }); + // time range was passed to getTimeRangeParams + expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange); + // time range was passed to request + expect(languageProvider.request).toHaveBeenCalled(); + expect(languageProvider.request).toHaveBeenCalledWith('series', { + end: 1546380000000, + 'match[]': '{job="grafana"}', + start: 1546372800000, + }); + }); }); describe('fetchSeriesLabels', () => { @@ -59,6 +98,26 @@ describe('Language completion provider', () => { start: 0, }); }); + + it('should be called with time range params if provided', () => { + const datasource = setup({}); + datasource.getTimeRangeParams = jest + .fn() + .mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() })); + const languageProvider = new LanguageProvider(datasource); + languageProvider.request = jest.fn().mockResolvedValue([]); + languageProvider.fetchSeriesLabels('stream', { timeRange: mockTimeRange }); + // time range was passed to getTimeRangeParams + expect(datasource.getTimeRangeParams).toHaveBeenCalled(); + expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange); + // time range was passed to request + expect(languageProvider.request).toHaveBeenCalled(); + expect(languageProvider.request).toHaveBeenCalledWith('series', { + end: 1546380000000, + 'match[]': 'stream', + start: 1546372800000, + }); + }); }); describe('label values', () => { @@ -87,6 +146,41 @@ describe('Language completion provider', () => { expect(labelValues).toEqual(['label1_val1', 'label1_val2']); }); + it('fetch label with options.timeRange when provided and values is not cached', async () => { + const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] }); + datasource.getTimeRangeParams = jest + .fn() + .mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() })); + const languageProvider = new LanguageProvider(datasource); + languageProvider.request = jest.fn().mockResolvedValue([]); + languageProvider.fetchLabelValues('testKey', { timeRange: mockTimeRange }); + // time range was passed to getTimeRangeParams + expect(datasource.getTimeRangeParams).toHaveBeenCalled(); + expect(datasource.getTimeRangeParams).toHaveBeenCalledWith(mockTimeRange); + // time range was passed to request + expect(languageProvider.request).toHaveBeenCalled(); + expect(languageProvider.request).toHaveBeenCalledWith('label/testKey/values', { + end: 1546380000000, + start: 1546372800000, + }); + }); + + it('uses default time range if fetch label does not receive options.timeRange', async () => { + const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] }); + datasource.getTimeRangeParams = jest + .fn() + .mockImplementation((range: TimeRange) => ({ start: range.from.valueOf(), end: range.to.valueOf() })); + const languageProvider = new LanguageProvider(datasource); + languageProvider.request = jest.fn().mockResolvedValue([]); + languageProvider.fetchLabelValues('testKey'); + expect(getDefaultTimeRange).toHaveBeenCalled(); + expect(languageProvider.request).toHaveBeenCalled(); + expect(languageProvider.request).toHaveBeenCalledWith('label/testKey/values', { + end: 1, + start: 0, + }); + }); + it('should return cached values', async () => { const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] }); const provider = await getLanguageProvider(datasource); @@ -149,7 +243,7 @@ describe('Language completion provider', () => { describe('Request URL', () => { it('should contain range params', async () => { const datasourceWithLabels = setup({ other: [] }); - const rangeParams = datasourceWithLabels.getTimeRangeParams(); + const rangeParams = datasourceWithLabels.getTimeRangeParams(mockTimeRange); const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest'); const instance = new LanguageProvider(datasourceWithLabels); @@ -191,6 +285,16 @@ describe('fetchLabels', () => { await instance.fetchLabels(); expect(instance.labelKeys).toEqual([]); }); + + it('should use time range param', async () => { + const datasourceWithLabels = setup({}); + datasourceWithLabels.languageProvider.request = jest.fn(); + + const instance = new LanguageProvider(datasourceWithLabels); + instance.request = jest.fn(); + await instance.fetchLabels({ timeRange: mockTimeRange }); + expect(instance.request).toBeCalledWith('labels', datasourceWithLabels.getTimeRangeParams(mockTimeRange)); + }); }); describe('Query imports', () => { @@ -286,11 +390,14 @@ describe('Query imports', () => { hasLogfmt: false, hasPack: false, }); - expect(datasource.getDataSamples).toHaveBeenCalledWith({ - expr: '{place="luna"}', - maxLines: DEFAULT_MAX_LINES_SAMPLE, - refId: 'data-samples', - }); + expect(datasource.getDataSamples).toHaveBeenCalledWith( + { + expr: '{place="luna"}', + maxLines: DEFAULT_MAX_LINES_SAMPLE, + refId: 'data-samples', + }, + undefined + ); }); it('calls dataSample with correctly set sampleSize', async () => { @@ -303,11 +410,27 @@ describe('Query imports', () => { hasLogfmt: false, hasPack: false, }); - expect(datasource.getDataSamples).toHaveBeenCalledWith({ - expr: '{place="luna"}', - maxLines: 5, - refId: 'data-samples', - }); + expect(datasource.getDataSamples).toHaveBeenCalledWith( + { + expr: '{place="luna"}', + maxLines: 5, + refId: 'data-samples', + }, + undefined + ); + }); + + it('calls dataSample with correctly set time range', async () => { + jest.spyOn(datasource, 'getDataSamples').mockResolvedValue([]); + languageProvider.getParserAndLabelKeys('{place="luna"}', { timeRange: mockTimeRange }); + expect(datasource.getDataSamples).toHaveBeenCalledWith( + { + expr: '{place="luna"}', + maxLines: 10, + refId: 'data-samples', + }, + mockTimeRange + ); }); }); }); diff --git a/public/app/plugins/datasource/loki/LanguageProvider.ts b/public/app/plugins/datasource/loki/LanguageProvider.ts index 7cf74f98113..5b89d5abeab 100644 --- a/public/app/plugins/datasource/loki/LanguageProvider.ts +++ b/public/app/plugins/datasource/loki/LanguageProvider.ts @@ -1,7 +1,7 @@ import { LRUCache } from 'lru-cache'; import Prism from 'prismjs'; -import { LanguageProvider, AbstractQuery, KeyValue } from '@grafana/data'; +import { LanguageProvider, AbstractQuery, KeyValue, getDefaultTimeRange, TimeRange } from '@grafana/data'; import { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils'; import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource'; @@ -50,9 +50,10 @@ export default class LokiLanguageProvider extends LanguageProvider { /** * Initialize the language provider by fetching set of labels. */ - start = () => { + start = (timeRange?: TimeRange) => { + const range = timeRange ?? this.getDefaultTimeRange(); if (!this.startTask) { - this.startTask = this.fetchLabels().then(() => { + this.startTask = this.fetchLabels({ timeRange: range }).then(() => { this.started = true; return []; }); @@ -101,12 +102,15 @@ export default class LokiLanguageProvider extends LanguageProvider { * This asynchronous function returns all available label keys from the data source. * It returns a promise that resolves to an array of strings containing the label keys. * + * @param options - (Optional) An object containing additional options - currently only time range. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * @returns A promise containing an array of label keys. * @throws An error if the fetch operation fails. */ - async fetchLabels(): Promise { + async fetchLabels(options?: { timeRange?: TimeRange }): Promise { const url = 'labels'; - const timeRange = this.datasource.getTimeRangeParams(); + const range = options?.timeRange ?? this.getDefaultTimeRange(); + const timeRange = this.datasource.getTimeRangeParams(range); const res = await this.request(url, timeRange); if (Array.isArray(res)) { @@ -128,13 +132,19 @@ export default class LokiLanguageProvider extends LanguageProvider { * It returns a promise that resolves to a record mapping label names to their corresponding values. * * @param streamSelector - The stream selector for which you want to retrieve labels. + * @param options - (Optional) An object containing additional options - currently only time range. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * @returns A promise containing a record of label names and their values. * @throws An error if the fetch operation fails. */ - fetchSeriesLabels = async (streamSelector: string): Promise> => { + fetchSeriesLabels = async ( + streamSelector: string, + options?: { timeRange?: TimeRange } + ): Promise> => { const interpolatedMatch = this.datasource.interpolateString(streamSelector); const url = 'series'; - const { start, end } = this.datasource.getTimeRangeParams(); + const range = options?.timeRange ?? this.getDefaultTimeRange(); + const { start, end } = this.datasource.getTimeRangeParams(range); const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch); let value = this.seriesCache.get(cacheKey); @@ -151,10 +161,15 @@ export default class LokiLanguageProvider extends LanguageProvider { /** * Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels. * @param match + * @param streamSelector - The stream selector for which you want to retrieve labels. + * @param options - (Optional) An object containing additional options. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. + * @returns A promise containing array with records of label names and their value. */ - fetchSeries = async (match: string): Promise>> => { + fetchSeries = async (match: string, options?: { timeRange?: TimeRange }): Promise>> => { const url = 'series'; - const { start, end } = this.datasource.getTimeRangeParams(); + const range = options?.timeRange ?? this.getDefaultTimeRange(); + const { start, end } = this.datasource.getTimeRangeParams(range); const params = { 'match[]': match, start, end }; return await this.request(url, params); }; @@ -179,19 +194,24 @@ export default class LokiLanguageProvider extends LanguageProvider { * It returns a promise that resolves to an array of strings containing the label values. * * @param labelName - The name of the label for which you want to retrieve values. - * @param options - (Optional) An object containing additional options - currently only stream selector. + * @param options - (Optional) An object containing additional options. * @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label values. If not provided, the default time range is used. * @returns A promise containing an array of label values. * @throws An error if the fetch operation fails. */ - async fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise { + async fetchLabelValues( + labelName: string, + options?: { streamSelector?: string; timeRange?: TimeRange } + ): Promise { const label = encodeURIComponent(this.datasource.interpolateString(labelName)); const streamParam = options?.streamSelector ? encodeURIComponent(this.datasource.interpolateString(options.streamSelector)) : undefined; const url = `label/${label}/values`; - const rangeParams = this.datasource.getTimeRangeParams(); + const range = options?.timeRange ?? this.getDefaultTimeRange(); + const rangeParams = this.datasource.getTimeRangeParams(range); const { start, end } = rangeParams; const params: KeyValue = { start, end }; let paramCacheKey = label; @@ -230,21 +250,25 @@ export default class LokiLanguageProvider extends LanguageProvider { * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * * @param streamSelector - The selector for the log stream you want to analyze. - * @param {Object} [options] - Optional parameters. - * @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. + * @param options - (Optional) An object containing additional options. + * @param options.maxLines - (Optional) The number of log lines requested when determining parsers and label keys. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * Smaller maxLines is recommended for improved query performance. The default count is 10. * @returns A promise containing an object with parser and label key information. * @throws An error if the fetch operation fails. */ async getParserAndLabelKeys( streamSelector: string, - options?: { maxLines?: number } + options?: { maxLines?: number; timeRange?: TimeRange } ): Promise { - const series = await this.datasource.getDataSamples({ - expr: streamSelector, - refId: 'data-samples', - maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE, - }); + const series = await this.datasource.getDataSamples( + { + expr: streamSelector, + refId: 'data-samples', + maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE, + }, + options?.timeRange + ); if (!series.length) { return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false }; @@ -260,4 +284,13 @@ export default class LokiLanguageProvider extends LanguageProvider { hasLogfmt, }; } + + /** + * Get the default time range + * + * @returns {TimeRange} The default time range + */ + private getDefaultTimeRange(): TimeRange { + return getDefaultTimeRange(); + } } diff --git a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx index cbb72d70d3d..bbe205f93fa 100644 --- a/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx +++ b/public/app/plugins/datasource/loki/components/LokiLabelBrowser.tsx @@ -3,7 +3,7 @@ import { sortBy } from 'lodash'; import React, { ChangeEvent } from 'react'; import { FixedSizeList } from 'react-window'; -import { CoreApp, GrafanaTheme2 } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { Button, @@ -17,7 +17,6 @@ import { fuzzyMatch, } from '@grafana/ui'; -import PromQlLanguageProvider from '../../prometheus/language_provider'; import LokiLanguageProvider from '../LanguageProvider'; import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../languageUtils'; @@ -28,12 +27,12 @@ const MAX_AUTO_SELECT = 4; const EMPTY_SELECTOR = '{}'; export interface BrowserProps { - // TODO #33976: Is it possible to use a common interface here? For example: LabelsLanguageProvider - languageProvider: LokiLanguageProvider | PromQlLanguageProvider; + languageProvider: LokiLanguageProvider; onChange: (selector: string) => void; theme: GrafanaTheme2; app?: CoreApp; autoSelect?: number; + timeRange?: TimeRange; hide?: () => void; lastUsedLabels: string[]; storeLastUsedLabels: (labels: string[]) => void; @@ -283,10 +282,10 @@ export class UnthemedLokiLabelBrowser extends React.Component { + languageProvider.start(timeRange).then(() => { let rawLabels: string[] = languageProvider.getLabelKeys(); if (rawLabels.length > MAX_LABEL_COUNT) { const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`; @@ -347,10 +346,10 @@ export class UnthemedLokiLabelBrowser extends React.Component((props) => { onClose={() => setLabelBrowserVisible(false)} onChange={onChangeInternal} onRunQuery={onRunQuery} + timeRange={timeRange} /> @@ -196,6 +197,7 @@ export const LokiQueryEditor = React.memo((props) => { onChange={onChangeInternal} onRunQuery={props.onRunQuery} showExplain={explain} + timeRange={timeRange} /> )} { rerender(); expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1); + expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledWith({ timeRange: newRange }); }); it('does not refreshes metrics when time range change by less than 1 minute', async () => { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 11fcfa8c9bf..237e2bff418 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -29,7 +29,7 @@ export class LokiQueryField extends React.PureComponent diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx index 1ce1c8ed76a..d39231aa477 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryField.tsx @@ -100,7 +100,16 @@ const getStyles = (theme: GrafanaTheme2, placeholder: string) => { }; }; -const MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasource, placeholder, onChange }: Props) => { +const MonacoQueryField = ({ + history, + onBlur, + onRunQuery, + initialValue, + datasource, + placeholder, + onChange, + timeRange, +}: Props) => { const id = uuidv4(); // we need only one instance of `overrideServices` during the lifetime of the react component const overrideServicesRef = useRef(getOverrideServices()); @@ -203,7 +212,7 @@ const MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasourc onTypeDebounced(query); monaco.editor.setModelMarkers(model, 'owner', markers); }); - const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef); + const dataProvider = new CompletionDataProvider(langProviderRef.current, historyRef, timeRange); const completionProvider = getCompletionProvider(monaco, dataProvider); // completion-providers in monaco are not registered directly to editor-instances, diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryFieldProps.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryFieldProps.ts index d52c126ceed..ab66b3b3d86 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryFieldProps.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/MonacoQueryFieldProps.ts @@ -1,4 +1,4 @@ -import { HistoryItem } from '@grafana/data'; +import { HistoryItem, TimeRange } from '@grafana/data'; import { LokiDatasource } from '../../datasource'; import { LokiQuery } from '../../types'; @@ -15,4 +15,5 @@ export type Props = { placeholder: string; datasource: LokiDatasource; onChange: (query: string) => void; + timeRange?: TimeRange; }; diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts index c08fbfc812e..8d99d46a2f9 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.test.ts @@ -1,4 +1,4 @@ -import { HistoryItem } from '@grafana/data'; +import { HistoryItem, dateTime } from '@grafana/data'; import LokiLanguageProvider from '../../../LanguageProvider'; import { LokiDatasource } from '../../../datasource'; @@ -56,6 +56,15 @@ const parserAndLabelKeys = { hasPack: false, }; +const mockTimeRange = { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, +}; + describe('CompletionDataProvider', () => { let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource; let historyRef: { current: Array> } = { current: [] }; @@ -63,7 +72,8 @@ describe('CompletionDataProvider', () => { datasource = createLokiDatasource(); languageProvider = new LokiLanguageProvider(datasource); historyRef.current = history; - completionProvider = new CompletionDataProvider(languageProvider, historyRef); + + completionProvider = new CompletionDataProvider(languageProvider, historyRef, mockTimeRange); jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys); jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues); @@ -163,6 +173,11 @@ describe('CompletionDataProvider', () => { expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledTimes(4); }); + test('Uses time range from CompletionProvider', async () => { + completionProvider.getParserAndLabelKeys(''); + expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledWith('', { timeRange: mockTimeRange }); + }); + test('Returns the expected series labels', async () => { expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels); }); diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts index f8934ff1fc2..83bb111de1f 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/CompletionDataProvider.ts @@ -1,6 +1,6 @@ import { chain } from 'lodash'; -import { HistoryItem } from '@grafana/data'; +import { HistoryItem, TimeRange } from '@grafana/data'; import { escapeLabelValueInExactSelector } from 'app/plugins/datasource/prometheus/language_utils'; import LanguageProvider from '../../../LanguageProvider'; @@ -15,7 +15,8 @@ interface HistoryRef { export class CompletionDataProvider { constructor( private languageProvider: LanguageProvider, - private historyRef: HistoryRef = { current: [] } + private historyRef: HistoryRef = { current: [] }, + private timeRange: TimeRange | undefined ) { this.queryToLabelKeysCache = new Map(); } @@ -51,7 +52,7 @@ export class CompletionDataProvider { async getLabelValues(labelName: string, otherLabels: Label[]) { if (otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint - return await this.languageProvider.fetchLabelValues(labelName); + return await this.languageProvider.fetchLabelValues(labelName, { timeRange: this.timeRange }); } const data = await this.getSeriesLabels(otherLabels); @@ -82,7 +83,7 @@ export class CompletionDataProvider { this.queryToLabelKeysCache.delete(firstKey); } // Fetch a fresh result from the backend - const labelKeys = await this.languageProvider.getParserAndLabelKeys(logQuery); + const labelKeys = await this.languageProvider.getParserAndLabelKeys(logQuery, { timeRange: this.timeRange }); // Add the result to the cache this.queryToLabelKeysCache.set(logQuery, labelKeys); return labelKeys; @@ -90,6 +91,8 @@ export class CompletionDataProvider { } async getSeriesLabels(labels: Label[]) { - return await this.languageProvider.fetchSeriesLabels(this.buildSelector(labels)).then((data) => data ?? {}); + return await this.languageProvider + .fetchSeriesLabels(this.buildSelector(labels), { timeRange: this.timeRange }) + .then((data) => data ?? {}); } } diff --git a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts index 56f691e9f8a..49606063a40 100644 --- a/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts +++ b/public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -1,3 +1,4 @@ +import { dateTime } from '@grafana/data'; import { Monaco, monacoTypes } from '@grafana/ui/src'; import LokiLanguageProvider from '../../../LanguageProvider'; @@ -31,6 +32,15 @@ const history = [ }, ]; +const mockTimeRange = { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, +}; + const labelNames = ['place', 'source']; const labelValues = ['moon', 'luna', 'server\\1']; // Source is duplicated to test handling duplicated labels @@ -195,9 +205,13 @@ describe('getCompletions', () => { beforeEach(() => { datasource = createLokiDatasource(); languageProvider = new LokiLanguageProvider(datasource); - completionProvider = new CompletionDataProvider(languageProvider, { - current: history, - }); + completionProvider = new CompletionDataProvider( + languageProvider, + { + current: history, + }, + mockTimeRange + ); jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames); jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues); @@ -433,9 +447,13 @@ describe('getAfterSelectorCompletions', () => { beforeEach(() => { datasource = createLokiDatasource(); languageProvider = new LokiLanguageProvider(datasource); - completionProvider = new CompletionDataProvider(languageProvider, { - current: history, - }); + completionProvider = new CompletionDataProvider( + languageProvider, + { + current: history, + }, + mockTimeRange + ); jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ extractedLabelKeys: ['abc', 'def'], @@ -524,9 +542,13 @@ describe('IN_LOGFMT completions', () => { beforeEach(() => { datasource = createLokiDatasource(); languageProvider = new LokiLanguageProvider(datasource); - completionProvider = new CompletionDataProvider(languageProvider, { - current: history, - }); + completionProvider = new CompletionDataProvider( + languageProvider, + { + current: history, + }, + mockTimeRange + ); jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({ extractedLabelKeys: ['label1', 'label2'], diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index dfdb4028a56..dd3426bb24b 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -458,12 +458,12 @@ export class LokiDatasource } /** - * Retrieve the current time range as Loki parameters. + * Given a time range, returns it as Loki parameters. * @returns An object containing the start and end times in nanoseconds since the Unix epoch. */ - getTimeRangeParams() { - const timeRange = this.getTimeRange(); - return { start: timeRange.from.valueOf() * NS_IN_MS, end: timeRange.to.valueOf() * NS_IN_MS }; + getTimeRangeParams(timeRange?: TimeRange) { + const range = timeRange ?? this.getTimeRange(); + return { start: range.from.valueOf() * NS_IN_MS, end: range.to.valueOf() * NS_IN_MS }; } /** @@ -758,7 +758,7 @@ export class LokiDatasource * Currently, it works for logs data only. * @returns A Promise that resolves to an array of DataFrames containing data samples. */ - async getDataSamples(query: LokiQuery): Promise { + async getDataSamples(query: LokiQuery, timeRange?: TimeRange): Promise { // Currently works only for logs sample if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) { return []; @@ -772,8 +772,8 @@ export class LokiDatasource supportingQueryType: SupportingQueryType.DataSample, }; - const timeRange = this.getTimeRange(); - const request = makeRequest(lokiLogsQuery, timeRange, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true); + const range = timeRange ?? this.getTimeRange(); + const request = makeRequest(lokiLogsQuery, range, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true); return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data)))); } diff --git a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md index 7dc09b6f771..2d50b981916 100644 --- a/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md +++ b/public/app/plugins/datasource/loki/docs/app_plugin_developer_documentation.md @@ -29,10 +29,12 @@ We strongly advise using these recommended methods instead of direct API calls b * This asynchronous function is designed to retrieve all available label keys from the data source. * It returns a promise that resolves to an array of strings containing the label keys. * + * @param options - (Optional) An object containing additional options - currently only time range. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * @returns A promise containing an array of label keys. * @throws An error if the fetch operation fails. */ -async function fetchLabels(): Promise; +async function fetchLabels(options?: { timeRange?: TimeRange }): Promise; /** * Example usage: @@ -58,12 +60,16 @@ The `datasource.languageProvider.fetchLabelValues()` method is designed for fetc * It returns a promise that resolves to an array of strings containing the label values. * * @param labelName - The name of the label for which you want to retrieve values. - * @param options - (Optional) An object containing additional options - currently only stream selector. + * @param options - (Optional) An object containing additional options. * @param options.streamSelector - (Optional) The stream selector to filter label values. If not provided, all label values are fetched. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label values. If not provided, the default time range is used. * @returns A promise containing an array of label values. * @throws An error if the fetch operation fails. */ -async function fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise; +async function fetchLabelValues( + labelName: string, + options?: { streamSelector?: string; timeRange?: TimeRange } +): Promise; /** * Example usage without stream selector: @@ -103,10 +109,15 @@ try { * It returns a promise that resolves to a record mapping label names to their corresponding values. * * @param streamSelector - The stream selector for which you want to retrieve labels. + * @param options - (Optional) An object containing additional options - currently only time range. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * @returns A promise containing a record of label names and their values. * @throws An error if the fetch operation fails. */ -async function fetchSeriesLabels(streamSelector: string): Promise>; +async function fetchSeriesLabels( + streamSelector: string, + options?: { timeRange?: TimeRange } +): Promise>; /** * Example usage: @@ -138,15 +149,16 @@ try { * - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data. * * @param streamSelector - The selector for the log stream you want to analyze. - * @param {Object} [options] - Optional parameters. - * @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys. + * @param options - (Optional) An object containing additional options. + * @param options.maxLines - (Optional) The number of log lines requested when determining parsers and label keys. + * @param options.timeRange - (Optional) The time range for which you want to retrieve label keys. If not provided, the default time range is used. * Smaller maxLines is recommended for improved query performance. The default count is 10. * @returns A promise containing an object with parser and label key information. * @throws An error if the fetch operation fails. */ async function getParserAndLabelKeys( streamSelector: string, - options?: { maxLines?: number } + options?: { maxLines?: number; timeRange?: TimeRange } ): Promise<{ extractedLabelKeys: string[]; hasJSON: boolean; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx index 35dc7595f25..b232f75cd73 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LabelBrowserModal.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/css'; import React, { useState, useEffect } from 'react'; -import { CoreApp, GrafanaTheme2 } from '@grafana/data'; +import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data'; import { reportInteraction } from '@grafana/runtime'; import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui'; import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider'; @@ -15,13 +15,14 @@ export interface Props { datasource: LokiDatasource; query: LokiQuery; app?: CoreApp; + timeRange?: TimeRange; onClose: () => void; onChange: (query: LokiQuery) => void; onRunQuery: () => void; } export const LabelBrowserModal = (props: Props) => { - const { isOpen, onClose, datasource, app } = props; + const { isOpen, onClose, datasource, app, timeRange } = props; const [labelsLoaded, setLabelsLoaded] = useState(false); const [hasLogLabels, setHasLogLabels] = useState(false); const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels'; @@ -33,11 +34,11 @@ export const LabelBrowserModal = (props: Props) => { return; } - datasource.languageProvider.fetchLabels().then((labels) => { + datasource.languageProvider.fetchLabels({ timeRange }).then((labels) => { setLabelsLoaded(true); setHasLogLabels(labels.length > 0); }); - }, [datasource, isOpen]); + }, [datasource, isOpen, timeRange]); const changeQuery = (value: string) => { const { query, onChange, onRunQuery } = props; @@ -74,6 +75,7 @@ export const LabelBrowserModal = (props: Props) => { storeLastUsedLabels={onLastUsedLabelsSave} deleteLastUsedLabels={onLastUsedLabelsDelete} app={app} + timeRange={timeRange} /> ); }} diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx index c0f9f7811e6..c98cbeffc77 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.test.tsx @@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { getSelectParent } from 'test/helpers/selectOptionInTest'; +import { dateTime } from '@grafana/data'; + import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters'; import { createLokiDatasource } from '../../mocks'; import { LokiOperationId, LokiVisualQuery } from '../types'; @@ -15,6 +17,15 @@ const defaultQuery: LokiVisualQuery = { operations: [], }; +const mockTimeRange = { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + raw: { + from: dateTime(1546372800000), + to: dateTime(1546380000000), + }, +}; + const createDefaultProps = () => { const datasource = createLokiDatasource(); @@ -23,6 +34,7 @@ const createDefaultProps = () => { onRunQuery: () => {}, onChange: () => {}, showExplain: false, + timeRange: mockTimeRange, }; return props; @@ -39,6 +51,9 @@ describe('LokiQueryBuilder', () => { const labels = screen.getByText(/Label filters/); const selects = getAllByRole(getSelectParent(labels)!, 'combobox'); await userEvent.click(selects[3]); + expect(props.datasource.languageProvider.fetchSeriesLabels).toBeCalledWith('{baz="bar"}', { + timeRange: mockTimeRange, + }); await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument()); }); diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx index 0e92e3cd358..0f1b76e1c28 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue } from '@grafana/data'; +import { DataSourceApi, getDefaultTimeRange, LoadingState, PanelData, SelectableValue, TimeRange } from '@grafana/data'; import { EditorRow } from '@grafana/experimental'; import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters'; import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox'; @@ -29,149 +29,152 @@ export interface Props { query: LokiVisualQuery; datasource: LokiDatasource; showExplain: boolean; + timeRange?: TimeRange; onChange: (update: LokiVisualQuery) => void; onRunQuery: () => void; } -export const LokiQueryBuilder = React.memo(({ datasource, query, onChange, onRunQuery, showExplain }) => { - const [sampleData, setSampleData] = useState(); - const [highlightedOp, setHighlightedOp] = useState(undefined); +export const LokiQueryBuilder = React.memo( + ({ datasource, query, onChange, onRunQuery, showExplain, timeRange }) => { + const [sampleData, setSampleData] = useState(); + const [highlightedOp, setHighlightedOp] = useState(undefined); - const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { - onChange({ ...query, labels }); - }; - - const withTemplateVariableOptions = async (optionsPromise: Promise): Promise => { - const options = await optionsPromise; - return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value })); - }; - - const onGetLabelNames = async (forLabel: Partial): Promise => { - const labelsToConsider = query.labels.filter((x) => x !== forLabel); - - if (labelsToConsider.length === 0) { - return await datasource.languageProvider.fetchLabels(); - } - - const expr = lokiQueryModeller.renderLabels(labelsToConsider); - const series = await datasource.languageProvider.fetchSeriesLabels(expr); - const labelsNamesToConsider = labelsToConsider.map((l) => l.label); - - const labelNames = Object.keys(series) - // Filter out label names that are already selected - .filter((name) => !labelsNamesToConsider.includes(name)) - .sort(); - - return labelNames; - }; - - const onGetLabelValues = async (forLabel: Partial) => { - if (!forLabel.label) { - return []; - } - - let values; - const labelsToConsider = query.labels.filter((x) => x !== forLabel); - if (labelsToConsider.length === 0) { - values = await datasource.languageProvider.fetchLabelValues(forLabel.label); - } else { - const expr = lokiQueryModeller.renderLabels(labelsToConsider); - const result = await datasource.languageProvider.fetchSeriesLabels(expr); - values = result[datasource.interpolateString(forLabel.label)]; - } - - return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return - }; - - const labelFilterRequired: boolean = useMemo(() => { - const { labels, operations: op } = query; - if (!labels.length && op.length) { - // Filter is required when operations are present (empty line contains operation is exception) - if (op.length === 1 && op[0].id === LokiOperationId.LineContains && op[0].params[0] === '') { - return false; - } - return true; - } - return false; - }, [query]); - - useEffect(() => { - const onGetSampleData = async () => { - const lokiQuery = { expr: lokiQueryModeller.renderQuery(query), refId: 'data-samples' }; - const series = await datasource.getDataSamples(lokiQuery); - const sampleData = { series, state: LoadingState.Done, timeRange: getDefaultTimeRange() }; - setSampleData(sampleData); + const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => { + onChange({ ...query, labels }); }; - onGetSampleData().catch(console.error); - }, [datasource, query]); + const withTemplateVariableOptions = async (optionsPromise: Promise): Promise => { + const options = await optionsPromise; + return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value })); + }; - const lang = { grammar: logqlGrammar, name: 'logql' }; - return ( -
- - ) => - withTemplateVariableOptions(onGetLabelNames(forLabel)) - } - onGetLabelValues={(forLabel: Partial) => - withTemplateVariableOptions(onGetLabelValues(forLabel)) - } - labelsFilters={query.labels} - onChange={onChangeLabels} - labelFilterRequired={labelFilterRequired} - /> - - {showExplain && ( - } - > - {EXPLAIN_LABEL_FILTER_CONTENT} - - )} - - - - datasource={datasource} - query={query} - onChange={onChange} - data={sampleData} - queryModeller={lokiQueryModeller} - buildVisualQueryFromString={buildVisualQueryFromString} - /> - - {showExplain && ( - - stepNumber={2} - queryModeller={lokiQueryModeller} - query={query} - lang={lang} - onMouseEnter={(op) => { - setHighlightedOp(op); - }} - onMouseLeave={() => { - setHighlightedOp(undefined); - }} - /> - )} - {query.binaryQueries && query.binaryQueries.length > 0 && ( - - )} -
- ); -}); + const onGetLabelNames = async (forLabel: Partial): Promise => { + const labelsToConsider = query.labels.filter((x) => x !== forLabel); + + if (labelsToConsider.length === 0) { + return await datasource.languageProvider.fetchLabels({ timeRange }); + } + + const expr = lokiQueryModeller.renderLabels(labelsToConsider); + const series = await datasource.languageProvider.fetchSeriesLabels(expr, { timeRange }); + const labelsNamesToConsider = labelsToConsider.map((l) => l.label); + + const labelNames = Object.keys(series) + // Filter out label names that are already selected + .filter((name) => !labelsNamesToConsider.includes(name)) + .sort(); + + return labelNames; + }; + + const onGetLabelValues = async (forLabel: Partial) => { + if (!forLabel.label) { + return []; + } + + let values; + const labelsToConsider = query.labels.filter((x) => x !== forLabel); + if (labelsToConsider.length === 0) { + values = await datasource.languageProvider.fetchLabelValues(forLabel.label, { timeRange }); + } else { + const expr = lokiQueryModeller.renderLabels(labelsToConsider); + const result = await datasource.languageProvider.fetchSeriesLabels(expr); + values = result[datasource.interpolateString(forLabel.label)]; + } + + return values ? values.map((v) => escapeLabelValueInSelector(v, forLabel.op)) : []; // Escape values in return + }; + + const labelFilterRequired: boolean = useMemo(() => { + const { labels, operations: op } = query; + if (!labels.length && op.length) { + // Filter is required when operations are present (empty line contains operation is exception) + if (op.length === 1 && op[0].id === LokiOperationId.LineContains && op[0].params[0] === '') { + return false; + } + return true; + } + return false; + }, [query]); + + useEffect(() => { + const onGetSampleData = async () => { + const lokiQuery = { expr: lokiQueryModeller.renderQuery(query), refId: 'data-samples' }; + const series = await datasource.getDataSamples(lokiQuery); + const sampleData = { series, state: LoadingState.Done, timeRange: getDefaultTimeRange() }; + setSampleData(sampleData); + }; + + onGetSampleData().catch(console.error); + }, [datasource, query]); + + const lang = { grammar: logqlGrammar, name: 'logql' }; + return ( +
+ + ) => + withTemplateVariableOptions(onGetLabelNames(forLabel)) + } + onGetLabelValues={(forLabel: Partial) => + withTemplateVariableOptions(onGetLabelValues(forLabel)) + } + labelsFilters={query.labels} + onChange={onChangeLabels} + labelFilterRequired={labelFilterRequired} + /> + + {showExplain && ( + } + > + {EXPLAIN_LABEL_FILTER_CONTENT} + + )} + + + + datasource={datasource} + query={query} + onChange={onChange} + data={sampleData} + queryModeller={lokiQueryModeller} + buildVisualQueryFromString={buildVisualQueryFromString} + /> + + {showExplain && ( + + stepNumber={2} + queryModeller={lokiQueryModeller} + query={query} + lang={lang} + onMouseEnter={(op) => { + setHighlightedOp(op); + }} + onMouseLeave={() => { + setHighlightedOp(undefined); + }} + /> + )} + {query.binaryQueries && query.binaryQueries.length > 0 && ( + + )} +
+ ); + } +); LokiQueryBuilder.displayName = 'LokiQueryBuilder'; diff --git a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx index e903b47b3df..b4715fee0a4 100644 --- a/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx +++ b/public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilderContainer.tsx @@ -1,6 +1,8 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'; import React, { useEffect, useReducer } from 'react'; +import { TimeRange } from '@grafana/data'; + import { testIds } from '../../components/LokiQueryEditor'; import { LokiDatasource } from '../../datasource'; import { LokiQuery } from '../../types'; @@ -17,6 +19,7 @@ export interface Props { onChange: (update: LokiQuery) => void; onRunQuery: () => void; showExplain: boolean; + timeRange?: TimeRange; } export interface State { @@ -28,7 +31,7 @@ export interface State { * This component is here just to contain the translation logic between string query and the visual query builder model. */ export function LokiQueryBuilderContainer(props: Props) { - const { query, onChange, onRunQuery, datasource, showExplain } = props; + const { query, onChange, onRunQuery, datasource, showExplain, timeRange } = props; const [state, dispatch] = useReducer(stateSlice.reducer, { expr: query.expr, // Use initial visual query only if query.expr is empty string @@ -65,6 +68,7 @@ export function LokiQueryBuilderContainer(props: Props) { onRunQuery={onRunQuery} showExplain={showExplain} data-testid={testIds.editor} + timeRange={timeRange} /> {query.expr !== '' && }