diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.test.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.test.tsx
new file mode 100644
index 00000000000..fdbfd450a6f
--- /dev/null
+++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/BasicMatcherEditor.test.tsx
@@ -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();
+
+ // 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();
+ });
+ });
+ });
+});
diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/NoopMatcherEditor.test.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/NoopMatcherEditor.test.tsx
new file mode 100644
index 00000000000..753e77855e1
--- /dev/null
+++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/NoopMatcherEditor.test.tsx
@@ -0,0 +1,10 @@
+import { render } from '@testing-library/react';
+
+import { NoopMatcherEditor } from './NoopMatcherEditor';
+
+describe('NoopMatcherEditor', () => {
+ it('renders nothing', () => {
+ const { container } = render();
+ expect(container).toBeEmptyDOMElement();
+ });
+});
diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.test.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.test.tsx
new file mode 100644
index 00000000000..eb9bf4c8f8e
--- /dev/null
+++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RangeMatcherEditor.test.tsx
@@ -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();
+
+ // 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();
+ });
+ });
+ });
+});
diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.test.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.test.tsx
new file mode 100644
index 00000000000..af9765cecd6
--- /dev/null
+++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.test.tsx
@@ -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();
+
+ // 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();
+ });
+ });
+ });
+});
diff --git a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx
index 09fc5936cda..e3e5a559770 100644
--- a/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx
+++ b/public/app/features/transformers/FilterByValueTransformer/ValueMatchers/RegexMatcherEditor.tsx
@@ -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((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}
/>
);
};
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 1e96a13e794..19f5eb75a63 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -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",