index.tsx 17 KB

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