diff --git a/packages/grafana-data/src/text/string.test.ts b/packages/grafana-data/src/text/string.test.ts index b99614c0406..b9cd3eb0866 100644 --- a/packages/grafana-data/src/text/string.test.ts +++ b/packages/grafana-data/src/text/string.test.ts @@ -1,6 +1,11 @@ -import { stringToJsRegex, stringToMs } from './string'; +import { escapeStringForRegex, stringToJsRegex, stringToMs, unEscapeStringFromRegex } from './string'; describe('stringToJsRegex', () => { + it('should just return string as RegEx if it does not start as a regex', () => { + const output = stringToJsRegex('validRegexp'); + expect(output).toBeInstanceOf(RegExp); + }); + it('should parse the valid regex value', () => { const output = stringToJsRegex('/validRegexp/'); expect(output).toBeInstanceOf(RegExp); @@ -51,3 +56,35 @@ describe('stringToMs', () => { }).toThrow(); }); }); + +describe('escapeStringForRegex', () => { + describe('when using a string with special chars', () => { + it('then all special chars should be escaped', () => { + const result = escapeStringForRegex('([{}])|*+-.?<>#&^$'); + expect(result).toBe('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$'); + }); + }); + + describe('when using a string without special chars', () => { + it('then nothing should change', () => { + const result = escapeStringForRegex('some string 123'); + expect(result).toBe('some string 123'); + }); + }); +}); + +describe('unEscapeStringFromRegex', () => { + describe('when using a string with escaped special chars', () => { + it('then all special chars should be unescaped', () => { + const result = unEscapeStringFromRegex('\\(\\[\\{\\}\\]\\)\\|\\*\\+\\-\\.\\?\\<\\>\\#\\&\\^\\$'); + expect(result).toBe('([{}])|*+-.?<>#&^$'); + }); + }); + + describe('when using a string without escaped special chars', () => { + it('then nothing should change', () => { + const result = unEscapeStringFromRegex('some string 123'); + expect(result).toBe('some string 123'); + }); + }); +}); diff --git a/packages/grafana-data/src/text/string.ts b/packages/grafana-data/src/text/string.ts index dd95136abbb..83754d51e01 100644 --- a/packages/grafana-data/src/text/string.ts +++ b/packages/grafana-data/src/text/string.ts @@ -1,6 +1,32 @@ +const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$']; + +export const escapeStringForRegex = (value: string) => { + if (!value) { + return value; + } + + return specialChars.reduce((escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), value); +}; + +export const unEscapeStringFromRegex = (value: string) => { + if (!value) { + return value; + } + + return specialChars.reduce((escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), value); +}; + +export function stringStartsAsRegEx(str: string): boolean { + if (!str) { + return false; + } + + return str[0] === '/'; +} + export function stringToJsRegex(str: string): RegExp { - if (str[0] !== '/') { - return new RegExp('^' + str + '$'); + if (!stringStartsAsRegEx(str)) { + return new RegExp(`^${str}$`); } const match = str.match(new RegExp('^/(.*?)/(g?i?m?y?)$')); diff --git a/public/app/core/components/FilterInput/FilterInput.tsx b/public/app/core/components/FilterInput/FilterInput.tsx index b69f7d9651d..5505166befa 100644 --- a/public/app/core/components/FilterInput/FilterInput.tsx +++ b/public/app/core/components/FilterInput/FilterInput.tsx @@ -1,32 +1,5 @@ import React, { forwardRef } from 'react'; - -const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$']; - -export const escapeStringForRegex = (value: string) => { - if (!value) { - return value; - } - - const newValue = specialChars.reduce( - (escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar), - value - ); - - return newValue; -}; - -export const unEscapeStringFromRegex = (value: string) => { - if (!value) { - return value; - } - - const newValue = specialChars.reduce( - (escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar), - value - ); - - return newValue; -}; +import { escapeStringForRegex, unEscapeStringFromRegex } from '@grafana/data'; export interface Props { value: string | undefined; diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 845b0838a4c..e17425b5e63 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -4,12 +4,11 @@ import React from 'react'; import { components } from '@torkelo/react-select'; // @ts-ignore import AsyncSelect from '@torkelo/react-select/lib/Async'; - +import { escapeStringForRegex } from '@grafana/data'; // Components import { TagOption } from './TagOption'; import { TagBadge } from './TagBadge'; -import { NoOptionsMessage, IndicatorsContainer, resetSelectStyles } from '@grafana/ui'; -import { escapeStringForRegex } from '../FilterInput/FilterInput'; +import { IndicatorsContainer, NoOptionsMessage, resetSelectStyles } from '@grafana/ui'; export interface TermCount { term: string; diff --git a/public/app/plugins/panel/table/renderer.ts b/public/app/plugins/panel/table/renderer.ts index 4e8d2a2dcf5..752ec2c0893 100644 --- a/public/app/plugins/panel/table/renderer.ts +++ b/public/app/plugins/panel/table/renderer.ts @@ -1,16 +1,19 @@ import _ from 'lodash'; import { dateTime, - getValueFormat, - getColorFromHexRgbOrName, - GrafanaThemeType, - stringToJsRegex, - ScopedVars, + escapeStringForRegex, formattedValueToString, + getColorFromHexRgbOrName, + getValueFormat, + GrafanaThemeType, + ScopedVars, + stringStartsAsRegEx, + stringToJsRegex, + unEscapeStringFromRegex, } from '@grafana/data'; import { ColumnStyle } from '@grafana/ui/src/components/Table/TableCellBuilder'; import { TemplateSrv } from 'app/features/templating/template_srv'; -import { TableRenderModel, ColumnRender } from './types'; +import { ColumnRender, TableRenderModel } from './types'; export class TableRenderer { formatters: any[]; @@ -44,7 +47,10 @@ export class TableRenderer { for (let i = 0; i < this.panel.styles.length; i++) { const style = this.panel.styles[i]; - const regex = stringToJsRegex(style.pattern); + const escapedPattern = stringStartsAsRegEx(style.pattern) + ? style.pattern + : escapeStringForRegex(unEscapeStringFromRegex(style.pattern)); + const regex = stringToJsRegex(escapedPattern); if (column.text.match(regex)) { column.style = style; diff --git a/public/app/plugins/panel/table/specs/renderer.test.ts b/public/app/plugins/panel/table/specs/renderer.test.ts index 1f7f8dc82e1..09cc7e34bbd 100644 --- a/public/app/plugins/panel/table/specs/renderer.test.ts +++ b/public/app/plugins/panel/table/specs/renderer.test.ts @@ -1,10 +1,25 @@ import _ from 'lodash'; import TableModel from 'app/core/table_model'; import { TableRenderer } from '../renderer'; -import { getColorDefinitionByName } from '@grafana/data'; -import { ScopedVars } from '@grafana/data'; +import { getColorDefinitionByName, ScopedVars } from '@grafana/data'; import { ColumnRender } from '../types'; +const sanitize = (value: any): string => { + return 'sanitized'; +}; + +const templateSrv = { + replace: (value: any, scopedVars: ScopedVars) => { + if (scopedVars) { + // For testing variables replacement in link + _.each(scopedVars, (val, key) => { + value = value.replace('$' + key, val.value); + }); + } + return value; + }, +}; + describe('when rendering table', () => { const SemiDarkOrange = getColorDefinitionByName('semi-dark-orange'); @@ -173,22 +188,6 @@ describe('when rendering table', () => { ], }; - const sanitize = (value: any): string => { - return 'sanitized'; - }; - - const templateSrv = { - replace: (value: any, scopedVars: ScopedVars) => { - if (scopedVars) { - // For testing variables replacement in link - _.each(scopedVars, (val, key) => { - value = value.replace('$' + key, val.value); - }); - } - return value; - }, - }; - //@ts-ignore const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); @@ -407,6 +406,51 @@ describe('when rendering table', () => { }); }); +describe('when rendering table with different patterns', () => { + it.each` + column | pattern | expected + ${'Requests (Failed)'} | ${'/Requests \\(Failed\\)/'} | ${'1.230 s'} + ${'Requests (Failed)'} | ${'/(Req)uests \\(Failed\\)/'} | ${'1.230 s'} + ${'Requests (Failed)'} | ${'Requests (Failed)'} | ${'1.230 s'} + ${'Requests (Failed)'} | ${'Requests \\(Failed\\)'} | ${'1.230 s'} + ${'Requests (Failed)'} | ${'/.*/'} | ${'1.230 s'} + ${'Some other column'} | ${'/.*/'} | ${'1.230 s'} + ${'Requests (Failed)'} | ${'/Requests (Failed)/'} | ${'1230'} + ${'Requests (Failed)'} | ${'Response (Failed)'} | ${'1230'} + `( + 'number column should be formatted for a column:$column with the pattern:$pattern', + ({ column, pattern, expected }) => { + const table = new TableModel(); + table.columns = [{ text: 'Time' }, { text: column }]; + table.rows = [[1388556366666, 1230]]; + const panel = { + pageSize: 10, + styles: [ + { + pattern: 'Time', + type: 'date', + format: 'LLL', + alias: 'Timestamp', + }, + { + pattern: pattern, + type: 'number', + unit: 'ms', + decimals: 3, + alias: pattern, + }, + ], + }; + + //@ts-ignore + const renderer = new TableRenderer(panel, table, 'utc', sanitize, templateSrv); + const html = renderer.renderCell(1, 0, 1230); + + expect(html).toBe(expected); + } + ); +}); + function normalize(str: string) { return str.replace(/\s+/gm, ' ').trim(); }