mirror of
https://github.com/grafana/grafana.git
synced 2025-09-24 07:54:12 +08:00
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>
This commit is contained in:
@ -54,6 +54,7 @@ exports[`LokiExploreQueryEditor should render component 1`] = `
|
||||
datasource={
|
||||
Object {
|
||||
"getTimeRangeParams": [Function],
|
||||
"interpolateString": [Function],
|
||||
"languageProvider": LokiLanguageProvider {
|
||||
"cleanText": [Function],
|
||||
"datasource": [Circular],
|
||||
|
@ -754,6 +754,14 @@ export class LokiDatasource
|
||||
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) {
|
||||
|
@ -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', () => {
|
||||
it('returns all label suggestions on empty selector', async () => {
|
||||
const datasource = makeMockLokiDatasource({ label1: [], label2: [] });
|
||||
|
@ -396,15 +396,16 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
* @param name
|
||||
*/
|
||||
fetchSeriesLabels = async (match: string): Promise<Record<string, string[]>> => {
|
||||
const interpolatedMatch = this.datasource.interpolateString(match);
|
||||
const url = '/loki/api/v1/series';
|
||||
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);
|
||||
if (!value) {
|
||||
// Clear value when requesting new one. Empty object being truthy also makes sure we don't request twice.
|
||||
this.seriesCache.set(cacheKey, {});
|
||||
const params = { 'match[]': match, start, end };
|
||||
const params = { 'match[]': interpolatedMatch, start, end };
|
||||
const data = await this.request(url, params);
|
||||
const { values } = processLabels(data);
|
||||
value = values;
|
||||
@ -442,11 +443,12 @@ export default class LokiLanguageProvider extends LanguageProvider {
|
||||
}
|
||||
|
||||
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 { start, end } = rangeParams;
|
||||
|
||||
const cacheKey = this.generateCacheKey(url, start, end, key);
|
||||
const cacheKey = this.generateCacheKey(url, start, end, interpolatedKey);
|
||||
const params = { start, end };
|
||||
|
||||
let labelValues = this.labelsCache.get(cacheKey);
|
||||
|
@ -43,6 +43,7 @@ export function makeMockLokiDatasource(labelsAndValues: Labels, series?: SeriesF
|
||||
}
|
||||
}
|
||||
},
|
||||
interpolateString: (string: string) => string,
|
||||
} 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 { QueryBuilderLabelFilter } from 'app/plugins/datasource/prometheus/querybuilder/shared/types';
|
||||
import { lokiQueryModeller } from '../LokiQueryModeller';
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||
import { EditorRow, EditorRows } from '@grafana/experimental';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
|
||||
@ -22,6 +22,11 @@ export const LokiQueryBuilder = React.memo<Props>(({ datasource, query, nested,
|
||||
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 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 result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||
return result[forLabel.label] ?? [];
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result[forLabelInterpolated] ?? [];
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorRows>
|
||||
<EditorRow>
|
||||
<LabelFilters
|
||||
onGetLabelNames={onGetLabelNames}
|
||||
onGetLabelValues={onGetLabelValues}
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelNames(forLabel))
|
||||
}
|
||||
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelValues(forLabel))
|
||||
}
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
/>
|
||||
|
@ -987,6 +987,14 @@ export class PrometheusDatasource
|
||||
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 = {
|
||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||
getTimeRangeParams: () => ({ start: '0', end: '1' }),
|
||||
interpolateString: (string: string) => string,
|
||||
} as any as PrometheusDatasource;
|
||||
|
||||
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', () => {
|
||||
it('returns no suggestions on empty context', async () => {
|
||||
const instance = new LanguageProvider(datasource);
|
||||
@ -266,6 +302,7 @@ describe('Language completion provider', () => {
|
||||
const datasources: PrometheusDatasource = {
|
||||
metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] as any[] } }),
|
||||
getTimeRangeParams: () => ({ start: '0', end: '1' }),
|
||||
interpolateString: (string: string) => string,
|
||||
} as any as PrometheusDatasource;
|
||||
const instance = new LanguageProvider(datasources);
|
||||
const value = Plain.deserialize('metric{}');
|
||||
@ -299,6 +336,7 @@ describe('Language completion provider', () => {
|
||||
},
|
||||
}),
|
||||
getTimeRangeParams: () => ({ start: '0', end: '1' }),
|
||||
interpolateString: (string: string) => string,
|
||||
} as any as PrometheusDatasource;
|
||||
const instance = new LanguageProvider(datasource);
|
||||
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
|
||||
@ -536,6 +574,7 @@ describe('Language completion provider', () => {
|
||||
const datasource: PrometheusDatasource = {
|
||||
metadataRequest: jest.fn(() => ({ data: { data: [] as any[] } })),
|
||||
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
|
||||
interpolateString: (string: string) => string,
|
||||
} as any as PrometheusDatasource;
|
||||
|
||||
const instance = new LanguageProvider(datasource);
|
||||
@ -586,6 +625,7 @@ describe('Language completion provider', () => {
|
||||
metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
|
||||
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
|
||||
lookupsDisabled: false,
|
||||
interpolateString: (string: string) => string,
|
||||
} as any as PrometheusDatasource;
|
||||
const instance = new LanguageProvider(datasource);
|
||||
|
||||
|
@ -460,7 +460,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
|
||||
fetchLabelValues = async (key: string): Promise<string[]> => {
|
||||
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);
|
||||
};
|
||||
|
||||
@ -491,10 +491,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
||||
* @param withName
|
||||
*/
|
||||
fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => {
|
||||
const interpolatedName = this.datasource.interpolateString(name);
|
||||
const range = this.datasource.getTimeRangeParams();
|
||||
const urlParams = {
|
||||
...range,
|
||||
'match[]': name,
|
||||
'match[]': interpolatedName,
|
||||
};
|
||||
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.
|
||||
@ -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
|
||||
// when user does not the newest values for a minute if already cached.
|
||||
const cacheParams = new URLSearchParams({
|
||||
'match[]': name,
|
||||
'match[]': interpolatedName,
|
||||
start: roundSecToMin(parseInt(range.start, 10)).toString(),
|
||||
end: roundSecToMin(parseInt(range.end, 10)).toString(),
|
||||
withName: withName ? 'true' : 'false',
|
||||
|
@ -8,7 +8,7 @@ import { css } from '@emotion/css';
|
||||
export interface Props {
|
||||
query: PromVisualQuery;
|
||||
onChange: (query: PromVisualQuery) => void;
|
||||
onGetMetrics: () => Promise<string[]>;
|
||||
onGetMetrics: () => Promise<SelectableValue[]>;
|
||||
}
|
||||
|
||||
export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
|
||||
@ -18,12 +18,6 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
|
||||
isLoading?: boolean;
|
||||
}>({});
|
||||
|
||||
const loadMetrics = async () => {
|
||||
return await onGetMetrics().then((res) => {
|
||||
return res.map((value) => ({ label: value, value }));
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorFieldGroup>
|
||||
<EditorField label="Metric">
|
||||
@ -35,7 +29,7 @@ export function MetricSelect({ query, onChange, onGetMetrics }: Props) {
|
||||
allowCustomValue
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoading: true });
|
||||
const metrics = await loadMetrics();
|
||||
const metrics = await onGetMetrics();
|
||||
setState({ metrics, isLoading: undefined });
|
||||
}}
|
||||
isLoading={state.isLoading}
|
||||
|
@ -87,12 +87,26 @@ describe('PromQueryBuilder', () => {
|
||||
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 () => {
|
||||
const { languageProvider } = setup();
|
||||
openLabelNameSelect();
|
||||
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 () => {
|
||||
const { languageProvider } = setup({
|
||||
...defaultQuery,
|
||||
@ -117,23 +131,24 @@ describe('PromQueryBuilder', () => {
|
||||
|
||||
function setup(query: PromVisualQuery = defaultQuery) {
|
||||
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
|
||||
const datasource = new PrometheusDatasource(
|
||||
{
|
||||
url: '',
|
||||
jsonData: {},
|
||||
meta: {} as any,
|
||||
} as any,
|
||||
undefined,
|
||||
undefined,
|
||||
languageProvider
|
||||
);
|
||||
const props = {
|
||||
datasource: new PrometheusDatasource(
|
||||
{
|
||||
url: '',
|
||||
jsonData: {},
|
||||
meta: {} as any,
|
||||
} as any,
|
||||
undefined,
|
||||
undefined,
|
||||
languageProvider
|
||||
),
|
||||
datasource,
|
||||
onRunQuery: () => {},
|
||||
onChange: () => {},
|
||||
};
|
||||
|
||||
render(<PromQueryBuilder {...props} query={query} />);
|
||||
return { languageProvider };
|
||||
return { languageProvider, datasource };
|
||||
}
|
||||
|
||||
function getMetricSelect() {
|
||||
|
@ -9,7 +9,7 @@ import { NestedQueryList } from './NestedQueryList';
|
||||
import { promQueryModeller } from '../PromQueryModeller';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { QueryPreview } from './QueryPreview';
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { DataSourceApi, SelectableValue } from '@grafana/data';
|
||||
import { OperationsEditorRow } from '../shared/OperationsEditorRow';
|
||||
|
||||
export interface Props {
|
||||
@ -25,6 +25,12 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
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[]> => {
|
||||
// If no metric we need to use a different method
|
||||
if (!query.metric) {
|
||||
@ -58,7 +64,8 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
const expr = promQueryModeller.renderLabels(labelsToConsider);
|
||||
const result = await datasource.languageProvider.fetchSeriesLabels(expr);
|
||||
return result[forLabel.label] ?? [];
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result[forLabelInterpolated] ?? [];
|
||||
};
|
||||
|
||||
const onGetMetrics = async () => {
|
||||
@ -73,12 +80,20 @@ export const PromQueryBuilder = React.memo<Props>(({ datasource, query, onChange
|
||||
return (
|
||||
<EditorRows>
|
||||
<EditorRow>
|
||||
<MetricSelect query={query} onChange={onChange} onGetMetrics={onGetMetrics} />
|
||||
<MetricSelect
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
onGetMetrics={() => withTemplateVariableOptions(onGetMetrics())}
|
||||
/>
|
||||
<LabelFilters
|
||||
labelsFilters={query.labels}
|
||||
onChange={onChangeLabels}
|
||||
onGetLabelNames={onGetLabelNames}
|
||||
onGetLabelValues={onGetLabelValues}
|
||||
onGetLabelNames={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelNames(forLabel))
|
||||
}
|
||||
onGetLabelValues={(forLabel: Partial<QueryBuilderLabelFilter>) =>
|
||||
withTemplateVariableOptions(onGetLabelValues(forLabel))
|
||||
}
|
||||
/>
|
||||
</EditorRow>
|
||||
<OperationsEditorRow>
|
||||
|
@ -8,8 +8,8 @@ export interface Props {
|
||||
defaultOp: string;
|
||||
item: Partial<QueryBuilderLabelFilter>;
|
||||
onChange: (value: QueryBuilderLabelFilter) => void;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onDelete: () => void;
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
|
||||
allowCustomValue
|
||||
onOpenMenu={async () => {
|
||||
setState({ isLoadingLabelNames: true });
|
||||
const labelNames = (await onGetLabelNames(item)).map((x) => ({ label: x, value: x }));
|
||||
const labelNames = await onGetLabelNames(item);
|
||||
setState({ labelNames, isLoadingLabelNames: undefined });
|
||||
}}
|
||||
isLoading={state.isLoadingLabelNames}
|
||||
@ -90,7 +90,7 @@ export function LabelFilterItem({ item, defaultOp, onChange, onDelete, onGetLabe
|
||||
const labelValues = await onGetLabelValues(item);
|
||||
setState({
|
||||
...state,
|
||||
labelValues: labelValues.map((value) => ({ label: value, value })),
|
||||
labelValues,
|
||||
isLoadingLabelValues: undefined,
|
||||
});
|
||||
}}
|
||||
|
@ -52,8 +52,16 @@ describe('LabelFilters', () => {
|
||||
function setup(labels: QueryBuilderLabelFilter[] = []) {
|
||||
const props = {
|
||||
onChange: jest.fn(),
|
||||
onGetLabelNames: async () => ['foo', 'bar', 'baz'],
|
||||
onGetLabelValues: async () => ['bar', 'qux', 'quux'],
|
||||
onGetLabelNames: async () => [
|
||||
{ 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} />);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup, EditorList } from '@grafana/experimental';
|
||||
import { isEqual } from 'lodash';
|
||||
import React, { useState } from 'react';
|
||||
@ -7,8 +8,8 @@ import { LabelFilterItem } from './LabelFilterItem';
|
||||
export interface Props {
|
||||
labelsFilters: QueryBuilderLabelFilter[];
|
||||
onChange: (labelFilters: QueryBuilderLabelFilter[]) => void;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<string[]>;
|
||||
onGetLabelNames: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
onGetLabelValues: (forLabel: Partial<QueryBuilderLabelFilter>) => Promise<SelectableValue[]>;
|
||||
}
|
||||
|
||||
export function LabelFilters({ labelsFilters, onChange, onGetLabelNames, onGetLabelValues }: Props) {
|
||||
|
Reference in New Issue
Block a user