From 683ce69347e3517d79d047324d8250877fe5cea6 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Tue, 15 Dec 2020 13:29:37 +0100 Subject: [PATCH] Link suppliers: getLinks API update (#29757) * ContextMenuPlugin WIP * Remove Add annotations menu item from graph context menu * ts ifx * WIP * Tests updates * ts check fix * Fix rebase * Use replace function in angular graph data links --- packages/grafana-data/src/types/ScopedVars.ts | 4 +- packages/grafana-data/src/types/dataLink.ts | 6 +- .../components/ShareModal/ShareLink.test.tsx | 69 +++---- .../dashboard/components/ShareModal/utils.ts | 9 +- .../dashboard/dashgrid/PanelChrome.tsx | 1 - .../dashboard/dashgrid/PanelChromeAngular.tsx | 1 - .../dashgrid/PanelHeader/PanelHeader.tsx | 9 +- .../PanelHeader/PanelHeaderCorner.tsx | 2 +- .../dashboard/state/PanelModel.test.ts | 180 ++++++++++++------ .../features/dashboard/state/PanelModel.ts | 23 ++- .../app/features/explore/utils/links.test.ts | 13 +- public/app/features/explore/utils/links.ts | 7 +- .../panel/panellinks/linkSuppliers.test.ts | 4 +- .../panel/panellinks/linkSuppliers.ts | 33 ++-- .../app/features/panel/panellinks/link_srv.ts | 49 +++-- .../panel/panellinks/specs/link_srv.test.ts | 150 +-------------- .../features/templating/template_srv.test.ts | 150 +++------------ .../app/features/templating/template_srv.ts | 17 -- .../getAllVariableValuesForUrl.test.ts | 104 ++++++++++ .../variables/getAllVariableValuesForUrl.ts | 27 +++ public/app/plugins/panel/graph/graph.ts | 2 +- .../app/plugins/panel/graph3/GraphPanel.tsx | 3 +- .../graph3/plugins/ContextMenuPlugin.tsx | 29 ++- public/test/helpers/initTemplateSrv.ts | 12 ++ scripts/ci-check-strict.sh | 2 +- 25 files changed, 458 insertions(+), 448 deletions(-) create mode 100644 public/app/features/variables/getAllVariableValuesForUrl.test.ts create mode 100644 public/app/features/variables/getAllVariableValuesForUrl.ts create mode 100644 public/test/helpers/initTemplateSrv.ts diff --git a/packages/grafana-data/src/types/ScopedVars.ts b/packages/grafana-data/src/types/ScopedVars.ts index 960aba1599d..b066d3a0a88 100644 --- a/packages/grafana-data/src/types/ScopedVars.ts +++ b/packages/grafana-data/src/types/ScopedVars.ts @@ -4,6 +4,4 @@ export interface ScopedVar { [key: string]: any; } -export interface ScopedVars { - [key: string]: ScopedVar; -} +export interface ScopedVars extends Record {} diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index 22bec524aea..2a7dab4ea8c 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -1,12 +1,12 @@ -import { ScopedVars } from './ScopedVars'; import { DataQuery } from './datasource'; +import { InterpolateFunction } from './panel'; /** * Callback info for DataLink click events */ export interface DataLinkClickEvent { origin: T; - scopedVars?: ScopedVars; + replaceVariables: InterpolateFunction | undefined; e?: any; // mouse|react event } @@ -67,7 +67,7 @@ export interface LinkModel { * TODO: ScopedVars in in GrafanaUI package! */ export interface LinkModelSupplier { - getLinks(scopedVars?: any): Array>; + getLinks(replaceVariables?: InterpolateFunction): Array>; } export enum VariableOrigin { diff --git a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx index 9e2f89e7265..a6e6ab79526 100644 --- a/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx +++ b/public/app/features/dashboard/components/ShareModal/ShareLink.test.tsx @@ -1,7 +1,11 @@ import React from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; +import { setTemplateSrv } from '@grafana/runtime'; import config from 'app/core/config'; import { ShareLink, Props, State } from './ShareLink'; +import { initTemplateSrv } from '../../../../../test/helpers/initTemplateSrv'; +import { variableAdapters } from '../../../variables/adapters'; +import { createQueryVariableAdapter } from '../../../variables/query/adapter'; jest.mock('app/features/dashboard/services/TimeSrv', () => ({ getTimeSrv: () => ({ @@ -11,16 +15,6 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({ }), })); -let fillVariableValuesForUrlMock = (params: any) => {}; - -jest.mock('app/features/templating/template_srv', () => ({ - getTemplateSrv: () => ({ - fillVariableValuesForUrl: (params: any) => { - fillVariableValuesForUrlMock(params); - }, - }), -})); - function mockLocationHref(href: string) { const location = window.location; @@ -98,6 +92,13 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex } describe('ShareModal', () => { + let templateSrv = initTemplateSrv([]); + + beforeAll(() => { + variableAdapters.register(createQueryVariableAdapter()); + setTemplateSrv(templateSrv); + }); + shareLinkScenario('shareUrl with current time range and panel', ctx => { ctx.setup(() => { mockLocationHref('http://server/#!/test'); @@ -174,33 +175,35 @@ describe('ShareModal', () => { expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); }); - it('should include template variables in url', async () => { - mockLocationHref('http://server/#!/test'); - fillVariableValuesForUrlMock = (params: any) => { - params['var-app'] = 'mupp'; - params['var-server'] = 'srv-01'; - }; - ctx.mount(); - ctx.wrapper?.setState({ includeTemplateVars: true }); + describe('template variables', () => { + beforeEach(() => { + templateSrv = initTemplateSrv([ + { type: 'query', name: 'app', current: { value: 'mupp' } }, + { type: 'query', name: 'server', current: { value: 'srv-01' } }, + ]); + setTemplateSrv(templateSrv); + }); - await ctx.wrapper?.instance().buildUrl(); - const state = ctx.wrapper?.state(); - expect(state?.shareUrl).toContain( - 'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01' - ); - }); + it('should include template variables in url', async () => { + mockLocationHref('http://server/#!/test'); + ctx.mount(); + ctx.wrapper?.setState({ includeTemplateVars: true }); - it('should shorten url', () => { - mockLocationHref('http://server/#!/test'); - fillVariableValuesForUrlMock = (params: any) => { - params['var-app'] = 'mupp'; - params['var-server'] = 'srv-01'; - }; - ctx.mount(); - ctx.wrapper?.setState({ includeTemplateVars: true, useShortUrl: true }, async () => { await ctx.wrapper?.instance().buildUrl(); const state = ctx.wrapper?.state(); - expect(state?.shareUrl).toContain(`/goto/${mockUid}`); + expect(state?.shareUrl).toContain( + 'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01' + ); + }); + + it('should shorten url', () => { + mockLocationHref('http://server/#!/test'); + ctx.mount(); + ctx.wrapper?.setState({ includeTemplateVars: true, useShortUrl: true }, async () => { + await ctx.wrapper?.instance().buildUrl(); + const state = ctx.wrapper?.state(); + expect(state?.shareUrl).toContain(`/goto/${mockUid}`); + }); }); }); }); diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index e3828ad975e..33f2add2e58 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -1,8 +1,8 @@ import { config } from '@grafana/runtime'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { getTemplateSrv } from 'app/features/templating/template_srv'; import { createShortLink } from 'app/core/utils/shortLinks'; import { PanelModel, dateTime, urlUtil } from '@grafana/data'; +import { getAllVariableValuesForUrl } from 'app/features/variables/getAllVariableValuesForUrl'; export function buildParams( useCurrentTimeRange: boolean, @@ -10,7 +10,7 @@ export function buildParams( selectedTheme?: string, panel?: PanelModel ) { - const params = urlUtil.getUrlSearchParams(); + let params = urlUtil.getUrlSearchParams(); const range = getTimeSrv().timeRange(); params.from = range.from.valueOf(); @@ -23,7 +23,10 @@ export function buildParams( } if (includeTemplateVars) { - getTemplateSrv().fillVariableValuesForUrl(params); + params = { + ...params, + ...getAllVariableValuesForUrl(), + }; } if (selectedTheme !== 'current') { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index c4961bb60eb..8c7d0453dce 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -332,7 +332,6 @@ export class PanelChrome extends PureComponent { dashboard={dashboard} title={panel.title} description={panel.description} - scopedVars={panel.scopedVars} links={panel.links} error={errorMessage} isEditing={isEditing} diff --git a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx index 5b902032b59..2eb10235bf4 100644 --- a/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChromeAngular.tsx @@ -247,7 +247,6 @@ export class PanelChromeAngularUnconnected extends PureComponent { dashboard={dashboard} title={panel.title} description={panel.description} - scopedVars={panel.scopedVars} angularComponent={angularComponent} links={panel.links} error={errorMessage} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 973d44de5a0..ef601bddc8c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -1,7 +1,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; -import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice, ScopedVars } from '@grafana/data'; -import { AngularComponent, config, getTemplateSrv } from '@grafana/runtime'; +import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice } from '@grafana/data'; +import { AngularComponent, config } from '@grafana/runtime'; import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui'; import { selectors } from '@grafana/e2e-selectors'; @@ -20,7 +20,6 @@ export interface Props { dashboard: DashboardModel; title?: string; description?: string; - scopedVars?: ScopedVars; angularComponent?: AngularComponent | null; links?: DataLink[]; error?: string; @@ -148,9 +147,9 @@ export class PanelHeader extends PureComponent { }; render() { - const { panel, scopedVars, error, isViewing, isEditing, data, alertState } = this.props; + const { panel, error, isViewing, isEditing, data, alertState } = this.props; const { menuItems } = this.state; - const title = getTemplateSrv().replace(panel.title, scopedVars, 'text'); + const title = panel.replaceVariables(panel.title, {}, 'text'); const panelHeaderClass = classNames({ 'panel-header': true, diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index 55d00f911c0..3aadbced5d7 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -46,7 +46,7 @@ export class PanelHeaderCorner extends Component { const markdown = panel.description || ''; const interpolatedMarkdown = getTemplateSrv().replace(markdown, panel.scopedVars); const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown); - const links = this.props.links && this.props.links.getLinks(panel); + const links = this.props.links && this.props.links.getLinks(panel.replaceVariables); return (
diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index 8087ac5bb2f..5164f77532d 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -9,71 +9,42 @@ import { PanelData, FieldColorModeId, FieldColorConfigSettings, + DataLinkBuiltInVars, + VariableModel, } from '@grafana/data'; import { ComponentClass } from 'react'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; - -class TablePanelCtrl {} - -export const mockStandardProperties = () => { - const unit = { - id: 'unit', - path: 'unit', - name: 'Unit', - description: 'Value units', - // @ts-ignore - editor: () => null, - // @ts-ignore - override: () => null, - process: identityOverrideProcessor, - shouldApply: () => true, - }; - - const decimals = { - id: 'decimals', - path: 'decimals', - name: 'Decimals', - description: 'Number of decimal to be shown for a value', - // @ts-ignore - editor: () => null, - // @ts-ignore - override: () => null, - process: identityOverrideProcessor, - shouldApply: () => true, - }; - - const boolean = { - id: 'boolean', - path: 'boolean', - name: 'Boolean', - description: '', - // @ts-ignore - editor: () => null, - // @ts-ignore - override: () => null, - process: identityOverrideProcessor, - shouldApply: () => true, - }; - - const fieldColor = { - id: 'color', - path: 'color', - name: 'color', - description: '', - // @ts-ignore - editor: () => null, - // @ts-ignore - override: () => null, - process: identityOverrideProcessor, - shouldApply: () => true, - }; - - return [unit, decimals, boolean, fieldColor]; -}; +import { setTimeSrv } from '../services/TimeSrv'; +import { TemplateSrv } from '../../templating/template_srv'; +import { setTemplateSrv } from '@grafana/runtime'; +import { variableAdapters } from '../../variables/adapters'; +import { createQueryVariableAdapter } from '../../variables/query/adapter'; standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties()); standardEditorsRegistry.setInit(() => mockStandardProperties()); +setTimeSrv({ + timeRangeForUrl: () => ({ + from: 1607687293000, + to: 1607687293100, + }), +} as any); + +setTemplateSrv( + new TemplateSrv({ + // @ts-ignore + getVariables: () => { + return variablesMock; + }, + // @ts-ignore + getVariableWithName: (name: string) => { + return variablesMock.filter(v => v.name === name)[0]; + }, + }) +); + +variableAdapters.setInit(() => [createQueryVariableAdapter()]); + describe('PanelModel', () => { describe('when creating new panel model', () => { let model: any; @@ -147,7 +118,7 @@ describe('PanelModel', () => { id: 'table', }, (null as unknown) as ComponentClass, // react - TablePanelCtrl // angular + {} // angular ); panelPlugin.setPanelOptions(builder => { @@ -240,6 +211,16 @@ describe('PanelModel', () => { expect(out).toBe('hello AAA'); }); + it('should interpolate $__url_time_range variable', () => { + const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.keepTime}`); + expect(out).toBe('/d/1?from=1607687293000&to=1607687293100'); + }); + + it('should interpolate $__all_variables variable', () => { + const out = model.replaceVariables(`/d/1?$${DataLinkBuiltInVars.includeVars}`); + expect(out).toBe('/d/1?var-test1=val1&var-test2=val2'); + }); + it('should prefer the local variable value', () => { const extra = { aaa: { text: '???', value: 'XXX' } }; const out = model.replaceVariables('hello $aaa and $bbb', extra); @@ -468,3 +449,84 @@ describe('PanelModel', () => { }); }); }); + +export const mockStandardProperties = () => { + const unit = { + id: 'unit', + path: 'unit', + name: 'Unit', + description: 'Value units', + // @ts-ignore + editor: () => null, + // @ts-ignore + override: () => null, + process: identityOverrideProcessor, + shouldApply: () => true, + }; + + const decimals = { + id: 'decimals', + path: 'decimals', + name: 'Decimals', + description: 'Number of decimal to be shown for a value', + // @ts-ignore + editor: () => null, + // @ts-ignore + override: () => null, + process: identityOverrideProcessor, + shouldApply: () => true, + }; + + const boolean = { + id: 'boolean', + path: 'boolean', + name: 'Boolean', + description: '', + // @ts-ignore + editor: () => null, + // @ts-ignore + override: () => null, + process: identityOverrideProcessor, + shouldApply: () => true, + }; + + const fieldColor = { + id: 'color', + path: 'color', + name: 'color', + description: '', + // @ts-ignore + editor: () => null, + // @ts-ignore + override: () => null, + process: identityOverrideProcessor, + shouldApply: () => true, + }; + + return [unit, decimals, boolean, fieldColor]; +}; + +const variablesMock = [ + { + type: 'query', + name: 'test1', + label: 'Test1', + hide: false, + current: { value: 'val1' }, + skipUrlSync: false, + getValueForUrl: function() { + return 'val1'; + }, + } as VariableModel, + { + type: 'query', + name: 'test2', + label: 'Test2', + hide: false, + current: { value: 'val2' }, + skipUrlSync: false, + getValueForUrl: function() { + return 'val2'; + }, + } as VariableModel, +]; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index da160c4ac97..6bbe807f9cf 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -22,11 +22,15 @@ import { EventBusExtended, EventBusSrv, DataFrameDTO, + urlUtil, + DataLinkBuiltInVars, } from '@grafana/data'; import { EDIT_PANEL_ID } from 'app/core/constants'; import config from 'app/core/config'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent, PanelTransformationsChangedEvent } from 'app/types/events'; +import { getTimeSrv } from '../services/TimeSrv'; +import { getAllVariableValuesForUrl } from '../../variables/getAllVariableValuesForUrl'; export interface GridPos { x: number; @@ -496,11 +500,28 @@ export class PanelModel implements DataConfigSource { this.events.publish(new PanelTransformationsChangedEvent()); } - replaceVariables(value: string, extraVars?: ScopedVars, format?: string) { + replaceVariables(value: string, extraVars: ScopedVars | undefined, format?: string | Function) { let vars = this.scopedVars; + if (extraVars) { vars = vars ? { ...vars, ...extraVars } : extraVars; } + const allVariablesParams = getAllVariableValuesForUrl(vars); + const variablesQuery = urlUtil.toUrlParams(allVariablesParams); + const timeRangeUrl = urlUtil.toUrlParams(getTimeSrv().timeRangeForUrl()); + + vars = { + ...vars, + [DataLinkBuiltInVars.keepTime]: { + text: timeRangeUrl, + value: timeRangeUrl, + }, + [DataLinkBuiltInVars.includeVars]: { + text: variablesQuery, + value: variablesQuery, + }, + }; + return getTemplateSrv().replace(value, vars, format); } diff --git a/public/app/features/explore/utils/links.test.ts b/public/app/features/explore/utils/links.test.ts index 57b275dcf1a..ad071d82ce3 100644 --- a/public/app/features/explore/utils/links.test.ts +++ b/public/app/features/explore/utils/links.test.ts @@ -1,5 +1,14 @@ import { getFieldLinksForExplore } from './links'; -import { ArrayVector, DataLink, dateTime, Field, FieldType, LinkModel, ScopedVars, TimeRange } from '@grafana/data'; +import { + ArrayVector, + DataLink, + dateTime, + Field, + FieldType, + InterpolateFunction, + LinkModel, + TimeRange, +} from '@grafana/data'; import { setLinkSrv } from '../../panel/panellinks/link_srv'; describe('getFieldLinksForExplore', () => { @@ -57,7 +66,7 @@ describe('getFieldLinksForExplore', () => { function setup(link: DataLink) { setLinkSrv({ - getDataLinkUIModel(link: DataLink, scopedVars: ScopedVars, origin: any): LinkModel { + getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel { return { href: link.url, title: link.title, diff --git a/public/app/features/explore/utils/links.ts b/public/app/features/explore/utils/links.ts index fc1a7c54842..478000a465f 100644 --- a/public/app/features/explore/utils/links.ts +++ b/public/app/features/explore/utils/links.ts @@ -1,4 +1,4 @@ -import { Field, LinkModel, TimeRange, mapInternalLinkToExplore } from '@grafana/data'; +import { Field, LinkModel, TimeRange, mapInternalLinkToExplore, InterpolateFunction } from '@grafana/data'; import { getLinkSrv } from '../../panel/panellinks/link_srv'; import { getTemplateSrv } from '@grafana/runtime'; import { splitOpen } from '../state/main'; @@ -27,7 +27,10 @@ export const getFieldLinksForExplore = ( return field.config.links ? field.config.links.map(link => { if (!link.internal) { - const linkModel = getLinkSrv().getDataLinkUIModel(link, scopedVars, field); + const replace: InterpolateFunction = (value, vars) => + getTemplateSrv().replace(value, { ...vars, ...scopedVars }); + + const linkModel = getLinkSrv().getDataLinkUIModel(link, replace, field); if (!linkModel.title) { linkModel.title = getTitleFromHref(linkModel.href); } diff --git a/public/app/features/panel/panellinks/linkSuppliers.test.ts b/public/app/features/panel/panellinks/linkSuppliers.test.ts index 4abf5a4a3aa..2445cafd2bd 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.test.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.test.ts @@ -7,6 +7,7 @@ import { getTheme } from '@grafana/ui'; describe('getFieldLinksSupplier', () => { let originalLinkSrv: LinkService; + let templateSrv = new TemplateSrv(); beforeAll(() => { // We do not need more here and TimeSrv is hard to setup fully. const timeSrvMock: TimeSrv = { @@ -18,6 +19,7 @@ describe('getFieldLinksSupplier', () => { } as any; const linkService = new LinkSrv(new TemplateSrv(), timeSrvMock); originalLinkSrv = getLinkSrv(); + setLinkSrv(linkService); }); @@ -108,7 +110,7 @@ describe('getFieldLinksSupplier', () => { }; const supplier = getFieldLinksSupplier(fieldDisp); - const links = supplier?.getLinks({}).map(m => { + const links = supplier?.getLinks(templateSrv.replace.bind(templateSrv)).map(m => { return { title: m.title, href: m.href, diff --git a/public/app/features/panel/panellinks/linkSuppliers.ts b/public/app/features/panel/panellinks/linkSuppliers.ts index 5453b11a7e6..6ffb90fcd23 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.ts @@ -6,6 +6,7 @@ import { formattedValueToString, getFieldDisplayValuesProxy, getTimeField, + InterpolateFunction, Labels, LinkModelSupplier, ScopedVar, @@ -38,11 +39,11 @@ interface DataViewVars { fields?: Record; } -interface DataLinkScopedVars { - __series?: ScopedVar; - __field?: ScopedVar; - __value?: ScopedVar; - __data?: ScopedVar; +interface DataLinkScopedVars extends ScopedVars { + __series: ScopedVar; + __field: ScopedVar; + __value: ScopedVar; + __data: ScopedVar; } /** @@ -55,10 +56,8 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier { - const scopedVars: DataLinkScopedVars = { - ...(existingScopedVars ?? {}), - }; + getLinks: (replaceVariables: InterpolateFunction) => { + const scopedVars: Partial = {}; if (value.view) { const { dataFrame } = value.view; @@ -124,15 +123,23 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier { + const finalVars: ScopedVars = { + ...(scopedVars as ScopedVars), + ...vars, + }; + return replaceVariables(value, finalVars, fmt); + }; + return links.map((link: DataLink) => { - return getLinkSrv().getDataLinkUIModel(link, scopedVars as ScopedVars, value); + return getLinkSrv().getDataLinkUIModel(link, replace, value); }); }, }; }; -export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier | undefined => { - const links = value.links; +export const getPanelLinksSupplier = (panel: PanelModel): LinkModelSupplier | undefined => { + const links = panel.links; if (!links || links.length === 0) { return undefined; @@ -141,7 +148,7 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier { return links.map(link => { - return getLinkSrv().getDataLinkUIModel(link, value.scopedVars, value); + return getLinkSrv().getDataLinkUIModel(link, panel.replaceVariables, panel); }); }, }; diff --git a/public/app/features/panel/panellinks/link_srv.ts b/public/app/features/panel/panellinks/link_srv.ts index 8fe6c348271..a291c115324 100644 --- a/public/app/features/panel/panellinks/link_srv.ts +++ b/public/app/features/panel/panellinks/link_srv.ts @@ -12,6 +12,7 @@ import { Field, FieldType, getFieldDisplayName, + InterpolateFunction, KeyValue, LinkModel, locationUtil, @@ -23,6 +24,7 @@ import { VariableSuggestion, VariableSuggestionsScope, } from '@grafana/data'; +import { getAllVariableValuesForUrl } from '../../variables/getAllVariableValuesForUrl'; const timeRangeVars = [ { @@ -253,7 +255,7 @@ export const getPanelOptionsVariableSuggestions = (plugin: PanelPlugin, data?: D }; export interface LinkService { - getDataLinkUIModel: (link: DataLink, scopedVars: ScopedVars | undefined, origin: T) => LinkModel; + getDataLinkUIModel: (link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T) => LinkModel; getAnchorInfo: (link: any) => any; getLinkUrl: (link: any) => string; } @@ -264,7 +266,7 @@ export class LinkSrv implements LinkService { getLinkUrl(link: any) { let url = locationUtil.assureBaseUrl(this.templateSrv.replace(link.url || '')); - const params: { [key: string]: any } = {}; + let params: { [key: string]: any } = {}; if (link.keepTime) { const range = this.timeSrv.timeRangeForUrl(); @@ -273,7 +275,10 @@ export class LinkSrv implements LinkService { } if (link.includeVars) { - this.templateSrv.fillVariableValuesForUrl(params); + params = { + ...params, + ...getAllVariableValuesForUrl(), + }; } url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params)); @@ -290,16 +295,17 @@ export class LinkSrv implements LinkService { /** * Returns LinkModel which is basically a DataLink with all values interpolated through the templateSrv. */ - getDataLinkUIModel = (link: DataLink, scopedVars: ScopedVars | undefined, origin: T): LinkModel => { - const params: KeyValue = {}; - const timeRangeUrl = urlUtil.toUrlParams(this.timeSrv.timeRangeForUrl()); - + getDataLinkUIModel = ( + link: DataLink, + replaceVariables: InterpolateFunction | undefined, + origin: T + ): LinkModel => { let href = link.url; if (link.onBuildUrl) { href = link.onBuildUrl({ origin, - scopedVars, + replaceVariables, }); } @@ -310,7 +316,7 @@ export class LinkSrv implements LinkService { if (link.onClick) { link.onClick({ origin, - scopedVars, + replaceVariables, e, }); } @@ -319,27 +325,15 @@ export class LinkSrv implements LinkService { const info: LinkModel = { href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')), - title: this.templateSrv.replace(link.title || '', scopedVars), + title: replaceVariables ? replaceVariables(link.title || '') : link.title, target: link.targetBlank ? '_blank' : '_self', origin, onClick, }; - this.templateSrv.fillVariableValuesForUrl(params, scopedVars); - - const variablesQuery = urlUtil.toUrlParams(params); - - info.href = this.templateSrv.replace(info.href, { - ...scopedVars, - [DataLinkBuiltInVars.keepTime]: { - text: timeRangeUrl, - value: timeRangeUrl, - }, - [DataLinkBuiltInVars.includeVars]: { - text: variablesQuery, - value: variablesQuery, - }, - }); + if (replaceVariables) { + info.href = replaceVariables(info.href); + } info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href); @@ -353,7 +347,10 @@ export class LinkSrv implements LinkService { */ getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) { deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel'); - return this.getDataLinkUIModel(link, scopedVars, {}); + const replace: InterpolateFunction = (value, vars, fmt) => + getTemplateSrv().replace(value, { ...scopedVars, ...vars }, fmt); + + return this.getDataLinkUIModel(link, replace, {}); } } diff --git a/public/app/features/panel/panellinks/specs/link_srv.test.ts b/public/app/features/panel/panellinks/specs/link_srv.test.ts index 71fb60ee64d..b3999980eb8 100644 --- a/public/app/features/panel/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panel/panellinks/specs/link_srv.test.ts @@ -1,19 +1,9 @@ -import { advanceTo } from 'jest-date-mock'; -import { - DataLinkBuiltInVars, - FieldType, - locationUtil, - toDataFrame, - VariableModel, - VariableOrigin, -} from '@grafana/data'; +import { FieldType, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data'; import { getDataFrameVars, LinkSrv } from '../link_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { updateConfig } from '../../../../core/config'; -import { variableAdapters } from '../../../variables/adapters'; -import { createQueryVariableAdapter } from '../../../variables/query/adapter'; jest.mock('app/core/core', () => ({ appEvents: { @@ -21,11 +11,6 @@ jest.mock('app/core/core', () => ({ }, })); -const dataPointMock = { - seriesName: 'A-series', - datapoint: [1000000001, 1], -}; - describe('linkSrv', () => { let linkSrv: LinkSrv; @@ -56,116 +41,14 @@ describe('linkSrv', () => { timeSrv.setTime({ from: 'now-1h', to: 'now' }); _dashboard.refresh = false; - const variablesMock = [ - { - type: 'query', - name: 'test1', - label: 'Test1', - hide: false, - current: { value: 'val1' }, - skipUrlSync: false, - getValueForUrl: function() { - return 'val1'; - }, - } as VariableModel, - { - type: 'query', - name: 'test2', - label: 'Test2', - hide: false, - current: { value: 'val2' }, - skipUrlSync: false, - getValueForUrl: function() { - return 'val2'; - }, - } as VariableModel, - ]; - const _templateSrv = new TemplateSrv({ - // @ts-ignore - getVariables: () => { - return variablesMock; - }, - // @ts-ignore - getVariableWithName: (name: string) => { - return variablesMock.filter(v => v.name === name)[0]; - }, - }); - - linkSrv = new LinkSrv(_templateSrv, timeSrv); + linkSrv = new LinkSrv(new TemplateSrv(), timeSrv); } - beforeAll(() => { - variableAdapters.register(createQueryVariableAdapter()); - }); - beforeEach(() => { initLinkSrv(); - advanceTo(1000000000); }); describe('built in variables', () => { - it('should add time range to url if $__url_time_range variable present', () => { - expect( - linkSrv.getDataLinkUIModel( - { - title: 'Any title', - url: `/d/1?$${DataLinkBuiltInVars.keepTime}`, - }, - {}, - {} - ).href - ).toEqual('/d/1?from=now-1h&to=now'); - }); - - it('should add all variables to url if $__all_variables variable present', () => { - expect( - linkSrv.getDataLinkUIModel( - { - title: 'Any title', - url: `/d/1?$${DataLinkBuiltInVars.includeVars}`, - }, - {}, - {} - ).href - ).toEqual('/d/1?var-test1=val1&var-test2=val2'); - }); - - it('should interpolate series name', () => { - expect( - linkSrv.getDataLinkUIModel( - { - title: 'Any title', - url: `/d/1?var-test=$\{${DataLinkBuiltInVars.seriesName}}`, - }, - { - __series: { - value: { - name: 'A-series', - }, - text: 'A-series', - }, - }, - {} - ).href - ).toEqual('/d/1?var-test=A-series'); - }); - it('should interpolate value time', () => { - expect( - linkSrv.getDataLinkUIModel( - { - title: 'Any title', - url: `/d/1?time=$\{${DataLinkBuiltInVars.valueTime}}`, - }, - { - __value: { - value: { time: dataPointMock.datapoint[0] }, - text: 'Value', - }, - }, - {} - ).href - ).toEqual('/d/1?time=1000000001'); - }); it('should not trim white space from data links', () => { expect( linkSrv.getDataLinkUIModel( @@ -173,16 +56,12 @@ describe('linkSrv', () => { title: 'White space', url: 'www.google.com?query=some query', }, - { - __value: { - value: { time: dataPointMock.datapoint[0] }, - text: 'Value', - }, - }, + v => v, {} ).href ).toEqual('www.google.com?query=some query'); }); + it('should remove new lines from data link', () => { expect( linkSrv.getDataLinkUIModel( @@ -190,12 +69,7 @@ describe('linkSrv', () => { title: 'New line', url: 'www.google.com?query=some\nquery', }, - { - __value: { - value: { time: dataPointMock.datapoint[0] }, - text: 'Value', - }, - }, + v => v, {} ).href ).toEqual('www.google.com?query=somequery'); @@ -220,12 +94,7 @@ describe('linkSrv', () => { title: 'Any title', url, }, - { - __value: { - value: { time: dataPointMock.datapoint[0] }, - text: 'Value', - }, - }, + v => v, {} ).href; @@ -261,12 +130,7 @@ describe('linkSrv', () => { title: 'Any title', url, }, - { - __value: { - value: { time: dataPointMock.datapoint[0] }, - text: 'Value', - }, - }, + v => v, {} ).href; diff --git a/public/app/features/templating/template_srv.test.ts b/public/app/features/templating/template_srv.test.ts index aa7a203ad84..74105e7012f 100644 --- a/public/app/features/templating/template_srv.test.ts +++ b/public/app/features/templating/template_srv.test.ts @@ -1,23 +1,12 @@ -import { TemplateSrv } from './template_srv'; -import { convertToStoreState } from 'test/helpers/convertToStoreState'; -import { getTemplateSrvDependencies } from '../../../test/helpers/getTemplateSrvDependencies'; -import { variableAdapters } from '../variables/adapters'; -import { createQueryVariableAdapter } from '../variables/query/adapter'; import { dateTime, TimeRange } from '@grafana/data'; +import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv'; describe('templateSrv', () => { let _templateSrv: any; - function initTemplateSrv(variables: any[], timeRange?: TimeRange) { - const state = convertToStoreState(variables); - - _templateSrv = new TemplateSrv(getTemplateSrvDependencies(state)); - _templateSrv.init(variables, timeRange); - } - describe('init', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); it('should initialize template data', () => { @@ -28,7 +17,7 @@ describe('templateSrv', () => { describe('replace can pass scoped vars', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); it('scoped vars should support objects', () => { @@ -112,7 +101,7 @@ describe('templateSrv', () => { describe('getAdhocFilters', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'datasource', name: 'ds', @@ -141,7 +130,7 @@ describe('templateSrv', () => { describe('replace can pass multi / all format', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'test', @@ -157,7 +146,7 @@ describe('templateSrv', () => { describe('when the globbed variable only has one value', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'test', @@ -205,7 +194,7 @@ describe('templateSrv', () => { describe('variable with all option', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'test', @@ -238,7 +227,7 @@ describe('templateSrv', () => { describe('variable with all option and custom value', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'test', @@ -272,19 +261,19 @@ describe('templateSrv', () => { describe('lucene format', () => { it('should properly escape $test with lucene escape sequences', () => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:$test', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test} with lucene escape sequences', () => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:${test}', {}, 'lucene'); expect(target).toBe('this:value\\/4'); }); it('should properly escape ${test:lucene} with lucene escape sequences', () => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'value/4' } }]); const target = _templateSrv.replace('this:${test:lucene}', {}); expect(target).toBe('this:value\\/4'); }); @@ -292,7 +281,9 @@ describe('templateSrv', () => { describe('html format', () => { it('should encode values html escape sequences', () => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: '' } }]); + _templateSrv = initTemplateSrv([ + { type: 'query', name: 'test', current: { value: '' } }, + ]); const target = _templateSrv.replace('$test', {}, 'html'); expect(target).toBe('<script>alert(asd)</script>'); }); @@ -391,7 +382,7 @@ describe('templateSrv', () => { describe('can check if variable exists', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); it('should return true if $test exists', () => { @@ -432,7 +423,7 @@ describe('templateSrv', () => { describe('can highlight variables in string', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'oogle' } }]); }); it('should insert html', () => { @@ -453,7 +444,7 @@ describe('templateSrv', () => { describe('updateIndex with simple value', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: 'muuuu' } }]); }); it('should set current value and update template data', () => { @@ -462,104 +453,9 @@ describe('templateSrv', () => { }); }); - describe('fillVariableValuesForUrl with multi value', () => { - beforeAll(() => { - variableAdapters.register(createQueryVariableAdapter()); - }); - beforeEach(() => { - initTemplateSrv([ - { - type: 'query', - name: 'test', - current: { value: ['val1', 'val2'] }, - getValueForUrl: function() { - return this.current.value; - }, - }, - ]); - }); - - it('should set multiple url params', () => { - const params: any = {}; - _templateSrv.fillVariableValuesForUrl(params); - expect(params['var-test']).toMatchObject(['val1', 'val2']); - }); - }); - - describe('fillVariableValuesForUrl skip url sync', () => { - beforeEach(() => { - initTemplateSrv([ - { - name: 'test', - skipUrlSync: true, - current: { value: 'value' }, - getValueForUrl: function() { - return this.current.value; - }, - }, - ]); - }); - - it('should not include template variable value in url', () => { - const params: any = {}; - _templateSrv.fillVariableValuesForUrl(params); - expect(params['var-test']).toBe(undefined); - }); - }); - - describe('fillVariableValuesForUrl with multi value with skip url sync', () => { - beforeEach(() => { - initTemplateSrv([ - { - type: 'query', - name: 'test', - skipUrlSync: true, - current: { value: ['val1', 'val2'] }, - getValueForUrl: function() { - return this.current.value; - }, - }, - ]); - }); - - it('should not include template variable value in url', () => { - const params: any = {}; - _templateSrv.fillVariableValuesForUrl(params); - expect(params['var-test']).toBe(undefined); - }); - }); - - describe('fillVariableValuesForUrl with multi value and scopedVars', () => { - beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); - }); - - it('should set scoped value as url params', () => { - const params: any = {}; - _templateSrv.fillVariableValuesForUrl(params, { - test: { value: 'val1' }, - }); - expect(params['var-test']).toBe('val1'); - }); - }); - - describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => { - beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }]); - }); - - it('should not set scoped value as url params', () => { - const params: any = {}; - _templateSrv.fillVariableValuesForUrl(params, { - test: { name: 'test', value: 'val1', skipUrlSync: true }, - }); - expect(params['var-test']).toBe(undefined); - }); - }); - describe('replaceWithText', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'server', @@ -604,7 +500,7 @@ describe('templateSrv', () => { describe('replaceWithText can pass all / multi value', () => { beforeEach(() => { - initTemplateSrv([ + _templateSrv = initTemplateSrv([ { type: 'query', name: 'server', @@ -643,7 +539,7 @@ describe('templateSrv', () => { describe('built in interval variables', () => { beforeEach(() => { - initTemplateSrv([]); + _templateSrv = initTemplateSrv([]); }); it('should replace $__interval_ms with interval milliseconds', () => { @@ -656,7 +552,7 @@ describe('templateSrv', () => { describe('date formating', () => { beforeEach(() => { - initTemplateSrv([], { + _templateSrv = initTemplateSrv([], { from: dateTime(1594671549254), to: dateTime(1595237229747), } as TimeRange); @@ -690,7 +586,7 @@ describe('templateSrv', () => { describe('handle objects gracefully', () => { beforeEach(() => { - initTemplateSrv([{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value: { test: 'A' } } }]); }); it('should not pass object to custom function', () => { @@ -706,7 +602,7 @@ describe('templateSrv', () => { describe('handle objects gracefully and call toString if defined', () => { beforeEach(() => { const value = { test: 'A', toString: () => 'hello' }; - initTemplateSrv([{ type: 'query', name: 'test', current: { value } }]); + _templateSrv = initTemplateSrv([{ type: 'query', name: 'test', current: { value } }]); }); it('should not pass object to custom function', () => { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 5dfc72a360f..812dbde0c2e 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -5,7 +5,6 @@ import { variableRegex } from '../variables/utils'; import { isAdHoc } from '../variables/guard'; import { VariableModel } from '../variables/types'; import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; -import { variableAdapters } from '../variables/adapters'; import { formatRegistry, FormatOptions } from './formatRegistry'; import { ALL_VARIABLE_TEXT } from '../variables/state/types'; @@ -309,22 +308,6 @@ export class TemplateSrv implements BaseTemplateSrv { return this.replace(target, scopedVars, 'text'); } - fillVariableValuesForUrl = (params: any, scopedVars?: ScopedVars) => { - _.each(this.getVariables(), variable => { - if (scopedVars && scopedVars[variable.name] !== void 0) { - if (scopedVars[variable.name].skipUrlSync) { - return; - } - params['var-' + variable.name] = scopedVars[variable.name].value; - } else { - if (variable.skipUrlSync) { - return; - } - params['var-' + variable.name] = variableAdapters.get(variable.type).getValueForUrl(variable); - } - }); - }; - private getVariableAtIndex(name: string) { if (!name) { return; diff --git a/public/app/features/variables/getAllVariableValuesForUrl.test.ts b/public/app/features/variables/getAllVariableValuesForUrl.test.ts new file mode 100644 index 00000000000..ffeec9e633d --- /dev/null +++ b/public/app/features/variables/getAllVariableValuesForUrl.test.ts @@ -0,0 +1,104 @@ +import { setTemplateSrv } from '@grafana/runtime'; +import { variableAdapters } from './adapters'; +import { createQueryVariableAdapter } from './query/adapter'; +import { getAllVariableValuesForUrl } from './getAllVariableValuesForUrl'; +import { initTemplateSrv } from '../../../test/helpers/initTemplateSrv'; + +describe('getAllVariableValuesForUrl', () => { + beforeAll(() => { + variableAdapters.register(createQueryVariableAdapter()); + }); + + describe('with multi value', () => { + beforeEach(() => { + setTemplateSrv( + initTemplateSrv([ + { + type: 'query', + name: 'test', + current: { value: ['val1', 'val2'] }, + getValueForUrl: function() { + return this.current.value; + }, + }, + ]) + ); + }); + + it('should set multiple url params', () => { + let params: any = getAllVariableValuesForUrl(); + expect(params['var-test']).toMatchObject(['val1', 'val2']); + }); + }); + + describe('skip url sync', () => { + beforeEach(() => { + setTemplateSrv( + initTemplateSrv([ + { + name: 'test', + skipUrlSync: true, + current: { value: 'value' }, + getValueForUrl: function() { + return this.current.value; + }, + }, + ]) + ); + }); + + it('should not include template variable value in url', () => { + const params = getAllVariableValuesForUrl(); + expect(params['var-test']).toBe(undefined); + }); + }); + + describe('with multi value with skip url sync', () => { + beforeEach(() => { + setTemplateSrv( + initTemplateSrv([ + { + type: 'query', + name: 'test', + skipUrlSync: true, + current: { value: ['val1', 'val2'] }, + getValueForUrl: function() { + return this.current.value; + }, + }, + ]) + ); + }); + + it('should not include template variable value in url', () => { + const params = getAllVariableValuesForUrl(); + expect(params['var-test']).toBe(undefined); + }); + }); + + describe('fillVariableValuesForUrl with multi value and scopedVars', () => { + beforeEach(() => { + setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }])); + }); + + it('should set scoped value as url params', () => { + const params = getAllVariableValuesForUrl({ + test: { value: 'val1', text: 'val1text' }, + }); + expect(params['var-test']).toBe('val1'); + }); + }); + + describe('fillVariableValuesForUrl with multi value, scopedVars and skip url sync', () => { + beforeEach(() => { + setTemplateSrv(initTemplateSrv([{ type: 'query', name: 'test', current: { value: ['val1', 'val2'] } }])); + }); + + it('should not set scoped value as url params', () => { + const params = getAllVariableValuesForUrl({ + test: { name: 'test', value: 'val1', text: 'val1text', skipUrlSync: true }, + }); + expect(params['var-test']).toBe(undefined); + }); + }); +}); diff --git a/public/app/features/variables/getAllVariableValuesForUrl.ts b/public/app/features/variables/getAllVariableValuesForUrl.ts new file mode 100644 index 00000000000..8abb33b9ffc --- /dev/null +++ b/public/app/features/variables/getAllVariableValuesForUrl.ts @@ -0,0 +1,27 @@ +import { ScopedVars } from '@grafana/data'; +import { getTemplateSrv } from '@grafana/runtime'; +import { variableAdapters } from './adapters'; + +export function getAllVariableValuesForUrl(scopedVars?: ScopedVars) { + const params: Record = {}; + const variables = getTemplateSrv().getVariables(); + + // console.log(variables) + for (let i = 0; i < variables.length; i++) { + const variable = variables[i]; + if (scopedVars && scopedVars[variable.name] !== void 0) { + if (scopedVars[variable.name].skipUrlSync) { + continue; + } + params['var-' + variable.name] = scopedVars[variable.name].value; + } else { + // @ts-ignore + if (variable.skipUrlSync) { + continue; + } + params['var-' + variable.name] = variableAdapters.get(variable.type).getValueForUrl(variable as any); + } + } + + return params; +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 3b858f0208f..f17de21fe19 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -218,7 +218,7 @@ class GraphElement { const dataLinks = [ { - items: linksSupplier.getLinks(this.panel.scopedVars).map(link => { + items: linksSupplier.getLinks(this.panel.replaceVariables).map(link => { return { label: link.title, url: link.href, diff --git a/public/app/plugins/panel/graph3/GraphPanel.tsx b/public/app/plugins/panel/graph3/GraphPanel.tsx index 45a1fc034a6..1e19fa3c437 100644 --- a/public/app/plugins/panel/graph3/GraphPanel.tsx +++ b/public/app/plugins/panel/graph3/GraphPanel.tsx @@ -16,6 +16,7 @@ export const GraphPanel: React.FC = ({ height, options, onChangeTimeRange, + replaceVariables, }) => { return ( = ({ > - + {data.annotations ? : <>} {data.annotations ? : <>} diff --git a/public/app/plugins/panel/graph3/plugins/ContextMenuPlugin.tsx b/public/app/plugins/panel/graph3/plugins/ContextMenuPlugin.tsx index db87accb81b..eb300332748 100644 --- a/public/app/plugins/panel/graph3/plugins/ContextMenuPlugin.tsx +++ b/public/app/plugins/panel/graph3/plugins/ContextMenuPlugin.tsx @@ -9,7 +9,14 @@ import { Portal, usePlotData, } from '@grafana/ui'; -import { DataFrameView, DisplayValue, Field, getDisplayProcessor, getFieldDisplayName } from '@grafana/data'; +import { + DataFrameView, + DisplayValue, + Field, + getDisplayProcessor, + getFieldDisplayName, + InterpolateFunction, +} from '@grafana/data'; import { TimeZone } from '@grafana/data'; import { useClickAway } from 'react-use'; import { getFieldLinksSupplier } from '../../../../features/panel/panellinks/linkSuppliers'; @@ -19,9 +26,15 @@ interface ContextMenuPluginProps { timeZone: TimeZone; onOpen?: () => void; onClose?: () => void; + replaceVariables?: InterpolateFunction; } -export const ContextMenuPlugin: React.FC = ({ onClose, timeZone, defaultItems }) => { +export const ContextMenuPlugin: React.FC = ({ + onClose, + timeZone, + defaultItems, + replaceVariables, +}) => { const [isOpen, setIsOpen] = useState(false); const onClick = useCallback(() => { @@ -37,6 +50,7 @@ export const ContextMenuPlugin: React.FC = ({ onClose, t defaultItems={defaultItems} timeZone={timeZone} selection={{ point, coords }} + replaceVariables={replaceVariables} onClose={() => { clearSelection(); if (onClose) { @@ -59,9 +73,16 @@ interface ContextMenuProps { point: { seriesIdx: number | null; dataIdx: number | null }; coords: { plotCanvas: { x: number; y: number }; viewport: { x: number; y: number } }; }; + replaceVariables?: InterpolateFunction; } -export const ContextMenuView: React.FC = ({ selection, timeZone, defaultItems, ...otherProps }) => { +export const ContextMenuView: React.FC = ({ + selection, + timeZone, + defaultItems, + replaceVariables, + ...otherProps +}) => { const ref = useRef(null); const { data } = usePlotData(); const { seriesIdx, dataIdx } = selection.point; @@ -102,7 +123,7 @@ export const ContextMenuView: React.FC = ({ selection, timeZon if (linksSupplier) { items.push({ - items: linksSupplier.getLinks(/*this.panel.scopedVars*/).map(link => { + items: linksSupplier.getLinks(replaceVariables).map(link => { return { label: link.title, url: link.href, diff --git a/public/test/helpers/initTemplateSrv.ts b/public/test/helpers/initTemplateSrv.ts new file mode 100644 index 00000000000..2af808175eb --- /dev/null +++ b/public/test/helpers/initTemplateSrv.ts @@ -0,0 +1,12 @@ +import { TimeRange } from '@grafana/data'; +import { convertToStoreState } from './convertToStoreState'; +import { TemplateSrv } from '../../app/features/templating/template_srv'; +import { getTemplateSrvDependencies } from './getTemplateSrvDependencies'; + +export function initTemplateSrv(variables: any[], timeRange?: TimeRange) { + const state = convertToStoreState(variables); + const srv = new TemplateSrv(getTemplateSrvDependencies(state)); + srv.init(variables, timeRange); + + return srv; +} diff --git a/scripts/ci-check-strict.sh b/scripts/ci-check-strict.sh index 5cead2b03ad..eeb79b1ba20 100755 --- a/scripts/ci-check-strict.sh +++ b/scripts/ci-check-strict.sh @@ -3,7 +3,7 @@ set -e echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=592 +ERROR_COUNT_LIMIT=584 ERROR_COUNT="$(./node_modules/.bin/tsc --project tsconfig.json --noEmit --strict true | grep -oP 'Found \K(\d+)')" if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then