FilterByValueTransformer: Suggest variable regex formatter (#105272)

---------

Co-authored-by: Sam Jewell <sam.jewell@grafana.com>
This commit is contained in:
Ivan Ortega Alba
2025-05-14 11:44:43 +02:00
committed by GitHub
co-authored by Sam Jewell
parent a30b1c4bee
commit aa72d800a1
6 changed files with 228 additions and 3 deletions
@@ -0,0 +1,58 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { FieldType, TypedVariableModel } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { basicMatcherEditor } from './BasicMatcherEditor';
jest.mock('@grafana/runtime', () => ({
getTemplateSrv: jest.fn(),
}));
describe('BasicMatcherEditor', () => {
it('shows variable suggestions', async () => {
// Mock template service variables
const mockVariables: TypedVariableModel[] = [
{ name: 'var1', label: 'Variable 1', type: 'custom' } as TypedVariableModel,
{ name: 'var2', label: 'Variable 2', type: 'custom' } as TypedVariableModel,
];
const mockTemplateSrv = {
getVariables: () => mockVariables,
replace: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
jest.mocked(getTemplateSrv).mockReturnValue(mockTemplateSrv);
const onChangeMock = jest.fn();
const options = { value: '' };
const field = {
name: 'test',
type: FieldType.string,
config: {},
values: [],
};
const Editor = basicMatcherEditor({ validator: () => true });
render(<Editor options={options} onChange={onChangeMock} field={field} />);
// Focus the input and press $ to trigger suggestions
const input = screen.getByPlaceholderText('Value or variable');
fireEvent.focus(input);
fireEvent.keyDown(input, { key: '$' });
// Wait for suggestions to appear and verify
await waitFor(() => {
const suggestions = screen.getAllByRole('menuitem');
// Verify exact number of suggestions
expect(suggestions).toHaveLength(mockVariables.length);
mockVariables.forEach((variable) => {
const suggestion = suggestions.find((s) => s.textContent?.includes(variable.label as string));
expect(suggestion).toBeInTheDocument();
});
});
});
});
@@ -0,0 +1,10 @@
import { render } from '@testing-library/react';
import { NoopMatcherEditor } from './NoopMatcherEditor';
describe('NoopMatcherEditor', () => {
it('renders nothing', () => {
const { container } = render(<NoopMatcherEditor />);
expect(container).toBeEmptyDOMElement();
});
});
@@ -0,0 +1,82 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { FieldType, TypedVariableModel } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { rangeMatcherEditor } from './RangeMatcherEditor';
jest.mock('@grafana/runtime', () => ({
getTemplateSrv: jest.fn(),
}));
describe('RangeMatcherEditor', () => {
it('shows variable suggestions for both from and to inputs', async () => {
// Mock template service variables
const mockVariables: TypedVariableModel[] = [
{ name: 'var1', label: 'Variable 1', type: 'custom' } as TypedVariableModel,
{ name: 'var2', label: 'Variable 2', type: 'custom' } as TypedVariableModel,
];
const mockTemplateSrv = {
getVariables: () => mockVariables,
replace: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
jest.mocked(getTemplateSrv).mockReturnValue(mockTemplateSrv);
const onChangeMock = jest.fn();
const options = { from: '', to: '' };
const field = {
name: 'test',
type: FieldType.string,
config: {},
values: [],
};
const Editor = rangeMatcherEditor({ validator: () => true });
render(<Editor options={options} onChange={onChangeMock} field={field} />);
// Test "from" input
const fromInput = screen.getByPlaceholderText('From');
fireEvent.focus(fromInput);
fireEvent.keyDown(fromInput, { key: '$' });
// Wait for suggestions to appear and verify for "from" input
await waitFor(() => {
const menus = screen.getAllByRole('menu');
const fromMenu = menus[0]; // First menu is for the "from" input
const fromSuggestions = fromMenu.querySelectorAll('[role="menuitem"]');
// Verify exact number of suggestions for "from" input
expect(fromSuggestions).toHaveLength(mockVariables.length);
mockVariables.forEach((variable) => {
const suggestion = Array.from(fromSuggestions).find((s) => s.textContent?.includes(variable.label as string));
expect(suggestion).toBeInTheDocument();
});
});
// Clear suggestions
fireEvent.blur(fromInput);
// Test "to" input
const toInput = screen.getByPlaceholderText('To');
fireEvent.focus(toInput);
fireEvent.keyDown(toInput, { key: '$' });
// Wait for suggestions to appear and verify for "to" input
await waitFor(() => {
const menus = screen.getAllByRole('menu');
const toMenu = menus[1];
const toSuggestions = toMenu.querySelectorAll('[role="menuitem"]');
// Verify exact number of suggestions for "to" input
expect(toSuggestions).toHaveLength(mockVariables.length);
mockVariables.forEach((variable) => {
const suggestion = Array.from(toSuggestions).find((s) => s.textContent?.includes(variable.label as string));
expect(suggestion).toBeInTheDocument();
});
});
});
});
@@ -0,0 +1,61 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { FieldType, TypedVariableModel } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { regexMatcherEditor } from './RegexMatcherEditor';
jest.mock('@grafana/runtime', () => ({
getTemplateSrv: jest.fn(),
}));
describe('RegexMatcherEditor', () => {
it('adds :regex suffix to variable suggestions', async () => {
// Mock template service variables
const mockVariables: TypedVariableModel[] = [
{ name: 'var1', label: 'Variable 1', type: 'custom' } as TypedVariableModel,
{ name: 'var2', label: 'Variable 2', type: 'custom' } as TypedVariableModel,
];
const mockTemplateSrv = {
getVariables: () => mockVariables,
replace: jest.fn(),
containsTemplate: jest.fn(),
updateTimeRange: jest.fn(),
};
jest.mocked(getTemplateSrv).mockReturnValue(mockTemplateSrv);
const onChangeMock = jest.fn();
const options = { value: '' };
const field = {
name: 'test',
type: FieldType.string,
config: {},
values: [],
};
const Editor = regexMatcherEditor({ validator: () => true });
render(<Editor options={options} onChange={onChangeMock} field={field} />);
// Focus the input and press $ to trigger suggestions
const input = screen.getByPlaceholderText('Value or variable');
fireEvent.focus(input);
fireEvent.keyDown(input, { key: '$' });
// Wait for suggestions to appear and verify
await waitFor(() => {
const suggestions = screen.getAllByRole('menuitem');
// Verify exact number of suggestions (each variable has both regular and regex versions)
expect(suggestions).toHaveLength(mockVariables.length * 2);
mockVariables.forEach((variable) => {
const regularSuggestion = suggestions.find((s) => s.textContent?.includes(variable.label as string));
const regexSuggestion = suggestions.find((s) => s.textContent?.includes(`${variable.label as string}:regex`));
expect(regularSuggestion).toBeInTheDocument();
expect(regexSuggestion).toBeInTheDocument();
});
});
});
});
@@ -1,7 +1,7 @@
import { useCallback, useState } from 'react';
import * as React from 'react';
import { ValueMatcherID, BasicValueMatcherOptions } from '@grafana/data';
import { ValueMatcherID, BasicValueMatcherOptions, VariableSuggestion } from '@grafana/data';
import { t } from 'app/core/internationalization';
import { SuggestionsInput } from '../../suggestionsInput/SuggestionsInput';
@@ -16,6 +16,19 @@ export function regexMatcherEditor(
const { validator } = config;
const { value } = options;
const [isInvalid, setInvalid] = useState(!validator(value));
const variableSuggestions = getVariableSuggestions().reduce<VariableSuggestion[]>((acc, v) => {
acc.push(v);
acc.push({
...v,
documentation: t(
'transformers.regex-matcher-editor.variable-regex-documentation',
'Formats multi-value variable into a regex string'
),
label: v.label.concat(':regex'),
value: v.value.concat(':regex'),
});
return acc;
}, []);
const onChangeVariableValue = useCallback(
(value: string) => {
@@ -34,7 +47,7 @@ export function regexMatcherEditor(
value={value}
onChange={onChangeVariableValue}
placeholder={t('transformers.regex-matcher-editor.placeholder-value-or-variable', 'Value or variable')}
suggestions={getVariableSuggestions()}
suggestions={variableSuggestions}
/>
);
};
+2 -1
View File
@@ -9019,7 +9019,8 @@
"placeholder-choose-stat": "Choose stat"
},
"regex-matcher-editor": {
"placeholder-value-or-variable": "Value or variable"
"placeholder-value-or-variable": "Value or variable",
"variable-regex-documentation": "Formats multi-value variable into a regex string"
},
"regression-transformer-editor": {
"label-degree": "Degree",