Transformation: Add support for variables to ALL transformations (#100225)
* Transformation: generic support for variables, yay
* clean up value matcher editors
* clean
* clean up filterByValue
* more
* clean
* clean
* more!
* lint
* Revert "Transformation: generic support for variables, yay"
This reverts commit deec7c306b.
* Bump scenes to canary
* filterByValue - remove irrelevant tests
* filterByName tests update
* calculateField test update
* nit
* is that gonna work?
* Update grafana-data tscnig config to use grafana-ui's react table types
---------
Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
Co-authored-by: oscarkilhed <oscar.kilhed@grafana.com>
This commit is contained in:
co-authored by
Leon Sorokin
oscarkilhed
parent
f15c961a90
commit
c9250c9135
@@ -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 |
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -139,10 +139,6 @@ export const calculateFieldTransformer: DataTransformerInfo<CalculateFieldTransf
|
||||
? ensureColumnsTransformer.operator(null, ctx)
|
||||
: noopTransformer.operator({}, ctx);
|
||||
|
||||
if (options.alias != null) {
|
||||
options.alias = ctx.interpolate(options.alias);
|
||||
}
|
||||
|
||||
return outerSource.pipe(
|
||||
operator,
|
||||
map((data) => {
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -28,15 +28,15 @@ export const filterFieldsTransformer: DataTransformerInfo<FilterOptions> = {
|
||||
}
|
||||
|
||||
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(
|
||||
|
||||
@@ -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!;
|
||||
}
|
||||
|
||||
@@ -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<FilterFieldsByNa
|
||||
source.pipe(
|
||||
filterFieldsTransformer.operator(
|
||||
{
|
||||
include: getMatcherConfig(ctx, options.include, options.byVariable),
|
||||
exclude: getMatcherConfig(ctx, options.exclude, options.byVariable),
|
||||
include: getMatcherConfig(options.include, options.byVariable),
|
||||
exclude: getMatcherConfig(options.exclude, options.byVariable),
|
||||
},
|
||||
ctx
|
||||
)
|
||||
@@ -35,7 +35,6 @@ export const filterFieldsByNameTransformer: DataTransformerInfo<FilterFieldsByNa
|
||||
|
||||
// Exported to share with other implementations, but not exported to `@grafana/data`
|
||||
export const getMatcherConfig = (
|
||||
ctx: DataTransformContext,
|
||||
options?: RegexpOrNamesMatcherOptions,
|
||||
byVariable?: boolean
|
||||
): MatcherConfig | undefined => {
|
||||
@@ -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 } };
|
||||
|
||||
@@ -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<BasicValueMatcherOptions<string | number>> = {
|
||||
id: ValueMatcherID.lower,
|
||||
options: { value: 'thiswillinterpolateto6' },
|
||||
};
|
||||
|
||||
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
|
||||
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<BasicValueMatcherOptions<string | number>> = {
|
||||
id: ValueMatcherID.regex,
|
||||
options: { value: '.*thiswillinterpolateto6' },
|
||||
};
|
||||
|
||||
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
|
||||
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<BasicValueMatcherOptions<number | string>> = {
|
||||
id: ValueMatcherID.lower,
|
||||
options: { value: 'notinterpolating' },
|
||||
};
|
||||
|
||||
const cfg: DataTransformerConfig<FilterByValueTransformerOptions> = {
|
||||
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<BasicValueMatcherOptions<number>> = {
|
||||
id: ValueMatcherID.lowerOrEqual,
|
||||
|
||||
@@ -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<FilterByValueTransfor
|
||||
return source.pipe(noopTransformer.operator({}, ctx));
|
||||
}
|
||||
|
||||
const interpolatedFilters: FilterByValueFilter[] = [];
|
||||
|
||||
if (transformationsVariableSupport()) {
|
||||
interpolatedFilters.push(
|
||||
...filters.map((filter) => {
|
||||
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<FilterByValueTransfor
|
||||
const rows = new Set<number>();
|
||||
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)) {
|
||||
|
||||
@@ -65,9 +65,9 @@ export const groupingToMatrixTransformer: DataTransformerInfo<GroupingToMatrixTr
|
||||
operator: (options: GroupingToMatrixTransformerOptions, ctx: DataTransformContext) => (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
|
||||
|
||||
@@ -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<HistogramTrans
|
||||
bucketOffset: number | undefined = undefined;
|
||||
|
||||
if (options.bucketSize) {
|
||||
if (transformationsVariableSupport()) {
|
||||
options.bucketSize = ctx.interpolate(options.bucketSize.toString());
|
||||
}
|
||||
if (typeof options.bucketSize === 'string') {
|
||||
bucketSize = parseFloat(options.bucketSize);
|
||||
} else {
|
||||
@@ -127,9 +123,6 @@ export const histogramTransformer: SynchronousDataTransformerInfo<HistogramTrans
|
||||
}
|
||||
|
||||
if (options.bucketOffset) {
|
||||
if (transformationsVariableSupport()) {
|
||||
options.bucketOffset = ctx.interpolate(options.bucketOffset.toString());
|
||||
}
|
||||
if (typeof options.bucketOffset === 'string') {
|
||||
bucketOffset = parseFloat(options.bucketOffset);
|
||||
} else {
|
||||
|
||||
@@ -38,7 +38,7 @@ export const joinByFieldTransformer: SynchronousDataTransformerInfo<JoinByFieldO
|
||||
return (data: DataFrame[]) => {
|
||||
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) {
|
||||
|
||||
@@ -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<LimitTransformerOptions> = {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -105,7 +105,6 @@ export interface FeatureToggles {
|
||||
enableNativeHTTPHistogram?: boolean;
|
||||
disableClassicHTTPHistogram?: boolean;
|
||||
formatString?: boolean;
|
||||
transformationsVariableSupport?: boolean;
|
||||
kubernetesPlaylists?: boolean;
|
||||
kubernetesSnapshots?: boolean;
|
||||
kubernetesDashboards?: boolean;
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+14
-58
@@ -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<T = any>(
|
||||
config: ValueMatcherEditorConfig
|
||||
): React.FC<ValueMatcherUIProps<BasicValueMatcherOptions>> {
|
||||
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<HTMLInputElement>) => {
|
||||
setInvalid(!validator(event.currentTarget.value));
|
||||
},
|
||||
[setInvalid, validator]
|
||||
);
|
||||
|
||||
const onChangeOptions = useCallback(
|
||||
(event: React.FocusEvent<HTMLInputElement>) => {
|
||||
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<T = any>(
|
||||
[setInvalid, validator, onChange, options]
|
||||
);
|
||||
|
||||
if (cfg.featureToggles.transformationsVariableSupport) {
|
||||
return (
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid}
|
||||
value={value}
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
onChange={onChangeVariableValue}
|
||||
placeholder="Value or variable"
|
||||
suggestions={variables}
|
||||
></SuggestionsInput>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Input
|
||||
className="flex-grow-1"
|
||||
invalid={isInvalid}
|
||||
defaultValue={String(options.value)}
|
||||
placeholder="Value"
|
||||
onChange={onChangeValue}
|
||||
onBlur={onChangeOptions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid}
|
||||
value={value}
|
||||
error={'Value needs to be a number or a variable'}
|
||||
onChange={onChangeVariableValue}
|
||||
placeholder="Value or variable"
|
||||
suggestions={getVariableSuggestions()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+19
-70
@@ -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<T = any>(
|
||||
config: ValueMatcherEditorConfig
|
||||
): React.FC<ValueMatcherUIProps<RangeValueMatcherOptions<T>>> {
|
||||
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<HTMLInputElement>, prop: PropNames) => {
|
||||
setInvalid({
|
||||
...isInvalid,
|
||||
[prop]: !validator(event.currentTarget.value),
|
||||
});
|
||||
},
|
||||
[setInvalid, validator, isInvalid]
|
||||
);
|
||||
|
||||
const onChangeOptions = useCallback(
|
||||
(event: React.FocusEvent<HTMLInputElement>, 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<T = any>(
|
||||
},
|
||||
[options, onChange, isInvalid, setInvalid, validator]
|
||||
);
|
||||
if (cfg.featureToggles.transformationsVariableSupport) {
|
||||
return (
|
||||
<>
|
||||
<SuggestionsInput
|
||||
value={String(options.from)}
|
||||
invalid={isInvalid.from}
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
placeholder="From"
|
||||
onChange={(val) => onChangeOptionsSuggestions(val, 'from')}
|
||||
suggestions={variables}
|
||||
/>
|
||||
<InlineLabel>and</InlineLabel>
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid.to}
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
value={String(options.to)}
|
||||
placeholder="To"
|
||||
suggestions={variables}
|
||||
onChange={(val) => onChangeOptionsSuggestions(val, 'to')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const suggestions = getVariableSuggestions();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input
|
||||
invalid={isInvalid['from']}
|
||||
defaultValue={String(options.from)}
|
||||
<SuggestionsInput
|
||||
value={String(options.from)}
|
||||
invalid={isInvalid.from}
|
||||
error={'Value needs to be a number or a variable'}
|
||||
placeholder="From"
|
||||
onChange={(event) => onChangeValue(event, 'from')}
|
||||
onBlur={(event) => onChangeOptions(event, 'from')}
|
||||
onChange={(val) => onChangeOptionsSuggestions(val, 'from')}
|
||||
suggestions={suggestions}
|
||||
/>
|
||||
<InlineLabel>and</InlineLabel>
|
||||
<Input
|
||||
invalid={isInvalid['to']}
|
||||
defaultValue={String(options.to)}
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid.to}
|
||||
error={'Value needs to be a number or a variable'}
|
||||
value={String(options.to)}
|
||||
placeholder="To"
|
||||
onChange={(event) => onChangeValue(event, 'to')}
|
||||
onBlur={(event) => onChangeOptions(event, 'to')}
|
||||
suggestions={suggestions}
|
||||
onChange={(val) => onChangeOptionsSuggestions(val, 'to')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
+9
-53
@@ -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<ValueMatcherUIProps<BasicValueMatcherOptions<string>>> {
|
||||
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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) => {
|
||||
if (isInvalid) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { value } = event.currentTarget;
|
||||
|
||||
onChange({
|
||||
...options,
|
||||
value: converter(value, field),
|
||||
});
|
||||
},
|
||||
[options, onChange, isInvalid, field, converter]
|
||||
);
|
||||
|
||||
if (cfg.featureToggles.transformationsVariableSupport) {
|
||||
return (
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid}
|
||||
value={value}
|
||||
onChange={onChangeVariableValue}
|
||||
placeholder="Value or variable"
|
||||
suggestions={variables}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Input
|
||||
className="flex-grow-1"
|
||||
<SuggestionsInput
|
||||
invalid={isInvalid}
|
||||
defaultValue={String(options.value)}
|
||||
placeholder="Value"
|
||||
onChange={onChangeValue}
|
||||
onBlur={onChangeOptions}
|
||||
value={value}
|
||||
onChange={onChangeVariableValue}
|
||||
placeholder="Value or variable"
|
||||
suggestions={getVariableSuggestions()}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -89,7 +46,6 @@ export const getRegexValueMatchersUI = (): Array<ValueMatcherUIRegistryItem<Basi
|
||||
id: ValueMatcherID.regex,
|
||||
component: regexMatcherEditor({
|
||||
validator: () => true,
|
||||
converter: (value) => String(value),
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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<HeatmapTransform
|
||||
isApplicableDescription:
|
||||
'The Heatmap transformation requires fields with Heatmap compatible data. No fields with Heatmap data could be found.',
|
||||
operator: (options, ctx) => (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[]) => {
|
||||
|
||||
+2
-4
@@ -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<string[], string[]> => {
|
||||
if (!cfg.featureToggles.transformationsVariableSupport) {
|
||||
return identity;
|
||||
}
|
||||
const templateSrv = getTemplateSrv();
|
||||
|
||||
return (source) =>
|
||||
source.pipe(
|
||||
map((input) => {
|
||||
|
||||
@@ -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 (
|
||||
<div>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
labelWidth={labelWidth}
|
||||
label={histogramFieldInfo.bucketCount.name}
|
||||
tooltip={histogramFieldInfo.bucketCount.description}
|
||||
>
|
||||
<NumberInput
|
||||
value={options.bucketCount}
|
||||
placeholder="Default: 30"
|
||||
onChange={onBucketCountChanged}
|
||||
min={0}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
labelWidth={labelWidth}
|
||||
label={histogramFieldInfo.bucketSize.name}
|
||||
tooltip={histogramFieldInfo.bucketSize.description}
|
||||
>
|
||||
<NumberInput value={bucketSize} placeholder="auto" onChange={onBucketSizeChanged} min={0} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
labelWidth={labelWidth}
|
||||
label={histogramFieldInfo.bucketOffset.name}
|
||||
tooltip={histogramFieldInfo.bucketOffset.description}
|
||||
>
|
||||
<NumberInput value={bucketOffset} placeholder="none" onChange={onBucketOffsetChanged} min={0} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
<InlineFieldRow>
|
||||
<InlineField
|
||||
labelWidth={labelWidth}
|
||||
label={histogramFieldInfo.combine.name}
|
||||
tooltip={histogramFieldInfo.combine.description}
|
||||
>
|
||||
<InlineSwitch value={options.combine ?? false} onChange={onToggleCombine} />
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const suggestions = getVariableSuggestions();
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -184,7 +86,7 @@ export const HistogramTransformerEditor = ({
|
||||
tooltip={histogramFieldInfo.bucketCount.description}
|
||||
>
|
||||
<SuggestionsInput
|
||||
suggestions={variables}
|
||||
suggestions={suggestions}
|
||||
value={options.bucketCount}
|
||||
placeholder="Default: 30"
|
||||
onChange={onVariableBucketCountChanged}
|
||||
@@ -200,7 +102,7 @@ export const HistogramTransformerEditor = ({
|
||||
tooltip={histogramFieldInfo.bucketSize.description}
|
||||
>
|
||||
<SuggestionsInput
|
||||
suggestions={variables}
|
||||
suggestions={suggestions}
|
||||
value={options.bucketSize}
|
||||
placeholder="auto"
|
||||
onChange={onVariableBucketSizeChanged}
|
||||
@@ -216,7 +118,7 @@ export const HistogramTransformerEditor = ({
|
||||
error={'Value needs to be an integer or a variable'}
|
||||
>
|
||||
<SuggestionsInput
|
||||
suggestions={variables}
|
||||
suggestions={suggestions}
|
||||
value={options.bucketOffset}
|
||||
placeholder="none"
|
||||
onChange={onVariableBucketOffsetChanged}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FormEvent, useCallback, useState } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
DataTransformerID,
|
||||
@@ -6,29 +6,17 @@ import {
|
||||
TransformerRegistryItem,
|
||||
TransformerUIProps,
|
||||
TransformerCategory,
|
||||
VariableOrigin,
|
||||
} from '@grafana/data';
|
||||
import { LimitTransformerOptions } from '@grafana/data/src/transformations/transformers/limit';
|
||||
import { getTemplateSrv, config as cfg } from '@grafana/runtime';
|
||||
import { InlineField, InlineFieldRow, Input } from '@grafana/ui';
|
||||
import { InlineFieldRow } from '@grafana/ui';
|
||||
|
||||
import { getTransformationContent } from '../docs/getTransformationContent';
|
||||
import { SuggestionsInput } from '../suggestionsInput/SuggestionsInput';
|
||||
import { numberOrVariableValidator } from '../utils';
|
||||
import { getVariableSuggestions, numberOrVariableValidator } from '../utils';
|
||||
|
||||
export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps<LimitTransformerOptions>) => {
|
||||
const [isInvalid, setInvalid] = useState<boolean>(false);
|
||||
|
||||
const onSetLimit = useCallback(
|
||||
(value: FormEvent<HTMLInputElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
<InlineField label="Limit" labelWidth={8}>
|
||||
<Input
|
||||
placeholder="Limit count"
|
||||
pattern="[0-9]*"
|
||||
value={options.limitField}
|
||||
onChange={onSetLimit}
|
||||
width={25}
|
||||
/>
|
||||
</InlineField>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<InlineFieldRow>
|
||||
@@ -71,7 +37,7 @@ export const LimitTransformerEditor = ({ options, onChange }: TransformerUIProps
|
||||
value={String(options.limitField)}
|
||||
onChange={onSetVariableLimit}
|
||||
placeholder="Value or variable"
|
||||
suggestions={variables}
|
||||
suggestions={getVariableSuggestions()}
|
||||
></SuggestionsInput>
|
||||
</InlineFieldRow>
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
<InlineFieldRow key={`${s.field}/${index}`}>
|
||||
<InlineField label="Field" labelWidth={10} grow={true}>
|
||||
<Select
|
||||
options={cfg.featureToggles.transformationsVariableSupport ? [...fieldNames, ...variables] : fieldNames}
|
||||
options={[...fieldNames, ...variables]}
|
||||
value={s.field}
|
||||
placeholder="Select field"
|
||||
onChange={(v) => {
|
||||
|
||||
@@ -75,7 +75,7 @@ export const partitionByValuesTransformer: SynchronousDataTransformerInfo<Partit
|
||||
source.pipe(map((data) => partitionByValuesTransformer.transformer(options, ctx)(data))),
|
||||
|
||||
transformer: (options: PartitionByValuesTransformerOptions, ctx: DataTransformContext) => {
|
||||
const matcherConfig = getMatcherConfig(ctx, { names: options.fields });
|
||||
const matcherConfig = getMatcherConfig({ names: options.fields });
|
||||
|
||||
if (!matcherConfig) {
|
||||
return noopTransformer.transformer({}, ctx);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { numberOrVariableValidator } from './utils';
|
||||
|
||||
describe('validator', () => {
|
||||
@@ -48,18 +46,10 @@ describe('validator', () => {
|
||||
});
|
||||
|
||||
it('validates a string that has a variable', () => {
|
||||
config.featureToggles.transformationsVariableSupport = true;
|
||||
expect(numberOrVariableValidator('$foo')).toBe(true);
|
||||
config.featureToggles.transformationsVariableSupport = false;
|
||||
});
|
||||
it('fails a string that has a variable if the feature flag is disabled', () => {
|
||||
config.featureToggles.transformationsVariableSupport = false;
|
||||
expect(numberOrVariableValidator('$foo')).toBe(false);
|
||||
config.featureToggles.transformationsVariableSupport = true;
|
||||
});
|
||||
|
||||
it('fails a string that has multiple variables', () => {
|
||||
config.featureToggles.transformationsVariableSupport = true;
|
||||
expect(numberOrVariableValidator('$foo$asd')).toBe(false);
|
||||
config.featureToggles.transformationsVariableSupport = false;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { DataFrame, getFieldDisplayName, TransformerCategory, SelectableValue, getTimeZones } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
import {
|
||||
DataFrame,
|
||||
getFieldDisplayName,
|
||||
TransformerCategory,
|
||||
SelectableValue,
|
||||
getTimeZones,
|
||||
VariableOrigin,
|
||||
VariableSuggestion,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
|
||||
export function useAllFieldNamesFromDataFrames(input: DataFrame[]): string[] {
|
||||
return useMemo(() => {
|
||||
@@ -56,7 +64,7 @@ export const numberOrVariableValidator = (value: string | number) => {
|
||||
if (!Number.isNaN(Number(value))) {
|
||||
return true;
|
||||
}
|
||||
if (/^\$[A-Za-z0-9_]+$/.test(value) && config.featureToggles.transformationsVariableSupport) {
|
||||
if (/^\$[A-Za-z0-9_]+$/.test(value)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -81,3 +89,9 @@ export function getTimezoneOptions(includeInternal: boolean) {
|
||||
|
||||
return timeZoneOptions;
|
||||
}
|
||||
|
||||
export function getVariableSuggestions(): VariableSuggestion[] {
|
||||
return getTemplateSrv()
|
||||
.getVariables()
|
||||
.map((v) => ({ value: v.name, label: v.label || v.name, origin: VariableOrigin.Template }));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user