breadcrumbs.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {LocationDescriptor} from 'history';
  5. import GlobalSelectionLink from 'sentry/components/globalSelectionLink';
  6. import Link, {LinkProps} from 'sentry/components/links/link';
  7. import {IconChevron} from 'sentry/icons';
  8. import space from 'sentry/styles/space';
  9. import {Theme} from 'sentry/utils/theme';
  10. import BreadcrumbDropdown, {
  11. BreadcrumbDropdownProps,
  12. } from 'sentry/views/settings/components/settingsBreadcrumb/breadcrumbDropdown';
  13. const BreadcrumbList = styled('nav')`
  14. display: flex;
  15. align-items: center;
  16. padding: ${space(1)} 0;
  17. `;
  18. export interface Crumb {
  19. /**
  20. * Label of the crumb
  21. */
  22. label: React.ReactNode;
  23. /**
  24. * Component will try to come up with unique key, but you can provide your own
  25. * (used when mapping over crumbs)
  26. */
  27. key?: string;
  28. /**
  29. * It will keep the page filter values (projects, environments, time) in the
  30. * querystring when navigating (GlobalSelectionLink)
  31. */
  32. preservePageFilters?: boolean;
  33. /**
  34. * Link of the crumb
  35. */
  36. to?: LinkProps['to'] | null;
  37. }
  38. export interface CrumbDropdown {
  39. /**
  40. * Items of the crumb dropdown
  41. */
  42. items: BreadcrumbDropdownProps['items'];
  43. /**
  44. * Name of the crumb
  45. */
  46. label: React.ReactNode;
  47. /**
  48. * Callback function for when an item is selected
  49. */
  50. onSelect: BreadcrumbDropdownProps['onSelect'];
  51. }
  52. interface Props extends React.HTMLAttributes<HTMLDivElement> {
  53. /**
  54. * Array of crumbs that will be rendered
  55. */
  56. crumbs: (Crumb | CrumbDropdown)[];
  57. /**
  58. * As a general rule of thumb we don't want the last item to be link as it most likely
  59. * points to the same page we are currently on. This is by default false, so that
  60. * people don't have to check if crumb is last in the array and then manually
  61. * assign `to: null/undefined` when passing props to this component.
  62. */
  63. linkLastItem?: boolean;
  64. }
  65. function isCrumbDropdown(crumb: Crumb | CrumbDropdown): crumb is CrumbDropdown {
  66. return (crumb as CrumbDropdown).items !== undefined;
  67. }
  68. /**
  69. * Page breadcrumbs used for navigation, not to be confused with sentry's event breadcrumbs
  70. */
  71. const Breadcrumbs = ({crumbs, linkLastItem = false, ...props}: Props) => {
  72. if (crumbs.length === 0) {
  73. return null;
  74. }
  75. if (!linkLastItem) {
  76. const lastCrumb = crumbs[crumbs.length - 1];
  77. if (!isCrumbDropdown(lastCrumb)) {
  78. lastCrumb.to = null;
  79. }
  80. }
  81. return (
  82. <BreadcrumbList {...props}>
  83. {crumbs.map((crumb, index) => {
  84. if (isCrumbDropdown(crumb)) {
  85. const {label, ...crumbProps} = crumb;
  86. return (
  87. <BreadcrumbDropdown
  88. key={index}
  89. isLast={index >= crumbs.length - 1}
  90. route={{}}
  91. name={label}
  92. {...crumbProps}
  93. />
  94. );
  95. }
  96. const {label, to, preservePageFilters, key} = crumb;
  97. const labelKey = typeof label === 'string' ? label : '';
  98. const mapKey =
  99. key ?? typeof to === 'string' ? `${labelKey}${to}` : `${labelKey}${index}`;
  100. return (
  101. <Fragment key={mapKey}>
  102. {to ? (
  103. <BreadcrumbLink
  104. to={to}
  105. preservePageFilters={preservePageFilters}
  106. data-test-id="breadcrumb-link"
  107. >
  108. {label}
  109. </BreadcrumbLink>
  110. ) : (
  111. <BreadcrumbItem>{label}</BreadcrumbItem>
  112. )}
  113. {index < crumbs.length - 1 && (
  114. <BreadcrumbDividerIcon size="xs" direction="right" />
  115. )}
  116. </Fragment>
  117. );
  118. })}
  119. </BreadcrumbList>
  120. );
  121. };
  122. const getBreadcrumbListItemStyles = (p: {theme: Theme}) => css`
  123. ${p.theme.overflowEllipsis}
  124. font-size: ${p.theme.fontSizeLarge};
  125. color: ${p.theme.gray300};
  126. width: auto;
  127. &:last-child {
  128. color: ${p.theme.textColor};
  129. }
  130. `;
  131. interface BreadcrumbLinkProps {
  132. to: LinkProps['to'];
  133. children?: React.ReactNode;
  134. preservePageFilters?: boolean;
  135. }
  136. const BreadcrumbLink = styled(
  137. ({preservePageFilters, to, ...props}: BreadcrumbLinkProps) =>
  138. preservePageFilters ? (
  139. <GlobalSelectionLink to={to as LocationDescriptor} {...props} />
  140. ) : (
  141. <Link to={to} {...props} />
  142. )
  143. )`
  144. ${getBreadcrumbListItemStyles}
  145. &:hover,
  146. &:active {
  147. color: ${p => p.theme.subText};
  148. }
  149. `;
  150. const BreadcrumbItem = styled('span')`
  151. ${getBreadcrumbListItemStyles}
  152. max-width: 400px;
  153. `;
  154. const BreadcrumbDividerIcon = styled(IconChevron)`
  155. color: ${p => p.theme.gray300};
  156. margin: 0 ${space(1)};
  157. flex-shrink: 0;
  158. `;
  159. export default Breadcrumbs;