Commit 83d933d0 by Hugo Häggmark Committed by GitHub

DashboardLinks: variables are resolved and limits to 100 (#25076)

parent 6a7cbd96
import { DashboardLink } from '../../state/DashboardModel';
import { DashboardSearchHit, DashboardSearchItemType } from '../../../search/types';
import { resolveLinks, searchForTags } from './DashboardLinksDashboard';
import { describe, expect } from '../../../../../test/lib/common';
describe('searchForTags', () => {
const setupTestContext = () => {
const tags = ['A', 'B'];
const link: DashboardLink = {
targetBlank: false,
asDropdown: false,
icon: 'some icon',
tags,
title: 'some title',
tooltip: 'some tooltip',
type: 'dashboards',
url: undefined,
};
const backendSrv: any = {
search: jest.fn(args => []),
};
return { link, backendSrv };
};
describe('when called', () => {
it('then tags from link should be used in search and limit should be 100', async () => {
const { link, backendSrv } = setupTestContext();
const results = await searchForTags(link, { getBackendSrv: () => backendSrv });
expect(results.length).toEqual(0);
expect(backendSrv.search).toHaveBeenCalledWith({ tag: ['A', 'B'], limit: 100 });
expect(backendSrv.search).toHaveBeenCalledTimes(1);
});
});
});
describe('resolveLinks', () => {
const setupTestContext = (dashboardId: number, searchHitId: number) => {
const link: DashboardLink = {
targetBlank: false,
asDropdown: false,
icon: 'some icon',
tags: [],
title: 'some title',
tooltip: 'some tooltip',
type: 'dashboards',
url: undefined,
};
const searchHits: DashboardSearchHit[] = [
{
id: searchHitId,
title: 'DashLinks',
url: '/d/6ieouugGk/DashLinks',
isStarred: false,
items: [],
tags: [],
uri: 'db/DashLinks',
type: DashboardSearchItemType.DashDB,
},
];
const linkSrv: any = {
getLinkUrl: jest.fn(args => args.url),
};
const sanitize = jest.fn(args => args);
const sanitizeUrl = jest.fn(args => args);
return { dashboardId, link, searchHits, linkSrv, sanitize, sanitizeUrl };
};
describe('when called', () => {
it('should filter out the calling dashboardId', () => {
const { dashboardId, link, searchHits, linkSrv, sanitize, sanitizeUrl } = setupTestContext(1, 1);
const results = resolveLinks(dashboardId, link, searchHits, { getLinkSrv: () => linkSrv, sanitize, sanitizeUrl });
expect(results.length).toEqual(0);
expect(linkSrv.getLinkUrl).toHaveBeenCalledTimes(0);
expect(sanitize).toHaveBeenCalledTimes(0);
expect(sanitizeUrl).toHaveBeenCalledTimes(0);
});
it('should resolve link url', () => {
const { dashboardId, link, searchHits, linkSrv, sanitize, sanitizeUrl } = setupTestContext(1, 2);
const results = resolveLinks(dashboardId, link, searchHits, { getLinkSrv: () => linkSrv, sanitize, sanitizeUrl });
expect(results.length).toEqual(1);
expect(linkSrv.getLinkUrl).toHaveBeenCalledTimes(1);
expect(linkSrv.getLinkUrl).toHaveBeenCalledWith({ ...link, url: searchHits[0].url });
});
it('should sanitize title', () => {
const { dashboardId, link, searchHits, linkSrv, sanitize, sanitizeUrl } = setupTestContext(1, 2);
const results = resolveLinks(dashboardId, link, searchHits, { getLinkSrv: () => linkSrv, sanitize, sanitizeUrl });
expect(results.length).toEqual(1);
expect(sanitize).toHaveBeenCalledTimes(1);
expect(sanitize).toHaveBeenCalledWith(searchHits[0].title);
});
it('should sanitize url', () => {
const { dashboardId, link, searchHits, linkSrv, sanitize, sanitizeUrl } = setupTestContext(1, 2);
const results = resolveLinks(dashboardId, link, searchHits, { getLinkSrv: () => linkSrv, sanitize, sanitizeUrl });
expect(results.length).toEqual(1);
expect(sanitizeUrl).toHaveBeenCalledTimes(1);
expect(sanitizeUrl).toHaveBeenCalledWith(searchHits[0].url);
});
});
});
...@@ -13,73 +13,53 @@ interface Props { ...@@ -13,73 +13,53 @@ interface Props {
} }
interface State { interface State {
searchHits: DashboardSearchHit[]; resolvedLinks: ResolvedLinkDTO[];
} }
export class DashboardLinksDashboard extends PureComponent<Props, State> { export class DashboardLinksDashboard extends PureComponent<Props, State> {
state = { searchHits: [] as DashboardSearchHit[] }; state: State = { resolvedLinks: [] };
componentDidMount() {
if (!this.props.link.asDropdown) { componentDidUpdate(prevProps: Readonly<Props>) {
this.onDropDownClick(); if (!this.props.link.asDropdown && prevProps.linkInfo !== this.props.linkInfo) {
this.onResolveLinks();
} }
} }
onDropDownClick = () => { onResolveLinks = async () => {
const { dashboardId, link } = this.props; const { dashboardId, link } = this.props;
const limit = 7; const searchHits = await searchForTags(link);
getBackendSrv() const resolvedLinks = resolveLinks(dashboardId, link, searchHits);
.search({ tag: link.tags, limit })
.then((dashboards: DashboardSearchHit[]) => {
const processed = dashboards
.filter(dash => dash.id !== dashboardId)
.map(dash => {
return {
...dash,
url: getLinkSrv().getLinkUrl(dash),
};
});
this.setState({ this.setState({ resolvedLinks });
searchHits: processed,
});
});
}; };
renderElement = (linkElement: JSX.Element) => { renderElement = (linkElement: JSX.Element, key: string) => {
const { link } = this.props; const { link } = this.props;
if (link.tooltip) {
return ( return (
<div className="gf-form"> <div className="gf-form" key={key}>
<Tooltip content={link.tooltip}>{linkElement}</Tooltip>; {link.tooltip && <Tooltip content={link.tooltip}>{linkElement}</Tooltip>}
{!link.tooltip && <>{linkElement}</>}
</div> </div>
); );
} else {
return <div className="gf-form">{linkElement}</div>;
}
}; };
renderList = () => { renderList = () => {
const { link } = this.props; const { link } = this.props;
const { searchHits } = this.state; const { resolvedLinks } = this.state;
return ( return (
<> <>
{searchHits.length > 0 && {resolvedLinks.length > 0 &&
searchHits.map((dashboard: any, index: number) => { resolvedLinks.map((resolvedLink, index) => {
const linkElement = ( const linkElement = (
<a <a className="gf-form-label" href={resolvedLink.url} target={link.targetBlank ? '_blank' : '_self'}>
key={`${dashboard.id}-${index}`}
className="gf-form-label"
href={sanitizeUrl(dashboard.url)}
target={link.targetBlank ? '_blank' : '_self'}
>
<Icon name="apps" style={{ marginRight: '4px' }} /> <Icon name="apps" style={{ marginRight: '4px' }} />
<span>{sanitize(dashboard.title)}</span> <span>{resolvedLink.title}</span>
</a> </a>
); );
return this.renderElement(linkElement); return this.renderElement(linkElement, `dashlinks-list-item-${resolvedLink.id}-${index}`);
})} })}
</> </>
); );
...@@ -87,13 +67,13 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> { ...@@ -87,13 +67,13 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> {
renderDropdown = () => { renderDropdown = () => {
const { link, linkInfo } = this.props; const { link, linkInfo } = this.props;
const { searchHits } = this.state; const { resolvedLinks } = this.state;
const linkElement = ( const linkElement = (
<> <>
<a <a
className="gf-form-label pointer" className="gf-form-label pointer"
onClick={this.onDropDownClick} onClick={this.onResolveLinks}
data-placement="bottom" data-placement="bottom"
data-toggle="dropdown" data-toggle="dropdown"
> >
...@@ -101,12 +81,12 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> { ...@@ -101,12 +81,12 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> {
<span>{linkInfo.title}</span> <span>{linkInfo.title}</span>
</a> </a>
<ul className="dropdown-menu pull-right" role="menu"> <ul className="dropdown-menu pull-right" role="menu">
{searchHits.length > 0 && {resolvedLinks.length > 0 &&
searchHits.map((dashboard: any, index: number) => { resolvedLinks.map((resolvedLink, index) => {
return ( return (
<li key={`${dashboard.id}-${index}`}> <li key={`dashlinks-dropdown-item-${resolvedLink.id}-${index}`}>
<a href={sanitizeUrl(dashboard.url)} target={link.targetBlank ? '_blank' : '_self'}> <a href={resolvedLink.url} target={link.targetBlank ? '_blank' : '_self'}>
{sanitize(dashboard.title)} {resolvedLink.title}
</a> </a>
</li> </li>
); );
...@@ -115,14 +95,52 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> { ...@@ -115,14 +95,52 @@ export class DashboardLinksDashboard extends PureComponent<Props, State> {
</> </>
); );
return this.renderElement(linkElement); return this.renderElement(linkElement, 'dashlinks-dropdown');
}; };
render() { render() {
if (this.props.link.asDropdown) { if (this.props.link.asDropdown) {
return this.renderDropdown(); return this.renderDropdown();
} else { }
return this.renderList(); return this.renderList();
} }
}
interface ResolvedLinkDTO {
id: any;
url: string;
title: string;
}
export async function searchForTags(
link: DashboardLink,
dependencies: { getBackendSrv: typeof getBackendSrv } = { getBackendSrv }
): Promise<DashboardSearchHit[]> {
const limit = 100;
const searchHits: DashboardSearchHit[] = await dependencies.getBackendSrv().search({ tag: link.tags, limit });
return searchHits;
}
export function resolveLinks(
dashboardId: any,
link: DashboardLink,
searchHits: DashboardSearchHit[],
dependencies: { getLinkSrv: typeof getLinkSrv; sanitize: typeof sanitize; sanitizeUrl: typeof sanitizeUrl } = {
getLinkSrv,
sanitize,
sanitizeUrl,
} }
): ResolvedLinkDTO[] {
return searchHits
.filter(searchHit => searchHit.id !== dashboardId)
.map(searchHit => {
const id = searchHit.id;
const title = dependencies.sanitize(searchHit.title);
const resolvedLink = dependencies.getLinkSrv().getLinkUrl({ ...link, url: searchHit.url });
const url = dependencies.sanitizeUrl(resolvedLink);
return { id, title, url };
});
} }
...@@ -12,13 +12,13 @@ import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel'; ...@@ -12,13 +12,13 @@ import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel';
import { DashboardMigrator } from './DashboardMigrator'; import { DashboardMigrator } from './DashboardMigrator';
import { import {
AppEvent, AppEvent,
dateTimeFormat,
dateTimeFormatTimeAgo,
DateTimeInput, DateTimeInput,
PanelEvents, PanelEvents,
TimeRange, TimeRange,
TimeZone, TimeZone,
UrlQueryValue, UrlQueryValue,
dateTimeFormat,
dateTimeFormatTimeAgo,
} from '@grafana/data'; } from '@grafana/data';
import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types'; import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types';
import { getConfig } from '../../../core/config'; import { getConfig } from '../../../core/config';
...@@ -42,8 +42,8 @@ export interface DashboardLink { ...@@ -42,8 +42,8 @@ export interface DashboardLink {
type: DashboardLinkType; type: DashboardLinkType;
url: string; url: string;
asDropdown: boolean; asDropdown: boolean;
tags: []; tags: any[];
searchHits?: []; searchHits?: any[];
targetBlank: boolean; targetBlank: boolean;
} }
......
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