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 MetricsOnboardingSidebar from 'sentry/views/metrics/ddmOnboarding/sidebar';
  58. import {releaseLevelAsBadgeProps as CacheModuleBadgeProps} from 'sentry/views/performance/cache/settings';
  59. import {releaseLevelAsBadgeProps as QueuesModuleBadgeProps} from 'sentry/views/performance/queues/settings';
  60. import {
  61. MODULE_TITLES,
  62. useModuleTitle,
  63. } from 'sentry/views/performance/utils/useModuleTitle';
  64. import {useModuleURLBuilder} from 'sentry/views/performance/utils/useModuleURL';
  65. import {ModuleName} from 'sentry/views/starfish/types';
  66. import {ProfilingOnboardingSidebar} from '../profiling/ProfilingOnboarding/profilingOnboardingSidebar';
  67. import Broadcasts from './broadcasts';
  68. import SidebarHelp from './help';
  69. import OnboardingStatus from './onboardingStatus';
  70. import ServiceIncidents from './serviceIncidents';
  71. import {SidebarAccordion} from './sidebarAccordion';
  72. import SidebarDropdown from './sidebarDropdown';
  73. import SidebarItem from './sidebarItem';
  74. import type {SidebarOrientation} from './types';
  75. import {SidebarPanelKey} from './types';
  76. function activatePanel(panel: SidebarPanelKey) {
  77. SidebarPanelStore.activatePanel(panel);
  78. }
  79. function togglePanel(panel: SidebarPanelKey) {
  80. SidebarPanelStore.togglePanel(panel);
  81. }
  82. function hidePanel(hash?: string) {
  83. SidebarPanelStore.hidePanel(hash);
  84. }
  85. function useOpenOnboardingSidebar(organization?: Organization) {
  86. const onboardingContext = useContext(OnboardingContext);
  87. const {projects: project} = useProjects();
  88. const location = useLocation();
  89. const openOnboardingSidebar = (() => {
  90. if (location?.hash === '#welcome') {
  91. if (organization && !ConfigStore.get('demoMode')) {
  92. const tasks = getMergedTasks({
  93. organization,
  94. projects: project,
  95. onboardingContext,
  96. });
  97. const allDisplayedTasks = tasks
  98. .filter(task => task.display)
  99. .filter(task => !task.renderCard);
  100. const doneTasks = allDisplayedTasks.filter(isDone);
  101. return !(doneTasks.length >= allDisplayedTasks.length);
  102. }
  103. return true;
  104. }
  105. return false;
  106. })();
  107. useEffect(() => {
  108. if (openOnboardingSidebar) {
  109. activatePanel(SidebarPanelKey.ONBOARDING_WIZARD);
  110. }
  111. }, [openOnboardingSidebar]);
  112. }
  113. function Sidebar() {
  114. const location = useLocation();
  115. const preferences = useLegacyStore(PreferencesStore);
  116. const activePanel = useLegacyStore(SidebarPanelStore);
  117. const organization = useOrganization({allowNull: true});
  118. const {shouldAccordionFloat} = useContext(ExpandedContext);
  119. const resourceModuleTitle = useModuleTitle(ModuleName.RESOURCE);
  120. const collapsed = !!preferences.collapsed;
  121. const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  122. // Avoid showing superuser UI on self-hosted instances
  123. const showSuperuserWarning = () => {
  124. return isActiveSuperuser() && !ConfigStore.get('isSelfHosted');
  125. };
  126. // Avoid showing superuser UI on certain organizations
  127. const isExcludedOrg = () => {
  128. return HookStore.get('component:superuser-warning-excluded')[0]?.(organization);
  129. };
  130. useOpenOnboardingSidebar();
  131. const toggleCollapse = useCallback(() => {
  132. if (collapsed) {
  133. showSidebar();
  134. } else {
  135. hideSidebar();
  136. }
  137. }, [collapsed]);
  138. // Close panel on any navigation
  139. useEffect(() => void hidePanel(), [location?.pathname]);
  140. // Add classname to body
  141. useEffect(() => {
  142. const bcl = document.body.classList;
  143. bcl.add('body-sidebar');
  144. return () => bcl.remove('body-sidebar');
  145. }, []);
  146. useEffect(() => {
  147. Object.values(SidebarPanelKey).forEach(key => {
  148. if (location?.hash === `#sidebar-${key}`) {
  149. togglePanel(key);
  150. }
  151. });
  152. }, [location?.hash]);
  153. // Add sidebar collapse classname to body
  154. useEffect(() => {
  155. const bcl = document.body.classList;
  156. if (collapsed) {
  157. bcl.add('collapsed');
  158. } else {
  159. bcl.remove('collapsed');
  160. }
  161. return () => bcl.remove('collapsed');
  162. }, [collapsed]);
  163. const hasPanel = !!activePanel;
  164. const hasOrganization = !!organization;
  165. const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
  166. const sidebarItemProps = {
  167. orientation,
  168. collapsed,
  169. hasPanel,
  170. organization,
  171. };
  172. // 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
  173. const hasNewSidebarHierarchy =
  174. hasOrganization && organization.features.includes('performance-insights');
  175. const sidebarAnchor = isDemoWalkthrough() ? (
  176. <GuideAnchor target="projects" disabled={!DemoWalkthroughStore.get('sidebar')}>
  177. {t('Projects')}
  178. </GuideAnchor>
  179. ) : (
  180. <GuideAnchor target="projects">{t('Projects')}</GuideAnchor>
  181. );
  182. const projects = hasOrganization && (
  183. <SidebarItem
  184. {...sidebarItemProps}
  185. index
  186. icon={<IconProject />}
  187. label={sidebarAnchor}
  188. to={`/organizations/${organization.slug}/projects/`}
  189. id="projects"
  190. />
  191. );
  192. const issues = hasOrganization && (
  193. <SidebarItem
  194. {...sidebarItemProps}
  195. icon={<IconIssues />}
  196. label={<GuideAnchor target="issues">{t('Issues')}</GuideAnchor>}
  197. to={`/organizations/${organization.slug}/issues/`}
  198. search="?referrer=sidebar"
  199. id="issues"
  200. />
  201. );
  202. const discover2 = hasOrganization && (
  203. <Feature
  204. hookName="feature-disabled:discover2-sidebar-item"
  205. features="discover-basic"
  206. organization={organization}
  207. >
  208. <SidebarItem
  209. {...sidebarItemProps}
  210. icon={hasNewSidebarHierarchy ? <SubitemDot collapsed /> : <IconTelescope />}
  211. label={<GuideAnchor target="discover">{t('Discover')}</GuideAnchor>}
  212. to={getDiscoverLandingUrl(organization)}
  213. id="discover-v2"
  214. />
  215. </Feature>
  216. );
  217. const moduleURLBuilder = useModuleURLBuilder(true);
  218. const queries = hasOrganization && (
  219. <Feature key="db" features="spans-first-ui" organization={organization}>
  220. <SidebarItem
  221. {...sidebarItemProps}
  222. label={
  223. <GuideAnchor target="performance-database">{MODULE_TITLES.db}</GuideAnchor>
  224. }
  225. to={`/organizations/${organization.slug}/${moduleURLBuilder('db')}/`}
  226. id="performance-database"
  227. icon={<SubitemDot collapsed />}
  228. />
  229. </Feature>
  230. );
  231. const requests = hasOrganization && (
  232. <Feature key="http" features="spans-first-ui" organization={organization}>
  233. <SidebarItem
  234. {...sidebarItemProps}
  235. label={<GuideAnchor target="performance-http">{MODULE_TITLES.http}</GuideAnchor>}
  236. to={`/organizations/${organization.slug}/${moduleURLBuilder('http')}/`}
  237. id="performance-http"
  238. icon={<SubitemDot collapsed />}
  239. />
  240. </Feature>
  241. );
  242. const caches = hasOrganization && (
  243. <Feature key="cache" features="performance-cache-view" organization={organization}>
  244. <SidebarItem
  245. {...sidebarItemProps}
  246. label={
  247. <GuideAnchor target="performance-cache">{MODULE_TITLES.cache}</GuideAnchor>
  248. }
  249. to={`/organizations/${organization.slug}/${moduleURLBuilder('cache')}/`}
  250. id="performance-cache"
  251. icon={<SubitemDot collapsed />}
  252. {...CacheModuleBadgeProps}
  253. />
  254. </Feature>
  255. );
  256. const webVitals = hasOrganization && (
  257. <Feature key="vital" features="spans-first-ui" organization={organization}>
  258. <SidebarItem
  259. {...sidebarItemProps}
  260. label={
  261. <GuideAnchor target="performance-webvitals">{MODULE_TITLES.vital}</GuideAnchor>
  262. }
  263. to={`/organizations/${organization.slug}/${moduleURLBuilder('vital')}/`}
  264. id="performance-webvitals"
  265. icon={<SubitemDot collapsed />}
  266. />
  267. </Feature>
  268. );
  269. const queues = hasOrganization && (
  270. <Feature key="queue" features="performance-queues-view" organization={organization}>
  271. <SidebarItem
  272. {...sidebarItemProps}
  273. label={
  274. <GuideAnchor target="performance-queues">{MODULE_TITLES.queue}</GuideAnchor>
  275. }
  276. {...QueuesModuleBadgeProps}
  277. to={`/organizations/${organization.slug}/${moduleURLBuilder('queue')}/`}
  278. id="performance-queues"
  279. icon={<SubitemDot collapsed />}
  280. />
  281. </Feature>
  282. );
  283. const screenLoads = hasOrganization && (
  284. <Feature key="screen_load" features="spans-first-ui" organization={organization}>
  285. <SidebarItem
  286. {...sidebarItemProps}
  287. label={MODULE_TITLES.screen_load}
  288. to={`/organizations/${organization.slug}/${moduleURLBuilder('screen_load')}/`}
  289. id="performance-mobile-screens"
  290. icon={<SubitemDot collapsed />}
  291. />
  292. </Feature>
  293. );
  294. const appStarts = hasOrganization && (
  295. <Feature key="app_start" features="spans-first-ui" organization={organization}>
  296. <SidebarItem
  297. {...sidebarItemProps}
  298. label={MODULE_TITLES.app_start}
  299. to={`/organizations/${organization.slug}/${moduleURLBuilder('app_start')}/`}
  300. id="performance-mobile-app-startup"
  301. icon={<SubitemDot collapsed />}
  302. />
  303. </Feature>
  304. );
  305. const mobileUI = hasOrganization && (
  306. <Feature
  307. key="mobile-ui"
  308. features={['spans-first-ui', 'starfish-mobile-ui-module']}
  309. organization={organization}
  310. >
  311. <SidebarItem
  312. {...sidebarItemProps}
  313. label={MODULE_TITLES['mobile-ui']}
  314. to={`/organizations/${organization.slug}/${moduleURLBuilder('mobile-ui')}/`}
  315. id="performance-mobile-ui"
  316. icon={<SubitemDot collapsed />}
  317. isAlpha
  318. />
  319. </Feature>
  320. );
  321. const resources = hasOrganization && (
  322. <Feature key="resource" features="spans-first-ui">
  323. <SidebarItem
  324. {...sidebarItemProps}
  325. label={<GuideAnchor target="starfish">{resourceModuleTitle}</GuideAnchor>}
  326. to={`/organizations/${organization.slug}/${moduleURLBuilder('resource')}/`}
  327. id="performance-browser-resources"
  328. icon={<SubitemDot collapsed />}
  329. />
  330. </Feature>
  331. );
  332. const traces = hasOrganization && (
  333. <Feature features="performance-trace-explorer">
  334. <SidebarItem
  335. {...sidebarItemProps}
  336. label={<GuideAnchor target="traces">{t('Traces')}</GuideAnchor>}
  337. to={`/organizations/${organization.slug}/performance/traces/`}
  338. id="performance-trace-explorer"
  339. icon={<SubitemDot collapsed />}
  340. isAlpha
  341. />
  342. </Feature>
  343. );
  344. const llmMonitoring = hasOrganization && (
  345. <Feature features="ai-analytics" organization={organization}>
  346. <SidebarItem
  347. {...sidebarItemProps}
  348. icon={hasNewSidebarHierarchy ? <SubitemDot collapsed /> : <IconRobot />}
  349. label={MODULE_TITLES.ai}
  350. isAlpha
  351. variant="short"
  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. `;