Variables: Adds queryparam formatting option (#30858)

* Variables: Adds queryparam formatting option

* Chore: fixes strict errors

* Chore: changes after PR comments
This commit is contained in:
Hugo Häggmark 2021-02-05 07:16:06 +01:00 committed by GitHub
parent 95efd3e51d
commit 2a3aa95163
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 163 additions and 34 deletions

View File

@ -27,14 +27,14 @@
"overrides": []
},
"gridPos": {
"h": 16,
"h": 18,
"w": 24,
"x": 0,
"y": 0
},
"id": 11,
"options": {
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n* `__user.email` = `${__user.email}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n* `Server:text` = `${Server:text}`\n\n",
"content": "## Global variables\n\n* `__dashboard` = `${__dashboard}`\n* `__dashboard.name` = `${__dashboard.name}`\n* `__dashboard.uid` = `${__dashboard.uid}`\n* `__org.name` = `${__org.name}`\n* `__org.id` = `${__org.id}`\n* `__user.id` = `${__user.id}`\n* `__user.login` = `${__user.login}`\n* `__user.email` = `${__user.email}`\n \n## Formats\n\n* `Server:raw` = `${Server:raw}`\n* `Server:regex` = `${Server:regex}`\n* `Server:lucene` = `${Server:lucene}`\n* `Server:glob` = `${Server:glob}`\n* `Server:pipe` = `${Server:pipe}`\n* `Server:distributed` = `${Server:distributed}`\n* `Server:csv` = `${Server:csv}`\n* `Server:html` = `${Server:html}`\n* `Server:json` = `${Server:json}`\n* `Server:percentencode` = `${Server:percentencode}`\n* `Server:singlequote` = `${Server:singlequote}`\n* `Server:doublequote` = `${Server:doublequote}`\n* `Server:sqlstring` = `${Server:sqlstring}`\n* `Server:date` = `${Server:date}`\n* `Server:text` = `${Server:text}`\n* `Server:queryparam` = `${Server:queryparam}`\n\n",
"mode": "markdown"
},
"pluginVersion": "7.1.0",

View File

@ -49,6 +49,6 @@ Value-specific variables are available under ``__value`` namespace:
When linking to another dashboard that uses template variables, select variable values for whoever clicks the link.
``var-myvar=${myvar}`` - where ``myvar`` is a name of the template variable that matches one in the current dashboard that you want to use.
``${myvar:queryparams}`` - where ``myvar`` is a name of the template variable that matches one in the current dashboard that you want to use.
If you want to add all of the current dashboard's variables to the URL, then use ``__all_variables``.

View File

@ -149,3 +149,13 @@ servers = ["test1", "test2"]
String to interpolate: '${servers:text}'
Interpolation result: "test1 + test2"
```
## Query parameters
Formats single- and multi-valued variables into their query parameter representation. Example: `var-foo=value1&var-foo=value2`
```bash
servers = ["test1", "test2"]
String to interpolate: '${servers:queryparam}'
Interpolation result: "var-servers=test1&var-servers=test2"
```

View File

@ -35,11 +35,12 @@ e2e.scenario({
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
`Server:date = null`,
`Server:text = All`,
`Server:queryparam = var-Server=All`,
];
e2e()
.get('.markdown-html li')
.should('have.length', 23)
.should('have.length', 24)
.each((element) => {
items.push(element.text());
})

View File

@ -1,9 +1,9 @@
import React, { useState, useMemo, useContext, useRef, RefObject, memo, useEffect } from 'react';
import React, { memo, RefObject, useContext, useEffect, useMemo, useRef, useState } from 'react';
import usePrevious from 'react-use/lib/usePrevious';
import { DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, makeValue } from '../../index';
import { makeValue, ThemeContext } from '../../index';
import { SelectionReference } from './SelectionReference';
import { Portal, getFormStyles } from '../index';
import { getFormStyles, Portal } from '../index';
// @ts-ignore
import Prism, { Grammar, LanguageMap } from 'prismjs';
@ -16,7 +16,7 @@ import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate';
import { stylesFactory } from '../../themes';
import { GrafanaTheme, VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data';
import { DataLinkBuiltInVars, GrafanaTheme, VariableOrigin, VariableSuggestion } from '@grafana/data';
const modulo = (a: number, n: number) => a - n * Math.floor(a / n);
@ -130,7 +130,7 @@ export const DataLinkInput: React.FC<DataLinkInputProps> = memo(
if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
} else {
editor.insertText(`var-${item.value}=$\{${item.value}}`);
editor.insertText(`\${${item.value}:queryparam}`);
}
setLinkUrl(editor.value);

View File

@ -3,6 +3,8 @@ import { dateTime, Registry, RegistryItem, textUtil, VariableModel } from '@graf
import { isArray, map, replace } from 'lodash';
import { formatVariableLabel } from '../variables/shared/formatVariable';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types';
import { variableAdapters } from '../variables/adapters';
import { VariableModel as ExtendedVariableModel } from '../variables/types';
export interface FormatOptions {
value: any;
@ -217,6 +219,23 @@ export const formatRegistry = new Registry<FormatRegistryItem>(() => {
return formatVariableLabel(variable);
},
},
{
id: 'queryparam',
name: 'Query Parameter',
description:
'Format variables as url parameter. Example in multi variable scenario A + B + C => var-foo=A&var-foo=B&var-foo=C.',
formatter: (options, variable) => {
const { name, type } = variable;
const adapter = variableAdapters.get(type);
const valueForUrl = adapter.getValueForUrl(variable as ExtendedVariableModel);
if (Array.isArray(valueForUrl)) {
return valueForUrl.map((v) => formatQueryParameter(name, v)).join('&');
}
return formatQueryParameter(name, valueForUrl);
},
},
];
return formats;
@ -236,3 +255,11 @@ function encodeURIComponentStrict(str: string) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase();
});
}
function formatQueryParameter(name: string, value: string): string {
return `var-${name}=${encodeURIComponentStrict(value)}`;
}
export function isAllValue(value: any) {
return value === ALL_VARIABLE_VALUE || (Array.isArray(value) && value[0] === ALL_VARIABLE_VALUE);
}

