diff --git a/docs/sources/developers/plugins/plugin.schema.json b/docs/sources/developers/plugins/plugin.schema.json index 1913d9aef08..3442ae7d2a4 100644 --- a/docs/sources/developers/plugins/plugin.schema.json +++ b/docs/sources/developers/plugins/plugin.schema.json @@ -308,6 +308,10 @@ "type": "boolean", "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": { "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).", diff --git a/package.json b/package.json index 91c01aca4f8..6a50860c13c 100644 --- a/package.json +++ b/package.json @@ -268,7 +268,7 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "^5.10.1", + "@grafana/scenes": "^5.11.0", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/packages/grafana-data/src/types/datasource.ts b/packages/grafana-data/src/types/datasource.ts index 8609dcff2a1..4d5b332350e 100644 --- a/packages/grafana-data/src/types/datasource.ts +++ b/packages/grafana-data/src/types/datasource.ts @@ -142,6 +142,7 @@ export interface DataSourcePluginMeta extends PluginMet unlicensed?: boolean; backend?: boolean; isBackend?: boolean; + multiValueFilterOperators?: boolean; } interface PluginMetaQueryOptions { diff --git a/packages/grafana-data/src/types/templateVars.ts b/packages/grafana-data/src/types/templateVars.ts index 14a1cd38fc8..b8d0de9092a 100644 --- a/packages/grafana-data/src/types/templateVars.ts +++ b/packages/grafana-data/src/types/templateVars.ts @@ -53,6 +53,7 @@ export interface AdHocVariableFilter { key: string; operator: string; value: string; + values?: string[]; /** @deprecated */ condition?: string; } diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 7c3dee6a3ea..637e701507d 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -603,7 +603,7 @@ export class PrometheusDatasource 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, value: f.value, op: f.operator, @@ -620,7 +620,7 @@ export class PrometheusDatasource // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality async getTagValues(options: DataSourceGetTagValuesOptions) { - const labelFilters: QueryBuilderLabelFilter[] = options.filters.map((f) => ({ + const labelFilters: QueryBuilderLabelFilter[] = options.filters.map(remapOneOf).map((f) => ({ label: f.key, value: f.value, op: f.operator, @@ -822,7 +822,7 @@ export class PrometheusDatasource return []; } - return filters.map((f) => ({ + return filters.map(remapOneOf).map((f) => ({ ...f, value: this.templateSrv.replace(f.value, {}, this.interpolateQueryExpr), operator: scopeFilterOperatorMap[f.operator], @@ -834,7 +834,7 @@ export class PrometheusDatasource return expr; } - const finalQuery = filters.reduce((acc, filter) => { + const finalQuery = filters.map(remapOneOf).reduce((acc, filter) => { const { key, operator } = filter; let { value } = filter; if (operator === '=~' || operator === '!~') { @@ -1001,3 +1001,19 @@ export function prometheusRegularEscape(value: T) { export function prometheusSpecialRegexEscape(value: T) { 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, + }; +} diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 9bf8c35ef75..677e8fa98aa 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -449,11 +449,12 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug dsDTO.Preload = plugin.Preload dsDTO.Module = plugin.Module dsDTO.PluginMeta = &plugins.PluginMetaDTO{ - JSONData: plugin.JSONData, - Signature: plugin.Signature, - Module: plugin.Module, - BaseURL: plugin.BaseURL, - Angular: plugin.Angular, + JSONData: plugin.JSONData, + Signature: plugin.Signature, + Module: plugin.Module, + BaseURL: plugin.BaseURL, + Angular: plugin.Angular, + MultiValueFilterOperators: plugin.MultiValueFilterOperators, } if ds.JsonData == nil { diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 96974cbcb1b..f08b192062c 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -171,13 +171,11 @@ type Signature struct { type PluginMetaDTO struct { JSONData - - Signature SignatureStatus `json:"signature"` - - Module string `json:"module"` - BaseURL string `json:"baseUrl"` - - Angular AngularMeta `json:"angular"` + Signature SignatureStatus `json:"signature"` + Module string `json:"module"` + BaseURL string `json:"baseUrl"` + Angular AngularMeta `json:"angular"` + MultiValueFilterOperators bool `json:"multiValueFilterOperators"` } type DataSourceDTO struct { diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index b52585c46ab..5a91ea24b69 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -112,18 +112,19 @@ type JSONData struct { AutoEnabled bool `json:"autoEnabled"` // Datasource settings - Annotations bool `json:"annotations"` - Metrics bool `json:"metrics"` - Alerting bool `json:"alerting"` - Explore bool `json:"explore"` - Table bool `json:"tables"` - Logs bool `json:"logs"` - Tracing bool `json:"tracing"` - QueryOptions map[string]bool `json:"queryOptions,omitempty"` - BuiltIn bool `json:"builtIn,omitempty"` - Mixed bool `json:"mixed,omitempty"` - Streaming bool `json:"streaming"` - SDK bool `json:"sdk,omitempty"` + Annotations bool `json:"annotations"` + Metrics bool `json:"metrics"` + Alerting bool `json:"alerting"` + Explore bool `json:"explore"` + Table bool `json:"tables"` + Logs bool `json:"logs"` + Tracing bool `json:"tracing"` + QueryOptions map[string]bool `json:"queryOptions,omitempty"` + BuiltIn bool `json:"builtIn,omitempty"` + Mixed bool `json:"mixed,omitempty"` + Streaming bool `json:"streaming"` + SDK bool `json:"sdk,omitempty"` + MultiValueFilterOperators bool `json:"multiValueFilterOperators,omitempty"` // Backend (Datasource + Renderer + SecretsManager) Executable string `json:"executable,omitempty"` diff --git a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts index dbbf225c129..b40a86b3618 100644 --- a/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts +++ b/public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts @@ -1,4 +1,5 @@ import { AnnotationChangeEvent, AnnotationEventUIModel, CoreApp, DataFrame } from '@grafana/data'; +import { getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable, dataLayers, sceneGraph, sceneUtils, VizPanel } from '@grafana/scenes'; import { DataSourceRef } from '@grafana/schema'; import { AdHocFilterItem, PanelContext } from '@grafana/ui'; @@ -160,6 +161,7 @@ export function getAdHocFilterVariableFor(scene: DashboardScene, ds: DataSourceR const newVariable = new AdHocFiltersVariable({ name: 'Filters', datasource: ds, + supportsMultiValueOperators: Boolean(getDataSourceSrv().getInstanceSettings(ds)?.meta.multiValueFilterOperators), useQueriesAsFilterForOptions: true, }); diff --git a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts index 9b02fcc2f6a..042d2cd2ce7 100644 --- a/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts +++ b/public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts @@ -136,6 +136,8 @@ jest.mock('@grafana/runtime', () => ({ toDataQuery: (q: StandardVariableQuery) => q, }, }), + // mock getInstanceSettings() + getInstanceSettings: jest.fn(), }), getRunRequest: () => (ds: DataSourceApi, request: DataQueryRequest) => { return runRequestMock(ds, request); diff --git a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx index 5e5ff504462..f165818ebfe 100644 --- a/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx +++ b/public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx @@ -28,6 +28,7 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp variable.setState({ datasource: dsRef, + supportsMultiValueOperators: ds.meta.multiValueFilterOperators, }); }; diff --git a/public/app/features/dashboard-scene/utils/variables.test.ts b/public/app/features/dashboard-scene/utils/variables.test.ts index cc97261c95b..0f3616b2f75 100644 --- a/public/app/features/dashboard-scene/utils/variables.test.ts +++ b/public/app/features/dashboard-scene/utils/variables.test.ts @@ -26,6 +26,14 @@ import { NEW_LINK } from '../settings/links/utils'; 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', () => { it('should migrate custom variable', () => { const variable: CustomVariableModel = { @@ -425,6 +433,7 @@ describe('when creating variables objects', () => { datasource: { uid: 'gdev-prometheus', type: 'prometheus' }, applyMode: 'auto', useQueriesAsFilterForOptions: true, + supportsMultiValueOperators: false, }); }); @@ -508,6 +517,7 @@ describe('when creating variables objects', () => { }, ], useQueriesAsFilterForOptions: true, + supportsMultiValueOperators: false, }); }); diff --git a/public/app/features/dashboard-scene/utils/variables.ts b/public/app/features/dashboard-scene/utils/variables.ts index 3eefdc84fc3..69d15d9daed 100644 --- a/public/app/features/dashboard-scene/utils/variables.ts +++ b/public/app/features/dashboard-scene/utils/variables.ts @@ -1,5 +1,5 @@ import { TypedVariableModel } from '@grafana/data'; -import { config } from '@grafana/runtime'; +import { config, getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable, ConstantVariable, @@ -56,6 +56,9 @@ export function createVariablesForSnapshot(oldModel: DashboardModel) { baseFilters: v.baseFilters ?? [], defaultKeys: v.defaultKeys, useQueriesAsFilterForOptions: true, + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings(v.datasource)?.meta.multiValueFilterOperators + ), }); } // for other variable types we are using the SnapshotVariable @@ -133,6 +136,9 @@ export function createSceneVariableFromVariableModel(variable: TypedVariableMode baseFilters: variable.baseFilters ?? [], defaultKeys: variable.defaultKeys, useQueriesAsFilterForOptions: true, + supportsMultiValueOperators: Boolean( + getDataSourceSrv().getInstanceSettings(variable.datasource)?.meta.multiValueFilterOperators + ), }); } if (variable.type === 'custom') { diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index 2168b2682f8..76aaf11db4b 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -262,6 +262,8 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad layout: 'vertical', filters: initialFilters ?? [], baseFilters: getBaseFiltersForMetric(metric), + // since we only support prometheus datasources, this is always true + supportsMultiValueOperators: true, }), ], }); diff --git a/public/app/plugins/datasource/prometheus/plugin.json b/public/app/plugins/datasource/prometheus/plugin.json index c11bd2e9809..9f4cf3c82e1 100644 --- a/public/app/plugins/datasource/prometheus/plugin.json +++ b/public/app/plugins/datasource/prometheus/plugin.json @@ -89,6 +89,7 @@ "queryOptions": { "minInterval": true }, + "multiValueFilterOperators": true, "info": { "description": "Open source time series database & alerting", "author": { diff --git a/yarn.lock b/yarn.lock index 8cf30d396fe..f7bf01c0297 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3918,9 +3918,9 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes@npm:^5.10.1": - version: 5.10.2 - resolution: "@grafana/scenes@npm:5.10.2" +"@grafana/scenes@npm:^5.11.0": + version: 5.11.0 + resolution: "@grafana/scenes@npm:5.11.0" dependencies: "@grafana/e2e-selectors": "npm:^11.0.0" "@leeoniya/ufuzzy": "npm:^1.0.14" @@ -3935,7 +3935,7 @@ __metadata: "@grafana/ui": ">=10.4" react: ^18.0.0 react-dom: ^18.0.0 - checksum: 10/f7409cc8b7d3687baba7d5af307fd3f836579f20a71e0a782841ba7031d786a2b039757d0e89822af650e825c6b6102571b524a7f24179002da02d2eeaa3cc8b + checksum: 10/4b56c0c831468651f75992979820541c07d3e9cfcb2547c58aed647a60ad02bd8d81b68d233c2ecc401f5b6400e7c29cbe9408f6f5e374bb30161d47ab0f08e2 languageName: node linkType: hard @@ -18495,7 +18495,7 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:^5.10.1" + "@grafana/scenes": "npm:^5.11.0" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0"