From 82b21fe35e9bbab99bf3c185e17ab6834ef70776 Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Wed, 9 Dec 2020 03:31:13 -0700 Subject: [PATCH] Transformer: Rename metrics based on regex (#29281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Grafana: Rename By Regex Transformer * Removing unused deps * Add scrollIntoView() to TranformTab.content() * Exporting RenameByRegexTransformerOptions * Add doc block to renameByRegex transformer * Adding doc block for RenameByRegexTransformerOptions * removing scrollIntoView() for transform tab * Adding back in scrollIntoView() for transform panel * Tests: fixes e2e tests * Apply to displayName instead of just the name of the frame * Rewrite docblock to match new functionality * Adding documentation * Changing TLD to domain name * Fixing typo * Update docs/sources/panels/transformations/types-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/panels/transformations/types-options.md Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> * Update docs/sources/panels/transformations/types-options.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Hugo Häggmark Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> --- .../panels/transformations/types-options.md | 15 ++ .../grafana-data/src/transformations/index.ts | 1 + .../src/transformations/transformers.ts | 2 + .../src/transformations/transformers/ids.ts | 1 + .../transformers/renameByRegex.test.ts | 180 ++++++++++++++++++ .../transformers/renameByRegex.ts | 62 ++++++ .../RenameByRegexTransformer.tsx | 131 +++++++++++++ public/app/core/utils/standardTransformers.ts | 2 + 8 files changed, 394 insertions(+) create mode 100644 packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts create mode 100644 packages/grafana-data/src/transformations/transformers/renameByRegex.ts create mode 100644 public/app/core/components/TransformersUI/RenameByRegexTransformer.tsx diff --git a/docs/sources/panels/transformations/types-options.md b/docs/sources/panels/transformations/types-options.md index 9f287e1f50a..081b169c846 100644 --- a/docs/sources/panels/transformations/types-options.md +++ b/docs/sources/panels/transformations/types-options.md @@ -19,6 +19,7 @@ Grafana comes with the following transformations: - [Concatenate fields](#concatenate-fields) - [Group by](#group-by) - [Merge](#merge) +- [Rename by regex](#rename-by-regex) Keep reading for detailed descriptions of each type of transformation and the options available for each, as well as suggestions on how to use them. @@ -398,3 +399,17 @@ When you have more than one condition, you can choose if you want the action (in In the example above we chose **Match all** because we wanted to include the rows that have a temperature lower than 30 _AND_ an altitude higher than 100. If we wanted to include the rows that have a temperature lower than 30 _OR_ an altitude higher than 100 instead, then we would select **Match any**. This would include the first row in the original data, which has a temperature of 32°C (does not match the first condition) but an altitude of 101 (which matches the second condition), so it is included. Conditions that are invalid or incompletely configured are ignored. + +## Rename by regex + +Use this transformation to rename parts of the query results using a regular expression and replacement pattern. + +You can specify a regular expression, which is only applied to matches, along with a replacement pattern that support back references. For example, let's imagine you're visualizing CPU usage per host and you want to remove the domain name. You could set the regex to `([^\.]+)\..+` and the replacement pattern to `$1`, `web-01.example.com` would become `web-01`. + +In the following example, we are stripping the prefix from event types. In the before image, you can see everything is prefixed with `system.` + +{{< docs-imagebox img="/img/docs/transformations/rename-by-regex-before-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}} + +With the transformation applied, you can see we are left with just the remainder of the string. + +{{< docs-imagebox img="/img/docs/transformations/rename-by-regex-after-7-3.png" class="docs-image--no-shadow" max-width= "1100px" >}} diff --git a/packages/grafana-data/src/transformations/index.ts b/packages/grafana-data/src/transformations/index.ts index 14f959a3195..c8b275ddcb1 100644 --- a/packages/grafana-data/src/transformations/index.ts +++ b/packages/grafana-data/src/transformations/index.ts @@ -10,4 +10,5 @@ export { standardTransformersRegistry, } from './standardTransformersRegistry'; export { RegexpOrNamesMatcherOptions } from './matchers/nameMatcher'; +export { RenameByRegexTransformerOptions } from './transformers/renameByRegex'; export { outerJoinDataFrames } from './transformers/seriesToColumns'; diff --git a/packages/grafana-data/src/transformations/transformers.ts b/packages/grafana-data/src/transformations/transformers.ts index d8f0b6edd16..a14560b4ac3 100644 --- a/packages/grafana-data/src/transformations/transformers.ts +++ b/packages/grafana-data/src/transformations/transformers.ts @@ -14,6 +14,7 @@ import { labelsToFieldsTransformer } from './transformers/labelsToFields'; import { ensureColumnsTransformer } from './transformers/ensureColumns'; import { groupByTransformer } from './transformers/groupBy'; import { mergeTransformer } from './transformers/merge'; +import { renameByRegexTransformer } from './transformers/renameByRegex'; import { filterByValueTransformer } from './transformers/filterByValue'; export const standardTransformers = { @@ -35,4 +36,5 @@ export const standardTransformers = { ensureColumnsTransformer, groupByTransformer, mergeTransformer, + renameByRegexTransformer, }; diff --git a/packages/grafana-data/src/transformations/transformers/ids.ts b/packages/grafana-data/src/transformations/transformers/ids.ts index ec1a30e064a..cc0b1535122 100644 --- a/packages/grafana-data/src/transformations/transformers/ids.ts +++ b/packages/grafana-data/src/transformations/transformers/ids.ts @@ -16,6 +16,7 @@ export enum DataTransformerID { filterFieldsByName = 'filterFieldsByName', filterFrames = 'filterFrames', filterByRefId = 'filterByRefId', + renameByRegex = 'renameByRegex', filterByValue = 'filterByValue', noop = 'noop', ensureColumns = 'ensureColumns', diff --git a/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts b/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts new file mode 100644 index 00000000000..217f581ad80 --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/renameByRegex.test.ts @@ -0,0 +1,180 @@ +import { DataTransformerConfig, DataTransformerID, FieldType, toDataFrame, transformDataFrame } from '@grafana/data'; +import { renameByRegexTransformer, RenameByRegexTransformerOptions } from './renameByRegex'; +import { mockTransformationsRegistry } from '../../utils/tests/mockTransformationsRegistry'; + +describe('Rename By Regex Transformer', () => { + beforeAll(() => { + mockTransformationsRegistry([renameByRegexTransformer]); + }); + + describe('when regex and replacement pattern', () => { + const data = toDataFrame({ + name: 'web-01.example.com', + fields: [ + { + name: 'Time', + type: FieldType.time, + config: { name: 'Time' }, + values: [3000, 4000, 5000, 6000], + }, + { + name: 'Value', + type: FieldType.number, + config: { displayName: 'web-01.example.com' }, + values: [10000.3, 10000.4, 10000.5, 10000.6], + }, + ], + }); + + it('should rename matches using references', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.renameByRegex, + options: { + regex: '([^.]+).example.com', + renamePattern: '$1', + }, + }; + await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => { + const data = received[0]; + const frame = data[0]; + expect(frame.fields).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "name": "Time", + }, + "name": "Time", + "state": Object { + "displayName": "Time", + }, + "type": "time", + "values": Array [ + 3000, + 4000, + 5000, + 6000, + ], + }, + Object { + "config": Object { + "displayName": "web-01", + }, + "name": "Value", + "state": Object { + "displayName": "web-01", + }, + "type": "number", + "values": Array [ + 10000.3, + 10000.4, + 10000.5, + 10000.6, + ], + }, + ] + `); + }); + }); + + it('should not rename misses', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.renameByRegex, + options: { + regex: '([^.]+).bad-domain.com', + renamePattern: '$1', + }, + }; + await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => { + const data = received[0]; + const frame = data[0]; + expect(frame.fields).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "name": "Time", + }, + "name": "Time", + "state": Object { + "displayName": "Time", + }, + "type": "time", + "values": Array [ + 3000, + 4000, + 5000, + 6000, + ], + }, + Object { + "config": Object { + "displayName": "web-01.example.com", + }, + "name": "Value", + "state": Object { + "displayName": "web-01.example.com", + }, + "type": "number", + "values": Array [ + 10000.3, + 10000.4, + 10000.5, + 10000.6, + ], + }, + ] + `); + }); + }); + + it('should not rename with empty regex and repacement pattern', async () => { + const cfg: DataTransformerConfig = { + id: DataTransformerID.renameByRegex, + options: { + regex: '', + renamePattern: '', + }, + }; + await expect(transformDataFrame([cfg], [data])).toEmitValuesWith(received => { + const data = received[0]; + const frame = data[0]; + expect(frame.fields).toMatchInlineSnapshot(` + Array [ + Object { + "config": Object { + "displayName": "Time", + "name": "Time", + }, + "name": "Time", + "state": Object { + "displayName": "Time", + }, + "type": "time", + "values": Array [ + 3000, + 4000, + 5000, + 6000, + ], + }, + Object { + "config": Object { + "displayName": "web-01.example.com", + }, + "name": "Value", + "state": Object { + "displayName": "web-01.example.com", + }, + "type": "number", + "values": Array [ + 10000.3, + 10000.4, + 10000.5, + 10000.6, + ], + }, + ] + `); + }); + }); + }); +}); diff --git a/packages/grafana-data/src/transformations/transformers/renameByRegex.ts b/packages/grafana-data/src/transformations/transformers/renameByRegex.ts new file mode 100644 index 00000000000..6e75385c86e --- /dev/null +++ b/packages/grafana-data/src/transformations/transformers/renameByRegex.ts @@ -0,0 +1,62 @@ +import { DataTransformerID } from './ids'; +import { DataTransformerInfo } from '../../types/transformations'; +import { map } from 'rxjs/operators'; +import { DataFrame } from '../../types/dataFrame'; +import { getFieldDisplayName } from '../../field/fieldState'; + +/** + * Options for renameByRegexTransformer + * + * @public + */ +export interface RenameByRegexTransformerOptions { + regex: string; + renamePattern: string; +} + +/** + * Replaces the displayName of a field by applying a regular expression + * to match the name and a pattern for the replacement. + * + * @public + */ +export const renameByRegexTransformer: DataTransformerInfo = { + id: DataTransformerID.renameByRegex, + name: 'Rename fields by regex', + description: 'Rename fields based on regular expression by users.', + defaultOptions: { + regex: '(.*)', + renamePattern: '$1', + }, + + /** + * Return a modified copy of the series. If the transform is not or should not + * be applied, just return the input series + */ + operator: options => source => + source.pipe( + map(data => { + if (!Array.isArray(data) || data.length === 0) { + return data; + } + return data.map(renameFieldsByRegex(options)); + }) + ), +}; + +const renameFieldsByRegex = (options: RenameByRegexTransformerOptions) => (frame: DataFrame) => { + const regex = new RegExp(options.regex); + const fields = frame.fields.map(field => { + const displayName = getFieldDisplayName(field, frame); + if (!regex.test(displayName)) { + return field; + } + const newDisplayName = displayName.replace(regex, options.renamePattern); + return { + ...field, + config: { ...field.config, displayName: newDisplayName }, + state: { ...field.state, displayName: newDisplayName }, + }; + }); + return { ...frame, fields }; +}; diff --git a/public/app/core/components/TransformersUI/RenameByRegexTransformer.tsx b/public/app/core/components/TransformersUI/RenameByRegexTransformer.tsx new file mode 100644 index 00000000000..c93da91e1b4 --- /dev/null +++ b/public/app/core/components/TransformersUI/RenameByRegexTransformer.tsx @@ -0,0 +1,131 @@ +import React from 'react'; +import { + DataTransformerID, + standardTransformers, + TransformerRegistyItem, + TransformerUIProps, + stringToJsRegex, +} from '@grafana/data'; +import { Field, Input } from '@grafana/ui'; +import { css } from 'emotion'; +import { RenameByRegexTransformerOptions } from '@grafana/data/src/transformations/transformers/renameByRegex'; + +interface RenameByRegexTransformerEditorProps extends TransformerUIProps {} + +interface RenameByRegexTransformerEditorState { + regex?: string; + renamePattern?: string; + isRegexValid?: boolean; +} + +export class RenameByRegexTransformerEditor extends React.PureComponent< + RenameByRegexTransformerEditorProps, + RenameByRegexTransformerEditorState +> { + constructor(props: RenameByRegexTransformerEditorProps) { + super(props); + this.state = { + regex: props.options.regex, + renamePattern: props.options.renamePattern, + isRegexValid: true, + }; + } + + handleRegexChange = (e: React.FormEvent) => { + const regex = e.currentTarget.value; + let isRegexValid = true; + if (regex) { + try { + if (regex) { + stringToJsRegex(regex); + } + } catch (e) { + isRegexValid = false; + } + } + this.setState(previous => ({ ...previous, regex, isRegexValid })); + }; + + handleRenameChange = (e: React.FormEvent) => { + const renamePattern = e.currentTarget.value; + this.setState(previous => ({ ...previous, renamePattern })); + }; + + handleRegexBlur = (e: React.FocusEvent) => { + const regex = e.currentTarget.value; + let isRegexValid = true; + + try { + if (regex) { + stringToJsRegex(regex); + } + } catch (e) { + isRegexValid = false; + } + + this.setState({ isRegexValid }, () => { + if (isRegexValid) { + this.props.onChange({ ...this.props.options, regex }); + } + }); + }; + + handleRenameBlur = (e: React.FocusEvent) => { + const renamePattern = e.currentTarget.value; + this.setState({ renamePattern }, () => this.props.onChange({ ...this.props.options, renamePattern })); + }; + + render() { + const { regex, renamePattern, isRegexValid } = this.state; + return ( + <> +
+
+
Match
+ + + +
+
+
+
+
Replace
+ + + +
+
+ + ); + } +} + +export const renameByRegexTransformRegistryItem: TransformerRegistyItem = { + id: DataTransformerID.renameByRegex, + editor: RenameByRegexTransformerEditor, + transformation: standardTransformers.renameByRegexTransformer, + name: 'Rename by regex', + description: 'Renames part of the query result by using regular expression with placeholders.', +}; diff --git a/public/app/core/utils/standardTransformers.ts b/public/app/core/utils/standardTransformers.ts index 25d81984164..fd1901ce884 100644 --- a/public/app/core/utils/standardTransformers.ts +++ b/public/app/core/utils/standardTransformers.ts @@ -11,11 +11,13 @@ import { groupByTransformRegistryItem } from '../components/TransformersUI/Group import { mergeTransformerRegistryItem } from '../components/TransformersUI/MergeTransformerEditor'; import { seriesToRowsTransformerRegistryItem } from '../components/TransformersUI/SeriesToRowsTransformerEditor'; import { concatenateTransformRegistryItem } from '../components/TransformersUI/ConcatenateTransformerEditor'; +import { renameByRegexTransformRegistryItem } from '../components/TransformersUI/RenameByRegexTransformer'; export const getStandardTransformers = (): Array> => { return [ reduceTransformRegistryItem, filterFieldsByNameTransformRegistryItem, + renameByRegexTransformRegistryItem, filterFramesByRefIdTransformRegistryItem, filterByValueTransformRegistryItem, organizeFieldsTransformRegistryItem,