index.tsx 26 KB

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