View File

@ -1,6 +1,15 @@
import { dateTime, TimeRange } from '@grafana/data';
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput';
import { VariableAdapter, variableAdapters } from '../variables/adapters';
import { createQueryVariableAdapter } from '../variables/query/adapter';
import { createAdHocVariableAdapter } from '../variables/adhoc/adapter';
import { VariableModel } from '../variables/types';
variableAdapters.setInit(() => [
(createQueryVariableAdapter() as unknown) as VariableAdapter<VariableModel>,
(createAdHocVariableAdapter() as unknown) as VariableAdapter<VariableModel>,
]);
describe('templateSrv', () => {
silenceConsoleOutput();
@ -225,6 +234,11 @@ describe('templateSrv', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}');
});
it('should replace ${test:queryparam} with correct query parameter', () => {
const target = _templateSrv.replace('${test:queryparam}', {});
expect(target).toBe('var-test=All');
});
});
describe('variable with all option and custom value', () => {
@ -264,6 +278,11 @@ describe('templateSrv', () => {
const target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).toBe('this.*');
});
it('should replace ${test:queryparam} with correct query parameter', () => {
const target = _templateSrv.replace('${test:queryparam}', {});
expect(target).toBe('var-test=All');
});
});
describe('lucene format', () => {
@ -640,4 +659,43 @@ describe('templateSrv', () => {
expect(passedValue).toBe('hello');
});
});
describe('queryparam', () => {
beforeEach(() => {
_templateSrv = initTemplateSrv([
{
type: 'query',
name: 'single',
current: { value: 'value1' },
options: [{ value: 'value1' }, { value: 'value2' }],
},
{
type: 'query',
name: 'multi',
current: { value: ['value1', 'value2'] },
options: [{ value: 'value1' }, { value: 'value2' }],
},
]);
});
it('query variable with single value with queryparam format should return correct queryparam', () => {
const target = _templateSrv.replace('${single:queryparam}', {});
expect(target).toBe('var-single=value1');
});
it('query variable with single value and queryparam format should return correct queryparam', () => {
const target = _templateSrv.replace('${single}', {}, 'queryparam');
expect(target).toBe('var-single=value1');
});
it('query variable with multi value with queryparam format should return correct queryparam', () => {
const target = _templateSrv.replace('${multi:queryparam}', {});
expect(target).toBe('var-multi=value1&var-multi=value2');
});
it('query variable with multi value and queryparam format should return correct queryparam', () => {
const target = _templateSrv.replace('${multi}', {}, 'queryparam');
expect(target).toBe('var-multi=value1&var-multi=value2');
});
});
});

View File

