Commit 683ce693 by Dominik Prokop Committed by GitHub

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
parent 5f4b5281
...@@ -4,6 +4,4 @@ export interface ScopedVar<T = any> { ...@@ -4,6 +4,4 @@ export interface ScopedVar<T = any> {
[key: string]: any; [key: string]: any;
} }
export interface ScopedVars { export interface ScopedVars extends Record<string, ScopedVar> {}
[key: string]: ScopedVar;
}
import { ScopedVars } from './ScopedVars';
import { DataQuery } from './datasource'; import { DataQuery } from './datasource';
import { InterpolateFunction } from './panel';
/** /**
* Callback info for DataLink click events * Callback info for DataLink click events
*/ */
export interface DataLinkClickEvent<T = any> { export interface DataLinkClickEvent<T = any> {
origin: T; origin: T;
scopedVars?: ScopedVars; replaceVariables: InterpolateFunction | undefined;
e?: any; // mouse|react event e?: any; // mouse|react event
} }
...@@ -67,7 +67,7 @@ export interface LinkModel<T = any> { ...@@ -67,7 +67,7 @@ export interface LinkModel<T = any> {
* TODO: ScopedVars in in GrafanaUI package! * TODO: ScopedVars in in GrafanaUI package!
*/ */
export interface LinkModelSupplier<T extends object> { export interface LinkModelSupplier<T extends object> {
getLinks(scopedVars?: any): Array<LinkModel<T>>; getLinks(replaceVariables?: InterpolateFunction): Array<LinkModel<T>>;
} }
export enum VariableOrigin { export enum VariableOrigin {
......
import React from 'react'; import React from 'react';
import { shallow, ShallowWrapper } from 'enzyme'; import { shallow, ShallowWrapper } from 'enzyme';
import { setTemplateSrv } from '@grafana/runtime';
import config from 'app/core/config'; import config from 'app/core/config';
import { ShareLink, Props, State } from './ShareLink'; 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', () => ({ jest.mock('app/features/dashboard/services/TimeSrv', () => ({
getTimeSrv: () => ({ getTimeSrv: () => ({
...@@ -11,16 +15,6 @@ jest.mock('app/features/dashboard/services/TimeSrv', () => ({ ...@@ -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) { function mockLocationHref(href: string) {
const location = window.location; const location = window.location;
...@@ -98,6 +92,13 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex ...@@ -98,6 +92,13 @@ function shareLinkScenario(description: string, scenarioFn: (ctx: ScenarioContex
} }
describe('ShareModal', () => { describe('ShareModal', () => {
let templateSrv = initTemplateSrv([]);
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
setTemplateSrv(templateSrv);
});
shareLinkScenario('shareUrl with current time range and panel', ctx => { shareLinkScenario('shareUrl with current time range and panel', ctx => {
ctx.setup(() => { ctx.setup(() => {
mockLocationHref('http://server/#!/test'); mockLocationHref('http://server/#!/test');
...@@ -174,33 +175,35 @@ describe('ShareModal', () => { ...@@ -174,33 +175,35 @@ describe('ShareModal', () => {
expect(state?.imageUrl).toContain('?from=1000&to=2000&orgId=1&panelId=1&width=1000&height=500&tz=UTC'); 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 () => { describe('template variables', () => {
mockLocationHref('http://server/#!/test'); beforeEach(() => {
fillVariableValuesForUrlMock = (params: any) => { templateSrv = initTemplateSrv([
params['var-app'] = 'mupp'; { type: 'query', name: 'app', current: { value: 'mupp' } },
params['var-server'] = 'srv-01'; { type: 'query', name: 'server', current: { value: 'srv-01' } },
}; ]);
ctx.mount(); setTemplateSrv(templateSrv);
ctx.wrapper?.setState({ includeTemplateVars: true }); });
await ctx.wrapper?.instance().buildUrl(); it('should include template variables in url', async () => {
const state = ctx.wrapper?.state(); mockLocationHref('http://server/#!/test');
expect(state?.shareUrl).toContain( ctx.mount();
'http://server/#!/test?from=1000&to=2000&orgId=1&var-app=mupp&var-server=srv-01' 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(); await ctx.wrapper?.instance().buildUrl();
const state = ctx.wrapper?.state(); 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}`);
});
}); });
}); });
}); });
......
import { config } from '@grafana/runtime'; import { config } from '@grafana/runtime';
import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv } from 'app/features/templating/template_srv';
import { createShortLink } from 'app/core/utils/shortLinks'; import { createShortLink } from 'app/core/utils/shortLinks';
import { PanelModel, dateTime, urlUtil } from '@grafana/data'; import { PanelModel, dateTime, urlUtil } from '@grafana/data';
import { getAllVariableValuesForUrl } from 'app/features/variables/getAllVariableValuesForUrl';
export function buildParams( export function buildParams(
useCurrentTimeRange: boolean, useCurrentTimeRange: boolean,
...@@ -10,7 +10,7 @@ export function buildParams( ...@@ -10,7 +10,7 @@ export function buildParams(
selectedTheme?: string, selectedTheme?: string,
panel?: PanelModel panel?: PanelModel
) { ) {
const params = urlUtil.getUrlSearchParams(); let params = urlUtil.getUrlSearchParams();
const range = getTimeSrv().timeRange(); const range = getTimeSrv().timeRange();
params.from = range.from.valueOf(); params.from = range.from.valueOf();
...@@ -23,7 +23,10 @@ export function buildParams( ...@@ -23,7 +23,10 @@ export function buildParams(
} }
if (includeTemplateVars) { if (includeTemplateVars) {
getTemplateSrv().fillVariableValuesForUrl(params); params = {
...params,
...getAllVariableValuesForUrl(),
};
} }
if (selectedTheme !== 'current') { if (selectedTheme !== 'current') {
......
...@@ -332,7 +332,6 @@ export class PanelChrome extends PureComponent<Props, State> { ...@@ -332,7 +332,6 @@ export class PanelChrome extends PureComponent<Props, State> {
dashboard={dashboard} dashboard={dashboard}
title={panel.title} title={panel.title}
description={panel.description} description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links} links={panel.links}
error={errorMessage} error={errorMessage}
isEditing={isEditing} isEditing={isEditing}
......
...@@ -247,7 +247,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> { ...@@ -247,7 +247,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
dashboard={dashboard} dashboard={dashboard}
title={panel.title} title={panel.title}
description={panel.description} description={panel.description}
scopedVars={panel.scopedVars}
angularComponent={angularComponent} angularComponent={angularComponent}
links={panel.links} links={panel.links}
error={errorMessage} error={errorMessage}
......
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice, ScopedVars } from '@grafana/data'; import { DataLink, LoadingState, PanelData, PanelMenuItem, QueryResultMetaNotice } from '@grafana/data';
import { AngularComponent, config, getTemplateSrv } from '@grafana/runtime'; import { AngularComponent, config } from '@grafana/runtime';
import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui'; import { ClickOutsideWrapper, Icon, IconName, Tooltip, stylesFactory } from '@grafana/ui';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
...@@ -20,7 +20,6 @@ export interface Props { ...@@ -20,7 +20,6 @@ export interface Props {
dashboard: DashboardModel; dashboard: DashboardModel;
title?: string; title?: string;
description?: string; description?: string;
scopedVars?: ScopedVars;
angularComponent?: AngularComponent | null; angularComponent?: AngularComponent | null;
links?: DataLink[]; links?: DataLink[];
error?: string; error?: string;
...@@ -148,9 +147,9 @@ export class PanelHeader extends PureComponent<Props, State> { ...@@ -148,9 +147,9 @@ export class PanelHeader extends PureComponent<Props, State> {
}; };
render() { 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 { menuItems } = this.state;
const title = getTemplateSrv().replace(panel.title, scopedVars, 'text'); const title = panel.replaceVariables(panel.title, {}, 'text');
const panelHeaderClass = classNames({ const panelHeaderClass = classNames({
'panel-header': true, 'panel-header': true,
......
...@@ -46,7 +46,7 @@ export class PanelHeaderCorner extends Component<Props> { ...@@ -46,7 +46,7 @@ export class PanelHeaderCorner extends Component<Props> {
const markdown = panel.description || ''; const markdown = panel.description || '';
const interpolatedMarkdown = getTemplateSrv().replace(markdown, panel.scopedVars); const interpolatedMarkdown = getTemplateSrv().replace(markdown, panel.scopedVars);
const markedInterpolatedMarkdown = renderMarkdown(interpolatedMarkdown); 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 ( return (
<div className="panel-info-content markdown-html"> <div className="panel-info-content markdown-html">
......
...@@ -9,70 +9,41 @@ import { ...@@ -9,70 +9,41 @@ import {
PanelData, PanelData,
FieldColorModeId, FieldColorModeId,
FieldColorConfigSettings, FieldColorConfigSettings,
DataLinkBuiltInVars,
VariableModel,
} from '@grafana/data'; } from '@grafana/data';
import { ComponentClass } from 'react'; import { ComponentClass } from 'react';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
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';
class TablePanelCtrl {} standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties());
standardEditorsRegistry.setInit(() => mockStandardProperties());
export const mockStandardProperties = () => { setTimeSrv({
const unit = { timeRangeForUrl: () => ({
id: 'unit', from: 1607687293000,
path: 'unit', to: 1607687293100,
name: 'Unit', }),
description: 'Value units', } as any);
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
const decimals = { setTemplateSrv(
id: 'decimals', new TemplateSrv({
path: 'decimals',
name: 'Decimals',
description: 'Number of decimal to be shown for a value',
// @ts-ignore // @ts-ignore
editor: () => null, getVariables: () => {
return variablesMock;
},
// @ts-ignore // @ts-ignore
override: () => null, getVariableWithName: (name: string) => {
process: identityOverrideProcessor, return variablesMock.filter(v => v.name === name)[0];
shouldApply: () => true, },
}; })
);
const boolean = { variableAdapters.setInit(() => [createQueryVariableAdapter()]);
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];
};
standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties());
standardEditorsRegistry.setInit(() => mockStandardProperties());
describe('PanelModel', () => { describe('PanelModel', () => {
describe('when creating new panel model', () => { describe('when creating new panel model', () => {
...@@ -147,7 +118,7 @@ describe('PanelModel', () => { ...@@ -147,7 +118,7 @@ describe('PanelModel', () => {
id: 'table', id: 'table',
}, },
(null as unknown) as ComponentClass<PanelProps>, // react (null as unknown) as ComponentClass<PanelProps>, // react
TablePanelCtrl // angular {} // angular
); );
panelPlugin.setPanelOptions(builder => { panelPlugin.setPanelOptions(builder => {
...@@ -240,6 +211,16 @@ describe('PanelModel', () => { ...@@ -240,6 +211,16 @@ describe('PanelModel', () => {
expect(out).toBe('hello AAA'); 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', () => { it('should prefer the local variable value', () => {
const extra = { aaa: { text: '???', value: 'XXX' } }; const extra = { aaa: { text: '???', value: 'XXX' } };
const out = model.replaceVariables('hello $aaa and $bbb', extra); const out = model.replaceVariables('hello $aaa and $bbb', extra);
...@@ -468,3 +449,84 @@ describe('PanelModel', () => { ...@@ -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,
];
...@@ -22,11 +22,15 @@ import { ...@@ -22,11 +22,15 @@ import {
EventBusExtended, EventBusExtended,
EventBusSrv, EventBusSrv,
DataFrameDTO, DataFrameDTO,
urlUtil,
DataLinkBuiltInVars,
} from '@grafana/data'; } from '@grafana/data';
import { EDIT_PANEL_ID } from 'app/core/constants'; import { EDIT_PANEL_ID } from 'app/core/constants';
import config from 'app/core/config'; import config from 'app/core/config';
import { PanelQueryRunner } from '../../query/state/PanelQueryRunner'; import { PanelQueryRunner } from '../../query/state/PanelQueryRunner';
import { PanelOptionsChangedEvent, PanelQueriesChangedEvent, PanelTransformationsChangedEvent } from 'app/types/events'; import { PanelOptionsChangedEvent, PanelQueriesChangedEvent, PanelTransformationsChangedEvent } from 'app/types/events';
import { getTimeSrv } from '../services/TimeSrv';
import { getAllVariableValuesForUrl } from '../../variables/getAllVariableValuesForUrl';
export interface GridPos { export interface GridPos {
x: number; x: number;
...@@ -496,11 +500,28 @@ export class PanelModel implements DataConfigSource { ...@@ -496,11 +500,28 @@ export class PanelModel implements DataConfigSource {
this.events.publish(new PanelTransformationsChangedEvent()); 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; let vars = this.scopedVars;
if (extraVars) { if (extraVars) {
vars = vars ? { ...vars, ...extraVars } : 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); return getTemplateSrv().replace(value, vars, format);
} }
......
import { getFieldLinksForExplore } from './links'; 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'; import { setLinkSrv } from '../../panel/panellinks/link_srv';
describe('getFieldLinksForExplore', () => { describe('getFieldLinksForExplore', () => {
...@@ -57,7 +66,7 @@ describe('getFieldLinksForExplore', () => { ...@@ -57,7 +66,7 @@ describe('getFieldLinksForExplore', () => {
function setup(link: DataLink) { function setup(link: DataLink) {
setLinkSrv({ setLinkSrv({
getDataLinkUIModel(link: DataLink, scopedVars: ScopedVars, origin: any): LinkModel<any> { getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
return { return {
href: link.url, href: link.url,
title: link.title, title: link.title,
......
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 { getLinkSrv } from '../../panel/panellinks/link_srv';
import { getTemplateSrv } from '@grafana/runtime'; import { getTemplateSrv } from '@grafana/runtime';
import { splitOpen } from '../state/main'; import { splitOpen } from '../state/main';
...@@ -27,7 +27,10 @@ export const getFieldLinksForExplore = ( ...@@ -27,7 +27,10 @@ export const getFieldLinksForExplore = (
return field.config.links return field.config.links
? field.config.links.map(link => { ? field.config.links.map(link => {
if (!link.internal) { 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) { if (!linkModel.title) {
linkModel.title = getTitleFromHref(linkModel.href); linkModel.title = getTitleFromHref(linkModel.href);
} }
......
...@@ -7,6 +7,7 @@ import { getTheme } from '@grafana/ui'; ...@@ -7,6 +7,7 @@ import { getTheme } from '@grafana/ui';
describe('getFieldLinksSupplier', () => { describe('getFieldLinksSupplier', () => {
let originalLinkSrv: LinkService; let originalLinkSrv: LinkService;
let templateSrv = new TemplateSrv();
beforeAll(() => { beforeAll(() => {
// We do not need more here and TimeSrv is hard to setup fully. // We do not need more here and TimeSrv is hard to setup fully.
const timeSrvMock: TimeSrv = { const timeSrvMock: TimeSrv = {
...@@ -18,6 +19,7 @@ describe('getFieldLinksSupplier', () => { ...@@ -18,6 +19,7 @@ describe('getFieldLinksSupplier', () => {
} as any; } as any;
const linkService = new LinkSrv(new TemplateSrv(), timeSrvMock); const linkService = new LinkSrv(new TemplateSrv(), timeSrvMock);
originalLinkSrv = getLinkSrv(); originalLinkSrv = getLinkSrv();
setLinkSrv(linkService); setLinkSrv(linkService);
}); });
...@@ -108,7 +110,7 @@ describe('getFieldLinksSupplier', () => { ...@@ -108,7 +110,7 @@ describe('getFieldLinksSupplier', () => {
}; };
const supplier = getFieldLinksSupplier(fieldDisp); const supplier = getFieldLinksSupplier(fieldDisp);
const links = supplier?.getLinks({}).map(m => { const links = supplier?.getLinks(templateSrv.replace.bind(templateSrv)).map(m => {
return { return {
title: m.title, title: m.title,
href: m.href, href: m.href,
......
...@@ -6,6 +6,7 @@ import { ...@@ -6,6 +6,7 @@ import {
formattedValueToString, formattedValueToString,
getFieldDisplayValuesProxy, getFieldDisplayValuesProxy,
getTimeField, getTimeField,
InterpolateFunction,
Labels, Labels,
LinkModelSupplier, LinkModelSupplier,
ScopedVar, ScopedVar,
...@@ -38,11 +39,11 @@ interface DataViewVars { ...@@ -38,11 +39,11 @@ interface DataViewVars {
fields?: Record<string, DisplayValue>; fields?: Record<string, DisplayValue>;
} }
interface DataLinkScopedVars { interface DataLinkScopedVars extends ScopedVars {
__series?: ScopedVar<SeriesVars>; __series: ScopedVar<SeriesVars>;
__field?: ScopedVar<FieldVars>; __field: ScopedVar<FieldVars>;
__value?: ScopedVar<ValueVars>; __value: ScopedVar<ValueVars>;
__data?: ScopedVar<DataViewVars>; __data: ScopedVar<DataViewVars>;
} }
/** /**
...@@ -55,10 +56,8 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi ...@@ -55,10 +56,8 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
} }
return { return {
getLinks: (existingScopedVars?: any) => { getLinks: (replaceVariables: InterpolateFunction) => {
const scopedVars: DataLinkScopedVars = { const scopedVars: Partial<DataLinkScopedVars> = {};
...(existingScopedVars ?? {}),
};
if (value.view) { if (value.view) {
const { dataFrame } = value.view; const { dataFrame } = value.view;
...@@ -124,15 +123,23 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi ...@@ -124,15 +123,23 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
console.log('VALUE', value); console.log('VALUE', value);
} }
const replace: InterpolateFunction = (value: string, vars: ScopedVars | undefined, fmt?: string | Function) => {
const finalVars: ScopedVars = {
...(scopedVars as ScopedVars),
...vars,
};
return replaceVariables(value, finalVars, fmt);
};
return links.map((link: DataLink) => { 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<PanelModel> | undefined => { export const getPanelLinksSupplier = (panel: PanelModel): LinkModelSupplier<PanelModel> | undefined => {
const links = value.links; const links = panel.links;
if (!links || links.length === 0) { if (!links || links.length === 0) {
return undefined; return undefined;
...@@ -141,7 +148,7 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane ...@@ -141,7 +148,7 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane
return { return {
getLinks: () => { getLinks: () => {
return links.map(link => { return links.map(link => {
return getLinkSrv().getDataLinkUIModel(link, value.scopedVars, value); return getLinkSrv().getDataLinkUIModel(link, panel.replaceVariables, panel);
}); });
}, },
}; };
......
...@@ -12,6 +12,7 @@ import { ...@@ -12,6 +12,7 @@ import {
Field, Field,
FieldType, FieldType,
getFieldDisplayName, getFieldDisplayName,
InterpolateFunction,
KeyValue, KeyValue,
LinkModel, LinkModel,
locationUtil, locationUtil,
...@@ -23,6 +24,7 @@ import { ...@@ -23,6 +24,7 @@ import {
VariableSuggestion, VariableSuggestion,
VariableSuggestionsScope, VariableSuggestionsScope,
} from '@grafana/data'; } from '@grafana/data';
import { getAllVariableValuesForUrl } from '../../variables/getAllVariableValuesForUrl';
const timeRangeVars = [ const timeRangeVars = [
{ {
...@@ -253,7 +255,7 @@ export const getPanelOptionsVariableSuggestions = (plugin: PanelPlugin, data?: D ...@@ -253,7 +255,7 @@ export const getPanelOptionsVariableSuggestions = (plugin: PanelPlugin, data?: D
}; };
export interface LinkService { export interface LinkService {
getDataLinkUIModel: <T>(link: DataLink, scopedVars: ScopedVars | undefined, origin: T) => LinkModel<T>; getDataLinkUIModel: <T>(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: T) => LinkModel<T>;
getAnchorInfo: (link: any) => any; getAnchorInfo: (link: any) => any;
getLinkUrl: (link: any) => string; getLinkUrl: (link: any) => string;
} }
...@@ -264,7 +266,7 @@ export class LinkSrv implements LinkService { ...@@ -264,7 +266,7 @@ export class LinkSrv implements LinkService {
getLinkUrl(link: any) { getLinkUrl(link: any) {
let url = locationUtil.assureBaseUrl(this.templateSrv.replace(link.url || '')); let url = locationUtil.assureBaseUrl(this.templateSrv.replace(link.url || ''));
const params: { [key: string]: any } = {}; let params: { [key: string]: any } = {};
if (link.keepTime) { if (link.keepTime) {
const range = this.timeSrv.timeRangeForUrl(); const range = this.timeSrv.timeRangeForUrl();
...@@ -273,7 +275,10 @@ export class LinkSrv implements LinkService { ...@@ -273,7 +275,10 @@ export class LinkSrv implements LinkService {
} }
if (link.includeVars) { if (link.includeVars) {
this.templateSrv.fillVariableValuesForUrl(params); params = {
...params,
...getAllVariableValuesForUrl(),
};
} }
url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params)); url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params));
...@@ -290,16 +295,17 @@ export class LinkSrv implements LinkService { ...@@ -290,16 +295,17 @@ export class LinkSrv implements LinkService {
/** /**
* Returns LinkModel which is basically a DataLink with all values interpolated through the templateSrv. * Returns LinkModel which is basically a DataLink with all values interpolated through the templateSrv.
*/ */
getDataLinkUIModel = <T>(link: DataLink, scopedVars: ScopedVars | undefined, origin: T): LinkModel<T> => { getDataLinkUIModel = <T>(
const params: KeyValue = {}; link: DataLink,
const timeRangeUrl = urlUtil.toUrlParams(this.timeSrv.timeRangeForUrl()); replaceVariables: InterpolateFunction | undefined,
origin: T
): LinkModel<T> => {
let href = link.url; let href = link.url;
if (link.onBuildUrl) { if (link.onBuildUrl) {
href = link.onBuildUrl({ href = link.onBuildUrl({
origin, origin,
scopedVars, replaceVariables,
}); });
} }
...@@ -310,7 +316,7 @@ export class LinkSrv implements LinkService { ...@@ -310,7 +316,7 @@ export class LinkSrv implements LinkService {
if (link.onClick) { if (link.onClick) {
link.onClick({ link.onClick({
origin, origin,
scopedVars, replaceVariables,
e, e,
}); });
} }
...@@ -319,27 +325,15 @@ export class LinkSrv implements LinkService { ...@@ -319,27 +325,15 @@ export class LinkSrv implements LinkService {
const info: LinkModel<T> = { const info: LinkModel<T> = {
href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')), 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', target: link.targetBlank ? '_blank' : '_self',
origin, origin,
onClick, onClick,
}; };
this.templateSrv.fillVariableValuesForUrl(params, scopedVars); if (replaceVariables) {
info.href = replaceVariables(info.href);
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,
},
});
info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href); info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href);
...@@ -353,7 +347,10 @@ export class LinkSrv implements LinkService { ...@@ -353,7 +347,10 @@ export class LinkSrv implements LinkService {
*/ */
getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) { getPanelLinkAnchorInfo(link: DataLink, scopedVars: ScopedVars) {
deprecationWarning('link_srv.ts', 'getPanelLinkAnchorInfo', 'getDataLinkUIModel'); 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, {});
} }
} }
......
import { advanceTo } from 'jest-date-mock'; import { FieldType, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data';
import {
DataLinkBuiltInVars,
FieldType,
locationUtil,
toDataFrame,
VariableModel,
VariableOrigin,
} from '@grafana/data';
import { getDataFrameVars, LinkSrv } from '../link_srv'; import { getDataFrameVars, LinkSrv } from '../link_srv';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { updateConfig } from '../../../../core/config'; import { updateConfig } from '../../../../core/config';
import { variableAdapters } from '../../../variables/adapters';
import { createQueryVariableAdapter } from '../../../variables/query/adapter';
jest.mock('app/core/core', () => ({ jest.mock('app/core/core', () => ({
appEvents: { appEvents: {
...@@ -21,11 +11,6 @@ jest.mock('app/core/core', () => ({ ...@@ -21,11 +11,6 @@ jest.mock('app/core/core', () => ({
}, },
})); }));
const dataPointMock = {
seriesName: 'A-series',
datapoint: [1000000001, 1],
};
describe('linkSrv', () => { describe('linkSrv', () => {
let linkSrv: LinkSrv; let linkSrv: LinkSrv;
...@@ -56,116 +41,14 @@ describe('linkSrv', () => { ...@@ -56,116 +41,14 @@ describe('linkSrv', () => {
timeSrv.setTime({ from: 'now-1h', to: 'now' }); timeSrv.setTime({ from: 'now-1h', to: 'now' });
_dashboard.refresh = false; _dashboard.refresh = false;
const variablesMock = [ linkSrv = new LinkSrv(new TemplateSrv(), timeSrv);
{
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);
} }
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
});
beforeEach(() => { beforeEach(() => {
initLinkSrv(); initLinkSrv();
advanceTo(1000000000);
}); });
describe('built in variables', () => { 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', () => { it('should not trim white space from data links', () => {
expect( expect(
linkSrv.getDataLinkUIModel( linkSrv.getDataLinkUIModel(
...@@ -173,16 +56,12 @@ describe('linkSrv', () => { ...@@ -173,16 +56,12 @@ describe('linkSrv', () => {
title: 'White space', title: 'White space',
url: 'www.google.com?query=some query', url: 'www.google.com?query=some query',
}, },
{ v => v,
__value: {
value: { time: dataPointMock.datapoint[0] },
text: 'Value',
},
},
{} {}
).href ).href
).toEqual('www.google.com?query=some query'); ).toEqual('www.google.com?query=some query');
}); });
it('should remove new lines from data link', () => { it('should remove new lines from data link', () => {
expect( expect(
linkSrv.getDataLinkUIModel( linkSrv.getDataLinkUIModel(
...@@ -190,12 +69,7 @@ describe('linkSrv', () => { ...@@ -190,12 +69,7 @@ describe('linkSrv', () => {
title: 'New line', title: 'New line',
url: 'www.google.com?query=some\nquery', url: 'www.google.com?query=some\nquery',
}, },
{ v => v,
__value: {
value: { time: dataPointMock.datapoint[0] },
text: 'Value',
},
},
{} {}
).href ).href
).toEqual('www.google.com?query=somequery'); ).toEqual('www.google.com?query=somequery');
...@@ -220,12 +94,7 @@ describe('linkSrv', () => { ...@@ -220,12 +94,7 @@ describe('linkSrv', () => {
title: 'Any title', title: 'Any title',
url, url,
}, },
{ v => v,
__value: {
value: { time: dataPointMock.datapoint[0] },
text: 'Value',
},
},
{} {}
).href; ).href;
...@@ -261,12 +130,7 @@ describe('linkSrv', () => { ...@@ -261,12 +130,7 @@ describe('linkSrv', () => {
title: 'Any title', title: 'Any title',
url, url,
}, },
{ v => v,
__value: {
value: { time: dataPointMock.datapoint[0] },
text: 'Value',
},
},
{} {}
).href; ).href;
......
...@@ -5,7 +5,6 @@ import { variableRegex } from '../variables/utils'; ...@@ -5,7 +5,6 @@ import { variableRegex } from '../variables/utils';
import { isAdHoc } from '../variables/guard'; import { isAdHoc } from '../variables/guard';
import { VariableModel } from '../variables/types'; import { VariableModel } from '../variables/types';
import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime';
import { variableAdapters } from '../variables/adapters';
import { formatRegistry, FormatOptions } from './formatRegistry'; import { formatRegistry, FormatOptions } from './formatRegistry';
import { ALL_VARIABLE_TEXT } from '../variables/state/types'; import { ALL_VARIABLE_TEXT } from '../variables/state/types';
...@@ -309,22 +308,6 @@ export class TemplateSrv implements BaseTemplateSrv { ...@@ -309,22 +308,6 @@ export class TemplateSrv implements BaseTemplateSrv {
return this.replace(target, scopedVars, 'text'); 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) { private getVariableAtIndex(name: string) {
if (!name) { if (!name) {
return; return;
......
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);
});
});
});
import { ScopedVars } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { variableAdapters } from './adapters';
export function getAllVariableValuesForUrl(scopedVars?: ScopedVars) {
const params: Record<string, string | string[]> = {};
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;
}
...@@ -218,7 +218,7 @@ class GraphElement { ...@@ -218,7 +218,7 @@ class GraphElement {
const dataLinks = [ const dataLinks = [
{ {
items: linksSupplier.getLinks(this.panel.scopedVars).map<MenuItem>(link => { items: linksSupplier.getLinks(this.panel.replaceVariables).map<MenuItem>(link => {
return { return {
label: link.title, label: link.title,
url: link.href, url: link.href,
......
...@@ -16,6 +16,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({ ...@@ -16,6 +16,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
height, height,
options, options,
onChangeTimeRange, onChangeTimeRange,
replaceVariables,
}) => { }) => {
return ( return (
<GraphNG <GraphNG
...@@ -28,7 +29,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({ ...@@ -28,7 +29,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
> >
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} /> <TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
<ZoomPlugin onZoom={onChangeTimeRange} /> <ZoomPlugin onZoom={onChangeTimeRange} />
<ContextMenuPlugin timeZone={timeZone} /> <ContextMenuPlugin timeZone={timeZone} replaceVariables={replaceVariables} />
{data.annotations ? <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} /> : <></>} {data.annotations ? <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} /> : <></>}
{data.annotations ? <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} /> : <></>} {data.annotations ? <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} /> : <></>}
</GraphNG> </GraphNG>
......
...@@ -9,7 +9,14 @@ import { ...@@ -9,7 +9,14 @@ import {
Portal, Portal,
usePlotData, usePlotData,
} from '@grafana/ui'; } 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 { TimeZone } from '@grafana/data';
import { useClickAway } from 'react-use'; import { useClickAway } from 'react-use';
import { getFieldLinksSupplier } from '../../../../features/panel/panellinks/linkSuppliers'; import { getFieldLinksSupplier } from '../../../../features/panel/panellinks/linkSuppliers';
...@@ -19,9 +26,15 @@ interface ContextMenuPluginProps { ...@@ -19,9 +26,15 @@ interface ContextMenuPluginProps {
timeZone: TimeZone; timeZone: TimeZone;
onOpen?: () => void; onOpen?: () => void;
onClose?: () => void; onClose?: () => void;
replaceVariables?: InterpolateFunction;
} }
export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({ onClose, timeZone, defaultItems }) => { export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
onClose,
timeZone,
defaultItems,
replaceVariables,
}) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const onClick = useCallback(() => { const onClick = useCallback(() => {
...@@ -37,6 +50,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({ onClose, t ...@@ -37,6 +50,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({ onClose, t
defaultItems={defaultItems} defaultItems={defaultItems}
timeZone={timeZone} timeZone={timeZone}
selection={{ point, coords }} selection={{ point, coords }}
replaceVariables={replaceVariables}
onClose={() => { onClose={() => {
clearSelection(); clearSelection();
if (onClose) { if (onClose) {
...@@ -59,9 +73,16 @@ interface ContextMenuProps { ...@@ -59,9 +73,16 @@ interface ContextMenuProps {
point: { seriesIdx: number | null; dataIdx: number | null }; point: { seriesIdx: number | null; dataIdx: number | null };
coords: { plotCanvas: { x: number; y: number }; viewport: { x: number; y: number } }; coords: { plotCanvas: { x: number; y: number }; viewport: { x: number; y: number } };
}; };
replaceVariables?: InterpolateFunction;
} }
export const ContextMenuView: React.FC<ContextMenuProps> = ({ selection, timeZone, defaultItems, ...otherProps }) => { export const ContextMenuView: React.FC<ContextMenuProps> = ({
selection,
timeZone,
defaultItems,
replaceVariables,
...otherProps
}) => {
const ref = useRef(null); const ref = useRef(null);
const { data } = usePlotData(); const { data } = usePlotData();
const { seriesIdx, dataIdx } = selection.point; const { seriesIdx, dataIdx } = selection.point;
...@@ -102,7 +123,7 @@ export const ContextMenuView: React.FC<ContextMenuProps> = ({ selection, timeZon ...@@ -102,7 +123,7 @@ export const ContextMenuView: React.FC<ContextMenuProps> = ({ selection, timeZon
if (linksSupplier) { if (linksSupplier) {
items.push({ items.push({
items: linksSupplier.getLinks(/*this.panel.scopedVars*/).map<MenuItem>(link => { items: linksSupplier.getLinks(replaceVariables).map<MenuItem>(link => {
return { return {
label: link.title, label: link.title,
url: link.href, url: link.href,
......
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;
}
...@@ -3,7 +3,7 @@ set -e ...@@ -3,7 +3,7 @@ set -e
echo -e "Collecting code stats (typescript errors & more)" 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+)')" 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 if [ "$ERROR_COUNT" -gt $ERROR_COUNT_LIMIT ]; then
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment