index.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import {Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import {logout} from 'sentry/actionCreators/account';
  4. import {Client} from 'sentry/api';
  5. import DemoModeGate from 'sentry/components/acl/demoModeGate';
  6. import Avatar from 'sentry/components/avatar';
  7. import DropdownMenu from 'sentry/components/dropdownMenu';
  8. import Hook from 'sentry/components/hook';
  9. import IdBadge from 'sentry/components/idBadge';
  10. import Link from 'sentry/components/links/link';
  11. import SidebarDropdownMenu from 'sentry/components/sidebar/sidebarDropdownMenu.styled';
  12. import SidebarMenuItem, {menuItemStyles} from 'sentry/components/sidebar/sidebarMenuItem';
  13. import SidebarOrgSummary from 'sentry/components/sidebar/sidebarOrgSummary';
  14. import TextOverflow from 'sentry/components/textOverflow';
  15. import {IconChevron, IconSentry} from 'sentry/icons';
  16. import {t} from 'sentry/locale';
  17. import ConfigStore from 'sentry/stores/configStore';
  18. import space from 'sentry/styles/space';
  19. import {Config, Organization, Project, User} from 'sentry/types';
  20. import withApi from 'sentry/utils/withApi';
  21. import withProjects from 'sentry/utils/withProjects';
  22. import SidebarMenuItemLink from '../sidebarMenuItemLink';
  23. import {CommonSidebarProps} from '../types';
  24. import Divider from './divider.styled';
  25. import SwitchOrganization from './switchOrganization';
  26. // TODO: make org and user optional props
  27. type Props = Pick<CommonSidebarProps, 'orientation' | 'collapsed'> & {
  28. api: Client;
  29. config: Config;
  30. projects: Project[];
  31. user: User;
  32. /**
  33. * Set to true to hide links within the organization
  34. */
  35. hideOrgLinks?: boolean;
  36. org?: Organization;
  37. };
  38. const SidebarDropdown = ({
  39. api,
  40. org,
  41. projects,
  42. orientation,
  43. collapsed,
  44. config,
  45. user,
  46. hideOrgLinks,
  47. }: Props) => {
  48. const handleLogout = async () => {
  49. await logout(api);
  50. window.location.assign('/auth/login/');
  51. };
  52. const hasOrganization = !!org;
  53. const hasUser = !!user;
  54. // It's possible we do not have an org in context (e.g. RouteNotFound)
  55. // Otherwise, we should have the full org
  56. const hasOrgRead = org?.access?.includes('org:read');
  57. const hasMemberRead = org?.access?.includes('member:read');
  58. const hasTeamRead = org?.access?.includes('team:read');
  59. const canCreateOrg = ConfigStore.get('features').has('organizations:create');
  60. // Avatar to use: Organization --> user --> Sentry
  61. const avatar =
  62. hasOrganization || hasUser ? (
  63. <StyledAvatar
  64. collapsed={collapsed}
  65. organization={org}
  66. user={!org ? user : undefined}
  67. size={32}
  68. round={false}
  69. />
  70. ) : (
  71. <SentryLink to="/">
  72. <IconSentry size="32px" />
  73. </SentryLink>
  74. );
  75. return (
  76. <DropdownMenu>
  77. {({isOpen, getRootProps, getActorProps, getMenuProps}) => (
  78. <SidebarDropdownRoot {...getRootProps()}>
  79. <SidebarDropdownActor
  80. type="button"
  81. data-test-id="sidebar-dropdown"
  82. {...getActorProps({})}
  83. >
  84. {avatar}
  85. {!collapsed && orientation !== 'top' && (
  86. <OrgAndUserWrapper>
  87. <OrgOrUserName>
  88. {hasOrganization ? org.name : user.name}{' '}
  89. <StyledIconChevron color="white" size="xs" direction="down" />
  90. </OrgOrUserName>
  91. <UserNameOrEmail>
  92. {hasOrganization ? user.name : user.email}
  93. </UserNameOrEmail>
  94. </OrgAndUserWrapper>
  95. )}
  96. </SidebarDropdownActor>
  97. {isOpen && (
  98. <OrgAndUserMenu {...getMenuProps({})}>
  99. {hasOrganization && (
  100. <Fragment>
  101. <SidebarOrgSummary organization={org} projectCount={projects.length} />
  102. {!hideOrgLinks && (
  103. <Fragment>
  104. {hasOrgRead && (
  105. <SidebarMenuItem to={`/settings/${org.slug}/`}>
  106. {t('Organization settings')}
  107. </SidebarMenuItem>
  108. )}
  109. {hasMemberRead && (
  110. <SidebarMenuItem to={`/settings/${org.slug}/members/`}>
  111. {t('Members')}
  112. </SidebarMenuItem>
  113. )}
  114. {hasTeamRead && (
  115. <SidebarMenuItem to={`/settings/${org.slug}/teams/`}>
  116. {t('Teams')}
  117. </SidebarMenuItem>
  118. )}
  119. <Hook
  120. name="sidebar:organization-dropdown-menu"
  121. organization={org}
  122. />
  123. </Fragment>
  124. )}
  125. {!config.singleOrganization && (
  126. <DemoModeGate>
  127. <SidebarMenuItem>
  128. <SwitchOrganization canCreateOrganization={canCreateOrg} />
  129. </SidebarMenuItem>
  130. </DemoModeGate>
  131. )}
  132. </Fragment>
  133. )}
  134. <DemoModeGate>
  135. {hasOrganization && user && <Divider />}
  136. {!!user && (
  137. <Fragment>
  138. <UserSummary to="/settings/account/details/">
  139. <UserBadgeNoOverflow user={user} avatarSize={32} />
  140. </UserSummary>
  141. <div>
  142. <SidebarMenuItem to="/settings/account/">
  143. {t('User settings')}
  144. </SidebarMenuItem>
  145. <SidebarMenuItem to="/settings/account/api/">
  146. {t('API keys')}
  147. </SidebarMenuItem>
  148. {hasOrganization && (
  149. <Hook
  150. name="sidebar:organization-dropdown-menu-bottom"
  151. organization={org}
  152. />
  153. )}
  154. {user.isSuperuser && (
  155. <SidebarMenuItem to="/manage/">{t('Admin')}</SidebarMenuItem>
  156. )}
  157. <SidebarMenuItem
  158. data-test-id="sidebar-signout"
  159. onClick={handleLogout}
  160. >
  161. {t('Sign out')}
  162. </SidebarMenuItem>
  163. </div>
  164. </Fragment>
  165. )}
  166. </DemoModeGate>
  167. </OrgAndUserMenu>
  168. )}
  169. </SidebarDropdownRoot>
  170. )}
  171. </DropdownMenu>
  172. );
  173. };
  174. export default withApi(withProjects(SidebarDropdown));
  175. const SentryLink = styled(Link)`
  176. color: ${p => p.theme.white};
  177. &:hover {
  178. color: ${p => p.theme.white};
  179. }
  180. `;
  181. const UserSummary = styled(Link)<
  182. Omit<React.ComponentProps<typeof SidebarMenuItemLink>, 'children'>
  183. >`
  184. ${p => menuItemStyles(p)}
  185. padding: 10px 15px;
  186. `;
  187. const UserBadgeNoOverflow = styled(IdBadge)`
  188. overflow: hidden;
  189. `;
  190. const SidebarDropdownRoot = styled('div')`
  191. position: relative;
  192. `;
  193. // So that long org names and user names do not overflow
  194. const OrgAndUserWrapper = styled('div')`
  195. overflow-x: hidden;
  196. text-align: left;
  197. `;
  198. const OrgOrUserName = styled(TextOverflow)`
  199. font-size: ${p => p.theme.fontSizeLarge};
  200. line-height: 1.2;
  201. font-weight: bold;
  202. color: ${p => p.theme.white};
  203. text-shadow: 0 0 6px rgba(255, 255, 255, 0);
  204. transition: 0.15s text-shadow linear;
  205. `;
  206. const UserNameOrEmail = styled(TextOverflow)`
  207. font-size: ${p => p.theme.fontSizeMedium};
  208. line-height: 16px;
  209. transition: 0.15s color linear;
  210. `;
  211. const SidebarDropdownActor = styled('button')`
  212. display: flex;
  213. align-items: flex-start;
  214. cursor: pointer;
  215. border: none;
  216. padding: 0;
  217. background: none;
  218. width: 100%;
  219. &:hover {
  220. ${OrgOrUserName} {
  221. text-shadow: 0 0 6px rgba(255, 255, 255, 0.1);
  222. }
  223. ${UserNameOrEmail} {
  224. color: ${p => p.theme.white};
  225. }
  226. }
  227. `;
  228. const StyledAvatar = styled(Avatar)<{collapsed: boolean}>`
  229. margin: ${space(0.25)} 0;
  230. margin-right: ${p => (p.collapsed ? '0' : space(1.5))};
  231. box-shadow: 0 2px 0 rgba(0, 0, 0, 0.08);
  232. border-radius: 6px; /* Fixes background bleeding on corners */
  233. `;
  234. const OrgAndUserMenu = styled('div')`
  235. ${SidebarDropdownMenu};
  236. top: 42px;
  237. min-width: 180px;
  238. z-index: ${p => p.theme.zIndex.orgAndUserMenu};
  239. `;
  240. const StyledIconChevron = styled(IconChevron)`
  241. margin-left: ${space(0.25)};
  242. `;