Prometheus: Show variable options in query builder (#44784)

* Prometheus: Show variable options

* Remove lint error

* Fix test for CodeQL

* Update public/app/plugins/datasource/prometheus/datasource.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>

* Update public/app/plugins/datasource/loki/datasource.ts

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>

Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
pull/44780/head^2
Ivana Huckova 4 years ago committed by GitHub
parent f582e6c86a
commit c23bc1e7b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      public/app/plugins/datasource/loki/components/__snapshots__/LokiExploreQueryEditor.test.tsx.snap
  2. 8
      public/app/plugins/datasource/loki/datasource.ts
  3. 21
      public/app/plugins/datasource/loki/language_provider.test.ts
  4. 10
      public/app/plugins/datasource/loki/language_provider.ts
  5. 1
      public/app/plugins/datasource/loki/mocks.ts
  6. 18
      public/app/plugins/datasource/loki/querybuilder/components/LokiQueryBuilder.tsx
  7. 8
      public/app/plugins/datasource/prometheus/datasource.ts
  8. 40
      public/app/plugins/datasource/prometheus/language_provider.test.ts
  9. 7
      public/app/plugins/datasource/prometheus/language_provider.ts
  10. 10
      public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx
  11. 37
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.test.tsx
  12. 25
      public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx
  13. 8
      public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx
  14. 12
      public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.test.tsx
  15. 5
      public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilters.tsx

@ -54,6 +54,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
datasource={ datasource={
Object { Object {
"getTimeRangeParams": [Function], "getTimeRangeParams": [Function],
"interpolateString": [Function],
"languageProvider": LokiLanguageProvider { "languageProvider": LokiLanguageProvider {
"cleanText": [Function], "cleanText": [Function],
"datasource": [Circular], "datasource": [Circular],

@ -754,6 +754,14 @@ export class LokiDatasource
return addLabelToQuery(queryExpr, key, value, operator, true); return addLabelToQuery(queryExpr, key, value, operator, true);
} }
} }
interpolateString(string: string) {
return this.templateSrv.replace(string, undefined, this.interpolateQueryExpr);
}
getVariables(): string[] {
return this.templateSrv.getVariables().map((v) => `$${v.name}`);
}
} }
export function lokiRegularEscape(value: any) { export function lokiRegularEscape(value: any) {

@ -103,6 +103,27 @@ describe('Language completion provider', () => {
}); });
}); });
describe('fetchSeriesLabels', () => {
it('should interpolate variable in series', () => {
const datasource: LokiDatasource = {
metadataRequest: () => ({ data: { data: [] as any[] } }),
getTimeRangeParams: () => ({ start: 0, end: 1 }),
interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
} as any as LokiDatasource;
const languageProvider = new LanguageProvider(datasource);
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request').mockResolvedValue([]);
fetchSeriesLabels('$stream');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith('/loki/api/v1/series', {
end: 1,
'match[]': 'interpolated-stream',
start: 0,
});
});
});
describe('label key suggestions', () => { describe('label key suggestions', () => {
it('returns all label suggestions on empty selector', async () => { it('returns all label suggestions on empty selector', async () => {
const datasource = makeMockLokiDatasource({ label1: [], label2: [] }); const datasource = makeMockLokiDatasource({ label1: [], label2: [] });

@ -396,15 +396,16 @@ export default class LokiLanguageProvider extends LanguageProvider {
* @param name * @param name
*/ */
fetchSeriesLabels = async (match: string): Promise<Record<string, string[]>> => { fetchSeriesLabels = async (match: string): Promise<Record<string, string[]>> => {
const interpolatedMatch = this.datasource.interpolateString(match);
const url = '/loki/api/v1/series'; const url = '/loki/api/v1/series';
const { start, end } = this.datasource.getTimeRangeParams(); const { start, end } = this.datasource.getTimeRangeParams();
const cacheKey = this.generateCacheKey(url, start, end, match); const cacheKey = this.generateCacheKey(url, start, end, interpolatedMatch);
let value = this.seriesCache.get(cacheKey); let value = this.seriesCache.get(cacheKey);
if (!value) { if (!value) {
// Clear value when requesting new one. Empty object being truthy also makes sure we don't request twice. // Clear value when requesting new one. Empty object being truthy also makes sure we don't request twice.
this.seriesCache.set(cacheKey, {}); this.seriesCache.set(cacheKey, {});
const params = { 'match[]': match, start, end }; const params = { 'match[]': interpolatedMatch, start, end };
const data = await this.request(url, params); const data = await this.request(url, params);
const { values } = processLabels(data); const { values } = processLabels(data);
value = values; value = values;
@ -442,11 +443,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
} }
async fetchLabelValues(key: string): Promise<string[]> { async fetchLabelValues(key: string): Promise<string[]> {
const url = `/loki/api/v1/label/${key}/values`; const interpolatedKey = this.datasource.interpolateString(key);
const url = `/loki/api/v1/label/${interpolatedKey}/values`;
const rangeParams = this.datasource.getTimeRangeParams(); const rangeParams = this.datasource.getTimeRangeParams();
const { start, end } = rangeParams; const { start, end } = rangeParams;
const cacheKey = this.generateCacheKey(url, start, end, key); const cacheKey = this.generateCacheKey(url, start, end, interpolatedKey);
const params = { start, end }; const params = { start, end };
let labelValues = this.labelsCache.get(cacheKey); let labelValues = this.labelsCache.get(cacheKey);

@ -43,6 +43,7 @@ export function makeMockLokiDatasource(labelsAndValues: Labels, series?: SeriesF
} }
} }
}, },
interpolateString: (string: string) => string,
} as any; } as any;
} }

@ -5,7 +5,7 @@ import { LabelFilters } from 'app/plugins/datasource/prometheus/querybuilder/sha
import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList'; import { OperationList } from 'app/plugins/datasource/prometheus/querybuilder/shared/OperationList';
import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types'; import { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
import { lokiQueryModeller } from '../LokiQueryModeller'; import { lokiQueryModeller } from '../LokiQueryModeller';
import { DataSourceApi } from '@grafana/data'; import { DataSourceApi, SelectableValue } from '@grafana/data';
import { EditorRow, EditorRows } from '@grafana/experimental'; import { EditorRow, EditorRows } from '@grafana/experimental';
import { QueryPreview } from './QueryPreview'; import { QueryPreview } from './QueryPreview';
@ -22,6 +22,11 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
onChange({ ...query, labels }); 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<any> => { const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<any> => {
const labelsToConsider = query.labels.filter((x) => x !== forLabel); const labelsToConsider = query.labels.filter((x) => x !== forLabel);
@ -46,15 +51,20 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
const expr = lokiQueryModeller.renderLabels(labelsToConsider); const expr = lokiQueryModeller.renderLabels(labelsToConsider);
const result = await datasource.languageProvider.fetchSeriesLabels(expr); const result = await datasource.languageProvider.fetchSeriesLabels(expr);
return result[forLabel.label] ?? []; const forLabelInterpolated = datasource.interpolateString(forLabel.label);
return result[forLabelInterpolated] ?? [];
}; };
return ( return (
<EditorRows> <EditorRows>
<EditorRow> <EditorRow>
<LabelFilters <LabelFilters
onGetLabelNames={onGetLabelNames} onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
onGetLabelValues={onGetLabelValues} withTemplateVariableOptions(onGetLabelNames(forLabel))
}
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
withTemplateVariableOptions(onGetLabelValues(forLabel))
}
labelsFilters={query.labels} labelsFilters={query.labels}
onChange={onChangeLabels} onChange={onChangeLabels}
/> />

@ -987,6 +987,14 @@ export class PrometheusDatasource
interval: this.templateSrv.replace(target.interval, variables), interval: this.templateSrv.replace(target.interval, variables),
}; };
} }
getVariables(): string[] {
return this.templateSrv.getVariables().map((v) => `$${v.name}`);
}
interpolateString(string: string) {
return this.templateSrv.replace(string, undefined, this.interpolateQueryExpr);
}
} }
/** /**

@ -11,6 +11,7 @@ describe('Language completion provider', () => {
const datasource: PrometheusDatasource = { const datasource: PrometheusDatasource = {
metadataRequest: () => ({ data: { data: [] as any[] } }), metadataRequest: () => ({ data: { data: [] as any[] } }),
getTimeRangeParams: () => ({ start: '0', end: '1' }), getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
} as any as PrometheusDatasource; } as any as PrometheusDatasource;
describe('cleanText', () => { describe('cleanText', () => {
@ -79,6 +80,41 @@ describe('Language completion provider', () => {
}); });
}); });
describe('fetchSeriesLabels', () => {
it('should interpolate variable in series', () => {
const languageProvider = new LanguageProvider({
...datasource,
interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
} as PrometheusDatasource);
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
fetchSeriesLabels('$metric');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith('/api/v1/series', [], {
end: '1',
'match[]': 'interpolated-metric',
start: '0',
});
});
});
describe('fetchLabelValues', () => {
it('should interpolate variable in series', () => {
const languageProvider = new LanguageProvider({
...datasource,
interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
} as PrometheusDatasource);
const fetchLabelValues = languageProvider.fetchLabelValues;
const requestSpy = jest.spyOn(languageProvider, 'request');
fetchLabelValues('$job');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith('/api/v1/label/interpolated-job/values', [], {
end: '1',
start: '0',
});
});
});
describe('empty query suggestions', () => { describe('empty query suggestions', () => {
it('returns no suggestions on empty context', async () => { it('returns no suggestions on empty context', async () => {
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
@ -266,6 +302,7 @@ describe('Language completion provider', () => {
const datasources: PrometheusDatasource = { const datasources: PrometheusDatasource = {
metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }), metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }),
getTimeRangeParams: () => ({ start: '0', end: '1' }), getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
} as any as PrometheusDatasource; } as any as PrometheusDatasource;
const instance = new LanguageProvider(datasources); const instance = new LanguageProvider(datasources);
const value = Plain.deserialize('metric{}'); const value = Plain.deserialize('metric{}');
@ -299,6 +336,7 @@ describe('Language completion provider', () => {
}, },
}), }),
getTimeRangeParams: () => ({ start: '0', end: '1' }), getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
} as any as PrometheusDatasource; } as any as PrometheusDatasource;
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}'); const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
@ -536,6 +574,7 @@ describe('Language completion provider', () => {
const datasource: PrometheusDatasource = { const datasource: PrometheusDatasource = {
metadataRequest: jest.fn(() => ({ data: { data: [] as any[] } })), metadataRequest: jest.fn(() => ({ data: { data: [] as any[] } })),
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })), getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
interpolateString: (string: string) => string,
} as any as PrometheusDatasource; } as any as PrometheusDatasource;
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);
@ -586,6 +625,7 @@ describe('Language completion provider', () => {
metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })), metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })), getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
lookupsDisabled: false, lookupsDisabled: false,
interpolateString: (string: string) => string,
} as any as PrometheusDatasource; } as any as PrometheusDatasource;
const instance = new LanguageProvider(datasource); const instance = new LanguageProvider(datasource);

@ -460,7 +460,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
fetchLabelValues = async (key: string): Promise<string[]> => { fetchLabelValues = async (key: string): Promise<string[]> => {
const params = this.datasource.getTimeRangeParams(); const params = this.datasource.getTimeRangeParams();
const url = `/api/v1/label/${key}/values`; const url = `/api/v1/label/${this.datasource.interpolateString(key)}/values`;
return await this.request(url, [], params); return await this.request(url, [], params);
}; };
@ -491,10 +491,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
* @param withName * @param withName
*/ */
fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => { fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => {
const interpolatedName = this.datasource.interpolateString(name);
const range = this.datasource.getTimeRangeParams(); const range = this.datasource.getTimeRangeParams();
const urlParams = { const urlParams = {
...range, ...range,
'match[]': name, 'match[]': interpolatedName,
}; };
const url = `/api/v1/series`; const url = `/api/v1/series`;
// Cache key is a bit different here. We add the `withName` param and also round up to a minute the intervals. // Cache key is a bit different here. We add the `withName` param and also round up to a minute the intervals.
@ -502,7 +503,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
// millisecond while still actually getting all the keys for the correct interval. This still can create problems // millisecond while still actually getting all the keys for the correct interval. This still can create problems
// when user does not the newest values for a minute if already cached. // when user does not the newest values for a minute if already cached.
const cacheParams = new URLSearchParams({ const cacheParams = new URLSearchParams({
'match[]': name, 'match[]': interpolatedName,
start: roundSecToMin(parseInt(range.start, 10)).toString(), start: roundSecToMin(parseInt(range.start, 10)).toString(),
end: roundSecToMin(parseInt(range.end, 10)).toString(), end: roundSecToMin(parseInt(range.end, 10)).toString(),
withName: withName ? 'true' : 'false', withName: withName ? 'true' : 'false',

@ -8,7 +8,7 @@ import { css } from '@emotion/css';
export interface Props { export interface Props {
query: PromVisualQuery; query: PromVisualQuery;
onChange: (query: PromVisualQuery) => void; onChange: (query: PromVisualQuery) => void;
onGetMetrics: () => Promise<string[]>; onGetMetrics: () => Promise<SelectableValue[]>;
} }
export function MetricSelect({ query, onChange, onGetMetrics }: Props) { export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
@ -18,12 +18,6 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
isLoading?: boolean; isLoading?: boolean;
}>({}); }>({});
const loadMetrics = async () => {
return await onGetMetrics().then((res) => {
return res.map((value) => ({ label: value, value }));
});
};
return ( return (
<EditorFieldGroup> <EditorFieldGroup>
<EditorField label="Metric"> <EditorField label="Metric">
@ -35,7 +29,7 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
allowCustomValue allowCustomValue
onOpenMenu={async () => { onOpenMenu={async () => {
setState({ isLoading: true }); setState({ isLoading: true });
const metrics = await loadMetrics(); const metrics = await onGetMetrics();
setState({ metrics, isLoading: undefined }); setState({ metrics, isLoading: undefined });
}} }}
isLoading={state.isLoading} isLoading={state.isLoading}

@ -87,12 +87,26 @@ describe('PromQueryBuilder', () => {
expect(languageProvider.getSeries).toBeCalledWith('{label_name="label_value"}', true); expect(languageProvider.getSeries).toBeCalledWith('{label_name="label_value"}', true);
}); });
it('tries to load variables in metric field', async () => {
const { datasource } = setup();
datasource.getVariables = jest.fn().mockReturnValue([]);
openMetricSelect();
expect(datasource.getVariables).toBeCalled();
});
it('tries to load labels when metric selected', async () => { it('tries to load labels when metric selected', async () => {
const { languageProvider } = setup(); const { languageProvider } = setup();
openLabelNameSelect(); openLabelNameSelect();
expect(languageProvider.fetchSeriesLabels).toBeCalledWith('{__name__="random_metric"}'); expect(languageProvider.fetchSeriesLabels).toBeCalledWith('{__name__="random_metric"}');
}); });
it('tries to load variables in label field', async () => {
const { datasource } = setup();
datasource.getVariables = jest.fn().mockReturnValue([]);
openLabelNameSelect();
expect(datasource.getVariables).toBeCalled();
});
it('tries to load labels when metric selected and other labels are already present', async () => { it('tries to load labels when metric selected and other labels are already present', async () => {
const { languageProvider } = setup({ const { languageProvider } = setup({
...defaultQuery, ...defaultQuery,
@ -117,23 +131,24 @@ describe('PromQueryBuilder', () => {
function setup(query: PromVisualQuery = defaultQuery) { function setup(query: PromVisualQuery = defaultQuery) {
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider; const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
const datasource = new PrometheusDatasource(
{
url: '',
jsonData: {},
meta: {} as any,
} as any,
undefined,
undefined,
languageProvider
);
const props = { const props = {
datasource: new PrometheusDatasource( datasource,
{
url: '',
jsonData: {},
meta: {} as any,
} as any,
undefined,
undefined,
languageProvider
),
onRunQuery: () => {}, onRunQuery: () => {},
onChange: () => {}, onChange: () => {},
}; };
render(<PromQueryBuilder {...props} query={query} />); render(<PromQueryBuilder {...props} query={query} />);
return { languageProvider }; return { languageProvider, datasource };
} }
function getMetricSelect() { function getMetricSelect() {

@ -9,7 +9,7 @@ import { NestedQueryList } from './NestedQueryList';
import { promQueryModeller } from '../PromQueryModeller'; import { promQueryModeller } from '../PromQueryModeller';
import { QueryBuilderLabelFilter } from '../shared/types'; import { QueryBuilderLabelFilter } from '../shared/types';
import { QueryPreview } from './QueryPreview'; import { QueryPreview } from './QueryPreview';
import { DataSourceApi } from '@grafana/data'; import { DataSourceApi, SelectableValue } from '@grafana/data';
import { OperationsEditorRow } from '../shared/OperationsEditorRow'; import { OperationsEditorRow } from '../shared/OperationsEditorRow';
export interface Props { export interface Props {
@ -25,6 +25,12 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
onChange({ ...query, labels }); onChange({ ...query, labels });
}; };
const withTemplateVariableOptions = async (optionsPromise: Promise<string[]>): Promise<SelectableValue[]> => {
const variables = datasource.getVariables();
const options = await optionsPromise;
return [...variables, ...options].map((value) => ({ label: value, value }));
};
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => { const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<string[]> => {
// If no metric we need to use a different method // If no metric we need to use a different method
if (!query.metric) { if (!query.metric) {
@ -58,7 +64,8 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric }); labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
const expr = promQueryModeller.renderLabels(labelsToConsider); const expr = promQueryModeller.renderLabels(labelsToConsider);
const result = await datasource.languageProvider.fetchSeriesLabels(expr); const result = await datasource.languageProvider.fetchSeriesLabels(expr);
return result[forLabel.label] ?? []; const forLabelInterpolated = datasource.interpolateString(forLabel.label);
return result[forLabelInterpolated] ?? [];
}; };
const onGetMetrics = async () => { const onGetMetrics = async () => {
@ -73,12 +80,20 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
return ( return (
<EditorRows> <EditorRows>
<EditorRow> <EditorRow>
<MetricSelect query={query} onChange={onChange} onGetMetrics={onGetMetrics} /> <MetricSelect
query={query}
onChange={onChange}
onGetMetrics={() => withTemplateVariableOptions(onGetMetrics())}
/>
<LabelFilters <LabelFilters
labelsFilters={query.labels} labelsFilters={query.labels}
onChange={onChangeLabels} onChange={onChangeLabels}
onGetLabelNames={onGetLabelNames} onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
onGetLabelValues={onGetLabelValues} withTemplateVariableOptions(onGetLabelNames(forLabel))
}
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
withTemplateVariableOptions(onGetLabelValues(forLabel))
}
/> />
</EditorRow> </EditorRow>
<OperationsEditorRow> <OperationsEditorRow>

@ -8,8 +8,8 @@ export interface Props {
defaultOp: string; defaultOp: string;
item: Partial<QueryBuilderLabelFilter>; item: Partial<QueryBuilderLabelFilter>;
onChange: (value: QueryBuilderLabelFilter) => void; onChange: (value: QueryBuilderLabelFilter) => void;
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>; onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>; onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
onDelete: () => void; onDelete: () => void;
} }
@ -53,7 +53,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
allowCustomValue allowCustomValue
onOpenMenu={async () => { onOpenMenu={async () => {
setState({ isLoadingLabelNames: true }); setState({ isLoadingLabelNames: true });
const labelNames = (await onGetLabelNames(item)).map((x) => ({ label: x, value: x })); const labelNames = await onGetLabelNames(item);
setState({ labelNames, isLoadingLabelNames: undefined }); setState({ labelNames, isLoadingLabelNames: undefined });
}} }}
isLoading={state.isLoadingLabelNames} isLoading={state.isLoadingLabelNames}
@ -90,7 +90,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
const labelValues = await onGetLabelValues(item); const labelValues = await onGetLabelValues(item);
setState({ setState({
...state, ...state,
labelValues: labelValues.map((value) => ({ label: value, value })), labelValues,
isLoadingLabelValues: undefined, isLoadingLabelValues: undefined,
}); });
}} }}

@ -52,8 +52,16 @@ describe('LabelFilters', () => {
function setup(labels: QueryBuilderLabelFilter[] = []) { function setup(labels: QueryBuilderLabelFilter[] = []) {
const props = { const props = {
onChange: jest.fn(), onChange: jest.fn(),
onGetLabelNames: async () => ['foo', 'bar', 'baz'], onGetLabelNames: async () => [
onGetLabelValues: async () => ['bar', 'qux', 'quux'], { label: 'foo', value: 'foo' },
{ label: 'bar', value: 'bar' },
{ label: 'baz', value: 'baz' },
],
onGetLabelValues: async () => [
{ label: 'bar', value: 'bar' },
{ label: 'qux', value: 'qux' },
{ label: 'quux', value: 'quux' },
],
}; };
render(<LabelFilters {...props} labelsFilters={labels} />); render(<LabelFilters {...props} labelsFilters={labels} />);

@ -1,3 +1,4 @@
import { SelectableValue } from '@grafana/data';
import { EditorField, EditorFieldGroup, EditorList } from '@grafana/experimental'; import { EditorField, EditorFieldGroup, EditorList } from '@grafana/experimental';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import React, { useState } from 'react'; import React, { useState } from 'react';
@ -7,8 +8,8 @@ import { LabelFilterItem } from './LabelFilterItem';
export interface Props { export interface Props {
labelsFilters: QueryBuilderLabelFilter[]; labelsFilters: QueryBuilderLabelFilter[];
onChange: (labelFilters: QueryBuilderLabelFilter[]) => void; onChange: (labelFilters: QueryBuilderLabelFilter[]) => void;
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>; onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>; onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
} }
export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues }: Props) { export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues }: Props) {

Loading…
Cancel
Save