breadcrumbs.tsx 4.5 KB

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