From 2a3aa951631cc0df141e96b251bd6011746f2ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 5 Feb 2021 07:16:06 +0100 Subject: [PATCH] Variables: Adds queryparam formatting option (#30858) * Variables: Adds queryparam formatting option * Chore: fixes strict errors * Chore: changes after PR comments --- .../global-variables-and-interpolation.json | 4 +- docs/sources/linking/data-link-variables.md | 2 +- .../advanced-variable-format-options.md | 10 ++++ e2e/suite1/specs/dashboard-templating.spec.ts | 3 +- .../components/DataLinks/DataLinkInput.tsx | 10 ++-- .../app/features/templating/formatRegistry.ts | 27 +++++++++ .../features/templating/template_srv.test.ts | 58 +++++++++++++++++++ .../app/features/templating/template_srv.ts | 2 +- public/app/features/variables/guard.ts | 2 +- public/app/features/variables/utils.test.ts | 53 +++++++++++------ public/app/features/variables/utils.ts | 26 +++++++-- 11 files changed, 163 insertions(+), 34 deletions(-) diff --git a/devenv/dev-dashboards/feature-templating/global-variables-and-interpolation.json b/devenv/dev-dashboards/feature-templating/global-variables-and-interpolation.json index 4f1ab116d58..0ef23ebc1c5 100644 --- a/devenv/dev-dashboards/feature-templating/global-variables-and-interpolation.json +++ b/devenv/dev-dashboards/feature-templating/global-variables-and-interpolation.json @@ -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", diff --git a/docs/sources/linking/data-link-variables.md b/docs/sources/linking/data-link-variables.md index ab34e4d040b..7e1d72448ef 100644 --- a/docs/sources/linking/data-link-variables.md +++ b/docs/sources/linking/data-link-variables.md @@ -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``. diff --git a/docs/sources/variables/advanced-variable-format-options.md b/docs/sources/variables/advanced-variable-format-options.md index b717cb4edc4..0650e78e5b1 100644 --- a/docs/sources/variables/advanced-variable-format-options.md +++ b/docs/sources/variables/advanced-variable-format-options.md @@ -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" +``` diff --git a/e2e/suite1/specs/dashboard-templating.spec.ts b/e2e/suite1/specs/dashboard-templating.spec.ts index 4efa4bcece5..a7c4db01033 100644 --- a/e2e/suite1/specs/dashboard-templating.spec.ts +++ b/e2e/suite1/specs/dashboard-templating.spec.ts @@ -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()); }) diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index 3d1e9b7304c..455a0aeadc3 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -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 = 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); diff --git a/public/app/features/templating/formatRegistry.ts b/public/app/features/templating/formatRegistry.ts index c981c25df44..ec88a3438fe 100644 --- a/public/app/features/templating/formatRegistry.ts +++ b/public/app/features/templating/formatRegistry.ts @@ -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(() => { 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); +} diff --git a/public/app/features/templating/template_srv.test.ts b/public/app/features/templating/template_srv.test.ts index 1c433f277cc..f0b85fe347f 100644 --- a/public/app/features/templating/template_srv.test.ts +++ b/public/app/features/templating/template_srv.test.ts @@ -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, + (createAdHocVariableAdapter() as unknown) as VariableAdapter, +]); 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'); + }); + }); }); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 0a881f69174..6693b3bc76a 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -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); } } diff --git a/public/app/features/variables/guard.ts b/public/app/features/variables/guard.ts index e4e22aa8434..701f6c8c41a 100644 --- a/public/app/features/variables/guard.ts +++ b/public/app/features/variables/guard.ts @@ -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, diff --git a/public/app/features/variables/utils.test.ts b/public/app/features/variables/utils.test.ts index 57c22152334..d39c502f153 100644 --- a/public/app/features/variables/utils.test.ts +++ b/public/app/features/variables/utils.test.ts @@ -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); }); diff --git a/public/app/features/variables/utils.ts b/public/app/features/variables/utils.ts index 675ca56b07f..76dd337073a 100644 --- a/public/app/features/variables/utils.ts +++ b/public/app/features/variables/utils.ts @@ -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 => {