index.tsx 16 KB

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