AdhocFilters: Improve typing and signature of getTagKeys and getTagValues and behaviors (#74962)

* Add adhocFilters to DataQueryRequest

* More changes

* Progress

* Working

* added baseFilters to picker

* Remove unused code

* minor fix
This commit is contained in:
Torkel Ödegaard 2023-09-19 08:24:45 +02:00 committed by GitHub
parent 695c1a08f3
commit 1105b93104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 174 additions and 92 deletions

View File

@ -414,9 +414,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "23"],
[0, 0, 0, "Unexpected any. Specify a different type.", "24"],
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
[0, 0, 0, "Unexpected any. Specify a different type.", "26"]
],
"packages/grafana-data/src/types/explore.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
@ -2869,17 +2867,6 @@ exports[`better eslint`] = {
"public/app/features/variables/adhoc/actions.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/features/variables/adhoc/picker/AdHocFilter.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/variables/adhoc/picker/AdHocFilterBuilder.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/variables/adhoc/picker/AdHocFilterRenderer.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
@ -3956,9 +3943,7 @@ exports[`better eslint`] = {
[0, 0, 0, "Unexpected any. Specify a different type.", "25"],
[0, 0, 0, "Unexpected any. Specify a different type.", "26"],
[0, 0, 0, "Unexpected any. Specify a different type.", "27"],
[0, 0, 0, "Unexpected any. Specify a different type.", "28"],
[0, 0, 0, "Unexpected any. Specify a different type.", "29"],
[0, 0, 0, "Unexpected any. Specify a different type.", "30"]
[0, 0, 0, "Unexpected any. Specify a different type.", "28"]
],
"public/app/plugins/datasource/prometheus/language_provider.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],

View File

@ -14,7 +14,7 @@ import { DataQuery } from './query';
import { RawTimeRange, TimeRange } from './time';
import { CustomVariableSupport, DataSourceVariableSupport, StandardVariableSupport } from './variables';
import { DataSourceRef, WithAccessControlMetadata } from '.';
import { AdHocVariableFilter, DataSourceRef, WithAccessControlMetadata } from '.';
export interface DataSourcePluginOptionsEditorProps<
JSONData extends DataSourceJsonData = DataSourceJsonData,
@ -287,12 +287,12 @@ abstract class DataSourceApi<
/**
* Get tag keys for adhoc filters
*/
getTagKeys?(options?: any): Promise<MetricFindValue[]>;
getTagKeys?(options?: DataSourceGetTagKeysOptions): Promise<MetricFindValue[]>;
/**
* Get tag values for adhoc filters
*/
getTagValues?(options: any): Promise<MetricFindValue[]>;
getTagValues?(options: DataSourceGetTagValuesOptions): Promise<MetricFindValue[]>;
/**
* Set after constructor call, as the data source instance is the most common thing to pass around
@ -370,6 +370,35 @@ abstract class DataSourceApi<
getDefaultQuery?(app: CoreApp): Partial<TQuery>;
}
/**
* Options argument to DataSourceAPI.getTagKeys
*/
export interface DataSourceGetTagKeysOptions {
/**
* The other existing filters or base filters. New in v10.3
*/
filters: AdHocVariableFilter[];
/**
* Context time range. New in v10.3
*/
timeRange?: TimeRange;
}
/**
* Options argument to DataSourceAPI.getTagValues
*/
export interface DataSourceGetTagValuesOptions {
key: string;
/**
* The other existing filters or base filters. New in v10.3
*/
filters: AdHocVariableFilter[];
/**
* Context time range. New in v10.3
*/
timeRange?: TimeRange;
}
export interface MetadataInspectorProps<
DSType extends DataSourceApi<TQuery, TOptions>,
TQuery extends DataQuery = DataQuery,

View File

@ -55,6 +55,10 @@ export interface AdHocVariableModel extends BaseVariableModel {
type: 'adhoc';
datasource: DataSourceRef | null;
filters: AdHocVariableFilter[];
/**
* Filters that are always applied to the lookup of keys. Not shown in the AdhocFilterBuilder UI.
*/
baseFilters?: AdHocVariableFilter[];
}
export interface VariableOption {

View File

@ -12,12 +12,10 @@ import { ConditionSegment } from './ConditionSegment';
interface Props {
datasource: DataSourceRef | null;
filters: AdHocVariableFilter[];
baseFilters?: AdHocVariableFilter[];
addFilter: (filter: AdHocVariableFilter) => void;
removeFilter: (index: number) => void;
changeFilter: (index: number, newFilter: AdHocVariableFilter) => void;
// Passes options to the datasources getTagKeys(options?: any) method
// which is called to fetch the available filter key options in AdHocFilterKey.tsx
getTagKeysOptions?: any;
disabled?: boolean;
}
@ -60,13 +58,21 @@ export class AdHocFilter extends PureComponent<Props> {
datasource={this.props.datasource!}
appendBefore={filters.length > 0 ? <ConditionSegment label="AND" /> : null}
onCompleted={this.appendFilterToVariable}
getTagKeysOptions={this.props.getTagKeysOptions}
allFilters={this.getAllFilters()}
/>
)}
</div>
);
}
getAllFilters() {
if (this.props.baseFilters) {
return this.props.baseFilters.concat(this.props.filters);
}
return this.props.filters;
}
renderFilters(filters: AdHocVariableFilter[], disabled?: boolean) {
if (filters.length === 0 && disabled) {
return <Segment disabled={disabled} value="No filters" options={[]} onChange={() => {}} />;
@ -91,7 +97,7 @@ export class AdHocFilter extends PureComponent<Props> {
onKeyChange={this.onChange(index, 'key')}
onOperatorChange={this.onChange(index, 'operator')}
onValueChange={this.onChange(index, 'value')}
getTagKeysOptions={this.props.getTagKeysOptions}
allFilters={this.getAllFilters()}
/>
</React.Fragment>
);

View File

@ -11,10 +11,10 @@ interface Props {
datasource: DataSourceRef;
onCompleted: (filter: AdHocVariableFilter) => void;
appendBefore?: React.ReactNode;
getTagKeysOptions?: any;
allFilters: AdHocVariableFilter[];
}
export const AdHocFilterBuilder = ({ datasource, appendBefore, onCompleted, getTagKeysOptions }: Props) => {
export const AdHocFilterBuilder = ({ datasource, appendBefore, onCompleted, allFilters }: Props) => {
const [key, setKey] = useState<string | null>(null);
const [operator, setOperator] = useState<string>('=');
@ -49,14 +49,7 @@ export const AdHocFilterBuilder = ({ datasource, appendBefore, onCompleted, getT
);
if (key === null) {
return (
<AdHocFilterKey
datasource={datasource}
filterKey={key}
onChange={onKeyChanged}
getTagKeysOptions={getTagKeysOptions}
/>
);
return <AdHocFilterKey datasource={datasource} filterKey={key} onChange={onKeyChanged} allFilters={allFilters} />;
}
return (
@ -69,7 +62,7 @@ export const AdHocFilterBuilder = ({ datasource, appendBefore, onCompleted, getT
onKeyChange={onKeyChanged}
onOperatorChange={onOperatorChanged}
onValueChange={onValueChanged}
getTagKeysOptions={getTagKeysOptions}
allFilters={allFilters}
/>
</React.Fragment>
);

View File

@ -1,6 +1,6 @@
import React, { ReactElement } from 'react';
import { DataSourceRef, SelectableValue } from '@grafana/data';
import { AdHocVariableFilter, DataSourceRef, SelectableValue } from '@grafana/data';
import { Icon, SegmentAsync } from '@grafana/ui';
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
@ -9,14 +9,14 @@ interface Props {
datasource: DataSourceRef;
filterKey: string | null;
onChange: (item: SelectableValue<string | null>) => void;
getTagKeysOptions?: any;
allFilters: AdHocVariableFilter[];
disabled?: boolean;
}
const MIN_WIDTH = 90;
export const AdHocFilterKey = ({ datasource, onChange, disabled, filterKey, getTagKeysOptions }: Props) => {
const loadKeys = () => fetchFilterKeys(datasource, getTagKeysOptions);
const loadKeysWithRemove = () => fetchFilterKeysWithRemove(datasource, getTagKeysOptions);
export const AdHocFilterKey = ({ datasource, onChange, disabled, filterKey, allFilters }: Props) => {
const loadKeys = () => fetchFilterKeys(datasource, filterKey, allFilters);
const loadKeysWithRemove = () => fetchFilterKeysWithRemove(datasource, filterKey, allFilters);
if (filterKey === null) {
return (
@ -59,7 +59,8 @@ const plusSegment: ReactElement = (
const fetchFilterKeys = async (
datasource: DataSourceRef,
getTagKeysOptions?: any
currentKey: string | null,
allFilters: AdHocVariableFilter[]
): Promise<Array<SelectableValue<string>>> => {
const ds = await getDatasourceSrv().get(datasource);
@ -67,14 +68,16 @@ const fetchFilterKeys = async (
return [];
}
const metrics = await ds.getTagKeys(getTagKeysOptions);
const otherFilters = allFilters.filter((f) => f.key !== currentKey);
const metrics = await ds.getTagKeys({ filters: otherFilters });
return metrics.map((m) => ({ label: m.text, value: m.text }));
};
const fetchFilterKeysWithRemove = async (
datasource: DataSourceRef,
getTagKeysOptions?: any
currentKey: string | null,
allFilters: AdHocVariableFilter[]
): Promise<Array<SelectableValue<string>>> => {
const keys = await fetchFilterKeys(datasource, getTagKeysOptions);
const keys = await fetchFilterKeys(datasource, currentKey, allFilters);
return [REMOVE_VALUE, ...keys];
};

View File

@ -10,6 +10,7 @@ import { OperatorSegment } from './OperatorSegment';
interface Props {
datasource: DataSourceRef;
filter: AdHocVariableFilter;
allFilters: AdHocVariableFilter[];
onKeyChange: (item: SelectableValue<string | null>) => void;
onOperatorChange: (item: SelectableValue<string>) => void;
onValueChange: (item: SelectableValue<string>) => void;
@ -25,7 +26,7 @@ export const AdHocFilterRenderer = ({
onOperatorChange,
onValueChange,
placeHolder,
getTagKeysOptions,
allFilters,
disabled,
}: Props) => {
return (
@ -35,7 +36,7 @@ export const AdHocFilterRenderer = ({
datasource={datasource}
filterKey={key}
onChange={onKeyChange}
getTagKeysOptions={getTagKeysOptions}
allFilters={allFilters}
/>
<div className="gf-form">
<OperatorSegment disabled={disabled} value={operator} onChange={onOperatorChange} />
@ -45,6 +46,7 @@ export const AdHocFilterRenderer = ({
datasource={datasource}
filterKey={key}
filterValue={value}
allFilters={allFilters}
onChange={onValueChange}
placeHolder={placeHolder}
/>

View File

@ -1,6 +1,6 @@
import React from 'react';
import { DataSourceRef, MetricFindValue, SelectableValue } from '@grafana/data';
import { AdHocVariableFilter, DataSourceRef, MetricFindValue, SelectableValue } from '@grafana/data';
import { SegmentAsync } from '@grafana/ui';
import { getDatasourceSrv } from '../../../plugins/datasource_srv';
@ -12,10 +12,19 @@ interface Props {
onChange: (item: SelectableValue<string>) => void;
placeHolder?: string;
disabled?: boolean;
allFilters: AdHocVariableFilter[];
}
export const AdHocFilterValue = ({ datasource, disabled, onChange, filterKey, filterValue, placeHolder }: Props) => {
const loadValues = () => fetchFilterValues(datasource, filterKey);
export const AdHocFilterValue = ({
datasource,
disabled,
onChange,
filterKey,
filterValue,
placeHolder,
allFilters,
}: Props) => {
const loadValues = () => fetchFilterValues(datasource, filterKey, allFilters);
return (
<div className="gf-form" data-testid="AdHocFilterValue-value-wrapper">
@ -31,13 +40,19 @@ export const AdHocFilterValue = ({ datasource, disabled, onChange, filterKey, fi
);
};
const fetchFilterValues = async (datasource: DataSourceRef, key: string): Promise<Array<SelectableValue<string>>> => {
const fetchFilterValues = async (
datasource: DataSourceRef,
key: string,
allFilters: AdHocVariableFilter[]
): Promise<Array<SelectableValue<string>>> => {
const ds = await getDatasourceSrv().get(datasource);
if (!ds || !ds.getTagValues) {
return [];
}
const metrics = await ds.getTagValues({ key });
// Filter out the current filter key from the list of all filters
const otherFilters = allFilters.filter((f) => f.key !== key);
const metrics = await ds.getTagValues({ key, filters: otherFilters });
return metrics.map((m: MetricFindValue) => ({ label: m.text, value: m.text }));
};

View File

@ -42,12 +42,13 @@ export class AdHocPickerUnconnected extends PureComponent<Props> {
};
render() {
const { filters, datasource } = this.props.variable;
const { filters, datasource, baseFilters } = this.props.variable;
return (
<AdHocFilter
datasource={datasource}
filters={filters}
baseFilters={baseFilters}
disabled={this.props.readOnly}
addFilter={this.addFilter}
removeFilter={this.removeFilter}

View File

@ -20,6 +20,8 @@ import {
TIME_SERIES_VALUE_FIELD_NAME,
TimeSeries,
toDataFrame,
DataSourceGetTagKeysOptions,
DataSourceGetTagValuesOptions,
} from '@grafana/data';
import {
BackendDataSourceResponse,
@ -345,27 +347,29 @@ export default class InfluxDatasource extends DataSourceWithBackend<InfluxQuery,
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
// Used in public/app/features/variables/adhoc/picker/AdHocFilterKey.tsx::fetchFilterKeys
getTagKeys(options?: InfluxQuery) {
getTagKeys(options?: DataSourceGetTagKeysOptions) {
const query = buildMetadataQuery({
type: 'TAG_KEYS',
templateService: this.templateSrv,
database: this.database,
measurement: options?.measurement ?? '',
measurement: '',
tags: [],
});
return this.metricFindQuery(query, options);
return this.metricFindQuery(query);
}
getTagValues(options: InfluxQuery) {
getTagValues(options: DataSourceGetTagValuesOptions) {
const query = buildMetadataQuery({
type: 'TAG_VALUES',
templateService: this.templateSrv,
database: this.database,
withKey: options.key ?? '',
measurement: options?.measurement ?? '',
withKey: options.key,
measurement: '',
tags: [],
});
return this.metricFindQuery(query, options);
return this.metricFindQuery(query);
}
/**

View File

@ -91,7 +91,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource }: Props)
const variables = datasource.getVariables().map((variable: string) => ({ label: variable, value: variable }));
if (!metric) {
// get all the labels
datasource.getTagKeys().then((labelNames: Array<{ text: string }>) => {
datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => {
const names = labelNames.map(({ text }) => ({ label: text, value: text }));
setLabelOptions([...variables, ...names]);
});

View File

@ -172,10 +172,14 @@ describe('PrometheusDatasource', () => {
).rejects.toMatchObject({
message: expect.stringMatching('Browser access'),
});
await expect(directDs.getTagKeys()).rejects.toMatchObject({
message: expect.stringMatching('Browser access'),
});
await expect(directDs.getTagValues()).rejects.toMatchObject({
const errorMock = jest.spyOn(console, 'error').mockImplementation(() => {});
await directDs.getTagKeys({ filters: [] });
// Language provider currently catches and just logs the error
expect(errorMock).toHaveBeenCalledTimes(1);
await expect(directDs.getTagValues({ filters: [], key: 'A' })).rejects.toMatchObject({
message: expect.stringMatching('Browser access'),
});
});

View File

@ -24,6 +24,9 @@ import {
ScopedVars,
TimeRange,
renderLegendFormat,
DataSourceGetTagKeysOptions,
DataSourceGetTagValuesOptions,
MetricFindValue,
} from '@grafana/data';
import {
BackendDataSourceResponse,
@ -55,7 +58,8 @@ import {
} from './language_utils';
import PrometheusMetricFindQuery from './metric_find_query';
import { getInitHints, getQueryHints } from './query_hints';
import { QueryEditorMode } from './querybuilder/shared/types';
import { promQueryModeller } from './querybuilder/PromQueryModeller';
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
import { getOriginalMetricName, transform, transformV2 } from './result_transformer';
import { trackQuery } from './tracking';
@ -971,27 +975,50 @@ export class PrometheusDatasource
// this is used to get label keys, a.k.a label names
// it is used in metric_find_query.ts
// and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx
async getTagKeys(options?: { series: string[] }) {
if (options?.series) {
// Get tags for the provided series only
const seriesLabels: Array<Record<string, string[]>> = await Promise.all(
options.series.map((series: string) => this.languageProvider.fetchSeriesLabels(series))
);
// Combines tags from all options.series provided
let tags: string[] = [];
seriesLabels.map((value) => (tags = tags.concat(Object.keys(value))));
const uniqueLabels = [...new Set(tags)];
return uniqueLabels.map((value: any) => ({ text: value }));
} else {
// Get all tags
const params = this.getTimeRangeParams();
const result = await this.metadataRequest('/api/v1/labels', params);
return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];
async getTagKeys(options: DataSourceGetTagKeysOptions): Promise<MetricFindValue[]> {
if (!options || options.filters.length === 0) {
await this.languageProvider.fetchLabels();
return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k }));
}
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
label: f.key,
value: f.value,
op: f.operator,
}));
const expr = promQueryModeller.renderLabels(labelFilters);
let labelsIndex: Record<string, string[]>;
if (this.hasLabelsMatchAPISupport()) {
labelsIndex = await this.languageProvider.fetchSeriesLabelsMatch(expr);
} else {
labelsIndex = await this.languageProvider.fetchSeriesLabels(expr);
}
// filter out already used labels
return Object.keys(labelsIndex)
.filter((labelName) => !options.filters.find((filter) => filter.key === labelName))
.map((k) => ({ value: k, text: k }));
}
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
async getTagValues(options: { key?: string } = {}) {
async getTagValues(options: DataSourceGetTagValuesOptions) {
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
label: f.key,
value: f.value,
op: f.operator,
}));
const expr = promQueryModeller.renderLabels(labelFilters);
if (this.hasLabelsMatchAPISupport()) {
return (await this.languageProvider.fetchSeriesValuesWithMatch(options.key, expr)).map((v) => ({
value: v,
text: v,
}));
}
const params = this.getTimeRangeParams();
const result = await this.metadataRequest(`/api/v1/label/${options.key}/values`, params);
return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];

View File

@ -46,7 +46,7 @@ export default class PrometheusMetricFindQuery {
}
if (labelNamesQuery) {
return this.datasource.getTagKeys();
return this.datasource.getTagKeys({ filters: [] });
}
const labelValuesQuery = this.query.match(labelValuesRegex);

View File

@ -22,7 +22,6 @@ export function ServiceGraphSection({
onChange: (value: TempoQuery) => void;
}) {
const styles = useStyles2(getStyles);
const dsState = useAsync(() => getDS(graphDatasourceUid), [graphDatasourceUid]);
// Check if service graph metrics are being collected. If not, displays a warning
@ -30,10 +29,14 @@ export function ServiceGraphSection({
useEffect(() => {
async function fn(ds: PrometheusDatasource) {
const keys = await ds.getTagKeys({
series: [
'traces_service_graph_request_server_seconds_sum',
'traces_service_graph_request_total',
'traces_service_graph_request_failed_total',
filters: [
{
key: '__name__',
operator: '=~',
value:
'traces_service_graph_request_server_seconds_sum|traces_service_graph_request_total|traces_service_graph_request_failed_total',
condition: '',
},
],
});
setHasKeys(Boolean(keys.length));
@ -61,6 +64,7 @@ export function ServiceGraphSection({
</div>
);
}
const filters = queryToFilter(query.serviceMapQuery || '');
return (
@ -70,9 +74,14 @@ export function ServiceGraphSection({
<AdHocFilter
datasource={{ uid: graphDatasourceUid }}
filters={filters}
getTagKeysOptions={{
series: ['traces_service_graph_request_total', 'traces_spanmetrics_calls_total'],
}}
baseFilters={[
{
key: '__name__',
operator: '=~',
value: 'traces_service_graph_request_total|traces_spanmetrics_calls_total',
condition: '',
},
]}
addFilter={(filter: AdHocVariableFilter) => {
onChange({
...query,