projectCrumb.tsx 3.3 KB

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