Commit 141202f5 by Ha. Huynh Sam Committed by GitHub

DashboardLinks: Support variable expression in to tooltip - Issue #30409 (#30569)

* add compile variable in tooltip link

* test(link_srv): introduce getAnchorInfo test

* test(link_srv): introduce tests for getLinkUrl

* test(link_srv): refer to anchorInfo.url rather than hardcode expected

Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
parent 24d37bed
...@@ -56,7 +56,7 @@ export const DashboardLinks: FC<Props> = ({ dashboard, links }) => { ...@@ -56,7 +56,7 @@ export const DashboardLinks: FC<Props> = ({ dashboard, links }) => {
return ( return (
<div key={key} className="gf-form" aria-label={selectors.components.DashboardLinks.container}> <div key={key} className="gf-form" aria-label={selectors.components.DashboardLinks.container}>
{link.tooltip ? <Tooltip content={link.tooltip}>{linkElement}</Tooltip> : linkElement} {link.tooltip ? <Tooltip content={linkInfo.tooltip}>{linkElement}</Tooltip> : linkElement}
</div> </div>
); );
})} })}
......
...@@ -286,6 +286,7 @@ export class LinkSrv implements LinkService { ...@@ -286,6 +286,7 @@ export class LinkSrv implements LinkService {
const info: any = {}; const info: any = {};
info.href = this.getLinkUrl(link); info.href = this.getLinkUrl(link);
info.title = this.templateSrv.replace(link.title || ''); info.title = this.templateSrv.replace(link.title || '');
info.tooltip = this.templateSrv.replace(link.tooltip || '');
return info; return info;
} }
......
import { FieldType, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data'; import { FieldType, locationUtil, toDataFrame, VariableOrigin } from '@grafana/data';
import { setTemplateSrv } from '@grafana/runtime';
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 { variableAdapters } from 'app/features/variables/adapters';
import { createQueryVariableAdapter } from 'app/features/variables/query/adapter';
import { updateConfig } from '../../../../core/config'; import { updateConfig } from '../../../../core/config';
import { initTemplateSrv } from '../../../../../test/helpers/initTemplateSrv';
jest.mock('app/core/core', () => ({ jest.mock('app/core/core', () => ({
appEvents: { appEvents: {
...@@ -13,6 +16,7 @@ jest.mock('app/core/core', () => ({ ...@@ -13,6 +16,7 @@ jest.mock('app/core/core', () => ({
describe('linkSrv', () => { describe('linkSrv', () => {
let linkSrv: LinkSrv; let linkSrv: LinkSrv;
let templateSrv: TemplateSrv;
function initLinkSrv() { function initLinkSrv() {
const rootScope = { const rootScope = {
...@@ -40,103 +44,186 @@ describe('linkSrv', () => { ...@@ -40,103 +44,186 @@ describe('linkSrv', () => {
timeSrv.init(_dashboard); timeSrv.init(_dashboard);
timeSrv.setTime({ from: 'now-1h', to: 'now' }); timeSrv.setTime({ from: 'now-1h', to: 'now' });
_dashboard.refresh = false; _dashboard.refresh = false;
templateSrv = initTemplateSrv([
{ type: 'query', name: 'home', current: { value: '127.0.0.1' } },
{ type: 'query', name: 'server1', current: { value: '192.168.0.100' } },
]);
setTemplateSrv(templateSrv);
linkSrv = new LinkSrv(new TemplateSrv(), timeSrv); linkSrv = new LinkSrv(templateSrv, timeSrv);
} }
beforeAll(() => {
variableAdapters.register(createQueryVariableAdapter());
});
beforeEach(() => { beforeEach(() => {
initLinkSrv(); initLinkSrv();
jest.resetAllMocks();
}); });
describe('built in variables', () => { describe('getDataLinkUIModel', () => {
it('should not trim white space from data links', () => { describe('built in variables', () => {
expect( it('should not trim white space from data links', () => {
linkSrv.getDataLinkUIModel( expect(
{ linkSrv.getDataLinkUIModel(
title: 'White space', {
url: 'www.google.com?query=some query', title: 'White space',
}, url: 'www.google.com?query=some query',
(v) => v, },
{} (v) => v,
).href {}
).toEqual('www.google.com?query=some query'); ).href
).toEqual('www.google.com?query=some query');
});
it('should remove new lines from data link', () => {
expect(
linkSrv.getDataLinkUIModel(
{
title: 'New line',
url: 'www.google.com?query=some\nquery',
},
(v) => v,
{}
).href
).toEqual('www.google.com?query=somequery');
});
});
describe('sanitization', () => {
const url = "javascript:alert('broken!);";
it.each`
disableSanitizeHtml | expected
${true} | ${url}
${false} | ${'about:blank'}
`(
"when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
({ disableSanitizeHtml, expected }) => {
updateConfig({
disableSanitizeHtml,
});
const link = linkSrv.getDataLinkUIModel(
{
title: 'Any title',
url,
},
(v) => v,
{}
).href;
expect(link).toBe(expected);
}
);
}); });
it('should remove new lines from data link', () => { describe('Building links with root_url set', () => {
expect( it.each`
linkSrv.getDataLinkUIModel( url | appSubUrl | expected
{ ${'/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
title: 'New line', ${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'}
url: 'www.google.com?query=some\nquery', ${'d/whatever'} | ${'/grafana'} | ${'d/whatever'}
}, ${'/d/XXX'} | ${''} | ${'/d/XXX'}
(v) => v, ${'/grafana/d/XXX'} | ${''} | ${'/grafana/d/XXX'}
{} ${'d/whatever'} | ${''} | ${'d/whatever'}
).href `(
).toEqual('www.google.com?query=somequery'); "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'",
({ url, appSubUrl, expected }) => {
locationUtil.initialize({
getConfig: () => {
return { appSubUrl } as any;
},
// @ts-ignore
buildParamsFromVariables: () => {},
// @ts-ignore
getTimeRangeForUrl: () => {},
});
const link = linkSrv.getDataLinkUIModel(
{
title: 'Any title',
url,
},
(v) => v,
{}
).href;
expect(link).toBe(expected);
}
);
}); });
}); });
describe('sanitization', () => { describe('getAnchorInfo', () => {
const url = "javascript:alert('broken!);"; it('returns variable values for variable names in link.href and link.tooltip', () => {
it.each` jest.spyOn(linkSrv, 'getLinkUrl');
disableSanitizeHtml | expected jest.spyOn(templateSrv, 'replace');
${true} | ${url}
${false} | ${'about:blank'} expect(linkSrv.getLinkUrl).toBeCalledTimes(0);
`( expect(templateSrv.replace).toBeCalledTimes(0);
"when disable disableSanitizeHtml set to '$disableSanitizeHtml' then result should be '$expected'",
({ disableSanitizeHtml, expected }) => { const link = linkSrv.getAnchorInfo({
updateConfig({ type: 'link',
disableSanitizeHtml, icon: 'dashboard',
}); tags: [],
url: '/graph?home=$home',
const link = linkSrv.getDataLinkUIModel( title: 'Visit home',
{ tooltip: 'Visit ${home:raw}',
title: 'Any title', });
url,
}, expect(linkSrv.getLinkUrl).toBeCalledTimes(1);
(v) => v, expect(templateSrv.replace).toBeCalledTimes(3);
{} expect(link).toStrictEqual({ href: '/graph?home=127.0.0.1', title: 'Visit home', tooltip: 'Visit 127.0.0.1' });
).href; });
expect(link).toBe(expected);
}
);
}); });
describe('Building links with root_url set', () => { describe('getLinkUrl', () => {
it.each` it('converts link urls', () => {
url | appSubUrl | expected const linkUrl = linkSrv.getLinkUrl({
${'/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'} url: '/graph',
${'/grafana/d/XXX'} | ${'/grafana'} | ${'/grafana/d/XXX'} });
${'d/whatever'} | ${'/grafana'} | ${'d/whatever'} const linkUrlWithVar = linkSrv.getLinkUrl({
${'/d/XXX'} | ${''} | ${'/d/XXX'} url: '/graph?home=$home',
${'/grafana/d/XXX'} | ${''} | ${'/grafana/d/XXX'} });
${'d/whatever'} | ${''} | ${'d/whatever'}
`( expect(linkUrl).toBe('/graph');
"when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'", expect(linkUrlWithVar).toBe('/graph?home=127.0.0.1');
({ url, appSubUrl, expected }) => { });
locationUtil.initialize({
getConfig: () => { it('appends current dashboard time range if keepTime is true', () => {
return { appSubUrl } as any; const anchorInfoKeepTime = linkSrv.getLinkUrl({
}, keepTime: true,
// @ts-ignore url: '/graph',
buildParamsFromVariables: () => {}, });
// @ts-ignore
getTimeRangeForUrl: () => {}, expect(anchorInfoKeepTime).toBe('/graph?from=now-1h&to=now');
}); });
const link = linkSrv.getDataLinkUIModel( it('adds all variables to the url if includeVars is true', () => {
{ const anchorInfoIncludeVars = linkSrv.getLinkUrl({
title: 'Any title', includeVars: true,
url, url: '/graph',
}, });
(v) => v,
{} expect(anchorInfoIncludeVars).toBe('/graph?var-home=127.0.0.1&var-server1=192.168.0.100');
).href; });
expect(link).toBe(expected); it('respects config disableSanitizeHtml', () => {
} const anchorInfo = {
); url: 'javascript:alert(document.domain)',
};
expect(linkSrv.getLinkUrl(anchorInfo)).toBe('about:blank');
updateConfig({
disableSanitizeHtml: true,
});
expect(linkSrv.getLinkUrl(anchorInfo)).toBe(anchorInfo.url);
});
}); });
}); });
......
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