breadcrumbs.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  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. const BreadcrumbList = styled('div')`
  11. display: flex;
  12. align-items: center;
  13. padding: ${space(1)} 0;
  14. `;
  15. export type Crumb = {
  16. /**
  17. * Label of the crumb
  18. */
  19. label: React.ReactNode;
  20. /**
  21. * Link of the crumb
  22. */
  23. to?: React.ComponentProps<typeof Link>['to'] | null;
  24. /**
  25. * It will keep the global selection values (projects, environments, time) in the
  26. * querystring when navigating (GlobalSelectionLink)
  27. */
  28. preserveGlobalSelection?: boolean;
  29. /**
  30. * Component will try to come up with unique key, but you can provide your own
  31. * (used when mapping over crumbs)
  32. */
  33. key?: string;
  34. };
  35. type Props = React.ComponentPropsWithoutRef<typeof BreadcrumbList> & {
  36. /**
  37. * Array of crumbs that will be rendered
  38. */
  39. crumbs: Crumb[];
  40. /**
  41. * As a general rule of thumb we don't want the last item to be link as it most likely
  42. * points to the same page we are currently on. This is by default false, so that
  43. * people don't have to check if crumb is last in the array and then manually
  44. * assign `to: null/undefined` when passing props to this component.
  45. */
  46. linkLastItem?: boolean;
  47. };
  48. /**
  49. * Page breadcrumbs used for navigation, not to be confused with sentry's event breadcrumbs
  50. */
  51. const Breadcrumbs = ({crumbs, linkLastItem = false, ...props}: Props) => {
  52. if (crumbs.length === 0) {
  53. return null;
  54. }
  55. if (!linkLastItem) {
  56. crumbs[crumbs.length - 1].to = null;
  57. }
  58. return (
  59. <BreadcrumbList {...props}>
  60. {crumbs.map(({label, to, preserveGlobalSelection, key}, index) => {
  61. const labelKey = typeof label === 'string' ? label : '';
  62. const mapKey =
  63. key ?? typeof to === 'string' ? `${labelKey}${to}` : `${labelKey}${index}`;
  64. return (
  65. <React.Fragment key={mapKey}>
  66. {to ? (
  67. <BreadcrumbLink to={to} preserveGlobalSelection={preserveGlobalSelection}>
  68. {label}
  69. </BreadcrumbLink>
  70. ) : (
  71. <BreadcrumbItem>{label}</BreadcrumbItem>
  72. )}
  73. {index < crumbs.length - 1 && (
  74. <BreadcrumbDividerIcon size="xs" direction="right" />
  75. )}
  76. </React.Fragment>
  77. );
  78. })}
  79. </BreadcrumbList>
  80. );
  81. };
  82. const getBreadcrumbListItemStyles = (p: {theme: Theme}) => `
  83. color: ${p.theme.gray300};
  84. ${overflowEllipsis};
  85. width: auto;
  86. &:last-child {
  87. color: ${p.theme.textColor};
  88. }
  89. `;
  90. type BreadcrumbLinkProps = {
  91. to: React.ComponentProps<typeof Link>['to'];
  92. preserveGlobalSelection?: boolean;
  93. children?: React.ReactNode;
  94. };
  95. const BreadcrumbLink = styled(
  96. ({preserveGlobalSelection, to, ...props}: BreadcrumbLinkProps) =>
  97. preserveGlobalSelection ? (
  98. <GlobalSelectionLink to={to as LocationDescriptor} {...props} />
  99. ) : (
  100. <Link to={to} {...props} />
  101. )
  102. )`
  103. ${getBreadcrumbListItemStyles}
  104. &:hover,
  105. &:active {
  106. color: ${p => p.theme.subText};
  107. }
  108. `;
  109. const BreadcrumbItem = styled('span')`
  110. ${getBreadcrumbListItemStyles}
  111. max-width: 400px;
  112. `;
  113. const BreadcrumbDividerIcon = styled(IconChevron)`
  114. color: ${p => p.theme.gray300};
  115. margin: 0 ${space(1)};
  116. flex-shrink: 0;
  117. `;
  118. export default Breadcrumbs;