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,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}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
|
||||
@@ -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());
|
||||
});
|
||||
|
||||
|
||||
@@ -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<Props>(({ datasource, query, onChange, onRunQuery, showExplain }) => {
|
||||
const [sampleData, setSampleData] = useState<PanelData>();
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
|
||||
export const LokiQueryBuilder = React.memo<Props>(
|
||||
({ datasource, query, onChange, onRunQuery, showExplain, timeRange }) => {
|
||||
const [sampleData, setSampleData] = useState<PanelData>();
|
||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>(undefined);
|
||||
|
||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||
onChange({ ...query, labels });
|
||||
};
|
||||
|
||||
const withTemplateVariableOptions = async (optionsPromise: Promise<string[]>): Promise<SelectableValue[]> => {
|
||||
const options = await optionsPromise;
|
||||
return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value }));
|
||||
};
|
||||
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
|
||||
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<QueryBuilderLabelFilter>) => {
|
||||
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<string[]>): Promise<SelectableValue[]> => {
|
||||
const options = await optionsPromise;
|
||||
return [...datasource.getVariables(), ...options].map((value) => ({ label: value, value }));
|
||||
};
|
||||
|
||||
const lang = { grammar: logqlGrammar, name: 'logql' };
|
||||
return (
|
||||
<div data-testid={testIds.editor}>
|
||||
<EditorRow>
|
||||
<LabelFilters
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelNames(forLabel))
|
||||
}
|
||||
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelValues(forLabel))
|
||||
}
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
labelFilterRequired={labelFilterRequired}
|
||||
/>
|
||||
</EditorRow>
|
||||
{showExplain && (
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
|
||||
>
|
||||
{EXPLAIN_LABEL_FILTER_CONTENT}
|
||||
</OperationExplainedBox>
|
||||
)}
|
||||
<OperationsEditorRow>
|
||||
<OperationList
|
||||
queryModeller={lokiQueryModeller}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource as DataSourceApi}
|
||||
highlightedOp={highlightedOp}
|
||||
/>
|
||||
<QueryBuilderHints<LokiVisualQuery>
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
data={sampleData}
|
||||
queryModeller={lokiQueryModeller}
|
||||
buildVisualQueryFromString={buildVisualQueryFromString}
|
||||
/>
|
||||
</OperationsEditorRow>
|
||||
{showExplain && (
|
||||
<OperationListExplained<LokiVisualQuery>
|
||||
stepNumber={2}
|
||||
queryModeller={lokiQueryModeller}
|
||||
query={query}
|
||||
lang={lang}
|
||||
onMouseEnter={(op) => {
|
||||
setHighlightedOp(op);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHighlightedOp(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||
<NestedQueryList
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
showExplain={showExplain}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
|
||||
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<QueryBuilderLabelFilter>) => {
|
||||
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 (
|
||||
<div data-testid={testIds.editor}>
|
||||
<EditorRow>
|
||||
<LabelFilters
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelNames(forLabel))
|
||||
}
|
||||
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelValues(forLabel))
|
||||
}
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
labelFilterRequired={labelFilterRequired}
|
||||
/>
|
||||
</EditorRow>
|
||||
{showExplain && (
|
||||
<OperationExplainedBox
|
||||
stepNumber={1}
|
||||
title={<RawQuery query={`${lokiQueryModeller.renderLabels(query.labels)}`} lang={lang} />}
|
||||
>
|
||||
{EXPLAIN_LABEL_FILTER_CONTENT}
|
||||
</OperationExplainedBox>
|
||||
)}
|
||||
<OperationsEditorRow>
|
||||
<OperationList
|
||||
queryModeller={lokiQueryModeller}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
datasource={datasource as DataSourceApi}
|
||||
highlightedOp={highlightedOp}
|
||||
/>
|
||||
<QueryBuilderHints<LokiVisualQuery>
|
||||
datasource={datasource}
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
data={sampleData}
|
||||
queryModeller={lokiQueryModeller}
|
||||
buildVisualQueryFromString={buildVisualQueryFromString}
|
||||
/>
|
||||
</OperationsEditorRow>
|
||||
{showExplain && (
|
||||
<OperationListExplained<LokiVisualQuery>
|
||||
stepNumber={2}
|
||||
queryModeller={lokiQueryModeller}
|
||||
query={query}
|
||||
lang={lang}
|
||||
onMouseEnter={(op) => {
|
||||
setHighlightedOp(op);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setHighlightedOp(undefined);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{query.binaryQueries && query.binaryQueries.length > 0 && (
|
||||
<NestedQueryList
|
||||
query={query}
|
||||
datasource={datasource}
|
||||
onChange={onChange}
|
||||
onRunQuery={onRunQuery}
|
||||
showExplain={showExplain}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
LokiQueryBuilder.displayName = 'LokiQueryBuilder';
|
||||
|
||||
@@ -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 !== '' && <QueryPreview query={query.expr} />}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user