index.tsx 28 KB

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