Value mappings: add regex value mapping. (#38931)

This commit is contained in:
Jim McDonald 2021-09-15 01:15:14 +01:00 committed by GitHub
parent 15e278e9e1
commit 1aaa00a1e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 174 additions and 1 deletions

View File

@ -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.
![Map a regular expression](/static/img/docs/value-mappings/map-regex-8-0.png)
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.

View File

@ -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' } } }];

View File

@ -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;

View File

@ -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: {

View File

@ -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',

View File

@ -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} />;

View File

@ -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>

View File

@ -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,
},
},
},
]);
});
});

View File

@ -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,