index.tsx 15 KB

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