projectCrumb.tsx 3.4 KB

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