mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: Matches column names with unescaped regex characters (#21164)
* Table: Matches column names with unescaped regex characters Fixes #21106 * Chore: Cleans up unused code
This commit is contained in:
parent
26aa1f0cca
commit
05cb85feba
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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?)$'));
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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\\)/'} | ${'<td>1.230 s</td>'}
|
||||
${'Requests (Failed)'} | ${'/(Req)uests \\(Failed\\)/'} | ${'<td>1.230 s</td>'}
|
||||
${'Requests (Failed)'} | ${'Requests (Failed)'} | ${'<td>1.230 s</td>'}
|
||||
${'Requests (Failed)'} | ${'Requests \\(Failed\\)'} | ${'<td>1.230 s</td>'}
|
||||
${'Requests (Failed)'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
|
||||
${'Some other column'} | ${'/.*/'} | ${'<td>1.230 s</td>'}
|
||||
${'Requests (Failed)'} | ${'/Requests (Failed)/'} | ${'<td>1230</td>'}
|
||||
${'Requests (Failed)'} | ${'Response (Failed)'} | ${'<td>1230</td>'}
|
||||
`(
|
||||
'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();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user