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.
|
- **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.
|
- **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**.
|
- **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.
|
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. (Optional) Set the color.
|
||||||
1. Click **Update** to save the value mapping.
|
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
|
## Map a special value
|
||||||
|
|
||||||
Create a mapping for a special value.
|
Create a mapping for a special value.
|
||||||
|
@ -157,6 +157,50 @@ describe('Format value', () => {
|
|||||||
expect(result.text).toEqual('elva');
|
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', () => {
|
it('should return value with color if mapping has color', () => {
|
||||||
const valueMappings: ValueMapping[] = [{ type: MappingType.ValueToText, options: { Low: { color: 'red' } } }];
|
const valueMappings: ValueMapping[] = [{ type: MappingType.ValueToText, options: { Low: { color: 'red' } } }];
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
export enum MappingType {
|
export enum MappingType {
|
||||||
ValueToText = 'value', // was 1
|
ValueToText = 'value', // was 1
|
||||||
RangeToText = 'range', // was 2
|
RangeToText = 'range', // was 2
|
||||||
|
RegexToText = 'regex',
|
||||||
SpecialValue = 'special',
|
SpecialValue = 'special',
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,6 +48,21 @@ export interface RangeMap extends BaseValueMap<RangeMapOptions> {
|
|||||||
type: MappingType.RangeToText;
|
type: MappingType.RangeToText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export interface RegexMapOptions {
|
||||||
|
pattern: string;
|
||||||
|
result: ValueMappingResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export interface RegexMap extends BaseValueMap<RegexMapOptions> {
|
||||||
|
type: MappingType.RegexToText;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @alpha
|
* @alpha
|
||||||
*/
|
*/
|
||||||
@ -77,4 +93,4 @@ export interface SpecialValueMap extends BaseValueMap<SpecialValueOptions> {
|
|||||||
/**
|
/**
|
||||||
* @alpha
|
* @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 { MappingType, SpecialValueMatch, ThresholdsConfig, ValueMap, ValueMapping, ValueMappingResult } from '../types';
|
||||||
import { getActiveThreshold } from '../field';
|
import { getActiveThreshold } from '../field';
|
||||||
|
import { stringToJsRegex } from '../text/string';
|
||||||
|
|
||||||
export function getValueMappingResult(valueMappings: ValueMapping[], value: any): ValueMappingResult | null {
|
export function getValueMappingResult(valueMappings: ValueMapping[], value: any): ValueMappingResult | null {
|
||||||
for (const vm of valueMappings) {
|
for (const vm of valueMappings) {
|
||||||
@ -38,6 +39,22 @@ export function getValueMappingResult(valueMappings: ValueMapping[], value: any)
|
|||||||
|
|
||||||
return vm.options.result;
|
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:
|
case MappingType.SpecialValue:
|
||||||
switch (vm.options.match) {
|
switch (vm.options.match) {
|
||||||
case SpecialValueMatch.Null: {
|
case SpecialValueMatch.Null: {
|
||||||
|
@ -15,6 +15,7 @@ export interface ValueMappingEditRowModel {
|
|||||||
type: MappingType;
|
type: MappingType;
|
||||||
from?: number;
|
from?: number;
|
||||||
to?: number;
|
to?: number;
|
||||||
|
pattern?: string;
|
||||||
key?: string;
|
key?: string;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
specialMatch?: SpecialValueMatch;
|
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>) => {
|
const onChangeSpecialMatch = (sel: SelectableValue<SpecialValueMatch>) => {
|
||||||
update((mapping) => {
|
update((mapping) => {
|
||||||
mapping.specialMatch = sel.value;
|
mapping.specialMatch = sel.value;
|
||||||
@ -146,6 +153,14 @@ export function ValueMappingEditRow({ mapping, index, onChange, onRemove, onDupl
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{mapping.type === MappingType.RegexToText && (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={mapping.pattern ?? ''}
|
||||||
|
placeholder="Regular expression"
|
||||||
|
onChange={onChangePattern}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{mapping.type === MappingType.SpecialValue && (
|
{mapping.type === MappingType.SpecialValue && (
|
||||||
<Select
|
<Select
|
||||||
menuShouldPortal
|
menuShouldPortal
|
||||||
@ -197,6 +212,12 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
regexInputWrapper: css({
|
||||||
|
display: 'flex',
|
||||||
|
'> div:first-child': {
|
||||||
|
marginRight: theme.spacing(2),
|
||||||
|
},
|
||||||
|
}),
|
||||||
typeColumn: css({
|
typeColumn: css({
|
||||||
textTransform: 'capitalize',
|
textTransform: 'capitalize',
|
||||||
textAlign: 'center',
|
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} />;
|
return <ValueMappingsEditor value={mappings} onChange={setMappings} />;
|
||||||
|
@ -44,6 +44,7 @@ export const ValueMappingsEditor = React.memo(({ value, onChange }: Props) => {
|
|||||||
[{row.from} - {row.to}]
|
[{row.from} - {row.to}]
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{row.type === MappingType.RegexToText && row.pattern}
|
||||||
{row.type === MappingType.SpecialValue && row.specialMatch}
|
{row.type === MappingType.SpecialValue && row.specialMatch}
|
||||||
</td>
|
</td>
|
||||||
<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>> = [
|
const mappingTypes: Array<SelectableValue<MappingType>> = [
|
||||||
{ label: 'Value', value: MappingType.ValueToText, description: 'Match a specific text value' },
|
{ label: 'Value', value: MappingType.ValueToText, description: 'Match a specific text value' },
|
||||||
{ label: 'Range', value: MappingType.RangeToText, description: 'Match a numerical range of values' },
|
{ 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' },
|
{ label: 'Special', value: MappingType.SpecialValue, description: 'Match on null, NaN, boolean and empty values' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -191,6 +192,17 @@ export function editModelToSaveModel(rows: ValueMappingEditRowModel[]) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case MappingType.RegexToText:
|
||||||
|
if (item.pattern != null) {
|
||||||
|
mappings.push({
|
||||||
|
type: item.type,
|
||||||
|
options: {
|
||||||
|
pattern: item.pattern,
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
case MappingType.SpecialValue:
|
case MappingType.SpecialValue:
|
||||||
mappings.push({
|
mappings.push({
|
||||||
type: item.type,
|
type: item.type,
|
||||||
@ -230,6 +242,13 @@ export function buildEditRowModels(value: ValueMapping[]) {
|
|||||||
to: mapping.options.to ?? 0,
|
to: mapping.options.to ?? 0,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case MappingType.RegexToText:
|
||||||
|
editRows.push({
|
||||||
|
type: mapping.type,
|
||||||
|
result: mapping.options.result,
|
||||||
|
pattern: mapping.options.pattern,
|
||||||
|
});
|
||||||
|
break;
|
||||||
case MappingType.SpecialValue:
|
case MappingType.SpecialValue:
|
||||||
editRows.push({
|
editRows.push({
|
||||||
type: mapping.type,
|
type: mapping.type,
|
||||||
|
Loading…
Reference in New Issue
Block a user