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>
pull/92744/head
Ashley Harrison 10 months ago committed by GitHub
parent 2e451b2ed7
commit c16cc488c9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      docs/sources/developers/plugins/plugin.schema.json
  2. 2
      package.json
  3. 1
      packages/grafana-data/src/types/datasource.ts
  4. 1
      packages/grafana-data/src/types/templateVars.ts
  5. 24
      packages/grafana-prometheus/src/datasource.ts
  6. 11
      pkg/api/frontendsettings.go
  7. 12
      pkg/plugins/models.go
  8. 25
      pkg/plugins/plugins.go
  9. 2
      public/app/features/dashboard-scene/scene/setDashboardPanelContext.ts
  10. 2
      public/app/features/dashboard-scene/serialization/transformSceneToSaveModel.test.ts
  11. 1
      public/app/features/dashboard-scene/settings/variables/editors/AdHocFiltersVariableEditor.tsx
  12. 10
      public/app/features/dashboard-scene/utils/variables.test.ts
  13. 8
      public/app/features/dashboard-scene/utils/variables.ts
  14. 2
      public/app/features/trails/DataTrail.tsx
  15. 1
      public/app/plugins/datasource/prometheus/plugin.json
  16. 10
      yarn.lock

@ -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).",

@ -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:*",

@ -142,6 +142,7 @@ export interface DataSourcePluginMeta<T extends KeyValue = {}> extends PluginMet
unlicensed?: boolean;
backend?: boolean;
isBackend?: boolean;
multiValueFilterOperators?: boolean;
}
interface PluginMetaQueryOptions {

@ -53,6 +53,7 @@ export interface AdHocVariableFilter {
key: string;
operator: string;
value: string;
values?: string[];
/** @deprecated */
condition?: string;
}

@ -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<PromQuery>) {
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<T>(value: T) {
export function prometheusSpecialRegexEscape<T>(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,
};
}

@ -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 {

@ -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 {

@ -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"`

@ -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,
});

@ -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);

@ -28,6 +28,7 @@ export function AdHocFiltersVariableEditor(props: AdHocFiltersVariableEditorProp
variable.setState({
datasource: dsRef,
supportsMultiValueOperators: ds.meta.multiValueFilterOperators,
});
};

@ -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,
});
});

@ -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') {

@ -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,
}),
],
});

@ -89,6 +89,7 @@
"queryOptions": {
"minInterval": true
},
"multiValueFilterOperators": true,
"info": {
"description": "Open source time series database & alerting",
"author": {

@ -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"

Loading…
Cancel
Save