mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Value mappings: add regex value mapping. (#38931)
This commit is contained in:
parent
15e278e9e1
commit
1aaa00a1e4
@ -57,6 +57,7 @@ You can map values to three different conditions:
|
||||
|
||||
- **Value** maps text values to a color or different display text. For example, if a value is `10`, I want Grafana to display **Perfection!** rather than the number.
|
||||
- **Range** maps numerical ranges to a display text and color. For example, if a value is within a certain range, I want Grafana to display **Low** or **High** rather than the number.
|
||||
- **Regex** maps regular expressions to replacement text and a color. For example, if a value is 'www.example.com', I want Grafana to display just **www**, truncating the domain.
|
||||
- **Special** maps special values like `Null`, `NaN` (not a number), and boolean values like `true` and `false` to a display text and color. For example, if Grafana encounters a `null`, I want Grafana to display **N/A**.
|
||||
|
||||
You can also use the dots on the left as a "handle" to drag and reorder value mappings in the list.
|
||||
@ -113,6 +114,20 @@ Create a mapping for a range of values.
|
||||
1. (Optional) Set the color.
|
||||
1. Click **Update** to save the value mapping.
|
||||
|
||||
## Map a regular expression
|
||||
|
||||
Create a mapping based on a regular expression.
|
||||
|
||||

|
||||
|
||||
1. [Open the panel editor]({{< relref "./panel-editor.md#open-the-panel-editor" >}}).
|
||||
1. In the Value mappings section of the side pane, click **Add value mappings**.
|
||||
1. Click **Add a new mapping** and then select **Regex**.
|
||||
1. Enter the regular expression pattern for Grafana to match.
|
||||
1. (Optional) Enter display text, which can include items such as $1 for replacements.
|
||||
1. (Optional) Set the color.
|
||||
1. Click **Update** to save the value mapping.
|
||||
|
||||
## Map a special value
|
||||
|
||||
Create a mapping for a special value.
|
||||
|
@ -157,6 +157,50 @@ describe('Format value', () => {
|
||||
expect(result.text).toEqual('elva');
|
||||
});
|
||||
|
||||
it('should replace a matching regex', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RegexToText, options: { pattern: '([^.]*).example.com', result: { text: '$1' } } },
|
||||
];
|
||||
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
const result = instance('hostname.example.com');
|
||||
|
||||
expect(result.text).toEqual('hostname');
|
||||
});
|
||||
|
||||
it('should not replace a non-matching regex', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RegexToText, options: { pattern: '([^.]*).example.com', result: { text: '$1' } } },
|
||||
];
|
||||
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
const result = instance('hostname.acme.com');
|
||||
|
||||
expect(result.text).toEqual('hostname.acme.com');
|
||||
});
|
||||
|
||||
it('should empty a matching regex without replacement', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RegexToText, options: { pattern: '([^.]*).example.com', result: { text: '' } } },
|
||||
];
|
||||
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
const result = instance('hostname.example.com');
|
||||
|
||||
expect(result.text).toEqual('');
|
||||
});
|
||||
|
||||
it('should not empty a non-matching regex', () => {
|
||||
const valueMappings: ValueMapping[] = [
|
||||
{ type: MappingType.RegexToText, options: { pattern: '([^.]*).example.com', result: { text: '' } } },
|
||||
];
|
||||
|
||||
const instance = getDisplayProcessorFromConfig({ decimals: 1, mappings: valueMappings });
|
||||
const result = instance('hostname.acme.com');
|
||||
|
||||
expect(result.text).toEqual('hostname.acme.com');
|
||||
});
|
||||
|
||||
it('should return value with color if mapping has color', () => {
|
||||
const valueMappings: ValueMapping[] = [{ type: MappingType.ValueToText, options: { Low: { color: 'red' } } }];
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
export enum MappingType {
|
||||
ValueToText = 'value', // was 1
|
||||
RangeToText = 'range', // was 2
|
||||
RegexToText = 'regex',
|
||||
SpecialValue = 'special',
|
||||
}
|
||||
|
||||
@ -47,6 +48,21 @@ export interface RangeMap extends BaseValueMap<RangeMapOptions> {
|
||||
type: MappingType.RangeToText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface RegexMapOptions {
|
||||
pattern: string;
|
||||
result: ValueMappingResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export interface RegexMap extends BaseValueMap<RegexMapOptions> {
|
||||
type: MappingType.RegexToText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
@ -77,4 +93,4 @@ export interface SpecialValueMap extends BaseValueMap<SpecialValueOptions> {
|
||||
/**
|
||||
* @alpha
|
||||
*/
|
||||
export type ValueMapping = ValueMap | RangeMap | SpecialValueMap;
|
||||
export type ValueMapping = ValueMap | RangeMap | RegexMap | SpecialValueMap;
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { MappingType, SpecialValueMatch, ThresholdsConfig, ValueMap, ValueMapping, ValueMappingResult } from '../types';
|
||||
import { getActiveThreshold } from '../field';
|
||||
import { stringToJsRegex } from '../text/string';
|
||||
|
||||
export function getValueMappingResult(valueMappings: ValueMapping[], value: any): ValueMappingResult | null {
|
||||
for (const vm of valueMappings) {
|
||||
@ -38,6 +39,22 @@ export function getValueMappingResult(valueMappings: ValueMapping[], value: any)
|
||||
|
||||
return vm.options.result;
|
||||
|
||||
case MappingType.RegexToText:
|
||||
if (value == null) {
|
||||
console.log('null value');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
console.log('non-string value', typeof value);
|
||||
continue;
|
||||
}
|
||||
|
||||
const regex = stringToJsRegex(vm.options.pattern);
|
||||
const thisResult = Object.create(vm.options.result);
|
||||
thisResult.text = value.replace(regex, vm.options.result.text || '');
|
||||
return thisResult;
|
||||
|
||||
case MappingType.SpecialValue:
|
||||
switch (vm.options.match) {
|
||||
case SpecialValueMatch.Null: {
|
||||
|
@ -15,6 +15,7 @@ export interface ValueMappingEditRowModel {
|
||||
type: MappingType;
|
||||
from?: number;
|
||||
to?: number;
|
||||
pattern?: string;
|
||||
key?: string;
|
||||
isNew?: boolean;
|
||||
specialMatch?: SpecialValueMatch;
|
||||
@ -93,6 +94,12 @@ export function ValueMappingEditRow({ mapping, index, onChange, onRemove, onDupl
|
||||
});
|
||||
};
|
||||
|
||||
const onChangePattern = (event: React.FormEvent<HTMLInputElement>) => {
|
||||
update((mapping) => {
|
||||
mapping.pattern = event.currentTarget.value;
|
||||
});
|
||||
};
|
||||
|
||||
const onChangeSpecialMatch = (sel: SelectableValue<SpecialValueMatch>) => {
|
||||
update((mapping) => {
|
||||
mapping.specialMatch = sel.value;
|
||||
@ -146,6 +153,14 @@ export function ValueMappingEditRow({ mapping, index, onChange, onRemove, onDupl
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mapping.type === MappingType.RegexToText && (
|
||||
<Input
|
||||
type="text"
|
||||
value={mapping.pattern ?? ''}
|
||||
placeholder="Regular expression"
|
||||
onChange={onChangePattern}
|
||||
/>
|
||||
)}
|
||||
{mapping.type === MappingType.SpecialValue && (
|
||||
<Select
|
||||
menuShouldPortal
|
||||
@ -197,6 +212,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
regexInputWrapper: css({
|
||||
display: 'flex',
|
||||
'> div:first-child': {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
typeColumn: css({
|
||||
textTransform: 'capitalize',
|
||||
textAlign: 'center',
|
||||
|
@ -30,6 +30,17 @@ export function Example() {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: MappingType.RegexToText,
|
||||
options: {
|
||||
pattern: '(.*).example.com',
|
||||
result: {
|
||||
index: 5,
|
||||
text: '$1',
|
||||
color: 'green',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
return <ValueMappingsEditor value={mappings} onChange={setMappings} />;
|
||||
|
@ -44,6 +44,7 @@ export const ValueMappingsEditor = React.memo(({ value, onChange }: Props) => {
|
||||
[{row.from} - {row.to}]
|
||||
</span>
|
||||
)}
|
||||
{row.type === MappingType.RegexToText && row.pattern}
|
||||
{row.type === MappingType.SpecialValue && row.specialMatch}
|
||||
</td>
|
||||
<td>
|
||||
|
@ -147,3 +147,32 @@ describe('When adding and updating range map', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When adding and updating tegex map', () => {
|
||||
it('should add new regex map', async () => {
|
||||
const onChangeSpy = jest.fn();
|
||||
setup(onChangeSpy, { value: [] });
|
||||
|
||||
fireEvent.click(screen.getByLabelText(selectors.components.ValuePicker.button('Add a new mapping')));
|
||||
const selectComponent = await screen.findByLabelText(selectors.components.ValuePicker.select('Add a new mapping'));
|
||||
await selectOptionInTest(selectComponent, 'Regex');
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Regular expression'), { target: { value: '(.*).example.com' } });
|
||||
fireEvent.change(screen.getByPlaceholderText('Optional display text'), { target: { value: '$1' } });
|
||||
|
||||
fireEvent.click(screen.getByText('Update'));
|
||||
|
||||
expect(onChangeSpy).toBeCalledWith([
|
||||
{
|
||||
type: MappingType.RegexToText,
|
||||
options: {
|
||||
pattern: '(.*).example.com',
|
||||
result: {
|
||||
text: '$1',
|
||||
index: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
@ -49,6 +49,7 @@ export function ValueMappingsEditorModal({ value, onChange, onClose }: Props) {
|
||||
const mappingTypes: Array<SelectableValue<MappingType>> = [
|
||||
{ label: 'Value', value: MappingType.ValueToText, description: 'Match a specific text value' },
|
||||
{ label: 'Range', value: MappingType.RangeToText, description: 'Match a numerical range of values' },
|
||||
{ label: 'Regex', value: MappingType.RegexToText, description: 'Match a regular expression with replacement' },
|
||||
{ label: 'Special', value: MappingType.SpecialValue, description: 'Match on null, NaN, boolean and empty values' },
|
||||
];
|
||||
|
||||
@ -191,6 +192,17 @@ export function editModelToSaveModel(rows: ValueMappingEditRowModel[]) {
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MappingType.RegexToText:
|
||||
if (item.pattern != null) {
|
||||
mappings.push({
|
||||
type: item.type,
|
||||
options: {
|
||||
pattern: item.pattern,
|
||||
result,
|
||||
},
|
||||
});
|
||||
}
|
||||
break;
|
||||
case MappingType.SpecialValue:
|
||||
mappings.push({
|
||||
type: item.type,
|
||||
@ -230,6 +242,13 @@ export function buildEditRowModels(value: ValueMapping[]) {
|
||||
to: mapping.options.to ?? 0,
|
||||
});
|
||||
break;
|
||||
case MappingType.RegexToText:
|
||||
editRows.push({
|
||||
type: mapping.type,
|
||||
result: mapping.options.result,
|
||||
pattern: mapping.options.pattern,
|
||||
});
|
||||
break;
|
||||
case MappingType.SpecialValue:
|
||||
editRows.push({
|
||||
type: mapping.type,
|
||||
|
Loading…
Reference in New Issue
Block a user