mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Variables: Adds named capture groups to variable regex (#28625)
* Dashboard: Add named capture groups to variable query regex
Variable query regex are able to use 'text' and 'value' named capture
groups to allow for separate display text to be extracted from the
query result. e.g.
Using a regex of /foo="(?<text>[^"]+)|bar="(?<value>[^"]+)/g on a query
result of metric{foo="FOO", bar="BAR"} would result in the variable
value being set to 'BAR' but display text being set to 'FOO'
Resolves #21076
* Improve regex capture group documentation
* Update docs/sources/variables/filter-variables-with-regex.md
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
* Apply suggestions from code review
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
* Use text capture if value capture does not match
This is to keep the behaviour consistent with the current behavior. See
discussion https://github.com/grafana/grafana/pull/28625/files#r516490942
* Improve regex field placeholder and tooltip message
To make the feature more discoverable to users the place holder example
now includes the named capture groups. The tool tip message also
includes a reference and link to the documentation.
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com>
This commit is contained in:
@@ -193,14 +193,26 @@ export class QueryVariableEditorUnConnected extends PureComponent<Props, State>
|
||||
<div className="gf-form">
|
||||
<InlineFormLabel
|
||||
width={10}
|
||||
tooltip={'Optional, if you want to extract part of a series name or metric node segment.'}
|
||||
tooltip={
|
||||
<div>
|
||||
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 (
|
||||
<a
|
||||
href="https://grafana.com/docs/grafana/latest/variables/filter-variables-with-regex#filter-and-modify-using-named-text-and-value-capture-groups"
|
||||
target="__blank"
|
||||
>
|
||||
see examples
|
||||
</a>
|
||||
).
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Regex
|
||||
</InlineFormLabel>
|
||||
<input
|
||||
type="text"
|
||||
className="gf-form-input"
|
||||
placeholder="/.*-(.*)-.*/"
|
||||
placeholder="/.*-(?<text>.*)-(?<value>.*)-.*/"
|
||||
value={this.state.regex ?? this.props.variable.regex}
|
||||
onChange={this.onRegExChange}
|
||||
onBlur={this.onRegExBlur}
|
||||
|
||||
@@ -141,6 +141,103 @@ describe('queryVariableReducer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
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="(?<text>[^"]+).*somevalue="(?<value>[^"]+)/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<VariablesState>()
|
||||
.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="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/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<VariablesState>()
|
||||
.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="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/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<VariablesState>()
|
||||
.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="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/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<VariablesState>()
|
||||
.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="(?<value>[^"]+)|somelabel="(?<text>[^"]+)/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<VariablesState>()
|
||||
.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);
|
||||
|
||||
@@ -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'];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user