index.tsx 15 KB

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