index.tsx 31 KB

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