@ -282,7 +282,7 @@ export class TemplateSrv implements BaseTemplateSrv {
value = this.getAllValue(variable);
text = ALL_VARIABLE_TEXT;
// skip formatting of custom all values
if (variable.allValue && fmt !== 'text') {
if (variable.allValue && fmt !== 'text' && fmt !== 'queryparam') {
return this.replace(value);
}
}

View File

@ -11,6 +11,7 @@ import {
QueryEditorProps,
StandardVariableQuery,
StandardVariableSupport,
VariableModel,
VariableSupportType,
} from '@grafana/data';
@ -18,7 +19,6 @@ import {
AdHocVariableModel,
ConstantVariableModel,
QueryVariableModel,
VariableModel,
VariableQueryEditorType,
VariableWithMultiSupport,
VariableWithOptions,

View File

@ -3,23 +3,42 @@ import { VariableRefresh } from './types';
describe('isAllVariable', () => {
it.each`
variable | expected
${null} | ${false}
${undefined} | ${false}
${{}} | ${false}
${{ current: {} }} | ${false}
${{ current: { text: '' } }} | ${false}
${{ current: { text: null } }} | ${false}
${{ current: { text: undefined } }} | ${false}
${{ current: { text: 'Alll' } }} | ${false}
${{ current: { text: 'All' } }} | ${true}
${{ current: { text: [] } }} | ${false}
${{ current: { text: [null] } }} | ${false}
${{ current: { text: [undefined] } }} | ${false}
${{ current: { text: ['Alll'] } }} | ${false}
${{ current: { text: ['Alll', 'All'] } }} | ${false}
${{ current: { text: ['All'] } }} | ${true}
${{ current: { text: { prop1: 'test' } } }} | ${false}
variable | expected
${null} | ${false}
${undefined} | ${false}
${{}} | ${false}
${{ current: {} }} | ${false}
${{ current: { text: '' } }} | ${false}
${{ current: { text: null } }} | ${false}
${{ current: { text: undefined } }} | ${false}
${{ current: { text: 'Alll' } }} | ${false}
${{ current: { text: 'All' } }} | ${true}
${{ current: { text: [] } }} | ${false}
${{ current: { text: [null] } }} | ${false}
${{ current: { text: [undefined] } }} | ${false}
${{ current: { text: ['Alll'] } }} | ${false}
${{ current: { text: ['Alll', 'All'] } }} | ${false}
${{ current: { text: ['All'] } }} | ${true}
${{ current: { text: ['All', 'Alll'] } }} | ${true}
${{ current: { text: { prop1: 'test' } } }} | ${false}
${{ current: { value: '' } }} | ${false}
${{ current: { value: null } }} | ${false}
${{ current: { value: undefined } }} | ${false}
${{ current: { value: '$__alll' } }} | ${false}
${{ current: { value: '$__all' } }} | ${true}
${{ current: { value: [] } }} | ${false}
${{ current: { value: [null] } }} | ${false}
${{ current: { value: [undefined] } }} | ${false}
${{ current: { value: ['$__alll'] } }} | ${false}
${{ current: { value: ['$__alll', '$__all'] } }} | ${false}
${{ current: { value: ['$__all'] } }} | ${true}
${{ current: { value: ['$__all', '$__alll'] } }} | ${true}
${{ current: { value: { prop1: 'test' } } }} | ${false}
${{ current: { value: '', text: '' } }} | ${false}
${{ current: { value: '', text: 'All' } }} | ${true}
${{ current: { value: '$__all', text: '' } }} | ${true}
${{ current: { value: '', text: ['All'] } }} | ${true}
${{ current: { value: ['$__all'], text: '' } }} | ${true}
`("when called with params: 'variable': '$variable' then result should be '$expected'", ({ variable, expected }) => {
expect(isAllVariable(variable)).toEqual(expected);
});

View File

@ -2,7 +2,7 @@ import isString from 'lodash/isString';
import { ScopedVars, VariableType } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { ALL_VARIABLE_TEXT } from './state/types';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from './state/types';
import { QueryVariableModel, VariableModel, VariableRefresh } from './types';
import { getTimeSrv } from '../dashboard/services/TimeSrv';
import { variableAdapters } from './adapters';
@ -74,15 +74,29 @@ export const isAllVariable = (variable: any): boolean => {
return false;
}
if (!variable.current.text) {
return false;
if (variable.current.value) {
const isArray = Array.isArray(variable.current.value);
if (isArray && variable.current.value.length && variable.current.value[0] === ALL_VARIABLE_VALUE) {
return true;
}
if (!isArray && variable.current.value === ALL_VARIABLE_VALUE) {
return true;
}
}
if (Array.isArray(variable.current.text)) {
return variable.current.text.length ? variable.current.text[0] === ALL_VARIABLE_TEXT : false;
if (variable.current.text) {
const isArray = Array.isArray(variable.current.text);
if (isArray && variable.current.text.length && variable.current.text[0] === ALL_VARIABLE_TEXT) {
return true;
}
if (!isArray && variable.current.text === ALL_VARIABLE_TEXT) {
return true;
}
}
return variable.current.text === ALL_VARIABLE_TEXT;
return false;
};
export const getCurrentText = (variable: any): string => {