index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  1. import {Fragment, useCallback, useContext, useEffect} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences';
  5. import Feature from 'sentry/components/acl/feature';
  6. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  7. import {Chevron} from 'sentry/components/chevron';
  8. import FeedbackOnboardingSidebar from 'sentry/components/feedback/feedbackOnboarding/sidebar';
  9. import Hook from 'sentry/components/hook';
  10. import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
  11. import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
  12. import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar';
  13. import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar';
  14. import {
  15. ExpandedContext,
  16. ExpandedContextProvider,
  17. } from 'sentry/components/sidebar/expandedContextProvider';
  18. import {isDone} from 'sentry/components/sidebar/utils';
  19. import {
  20. IconDashboard,
  21. IconGraph,
  22. IconIssues,
  23. IconLightning,
  24. IconMegaphone,
  25. IconProject,
  26. IconReleases,
  27. IconSearch,
  28. IconSettings,
  29. IconSiren,
  30. IconStats,
  31. IconSupport,
  32. IconTimer,
  33. } from 'sentry/icons';
  34. import {t} from 'sentry/locale';
  35. import ConfigStore from 'sentry/stores/configStore';
  36. import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore';
  37. import HookStore from 'sentry/stores/hookStore';
  38. import PreferencesStore from 'sentry/stores/preferencesStore';
  39. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  40. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  41. import {space} from 'sentry/styles/space';
  42. import type {Organization} from 'sentry/types/organization';
  43. import {isDemoWalkthrough} from 'sentry/utils/demoMode';
  44. import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
  45. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  46. import {hasCustomMetrics} from 'sentry/utils/metrics/features';
  47. import theme from 'sentry/utils/theme';
  48. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  49. import {useLocation} from 'sentry/utils/useLocation';
  50. import useMedia from 'sentry/utils/useMedia';
  51. import useOrganization from 'sentry/utils/useOrganization';
  52. import useProjects from 'sentry/utils/useProjects';
  53. import {useModuleURLBuilder} from 'sentry/views/insights/common/utils/useModuleURL';
  54. import {MODULE_SIDEBAR_TITLE as HTTP_MODULE_SIDEBAR_TITLE} from 'sentry/views/insights/http/settings';
  55. import {MODULE_TITLES} from 'sentry/views/insights/settings';
  56. import MetricsOnboardingSidebar from 'sentry/views/metrics/ddmOnboarding/sidebar';
  57. import {ProfilingOnboardingSidebar} from '../profiling/profilingOnboardingSidebar';
  58. import Broadcasts from './broadcasts';
  59. import SidebarHelp from './help';
  60. import OnboardingStatus from './onboardingStatus';
  61. import ServiceIncidents from './serviceIncidents';
  62. import {SidebarAccordion} from './sidebarAccordion';
  63. import SidebarDropdown from './sidebarDropdown';
  64. import SidebarItem from './sidebarItem';
  65. import type {SidebarOrientation} from './types';
  66. import {SidebarPanelKey} from './types';
  67. function activatePanel(panel: SidebarPanelKey) {
  68. SidebarPanelStore.activatePanel(panel);
  69. }
  70. function togglePanel(panel: SidebarPanelKey) {
  71. SidebarPanelStore.togglePanel(panel);
  72. }
  73. function hidePanel(hash?: string) {
  74. SidebarPanelStore.hidePanel(hash);
  75. }
  76. function useOpenOnboardingSidebar(organization?: Organization) {
  77. const onboardingContext = useContext(OnboardingContext);
  78. const {projects: project} = useProjects();
  79. const location = useLocation();
  80. const openOnboardingSidebar = (() => {
  81. if (location?.hash === '#welcome') {
  82. if (organization && !ConfigStore.get('demoMode')) {
  83. const tasks = getMergedTasks({
  84. organization,
  85. projects: project,
  86. onboardingContext,
  87. });
  88. const allDisplayedTasks = tasks
  89. .filter(task => task.display)
  90. .filter(task => !task.renderCard);
  91. const doneTasks = allDisplayedTasks.filter(isDone);
  92. return !(doneTasks.length >= allDisplayedTasks.length);
  93. }
  94. return true;
  95. }
  96. return false;
  97. })();
  98. useEffect(() => {
  99. if (openOnboardingSidebar) {
  100. activatePanel(SidebarPanelKey.ONBOARDING_WIZARD);
  101. }
  102. }, [openOnboardingSidebar]);
  103. }
  104. function Sidebar() {
  105. const location = useLocation();
  106. const preferences = useLegacyStore(PreferencesStore);
  107. const activePanel = useLegacyStore(SidebarPanelStore);
  108. const organization = useOrganization({allowNull: true});
  109. const {shouldAccordionFloat} = useContext(ExpandedContext);
  110. const hasNewNav = organization?.features.includes('navigation-sidebar-v2');
  111. const hasOrganization = !!organization;
  112. const isSelfHostedErrorsOnly = ConfigStore.get('isSelfHostedErrorsOnly');
  113. const collapsed = hasNewNav ? true : !!preferences.collapsed;
  114. const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  115. // Panel determines whether to highlight
  116. const hasPanel = !!activePanel;
  117. const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
  118. const sidebarItemProps = {
  119. orientation,
  120. collapsed,
  121. hasPanel,
  122. organization,
  123. hasNewNav,
  124. };
  125. // Avoid showing superuser UI on self-hosted instances
  126. const showSuperuserWarning = () => {
  127. return isActiveSuperuser() && !ConfigStore.get('isSelfHosted');
  128. };
  129. // Avoid showing superuser UI on certain organizations
  130. const isExcludedOrg = () => {
  131. return HookStore.get('component:superuser-warning-excluded')[0]?.(organization);
  132. };
  133. useOpenOnboardingSidebar();
  134. const toggleCollapse = useCallback(() => {
  135. if (collapsed) {
  136. showSidebar();
  137. } else {
  138. hideSidebar();
  139. }
  140. }, [collapsed]);
  141. // Close panel on any navigation
  142. useEffect(() => void hidePanel(), [location?.pathname]);
  143. // Add classname to body
  144. useEffect(() => {
  145. const bcl = document.body.classList;
  146. bcl.add('body-sidebar');
  147. return () => bcl.remove('body-sidebar');
  148. }, []);
  149. useEffect(() => {
  150. Object.values(SidebarPanelKey).forEach(key => {
  151. if (location?.hash === `#sidebar-${key}`) {
  152. togglePanel(key);
  153. }
  154. });
  155. }, [location?.hash]);
  156. // Add sidebar collapse classname to body
  157. useEffect(() => {
  158. const bcl = document.body.classList;
  159. if (collapsed) {
  160. bcl.add('collapsed');
  161. } else {
  162. bcl.remove('collapsed');
  163. }
  164. return () => bcl.remove('collapsed');
  165. }, [collapsed]);
  166. // Add sidebar hasNewNav classname to body
  167. useEffect(() => {
  168. const bcl = document.body.classList;
  169. if (hasNewNav) {
  170. bcl.add('hasNewNav');
  171. } else {
  172. bcl.remove('hasNewNav');
  173. }
  174. return () => bcl.remove('hasNewNav');
  175. }, [hasNewNav]);
  176. const sidebarAnchor = isDemoWalkthrough() ? (
  177. <GuideAnchor target="projects" disabled={!DemoWalkthroughStore.get('sidebar')}>
  178. {t('Projects')}
  179. </GuideAnchor>
  180. ) : (
  181. <GuideAnchor target="projects">{t('Projects')}</GuideAnchor>
  182. );
  183. const projects = hasOrganization && (
  184. <SidebarItem
  185. {...sidebarItemProps}
  186. index
  187. icon={<IconProject />}
  188. label={sidebarAnchor}
  189. to={`/organizations/${organization.slug}/projects/`}
  190. id="projects"
  191. />
  192. );
  193. const issues = hasOrganization && (
  194. <SidebarItem
  195. {...sidebarItemProps}
  196. icon={<IconIssues />}
  197. label={<GuideAnchor target="issues">{t('Issues')}</GuideAnchor>}
  198. to={`/organizations/${organization.slug}/issues/`}
  199. search="?referrer=sidebar"
  200. id="issues"
  201. hasNewNav={hasNewNav}
  202. />
  203. );
  204. const discover2 = hasOrganization && (
  205. <Feature
  206. hookName="feature-disabled:discover2-sidebar-item"
  207. features="discover-basic"
  208. organization={organization}
  209. >
  210. <SidebarItem
  211. {...sidebarItemProps}
  212. icon={<SubitemDot collapsed />}
  213. label={<GuideAnchor target="discover">{t('Discover')}</GuideAnchor>}
  214. to={getDiscoverLandingUrl(organization)}
  215. id="discover-v2"
  216. />
  217. </Feature>
  218. );
  219. const moduleURLBuilder = useModuleURLBuilder(true);
  220. const queries = hasOrganization && (
  221. <Feature key="db" features="insights-entry-points" organization={organization}>
  222. <SidebarItem
  223. {...sidebarItemProps}
  224. label={
  225. <GuideAnchor target="performance-database">{MODULE_TITLES.db}</GuideAnchor>
  226. }
  227. to={`/organizations/${organization.slug}/${moduleURLBuilder('db')}/`}
  228. id="performance-database"
  229. icon={<SubitemDot collapsed />}
  230. />
  231. </Feature>
  232. );
  233. const requests = hasOrganization && (
  234. <Feature key="http" features="insights-entry-points" organization={organization}>
  235. <SidebarItem
  236. {...sidebarItemProps}
  237. label={
  238. <GuideAnchor target="performance-http">{HTTP_MODULE_SIDEBAR_TITLE}</GuideAnchor>
  239. }
  240. to={`/organizations/${organization.slug}/${moduleURLBuilder('http')}/`}
  241. id="performance-http"
  242. icon={<SubitemDot collapsed />}
  243. />
  244. </Feature>
  245. );
  246. const caches = hasOrganization && (
  247. <Feature key="cache" features="insights-entry-points" organization={organization}>
  248. <SidebarItem
  249. {...sidebarItemProps}
  250. label={
  251. <GuideAnchor target="performance-cache">{MODULE_TITLES.cache}</GuideAnchor>
  252. }
  253. to={`/organizations/${organization.slug}/${moduleURLBuilder('cache')}/`}
  254. id="performance-cache"
  255. icon={<SubitemDot collapsed />}
  256. />
  257. </Feature>
  258. );
  259. const webVitals = hasOrganization && (
  260. <Feature key="vital" features="insights-entry-points" organization={organization}>
  261. <SidebarItem
  262. {...sidebarItemProps}
  263. label={
  264. <GuideAnchor target="performance-webvitals">{MODULE_TITLES.vital}</GuideAnchor>
  265. }
  266. to={`/organizations/${organization.slug}/${moduleURLBuilder('vital')}/`}
  267. id="performance-webvitals"
  268. icon={<SubitemDot collapsed />}
  269. />
  270. </Feature>
  271. );
  272. const queues = hasOrganization && (
  273. <Feature key="queue" features="insights-entry-points" organization={organization}>
  274. <SidebarItem
  275. {...sidebarItemProps}
  276. label={
  277. <GuideAnchor target="performance-queues">{MODULE_TITLES.queue}</GuideAnchor>
  278. }
  279. to={`/organizations/${organization.slug}/${moduleURLBuilder('queue')}/`}
  280. id="performance-queues"
  281. icon={<SubitemDot collapsed />}
  282. />
  283. </Feature>
  284. );
  285. // the mobile screens module is meant to be as a replacement for screen load, app start, and mobile ui
  286. // so if mobile screens is enabled, we should not show the other mobile modules
  287. const hasMobileScreensModule =
  288. hasOrganization && organization.features.includes('insights-mobile-screens-module');
  289. const screenLoads = hasOrganization && !hasMobileScreensModule && (
  290. <Feature
  291. key="screen_load"
  292. features="insights-entry-points"
  293. organization={organization}
  294. >
  295. <SidebarItem
  296. {...sidebarItemProps}
  297. label={MODULE_TITLES.screen_load}
  298. to={`/organizations/${organization.slug}/${moduleURLBuilder('screen_load')}/`}
  299. id="performance-mobile-screens"
  300. icon={<SubitemDot collapsed />}
  301. />
  302. </Feature>
  303. );
  304. const appStarts = hasOrganization && !hasMobileScreensModule && (
  305. <Feature key="app_start" features="insights-entry-points" organization={organization}>
  306. <SidebarItem
  307. {...sidebarItemProps}
  308. label={MODULE_TITLES.app_start}
  309. to={`/organizations/${organization.slug}/${moduleURLBuilder('app_start')}/`}
  310. id="performance-mobile-app-startup"
  311. icon={<SubitemDot collapsed />}
  312. />
  313. </Feature>
  314. );
  315. const mobileUI = hasOrganization && !hasMobileScreensModule && (
  316. <Feature
  317. key="mobile-ui"
  318. features={['insights-entry-points', 'starfish-mobile-ui-module']}
  319. organization={organization}
  320. >
  321. <SidebarItem
  322. {...sidebarItemProps}
  323. label={MODULE_TITLES['mobile-ui']}
  324. to={`/organizations/${organization.slug}/${moduleURLBuilder('mobile-ui')}/`}
  325. id="performance-mobile-ui"
  326. icon={<SubitemDot collapsed />}
  327. isAlpha
  328. />
  329. </Feature>
  330. );
  331. const mobileScreens = hasOrganization && hasMobileScreensModule && (
  332. <Feature
  333. key="mobile-screens"
  334. features={['insights-entry-points']}
  335. organization={organization}
  336. >
  337. <SidebarItem
  338. {...sidebarItemProps}
  339. label={MODULE_TITLES['mobile-screens']}
  340. to={`/organizations/${organization.slug}/${moduleURLBuilder('mobile-screens')}/`}
  341. id="performance-mobile-screens"
  342. icon={<SubitemDot collapsed />}
  343. />
  344. </Feature>
  345. );
  346. const resources = hasOrganization && (
  347. <Feature key="resource" features="insights-entry-points">
  348. <SidebarItem
  349. {...sidebarItemProps}
  350. label={<GuideAnchor target="starfish">{MODULE_TITLES.resource}</GuideAnchor>}
  351. to={`/organizations/${organization.slug}/${moduleURLBuilder('resource')}/`}
  352. id="performance-browser-resources"
  353. icon={<SubitemDot collapsed />}
  354. />
  355. </Feature>
  356. );
  357. const traces = hasOrganization && (
  358. <Feature features="performance-trace-explorer">
  359. <SidebarItem
  360. {...sidebarItemProps}
  361. label={<GuideAnchor target="traces">{t('Traces')}</GuideAnchor>}
  362. to={`/organizations/${organization.slug}/traces/`}
  363. id="performance-trace-explorer"
  364. icon={<SubitemDot collapsed />}
  365. isBeta
  366. />
  367. </Feature>
  368. );
  369. const llmMonitoring = hasOrganization && (
  370. <Feature features={['insights-entry-points']} organization={organization}>
  371. <SidebarItem
  372. {...sidebarItemProps}
  373. icon={<SubitemDot collapsed />}
  374. label={MODULE_TITLES.ai}
  375. to={`/organizations/${organization.slug}/${moduleURLBuilder('ai')}/`}
  376. id="llm-monitoring"
  377. />
  378. </Feature>
  379. );
  380. const performance = hasOrganization && (
  381. <Feature
  382. hookName="feature-disabled:performance-sidebar-item"
  383. features="performance-view"
  384. organization={organization}
  385. >
  386. <SidebarItem
  387. {...sidebarItemProps}
  388. icon={<IconLightning />}
  389. label={
  390. <GuideAnchor target="performance">
  391. {hasNewNav ? 'Perf.' : t('Performance')}
  392. </GuideAnchor>
  393. }
  394. to={`/organizations/${organization.slug}/performance/`}
  395. id="performance"
  396. />
  397. </Feature>
  398. );
  399. const releases = hasOrganization && (
  400. <SidebarItem
  401. {...sidebarItemProps}
  402. icon={<IconReleases />}
  403. label={<GuideAnchor target="releases">{t('Releases')}</GuideAnchor>}
  404. to={`/organizations/${organization.slug}/releases/`}
  405. id="releases"
  406. />
  407. );
  408. const userFeedback = hasOrganization && (
  409. <Feature features="old-user-feedback" organization={organization}>
  410. <SidebarItem
  411. {...sidebarItemProps}
  412. icon={<IconSupport />}
  413. label={t('User Feedback')}
  414. to={`/organizations/${organization.slug}/user-feedback/`}
  415. id="user-feedback"
  416. />
  417. </Feature>
  418. );
  419. const feedback = hasOrganization && (
  420. <Feature features="user-feedback-ui" organization={organization}>
  421. <SidebarItem
  422. {...sidebarItemProps}
  423. icon={<IconMegaphone />}
  424. label={t('User Feedback')}
  425. variant="short"
  426. to={`/organizations/${organization.slug}/feedback/`}
  427. id="feedback"
  428. />
  429. </Feature>
  430. );
  431. const alerts = hasOrganization && (
  432. <SidebarItem
  433. {...sidebarItemProps}
  434. icon={<IconSiren />}
  435. label={t('Alerts')}
  436. to={`/organizations/${organization.slug}/alerts/rules/`}
  437. id="alerts"
  438. />
  439. );
  440. const monitors = hasOrganization && (
  441. <SidebarItem
  442. {...sidebarItemProps}
  443. icon={<IconTimer />}
  444. label={t('Crons')}
  445. to={`/organizations/${organization.slug}/crons/`}
  446. id="crons"
  447. />
  448. );
  449. const replays = hasOrganization && (
  450. <Feature
  451. hookName="feature-disabled:replay-sidebar-item"
  452. features="session-replay-ui"
  453. organization={organization}
  454. requireAll={false}
  455. >
  456. <SidebarItem
  457. {...sidebarItemProps}
  458. icon={<SubitemDot collapsed />}
  459. label={t('Replays')}
  460. to={`/organizations/${organization.slug}/replays/`}
  461. id="replays"
  462. />
  463. </Feature>
  464. );
  465. const metricsPath = `/organizations/${organization?.slug}/metrics/`;
  466. const metrics = hasOrganization && hasCustomMetrics(organization) && (
  467. <SidebarItem
  468. {...sidebarItemProps}
  469. icon={<SubitemDot collapsed />}
  470. label={t('Metrics')}
  471. to={metricsPath}
  472. search={location?.pathname === normalizeUrl(metricsPath) ? location.search : ''}
  473. id="metrics"
  474. badgeTitle={t(
  475. 'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
  476. )}
  477. isBeta
  478. />
  479. );
  480. const dashboards = hasOrganization && (
  481. <Feature
  482. hookName="feature-disabled:dashboards-sidebar-item"
  483. features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
  484. organization={organization}
  485. requireAll={false}
  486. >
  487. <SidebarItem
  488. {...sidebarItemProps}
  489. index
  490. icon={<IconDashboard />}
  491. label={hasNewNav ? 'Dash.' : t('Dashboards')}
  492. to={`/organizations/${organization.slug}/dashboards/`}
  493. id="customizable-dashboards"
  494. />
  495. </Feature>
  496. );
  497. const profiling = hasOrganization && (
  498. <Feature
  499. hookName="feature-disabled:profiling-sidebar-item"
  500. features="profiling"
  501. organization={organization}
  502. requireAll={false}
  503. >
  504. <SidebarItem
  505. {...sidebarItemProps}
  506. index
  507. icon={<SubitemDot collapsed />}
  508. label={t('Profiles')}
  509. to={`/organizations/${organization.slug}/profiling/`}
  510. id="profiling"
  511. />
  512. </Feature>
  513. );
  514. const stats = hasOrganization && (
  515. <SidebarItem
  516. {...sidebarItemProps}
  517. icon={<IconStats />}
  518. label={t('Stats')}
  519. to={`/organizations/${organization.slug}/stats/`}
  520. id="stats"
  521. />
  522. );
  523. const settings = hasOrganization && (
  524. <SidebarItem
  525. {...sidebarItemProps}
  526. icon={<IconSettings />}
  527. label={t('Settings')}
  528. to={`/settings/${organization.slug}/`}
  529. id="settings"
  530. />
  531. );
  532. const insights = hasOrganization && (
  533. <Feature key="insights" features="insights-entry-points" organization={organization}>
  534. <SidebarAccordion
  535. {...sidebarItemProps}
  536. icon={<IconGraph />}
  537. label={<GuideAnchor target="insights">{t('Insights')}</GuideAnchor>}
  538. id="insights"
  539. initiallyExpanded={false}
  540. exact={!shouldAccordionFloat}
  541. >
  542. {requests}
  543. {queries}
  544. {resources}
  545. {appStarts}
  546. {screenLoads}
  547. {webVitals}
  548. {caches}
  549. {queues}
  550. {mobileUI}
  551. {mobileScreens}
  552. {llmMonitoring}
  553. </SidebarAccordion>
  554. </Feature>
  555. );
  556. // Sidebar accordion includes a secondary list of nav items
  557. // TODO: replace with a secondary panel
  558. const explore = (
  559. <SidebarAccordion
  560. {...sidebarItemProps}
  561. icon={<IconSearch />}
  562. label={<GuideAnchor target="explore">{t('Explore')}</GuideAnchor>}
  563. id="explore"
  564. exact={!shouldAccordionFloat}
  565. >
  566. {traces}
  567. {metrics}
  568. {profiling}
  569. {replays}
  570. {discover2}
  571. </SidebarAccordion>
  572. );
  573. return (
  574. <SidebarWrapper
  575. aria-label={t('Primary Navigation')}
  576. collapsed={collapsed}
  577. hasNewNav={hasNewNav}
  578. >
  579. <ExpandedContextProvider>
  580. <SidebarSectionGroupPrimary>
  581. <DropdownSidebarSection
  582. isSuperuser={showSuperuserWarning() && !isExcludedOrg()}
  583. hasNewNav={hasNewNav}
  584. >
  585. <SidebarDropdown
  586. orientation={orientation}
  587. collapsed={hasNewNav || collapsed}
  588. />
  589. {showSuperuserWarning() && !isExcludedOrg() && (
  590. <Hook name="component:superuser-warning" organization={organization} />
  591. )}
  592. </DropdownSidebarSection>
  593. <PrimaryItems>
  594. {hasOrganization && (
  595. <Fragment>
  596. <SidebarSection hasNewNav={hasNewNav}>
  597. {issues}
  598. {projects}
  599. </SidebarSection>
  600. {!isSelfHostedErrorsOnly && (
  601. <Fragment>
  602. <SidebarSection hasNewNav={hasNewNav}>
  603. {explore}
  604. {insights}
  605. </SidebarSection>
  606. <SidebarSection hasNewNav={hasNewNav}>
  607. {performance}
  608. {feedback}
  609. {monitors}
  610. {alerts}
  611. {dashboards}
  612. {releases}
  613. </SidebarSection>
  614. </Fragment>
  615. )}
  616. {isSelfHostedErrorsOnly && (
  617. <Fragment>
  618. <SidebarSection hasNewNav={hasNewNav}>
  619. {alerts}
  620. {discover2}
  621. {dashboards}
  622. {releases}
  623. {userFeedback}
  624. </SidebarSection>
  625. </Fragment>
  626. )}
  627. <SidebarSection hasNewNav={hasNewNav}>
  628. {stats}
  629. {settings}
  630. </SidebarSection>
  631. </Fragment>
  632. )}
  633. </PrimaryItems>
  634. </SidebarSectionGroupPrimary>
  635. {hasOrganization && (
  636. <SidebarSectionGroup hasNewNav={hasNewNav}>
  637. {/* What are the onboarding sidebars? */}
  638. <PerformanceOnboardingSidebar
  639. currentPanel={activePanel}
  640. onShowPanel={() => togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)}
  641. hidePanel={() => hidePanel('performance-sidequest')}
  642. {...sidebarItemProps}
  643. />
  644. <FeedbackOnboardingSidebar
  645. currentPanel={activePanel}
  646. onShowPanel={() => togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)}
  647. hidePanel={hidePanel}
  648. {...sidebarItemProps}
  649. />
  650. <ReplaysOnboardingSidebar
  651. currentPanel={activePanel}
  652. onShowPanel={() => togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)}
  653. hidePanel={hidePanel}
  654. {...sidebarItemProps}
  655. />
  656. <ProfilingOnboardingSidebar
  657. currentPanel={activePanel}
  658. onShowPanel={() => togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)}
  659. hidePanel={hidePanel}
  660. {...sidebarItemProps}
  661. />
  662. <MetricsOnboardingSidebar
  663. currentPanel={activePanel}
  664. onShowPanel={() => togglePanel(SidebarPanelKey.METRICS_ONBOARDING)}
  665. hidePanel={hidePanel}
  666. {...sidebarItemProps}
  667. />
  668. <SidebarSection hasNewNav={hasNewNav} noMargin noPadding>
  669. <OnboardingStatus
  670. org={organization}
  671. currentPanel={activePanel}
  672. onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
  673. hidePanel={hidePanel}
  674. {...sidebarItemProps}
  675. />
  676. </SidebarSection>
  677. <SidebarSection hasNewNav={hasNewNav}>
  678. {HookStore.get('sidebar:bottom-items').length > 0 &&
  679. HookStore.get('sidebar:bottom-items')[0]({
  680. orientation,
  681. collapsed,
  682. hasPanel,
  683. organization,
  684. })}
  685. <SidebarHelp
  686. orientation={orientation}
  687. collapsed={collapsed}
  688. hidePanel={hidePanel}
  689. organization={organization}
  690. />
  691. <Broadcasts
  692. orientation={orientation}
  693. collapsed={collapsed}
  694. currentPanel={activePanel}
  695. onShowPanel={() => togglePanel(SidebarPanelKey.BROADCASTS)}
  696. hidePanel={hidePanel}
  697. organization={organization}
  698. />
  699. <ServiceIncidents
  700. orientation={orientation}
  701. collapsed={collapsed}
  702. currentPanel={activePanel}
  703. onShowPanel={() => togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)}
  704. hidePanel={hidePanel}
  705. />
  706. </SidebarSection>
  707. {!horizontal && !hasNewNav && (
  708. <SidebarSection hasNewNav={hasNewNav}>
  709. <SidebarCollapseItem
  710. id="collapse"
  711. data-test-id="sidebar-collapse"
  712. {...sidebarItemProps}
  713. icon={<Chevron direction={collapsed ? 'right' : 'left'} />}
  714. label={collapsed ? t('Expand') : t('Collapse')}
  715. onClick={toggleCollapse}
  716. />
  717. </SidebarSection>
  718. )}
  719. </SidebarSectionGroup>
  720. )}
  721. </ExpandedContextProvider>
  722. </SidebarWrapper>
  723. );
  724. }
  725. export default Sidebar;
  726. const responsiveFlex = css`
  727. display: flex;
  728. flex-direction: column;
  729. @media (max-width: ${theme.breakpoints.medium}) {
  730. flex-direction: row;
  731. }
  732. `;
  733. export const SidebarWrapper = styled('nav')<{collapsed: boolean; hasNewNav?: boolean}>`
  734. background: ${p => p.theme.sidebarGradient};
  735. color: ${p => p.theme.sidebar.color};
  736. line-height: 1;
  737. padding: 12px 0 2px; /* Allows for 32px avatars */
  738. width: ${p =>
  739. p.theme.sidebar[
  740. p.hasNewNav
  741. ? 'semiCollapsedWidth'
  742. : p.collapsed
  743. ? 'collapsedWidth'
  744. : 'expandedWidth'
  745. ]};
  746. position: fixed;
  747. top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  748. left: 0;
  749. bottom: 0;
  750. justify-content: space-between;
  751. z-index: ${p => p.theme.zIndex.sidebar};
  752. border-right: solid 1px ${p => p.theme.sidebarBorder};
  753. ${responsiveFlex};
  754. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  755. top: 0;
  756. left: 0;
  757. right: 0;
  758. height: ${p => p.theme.sidebar.mobileHeight};
  759. bottom: auto;
  760. width: auto;
  761. padding: 0 ${space(1)};
  762. align-items: center;
  763. border-right: none;
  764. border-bottom: solid 1px ${p => p.theme.sidebarBorder};
  765. }
  766. `;
  767. const SidebarSectionGroup = styled('div')<{hasNewNav?: boolean}>`
  768. ${responsiveFlex};
  769. flex-shrink: 0; /* prevents shrinking on Safari */
  770. gap: 1px;
  771. ${p => p.hasNewNav && `align-items: center;`}
  772. `;
  773. const SidebarSectionGroupPrimary = styled('div')`
  774. ${responsiveFlex};
  775. /* necessary for child flexing on msedge and ff */
  776. min-height: 0;
  777. min-width: 0;
  778. flex: 1;
  779. /* expand to fill the entire height on mobile */
  780. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  781. height: 100%;
  782. align-items: center;
  783. }
  784. `;
  785. const PrimaryItems = styled('div')`
  786. overflow-y: auto;
  787. overflow-x: hidden;
  788. flex: 1;
  789. display: flex;
  790. flex-direction: column;
  791. gap: 1px;
  792. -ms-overflow-style: -ms-autohiding-scrollbar;
  793. scrollbar-color: ${p => p.theme.sidebar.scrollbarThumbColor}
  794. ${p => p.theme.sidebar.scrollbarColorTrack};
  795. scrollbar-width: ${p => p.theme.sidebar.scrollbarWidth};
  796. @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
  797. border-bottom: 1px solid ${p => p.theme.sidebarBorder};
  798. padding-bottom: ${space(1)};
  799. box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
  800. }
  801. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  802. overflow-y: hidden;
  803. overflow-x: auto;
  804. flex-direction: row;
  805. height: 100%;
  806. align-items: center;
  807. border-right: 1px solid ${p => p.theme.sidebarBorder};
  808. padding-right: ${space(1)};
  809. margin-right: ${space(0.5)};
  810. box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset;
  811. ::-webkit-scrollbar {
  812. display: none;
  813. }
  814. }
  815. `;
  816. const SubitemDot = styled('div')<{collapsed: boolean}>`
  817. width: 3px;
  818. height: 3px;
  819. background: currentcolor;
  820. border-radius: 50%;
  821. opacity: ${p => (p.collapsed ? 1 : 0)};
  822. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  823. opacity: 1;
  824. }
  825. `;
  826. const SidebarSection = styled(SidebarSectionGroup)<{
  827. hasNewNav?: boolean;
  828. noMargin?: boolean;
  829. noPadding?: boolean;
  830. }>`
  831. ${p => !p.noMargin && !p.hasNewNav && `margin: ${space(1)} 0`};
  832. ${p => !p.noPadding && !p.hasNewNav && `padding: 0 ${space(2)}`};
  833. @media (max-width: ${p => p.theme.breakpoints.small}) {
  834. margin: 0;
  835. padding: 0;
  836. }
  837. ${p =>
  838. p.hasNewNav &&
  839. css`
  840. @media (max-width: ${p.theme.breakpoints.medium}) {
  841. margin: 0;
  842. padding: 0;
  843. }
  844. `}
  845. &:empty {
  846. display: none;
  847. }
  848. `;
  849. const DropdownSidebarSection = styled(SidebarSection)<{
  850. hasNewNav?: boolean;
  851. isSuperuser?: boolean;
  852. }>`
  853. position: relative;
  854. margin: 0;
  855. padding: ${space(1)} ${space(2)};
  856. ${p =>
  857. p.isSuperuser &&
  858. css`
  859. &:before {
  860. content: '';
  861. position: absolute;
  862. inset: 0 ${space(1)};
  863. border-radius: ${p.theme.borderRadius};
  864. background: ${p.theme.superuserSidebar};
  865. }
  866. `}
  867. ${p => p.hasNewNav && `align-items: center;`}
  868. `;
  869. const SidebarCollapseItem = styled(SidebarItem)`
  870. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  871. display: none;
  872. }
  873. `;