breadcrumbs.tsx 4.5 KB

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