The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/dashboard/components/SubMenu/DashboardLinksDashboard.tsx

186 lines
5.5 KiB

import { css, cx } from '@emotion/css';
import { forwardRef } from 'react';
import { useAsync } from 'react-use';
import { GrafanaTheme2, ScopedVars } from '@grafana/data';
import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize';
import { selectors } from '@grafana/e2e-selectors';
import { DashboardLink } from '@grafana/schema';
import { Dropdown, Icon, LinkButton, Button, Menu, ScrollContainer, useStyles2 } from '@grafana/ui';
import { ButtonLinkProps } from '@grafana/ui/internal';
import { getBackendSrv } from 'app/core/services/backend_srv';
import { DashboardSearchItem } from 'app/features/search/types';
import { getLinkSrv } from '../../../panel/panellinks/link_srv';
interface Props {
link: DashboardLink;
linkInfo: { title: string; href: string };
dashboardUID: string;
scopedVars?: ScopedVars;
}
interface DashboardLinksMenuProps {
link: DashboardLink;
dashboardUID: string;
}
function DashboardLinksMenu({ dashboardUID, link }: DashboardLinksMenuProps) {
const styles = useStyles2(getStyles);
const resolvedLinks = useResolvedLinks({ dashboardUID, link });
if (!resolvedLinks || resolveLinks.length === 0) {
return null;
}
return (
<Menu>
<div className={styles.dropdown}>
<ScrollContainer maxHeight="inherit">
{resolvedLinks.map((resolvedLink, index) => {
return (
<Menu.Item
url={resolvedLink.url}
target={link.targetBlank ? '_blank' : undefined}
key={`dashlinks-dropdown-item-${resolvedLink.uid}-${index}`}
label={resolvedLink.title}
testId={selectors.components.DashboardLinks.link}
aria-label={`${resolvedLink.title} dashboard`}
/>
);
})}
</ScrollContainer>
</div>
</Menu>
);
}
export const DashboardLinksDashboard = (props: Props) => {
const { link, linkInfo, dashboardUID } = props;
const resolvedLinks = useResolvedLinks(props);
const styles = useStyles2(getStyles);
if (link.asDropdown) {
return (
<Dropdown overlay={<DashboardLinksMenu link={link} dashboardUID={dashboardUID} />}>
<DashboardLinkButton
data-placement="bottom"
data-toggle="dropdown"
aria-controls="dropdown-list"
aria-haspopup="menu"
fill="outline"
variant="secondary"
data-testid={selectors.components.DashboardLinks.dropDown}
>
<Icon aria-hidden name="bars" className={styles.iconMargin} />
<span>{linkInfo.title}</span>
</DashboardLinkButton>
</Dropdown>
);
}
return (
<>
{resolvedLinks.length > 0 &&
resolvedLinks.map((resolvedLink, index) => {
return (
<DashboardLinkButton
key={`dashlinks-list-item-${resolvedLink.uid}-${index}`}
icon="apps"
variant="secondary"
fill="outline"
href={resolvedLink.url}
target={link.targetBlank ? '_blank' : undefined}
rel="noreferrer"
data-testid={selectors.components.DashboardLinks.link}
>
{resolvedLink.title}
</DashboardLinkButton>
);
})}
</>
);
};
const useResolvedLinks = ({ link, dashboardUID }: Pick<Props, 'link' | 'dashboardUID'>): ResolvedLinkDTO[] => {
const { tags } = link;
const result = useAsync(() => searchForTags(tags), [tags]);
if (!result.value) {
return [];
}
return resolveLinks(dashboardUID, link, result.value);
};
interface ResolvedLinkDTO {
uid: string;
url: string;
title: string;
}
export async function searchForTags(
tags: string[],
dependencies: { getBackendSrv: typeof getBackendSrv } = { getBackendSrv }
): Promise<DashboardSearchItem[]> {
const limit = 100;
const searchHits: DashboardSearchItem[] = await dependencies.getBackendSrv().search({ tag: tags, limit });
return searchHits;
}
export function resolveLinks(
dashboardUID: string,
link: DashboardLink,
searchHits: DashboardSearchItem[],
dependencies: { getLinkSrv: typeof getLinkSrv; sanitize: typeof sanitize; sanitizeUrl: typeof sanitizeUrl } = {
getLinkSrv,
sanitize,
sanitizeUrl,
}
): ResolvedLinkDTO[] {
return searchHits
.filter((searchHit) => searchHit.uid !== dashboardUID)
.map((searchHit) => {
const uid = searchHit.uid;
const title = dependencies.sanitize(searchHit.title);
const resolvedLink = dependencies.getLinkSrv().getLinkUrl({ ...link, url: searchHit.url });
const url = dependencies.sanitizeUrl(resolvedLink);
return { uid, title, url };
});
}
function getStyles(theme: GrafanaTheme2) {
return {
iconMargin: css({
marginRight: theme.spacing(0.5),
}),
dropdown: css({
maxWidth: 'max(30vw, 300px)',
maxHeight: '70vh',
}),
button: css({
color: theme.colors.text.primary,
}),
dashButton: css({
fontSize: theme.typography.bodySmall.fontSize,
paddingLeft: theme.spacing(1),
paddingRight: theme.spacing(1),
}),
};
}
export const DashboardLinkButton = forwardRef<unknown, ButtonLinkProps>(({ className, ...otherProps }, ref) => {
const styles = useStyles2(getStyles);
const Component = otherProps.href ? LinkButton : Button;
return (
<Component
{...otherProps}
variant="secondary"
fill="outline"
className={cx(className, styles.dashButton)}
ref={ref as any}
/>
);
});
DashboardLinkButton.displayName = 'DashboardLinkButton';