projectCrumb.tsx 3.3 KB

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