index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965
  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}/performance/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. isNew
  561. exact={!shouldAccordionFloat}
  562. >
  563. {requests}
  564. {queries}
  565. {resources}
  566. {appStarts}
  567. {screenLoads}
  568. {webVitals}
  569. {caches}
  570. {queues}
  571. {mobileUI}
  572. {llmMonitoring}
  573. </SidebarAccordion>
  574. </Feature>
  575. );
  576. const explore = (
  577. <SidebarAccordion
  578. {...sidebarItemProps}
  579. icon={<IconSearch />}
  580. label={<GuideAnchor target="explore">{t('Explore')}</GuideAnchor>}
  581. id="explore"
  582. exact={!shouldAccordionFloat}
  583. >
  584. {traces}
  585. {metrics}
  586. {profiling}
  587. {replays}
  588. {discover2}
  589. </SidebarAccordion>
  590. );
  591. return (
  592. <SidebarWrapper aria-label={t('Primary Navigation')} collapsed={collapsed}>
  593. <ExpandedContextProvider>
  594. <SidebarSectionGroupPrimary>
  595. <DropdownSidebarSection
  596. isSuperuser={showSuperuserWarning() && !isExcludedOrg()}
  597. >
  598. <SidebarDropdown orientation={orientation} collapsed={collapsed} />
  599. {showSuperuserWarning() && !isExcludedOrg() && (
  600. <Hook name="component:superuser-warning" organization={organization} />
  601. )}
  602. </DropdownSidebarSection>
  603. <PrimaryItems>
  604. {hasOrganization && (
  605. <Fragment>
  606. <SidebarSection>
  607. {issues}
  608. {projects}
  609. </SidebarSection>
  610. {hasNewSidebarHierarchy && !isSelfHostedErrorsOnly && (
  611. <Fragment>
  612. <SidebarSection>
  613. {explore}
  614. {insights}
  615. </SidebarSection>
  616. <SidebarSection>
  617. {performance}
  618. {feedback}
  619. {monitors}
  620. {alerts}
  621. {dashboards}
  622. {releases}
  623. </SidebarSection>
  624. </Fragment>
  625. )}
  626. {!hasNewSidebarHierarchy && !isSelfHostedErrorsOnly && (
  627. <Fragment>
  628. <SidebarSection>
  629. {performance}
  630. {profiling}
  631. {metrics}
  632. {replays}
  633. {llmMonitoring}
  634. {feedback}
  635. {monitors}
  636. {alerts}
  637. </SidebarSection>
  638. <SidebarSection>
  639. {discover2}
  640. {dashboards}
  641. {releases}
  642. {userFeedback}
  643. </SidebarSection>
  644. </Fragment>
  645. )}
  646. {isSelfHostedErrorsOnly && (
  647. <Fragment>
  648. <SidebarSection>
  649. {alerts}
  650. {discover2}
  651. {dashboards}
  652. {releases}
  653. {userFeedback}
  654. </SidebarSection>
  655. </Fragment>
  656. )}
  657. <SidebarSection>
  658. {stats}
  659. {settings}
  660. </SidebarSection>
  661. </Fragment>
  662. )}
  663. </PrimaryItems>
  664. </SidebarSectionGroupPrimary>
  665. {hasOrganization && (
  666. <SidebarSectionGroup>
  667. <PerformanceOnboardingSidebar
  668. currentPanel={activePanel}
  669. onShowPanel={() => togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)}
  670. hidePanel={() => hidePanel('performance-sidequest')}
  671. {...sidebarItemProps}
  672. />
  673. <FeedbackOnboardingSidebar
  674. currentPanel={activePanel}
  675. onShowPanel={() => togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)}
  676. hidePanel={hidePanel}
  677. {...sidebarItemProps}
  678. />
  679. <ReplaysOnboardingSidebar
  680. currentPanel={activePanel}
  681. onShowPanel={() => togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)}
  682. hidePanel={hidePanel}
  683. {...sidebarItemProps}
  684. />
  685. <ProfilingOnboardingSidebar
  686. currentPanel={activePanel}
  687. onShowPanel={() => togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)}
  688. hidePanel={hidePanel}
  689. {...sidebarItemProps}
  690. />
  691. <MetricsOnboardingSidebar
  692. currentPanel={activePanel}
  693. onShowPanel={() => togglePanel(SidebarPanelKey.METRICS_ONBOARDING)}
  694. hidePanel={hidePanel}
  695. {...sidebarItemProps}
  696. />
  697. <SidebarSection noMargin noPadding>
  698. <OnboardingStatus
  699. org={organization}
  700. currentPanel={activePanel}
  701. onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
  702. hidePanel={hidePanel}
  703. {...sidebarItemProps}
  704. />
  705. </SidebarSection>
  706. <SidebarSection>
  707. {HookStore.get('sidebar:bottom-items').length > 0 &&
  708. HookStore.get('sidebar:bottom-items')[0]({
  709. orientation,
  710. collapsed,
  711. hasPanel,
  712. organization,
  713. })}
  714. <SidebarHelp
  715. orientation={orientation}
  716. collapsed={collapsed}
  717. hidePanel={hidePanel}
  718. organization={organization}
  719. />
  720. <Broadcasts
  721. orientation={orientation}
  722. collapsed={collapsed}
  723. currentPanel={activePanel}
  724. onShowPanel={() => togglePanel(SidebarPanelKey.BROADCASTS)}
  725. hidePanel={hidePanel}
  726. organization={organization}
  727. />
  728. <ServiceIncidents
  729. orientation={orientation}
  730. collapsed={collapsed}
  731. currentPanel={activePanel}
  732. onShowPanel={() => togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)}
  733. hidePanel={hidePanel}
  734. />
  735. </SidebarSection>
  736. {!horizontal && (
  737. <SidebarSection>
  738. <SidebarCollapseItem
  739. id="collapse"
  740. data-test-id="sidebar-collapse"
  741. {...sidebarItemProps}
  742. icon={<Chevron direction={collapsed ? 'right' : 'left'} />}
  743. label={collapsed ? t('Expand') : t('Collapse')}
  744. onClick={toggleCollapse}
  745. />
  746. </SidebarSection>
  747. )}
  748. </SidebarSectionGroup>
  749. )}
  750. </ExpandedContextProvider>
  751. </SidebarWrapper>
  752. );
  753. }
  754. export default Sidebar;
  755. const responsiveFlex = css`
  756. display: flex;
  757. flex-direction: column;
  758. @media (max-width: ${theme.breakpoints.medium}) {
  759. flex-direction: row;
  760. }
  761. `;
  762. export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
  763. background: ${p => p.theme.sidebarGradient};
  764. color: ${p => p.theme.sidebar.color};
  765. line-height: 1;
  766. padding: 12px 0 2px; /* Allows for 32px avatars */
  767. width: ${p => p.theme.sidebar[p.collapsed ? 'collapsedWidth' : 'expandedWidth']};
  768. position: fixed;
  769. top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
  770. left: 0;
  771. bottom: 0;
  772. justify-content: space-between;
  773. z-index: ${p => p.theme.zIndex.sidebar};
  774. border-right: solid 1px ${p => p.theme.sidebarBorder};
  775. ${responsiveFlex};
  776. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  777. top: 0;
  778. left: 0;
  779. right: 0;
  780. height: ${p => p.theme.sidebar.mobileHeight};
  781. bottom: auto;
  782. width: auto;
  783. padding: 0 ${space(1)};
  784. align-items: center;
  785. border-right: none;
  786. border-bottom: solid 1px ${p => p.theme.sidebarBorder};
  787. }
  788. `;
  789. const SidebarSectionGroup = styled('div')`
  790. ${responsiveFlex};
  791. flex-shrink: 0; /* prevents shrinking on Safari */
  792. gap: 1px;
  793. `;
  794. const SidebarSectionGroupPrimary = styled('div')`
  795. ${responsiveFlex};
  796. /* necessary for child flexing on msedge and ff */
  797. min-height: 0;
  798. min-width: 0;
  799. flex: 1;
  800. /* expand to fill the entire height on mobile */
  801. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  802. height: 100%;
  803. align-items: center;
  804. }
  805. `;
  806. const PrimaryItems = styled('div')`
  807. overflow: auto;
  808. flex: 1;
  809. display: flex;
  810. flex-direction: column;
  811. gap: 1px;
  812. -ms-overflow-style: -ms-autohiding-scrollbar;
  813. @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
  814. border-bottom: 1px solid ${p => p.theme.sidebarBorder};
  815. padding-bottom: ${space(1)};
  816. box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
  817. }
  818. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  819. overflow-y: visible;
  820. flex-direction: row;
  821. height: 100%;
  822. align-items: center;
  823. border-right: 1px solid ${p => p.theme.sidebarBorder};
  824. padding-right: ${space(1)};
  825. margin-right: ${space(0.5)};
  826. box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset;
  827. ::-webkit-scrollbar {
  828. display: none;
  829. }
  830. }
  831. `;
  832. const SubitemDot = styled('div')<{collapsed: boolean}>`
  833. width: 3px;
  834. height: 3px;
  835. background: currentcolor;
  836. border-radius: 50%;
  837. opacity: ${p => (p.collapsed ? 1 : 0)};
  838. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  839. opacity: 1;
  840. }
  841. `;
  842. const SidebarSection = styled(SidebarSectionGroup)<{
  843. noMargin?: boolean;
  844. noPadding?: boolean;
  845. }>`
  846. ${p => !p.noMargin && `margin: ${space(1)} 0`};
  847. ${p => !p.noPadding && `padding: 0 ${space(2)}`};
  848. @media (max-width: ${p => p.theme.breakpoints.small}) {
  849. margin: 0;
  850. padding: 0;
  851. }
  852. &:empty {
  853. display: none;
  854. }
  855. `;
  856. const DropdownSidebarSection = styled(SidebarSection)<{
  857. isSuperuser?: boolean;
  858. }>`
  859. position: relative;
  860. margin: 0;
  861. padding: ${space(1)} ${space(2)};
  862. ${p =>
  863. p.isSuperuser &&
  864. css`
  865. &:before {
  866. content: '';
  867. position: absolute;
  868. inset: 0 ${space(1)};
  869. border-radius: ${p.theme.borderRadius};
  870. background: ${p.theme.superuserSidebar};
  871. }
  872. `}
  873. `;
  874. const SidebarCollapseItem = styled(SidebarItem)`
  875. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  876. display: none;
  877. }
  878. `;