index.tsx 31 KB

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