DashboardLinksDashboard.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import { css, cx } from '@emotion/css';
  2. import React, { useRef, useState, useLayoutEffect } from 'react';
  3. import { useAsync } from 'react-use';
  4. import { sanitize, sanitizeUrl } from '@grafana/data/src/text/sanitize';
  5. import { selectors } from '@grafana/e2e-selectors';
  6. import { Icon, ToolbarButton, Tooltip, useStyles2 } from '@grafana/ui';
  7. import { getBackendSrv } from 'app/core/services/backend_srv';
  8. import { DashboardSearchHit } from 'app/features/search/types';
  9. import { getLinkSrv } from '../../../panel/panellinks/link_srv';
  10. import { DashboardLink } from '../../state/DashboardModel';
  11. interface Props {
  12. link: DashboardLink;
  13. linkInfo: { title: string; href: string };
  14. dashboardId: any;
  15. }
  16. export const DashboardLinksDashboard: React.FC<Props> = (props) => {
  17. const { link, linkInfo } = props;
  18. const listRef = useRef<HTMLUListElement>(null);
  19. const [dropdownCssClass, setDropdownCssClass] = useState('invisible');
  20. const [opened, setOpened] = useState(0);
  21. const resolvedLinks = useResolvedLinks(props, opened);
  22. const buttonStyle = useStyles2(
  23. (theme) =>
  24. css`
  25. color: ${theme.colors.text.primary};
  26. `
  27. );
  28. useLayoutEffect(() => {
  29. setDropdownCssClass(getDropdownLocationCssClass(listRef.current));
  30. }, [resolvedLinks]);
  31. if (link.asDropdown) {
  32. return (
  33. <LinkElement link={link} key="dashlinks-dropdown" data-testid={selectors.components.DashboardLinks.dropDown}>
  34. <>
  35. <ToolbarButton
  36. onClick={() => setOpened(Date.now())}
  37. className={cx('gf-form-label gf-form-label--dashlink', buttonStyle)}
  38. data-placement="bottom"
  39. data-toggle="dropdown"
  40. aria-expanded={!!opened}
  41. aria-controls="dropdown-list"
  42. aria-haspopup="menu"
  43. >
  44. <Icon aria-hidden name="bars" style={{ marginRight: '4px' }} />
  45. <span>{linkInfo.title}</span>
  46. </ToolbarButton>
  47. <ul id="dropdown-list" className={`dropdown-menu ${dropdownCssClass}`} role="menu" ref={listRef}>
  48. {resolvedLinks.length > 0 &&
  49. resolvedLinks.map((resolvedLink, index) => {
  50. return (
  51. <li role="none" key={`dashlinks-dropdown-item-${resolvedLink.id}-${index}`}>
  52. <a
  53. role="menuitem"
  54. href={resolvedLink.url}
  55. target={link.targetBlank ? '_blank' : undefined}
  56. rel="noreferrer"
  57. data-testid={selectors.components.DashboardLinks.link}
  58. aria-label={`${resolvedLink.title} dashboard`}
  59. >
  60. {resolvedLink.title}
  61. </a>
  62. </li>
  63. );
  64. })}
  65. </ul>
  66. </>
  67. </LinkElement>
  68. );
  69. }
  70. return (
  71. <>
  72. {resolvedLinks.length > 0 &&
  73. resolvedLinks.map((resolvedLink, index) => {
  74. return (
  75. <LinkElement
  76. link={link}
  77. key={`dashlinks-list-item-${resolvedLink.id}-${index}`}
  78. data-testid={selectors.components.DashboardLinks.container}
  79. >
  80. <a
  81. className="gf-form-label gf-form-label--dashlink"
  82. href={resolvedLink.url}
  83. target={link.targetBlank ? '_blank' : undefined}
  84. rel="noreferrer"
  85. data-testid={selectors.components.DashboardLinks.link}
  86. aria-label={`${resolvedLink.title} dashboard`}
  87. >
  88. <Icon aria-hidden name="apps" style={{ marginRight: '4px' }} />
  89. <span>{resolvedLink.title}</span>
  90. </a>
  91. </LinkElement>
  92. );
  93. })}
  94. </>
  95. );
  96. };
  97. interface LinkElementProps {
  98. link: DashboardLink;
  99. key: string;
  100. children: JSX.Element;
  101. }
  102. const LinkElement: React.FC<LinkElementProps> = (props) => {
  103. const { link, children, ...rest } = props;
  104. return (
  105. <div {...rest} className="gf-form">
  106. {link.tooltip && <Tooltip content={link.tooltip}>{children}</Tooltip>}
  107. {!link.tooltip && <>{children}</>}
  108. </div>
  109. );
  110. };
  111. const useResolvedLinks = ({ link, dashboardId }: Props, opened: number): ResolvedLinkDTO[] => {
  112. const { tags } = link;
  113. const result = useAsync(() => searchForTags(tags), [tags, opened]);
  114. if (!result.value) {
  115. return [];
  116. }
  117. return resolveLinks(dashboardId, link, result.value);
  118. };
  119. interface ResolvedLinkDTO {
  120. id: any;
  121. url: string;
  122. title: string;
  123. }
  124. export async function searchForTags(
  125. tags: any[],
  126. dependencies: { getBackendSrv: typeof getBackendSrv } = { getBackendSrv }
  127. ): Promise<DashboardSearchHit[]> {
  128. const limit = 100;
  129. const searchHits: DashboardSearchHit[] = await dependencies.getBackendSrv().search({ tag: tags, limit });
  130. return searchHits;
  131. }
  132. export function resolveLinks(
  133. dashboardId: any,
  134. link: DashboardLink,
  135. searchHits: DashboardSearchHit[],
  136. dependencies: { getLinkSrv: typeof getLinkSrv; sanitize: typeof sanitize; sanitizeUrl: typeof sanitizeUrl } = {
  137. getLinkSrv,
  138. sanitize,
  139. sanitizeUrl,
  140. }
  141. ): ResolvedLinkDTO[] {
  142. return searchHits
  143. .filter((searchHit) => searchHit.id !== dashboardId)
  144. .map((searchHit) => {
  145. const id = searchHit.id;
  146. const title = dependencies.sanitize(searchHit.title);
  147. const resolvedLink = dependencies.getLinkSrv().getLinkUrl({ ...link, url: searchHit.url });
  148. const url = dependencies.sanitizeUrl(resolvedLink);
  149. return { id, title, url };
  150. });
  151. }
  152. function getDropdownLocationCssClass(element: HTMLElement | null) {
  153. if (!element) {
  154. return 'invisible';
  155. }
  156. const wrapperPos = element.parentElement!.getBoundingClientRect();
  157. const pos = element.getBoundingClientRect();
  158. if (pos.width === 0) {
  159. return 'invisible';
  160. }
  161. if (wrapperPos.left + pos.width + 10 > window.innerWidth) {
  162. return 'pull-left';
  163. } else {
  164. return 'pull-right';
  165. }
  166. }