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
11 changed files with 163 additions and 34 deletions
@@ -27,14 +27,14 @@
"overrides": [] "overrides": []
}, },
"gridPos": { "gridPos": {
"h": 16, "h": 18,
"w": 24, "w": 24,
"x": 0, "x": 0,
"y": 0 "y": 0
}, },
"id": 11, "id": 11,
"options": { "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" "mode": "markdown"
}, },
"pluginVersion": "7.1.0", "pluginVersion": "7.1.0",
+1 -1
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. 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``. If you want to add all of the current dashboard's variables to the URL, then use ``__all_variables``.
@@ -149,3 +149,13 @@ servers = ["test1", "test2"]
String to interpolate: '${servers:text}' String to interpolate: '${servers:text}'
Interpolation result: "test1 + test2" 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"
```
@@ -35,11 +35,12 @@ e2e.scenario({
`Server:sqlstring = 'A''A"A','BB\\\B','CCC'`, `Server:sqlstring = 'A''A"A','BB\\\B','CCC'`,
`Server:date = null`, `Server:date = null`,
`Server:text = All`, `Server:text = All`,
`Server:queryparam = var-Server=All`,
]; ];
e2e() e2e()
.get('.markdown-html li') .get('.markdown-html li')
.should('have.length', 23) .should('have.length', 24)
.each((element) => { .each((element) => {
items.push(element.text()); items.push(element.text());
}) })
@@ -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 usePrevious from 'react-use/lib/usePrevious';
import { DataLinkSuggestions } from './DataLinkSuggestions'; import { DataLinkSuggestions } from './DataLinkSuggestions';
import { ThemeContext, makeValue } from '../../index'; import { makeValue, ThemeContext } from '../../index';
import { SelectionReference } from './SelectionReference'; import { SelectionReference } from './SelectionReference';
import { Portal, getFormStyles } from '../index'; import { getFormStyles, Portal } from '../index';
// @ts-ignore // @ts-ignore
import Prism, { Grammar, LanguageMap } from 'prismjs'; import Prism, { Grammar, LanguageMap } from 'prismjs';
@@ -16,7 +16,7 @@ import { css, cx } from 'emotion';
import { SlatePrism } from '../../slate-plugins'; import { SlatePrism } from '../../slate-plugins';
import { SCHEMA } from '../../utils/slate'; import { SCHEMA } from '../../utils/slate';
import { stylesFactory } from '../../themes'; 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); 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) { if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) {
editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`); editor.insertText(`${includeDollarSign ? '$' : ''}\{${item.value}}`);
} else { } else {
editor.insertText(`var-${item.value}=$\{${item.value}}`); editor.insertText(`\${${item.value}:queryparam}`);
} }
setLinkUrl(editor.value); setLinkUrl(editor.value);
@@ -3,6 +3,8 @@ import { dateTime, Registry, RegistryItem, textUtil, VariableModel } from '@graf
import { isArray, map, replace } from 'lodash'; import { isArray, map, replace } from 'lodash';
import { formatVariableLabel } from '../variables/shared/formatVariable'; import { formatVariableLabel } from '../variables/shared/formatVariable';
import { ALL_VARIABLE_TEXT, ALL_VARIABLE_VALUE } from '../variables/state/types'; 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 { export interface FormatOptions {
value: any; value: any;
@@ -217,6 +219,23 @@ export const formatRegistry = new Registry<FormatRegistryItem>(() => {
return formatVariableLabel(variable); 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; return formats;
@@ -236,3 +255,11 @@ function encodeURIComponentStrict(str: string) {
return '%' + c.charCodeAt(0).toString(16).toUpperCase(); 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);
}
@@ -1,6 +1,15 @@
import { dateTime, TimeRange } from '@grafana/data'; import { dateTime, TimeRange } from '@grafana/data';
import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv'; import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv';
import { silenceConsoleOutput } from '../../../test/core/utils/silenceConsoleOutput'; 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', () => { describe('templateSrv', () => {
silenceConsoleOutput(); silenceConsoleOutput();
@@ -225,6 +234,11 @@ describe('templateSrv', () => {
const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob'); const target = _templateSrv.replace('${test:pipe},$test', {}, 'glob');
expect(target).toBe('value1|value2,{value1,value2}'); 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', () => { describe('variable with all option and custom value', () => {
@@ -264,6 +278,11 @@ describe('templateSrv', () => {
const target = _templateSrv.replace('this.$test', {}, 'regex'); const target = _templateSrv.replace('this.$test', {}, 'regex');
expect(target).toBe('this.*'); 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', () => { describe('lucene format', () => {
@@ -640,4 +659,43 @@ describe('templateSrv', () => {
expect(passedValue).toBe('hello'); 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');
});
});
}); });
@@ -282,7 +282,7 @@ export class TemplateSrv implements BaseTemplateSrv {
value = this.getAllValue(variable); value = this.getAllValue(variable);
text = ALL_VARIABLE_TEXT; text = ALL_VARIABLE_TEXT;
// skip formatting of custom all values // skip formatting of custom all values
if (variable.allValue && fmt !== 'text') { if (variable.allValue && fmt !== 'text' && fmt !== 'queryparam') {
return this.replace(value); return this.replace(value);
} }
} }
+1 -1
View File
@@ -11,6 +11,7 @@ import {
QueryEditorProps, QueryEditorProps,
StandardVariableQuery, StandardVariableQuery,
StandardVariableSupport, StandardVariableSupport,
VariableModel,
VariableSupportType, VariableSupportType,
} from '@grafana/data'; } from '@grafana/data';
@@ -18,7 +19,6 @@ import {
AdHocVariableModel, AdHocVariableModel,
ConstantVariableModel, ConstantVariableModel,
QueryVariableModel, QueryVariableModel,
VariableModel,
VariableQueryEditorType, VariableQueryEditorType,
VariableWithMultiSupport, VariableWithMultiSupport,
VariableWithOptions, VariableWithOptions,
+36 -17
View File
@@ -3,23 +3,42 @@ import { VariableRefresh } from './types';
describe('isAllVariable', () => { describe('isAllVariable', () => {
it.each` it.each`
variable | expected variable | expected
${null} | ${false} ${null} | ${false}
${undefined} | ${false} ${undefined} | ${false}
${{}} | ${false} ${{}} | ${false}
${{ current: {} }} | ${false} ${{ current: {} }} | ${false}
${{ current: { text: '' } }} | ${false} ${{ current: { text: '' } }} | ${false}
${{ current: { text: null } }} | ${false} ${{ current: { text: null } }} | ${false}
${{ current: { text: undefined } }} | ${false} ${{ current: { text: undefined } }} | ${false}
${{ current: { text: 'Alll' } }} | ${false} ${{ current: { text: 'Alll' } }} | ${false}
${{ current: { text: 'All' } }} | ${true} ${{ current: { text: 'All' } }} | ${true}
${{ current: { text: [] } }} | ${false} ${{ current: { text: [] } }} | ${false}
${{ current: { text: [null] } }} | ${false} ${{ current: { text: [null] } }} | ${false}
${{ current: { text: [undefined] } }} | ${false} ${{ current: { text: [undefined] } }} | ${false}
${{ current: { text: ['Alll'] } }} | ${false} ${{ current: { text: ['Alll'] } }} | ${false}
${{ current: { text: ['Alll', 'All'] } }} | ${false} ${{ current: { text: ['Alll', 'All'] } }} | ${false}
${{ current: { text: ['All'] } }} | ${true} ${{ current: { text: ['All'] } }} | ${true}
${{ current: { text: { prop1: 'test' } } }} | ${false} ${{ 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 }) => { `("when called with params: 'variable': '$variable' then result should be '$expected'", ({ variable, expected }) => {
expect(isAllVariable(variable)).toEqual(expected); expect(isAllVariable(variable)).toEqual(expected);
}); });
+20 -6
View File
@@ -2,7 +2,7 @@ import isString from 'lodash/isString';
import { ScopedVars, VariableType } from '@grafana/data'; import { ScopedVars, VariableType } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime'; 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 { QueryVariableModel, VariableModel, VariableRefresh } from './types';
import { getTimeSrv } from '../dashboard/services/TimeSrv'; import { getTimeSrv } from '../dashboard/services/TimeSrv';
import { variableAdapters } from './adapters'; import { variableAdapters } from './adapters';
@@ -74,15 +74,29 @@ export const isAllVariable = (variable: any): boolean => {
return false; return false;
} }
if (!variable.current.text) { if (variable.current.value) {
return false; 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)) { if (variable.current.text) {
return variable.current.text.length ? variable.current.text[0] === ALL_VARIABLE_TEXT : false; 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 => { export const getCurrentText = (variable: any): string => {