index.tsx 29 KB

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