projectCrumb.tsx 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. import type {RouteComponentProps} from 'react-router';
  2. import styled from '@emotion/styled';
  3. import IdBadge from 'sentry/components/idBadge';
  4. import LoadingIndicator from 'sentry/components/loadingIndicator';
  5. import {space} from 'sentry/styles/space';
  6. import type {Organization} from 'sentry/types/organization';
  7. import type {Project} from 'sentry/types/project';
  8. import {browserHistory} from 'sentry/utils/browserHistory';
  9. import recreateRoute from 'sentry/utils/recreateRoute';
  10. import replaceRouterParams from 'sentry/utils/replaceRouterParams';
  11. import useProjects from 'sentry/utils/useProjects';
  12. import withLatestContext from 'sentry/utils/withLatestContext';
  13. import BreadcrumbDropdown from './breadcrumbDropdown';
  14. import findFirstRouteWithoutRouteParam from './findFirstRouteWithoutRouteParam';
  15. import MenuItem from './menuItem';
  16. import {CrumbLink} from '.';
  17. type Props = RouteComponentProps<{projectId?: string}, {}> & {
  18. organization: Organization;
  19. project: Project;
  20. projects: Project[];
  21. };
  22. function ProjectCrumb({
  23. organization: latestOrganization,
  24. project: latestProject,
  25. params,
  26. routes,
  27. route,
  28. ...props
  29. }: Props) {
  30. const {projects} = useProjects();
  31. const handleSelect = (item: {value: string}) => {
  32. // We have to make exceptions for routes like "Project Alerts Rule Edit" or "Client Key Details"
  33. // Since these models are project specific, we need to traverse up a route when switching projects
  34. //
  35. // we manipulate `routes` so that it doesn't include the current project's route
  36. // which, unlike the org version, does not start with a route param
  37. const returnTo = findFirstRouteWithoutRouteParam(
  38. routes.slice(routes.indexOf(route) + 1),
  39. route
  40. );
  41. if (returnTo === undefined) {
  42. return;
  43. }
  44. browserHistory.push(
  45. recreateRoute(returnTo, {routes, params: {...params, projectId: item.value}})
  46. );
  47. };
  48. if (!latestOrganization) {
  49. return null;
  50. }
  51. if (!projects) {
  52. return null;
  53. }
  54. const hasMenu = projects && projects.length > 1;
  55. return (
  56. <BreadcrumbDropdown
  57. hasMenu={hasMenu}
  58. route={route}
  59. name={
  60. <ProjectName>
  61. {!latestProject ? (
  62. <LoadingIndicator mini />
  63. ) : (
  64. <CrumbLink
  65. to={replaceRouterParams('/settings/:orgId/projects/:projectId/', {
  66. orgId: latestOrganization.slug,
  67. projectId: latestProject.slug,
  68. })}
  69. >
  70. <IdBadge project={latestProject} avatarSize={18} disableLink />
  71. </CrumbLink>
  72. )}
  73. </ProjectName>
  74. }
  75. onSelect={handleSelect}
  76. items={projects.map((project, index) => ({
  77. index,
  78. value: project.slug,
  79. label: (
  80. <MenuItem>
  81. <IdBadge
  82. project={project}
  83. avatarProps={{consistentWidth: true}}
  84. avatarSize={18}
  85. disableLink
  86. />
  87. </MenuItem>
  88. ),
  89. }))}
  90. {...props}
  91. />
  92. );
  93. }
  94. export {ProjectCrumb};
  95. export default withLatestContext(ProjectCrumb);
  96. // Set height of crumb because of spinner
  97. const SPINNER_SIZE = '24px';
  98. const ProjectName = styled('div')`
  99. display: flex;
  100. .loading {
  101. width: ${SPINNER_SIZE};
  102. height: ${SPINNER_SIZE};
  103. margin: 0 ${space(0.25)} 0 0;
  104. }
  105. `;