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> {
[key: string]: any;
}
export interface ScopedVars {
[key: string]: ScopedVar;
}
export interface ScopedVars extends Record<string, ScopedVar> {}
import { ScopedVars } from './ScopedVars';
import { DataQuery } from './datasource';
import { InterpolateFunction } from './panel';
/**
* Callback info for DataLink click events
*/
export interface DataLinkClickEvent<T = any> {
origin: T;
scopedVars?: ScopedVars;
replaceVariables: InterpolateFunction | undefined;
e?: any; // mouse|react event
}
......@@ -67,7 +67,7 @@ export interface LinkModel<T = any> {
* TODO: ScopedVars in in GrafanaUI package!
*/
export interface LinkModelSupplier<T extends object> {
getLinks(scopedVars?: any): Array<LinkModel<T>>;
getLinks(replaceVariables?: InterpolateFunction): Array<LinkModel<T>>;
}
export enum VariableOrigin {
......
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}`);
});
});
});
});
......
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') {
......
......@@ -332,7 +332,6 @@ export class PanelChrome extends PureComponent<Props, State> {
dashboard={dashboard}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
links={panel.links}
error={errorMessage}
isEditing={isEditing}
......
......@@ -247,7 +247,6 @@ export class PanelChromeAngularUnconnected extends PureComponent<Props, State> {
dashboard={dashboard}
title={panel.title}
description={panel.description}
scopedVars={panel.scopedVars}
angularComponent={angularComponent}
links={panel.links}
error={errorMessage}
......
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<Props, State> {
};
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,
......
......@@ -46,7 +46,7 @@ export class PanelHeaderCorner extends Component<Props> {
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 (
<div className="panel-info-content markdown-html">
......
......@@ -9,70 +9,41 @@ import {
PanelData,
FieldColorModeId,
FieldColorConfigSettings,
DataLinkBuiltInVars,
VariableModel,
} from '@grafana/data';
import { ComponentClass } from 'react';
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 = () => {
const unit = {
id: 'unit',
path: 'unit',
name: 'Unit',
description: 'Value units',
// @ts-ignore
editor: () => null,
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
setTimeSrv({
timeRangeForUrl: () => ({
from: 1607687293000,
to: 1607687293100,
}),
} as any);
const decimals = {
id: 'decimals',
path: 'decimals',
name: 'Decimals',
description: 'Number of decimal to be shown for a value',
setTemplateSrv(
new TemplateSrv({
// @ts-ignore
editor: () => null,
getVariables: () => {
return variablesMock;
},
// @ts-ignore
override: () => null,
process: identityOverrideProcessor,
shouldApply: () => true,
};
getVariableWithName: (name: string) => {
return variablesMock.filter(v => v.name === name)[0];
},
})
);
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];
};
standardFieldConfigEditorRegistry.setInit(() => mockStandardProperties());
standardEditorsRegistry.setInit(() => mockStandardProperties());
variableAdapters.setInit(() => [createQueryVariableAdapter()]);
describe('PanelModel', () => {
describe('when creating new panel model', () => {
......@@ -147,7 +118,7 @@ describe('PanelModel', () => {
id: 'table',
},
(null as unknown) as ComponentClass<PanelProps>, // 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,
];
......@@ -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);
}
......
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<any> {
getDataLinkUIModel(link: DataLink, replaceVariables: InterpolateFunction | undefined, origin: any): LinkModel<any> {
return {
href: link.url,
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 { 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);
}
......
......@@ -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,
......
......@@ -6,6 +6,7 @@ import {
formattedValueToString,
getFieldDisplayValuesProxy,
getTimeField,
InterpolateFunction,
Labels,
LinkModelSupplier,
ScopedVar,
......@@ -38,11 +39,11 @@ interface DataViewVars {
fields?: Record<string, DisplayValue>;
}
interface DataLinkScopedVars {
__series?: ScopedVar<SeriesVars>;
__field?: ScopedVar<FieldVars>;
__value?: ScopedVar<ValueVars>;
__data?: ScopedVar<DataViewVars>;
interface DataLinkScopedVars extends ScopedVars {
__series: ScopedVar<SeriesVars>;
__field: ScopedVar<FieldVars>;
__value: ScopedVar<ValueVars>;
__data: ScopedVar<DataViewVars>;
}
/**
......@@ -55,10 +56,8 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
}
return {
getLinks: (existingScopedVars?: any) => {
const scopedVars: DataLinkScopedVars = {
...(existingScopedVars ?? {}),
};
getLinks: (replaceVariables: InterpolateFunction) => {
const scopedVars: Partial<DataLinkScopedVars> = {};
if (value.view) {
const { dataFrame } = value.view;
......@@ -124,15 +123,23 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier<Fi
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 getLinkSrv().getDataLinkUIModel(link, scopedVars as ScopedVars, value);
return getLinkSrv().getDataLinkUIModel(link, replace, value);
});
},
};
};
export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<PanelModel> | undefined => {
const links = value.links;
export const getPanelLinksSupplier = (panel: PanelModel): LinkModelSupplier<PanelModel> | undefined => {
const links = panel.links;
if (!links || links.length === 0) {
return undefined;
......@@ -141,7 +148,7 @@ export const getPanelLinksSupplier = (value: PanelModel): LinkModelSupplier<Pane
return {
getLinks: () => {
return links.map(link => {
return getLinkSrv().getDataLinkUIModel(link, value.scopedVars, value);
return getLinkSrv().getDataLinkUIModel(link, panel.replaceVariables, panel);
});
},
};
......
......@@ -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: <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;
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 = <T>(link: DataLink, scopedVars: ScopedVars | undefined, origin: T): LinkModel<T> => {
const params: KeyValue = {};
const timeRangeUrl = urlUtil.toUrlParams(this.timeSrv.timeRangeForUrl());
getDataLinkUIModel = <T>(
link: DataLink,
replaceVariables: InterpolateFunction | undefined,
origin: T
): LinkModel<T> => {
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<T> = {
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, {});
}
}
......
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;
......
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: '<script>alert(asd)</script>' } }]);
_templateSrv = initTemplateSrv([
{ type: 'query', name: 'test', current: { value: '<script>alert(asd)</script>' } },
]);
const target = _templateSrv.replace('$test', {}, 'html');
expect(target).toBe('&lt;script&gt;alert(asd)&lt;/script&gt;');
});
......@@ -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', () => {
......
......@@ -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;
......
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 {
const dataLinks = [
{
items: linksSupplier.getLinks(this.panel.scopedVars).map<MenuItem>(link => {
items: linksSupplier.getLinks(this.panel.replaceVariables).map<MenuItem>(link => {
return {
label: link.title,
url: link.href,
......
......@@ -16,6 +16,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
height,
options,
onChangeTimeRange,
replaceVariables,
}) => {
return (
<GraphNG
......@@ -28,7 +29,7 @@ export const GraphPanel: React.FC<GraphPanelProps> = ({
>
<TooltipPlugin mode={options.tooltipOptions.mode as any} timeZone={timeZone} />
<ZoomPlugin onZoom={onChangeTimeRange} />
<ContextMenuPlugin timeZone={timeZone} />
<ContextMenuPlugin timeZone={timeZone} replaceVariables={replaceVariables} />
{data.annotations ? <ExemplarsPlugin exemplars={data.annotations} timeZone={timeZone} /> : <></>}
{data.annotations ? <AnnotationsPlugin annotations={data.annotations} timeZone={timeZone} /> : <></>}
</GraphNG>
......
......@@ -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<ContextMenuPluginProps> = ({ onClose, timeZone, defaultItems }) => {
export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
onClose,
timeZone,
defaultItems,
replaceVariables,
}) => {
const [isOpen, setIsOpen] = useState(false);
const onClick = useCallback(() => {
......@@ -37,6 +50,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({ 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<ContextMenuProps> = ({ selection, timeZone, defaultItems, ...otherProps }) => {
export const ContextMenuView: React.FC<ContextMenuProps> = ({
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<ContextMenuProps> = ({ selection, timeZon
if (linksSupplier) {
items.push({
items: linksSupplier.getLinks(/*this.panel.scopedVars*/).map<MenuItem>(link => {
items: linksSupplier.getLinks(replaceVariables).map<MenuItem>(link => {
return {
label: link.title,
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
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
......
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