mirror of
https://github.com/grafana/grafana.git
synced 2025-07-31 19:52:17 +08:00
AdHocFilters: Add support for new isOneOf
multi value operator (#91837)
* handle oneOf operator in prometheus * use new supportsMultiValueOperators * remap oneOf to regex in prometheus datasource * Remap one of operators for scope filters * use plugin.json property instead of feature toggle * optional chaining * fix unit tests * use getInstanceSettings * update to latest scenes * fix unit tests --------- Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
@ -308,6 +308,10 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor."
|
"description": "For data source plugins, if the plugin supports metric queries. Used to enable the plugin in the panel editor."
|
||||||
},
|
},
|
||||||
|
"multiValueFilterOperators": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "For data source plugins, if the plugin supports multi value operators in adhoc filters."
|
||||||
|
},
|
||||||
"pascalName": {
|
"pascalName": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "[internal only] The PascalCase name for the plugin. Used for creating machine-friendly identifiers, typically in code generation. If not provided, defaults to name, but title-cased and sanitized (only alphabetical characters allowed).",
|
"description": "[internal only] The PascalCase name for the plugin. Used for creating machine-friendly identifiers, typically in code generation. If not provided, defaults to name, but title-cased and sanitized (only alphabetical characters allowed).",
|
||||||
|
@ -268,7 +268,7 @@
|
|||||||
"@grafana/prometheus": "workspace:*",
|
"@grafana/prometheus": "workspace:*",
|
||||||
"@grafana/runtime": "workspace:*",
|
"@grafana/runtime": "workspace:*",
|
||||||
"@grafana/saga-icons": "workspace:*",
|
"@grafana/saga-icons": "workspace:*",
|
||||||
"@grafana/scenes": "^5.10.1",
|
"@grafana/scenes": "^5.11.0",
|
||||||
"@grafana/schema": "workspace:*",
|
"@grafana/schema": "workspace:*",
|
||||||
"@grafana/sql": "workspace:*",
|
"@grafana/sql": "workspace:*",
|
||||||
"@grafana/ui": "workspace:*",
|
"@grafana/ui": "workspace:*",
|
||||||
|
@ -142,6 +142,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet
|
|||||||
unlicensed?: boolean;
|
unlicensed?: boolean;
|
||||||
backend?: boolean;
|
backend?: boolean;
|
||||||
isBackend?: boolean;
|
isBackend?: boolean;
|
||||||
|
multiValueFilterOperators?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PluginMetaQueryOptions {
|
interface PluginMetaQueryOptions {
|
||||||
|
@ -53,6 +53,7 @@ export interface AdHocVariableFilter {
|
|||||||
key: string;
|
key: string;
|
||||||
operator: string;
|
operator: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
values?: string[];
|
||||||
/** @deprecated */
|
/** @deprecated */
|
||||||
condition?: string;
|
condition?: string;
|
||||||
}
|
}
|
||||||
|
@ -603,7 +603,7 @@ export class PrometheusDatasource
|
|||||||
return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k }));
|
return this.languageProvider.getLabelKeys().map((k) => ({ value: k, text: k }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
|
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map(remapOneOf).map((f) => ({
|
||||||
label: f.key,
|
label: f.key,
|
||||||
value: f.value,
|
value: f.value,
|
||||||
op: f.operator,
|
op: f.operator,
|
||||||
@ -620,7 +620,7 @@ export class PrometheusDatasource
|
|||||||
|
|
||||||
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
|
||||||
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>) {
|
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>) {
|
||||||
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({
|
const labelFilters: QueryBuilderLabelFilter[] = options.filters.map(remapOneOf).map((f) => ({
|
||||||
label: f.key,
|
label: f.key,
|
||||||
value: f.value,
|
value: f.value,
|
||||||
op: f.operator,
|
op: f.operator,
|
||||||
@ -822,7 +822,7 @@ export class PrometheusDatasource
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return filters.map((f) => ({
|
return filters.map(remapOneOf).map((f) => ({
|
||||||
...f,
|
...f,
|
||||||
value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr),
|
value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr),
|
||||||
operator: scopeFilterOperatorMap[f.operator],
|
operator: scopeFilterOperatorMap[f.operator],
|
||||||
@ -834,7 +834,7 @@ export class PrometheusDatasource
|
|||||||
return expr;
|
return expr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const finalQuery = filters.reduce((acc, filter) => {
|
const finalQuery = filters.map(remapOneOf).reduce((acc, filter) => {
|
||||||
const { key, operator } = filter;
|
const { key, operator } = filter;
|
||||||
let { value } = filter;
|
let { value } = filter;
|
||||||
if (operator === '=~' || operator === '!~') {
|
if (operator === '=~' || operator === '!~') {
|
||||||
@ -1001,3 +1001,19 @@ export function prometheusRegularEscape<T>(value: T) {
|
|||||||
export function prometheusSpecialRegexEscape<T>(value: T) {
|
export function prometheusSpecialRegexEscape<T>(value: T) {
|
||||||
return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value;
|
return typeof value === 'string' ? value.replace(/\\/g, '\\\\\\\\').replace(/[$^*{}\[\]\'+?.()|]/g, '\\\\$&') : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function remapOneOf(filter: AdHocVariableFilter) {
|
||||||
|
let { operator, value, values } = filter;
|
||||||
|
if (operator === '=|') {
|
||||||
|
operator = '=~';
|
||||||
|
value = values?.map(prometheusRegularEscape).join('|') ?? '';
|
||||||
|
} else if (operator === '!=|') {
|
||||||
|
operator = '!~';
|
||||||
|
value = values?.map(prometheusRegularEscape).join('|') ?? '';
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...filter,
|
||||||
|
operator,
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -454,6 +454,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
|
|||||||
Module: plugin.Module,
|
Module: plugin.Module,
|
||||||
BaseURL: plugin.BaseURL,
|
BaseURL: plugin.BaseURL,
|
||||||
Angular: plugin.Angular,
|
Angular: plugin.Angular,
|
||||||
|
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
|
||||||
}
|
}
|
||||||
|
|
||||||
if ds.JsonData == nil {
|
if ds.JsonData == nil {
|
||||||
|
@ -171,13 +171,11 @@ type Signature struct {
|
|||||||
|
|
||||||
type PluginMetaDTO struct {
|
type PluginMetaDTO struct {
|
||||||
JSONData
|
JSONData
|
||||||
|
|
||||||
Signature SignatureStatus `json:"signature"`
|
Signature SignatureStatus `json:"signature"`
|
||||||
|
|
||||||
Module string `json:"module"`
|
Module string `json:"module"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
|
|
||||||
Angular AngularMeta `json:"angular"`
|
Angular AngularMeta `json:"angular"`
|
||||||
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSourceDTO struct {
|
type DataSourceDTO struct {
|
||||||
|
@ -124,6 +124,7 @@ type JSONData struct {
|
|||||||
Mixed bool `json:"mixed,omitempty"`
|
Mixed bool `json:"mixed,omitempty"`
|
||||||
Streaming bool `json:"streaming"`
|
Streaming bool `json:"streaming"`
|
||||||
SDK bool `json:"sdk,omitempty"`
|
SDK bool `json:"sdk,omitempty"`
|
||||||
|
MultiValueFilterOperators bool `json:"multiValueFilterOperators,omitempty"`
|
||||||
|
|
||||||
// Backend (Datasource + Renderer + SecretsManager)
|
// Backend (Datasource + Renderer + SecretsManager)
|
||||||
Executable string `json:"executable,omitempty"`
|
Executable string `json:"executable,omitempty"`
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data';
|
import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data';
|
||||||
|
import { getDataSourceSrv } from '@grafana/runtime';
|
||||||
import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
|
import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes';
|
||||||
import { DataSourceRef } from '@grafana/schema';
|
import { DataSourceRef } from '@grafana/schema';
|
||||||
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
|
import { AdHocFilterItem, PanelContext } from '@grafana/ui';
|
||||||
@ -160,6 +161,7 @@ export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceR
|
|||||||
const newVariable = new AdHocFiltersVariable({
|
const newVariable = new AdHocFiltersVariable({
|
||||||
name: 'Filters',
|
name: 'Filters',
|
||||||
datasource: ds,
|
datasource: ds,
|
||||||
|
supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators),
|
||||||
useQueriesAsFilterForOptions: true,
|
useQueriesAsFilterForOptions: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -136,6 +136,8 @@ jest.mock('@grafana/runtime', () => ({
|
|||||||
toDataQuery: (q: StandardVariableQuery) => q,
|
toDataQuery: (q: StandardVariableQuery) => q,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// mock getInstanceSettings()
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
}),
|
}),
|
||||||
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => {
|
||||||
return runRequestMock(ds, request);
|
return runRequestMock(ds, request);
|
||||||
|
@ -28,6 +28,7 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp
|
|||||||
|
|
||||||
variable.setState({
|
variable.setState({
|
||||||
datasource: dsRef,
|
datasource: dsRef,
|
||||||
|
supportsMultiValueOperators: ds.meta.multiValueFilterOperators,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -26,6 +26,14 @@ import { NEW_LINK } from '../settings/links/utils';
|
|||||||
|
|
||||||
import { createSceneVariableFromVariableModel, createVariablesForSnapshot } from './variables';
|
import { createSceneVariableFromVariableModel, createVariablesForSnapshot } from './variables';
|
||||||
|
|
||||||
|
// mock getDataSourceSrv.getInstanceSettings()
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
getDataSourceSrv: () => ({
|
||||||
|
getInstanceSettings: jest.fn(),
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('when creating variables objects', () => {
|
describe('when creating variables objects', () => {
|
||||||
it('should migrate custom variable', () => {
|
it('should migrate custom variable', () => {
|
||||||
const variable: CustomVariableModel = {
|
const variable: CustomVariableModel = {
|
||||||
@ -425,6 +433,7 @@ describe('when creating variables objects', () => {
|
|||||||
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
|
datasource: { uid: 'gdev-prometheus', type: 'prometheus' },
|
||||||
applyMode: 'auto',
|
applyMode: 'auto',
|
||||||
useQueriesAsFilterForOptions: true,
|
useQueriesAsFilterForOptions: true,
|
||||||
|
supportsMultiValueOperators: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -508,6 +517,7 @@ describe('when creating variables objects', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
useQueriesAsFilterForOptions: true,
|
useQueriesAsFilterForOptions: true,
|
||||||
|
supportsMultiValueOperators: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { TypedVariableModel } from '@grafana/data';
|
import { TypedVariableModel } from '@grafana/data';
|
||||||
import { config } from '@grafana/runtime';
|
import { config, getDataSourceSrv } from '@grafana/runtime';
|
||||||
import {
|
import {
|
||||||
AdHocFiltersVariable,
|
AdHocFiltersVariable,
|
||||||
ConstantVariable,
|
ConstantVariable,
|
||||||
@ -56,6 +56,9 @@ export function createVariablesForSnapshot(oldModel: DashboardModel) {
|
|||||||
baseFilters: v.baseFilters ?? [],
|
baseFilters: v.baseFilters ?? [],
|
||||||
defaultKeys: v.defaultKeys,
|
defaultKeys: v.defaultKeys,
|
||||||
useQueriesAsFilterForOptions: true,
|
useQueriesAsFilterForOptions: true,
|
||||||
|
supportsMultiValueOperators: Boolean(
|
||||||
|
getDataSourceSrv().getInstanceSettings(v.datasource)?.meta.multiValueFilterOperators
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// for other variable types we are using the SnapshotVariable
|
// for other variable types we are using the SnapshotVariable
|
||||||
@ -133,6 +136,9 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode
|
|||||||
baseFilters: variable.baseFilters ?? [],
|
baseFilters: variable.baseFilters ?? [],
|
||||||
defaultKeys: variable.defaultKeys,
|
defaultKeys: variable.defaultKeys,
|
||||||
useQueriesAsFilterForOptions: true,
|
useQueriesAsFilterForOptions: true,
|
||||||
|
supportsMultiValueOperators: Boolean(
|
||||||
|
getDataSourceSrv().getInstanceSettings(variable.datasource)?.meta.multiValueFilterOperators
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (variable.type === 'custom') {
|
if (variable.type === 'custom') {
|
||||||
|
@ -262,6 +262,8 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad
|
|||||||
layout: 'vertical',
|
layout: 'vertical',
|
||||||
filters: initialFilters ?? [],
|
filters: initialFilters ?? [],
|
||||||
baseFilters: getBaseFiltersForMetric(metric),
|
baseFilters: getBaseFiltersForMetric(metric),
|
||||||
|
// since we only support prometheus datasources, this is always true
|
||||||
|
supportsMultiValueOperators: true,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@ -89,6 +89,7 @@
|
|||||||
"queryOptions": {
|
"queryOptions": {
|
||||||
"minInterval": true
|
"minInterval": true
|
||||||
},
|
},
|
||||||
|
"multiValueFilterOperators": true,
|
||||||
"info": {
|
"info": {
|
||||||
"description": "Open source time series database & alerting",
|
"description": "Open source time series database & alerting",
|
||||||
"author": {
|
"author": {
|
||||||
|
10
yarn.lock
10
yarn.lock
@ -3918,9 +3918,9 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
"@grafana/scenes@npm:^5.10.1":
|
"@grafana/scenes@npm:^5.11.0":
|
||||||
version: 5.10.2
|
version: 5.11.0
|
||||||
resolution: "@grafana/scenes@npm:5.10.2"
|
resolution: "@grafana/scenes@npm:5.11.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@grafana/e2e-selectors": "npm:^11.0.0"
|
"@grafana/e2e-selectors": "npm:^11.0.0"
|
||||||
"@leeoniya/ufuzzy": "npm:^1.0.14"
|
"@leeoniya/ufuzzy": "npm:^1.0.14"
|
||||||
@ -3935,7 +3935,7 @@ __metadata:
|
|||||||
"@grafana/ui": ">=10.4"
|
"@grafana/ui": ">=10.4"
|
||||||
react: ^18.0.0
|
react: ^18.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0
|
||||||
checksum: 10/f7409cc8b7d3687baba7d5af307fd3f836579f20a71e0a782841ba7031d786a2b039757d0e89822af650e825c6b6102571b524a7f24179002da02d2eeaa3cc8b
|
checksum: 10/4b56c0c831468651f75992979820541c07d3e9cfcb2547c58aed647a60ad02bd8d81b68d233c2ecc401f5b6400e7c29cbe9408f6f5e374bb30161d47ab0f08e2
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -18495,7 +18495,7 @@ __metadata:
|
|||||||
"@grafana/prometheus": "workspace:*"
|
"@grafana/prometheus": "workspace:*"
|
||||||
"@grafana/runtime": "workspace:*"
|
"@grafana/runtime": "workspace:*"
|
||||||
"@grafana/saga-icons": "workspace:*"
|
"@grafana/saga-icons": "workspace:*"
|
||||||
"@grafana/scenes": "npm:^5.10.1"
|
"@grafana/scenes": "npm:^5.11.0"
|
||||||
"@grafana/schema": "workspace:*"
|
"@grafana/schema": "workspace:*"
|
||||||
"@grafana/sql": "workspace:*"
|
"@grafana/sql": "workspace:*"
|
||||||
"@grafana/tsconfig": "npm:^2.0.0"
|
"@grafana/tsconfig": "npm:^2.0.0"
|
||||||
|
Reference in New Issue
Block a user