mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
DashboardLinks: variables are resolved and limits to 100 (#25076)
This commit is contained in:
parent
6a7cbd968f
commit
83d933d010
@ -0,0 +1,114 @@
|
|||||||
|
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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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> {
|
|||||||
|
|
||||||
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> {
|
|||||||
<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> {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
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';
|
|||||||
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 {
|
|||||||
type: DashboardLinkType;
|
type: DashboardLinkType;
|
||||||
url: string;
|
url: string;
|
||||||
asDropdown: boolean;
|
asDropdown: boolean;
|
||||||
tags: [];
|
tags: any[];
|
||||||
searchHits?: [];
|
searchHits?: any[];
|
||||||
targetBlank: boolean;
|
targetBlank: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user