index.tsx 15 KB

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