diff --git a/docs/sources/variables/filter-variables-with-regex.md b/docs/sources/variables/filter-variables-with-regex.md index 4a79dabdaeb..2f6c1c63fe7 100644 --- a/docs/sources/variables/filter-variables-with-regex.md +++ b/docs/sources/variables/filter-variables-with-regex.md @@ -79,4 +79,35 @@ demo.robustperception.io:9090 demo.robustperception.io:9093 demo.robustperception.io:9100 ``` + +## Filter and modify using named text and value capture groups + +Using named capture groups, you can capture separate 'text' and 'value' parts from the options returned by the variable query. This allows the variable drop-down list to contain a friendly name for each value that can be selected. + +For example, when querying the `node_hwmon_chip_names` Prometheus metric, the `chip_name` is a lot friendlier that the `chip` value. So the following variable query result: + +```text +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_0",chip_name="enp216s0f0np0"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_1",chip_name="enp216s0f0np1"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_2",chip_name="enp216s0f0np2"} 1 +node_hwmon_chip_names{chip="0000:d7:00_0_0000:d8:00_3",chip_name="enp216s0f0np3"} 1 +``` + +Passed through the following Regex: + +```regex +/chip_name="(?[^"]+)|chip="(?[^"]+)/g ``` + +Would produce the following drop-down list: + +```text +Display Name Value +------------ ------------------------- +enp216s0f0np0 0000:d7:00_0_0000:d8:00_0 +enp216s0f0np1 0000:d7:00_0_0000:d8:00_1 +enp216s0f0np2 0000:d7:00_0_0000:d8:00_2 +enp216s0f0np3 0000:d7:00_0_0000:d8:00_3 +``` + +**Note:** Only `text` and `value` capture group names are supported. diff --git a/public/app/features/variables/query/QueryVariableEditor.tsx b/public/app/features/variables/query/QueryVariableEditor.tsx index f43c7456d2d..25853601cee 100644 --- a/public/app/features/variables/query/QueryVariableEditor.tsx +++ b/public/app/features/variables/query/QueryVariableEditor.tsx @@ -193,14 +193,26 @@ export class QueryVariableEditorUnConnected extends PureComponent
+ Optional, if you want to extract part of a series name or metric node segment. Named capture groups + can be used to separate the display text and value ( + + see examples + + ). +
+ } > Regex { }); }); + describe('when updateVariableOptions is dispatched and includeAll is false and regex is set and uses capture groups', () => { + it('normal regex should capture in order matches', () => { + const regex = '/somelabel="(?[^"]+).*somevalue="(?[^"]+)/i'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('global regex should capture out of order matches', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched text capture will use value capture', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somename="atext",somevalue="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'avalue', value: 'avalue', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched value capture will use text capture', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{somelabel="atext",somename="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'atext', value: 'atext', selected: false }], + } as unknown) as QueryVariableModel, + }); + }); + + it('unmatched text capture and unmatched value capture returns empty state', () => { + const regex = '/somevalue="(?[^"]+)|somelabel="(?[^"]+)/gi'; + const { initialState } = getVariableTestContext(adapter, { includeAll: false, regex }); + const metrics = [createMetric('A{someother="atext",something="avalue"}'), createMetric('B')]; + const update = { results: metrics, templatedRegex: regex }; + const payload = toVariablePayload({ id: '0', type: 'query' }, update); + + reducerTester() + .givenReducer(queryVariableReducer, cloneDeep(initialState)) + .whenActionIsDispatched(updateVariableOptions(payload)) + .thenStateShouldEqual({ + ...initialState, + '0': ({ + ...initialState[0], + options: [{ text: 'None', value: '', selected: false, isNone: true }], + } as unknown) as QueryVariableModel, + }); + }); + }); + describe('when updateVariableTags is dispatched', () => { it('then state should be correct', () => { const { initialState } = getVariableTestContext(adapter); diff --git a/public/app/features/variables/query/reducer.ts b/public/app/features/variables/query/reducer.ts index ad77b03ce6b..d49120b6064 100644 --- a/public/app/features/variables/query/reducer.ts +++ b/public/app/features/variables/query/reducer.ts @@ -86,6 +86,18 @@ const sortVariableValues = (options: any[], sortOrder: VariableSort) => { return options; }; +const getAllMatches = (str: string, regex: RegExp): any => { + const results = {}; + let matches; + + do { + matches = regex.exec(str); + _.merge(results, matches); + } while (regex.global && matches); + + return results; +}; + const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, metricNames: any[]) => { let regex, i, matches; let options: VariableOption[] = []; @@ -109,13 +121,23 @@ const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, } if (regex) { - matches = regex.exec(value); - if (!matches) { + matches = getAllMatches(value, regex); + + if (_.isEmpty(matches)) { continue; } - if (matches.length > 1) { - value = matches[1]; - text = matches[1]; + + if (matches.groups && matches.groups.value && matches.groups.text) { + value = matches.groups.value; + text = matches.groups.text; + } else if (matches.groups && matches.groups.value) { + value = matches.groups.value; + text = value; + } else if (matches.groups && matches.groups.text) { + text = matches.groups.text; + value = text; + } else if (matches['1']) { + value = matches['1']; } }