index.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532
  1. import {Fragment, useEffect} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences';
  6. import Feature from 'sentry/components/acl/feature';
  7. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  8. import HookOrDefault from 'sentry/components/hookOrDefault';
  9. import {
  10. IconChevron,
  11. IconDashboard,
  12. IconIssues,
  13. IconLab,
  14. IconLightning,
  15. IconList,
  16. IconPlay,
  17. IconProject,
  18. IconReleases,
  19. IconSettings,
  20. IconSiren,
  21. IconSpan,
  22. IconStats,
  23. IconSupport,
  24. IconTelescope,
  25. } from 'sentry/icons';
  26. import {t} from 'sentry/locale';
  27. import ConfigStore from 'sentry/stores/configStore';
  28. import HookStore from 'sentry/stores/hookStore';
  29. import PreferencesStore from 'sentry/stores/preferencesStore';
  30. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  31. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  32. import space from 'sentry/styles/space';
  33. import {Organization} from 'sentry/types';
  34. import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
  35. import theme from 'sentry/utils/theme';
  36. import useMedia from 'sentry/utils/useMedia';
  37. import Broadcasts from './broadcasts';
  38. import SidebarHelp from './help';
  39. import OnboardingStatus from './onboardingStatus';
  40. import ServiceIncidents from './serviceIncidents';
  41. import SidebarDropdown from './sidebarDropdown';
  42. import SidebarItem from './sidebarItem';
  43. import {SidebarOrientation, SidebarPanelKey} from './types';
  44. const SidebarOverride = HookOrDefault({
  45. hookName: 'sidebar:item-override',
  46. defaultComponent: ({children}) => <Fragment>{children({})}</Fragment>,
  47. });
  48. type Props = {
  49. location?: Location;
  50. organization?: Organization;
  51. };
  52. function togglePanel(panel: SidebarPanelKey) {
  53. SidebarPanelStore.togglePanel(panel);
  54. }
  55. function hidePanel() {
  56. SidebarPanelStore.hidePanel();
  57. }
  58. function Sidebar({location, organization}: Props) {
  59. const config = useLegacyStore(ConfigStore);
  60. const preferences = useLegacyStore(PreferencesStore);
  61. const activePanel = useLegacyStore(SidebarPanelStore);
  62. const collapsed = !!preferences.collapsed;
  63. const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  64. const toggleCollapse = () => {
  65. const action = collapsed ? showSidebar : hideSidebar;
  66. action();
  67. };
  68. const bcl = document.body.classList;
  69. // Close panel on any navigation
  70. useEffect(() => void hidePanel(), [location?.pathname]);
  71. // Add classname to body
  72. useEffect(() => {
  73. bcl.add('body-sidebar');
  74. return () => bcl.remove('body-sidebar');
  75. }, [bcl]);
  76. // Add sidebar collapse classname to body
  77. useEffect(() => {
  78. if (collapsed) {
  79. bcl.add('collapsed');
  80. } else {
  81. bcl.remove('collapsed');
  82. }
  83. return () => bcl.remove('collapsed');
  84. }, [collapsed, bcl]);
  85. // Trigger panels depending on the location hash
  86. useEffect(() => {
  87. if (location?.hash === '#welcome') {
  88. togglePanel(SidebarPanelKey.OnboardingWizard);
  89. }
  90. }, [location?.hash]);
  91. const hasPanel = !!activePanel;
  92. const hasOrganization = !!organization;
  93. const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
  94. const sidebarItemProps = {
  95. orientation,
  96. collapsed,
  97. hasPanel,
  98. organization,
  99. };
  100. const projects = hasOrganization && (
  101. <SidebarItem
  102. {...sidebarItemProps}
  103. index
  104. icon={<IconProject size="md" />}
  105. label={<GuideAnchor target="projects">{t('Projects')}</GuideAnchor>}
  106. to={`/organizations/${organization.slug}/projects/`}
  107. id="projects"
  108. />
  109. );
  110. const issues = hasOrganization && (
  111. <SidebarItem
  112. {...sidebarItemProps}
  113. icon={<IconIssues size="md" />}
  114. label={<GuideAnchor target="issues">{t('Issues')}</GuideAnchor>}
  115. to={`/organizations/${organization.slug}/issues/`}
  116. id="issues"
  117. />
  118. );
  119. const discover2 = hasOrganization && (
  120. <Feature
  121. hookName="feature-disabled:discover2-sidebar-item"
  122. features={['discover-basic']}
  123. organization={organization}
  124. >
  125. <SidebarItem
  126. {...sidebarItemProps}
  127. icon={<IconTelescope size="md" />}
  128. label={<GuideAnchor target="discover">{t('Discover')}</GuideAnchor>}
  129. to={getDiscoverLandingUrl(organization)}
  130. id="discover-v2"
  131. />
  132. </Feature>
  133. );
  134. const performance = hasOrganization && (
  135. <Feature
  136. hookName="feature-disabled:performance-sidebar-item"
  137. features={['performance-view']}
  138. organization={organization}
  139. >
  140. <SidebarOverride id="performance-override">
  141. {(overideProps: Partial<React.ComponentProps<typeof SidebarItem>>) => (
  142. <SidebarItem
  143. {...sidebarItemProps}
  144. icon={<IconLightning size="md" />}
  145. label={<GuideAnchor target="performance">{t('Performance')}</GuideAnchor>}
  146. to={`/organizations/${organization.slug}/performance/`}
  147. id="performance"
  148. {...overideProps}
  149. />
  150. )}
  151. </SidebarOverride>
  152. </Feature>
  153. );
  154. const releases = hasOrganization && (
  155. <SidebarItem
  156. {...sidebarItemProps}
  157. icon={<IconReleases size="md" />}
  158. label={<GuideAnchor target="releases">{t('Releases')}</GuideAnchor>}
  159. to={`/organizations/${organization.slug}/releases/`}
  160. id="releases"
  161. />
  162. );
  163. const userFeedback = hasOrganization && (
  164. <SidebarItem
  165. {...sidebarItemProps}
  166. icon={<IconSupport size="md" />}
  167. label={t('User Feedback')}
  168. to={`/organizations/${organization.slug}/user-feedback/`}
  169. id="user-feedback"
  170. />
  171. );
  172. const alerts = hasOrganization && (
  173. <SidebarItem
  174. {...sidebarItemProps}
  175. icon={<IconSiren size="md" />}
  176. label={t('Alerts')}
  177. to={`/organizations/${organization.slug}/alerts/rules/`}
  178. id="alerts"
  179. />
  180. );
  181. const monitors = hasOrganization && (
  182. <Feature features={['monitors']} organization={organization}>
  183. <SidebarItem
  184. {...sidebarItemProps}
  185. icon={<IconLab size="md" />}
  186. label={t('Monitors')}
  187. to={`/organizations/${organization.slug}/monitors/`}
  188. id="monitors"
  189. />
  190. </Feature>
  191. );
  192. const replays = hasOrganization && (
  193. <Feature features={['session-replay']} organization={organization}>
  194. <SidebarItem
  195. {...sidebarItemProps}
  196. icon={<IconPlay size="md" />}
  197. label={t('Replays')}
  198. to={`/organizations/${organization.slug}/replays/`}
  199. id="replays"
  200. />
  201. </Feature>
  202. );
  203. const dashboards = hasOrganization && (
  204. <Feature
  205. hookName="feature-disabled:dashboards-sidebar-item"
  206. features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
  207. organization={organization}
  208. requireAll={false}
  209. >
  210. <SidebarItem
  211. {...sidebarItemProps}
  212. index
  213. icon={<IconDashboard size="md" />}
  214. label={t('Dashboards')}
  215. to={`/organizations/${organization.slug}/dashboards/`}
  216. id="customizable-dashboards"
  217. />
  218. </Feature>
  219. );
  220. const profiling = hasOrganization && (
  221. <Feature
  222. hookName="feature-disabled:profiling-sidebar-item"
  223. features={['profiling']}
  224. organization={organization}
  225. requireAll={false}
  226. >
  227. <SidebarItem
  228. {...sidebarItemProps}
  229. index
  230. icon={<IconSpan size="md" />}
  231. label={t('Profiling')}
  232. to={`/organizations/${organization.slug}/profiling/`}
  233. id="profiling"
  234. isAlpha
  235. />
  236. </Feature>
  237. );
  238. const activity = hasOrganization && (
  239. <SidebarItem
  240. {...sidebarItemProps}
  241. icon={<IconList size="md" />}
  242. label={t('Activity')}
  243. to={`/organizations/${organization.slug}/activity/`}
  244. id="activity"
  245. />
  246. );
  247. const stats = hasOrganization && (
  248. <SidebarItem
  249. {...sidebarItemProps}
  250. icon={<IconStats size="md" />}
  251. label={t('Stats')}
  252. to={`/organizations/${organization.slug}/stats/`}
  253. id="stats"
  254. />
  255. );
  256. const settings = hasOrganization && (
  257. <SidebarItem
  258. {...sidebarItemProps}
  259. icon={<IconSettings size="md" />}
  260. label={t('Settings')}
  261. to={`/settings/${organization.slug}/`}
  262. id="settings"
  263. />
  264. );
  265. return (
  266. <SidebarWrapper collapsed={collapsed}>
  267. <SidebarSectionGroupPrimary>
  268. <SidebarSection>
  269. <SidebarDropdown
  270. orientation={orientation}
  271. collapsed={collapsed}
  272. org={organization}
  273. user={config.user}
  274. config={config}
  275. />
  276. </SidebarSection>
  277. <PrimaryItems>
  278. {hasOrganization && (
  279. <Fragment>
  280. <SidebarSection>
  281. {projects}
  282. {issues}
  283. {performance}
  284. {releases}
  285. {userFeedback}
  286. {alerts}
  287. {discover2}
  288. {dashboards}
  289. {profiling}
  290. </SidebarSection>
  291. <SidebarSection>
  292. {replays}
  293. {monitors}
  294. </SidebarSection>
  295. <SidebarSection>
  296. {activity}
  297. {stats}
  298. </SidebarSection>
  299. <SidebarSection>{settings}</SidebarSection>
  300. </Fragment>
  301. )}
  302. </PrimaryItems>
  303. </SidebarSectionGroupPrimary>
  304. {hasOrganization && (
  305. <SidebarSectionGroup>
  306. <SidebarSection noMargin noPadding>
  307. <OnboardingStatus
  308. org={organization}
  309. currentPanel={activePanel}
  310. onShowPanel={() => togglePanel(SidebarPanelKey.OnboardingWizard)}
  311. hidePanel={hidePanel}
  312. {...sidebarItemProps}
  313. />
  314. </SidebarSection>
  315. <SidebarSection>
  316. {HookStore.get('sidebar:bottom-items').length > 0 &&
  317. HookStore.get('sidebar:bottom-items')[0]({
  318. orientation,
  319. collapsed,
  320. hasPanel,
  321. organization,
  322. })}
  323. <SidebarHelp
  324. orientation={orientation}
  325. collapsed={collapsed}
  326. hidePanel={hidePanel}
  327. organization={organization}
  328. />
  329. <Broadcasts
  330. orientation={orientation}
  331. collapsed={collapsed}
  332. currentPanel={activePanel}
  333. onShowPanel={() => togglePanel(SidebarPanelKey.Broadcasts)}
  334. hidePanel={hidePanel}
  335. organization={organization}
  336. />
  337. <ServiceIncidents
  338. orientation={orientation}
  339. collapsed={collapsed}
  340. currentPanel={activePanel}
  341. onShowPanel={() => togglePanel(SidebarPanelKey.ServiceIncidents)}
  342. hidePanel={hidePanel}
  343. />
  344. </SidebarSection>
  345. {!horizontal && (
  346. <SidebarSection>
  347. <SidebarCollapseItem
  348. id="collapse"
  349. data-test-id="sidebar-collapse"
  350. {...sidebarItemProps}
  351. icon={<StyledIconChevron collapsed={collapsed} />}
  352. label={collapsed ? t('Expand') : t('Collapse')}
  353. onClick={toggleCollapse}
  354. />
  355. </SidebarSection>
  356. )}
  357. </SidebarSectionGroup>
  358. )}
  359. </SidebarWrapper>
  360. );
  361. }
  362. export default Sidebar;
  363. const responsiveFlex = css`
  364. display: flex;
  365. flex-direction: column;
  366. @media (max-width: ${theme.breakpoints.medium}) {
  367. flex-direction: row;
  368. }
  369. `;
  370. export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
  371. background: ${p => p.theme.sidebarGradient};
  372. color: ${p => p.theme.sidebar.color};
  373. line-height: 1;
  374. padding: 12px 0 2px; /* Allows for 32px avatars */
  375. width: ${p => p.theme.sidebar[p.collapsed ? 'collapsedWidth' : 'expandedWidth']};
  376. position: fixed;
  377. top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  378. left: 0;
  379. bottom: 0;
  380. justify-content: space-between;
  381. z-index: ${p => p.theme.zIndex.sidebar};
  382. border-right: solid 1px ${p => p.theme.sidebarBorder};
  383. ${responsiveFlex};
  384. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  385. top: 0;
  386. left: 0;
  387. right: 0;
  388. height: ${p => p.theme.sidebar.mobileHeight};
  389. bottom: auto;
  390. width: auto;
  391. padding: 0 ${space(1)};
  392. align-items: center;
  393. border-right: none;
  394. border-bottom: solid 1px ${p => p.theme.sidebarBorder};
  395. }
  396. `;
  397. const SidebarSectionGroup = styled('div')`
  398. ${responsiveFlex};
  399. flex-shrink: 0; /* prevents shrinking on Safari */
  400. `;
  401. const SidebarSectionGroupPrimary = styled('div')`
  402. ${responsiveFlex};
  403. /* necessary for child flexing on msedge and ff */
  404. min-height: 0;
  405. min-width: 0;
  406. flex: 1;
  407. /* expand to fill the entire height on mobile */
  408. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  409. height: 100%;
  410. align-items: center;
  411. }
  412. `;
  413. const PrimaryItems = styled('div')`
  414. overflow: auto;
  415. flex: 1;
  416. display: flex;
  417. flex-direction: column;
  418. -ms-overflow-style: -ms-autohiding-scrollbar;
  419. @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
  420. border-bottom: 1px solid ${p => p.theme.gray400};
  421. padding-bottom: ${space(1)};
  422. box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
  423. &::-webkit-scrollbar {
  424. background-color: transparent;
  425. width: 8px;
  426. }
  427. &::-webkit-scrollbar-thumb {
  428. background: ${p => p.theme.gray400};
  429. border-radius: 8px;
  430. }
  431. }
  432. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  433. overflow-y: visible;
  434. flex-direction: row;
  435. height: 100%;
  436. align-items: center;
  437. border-right: 1px solid ${p => p.theme.gray400};
  438. padding-right: ${space(1)};
  439. margin-right: ${space(0.5)};
  440. box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset;
  441. ::-webkit-scrollbar {
  442. display: none;
  443. }
  444. }
  445. `;
  446. const SidebarSection = styled(SidebarSectionGroup)<{
  447. noMargin?: boolean;
  448. noPadding?: boolean;
  449. }>`
  450. ${p => !p.noMargin && `margin: ${space(1)} 0`};
  451. ${p => !p.noPadding && 'padding: 0 19px'};
  452. @media (max-width: ${p => p.theme.breakpoints.small}) {
  453. margin: 0;
  454. padding: 0;
  455. }
  456. &:empty {
  457. display: none;
  458. }
  459. `;
  460. const ExpandedIcon = css`
  461. transition: 0.3s transform ease;
  462. transform: rotate(270deg);
  463. `;
  464. const CollapsedIcon = css`
  465. transform: rotate(90deg);
  466. `;
  467. const StyledIconChevron = styled(({collapsed, ...props}) => (
  468. <IconChevron
  469. direction="left"
  470. size="md"
  471. isCircled
  472. css={[ExpandedIcon, collapsed && CollapsedIcon]}
  473. {...props}
  474. />
  475. ))``;
  476. const SidebarCollapseItem = styled(SidebarItem)`
  477. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  478. display: none;
  479. }
  480. `;