index.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543
  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']} 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. />
  205. </Feature>
  206. );
  207. const dashboards = hasOrganization && (
  208. <Feature
  209. hookName="feature-disabled:dashboards-sidebar-item"
  210. features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
  211. organization={organization}
  212. requireAll={false}
  213. >
  214. <SidebarItem
  215. {...sidebarItemProps}
  216. index
  217. icon={<IconDashboard size="md" />}
  218. label={t('Dashboards')}
  219. to={`/organizations/${organization.slug}/dashboards/`}
  220. id="customizable-dashboards"
  221. />
  222. </Feature>
  223. );
  224. const profiling = hasOrganization && (
  225. <Feature
  226. hookName="feature-disabled:profiling-sidebar-item"
  227. features={['profiling']}
  228. organization={organization}
  229. requireAll={false}
  230. >
  231. <SidebarItem
  232. {...sidebarItemProps}
  233. index
  234. icon={<IconSpan size="md" />}
  235. label={t('Profiling')}
  236. to={`/organizations/${organization.slug}/profiling/`}
  237. id="profiling"
  238. isAlpha
  239. />
  240. </Feature>
  241. );
  242. const activity = hasOrganization && (
  243. <SidebarItem
  244. {...sidebarItemProps}
  245. icon={<IconList size="md" />}
  246. label={t('Activity')}
  247. to={`/organizations/${organization.slug}/activity/`}
  248. id="activity"
  249. />
  250. );
  251. const stats = hasOrganization && (
  252. <SidebarItem
  253. {...sidebarItemProps}
  254. icon={<IconStats size="md" />}
  255. label={t('Stats')}
  256. to={`/organizations/${organization.slug}/stats/`}
  257. id="stats"
  258. />
  259. );
  260. const settings = hasOrganization && (
  261. <SidebarItem
  262. {...sidebarItemProps}
  263. icon={<IconSettings size="md" />}
  264. label={t('Settings')}
  265. to={`/settings/${organization.slug}/`}
  266. id="settings"
  267. />
  268. );
  269. return (
  270. <SidebarWrapper collapsed={collapsed}>
  271. <SidebarSectionGroupPrimary>
  272. <SidebarSection>
  273. <SidebarDropdown
  274. orientation={orientation}
  275. collapsed={collapsed}
  276. org={organization}
  277. user={config.user}
  278. config={config}
  279. />
  280. </SidebarSection>
  281. <PrimaryItems>
  282. {hasOrganization && (
  283. <Fragment>
  284. <SidebarSection>
  285. {projects}
  286. {issues}
  287. {performance}
  288. {releases}
  289. {userFeedback}
  290. {alerts}
  291. {discover2}
  292. {dashboards}
  293. {profiling}
  294. </SidebarSection>
  295. <SidebarSection>
  296. {replays}
  297. {monitors}
  298. </SidebarSection>
  299. <SidebarSection>
  300. {activity}
  301. {stats}
  302. </SidebarSection>
  303. <SidebarSection>{settings}</SidebarSection>
  304. </Fragment>
  305. )}
  306. </PrimaryItems>
  307. </SidebarSectionGroupPrimary>
  308. {hasOrganization && (
  309. <SidebarSectionGroup>
  310. <PerformanceOnboardingSidebar
  311. currentPanel={activePanel}
  312. onShowPanel={() => togglePanel(SidebarPanelKey.PerformanceOnboarding)}
  313. hidePanel={hidePanel}
  314. {...sidebarItemProps}
  315. />
  316. <SidebarSection noMargin noPadding>
  317. <OnboardingStatus
  318. org={organization}
  319. currentPanel={activePanel}
  320. onShowPanel={() => togglePanel(SidebarPanelKey.OnboardingWizard)}
  321. hidePanel={hidePanel}
  322. {...sidebarItemProps}
  323. />
  324. </SidebarSection>
  325. <SidebarSection>
  326. {HookStore.get('sidebar:bottom-items').length > 0 &&
  327. HookStore.get('sidebar:bottom-items')[0]({
  328. orientation,
  329. collapsed,
  330. hasPanel,
  331. organization,
  332. })}
  333. <SidebarHelp
  334. orientation={orientation}
  335. collapsed={collapsed}
  336. hidePanel={hidePanel}
  337. organization={organization}
  338. />
  339. <Broadcasts
  340. orientation={orientation}
  341. collapsed={collapsed}
  342. currentPanel={activePanel}
  343. onShowPanel={() => togglePanel(SidebarPanelKey.Broadcasts)}
  344. hidePanel={hidePanel}
  345. organization={organization}
  346. />
  347. <ServiceIncidents
  348. orientation={orientation}
  349. collapsed={collapsed}
  350. currentPanel={activePanel}
  351. onShowPanel={() => togglePanel(SidebarPanelKey.ServiceIncidents)}
  352. hidePanel={hidePanel}
  353. />
  354. </SidebarSection>
  355. {!horizontal && (
  356. <SidebarSection>
  357. <SidebarCollapseItem
  358. id="collapse"
  359. data-test-id="sidebar-collapse"
  360. {...sidebarItemProps}
  361. icon={<StyledIconChevron collapsed={collapsed} />}
  362. label={collapsed ? t('Expand') : t('Collapse')}
  363. onClick={toggleCollapse}
  364. />
  365. </SidebarSection>
  366. )}
  367. </SidebarSectionGroup>
  368. )}
  369. </SidebarWrapper>
  370. );
  371. }
  372. export default Sidebar;
  373. const responsiveFlex = css`
  374. display: flex;
  375. flex-direction: column;
  376. @media (max-width: ${theme.breakpoints.medium}) {
  377. flex-direction: row;
  378. }
  379. `;
  380. export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
  381. background: ${p => p.theme.sidebarGradient};
  382. color: ${p => p.theme.sidebar.color};
  383. line-height: 1;
  384. padding: 12px 0 2px; /* Allows for 32px avatars */
  385. width: ${p => p.theme.sidebar[p.collapsed ? 'collapsedWidth' : 'expandedWidth']};
  386. position: fixed;
  387. top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  388. left: 0;
  389. bottom: 0;
  390. justify-content: space-between;
  391. z-index: ${p => p.theme.zIndex.sidebar};
  392. border-right: solid 1px ${p => p.theme.sidebarBorder};
  393. ${responsiveFlex};
  394. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  395. top: 0;
  396. left: 0;
  397. right: 0;
  398. height: ${p => p.theme.sidebar.mobileHeight};
  399. bottom: auto;
  400. width: auto;
  401. padding: 0 ${space(1)};
  402. align-items: center;
  403. border-right: none;
  404. border-bottom: solid 1px ${p => p.theme.sidebarBorder};
  405. }
  406. `;
  407. const SidebarSectionGroup = styled('div')`
  408. ${responsiveFlex};
  409. flex-shrink: 0; /* prevents shrinking on Safari */
  410. `;
  411. const SidebarSectionGroupPrimary = styled('div')`
  412. ${responsiveFlex};
  413. /* necessary for child flexing on msedge and ff */
  414. min-height: 0;
  415. min-width: 0;
  416. flex: 1;
  417. /* expand to fill the entire height on mobile */
  418. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  419. height: 100%;
  420. align-items: center;
  421. }
  422. `;
  423. const PrimaryItems = styled('div')`
  424. overflow: auto;
  425. flex: 1;
  426. display: flex;
  427. flex-direction: column;
  428. -ms-overflow-style: -ms-autohiding-scrollbar;
  429. @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
  430. border-bottom: 1px solid ${p => p.theme.gray400};
  431. padding-bottom: ${space(1)};
  432. box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
  433. &::-webkit-scrollbar {
  434. background-color: transparent;
  435. width: 8px;
  436. }
  437. &::-webkit-scrollbar-thumb {
  438. background: ${p => p.theme.gray400};
  439. border-radius: 8px;
  440. }
  441. }
  442. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  443. overflow-y: visible;
  444. flex-direction: row;
  445. height: 100%;
  446. align-items: center;
  447. border-right: 1px solid ${p => p.theme.gray400};
  448. padding-right: ${space(1)};
  449. margin-right: ${space(0.5)};
  450. box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset;
  451. ::-webkit-scrollbar {
  452. display: none;
  453. }
  454. }
  455. `;
  456. const SidebarSection = styled(SidebarSectionGroup)<{
  457. noMargin?: boolean;
  458. noPadding?: boolean;
  459. }>`
  460. ${p => !p.noMargin && `margin: ${space(1)} 0`};
  461. ${p => !p.noPadding && 'padding: 0 19px'};
  462. @media (max-width: ${p => p.theme.breakpoints.small}) {
  463. margin: 0;
  464. padding: 0;
  465. }
  466. &:empty {
  467. display: none;
  468. }
  469. `;
  470. const ExpandedIcon = css`
  471. transition: 0.3s transform ease;
  472. transform: rotate(270deg);
  473. `;
  474. const CollapsedIcon = css`
  475. transform: rotate(90deg);
  476. `;
  477. const StyledIconChevron = styled(({collapsed, ...props}) => (
  478. <IconChevron
  479. direction="left"
  480. size="md"
  481. isCircled
  482. css={[ExpandedIcon, collapsed && CollapsedIcon]}
  483. {...props}
  484. />
  485. ))``;
  486. const SidebarCollapseItem = styled(SidebarItem)`
  487. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  488. display: none;
  489. }
  490. `;