mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 <matyax@gmail.com> * Update public/app/plugins/datasource/loki/LanguageProvider.test.ts Co-authored-by: Matias Chomicki <matyax@gmail.com> * Not needing to use languageProvider.getDefaultTime in Monaco * Update comment * Update getDefaultTimeRange to be ptivate --------- Co-authored-by: Matias Chomicki <matyax@gmail.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { AbstractLabelOperator, DataFrame } from '@grafana/data';
|
import { AbstractLabelOperator, DataFrame, TimeRange, dateTime, getDefaultTimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import LanguageProvider from './LanguageProvider';
|
import LanguageProvider from './LanguageProvider';
|
||||||
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
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('Language completion provider', () => {
|
||||||
describe('fetchSeries', () => {
|
describe('fetchSeries', () => {
|
||||||
it('should use match[] parameter', () => {
|
it('should use match[] parameter', () => {
|
||||||
@@ -38,6 +58,25 @@ describe('Language completion provider', () => {
|
|||||||
start: 1560153109000,
|
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', () => {
|
describe('fetchSeriesLabels', () => {
|
||||||
@@ -59,6 +98,26 @@ describe('Language completion provider', () => {
|
|||||||
start: 0,
|
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', () => {
|
describe('label values', () => {
|
||||||
@@ -87,6 +146,41 @@ describe('Language completion provider', () => {
|
|||||||
expect(labelValues).toEqual(['label1_val1', 'label1_val2']);
|
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 () => {
|
it('should return cached values', async () => {
|
||||||
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
|
const datasource = setup({ testkey: ['label1_val1', 'label1_val2'], label2: [] });
|
||||||
const provider = await getLanguageProvider(datasource);
|
const provider = await getLanguageProvider(datasource);
|
||||||
@@ -149,7 +243,7 @@ describe('Language completion provider', () => {
|
|||||||
describe('Request URL', () => {
|
describe('Request URL', () => {
|
||||||
it('should contain range params', async () => {
|
it('should contain range params', async () => {
|
||||||
const datasourceWithLabels = setup({ other: [] });
|
const datasourceWithLabels = setup({ other: [] });
|
||||||
const rangeParams = datasourceWithLabels.getTimeRangeParams();
|
const rangeParams = datasourceWithLabels.getTimeRangeParams(mockTimeRange);
|
||||||
const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest');
|
const datasourceSpy = jest.spyOn(datasourceWithLabels, 'metadataRequest');
|
||||||
|
|
||||||
const instance = new LanguageProvider(datasourceWithLabels);
|
const instance = new LanguageProvider(datasourceWithLabels);
|
||||||
@@ -191,6 +285,16 @@ describe('fetchLabels', () => {
|
|||||||
await instance.fetchLabels();
|
await instance.fetchLabels();
|
||||||
expect(instance.labelKeys).toEqual([]);
|
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', () => {
|
describe('Query imports', () => {
|
||||||
@@ -286,11 +390,14 @@ describe('Query imports', () => {
|
|||||||
hasLogfmt: false,
|
hasLogfmt: false,
|
||||||
hasPack: false,
|
hasPack: false,
|
||||||
});
|
});
|
||||||
expect(datasource.getDataSamples).toHaveBeenCalledWith({
|
expect(datasource.getDataSamples).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
expr: '{place="luna"}',
|
expr: '{place="luna"}',
|
||||||
maxLines: DEFAULT_MAX_LINES_SAMPLE,
|
maxLines: DEFAULT_MAX_LINES_SAMPLE,
|
||||||
refId: 'data-samples',
|
refId: 'data-samples',
|
||||||
});
|
},
|
||||||
|
undefined
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calls dataSample with correctly set sampleSize', async () => {
|
it('calls dataSample with correctly set sampleSize', async () => {
|
||||||
@@ -303,11 +410,27 @@ describe('Query imports', () => {
|
|||||||
hasLogfmt: false,
|
hasLogfmt: false,
|
||||||
hasPack: false,
|
hasPack: false,
|
||||||
});
|
});
|
||||||
expect(datasource.getDataSamples).toHaveBeenCalledWith({
|
expect(datasource.getDataSamples).toHaveBeenCalledWith(
|
||||||
|
{
|
||||||
expr: '{place="luna"}',
|
expr: '{place="luna"}',
|
||||||
maxLines: 5,
|
maxLines: 5,
|
||||||
refId: 'data-samples',
|
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
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LRUCache } from 'lru-cache';
|
import { LRUCache } from 'lru-cache';
|
||||||
import Prism from 'prismjs';
|
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 { extractLabelMatchers, processLabels, toPromLikeExpr } from 'app/plugins/datasource/prometheus/language_utils';
|
||||||
|
|
||||||
import { DEFAULT_MAX_LINES_SAMPLE, LokiDatasource } from './datasource';
|
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.
|
* Initialize the language provider by fetching set of labels.
|
||||||
*/
|
*/
|
||||||
start = () => {
|
start = (timeRange?: TimeRange) => {
|
||||||
|
const range = timeRange ?? this.getDefaultTimeRange();
|
||||||
if (!this.startTask) {
|
if (!this.startTask) {
|
||||||
this.startTask = this.fetchLabels().then(() => {
|
this.startTask = this.fetchLabels({ timeRange: range }).then(() => {
|
||||||
this.started = true;
|
this.started = true;
|
||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
@@ -101,12 +102,15 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
* This asynchronous function returns all available label keys from the data source.
|
* 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.
|
* 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.
|
* @returns A promise containing an array of label keys.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async fetchLabels(): Promise<string[]> {
|
async fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]> {
|
||||||
const url = 'labels';
|
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);
|
const res = await this.request(url, timeRange);
|
||||||
if (Array.isArray(res)) {
|
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.
|
* 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 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.
|
* @returns A promise containing a record of label names and their values.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
fetchSeriesLabels = async (streamSelector: string): Promise<Record<string, string[]>> => {
|
fetchSeriesLabels = async (
|
||||||
|
streamSelector: string,
|
||||||
|
options?: { timeRange?: TimeRange }
|
||||||
|
): Promise<Record<string, string[]>> => {
|
||||||
const interpolatedMatch = this.datasource.interpolateString(streamSelector);
|
const interpolatedMatch = this.datasource.interpolateString(streamSelector);
|
||||||
const url = 'series';
|
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);
|
const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch);
|
||||||
let value = this.seriesCache.get(cacheKey);
|
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.
|
* Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels.
|
||||||
* @param match
|
* @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<Array<Record<string, string>>> => {
|
fetchSeries = async (match: string, options?: { timeRange?: TimeRange }): Promise<Array<Record<string, string>>> => {
|
||||||
const url = 'series';
|
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 };
|
const params = { 'match[]': match, start, end };
|
||||||
return await this.request(url, params);
|
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.
|
* 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 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.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.
|
* @returns A promise containing an array of label values.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise<string[]> {
|
async fetchLabelValues(
|
||||||
|
labelName: string,
|
||||||
|
options?: { streamSelector?: string; timeRange?: TimeRange }
|
||||||
|
): Promise<string[]> {
|
||||||
const label = encodeURIComponent(this.datasource.interpolateString(labelName));
|
const label = encodeURIComponent(this.datasource.interpolateString(labelName));
|
||||||
const streamParam = options?.streamSelector
|
const streamParam = options?.streamSelector
|
||||||
? encodeURIComponent(this.datasource.interpolateString(options.streamSelector))
|
? encodeURIComponent(this.datasource.interpolateString(options.streamSelector))
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const url = `label/${label}/values`;
|
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 { start, end } = rangeParams;
|
||||||
const params: KeyValue<string | number> = { start, end };
|
const params: KeyValue<string | number> = { start, end };
|
||||||
let paramCacheKey = label;
|
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.
|
* - `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 streamSelector - The selector for the log stream you want to analyze.
|
||||||
* @param {Object} [options] - Optional parameters.
|
* @param options - (Optional) An object containing additional options.
|
||||||
* @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys.
|
* @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.
|
* 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.
|
* @returns A promise containing an object with parser and label key information.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async getParserAndLabelKeys(
|
async getParserAndLabelKeys(
|
||||||
streamSelector: string,
|
streamSelector: string,
|
||||||
options?: { maxLines?: number }
|
options?: { maxLines?: number; timeRange?: TimeRange }
|
||||||
): Promise<ParserAndLabelKeysResult> {
|
): Promise<ParserAndLabelKeysResult> {
|
||||||
const series = await this.datasource.getDataSamples({
|
const series = await this.datasource.getDataSamples(
|
||||||
|
{
|
||||||
expr: streamSelector,
|
expr: streamSelector,
|
||||||
refId: 'data-samples',
|
refId: 'data-samples',
|
||||||
maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE,
|
maxLines: options?.maxLines || DEFAULT_MAX_LINES_SAMPLE,
|
||||||
});
|
},
|
||||||
|
options?.timeRange
|
||||||
|
);
|
||||||
|
|
||||||
if (!series.length) {
|
if (!series.length) {
|
||||||
return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false };
|
return { extractedLabelKeys: [], unwrapLabelKeys: [], hasJSON: false, hasLogfmt: false, hasPack: false };
|
||||||
@@ -260,4 +284,13 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
|||||||
hasLogfmt,
|
hasLogfmt,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default time range
|
||||||
|
*
|
||||||
|
* @returns {TimeRange} The default time range
|
||||||
|
*/
|
||||||
|
private getDefaultTimeRange(): TimeRange {
|
||||||
|
return getDefaultTimeRange();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { sortBy } from 'lodash';
|
|||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
|
|
||||||
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
|
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
@@ -17,7 +17,6 @@ import {
|
|||||||
fuzzyMatch,
|
fuzzyMatch,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
|
|
||||||
import PromQlLanguageProvider from '../../prometheus/language_provider';
|
|
||||||
import LokiLanguageProvider from '../LanguageProvider';
|
import LokiLanguageProvider from '../LanguageProvider';
|
||||||
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../languageUtils';
|
import { escapeLabelValueInExactSelector, escapeLabelValueInRegexSelector } from '../languageUtils';
|
||||||
|
|
||||||
@@ -28,12 +27,12 @@ const MAX_AUTO_SELECT = 4;
|
|||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
|
|
||||||
export interface BrowserProps {
|
export interface BrowserProps {
|
||||||
// TODO #33976: Is it possible to use a common interface here? For example: LabelsLanguageProvider
|
languageProvider: LokiLanguageProvider;
|
||||||
languageProvider: LokiLanguageProvider | PromQlLanguageProvider;
|
|
||||||
onChange: (selector: string) => void;
|
onChange: (selector: string) => void;
|
||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
app?: CoreApp;
|
app?: CoreApp;
|
||||||
autoSelect?: number;
|
autoSelect?: number;
|
||||||
|
timeRange?: TimeRange;
|
||||||
hide?: () => void;
|
hide?: () => void;
|
||||||
lastUsedLabels: string[];
|
lastUsedLabels: string[];
|
||||||
storeLastUsedLabels: (labels: string[]) => void;
|
storeLastUsedLabels: (labels: string[]) => void;
|
||||||
@@ -283,10 +282,10 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { languageProvider, autoSelect = MAX_AUTO_SELECT, lastUsedLabels } = this.props;
|
const { languageProvider, autoSelect = MAX_AUTO_SELECT, lastUsedLabels, timeRange } = this.props;
|
||||||
if (languageProvider) {
|
if (languageProvider) {
|
||||||
const selectedLabels: string[] = lastUsedLabels;
|
const selectedLabels: string[] = lastUsedLabels;
|
||||||
languageProvider.start().then(() => {
|
languageProvider.start(timeRange).then(() => {
|
||||||
let rawLabels: string[] = languageProvider.getLabelKeys();
|
let rawLabels: string[] = languageProvider.getLabelKeys();
|
||||||
if (rawLabels.length > MAX_LABEL_COUNT) {
|
if (rawLabels.length > MAX_LABEL_COUNT) {
|
||||||
const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`;
|
const error = `Too many labels found (showing only ${MAX_LABEL_COUNT} of ${rawLabels.length})`;
|
||||||
@@ -347,10 +346,10 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
};
|
};
|
||||||
|
|
||||||
async fetchValues(name: string, selector: string) {
|
async fetchValues(name: string, selector: string) {
|
||||||
const { languageProvider } = this.props;
|
const { languageProvider, timeRange } = this.props;
|
||||||
this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`);
|
this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`);
|
||||||
try {
|
try {
|
||||||
let rawValues = await languageProvider.fetchLabelValues(name);
|
let rawValues = await languageProvider.fetchLabelValues(name, { timeRange });
|
||||||
// If selector changed, clear loading state and discard result by returning early
|
// If selector changed, clear loading state and discard result by returning early
|
||||||
if (selector !== buildSelector(this.state.labels)) {
|
if (selector !== buildSelector(this.state.labels)) {
|
||||||
this.updateLabelState(name, { loading: false }, '');
|
this.updateLabelState(name, { loading: false }, '');
|
||||||
@@ -369,12 +368,12 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fetchSeries(selector: string, lastFacetted?: string) {
|
async fetchSeries(selector: string, lastFacetted?: string) {
|
||||||
const { languageProvider } = this.props;
|
const { languageProvider, timeRange } = this.props;
|
||||||
if (lastFacetted) {
|
if (lastFacetted) {
|
||||||
this.updateLabelState(lastFacetted, { loading: true }, `Loading labels for ${selector}`);
|
this.updateLabelState(lastFacetted, { loading: true }, `Loading labels for ${selector}`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const possibleLabels = await languageProvider.fetchSeriesLabels(selector, true);
|
const possibleLabels = await languageProvider.fetchSeriesLabels(selector, { timeRange });
|
||||||
// If selector changed, clear loading state and discard result by returning early
|
// If selector changed, clear loading state and discard result by returning early
|
||||||
if (selector !== buildSelector(this.state.labels)) {
|
if (selector !== buildSelector(this.state.labels)) {
|
||||||
if (lastFacetted) {
|
if (lastFacetted) {
|
||||||
@@ -397,9 +396,9 @@ export class UnthemedLokiLabelBrowser extends React.Component<BrowserProps, Brow
|
|||||||
}
|
}
|
||||||
|
|
||||||
async validateSelector(selector: string) {
|
async validateSelector(selector: string) {
|
||||||
const { languageProvider } = this.props;
|
const { languageProvider, timeRange } = this.props;
|
||||||
this.setState({ validationStatus: `Validating selector ${selector}`, error: '' });
|
this.setState({ validationStatus: `Validating selector ${selector}`, error: '' });
|
||||||
const streams = await languageProvider.fetchSeries(selector);
|
const streams = await languageProvider.fetchSeries(selector, { timeRange });
|
||||||
this.setState({ validationStatus: `Selector is valid (${streams.length} streams found)` });
|
this.setState({ validationStatus: `Selector is valid (${streams.length} streams found)` });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
|
|||||||
onClose={() => setLabelBrowserVisible(false)}
|
onClose={() => setLabelBrowserVisible(false)}
|
||||||
onChange={onChangeInternal}
|
onChange={onChangeInternal}
|
||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
<EditorHeader>
|
<EditorHeader>
|
||||||
<Stack gap={1}>
|
<Stack gap={1}>
|
||||||
@@ -196,6 +197,7 @@ export const LokiQueryEditor = React.memo<LokiQueryEditorProps>((props) => {
|
|||||||
onChange={onChangeInternal}
|
onChange={onChangeInternal}
|
||||||
onRunQuery={props.onRunQuery}
|
onRunQuery={props.onRunQuery}
|
||||||
showExplain={explain}
|
showExplain={explain}
|
||||||
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<LokiQueryBuilderOptions
|
<LokiQueryBuilderOptions
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ describe('LokiQueryField', () => {
|
|||||||
|
|
||||||
rerender(<LokiQueryField {...props} range={newRange} />);
|
rerender(<LokiQueryField {...props} range={newRange} />);
|
||||||
expect(props.datasource.languageProvider.fetchLabels).toHaveBeenCalledTimes(1);
|
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 () => {
|
it('does not refreshes metrics when time range change by less than 1 minute', async () => {
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
this._isMounted = true;
|
this._isMounted = true;
|
||||||
await this.props.datasource.languageProvider.start();
|
await this.props.datasource.languageProvider.start(this.props.range);
|
||||||
if (this._isMounted) {
|
if (this._isMounted) {
|
||||||
this.setState({ labelsLoaded: true });
|
this.setState({ labelsLoaded: true });
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
const refreshLabels = shouldRefreshLabels(range, prevProps.range);
|
const refreshLabels = shouldRefreshLabels(range, prevProps.range);
|
||||||
// We want to refresh labels when range changes (we round up intervals to a minute)
|
// We want to refresh labels when range changes (we round up intervals to a minute)
|
||||||
if (refreshLabels) {
|
if (refreshLabels) {
|
||||||
languageProvider.fetchLabels();
|
languageProvider.fetchLabels({ timeRange: range });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { ExtraFieldElement, query, datasource, history, onRunQuery } = this.props;
|
const { ExtraFieldElement, query, datasource, history, onRunQuery, range } = this.props;
|
||||||
const placeholder = this.props.placeholder ?? 'Enter a Loki query (run with Shift+Enter)';
|
const placeholder = this.props.placeholder ?? 'Enter a Loki query (run with Shift+Enter)';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -82,6 +82,7 @@ export class LokiQueryField extends React.PureComponent<LokiQueryFieldProps, Lok
|
|||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
initialValue={query.expr ?? ''}
|
initialValue={query.expr ?? ''}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
timeRange={range}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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();
|
const id = uuidv4();
|
||||||
// we need only one instance of `overrideServices` during the lifetime of the react component
|
// we need only one instance of `overrideServices` during the lifetime of the react component
|
||||||
const overrideServicesRef = useRef(getOverrideServices());
|
const overrideServicesRef = useRef(getOverrideServices());
|
||||||
@@ -203,7 +212,7 @@ const MonacoQueryField = ({ history, onBlur, onRunQuery, initialValue, datasourc
|
|||||||
onTypeDebounced(query);
|
onTypeDebounced(query);
|
||||||
monaco.editor.setModelMarkers(model, 'owner', markers);
|
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);
|
const completionProvider = getCompletionProvider(monaco, dataProvider);
|
||||||
|
|
||||||
// completion-providers in monaco are not registered directly to editor-instances,
|
// completion-providers in monaco are not registered directly to editor-instances,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HistoryItem } from '@grafana/data';
|
import { HistoryItem, TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import { LokiDatasource } from '../../datasource';
|
import { LokiDatasource } from '../../datasource';
|
||||||
import { LokiQuery } from '../../types';
|
import { LokiQuery } from '../../types';
|
||||||
@@ -15,4 +15,5 @@ export type Props = {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
datasource: LokiDatasource;
|
datasource: LokiDatasource;
|
||||||
onChange: (query: string) => void;
|
onChange: (query: string) => void;
|
||||||
|
timeRange?: TimeRange;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HistoryItem } from '@grafana/data';
|
import { HistoryItem, dateTime } from '@grafana/data';
|
||||||
|
|
||||||
import LokiLanguageProvider from '../../../LanguageProvider';
|
import LokiLanguageProvider from '../../../LanguageProvider';
|
||||||
import { LokiDatasource } from '../../../datasource';
|
import { LokiDatasource } from '../../../datasource';
|
||||||
@@ -56,6 +56,15 @@ const parserAndLabelKeys = {
|
|||||||
hasPack: false,
|
hasPack: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTimeRange = {
|
||||||
|
from: dateTime(1546372800000),
|
||||||
|
to: dateTime(1546380000000),
|
||||||
|
raw: {
|
||||||
|
from: dateTime(1546372800000),
|
||||||
|
to: dateTime(1546380000000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe('CompletionDataProvider', () => {
|
describe('CompletionDataProvider', () => {
|
||||||
let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource;
|
let completionProvider: CompletionDataProvider, languageProvider: LokiLanguageProvider, datasource: LokiDatasource;
|
||||||
let historyRef: { current: Array<HistoryItem<LokiQuery>> } = { current: [] };
|
let historyRef: { current: Array<HistoryItem<LokiQuery>> } = { current: [] };
|
||||||
@@ -63,7 +72,8 @@ describe('CompletionDataProvider', () => {
|
|||||||
datasource = createLokiDatasource();
|
datasource = createLokiDatasource();
|
||||||
languageProvider = new LokiLanguageProvider(datasource);
|
languageProvider = new LokiLanguageProvider(datasource);
|
||||||
historyRef.current = history;
|
historyRef.current = history;
|
||||||
completionProvider = new CompletionDataProvider(languageProvider, historyRef);
|
|
||||||
|
completionProvider = new CompletionDataProvider(languageProvider, historyRef, mockTimeRange);
|
||||||
|
|
||||||
jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys);
|
jest.spyOn(languageProvider, 'getLabelKeys').mockReturnValue(labelKeys);
|
||||||
jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues);
|
jest.spyOn(languageProvider, 'fetchLabelValues').mockResolvedValue(labelValues);
|
||||||
@@ -163,6 +173,11 @@ describe('CompletionDataProvider', () => {
|
|||||||
expect(languageProvider.getParserAndLabelKeys).toHaveBeenCalledTimes(4);
|
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 () => {
|
test('Returns the expected series labels', async () => {
|
||||||
expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels);
|
expect(await completionProvider.getSeriesLabels([])).toEqual(seriesLabels);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { chain } from 'lodash';
|
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 { escapeLabelValueInExactSelector } from 'app/plugins/datasource/prometheus/language_utils';
|
||||||
|
|
||||||
import LanguageProvider from '../../../LanguageProvider';
|
import LanguageProvider from '../../../LanguageProvider';
|
||||||
@@ -15,7 +15,8 @@ interface HistoryRef {
|
|||||||
export class CompletionDataProvider {
|
export class CompletionDataProvider {
|
||||||
constructor(
|
constructor(
|
||||||
private languageProvider: LanguageProvider,
|
private languageProvider: LanguageProvider,
|
||||||
private historyRef: HistoryRef = { current: [] }
|
private historyRef: HistoryRef = { current: [] },
|
||||||
|
private timeRange: TimeRange | undefined
|
||||||
) {
|
) {
|
||||||
this.queryToLabelKeysCache = new Map();
|
this.queryToLabelKeysCache = new Map();
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ export class CompletionDataProvider {
|
|||||||
async getLabelValues(labelName: string, otherLabels: Label[]) {
|
async getLabelValues(labelName: string, otherLabels: Label[]) {
|
||||||
if (otherLabels.length === 0) {
|
if (otherLabels.length === 0) {
|
||||||
// if there is no filtering, we have to use a special endpoint
|
// 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);
|
const data = await this.getSeriesLabels(otherLabels);
|
||||||
@@ -82,7 +83,7 @@ export class CompletionDataProvider {
|
|||||||
this.queryToLabelKeysCache.delete(firstKey);
|
this.queryToLabelKeysCache.delete(firstKey);
|
||||||
}
|
}
|
||||||
// Fetch a fresh result from the backend
|
// 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
|
// Add the result to the cache
|
||||||
this.queryToLabelKeysCache.set(logQuery, labelKeys);
|
this.queryToLabelKeysCache.set(logQuery, labelKeys);
|
||||||
return labelKeys;
|
return labelKeys;
|
||||||
@@ -90,6 +91,8 @@ export class CompletionDataProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSeriesLabels(labels: Label[]) {
|
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 ?? {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { dateTime } from '@grafana/data';
|
||||||
import { Monaco, monacoTypes } from '@grafana/ui/src';
|
import { Monaco, monacoTypes } from '@grafana/ui/src';
|
||||||
|
|
||||||
import LokiLanguageProvider from '../../../LanguageProvider';
|
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 labelNames = ['place', 'source'];
|
||||||
const labelValues = ['moon', 'luna', 'server\\1'];
|
const labelValues = ['moon', 'luna', 'server\\1'];
|
||||||
// Source is duplicated to test handling duplicated labels
|
// Source is duplicated to test handling duplicated labels
|
||||||
@@ -195,9 +205,13 @@ describe('getCompletions', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasource = createLokiDatasource();
|
datasource = createLokiDatasource();
|
||||||
languageProvider = new LokiLanguageProvider(datasource);
|
languageProvider = new LokiLanguageProvider(datasource);
|
||||||
completionProvider = new CompletionDataProvider(languageProvider, {
|
completionProvider = new CompletionDataProvider(
|
||||||
|
languageProvider,
|
||||||
|
{
|
||||||
current: history,
|
current: history,
|
||||||
});
|
},
|
||||||
|
mockTimeRange
|
||||||
|
);
|
||||||
|
|
||||||
jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames);
|
jest.spyOn(completionProvider, 'getLabelNames').mockResolvedValue(labelNames);
|
||||||
jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues);
|
jest.spyOn(completionProvider, 'getLabelValues').mockResolvedValue(labelValues);
|
||||||
@@ -433,9 +447,13 @@ describe('getAfterSelectorCompletions', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasource = createLokiDatasource();
|
datasource = createLokiDatasource();
|
||||||
languageProvider = new LokiLanguageProvider(datasource);
|
languageProvider = new LokiLanguageProvider(datasource);
|
||||||
completionProvider = new CompletionDataProvider(languageProvider, {
|
completionProvider = new CompletionDataProvider(
|
||||||
|
languageProvider,
|
||||||
|
{
|
||||||
current: history,
|
current: history,
|
||||||
});
|
},
|
||||||
|
mockTimeRange
|
||||||
|
);
|
||||||
|
|
||||||
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
|
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
|
||||||
extractedLabelKeys: ['abc', 'def'],
|
extractedLabelKeys: ['abc', 'def'],
|
||||||
@@ -524,9 +542,13 @@ describe('IN_LOGFMT completions', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
datasource = createLokiDatasource();
|
datasource = createLokiDatasource();
|
||||||
languageProvider = new LokiLanguageProvider(datasource);
|
languageProvider = new LokiLanguageProvider(datasource);
|
||||||
completionProvider = new CompletionDataProvider(languageProvider, {
|
completionProvider = new CompletionDataProvider(
|
||||||
|
languageProvider,
|
||||||
|
{
|
||||||
current: history,
|
current: history,
|
||||||
});
|
},
|
||||||
|
mockTimeRange
|
||||||
|
);
|
||||||
|
|
||||||
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
|
jest.spyOn(completionProvider, 'getParserAndLabelKeys').mockResolvedValue({
|
||||||
extractedLabelKeys: ['label1', 'label2'],
|
extractedLabelKeys: ['label1', 'label2'],
|
||||||
|
|||||||
@@ -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.
|
* @returns An object containing the start and end times in nanoseconds since the Unix epoch.
|
||||||
*/
|
*/
|
||||||
getTimeRangeParams() {
|
getTimeRangeParams(timeRange?: TimeRange) {
|
||||||
const timeRange = this.getTimeRange();
|
const range = timeRange ?? this.getTimeRange();
|
||||||
return { start: timeRange.from.valueOf() * NS_IN_MS, end: timeRange.to.valueOf() * NS_IN_MS };
|
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.
|
* Currently, it works for logs data only.
|
||||||
* @returns A Promise that resolves to an array of DataFrames containing data samples.
|
* @returns A Promise that resolves to an array of DataFrames containing data samples.
|
||||||
*/
|
*/
|
||||||
async getDataSamples(query: LokiQuery): Promise<DataFrame[]> {
|
async getDataSamples(query: LokiQuery, timeRange?: TimeRange): Promise<DataFrame[]> {
|
||||||
// Currently works only for logs sample
|
// Currently works only for logs sample
|
||||||
if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) {
|
if (!isLogsQuery(query.expr) || isQueryWithError(this.interpolateString(query.expr, placeHolderScopedVars))) {
|
||||||
return [];
|
return [];
|
||||||
@@ -772,8 +772,8 @@ export class LokiDatasource
|
|||||||
supportingQueryType: SupportingQueryType.DataSample,
|
supportingQueryType: SupportingQueryType.DataSample,
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeRange = this.getTimeRange();
|
const range = timeRange ?? this.getTimeRange();
|
||||||
const request = makeRequest(lokiLogsQuery, timeRange, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true);
|
const request = makeRequest(lokiLogsQuery, range, CoreApp.Unknown, REF_ID_DATA_SAMPLES, true);
|
||||||
return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data))));
|
return await lastValueFrom(this.query(request).pipe(switchMap((res) => of(res.data))));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
* 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.
|
* 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.
|
* @returns A promise containing an array of label keys.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async function fetchLabels(): Promise<string[]>;
|
async function fetchLabels(options?: { timeRange?: TimeRange }): Promise<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example usage:
|
* 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.
|
* 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 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.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.
|
* @returns A promise containing an array of label values.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async function fetchLabelValues(labelName: string, options?: { streamSelector?: string }): Promise<string[]>;
|
async function fetchLabelValues(
|
||||||
|
labelName: string,
|
||||||
|
options?: { streamSelector?: string; timeRange?: TimeRange }
|
||||||
|
): Promise<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example usage without stream selector:
|
* 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.
|
* 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 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.
|
* @returns A promise containing a record of label names and their values.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async function fetchSeriesLabels(streamSelector: string): Promise<Record<string, string[]>>;
|
async function fetchSeriesLabels(
|
||||||
|
streamSelector: string,
|
||||||
|
options?: { timeRange?: TimeRange }
|
||||||
|
): Promise<Record<string, string[]>>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Example usage:
|
* Example usage:
|
||||||
@@ -138,15 +149,16 @@ try {
|
|||||||
* - `unwrapLabelKeys`: An array of label keys that can be used for unwrapping log data.
|
* - `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 streamSelector - The selector for the log stream you want to analyze.
|
||||||
* @param {Object} [options] - Optional parameters.
|
* @param options - (Optional) An object containing additional options.
|
||||||
* @param {number} [options.maxLines] - The number of log lines requested when determining parsers and label keys.
|
* @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.
|
* 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.
|
* @returns A promise containing an object with parser and label key information.
|
||||||
* @throws An error if the fetch operation fails.
|
* @throws An error if the fetch operation fails.
|
||||||
*/
|
*/
|
||||||
async function getParserAndLabelKeys(
|
async function getParserAndLabelKeys(
|
||||||
streamSelector: string,
|
streamSelector: string,
|
||||||
options?: { maxLines?: number }
|
options?: { maxLines?: number; timeRange?: TimeRange }
|
||||||
): Promise<{
|
): Promise<{
|
||||||
extractedLabelKeys: string[];
|
extractedLabelKeys: string[];
|
||||||
hasJSON: boolean;
|
hasJSON: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import React, { useState, useEffect } from 'react';
|
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 { reportInteraction } from '@grafana/runtime';
|
||||||
import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
|
import { LoadingPlaceholder, Modal, useStyles2 } from '@grafana/ui';
|
||||||
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
import { LocalStorageValueProvider } from 'app/core/components/LocalStorageValueProvider';
|
||||||
@@ -15,13 +15,14 @@ export interface Props {
|
|||||||
datasource: LokiDatasource;
|
datasource: LokiDatasource;
|
||||||
query: LokiQuery;
|
query: LokiQuery;
|
||||||
app?: CoreApp;
|
app?: CoreApp;
|
||||||
|
timeRange?: TimeRange;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onChange: (query: LokiQuery) => void;
|
onChange: (query: LokiQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LabelBrowserModal = (props: Props) => {
|
export const LabelBrowserModal = (props: Props) => {
|
||||||
const { isOpen, onClose, datasource, app } = props;
|
const { isOpen, onClose, datasource, app, timeRange } = props;
|
||||||
const [labelsLoaded, setLabelsLoaded] = useState(false);
|
const [labelsLoaded, setLabelsLoaded] = useState(false);
|
||||||
const [hasLogLabels, setHasLogLabels] = useState(false);
|
const [hasLogLabels, setHasLogLabels] = useState(false);
|
||||||
const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels';
|
const LAST_USED_LABELS_KEY = 'grafana.datasources.loki.browser.labels';
|
||||||
@@ -33,11 +34,11 @@ export const LabelBrowserModal = (props: Props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
datasource.languageProvider.fetchLabels().then((labels) => {
|
datasource.languageProvider.fetchLabels({ timeRange }).then((labels) => {
|
||||||
setLabelsLoaded(true);
|
setLabelsLoaded(true);
|
||||||
setHasLogLabels(labels.length > 0);
|
setHasLogLabels(labels.length > 0);
|
||||||
});
|
});
|
||||||
}, [datasource, isOpen]);
|
}, [datasource, isOpen, timeRange]);
|
||||||
|
|
||||||
const changeQuery = (value: string) => {
|
const changeQuery = (value: string) => {
|
||||||
const { query, onChange, onRunQuery } = props;
|
const { query, onChange, onRunQuery } = props;
|
||||||
@@ -74,6 +75,7 @@ export const LabelBrowserModal = (props: Props) => {
|
|||||||
storeLastUsedLabels={onLastUsedLabelsSave}
|
storeLastUsedLabels={onLastUsedLabelsSave}
|
||||||
deleteLastUsedLabels={onLastUsedLabelsDelete}
|
deleteLastUsedLabels={onLastUsedLabelsDelete}
|
||||||
app={app}
|
app={app}
|
||||||
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import userEvent from '@testing-library/user-event';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { getSelectParent } from 'test/helpers/selectOptionInTest';
|
import { getSelectParent } from 'test/helpers/selectOptionInTest';
|
||||||
|
|
||||||
|
import { dateTime } from '@grafana/data';
|
||||||
|
|
||||||
import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters';
|
import { MISSING_LABEL_FILTER_ERROR_MESSAGE } from '../../../prometheus/querybuilder/shared/LabelFilters';
|
||||||
import { createLokiDatasource } from '../../mocks';
|
import { createLokiDatasource } from '../../mocks';
|
||||||
import { LokiOperationId, LokiVisualQuery } from '../types';
|
import { LokiOperationId, LokiVisualQuery } from '../types';
|
||||||
@@ -15,6 +17,15 @@ const defaultQuery: LokiVisualQuery = {
|
|||||||
operations: [],
|
operations: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockTimeRange = {
|
||||||
|
from: dateTime(1546372800000),
|
||||||
|
to: dateTime(1546380000000),
|
||||||
|
raw: {
|
||||||
|
from: dateTime(1546372800000),
|
||||||
|
to: dateTime(1546380000000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const createDefaultProps = () => {
|
const createDefaultProps = () => {
|
||||||
const datasource = createLokiDatasource();
|
const datasource = createLokiDatasource();
|
||||||
|
|
||||||
@@ -23,6 +34,7 @@ const createDefaultProps = () => {
|
|||||||
onRunQuery: () => {},
|
onRunQuery: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
showExplain: false,
|
showExplain: false,
|
||||||
|
timeRange: mockTimeRange,
|
||||||
};
|
};
|
||||||
|
|
||||||
return props;
|
return props;
|
||||||
@@ -39,6 +51,9 @@ describe('LokiQueryBuilder', () => {
|
|||||||
const labels = screen.getByText(/Label filters/);
|
const labels = screen.getByText(/Label filters/);
|
||||||
const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
|
const selects = getAllByRole(getSelectParent(labels)!, 'combobox');
|
||||||
await userEvent.click(selects[3]);
|
await userEvent.click(selects[3]);
|
||||||
|
expect(props.datasource.languageProvider.fetchSeriesLabels).toBeCalledWith('{baz="bar"}', {
|
||||||
|
timeRange: mockTimeRange,
|
||||||
|
});
|
||||||
await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText('job')).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 { EditorRow } from '@grafana/experimental';
|
||||||
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
|
import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters';
|
||||||
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
|
import { OperationExplainedBox } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationExplainedBox';
|
||||||
@@ -29,10 +29,12 @@ export interface Props {
|
|||||||
query: LokiVisualQuery;
|
query: LokiVisualQuery;
|
||||||
datasource: LokiDatasource;
|
datasource: LokiDatasource;
|
||||||
showExplain: boolean;
|
showExplain: boolean;
|
||||||
|
timeRange?: TimeRange;
|
||||||
onChange: (update: LokiVisualQuery) => void;
|
onChange: (update: LokiVisualQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
}
|
}
|
||||||
export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange, onRunQuery, showExplain }) => {
|
export const LokiQueryBuilder = React.memo<Props>(
|
||||||
|
({ datasource, query, onChange, onRunQuery, showExplain, timeRange }) => {
|
||||||
const [sampleData, setSampleData] = useState<PanelData>();
|
const [sampleData, setSampleData] = useState<PanelData>();
|
||||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
|
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
|
||||||
|
|
||||||
@@ -49,11 +51,11 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||||
|
|
||||||
if (labelsToConsider.length === 0) {
|
if (labelsToConsider.length === 0) {
|
||||||
return await datasource.languageProvider.fetchLabels();
|
return await datasource.languageProvider.fetchLabels({ timeRange });
|
||||||
}
|
}
|
||||||
|
|
||||||
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||||
const series = await datasource.languageProvider.fetchSeriesLabels(expr);
|
const series = await datasource.languageProvider.fetchSeriesLabels(expr, { timeRange });
|
||||||
const labelsNamesToConsider = labelsToConsider.map((l) => l.label);
|
const labelsNamesToConsider = labelsToConsider.map((l) => l.label);
|
||||||
|
|
||||||
const labelNames = Object.keys(series)
|
const labelNames = Object.keys(series)
|
||||||
@@ -72,7 +74,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
let values;
|
let values;
|
||||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||||
if (labelsToConsider.length === 0) {
|
if (labelsToConsider.length === 0) {
|
||||||
values = await datasource.languageProvider.fetchLabelValues(forLabel.label);
|
values = await datasource.languageProvider.fetchLabelValues(forLabel.label, { timeRange });
|
||||||
} else {
|
} else {
|
||||||
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
const expr = lokiQueryModeller.renderLabels(labelsToConsider);
|
||||||
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||||
@@ -172,6 +174,7 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
LokiQueryBuilder.displayName = 'LokiQueryBuilder';
|
LokiQueryBuilder.displayName = 'LokiQueryBuilder';
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
|
||||||
import React, { useEffect, useReducer } from 'react';
|
import React, { useEffect, useReducer } from 'react';
|
||||||
|
|
||||||
|
import { TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
import { testIds } from '../../components/LokiQueryEditor';
|
import { testIds } from '../../components/LokiQueryEditor';
|
||||||
import { LokiDatasource } from '../../datasource';
|
import { LokiDatasource } from '../../datasource';
|
||||||
import { LokiQuery } from '../../types';
|
import { LokiQuery } from '../../types';
|
||||||
@@ -17,6 +19,7 @@ export interface Props {
|
|||||||
onChange: (update: LokiQuery) => void;
|
onChange: (update: LokiQuery) => void;
|
||||||
onRunQuery: () => void;
|
onRunQuery: () => void;
|
||||||
showExplain: boolean;
|
showExplain: boolean;
|
||||||
|
timeRange?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface State {
|
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.
|
* This component is here just to contain the translation logic between string query and the visual query builder model.
|
||||||
*/
|
*/
|
||||||
export function LokiQueryBuilderContainer(props: Props) {
|
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, {
|
const [state, dispatch] = useReducer(stateSlice.reducer, {
|
||||||
expr: query.expr,
|
expr: query.expr,
|
||||||
// Use initial visual query only if query.expr is empty string
|
// Use initial visual query only if query.expr is empty string
|
||||||
@@ -65,6 +68,7 @@ export function LokiQueryBuilderContainer(props: Props) {
|
|||||||
onRunQuery={onRunQuery}
|
onRunQuery={onRunQuery}
|
||||||
showExplain={showExplain}
|
showExplain={showExplain}
|
||||||
data-testid={testIds.editor}
|
data-testid={testIds.editor}
|
||||||
|
timeRange={timeRange}
|
||||||
/>
|
/>
|
||||||
{query.expr !== '' && <QueryPreview query={query.expr} />}
|
{query.expr !== '' && <QueryPreview query={query.expr} />}
|
||||||
</>
|
</>
|
||||||
|
|||||||
Reference in New Issue
Block a user