From 1aaa00a1e4a4f7eb3daea0d60e58600d1dcaca22 Mon Sep 17 00:00:00 2001 From: Jim McDonald Date: Wed, 15 Sep 2021 01:15:14 +0100 Subject: [PATCH] Value mappings: add regex value mapping. (#38931) --- docs/sources/panels/value-mappings.md | 15 +++++++ .../src/field/displayProcessor.test.ts | 44 +++++++++++++++++++ .../grafana-data/src/types/valueMapping.ts | 18 +++++++- .../grafana-data/src/utils/valueMappings.ts | 17 +++++++ .../ValueMappingEditRow.tsx | 21 +++++++++ .../ValueMappingsEditor.story.tsx | 11 +++++ .../ValueMappingsEditor.tsx | 1 + .../ValueMappingsEditorModal.test.tsx | 29 ++++++++++++ .../ValueMappingsEditorModal.tsx | 19 ++++++++ 9 files changed, 174 insertions(+), 1 deletion(-) diff --git a/docs/sources/panels/value-mappings.md b/docs/sources/panels/value-mappings.md index 45a8ef6facc..2aa3a5b3a1c 100644 --- a/docs/sources/panels/value-mappings.md +++ b/docs/sources/panels/value-mappings.md @@ -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. diff --git a/packages/grafana-data/src/field/displayProcessor.test.ts b/packages/grafana-data/src/field/displayProcessor.test.ts index f527ae04ca4..4e3f6a3ddc9 100644 --- a/packages/grafana-data/src/field/displayProcessor.test.ts +++ b/packages/grafana-data/src/field/displayProcessor.test.ts @@ -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' } } }]; diff --git a/packages/grafana-data/src/types/valueMapping.ts b/packages/grafana-data/src/types/valueMapping.ts index 9a6eda042d5..6a4b373d305 100644 --- a/packages/grafana-data/src/types/valueMapping.ts +++ b/packages/grafana-data/src/types/valueMapping.ts @@ -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 { type: MappingType.RangeToText; } +/** + * @alpha + */ +export interface RegexMapOptions { + pattern: string; + result: ValueMappingResult; +} + +/** + * @alpha + */ +export interface RegexMap extends BaseValueMap { + type: MappingType.RegexToText; +} + /** * @alpha */ @@ -77,4 +93,4 @@ export interface SpecialValueMap extends BaseValueMap { /** * @alpha */ -export type ValueMapping = ValueMap | RangeMap | SpecialValueMap; +export type ValueMapping = ValueMap | RangeMap | RegexMap | SpecialValueMap; diff --git a/packages/grafana-data/src/utils/valueMappings.ts b/packages/grafana-data/src/utils/valueMappings.ts index f8095d49a01..56812a75e8c 100644 --- a/packages/grafana-data/src/utils/valueMappings.ts +++ b/packages/grafana-data/src/utils/valueMappings.ts @@ -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: { diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingEditRow.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingEditRow.tsx index 90d89a6ccfb..836df23696a 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingEditRow.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingEditRow.tsx @@ -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) => { + update((mapping) => { + mapping.pattern = event.currentTarget.value; + }); + }; + const onChangeSpecialMatch = (sel: SelectableValue) => { update((mapping) => { mapping.specialMatch = sel.value; @@ -146,6 +153,14 @@ export function ValueMappingEditRow({ mapping, index, onChange, onRemove, onDupl /> )} + {mapping.type === MappingType.RegexToText && ( + + )} {mapping.type === MappingType.SpecialValue && (