breadcrumbs.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import * as React from 'react';
  2. import styled from '@emotion/styled';
  3. import {LocationDescriptor} from 'history';
  4. import GlobalSelectionLink from 'app/components/globalSelectionLink';
  5. import Link from 'app/components/links/link';
  6. import {IconChevron} from 'app/icons';
  7. import overflowEllipsis from 'app/styles/overflowEllipsis';
  8. import space from 'app/styles/space';
  9. import {Theme} from 'app/utils/theme';
  10. import BreadcrumbDropdown from 'app/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. * Link of the crumb
  23. */
  24. to?: React.ComponentProps<typeof Link>['to'] | null;
  25. /**
  26. * It will keep the global selection values (projects, environments, time) in the
  27. * querystring when navigating (GlobalSelectionLink)
  28. */
  29. preserveGlobalSelection?: boolean;
  30. /**
  31. * Component will try to come up with unique key, but you can provide your own
  32. * (used when mapping over crumbs)
  33. */
  34. key?: string;
  35. };
  36. export type CrumbDropdown = {
  37. /**
  38. * Name of the crumb
  39. */
  40. label: React.ReactNode;
  41. /**
  42. * Items of the crumb dropdown
  43. */
  44. items: React.ComponentProps<typeof BreadcrumbDropdown>['items'];
  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. } else {
  94. const {label, to, preserveGlobalSelection, 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} preserveGlobalSelection={preserveGlobalSelection}>
  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. })}
  114. </BreadcrumbList>
  115. );
  116. };
  117. const getBreadcrumbListItemStyles = (p: {theme: Theme}) => `
  118. color: ${p.theme.gray300};
  119. ${overflowEllipsis};
  120. width: auto;
  121. &:last-child {
  122. color: ${p.theme.textColor};
  123. }
  124. `;
  125. type BreadcrumbLinkProps = {
  126. to: React.ComponentProps<typeof Link>['to'];
  127. preserveGlobalSelection?: boolean;
  128. children?: React.ReactNode;
  129. };
  130. const BreadcrumbLink = styled(
  131. ({preserveGlobalSelection, to, ...props}: BreadcrumbLinkProps) =>
  132. preserveGlobalSelection ? (
  133. <GlobalSelectionLink to={to as LocationDescriptor} {...props} />
  134. ) : (
  135. <Link to={to} {...props} />
  136. )
  137. )`
  138. ${getBreadcrumbListItemStyles}
  139. &:hover,
  140. &:active {
  141. color: ${p => p.theme.subText};
  142. }
  143. `;
  144. const BreadcrumbItem = styled('span')`
  145. ${getBreadcrumbListItemStyles}
  146. max-width: 400px;
  147. `;
  148. const BreadcrumbDividerIcon = styled(IconChevron)`
  149. color: ${p => p.theme.gray300};
  150. margin: 0 ${space(1)};
  151. flex-shrink: 0;
  152. `;
  153. export default Breadcrumbs;