diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 169d0a6568d..bb09fdc4e96 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -46,7 +46,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general- | `alertingInsights` | Show the new alerting insights landing page | Yes | | `panelMonitoring` | Enables panel monitoring through logs and measurements | Yes | | `formatString` | Enable format string transformer | Yes | -| `transformationsVariableSupport` | Allows using variables in transformations | Yes | | `kubernetesPlaylists` | Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s | Yes | | `recoveryThreshold` | Enables feature recovery threshold (aka hysteresis) for threshold server-side expression | Yes | | `lokiStructuredMetadata` | Enables the loki data source to request structured metadata from the Loki server | Yes | diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts index 59872c2019b..f9d049f309b 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.test.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.test.ts @@ -1,8 +1,22 @@ +import { + SceneDataNode, + SceneDataTransformer, + SceneDeactivationHandler, + SceneFlexItem, + SceneFlexLayout, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneVariable, + SceneVariableSet, + TestVariable, +} from '@grafana/scenes'; +import { DataTransformerConfig, LoadingState } from '@grafana/schema'; + import { DataFrameView } from '../../dataframe/DataFrameView'; import { toDataFrame } from '../../dataframe/processDataFrame'; -import { ScopedVars } from '../../types/ScopedVars'; -import { FieldType } from '../../types/dataFrame'; -import { DataTransformContext } from '../../types/transformations'; +import { DataFrame, FieldType } from '../../types/dataFrame'; +import { getDefaultTimeRange } from '../../types/time'; import { BinaryOperationID } from '../../utils/binaryOperators'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { UnaryOperationID } from '../../utils/unaryOperators'; @@ -565,51 +579,26 @@ describe('calculateField transformer w/ timeseries', () => { replaceFields: true, }, }; - const context: DataTransformContext = { - interpolate: (target: string | undefined, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!target) { - return ''; - } - const variables: ScopedVars = { - var1: { - value: 'Test', - text: 'Test', - }, - var2: { - value: 5, - text: '5', - }, - __interval: { - value: 10000, - text: '10000', - }, - }; - for (const key in variables) { - if (target === `$${key}`) { - return variables[key]!.value + ''; - } - } - return target; - }, - }; - await expect(transformDataFrame([cfg], [seriesA], context)).toEmitValuesWith((received) => { - const data = received[0]; - const filtered = data[0]; - const rows = new DataFrameView(filtered).toArray(); - expect(rows).toMatchInlineSnapshot(` - [ - { - "Test": 6, - "TheTime": 1000, - }, - { - "Test": 105, - "TheTime": 2000, - }, - ] - `); - }); + const data = setupTransformationScene(seriesA, cfg, [ + new TestVariable({ name: 'var1', value: 'Test' }), + new TestVariable({ name: 'var2', value: 5 }), + ]); + + const filtered = data[0]; + const rows = new DataFrameView(filtered).toArray(); + expect(rows).toMatchInlineSnapshot(` + [ + { + "Test": 6, + "TheTime": 1000, + }, + { + "Test": 105, + "TheTime": 2000, + }, + ] + `); }); it('calculates centered moving average on odd window size', async () => { @@ -750,8 +739,6 @@ describe('calculateField transformer w/ timeseries', () => { await expect(transformDataFrame([cfg], [series])).toEmitValuesWith((received) => { const data = received[0][0]; - //console.log(data.fields); - expect(data.fields.length).toEqual(2); expect(data.fields[1].values).toEqual([1, 1, 3, 3.5, 4.5]); }); @@ -1185,3 +1172,63 @@ describe('calculateField transformer w/ timeseries', () => { }); }); }); + +function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler { + const deactivationHandlers: SceneDeactivationHandler[] = []; + + // Important that variables are activated before other children + if (scene.state.$variables) { + deactivationHandlers.push(activateFullSceneTree(scene.state.$variables)); + } + + scene.forEachChild((child) => { + // For query runners which by default use the container width for maxDataPoints calculation we are setting a width. + // In real life this is done by the React component when VizPanel is rendered. + if ('setContainerWidth' in child) { + // @ts-expect-error + child.setContainerWidth(500); + } + deactivationHandlers.push(activateFullSceneTree(child)); + }); + + deactivationHandlers.push(scene.activate()); + + return () => { + for (const handler of deactivationHandlers) { + handler(); + } + }; +} + +function setupTransformationScene( + inputData: DataFrame, + cfg: DataTransformerConfig, + variables: SceneVariable[] +): DataFrame[] { + class TestSceneObject extends SceneObjectBase<{}> {} + const dataNode = new SceneDataNode({ + data: { + state: LoadingState.Loading, + timeRange: getDefaultTimeRange(), + series: [inputData], + }, + }); + + const transformationNode = new SceneDataTransformer({ + transformations: [cfg], + }); + + const consumer = new TestSceneObject({ + $data: transformationNode, + }); + + const scene = new SceneFlexLayout({ + $data: dataNode, + $variables: new SceneVariableSet({ variables }), + children: [new SceneFlexItem({ body: consumer })], + }); + + activateFullSceneTree(scene); + + return sceneGraph.getData(consumer).state.data?.series!; +} diff --git a/packages/grafana-data/src/transformations/transformers/calculateField.ts b/packages/grafana-data/src/transformations/transformers/calculateField.ts index 80f41cc985e..f7b8989c159 100644 --- a/packages/grafana-data/src/transformations/transformers/calculateField.ts +++ b/packages/grafana-data/src/transformations/transformers/calculateField.ts @@ -139,10 +139,6 @@ export const calculateFieldTransformer: DataTransformerInfo { @@ -573,7 +569,8 @@ function findFieldValuesWithNameOrConstant( } if (value.matcher && value.matcher.id === FieldMatcherID.byName) { - const name = ctx.interpolate(value.matcher.options ?? ''); + const name = value.matcher.options ?? ''; + for (const f of frame.fields) { if (name === getFieldDisplayName(f, frame, allFrames)) { if (f.type === FieldType.boolean) { @@ -584,7 +581,7 @@ function findFieldValuesWithNameOrConstant( } } - const v = parseFloat(value.fixed ?? ctx.interpolate(value.matcher?.options ?? '')); + const v = parseFloat(value.fixed ?? value.matcher?.options ?? ''); if (!isNaN(v)) { return new Array(frame.length).fill(v); } diff --git a/packages/grafana-data/src/transformations/transformers/filter.ts b/packages/grafana-data/src/transformations/transformers/filter.ts index 86cd60d03cf..3be485146c2 100644 --- a/packages/grafana-data/src/transformations/transformers/filter.ts +++ b/packages/grafana-data/src/transformations/transformers/filter.ts @@ -28,15 +28,15 @@ export const filterFieldsTransformer: DataTransformerInfo = { } if (typeof options.include?.options === 'string') { - options.include.options = ctx.interpolate(options.include?.options); + options.include.options = options.include?.options; } else if (typeof options.include?.options?.pattern === 'string') { - options.include.options.pattern = ctx.interpolate(options.include?.options.pattern); + options.include.options.pattern = options.include?.options.pattern; } if (typeof options.exclude?.options === 'string') { - options.exclude.options = ctx.interpolate(options.exclude?.options); + options.exclude.options = options.exclude?.options; } else if (typeof options.exclude?.options?.pattern === 'string') { - options.exclude.options.pattern = ctx.interpolate(options.exclude?.options.pattern); + options.exclude.options.pattern = options.exclude?.options.pattern; } return source.pipe( diff --git a/packages/grafana-data/src/transformations/transformers/filterByName.test.ts b/packages/grafana-data/src/transformations/transformers/filterByName.test.ts index 82fe567ee3d..04929528e05 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByName.test.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByName.test.ts @@ -1,6 +1,21 @@ +import { + SceneDataNode, + SceneDataTransformer, + SceneDeactivationHandler, + SceneFlexItem, + SceneFlexLayout, + sceneGraph, + SceneObject, + SceneObjectBase, + SceneVariable, + SceneVariableSet, + TestVariable, +} from '@grafana/scenes'; +import { DataTransformerConfig, LoadingState } from '@grafana/schema'; + import { toDataFrame } from '../../dataframe/processDataFrame'; -import { ScopedVars } from '../../types/ScopedVars'; -import { FieldType } from '../../types/dataFrame'; +import { DataFrame, FieldType } from '../../types/dataFrame'; +import { getDefaultTimeRange } from '../../types/time'; import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; import { transformDataFrame } from '../transformDataFrame'; @@ -196,6 +211,7 @@ describe('filterByName transformer', () => { expect(filtered.fields[0].name).toBe('B'); }); }); + it('it can use a variable with multiple comma separated', async () => { const cfg = { id: DataTransformerID.filterFieldsByName, @@ -207,31 +223,13 @@ describe('filterByName transformer', () => { }, }; - const ctx = { - interpolate: (target: string | undefined, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!target) { - return ''; - } - const variables: ScopedVars = { - var: { - value: 'B,D', - text: 'Test', - }, - }; - for (const key in variables) { - return target.replace(`$${key}`, variables[key]!.value); - } - return target; - }, - }; - - await expect(transformDataFrame([cfg], [seriesWithNamesToMatch], ctx)).toEmitValuesWith((received) => { - const data = received[0]; - const filtered = data[0]; - expect(filtered.fields.length).toBe(2); - expect(filtered.fields[0].name).toBe('B'); - expect(filtered.fields[1].name).toBe('D'); - }); + const data = setupTransformationScene(seriesWithNamesToMatch, cfg, [ + new TestVariable({ name: 'var', value: 'B,D' }), + ]); + const filtered = data[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('B'); + expect(filtered.fields[1].name).toBe('D'); }); it('it can use a variable with multiple comma separated values in {}', async () => { @@ -245,31 +243,14 @@ describe('filterByName transformer', () => { }, }; - const ctx = { - interpolate: (target: string | undefined, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!target) { - return ''; - } - const variables: ScopedVars = { - var: { - value: '{B,D}', - text: 'Test', - }, - }; - for (const key in variables) { - return target.replace(`$${key}`, variables[key]!.value); - } - return target; - }, - }; + const data = setupTransformationScene(seriesWithNamesToMatch, cfg, [ + new TestVariable({ name: 'var', value: 'B,D' }), + ]); - await expect(transformDataFrame([cfg], [seriesWithNamesToMatch], ctx)).toEmitValuesWith((received) => { - const data = received[0]; - const filtered = data[0]; - expect(filtered.fields.length).toBe(2); - expect(filtered.fields[0].name).toBe('B'); - expect(filtered.fields[1].name).toBe('D'); - }); + const filtered = data[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('B'); + expect(filtered.fields[1].name).toBe('D'); }); it('uses template variable substitution', async () => { @@ -277,35 +258,78 @@ describe('filterByName transformer', () => { id: DataTransformerID.filterFieldsByName, options: { include: { - pattern: '/^$var1/', + pattern: '/^$var/', }, }, }; - const ctx = { - interpolate: (target: string | undefined, scopedVars?: ScopedVars, format?: string | Function): string => { - if (!target) { - return ''; - } - const variables: ScopedVars = { - var1: { - value: 'startsWith', - text: 'Test', - }, - }; - for (const key in variables) { - return target.replace(`$${key}`, variables[key]!.value); - } - return target; - }, - }; + const data = setupTransformationScene(seriesWithNamesToMatch, cfg, [ + new TestVariable({ name: 'var', value: 'startsWith' }), + ]); - await expect(transformDataFrame([cfg], [seriesWithNamesToMatch], ctx)).toEmitValuesWith((received) => { - const data = received[0]; - const filtered = data[0]; - expect(filtered.fields.length).toBe(2); - expect(filtered.fields[0].name).toBe('startsWithA'); - }); + const filtered = data[0]; + expect(filtered.fields.length).toBe(2); + expect(filtered.fields[0].name).toBe('startsWithA'); }); }); }); + +function activateFullSceneTree(scene: SceneObject): SceneDeactivationHandler { + const deactivationHandlers: SceneDeactivationHandler[] = []; + + // Important that variables are activated before other children + if (scene.state.$variables) { + deactivationHandlers.push(activateFullSceneTree(scene.state.$variables)); + } + + scene.forEachChild((child) => { + // For query runners which by default use the container width for maxDataPoints calculation we are setting a width. + // In real life this is done by the React component when VizPanel is rendered. + if ('setContainerWidth' in child) { + // @ts-expect-error + child.setContainerWidth(500); + } + deactivationHandlers.push(activateFullSceneTree(child)); + }); + + deactivationHandlers.push(scene.activate()); + + return () => { + for (const handler of deactivationHandlers) { + handler(); + } + }; +} + +export function setupTransformationScene( + inputData: DataFrame, + cfg: DataTransformerConfig, + variables: SceneVariable[] +): DataFrame[] { + class TestSceneObject extends SceneObjectBase<{}> {} + const dataNode = new SceneDataNode({ + data: { + state: LoadingState.Loading, + timeRange: getDefaultTimeRange(), + series: [inputData], + }, + }); + + const transformationNode = new SceneDataTransformer({ + transformations: [cfg], + }); + + const consumer = new TestSceneObject({ + $data: transformationNode, + }); + + const scene = new SceneFlexLayout({ + $data: dataNode, + $variables: new SceneVariableSet({ variables }), + children: [new SceneFlexItem({ body: consumer })], + }); + + activateFullSceneTree(scene); + + return sceneGraph.getData(consumer).state.data?.series!; +} diff --git a/packages/grafana-data/src/transformations/transformers/filterByName.ts b/packages/grafana-data/src/transformations/transformers/filterByName.ts index 0ecc422e866..0f58d0b3f95 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByName.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByName.ts @@ -1,4 +1,4 @@ -import { DataTransformContext, DataTransformerInfo, MatcherConfig } from '../../types/transformations'; +import { DataTransformerInfo, MatcherConfig } from '../../types/transformations'; import { FieldMatcherID } from '../matchers/ids'; import { RegexpOrNamesMatcherOptions } from '../matchers/nameMatcher'; @@ -25,8 +25,8 @@ export const filterFieldsByNameTransformer: DataTransformerInfo { @@ -46,7 +45,8 @@ export const getMatcherConfig = ( const { names, pattern, variable } = options; if (byVariable && variable) { - const stringOfNames = ctx.interpolate(variable); + const stringOfNames = variable; + if (/\{.*\}/.test(stringOfNames)) { const namesFromString = stringOfNames.slice(1).slice(0, -1).split(','); return { id: FieldMatcherID.byNames, options: { names: namesFromString } }; diff --git a/packages/grafana-data/src/transformations/transformers/filterByValue.test.ts b/packages/grafana-data/src/transformations/transformers/filterByValue.test.ts index 05517cf70c4..83d223a0603 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByValue.test.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByValue.test.ts @@ -13,10 +13,6 @@ import { FilterByValueType, } from './filterByValue'; import { DataTransformerID } from './ids'; -import * as utils from './utils'; - -const mockTransformationsVariableSupport = jest.spyOn(utils, 'transformationsVariableSupport'); -mockTransformationsVariableSupport.mockReturnValue(false); const seriesAWithSingleField = toDataFrame({ name: 'A', @@ -260,139 +256,6 @@ describe('FilterByValue transformer', () => { }); }); - it('should interpolate dashboard variables', async () => { - mockTransformationsVariableSupport.mockReturnValue(true); - - const lower: MatcherConfig> = { - id: ValueMatcherID.lower, - options: { value: 'thiswillinterpolateto6' }, - }; - - const cfg: DataTransformerConfig = { - id: DataTransformerID.filterByValue, - options: { - type: FilterByValueType.exclude, - match: FilterByValueMatch.all, - filters: [ - { - fieldName: 'numbers', - config: lower, - }, - ], - }, - }; - - const ctxmock = { interpolate: jest.fn(() => '6') }; - - await expect(transformDataFrame([cfg], [seriesAWithSingleField], ctxmock)).toEmitValuesWith((received) => { - const processed = received[0]; - - expect(processed.length).toEqual(1); - expect(processed[0].fields).toEqual([ - { - name: 'time', - type: FieldType.time, - values: [6000, 7000], - state: {}, - }, - { - name: 'numbers', - type: FieldType.number, - values: [6, 7], - state: {}, - }, - ]); - }); - }); - - it('should interpolate dashboard variables for regex matcher', async () => { - mockTransformationsVariableSupport.mockReturnValue(true); - - const regex: MatcherConfig> = { - id: ValueMatcherID.regex, - options: { value: '.*thiswillinterpolateto6' }, - }; - - const cfg: DataTransformerConfig = { - id: DataTransformerID.filterByValue, - options: { - type: FilterByValueType.include, - match: FilterByValueMatch.all, - filters: [ - { - fieldName: 'numbers', - config: regex, - }, - ], - }, - }; - - const ctxmock = { interpolate: jest.fn(() => '6') }; - - await expect(transformDataFrame([cfg], [seriesAWithSingleField], ctxmock)).toEmitValuesWith((received) => { - const processed = received[0]; - - expect(processed.length).toEqual(1); - expect(processed[0].fields).toEqual([ - { - name: 'time', - type: FieldType.time, - values: [6000], - state: {}, - }, - { - name: 'numbers', - type: FieldType.number, - values: [6], - state: {}, - }, - ]); - }); - }); - - it('should not interpolate dashboard variables when feature toggle is off', async () => { - mockTransformationsVariableSupport.mockReturnValue(false); - - const lower: MatcherConfig> = { - id: ValueMatcherID.lower, - options: { value: 'notinterpolating' }, - }; - - const cfg: DataTransformerConfig = { - id: DataTransformerID.filterByValue, - options: { - type: FilterByValueType.exclude, - match: FilterByValueMatch.all, - filters: [ - { - fieldName: 'numbers', - config: lower, - }, - ], - }, - }; - - await expect(transformDataFrame([cfg], [seriesAWithSingleField])).toEmitValuesWith((received) => { - const processed = received[0]; - - expect(processed.length).toEqual(1); - expect(processed[0].fields).toEqual([ - { - name: 'time', - type: FieldType.time, - values: [1000, 2000, 3000, 4000, 5000, 6000, 7000], - state: {}, - }, - { - name: 'numbers', - type: FieldType.number, - values: [1, 2, 3, 4, 5, 6, 7], - state: {}, - }, - ]); - }); - }); - it('should match any condition', async () => { const lowerOrEqual: MatcherConfig> = { id: ValueMatcherID.lowerOrEqual, diff --git a/packages/grafana-data/src/transformations/transformers/filterByValue.ts b/packages/grafana-data/src/transformations/transformers/filterByValue.ts index 285a0aa293c..0db3ecab179 100644 --- a/packages/grafana-data/src/transformations/transformers/filterByValue.ts +++ b/packages/grafana-data/src/transformations/transformers/filterByValue.ts @@ -4,11 +4,9 @@ import { getFieldDisplayName } from '../../field/fieldState'; import { DataFrame, Field } from '../../types/dataFrame'; import { DataTransformerInfo, MatcherConfig } from '../../types/transformations'; import { getValueMatcher } from '../matchers'; -import { ValueMatcherID } from '../matchers/ids'; import { DataTransformerID } from './ids'; import { noopTransformer } from './noop'; -import { transformationsVariableSupport } from './utils'; export enum FilterByValueType { exclude = 'exclude', @@ -50,50 +48,6 @@ export const filterByValueTransformer: DataTransformerInfo { - if (filter.config.id === ValueMatcherID.between) { - let valueFrom = filter.config.options.from; - let valueTo = filter.config.options.to; - - if (typeof filter.config.options.from === 'string') { - valueFrom = ctx.interpolate(valueFrom); - } - if (typeof filter.config.options.to === 'string') { - valueTo = ctx.interpolate(valueTo); - } - - return { - ...filter, - config: { - ...filter.config, - options: { - ...filter.config.options, - to: valueTo, - from: valueFrom, - }, - }, - }; - } else if (filter.config.options.value) { - let value = filter.config.options.value; - if (typeof filter.config.options.value === 'string') { - value = ctx.interpolate(value); - } - - return { - ...filter, - config: { ...filter.config, options: { ...filter.config.options, value } }, - }; - } - - return filter; - }) - ); - } - return source.pipe( map((data) => { if (data.length === 0) { @@ -106,12 +60,7 @@ export const filterByValueTransformer: DataTransformerInfo(); const fieldIndexByName = groupFieldIndexByName(frame, data); - let matchers; - if (transformationsVariableSupport()) { - matchers = createFilterValueMatchers(interpolatedFilters, fieldIndexByName); - } else { - matchers = createFilterValueMatchers(filters, fieldIndexByName); - } + const matchers = createFilterValueMatchers(filters, fieldIndexByName); for (let index = 0; index < frame.length; index++) { if (rows.has(index)) { diff --git a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts index d3b04d281ca..d39a64cfdce 100644 --- a/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts +++ b/packages/grafana-data/src/transformations/transformers/groupingToMatrix.ts @@ -65,9 +65,9 @@ export const groupingToMatrixTransformer: DataTransformerInfo (source) => source.pipe( map((data) => { - const columnFieldMatch = ctx.interpolate(options.columnField || DEFAULT_COLUMN_FIELD); - const rowFieldMatch = ctx.interpolate(options.rowField || DEFAULT_ROW_FIELD); - const valueFieldMatch = ctx.interpolate(options.valueField || DEFAULT_VALUE_FIELD); + const columnFieldMatch = options.columnField || DEFAULT_COLUMN_FIELD; + const rowFieldMatch = options.rowField || DEFAULT_ROW_FIELD; + const valueFieldMatch = options.valueField || DEFAULT_VALUE_FIELD; const emptyValue = options.emptyValue || DEFAULT_EMPTY_VALUE; // Accept only single queries diff --git a/packages/grafana-data/src/transformations/transformers/histogram.ts b/packages/grafana-data/src/transformations/transformers/histogram.ts index 6d534d22a4b..7e5def97a2f 100644 --- a/packages/grafana-data/src/transformations/transformers/histogram.ts +++ b/packages/grafana-data/src/transformations/transformers/histogram.ts @@ -11,7 +11,6 @@ import { roundDecimals } from '../../utils/numbers'; import { DataTransformerID } from './ids'; import { AlignedData, join } from './joinDataFrames'; import { nullToValueField } from './nulls/nullToValue'; -import { transformationsVariableSupport } from './utils'; /** * @internal @@ -112,9 +111,6 @@ export const histogramTransformer: SynchronousDataTransformerInfo { if (data.length > 1) { if (options.byField && !joinBy) { - joinBy = fieldMatchers.get(FieldMatcherID.byName).get(ctx.interpolate(options.byField)); + joinBy = fieldMatchers.get(FieldMatcherID.byName).get(options.byField); } const joined = joinDataFrames({ frames: data, joinBy, mode: options.mode }); if (joined) { diff --git a/packages/grafana-data/src/transformations/transformers/limit.ts b/packages/grafana-data/src/transformations/transformers/limit.ts index ee51e159543..230e66d9dd3 100644 --- a/packages/grafana-data/src/transformations/transformers/limit.ts +++ b/packages/grafana-data/src/transformations/transformers/limit.ts @@ -3,7 +3,6 @@ import { map } from 'rxjs/operators'; import { DataTransformerInfo } from '../../types/transformations'; import { DataTransformerID } from './ids'; -import { transformationsVariableSupport } from './utils'; export interface LimitTransformerOptions { limitField?: number | string; @@ -25,11 +24,7 @@ export const limitTransformer: DataTransformerInfo = { let limit = DEFAULT_LIMIT_FIELD; if (options.limitField !== undefined) { if (typeof options.limitField === 'string') { - if (transformationsVariableSupport()) { - limit = parseInt(ctx.interpolate(options.limitField), 10); - } else { - limit = parseInt(options.limitField, 10); - } + limit = parseInt(options.limitField, 10); } else { limit = options.limitField; } diff --git a/packages/grafana-data/src/transformations/transformers/sortBy.ts b/packages/grafana-data/src/transformations/transformers/sortBy.ts index 8df10fd7aaa..5238a3b8e9c 100644 --- a/packages/grafana-data/src/transformations/transformers/sortBy.ts +++ b/packages/grafana-data/src/transformations/transformers/sortBy.ts @@ -6,7 +6,6 @@ import { DataFrame } from '../../types/dataFrame'; import { DataTransformContext, DataTransformerInfo } from '../../types/transformations'; import { DataTransformerID } from './ids'; -import { transformationsVariableSupport } from './utils'; export interface SortByField { field: string; @@ -59,12 +58,7 @@ function attachFieldIndex(frame: DataFrame, sort: SortByField[], ctx: DataTransf // null or undefined return s; } - if (transformationsVariableSupport()) { - return { - ...s, - index: frame.fields.findIndex((f) => ctx.interpolate(s.field) === getFieldDisplayName(f, frame)), - }; - } + return { ...s, index: frame.fields.findIndex((f) => s.field === getFieldDisplayName(f, frame)), diff --git a/packages/grafana-data/src/transformations/transformers/utils.ts b/packages/grafana-data/src/transformations/transformers/utils.ts index 699cb3517ca..c10b1e8a1d7 100644 --- a/packages/grafana-data/src/transformations/transformers/utils.ts +++ b/packages/grafana-data/src/transformations/transformers/utils.ts @@ -6,11 +6,6 @@ declare global { grafanaBootData?: BootData; } } - -export const transformationsVariableSupport = () => { - return window?.grafanaBootData?.settings?.featureToggles?.transformationsVariableSupport; -}; - /** * Retrieve the maximum number of fields in a series of a dataframe. */ diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 962d7d8a2d6..6fe30533f7b 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -105,7 +105,6 @@ export interface FeatureToggles { enableNativeHTTPHistogram?: boolean; disableClassicHTTPHistogram?: boolean; formatString?: boolean; - transformationsVariableSupport?: boolean; kubernetesPlaylists?: boolean; kubernetesSnapshots?: boolean; kubernetesDashboards?: boolean; diff --git a/packages/grafana-data/tsconfig.json b/packages/grafana-data/tsconfig.json index d6da863dda3..26f29296c23 100644 --- a/packages/grafana-data/tsconfig.json +++ b/packages/grafana-data/tsconfig.json @@ -8,5 +8,11 @@ }, "exclude": ["dist/**/*"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "typings/jest", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"] + "include": [ + "src/**/*.ts*", + "typings/jest", + "../../public/app/types/jquery/*.ts", + "../../public/app/types/*.d.ts", + "../grafana-ui/src/types/react-table-config.d.ts" + ] } diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index f8e39774f11..5b2ff8583e2 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -658,14 +658,6 @@ var ( Owner: grafanaDatavizSquad, Expression: "true", // enabled by default }, - { - Name: "transformationsVariableSupport", - Description: "Allows using variables in transformations", - FrontendOnly: true, - Stage: FeatureStageGeneralAvailability, - Owner: grafanaDatavizSquad, - Expression: "true", // Enabled by default - }, { Name: "kubernetesPlaylists", Description: "Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index b835fc5cc53..91c27f35805 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -86,7 +86,6 @@ panelMonitoring,GA,@grafana/dataviz-squad,false,false,true enableNativeHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false disableClassicHTTPHistogram,experimental,@grafana/grafana-backend-services-squad,false,true,false formatString,GA,@grafana/dataviz-squad,false,false,true -transformationsVariableSupport,GA,@grafana/dataviz-squad,false,false,true kubernetesPlaylists,GA,@grafana/grafana-app-platform-squad,false,true,false kubernetesSnapshots,experimental,@grafana/grafana-app-platform-squad,false,true,false kubernetesDashboards,experimental,@grafana/grafana-app-platform-squad,false,false,true diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 692b13683e5..62ca16ff004 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -355,10 +355,6 @@ const ( // Enable format string transformer FlagFormatString = "formatString" - // FlagTransformationsVariableSupport - // Allows using variables in transformations - FlagTransformationsVariableSupport = "transformationsVariableSupport" - // FlagKubernetesPlaylists // Use the kubernetes API in the frontend for playlists, and route /api/playlist requests to k8s FlagKubernetesPlaylists = "kubernetesPlaylists" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index ea5d965d4ac..8ee5f1785ca 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -3945,6 +3945,7 @@ "name": "transformationsVariableSupport", "resourceVersion": "1720021873452", "creationTimestamp": "2023-10-04T14:28:46Z", + "deletionTimestamp": "2025-02-06T20:19:53Z", "annotations": { "grafana.app/updatedTimestamp": "2024-07-03 15:51:13.452477 +0000 UTC" } diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx index 3c8fb0aaa3f..6cb7328c59b 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.tsx @@ -1,52 +1,21 @@ import { useCallback, useState } from 'react'; import * as React from 'react'; -import { ValueMatcherID, BasicValueMatcherOptions, VariableOrigin } from '@grafana/data'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; -import { Input } from '@grafana/ui'; +import { ValueMatcherID, BasicValueMatcherOptions } from '@grafana/data'; import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput'; -import { numberOrVariableValidator } from '../../utils'; +import { getVariableSuggestions, numberOrVariableValidator } from '../../utils'; import { ValueMatcherEditorConfig, ValueMatcherUIProps, ValueMatcherUIRegistryItem } from './types'; -import { convertToType } from './utils'; export function basicMatcherEditor( config: ValueMatcherEditorConfig ): React.FC> { - return function Render({ options, onChange, field }) { - const { validator, converter = convertToType } = config; + return function Render({ options, onChange }) { + const { validator } = config; const { value } = options; const [isInvalid, setInvalid] = useState(!validator(value)); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - const onChangeValue = useCallback( - (event: React.FormEvent) => { - setInvalid(!validator(event.currentTarget.value)); - }, - [setInvalid, validator] - ); - - const onChangeOptions = useCallback( - (event: React.FocusEvent) => { - if (isInvalid) { - return; - } - - const { value } = event.currentTarget; - - onChange({ - ...options, - value: converter(value, field), - }); - }, - [options, onChange, isInvalid, field, converter] - ); - const onChangeVariableValue = useCallback( (value: string) => { setInvalid(!validator(value)); @@ -58,29 +27,16 @@ export function basicMatcherEditor( [setInvalid, validator, onChange, options] ); - if (cfg.featureToggles.transformationsVariableSupport) { - return ( - - ); - } else { - return ( - - ); - } + return ( + + ); }; } diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx index 3202d5e37eb..0dd2c09ff92 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.tsx @@ -1,59 +1,26 @@ import { useCallback, useState } from 'react'; import * as React from 'react'; -import { ValueMatcherID, RangeValueMatcherOptions, VariableOrigin } from '@grafana/data'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; -import { InlineLabel, Input } from '@grafana/ui'; +import { ValueMatcherID, RangeValueMatcherOptions } from '@grafana/data'; +import { InlineLabel } from '@grafana/ui'; import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput'; -import { numberOrVariableValidator } from '../../utils'; +import { getVariableSuggestions, numberOrVariableValidator } from '../../utils'; import { ValueMatcherEditorConfig, ValueMatcherUIProps, ValueMatcherUIRegistryItem } from './types'; -import { convertToType } from './utils'; type PropNames = 'from' | 'to'; export function rangeMatcherEditor( config: ValueMatcherEditorConfig ): React.FC>> { - return function RangeMatcherEditor({ options, onChange, field }) { + return function RangeMatcherEditor({ options, onChange }) { const { validator } = config; const [isInvalid, setInvalid] = useState({ from: !validator(options.from), to: !validator(options.to), }); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - const onChangeValue = useCallback( - (event: React.FormEvent, prop: PropNames) => { - setInvalid({ - ...isInvalid, - [prop]: !validator(event.currentTarget.value), - }); - }, - [setInvalid, validator, isInvalid] - ); - - const onChangeOptions = useCallback( - (event: React.FocusEvent, prop: PropNames) => { - if (isInvalid[prop]) { - return; - } - - const { value } = event.currentTarget; - - onChange({ - ...options, - [prop]: convertToType(value, field), - }); - }, - [options, onChange, isInvalid, field] - ); - const onChangeOptionsSuggestions = useCallback( (value: string, prop: PropNames) => { const invalid = !validator(value); @@ -74,45 +41,27 @@ export function rangeMatcherEditor( }, [options, onChange, isInvalid, setInvalid, validator] ); - if (cfg.featureToggles.transformationsVariableSupport) { - return ( - <> - onChangeOptionsSuggestions(val, 'from')} - suggestions={variables} - /> - and - onChangeOptionsSuggestions(val, 'to')} - /> - - ); - } + + const suggestions = getVariableSuggestions(); + return ( <> - onChangeValue(event, 'from')} - onBlur={(event) => onChangeOptions(event, 'from')} + onChange={(val) => onChangeOptionsSuggestions(val, 'from')} + suggestions={suggestions} /> and - onChangeValue(event, 'to')} - onBlur={(event) => onChangeOptions(event, 'to')} + suggestions={suggestions} + onChange={(val) => onChangeOptionsSuggestions(val, 'to')} /> ); diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx index b99f246b358..451bcf65999 100644 --- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx +++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx @@ -1,35 +1,21 @@ import { useCallback, useState } from 'react'; import * as React from 'react'; -import { ValueMatcherID, BasicValueMatcherOptions, VariableOrigin } from '@grafana/data'; -import { config as cfg, getTemplateSrv } from '@grafana/runtime'; -import { Input } from '@grafana/ui'; +import { ValueMatcherID, BasicValueMatcherOptions } from '@grafana/data'; import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput'; +import { getVariableSuggestions } from '../../utils'; import { ValueMatcherEditorConfig, ValueMatcherUIProps, ValueMatcherUIRegistryItem } from './types'; -import { convertToType } from './utils'; export function regexMatcherEditor( config: ValueMatcherEditorConfig ): React.FC>> { - return function Render({ options, onChange, field }) { - const { validator, converter = convertToType } = config; + return function Render({ options, onChange }) { + const { validator } = config; const { value } = options; const [isInvalid, setInvalid] = useState(!validator(value)); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - const onChangeValue = useCallback( - (event: React.FormEvent) => { - setInvalid(!validator(event.currentTarget.value)); - }, - [setInvalid, validator] - ); - const onChangeVariableValue = useCallback( (value: string) => { setInvalid(!validator(value)); @@ -41,42 +27,13 @@ export function regexMatcherEditor( [setInvalid, validator, onChange, options] ); - const onChangeOptions = useCallback( - (event: React.FocusEvent) => { - if (isInvalid) { - return; - } - - const { value } = event.currentTarget; - - onChange({ - ...options, - value: converter(value, field), - }); - }, - [options, onChange, isInvalid, field, converter] - ); - - if (cfg.featureToggles.transformationsVariableSupport) { - return ( - - ); - } - return ( - ); }; @@ -89,7 +46,6 @@ export const getRegexValueMatchersUI = (): Array true, - converter: (value) => String(value), }), }, ]; diff --git a/public/app/features/transformers/calculateHeatmap/heatmap.ts b/public/app/features/transformers/calculateHeatmap/heatmap.ts index 0b086eb0907..6a7a29ce69c 100644 --- a/public/app/features/transformers/calculateHeatmap/heatmap.ts +++ b/public/app/features/transformers/calculateHeatmap/heatmap.ts @@ -16,7 +16,6 @@ import { TimeRange, } from '@grafana/data'; import { isLikelyAscendingVector } from '@grafana/data/src/transformations/transformers/joinDataFrames'; -import { config } from '@grafana/runtime'; import { ScaleDistribution, HeatmapCellLayout, @@ -52,29 +51,7 @@ export const heatmapTransformer: SynchronousDataTransformerInfo (source) => - source.pipe( - map((data) => { - if (config.featureToggles.transformationsVariableSupport) { - const optionsCopy = { - ...options, - xBuckets: { ...options.xBuckets }, - yBuckets: { ...options.yBuckets }, - }; - - if (optionsCopy.xBuckets?.value) { - optionsCopy.xBuckets.value = ctx.interpolate(optionsCopy.xBuckets.value); - } - - if (optionsCopy.yBuckets?.value) { - optionsCopy.yBuckets.value = ctx.interpolate(optionsCopy.yBuckets.value); - } - - return heatmapTransformer.transformer(optionsCopy, ctx)(data); - } else { - return heatmapTransformer.transformer(options, ctx)(data); - } - }) - ), + source.pipe(map((data) => heatmapTransformer.transformer(options, ctx)(data))), transformer: (options: HeatmapTransformerOptions) => { return (data: DataFrame[]) => { diff --git a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx index 9fb3537abd8..bc39a4e2c62 100644 --- a/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx +++ b/public/app/features/transformers/editors/CalculateFieldTransformerEditor/CalculateFieldTransformerEditor.tsx @@ -1,6 +1,6 @@ import { ChangeEvent, useEffect, useState } from 'react'; import * as React from 'react'; -import { identity, of, OperatorFunction } from 'rxjs'; +import { of, OperatorFunction } from 'rxjs'; import { map } from 'rxjs/operators'; import { @@ -81,10 +81,8 @@ export const CalculateFieldTransformerEditor = (props: CalculateFieldTransformer }, [input, configuredOptions]); const getVariableNames = (): OperatorFunction => { - if (!cfg.featureToggles.transformationsVariableSupport) { - return identity; - } const templateSrv = getTemplateSrv(); + return (source) => source.pipe( map((input) => { diff --git a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx index 696f3458d59..a257f02b0af 100644 --- a/public/app/features/transformers/editors/HistogramTransformerEditor.tsx +++ b/public/app/features/transformers/editors/HistogramTransformerEditor.tsx @@ -6,19 +6,16 @@ import { TransformerRegistryItem, TransformerUIProps, TransformerCategory, - VariableOrigin, } from '@grafana/data'; import { histogramFieldInfo, HistogramTransformerInputs, } from '@grafana/data/src/transformations/transformers/histogram'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; import { InlineField, InlineFieldRow, InlineSwitch } from '@grafana/ui'; -import { NumberInput } from 'app/core/components/OptionsUI/NumberInput'; import { getTransformationContent } from '../docs/getTransformationContent'; import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput'; -import { numberOrVariableValidator } from '../utils'; +import { getVariableSuggestions, numberOrVariableValidator } from '../utils'; export const HistogramTransformerEditor = ({ input, @@ -33,36 +30,6 @@ export const HistogramTransformerEditor = ({ bucketOffset: !numberOrVariableValidator(options.bucketOffset || ''), }); - const onBucketCountChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketCount: val, - }); - }, - [onChange, options] - ); - - const onBucketSizeChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketSize: val, - }); - }, - [onChange, options] - ); - - const onBucketOffsetChanged = useCallback( - (val?: number) => { - onChange({ - ...options, - bucketOffset: val, - }); - }, - [onChange, options] - ); - const onVariableBucketCountChanged = useCallback( (value: string) => { setInvalid({ ...isInvalid, bucketCount: !numberOrVariableValidator(value) }); @@ -106,72 +73,7 @@ export const HistogramTransformerEditor = ({ }); }, [onChange, options]); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - if (!cfg.featureToggles.transformationsVariableSupport) { - let bucketSize; - if (typeof options.bucketSize === 'string') { - bucketSize = parseInt(options.bucketSize, 10); - } else { - bucketSize = options.bucketSize; - } - - let bucketOffset; - if (typeof options.bucketOffset === 'string') { - bucketOffset = parseInt(options.bucketOffset, 10); - } else { - bucketOffset = options.bucketOffset; - } - - return ( -
- - - - - - - - - - - - - - - - - - - - -
- ); - } + const suggestions = getVariableSuggestions(); return (
@@ -184,7 +86,7 @@ export const HistogramTransformerEditor = ({ tooltip={histogramFieldInfo.bucketCount.description} > ) => { const [isInvalid, setInvalid] = useState(false); - const onSetLimit = useCallback( - (value: FormEvent) => { - onChange({ - ...options, - limitField: Number(value.currentTarget.value), - }); - }, - [onChange, options] - ); - const onSetVariableLimit = useCallback( (value: string) => { setInvalid(!numberOrVariableValidator(value)); @@ -40,28 +28,6 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps [onChange, options] ); - const templateSrv = getTemplateSrv(); - const variables = templateSrv.getVariables().map((v) => { - return { value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }; - }); - - if (!cfg.featureToggles.transformationsVariableSupport) { - return ( - <> - - - - - - - ); - } return ( <> @@ -71,7 +37,7 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps value={String(options.limitField)} onChange={onSetVariableLimit} placeholder="Value or variable" - suggestions={variables} + suggestions={getVariableSuggestions()} > diff --git a/public/app/features/transformers/editors/SortByTransformerEditor.tsx b/public/app/features/transformers/editors/SortByTransformerEditor.tsx index f0362a65ddb..3faa19ebf8f 100644 --- a/public/app/features/transformers/editors/SortByTransformerEditor.tsx +++ b/public/app/features/transformers/editors/SortByTransformerEditor.tsx @@ -8,7 +8,7 @@ import { TransformerCategory, } from '@grafana/data'; import { SortByField, SortByTransformerOptions } from '@grafana/data/src/transformations/transformers/sortBy'; -import { getTemplateSrv, config as cfg } from '@grafana/runtime'; +import { getTemplateSrv } from '@grafana/runtime'; import { InlineField, InlineSwitch, InlineFieldRow, Select } from '@grafana/ui'; import { getTransformationContent } from '../docs/getTransformationContent'; @@ -36,7 +36,7 @@ export const SortByTransformerEditor = ({ input, options, onChange }: Transforme