index.tsx 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  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 FeatureFlagOnboardingSidebar from 'sentry/components/events/featureFlags/featureFlagOnboardingSidebar';
  9. import FeedbackOnboardingSidebar from 'sentry/components/feedback/feedbackOnboarding/sidebar';
  10. import Hook from 'sentry/components/hook';
  11. import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
  12. import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
  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 {OnboardingStatus} from 'sentry/components/sidebar/onboardingStatus';
  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. IconTelescope,
  35. IconTimer,
  36. } from 'sentry/icons';
  37. import {t} from 'sentry/locale';
  38. import ConfigStore from 'sentry/stores/configStore';
  39. import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore';
  40. import HookStore from 'sentry/stores/hookStore';
  41. import PreferencesStore from 'sentry/stores/preferencesStore';
  42. import SidebarPanelStore from 'sentry/stores/sidebarPanelStore';
  43. import {useLegacyStore} from 'sentry/stores/useLegacyStore';
  44. import {space} from 'sentry/styles/space';
  45. import type {Organization} from 'sentry/types/organization';
  46. import {isDemoModeEnabled} from 'sentry/utils/demoMode';
  47. import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
  48. import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
  49. import {hasCustomMetrics} from 'sentry/utils/metrics/features';
  50. import theme from 'sentry/utils/theme';
  51. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  52. import {useLocation} from 'sentry/utils/useLocation';
  53. import useMedia from 'sentry/utils/useMedia';
  54. import useOrganization from 'sentry/utils/useOrganization';
  55. import usePageFilters from 'sentry/utils/usePageFilters';
  56. import useProjects from 'sentry/utils/useProjects';
  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 MetricsOnboardingSidebar from 'sentry/views/metrics/ddmOnboarding/sidebar';
  78. import {
  79. getPerformanceBaseUrl,
  80. platformToDomainView,
  81. } from 'sentry/views/performance/utils';
  82. import {ProfilingOnboardingSidebar} from '../profiling/profilingOnboardingSidebar';
  83. import {Broadcasts} from './broadcasts';
  84. import SidebarHelp from './help';
  85. import ServiceIncidents from './serviceIncidents';
  86. import {SidebarAccordion} from './sidebarAccordion';
  87. import SidebarDropdown from './sidebarDropdown';
  88. import SidebarItem from './sidebarItem';
  89. import type {SidebarOrientation} from './types';
  90. import {SidebarPanelKey} from './types';
  91. function activatePanel(panel: SidebarPanelKey) {
  92. SidebarPanelStore.activatePanel(panel);
  93. }
  94. function togglePanel(panel: SidebarPanelKey) {
  95. SidebarPanelStore.togglePanel(panel);
  96. }
  97. function hidePanel(hash?: string) {
  98. SidebarPanelStore.hidePanel(hash);
  99. }
  100. function useOpenOnboardingSidebar(organization?: Organization) {
  101. const onboardingContext = useContext(OnboardingContext);
  102. const {projects: project} = useProjects();
  103. const location = useLocation();
  104. const openOnboardingSidebar = (() => {
  105. if (location?.hash === '#welcome') {
  106. if (organization && !isDemoModeEnabled()) {
  107. const tasks = getMergedTasks({
  108. organization,
  109. projects: project,
  110. onboardingContext,
  111. });
  112. const allDisplayedTasks = tasks.filter(task => task.display);
  113. const doneTasks = allDisplayedTasks.filter(isDone);
  114. return !(doneTasks.length >= allDisplayedTasks.length);
  115. }
  116. return true;
  117. }
  118. return false;
  119. })();
  120. useEffect(() => {
  121. if (openOnboardingSidebar) {
  122. activatePanel(SidebarPanelKey.ONBOARDING_WIZARD);
  123. }
  124. }, [openOnboardingSidebar]);
  125. }
  126. function Sidebar() {
  127. const location = useLocation();
  128. const preferences = useLegacyStore(PreferencesStore);
  129. const activePanel = useLegacyStore(SidebarPanelStore);
  130. const organization = useOrganization({allowNull: true});
  131. const {shouldAccordionFloat} = useContext(ExpandedContext);
  132. const {selection} = usePageFilters();
  133. const {projects: projectList} = useProjects();
  134. const hasNewNav = organization?.features.includes('navigation-sidebar-v2');
  135. const hasOrganization = !!organization;
  136. const isSelfHostedErrorsOnly = ConfigStore.get('isSelfHostedErrorsOnly');
  137. const collapsed = hasNewNav ? true : !!preferences.collapsed;
  138. const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`);
  139. // Panel determines whether to highlight
  140. const hasPanel = !!activePanel;
  141. const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
  142. const sidebarItemProps = {
  143. orientation,
  144. collapsed,
  145. hasPanel,
  146. organization,
  147. hasNewNav,
  148. };
  149. // Avoid showing superuser UI on self-hosted instances
  150. const showSuperuserWarning = () => {
  151. return isActiveSuperuser() && !ConfigStore.get('isSelfHosted');
  152. };
  153. // Avoid showing superuser UI on certain organizations
  154. const isExcludedOrg = () => {
  155. return HookStore.get('component:superuser-warning-excluded')[0]?.(organization);
  156. };
  157. useOpenOnboardingSidebar();
  158. const toggleCollapse = useCallback(() => {
  159. if (collapsed) {
  160. showSidebar();
  161. } else {
  162. hideSidebar();
  163. }
  164. }, [collapsed]);
  165. // Close panel on any navigation
  166. useEffect(() => void hidePanel(), [location?.pathname]);
  167. // Add classname to body
  168. useEffect(() => {
  169. const bcl = document.body.classList;
  170. bcl.add('body-sidebar');
  171. return () => bcl.remove('body-sidebar');
  172. }, []);
  173. useEffect(() => {
  174. Object.values(SidebarPanelKey).forEach(key => {
  175. if (location?.hash === `#sidebar-${key}`) {
  176. togglePanel(key);
  177. }
  178. });
  179. }, [location?.hash]);
  180. // Add sidebar collapse classname to body
  181. useEffect(() => {
  182. const bcl = document.body.classList;
  183. if (collapsed) {
  184. bcl.add('collapsed');
  185. } else {
  186. bcl.remove('collapsed');
  187. }
  188. return () => bcl.remove('collapsed');
  189. }, [collapsed]);
  190. // Add sidebar hasNewNav classname to body
  191. useEffect(() => {
  192. const bcl = document.body.classList;
  193. if (hasNewNav) {
  194. bcl.add('hasNewNav');
  195. } else {
  196. bcl.remove('hasNewNav');
  197. }
  198. return () => bcl.remove('hasNewNav');
  199. }, [hasNewNav]);
  200. const sidebarAnchor = isDemoModeEnabled() ? (
  201. <GuideAnchor target="projects" disabled={!DemoWalkthroughStore.get('sidebar')}>
  202. {t('Projects')}
  203. </GuideAnchor>
  204. ) : (
  205. <GuideAnchor target="projects">{t('Projects')}</GuideAnchor>
  206. );
  207. const projects = hasOrganization && (
  208. <SidebarItem
  209. {...sidebarItemProps}
  210. index
  211. icon={<IconProject />}
  212. label={sidebarAnchor}
  213. to={`/organizations/${organization.slug}/projects/`}
  214. id="projects"
  215. />
  216. );
  217. const issues = hasOrganization && (
  218. <SidebarItem
  219. {...sidebarItemProps}
  220. icon={<IconIssues />}
  221. label={<GuideAnchor target="issues">{t('Issues')}</GuideAnchor>}
  222. to={`/organizations/${organization.slug}/issues/`}
  223. search="?referrer=sidebar"
  224. id="issues"
  225. hasNewNav={hasNewNav}
  226. />
  227. );
  228. const discover = hasOrganization && (
  229. <Feature
  230. hookName="feature-disabled:discover2-sidebar-item"
  231. features="discover-basic"
  232. organization={organization}
  233. >
  234. <SidebarItem
  235. {...sidebarItemProps}
  236. // In errors-only deploys, Discover isn't a nested link, so it needs a proper icon
  237. icon={isSelfHostedErrorsOnly ? <IconTelescope /> : <SubitemDot collapsed />}
  238. label={<GuideAnchor target="discover">{t('Discover')}</GuideAnchor>}
  239. to={getDiscoverLandingUrl(organization)}
  240. id="discover-v2"
  241. />
  242. </Feature>
  243. );
  244. const traces = hasOrganization && (
  245. <Feature features="performance-trace-explorer">
  246. <SidebarItem
  247. {...sidebarItemProps}
  248. label={<GuideAnchor target="traces">{t('Traces')}</GuideAnchor>}
  249. to={`/organizations/${organization.slug}/traces/`}
  250. id="performance-trace-explorer"
  251. icon={<SubitemDot collapsed />}
  252. />
  253. </Feature>
  254. );
  255. const logs = hasOrganization && (
  256. <Feature features="ourlogs-enabled">
  257. <SidebarItem
  258. {...sidebarItemProps}
  259. label={<GuideAnchor target="logs">{t('Logs')}</GuideAnchor>}
  260. to={`/organizations/${organization?.slug}/explore/logs/`}
  261. id="ourlogs"
  262. icon={<SubitemDot collapsed />}
  263. />
  264. </Feature>
  265. );
  266. const hasPerfLandingRemovalFlag = organization?.features.includes(
  267. 'insights-performance-landing-removal'
  268. );
  269. const view = hasPerfLandingRemovalFlag
  270. ? platformToDomainView(projectList, selection.projects)
  271. : undefined;
  272. const performance = hasOrganization && (
  273. <Feature
  274. hookName="feature-disabled:performance-sidebar-item"
  275. features="performance-view"
  276. organization={organization}
  277. >
  278. <SidebarItem
  279. {...sidebarItemProps}
  280. icon={<IconLightning />}
  281. label={
  282. <GuideAnchor target="performance">
  283. {hasNewNav ? 'Perf.' : t('Performance')}
  284. </GuideAnchor>
  285. }
  286. to={`${getPerformanceBaseUrl(organization.slug, view)}/`}
  287. id="performance"
  288. />
  289. </Feature>
  290. );
  291. const releases = hasOrganization && (
  292. <SidebarItem
  293. {...sidebarItemProps}
  294. icon={<IconReleases />}
  295. label={<GuideAnchor target="releases">{t('Releases')}</GuideAnchor>}
  296. to={`/organizations/${organization.slug}/releases/`}
  297. id="releases"
  298. />
  299. );
  300. const userFeedback = hasOrganization && (
  301. <Feature features="old-user-feedback" organization={organization}>
  302. <SidebarItem
  303. {...sidebarItemProps}
  304. icon={<IconSupport />}
  305. label={t('User Feedback')}
  306. to={`/organizations/${organization.slug}/user-feedback/`}
  307. id="user-feedback"
  308. />
  309. </Feature>
  310. );
  311. const feedback = hasOrganization && (
  312. <Feature features="user-feedback-ui" organization={organization}>
  313. <SidebarItem
  314. {...sidebarItemProps}
  315. icon={<IconMegaphone />}
  316. label={t('User Feedback')}
  317. variant="short"
  318. to={`/organizations/${organization.slug}/feedback/`}
  319. id="feedback"
  320. />
  321. </Feature>
  322. );
  323. const alerts = hasOrganization && (
  324. <SidebarItem
  325. {...sidebarItemProps}
  326. icon={<IconSiren />}
  327. label={t('Alerts')}
  328. to={`/organizations/${organization.slug}/alerts/rules/`}
  329. id="alerts"
  330. />
  331. );
  332. const monitors = hasOrganization && (
  333. <SidebarItem
  334. {...sidebarItemProps}
  335. icon={<IconTimer />}
  336. label={t('Crons')}
  337. to={`/organizations/${organization.slug}/crons/`}
  338. id="crons"
  339. />
  340. );
  341. const replays = hasOrganization && (
  342. <Feature
  343. hookName="feature-disabled:replay-sidebar-item"
  344. features="session-replay-ui"
  345. organization={organization}
  346. requireAll={false}
  347. >
  348. <SidebarItem
  349. {...sidebarItemProps}
  350. icon={<SubitemDot collapsed />}
  351. label={t('Replays')}
  352. to={`/organizations/${organization.slug}/replays/`}
  353. id="replays"
  354. />
  355. </Feature>
  356. );
  357. const metricsPath = `/organizations/${organization?.slug}/metrics/`;
  358. const metrics = hasOrganization && hasCustomMetrics(organization) && (
  359. <SidebarItem
  360. {...sidebarItemProps}
  361. icon={<SubitemDot collapsed />}
  362. label={t('Metrics')}
  363. to={metricsPath}
  364. search={location?.pathname === normalizeUrl(metricsPath) ? location.search : ''}
  365. id="metrics"
  366. badgeTitle={t(
  367. 'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
  368. )}
  369. isBeta
  370. />
  371. );
  372. const dashboards = hasOrganization && (
  373. <Feature
  374. hookName="feature-disabled:dashboards-sidebar-item"
  375. features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
  376. organization={organization}
  377. requireAll={false}
  378. >
  379. <SidebarItem
  380. {...sidebarItemProps}
  381. index
  382. icon={<IconDashboard />}
  383. label={hasNewNav ? 'Dash.' : t('Dashboards')}
  384. to={`/organizations/${organization.slug}/dashboards/`}
  385. id="customizable-dashboards"
  386. />
  387. </Feature>
  388. );
  389. const profiling = hasOrganization && (
  390. <Feature
  391. hookName="feature-disabled:profiling-sidebar-item"
  392. features="profiling"
  393. organization={organization}
  394. requireAll={false}
  395. >
  396. <SidebarItem
  397. {...sidebarItemProps}
  398. index
  399. icon={<SubitemDot collapsed />}
  400. label={t('Profiles')}
  401. to={`/organizations/${organization.slug}/profiling/`}
  402. id="profiling"
  403. />
  404. </Feature>
  405. );
  406. const stats = hasOrganization && (
  407. <SidebarItem
  408. {...sidebarItemProps}
  409. icon={<IconStats />}
  410. label={t('Stats')}
  411. to={`/organizations/${organization.slug}/stats/`}
  412. id="stats"
  413. />
  414. );
  415. const settings = hasOrganization && (
  416. <SidebarItem
  417. {...sidebarItemProps}
  418. icon={<IconSettings />}
  419. label={t('Settings')}
  420. to={`/settings/${organization.slug}/`}
  421. id="settings"
  422. />
  423. );
  424. const performanceDomains = hasOrganization && (
  425. <Feature features={['performance-view']} organization={organization}>
  426. <SidebarAccordion
  427. {...sidebarItemProps}
  428. icon={<IconGraph />}
  429. label={DOMAIN_VIEW_BASE_TITLE}
  430. id="insights-domains"
  431. initiallyExpanded
  432. exact={!shouldAccordionFloat}
  433. >
  434. <SidebarItem
  435. {...sidebarItemProps}
  436. label={FRONTEND_SIDEBAR_LABEL}
  437. to={`/organizations/${organization.slug}/${DOMAIN_VIEW_BASE_URL}/${FRONTEND_LANDING_SUB_PATH}/`}
  438. id="performance-domains-web"
  439. icon={<SubitemDot collapsed />}
  440. />
  441. <SidebarItem
  442. {...sidebarItemProps}
  443. label={BACKEND_SIDEBAR_LABEL}
  444. to={`/organizations/${organization.slug}/${DOMAIN_VIEW_BASE_URL}/${BACKEND_LANDING_SUB_PATH}/`}
  445. id="performance-domains-platform"
  446. icon={<SubitemDot collapsed />}
  447. />
  448. <SidebarItem
  449. {...sidebarItemProps}
  450. label={MOBILE_SIDEBAR_LABEL}
  451. to={`/organizations/${organization.slug}/${DOMAIN_VIEW_BASE_URL}/${MOBILE_LANDING_SUB_PATH}/`}
  452. id="performance-domains-mobile"
  453. icon={<SubitemDot collapsed />}
  454. />
  455. <SidebarItem
  456. {...sidebarItemProps}
  457. label={AI_SIDEBAR_LABEL}
  458. to={`/organizations/${organization.slug}/${DOMAIN_VIEW_BASE_URL}/${AI_LANDING_SUB_PATH}/`}
  459. id="performance-domains-ai"
  460. icon={<SubitemDot collapsed />}
  461. />
  462. </SidebarAccordion>
  463. </Feature>
  464. );
  465. // Sidebar accordion includes a secondary list of nav items
  466. // TODO: replace with a secondary panel
  467. const explore = (
  468. <SidebarAccordion
  469. {...sidebarItemProps}
  470. icon={<IconSearch />}
  471. label={<GuideAnchor target="explore">{t('Explore')}</GuideAnchor>}
  472. id="explore"
  473. exact={!shouldAccordionFloat}
  474. >
  475. {traces}
  476. {logs}
  477. {metrics}
  478. {profiling}
  479. {replays}
  480. {discover}
  481. </SidebarAccordion>
  482. );
  483. return (
  484. <SidebarWrapper
  485. aria-label={t('Primary Navigation')}
  486. collapsed={collapsed}
  487. hasNewNav={hasNewNav}
  488. >
  489. <ExpandedContextProvider>
  490. <SidebarSectionGroupPrimary>
  491. <DropdownSidebarSection
  492. isSuperuser={showSuperuserWarning() && !isExcludedOrg()}
  493. hasNewNav={hasNewNav}
  494. >
  495. <SidebarDropdown
  496. orientation={orientation}
  497. collapsed={hasNewNav || collapsed}
  498. />
  499. {showSuperuserWarning() && !isExcludedOrg() && (
  500. <Hook name="component:superuser-warning" organization={organization} />
  501. )}
  502. </DropdownSidebarSection>
  503. <PrimaryItems>
  504. {hasOrganization && (
  505. <Fragment>
  506. <SidebarSection hasNewNav={hasNewNav}>
  507. {issues}
  508. {projects}
  509. </SidebarSection>
  510. {!isSelfHostedErrorsOnly && (
  511. <Fragment>
  512. <SidebarSection hasNewNav={hasNewNav}>
  513. {explore}
  514. {performanceDomains}
  515. </SidebarSection>
  516. <SidebarSection hasNewNav={hasNewNav}>
  517. {performance}
  518. {feedback}
  519. {monitors}
  520. {alerts}
  521. {dashboards}
  522. {releases}
  523. </SidebarSection>
  524. </Fragment>
  525. )}
  526. {isSelfHostedErrorsOnly && (
  527. <Fragment>
  528. <SidebarSection hasNewNav={hasNewNav}>
  529. {alerts}
  530. {discover}
  531. {dashboards}
  532. {releases}
  533. {userFeedback}
  534. </SidebarSection>
  535. </Fragment>
  536. )}
  537. <SidebarSection hasNewNav={hasNewNav}>
  538. {stats}
  539. {settings}
  540. </SidebarSection>
  541. </Fragment>
  542. )}
  543. </PrimaryItems>
  544. </SidebarSectionGroupPrimary>
  545. {hasOrganization && (
  546. <SidebarSectionGroup hasNewNav={hasNewNav}>
  547. {/* What are the onboarding sidebars? */}
  548. <PerformanceOnboardingSidebar
  549. currentPanel={activePanel}
  550. onShowPanel={() => togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)}
  551. hidePanel={() => hidePanel('performance-sidequest')}
  552. {...sidebarItemProps}
  553. />
  554. <FeedbackOnboardingSidebar
  555. currentPanel={activePanel}
  556. onShowPanel={() => togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)}
  557. hidePanel={hidePanel}
  558. {...sidebarItemProps}
  559. />
  560. <ReplaysOnboardingSidebar
  561. currentPanel={activePanel}
  562. onShowPanel={() => togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)}
  563. hidePanel={hidePanel}
  564. {...sidebarItemProps}
  565. />
  566. <FeatureFlagOnboardingSidebar
  567. currentPanel={activePanel}
  568. onShowPanel={() => togglePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING)}
  569. hidePanel={hidePanel}
  570. {...sidebarItemProps}
  571. />
  572. <ProfilingOnboardingSidebar
  573. currentPanel={activePanel}
  574. onShowPanel={() => togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)}
  575. hidePanel={hidePanel}
  576. {...sidebarItemProps}
  577. />
  578. <MetricsOnboardingSidebar
  579. currentPanel={activePanel}
  580. onShowPanel={() => togglePanel(SidebarPanelKey.METRICS_ONBOARDING)}
  581. hidePanel={hidePanel}
  582. {...sidebarItemProps}
  583. />
  584. <SidebarSection hasNewNav={hasNewNav} noMargin noPadding>
  585. <OnboardingStatus
  586. currentPanel={activePanel}
  587. onShowPanel={() => togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
  588. hidePanel={hidePanel}
  589. {...sidebarItemProps}
  590. />
  591. </SidebarSection>
  592. <SidebarSection hasNewNav={hasNewNav} centeredItems={horizontal}>
  593. {HookStore.get('sidebar:bottom-items').length > 0 &&
  594. HookStore.get('sidebar:bottom-items')[0]!({
  595. orientation,
  596. collapsed,
  597. hasPanel,
  598. organization,
  599. })}
  600. <SidebarHelp
  601. orientation={orientation}
  602. collapsed={collapsed}
  603. hidePanel={hidePanel}
  604. organization={organization}
  605. />
  606. <Broadcasts
  607. orientation={orientation}
  608. collapsed={collapsed}
  609. currentPanel={activePanel}
  610. onShowPanel={() => togglePanel(SidebarPanelKey.BROADCASTS)}
  611. hidePanel={hidePanel}
  612. />
  613. <ServiceIncidents
  614. orientation={orientation}
  615. collapsed={collapsed}
  616. currentPanel={activePanel}
  617. onShowPanel={() => togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)}
  618. hidePanel={hidePanel}
  619. />
  620. </SidebarSection>
  621. {!horizontal && !hasNewNav && (
  622. <SidebarSection hasNewNav={hasNewNav}>
  623. <SidebarCollapseItem
  624. id="collapse"
  625. data-test-id="sidebar-collapse"
  626. {...sidebarItemProps}
  627. icon={<Chevron direction={collapsed ? 'right' : 'left'} />}
  628. label={collapsed ? t('Expand') : t('Collapse')}
  629. onClick={toggleCollapse}
  630. />
  631. </SidebarSection>
  632. )}
  633. </SidebarSectionGroup>
  634. )}
  635. </ExpandedContextProvider>
  636. </SidebarWrapper>
  637. );
  638. }
  639. export default Sidebar;
  640. const responsiveFlex = css`
  641. display: flex;
  642. flex-direction: column;
  643. @media (max-width: ${theme.breakpoints.medium}) {
  644. flex-direction: row;
  645. }
  646. `;
  647. export const SidebarWrapper = styled('nav')<{collapsed: boolean; hasNewNav?: boolean}>`
  648. background: ${p => p.theme.sidebarGradient};
  649. color: ${p => p.theme.sidebar.color};
  650. line-height: 1;
  651. padding: 12px 0 2px; /* Allows for 32px avatars */
  652. width: ${p =>
  653. p.theme.sidebar[
  654. p.hasNewNav
  655. ? 'semiCollapsedWidth'
  656. : p.collapsed
  657. ? 'collapsedWidth'
  658. : 'expandedWidth'
  659. ]};
  660. position: fixed;
  661. top: ${p => (isDemoModeEnabled() ? p.theme.demo.headerSize : 0)};
  662. left: 0;
  663. bottom: 0;
  664. justify-content: space-between;
  665. z-index: ${p => p.theme.zIndex.sidebar};
  666. border-right: solid 1px ${p => p.theme.sidebarBorder};
  667. ${responsiveFlex};
  668. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  669. top: 0;
  670. left: 0;
  671. right: 0;
  672. height: ${p => p.theme.sidebar.mobileHeight};
  673. bottom: auto;
  674. width: auto;
  675. padding: 0 ${space(1)};
  676. align-items: center;
  677. border-right: none;
  678. border-bottom: solid 1px ${p => p.theme.sidebarBorder};
  679. }
  680. `;
  681. const SidebarSectionGroup = styled('div')<{hasNewNav?: boolean}>`
  682. ${responsiveFlex};
  683. flex-shrink: 0; /* prevents shrinking on Safari */
  684. gap: 1px;
  685. ${p => p.hasNewNav && `align-items: center;`}
  686. `;
  687. const SidebarSectionGroupPrimary = styled('div')`
  688. ${responsiveFlex};
  689. /* necessary for child flexing on msedge and ff */
  690. min-height: 0;
  691. min-width: 0;
  692. flex: 1;
  693. /* expand to fill the entire height on mobile */
  694. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  695. height: 100%;
  696. align-items: center;
  697. }
  698. `;
  699. const PrimaryItems = styled('div')`
  700. overflow-y: auto;
  701. overflow-x: hidden;
  702. flex: 1;
  703. display: flex;
  704. flex-direction: column;
  705. gap: 1px;
  706. -ms-overflow-style: -ms-autohiding-scrollbar;
  707. scrollbar-color: ${p => p.theme.sidebar.scrollbarThumbColor}
  708. ${p => p.theme.sidebar.scrollbarColorTrack};
  709. scrollbar-width: ${p => p.theme.sidebar.scrollbarWidth};
  710. @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
  711. border-bottom: 1px solid ${p => p.theme.sidebarBorder};
  712. padding-bottom: ${space(1)};
  713. box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
  714. }
  715. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  716. overflow-y: hidden;
  717. overflow-x: auto;
  718. flex-direction: row;
  719. height: 100%;
  720. align-items: center;
  721. border-right: 1px solid ${p => p.theme.sidebarBorder};
  722. padding-right: ${space(1)};
  723. margin-right: ${space(0.5)};
  724. box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset;
  725. ::-webkit-scrollbar {
  726. display: none;
  727. }
  728. }
  729. `;
  730. const SubitemDot = styled('div')<{collapsed: boolean}>`
  731. width: 3px;
  732. height: 3px;
  733. background: currentcolor;
  734. border-radius: 50%;
  735. opacity: ${p => (p.collapsed ? 1 : 0)};
  736. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  737. opacity: 1;
  738. }
  739. `;
  740. const SidebarSection = styled(SidebarSectionGroup)<{
  741. centeredItems?: boolean;
  742. hasNewNav?: boolean;
  743. noMargin?: boolean;
  744. noPadding?: boolean;
  745. }>`
  746. ${p => !p.noMargin && !p.hasNewNav && `margin: ${space(1)} 0`};
  747. ${p => !p.noPadding && !p.hasNewNav && `padding: 0 ${space(2)}`};
  748. @media (max-width: ${p => p.theme.breakpoints.small}) {
  749. margin: 0;
  750. padding: 0;
  751. }
  752. ${p =>
  753. p.hasNewNav &&
  754. css`
  755. @media (max-width: ${p.theme.breakpoints.medium}) {
  756. margin: 0;
  757. padding: 0;
  758. }
  759. `}
  760. ${p =>
  761. p.centeredItems &&
  762. css`
  763. align-items: center;
  764. `}
  765. &:empty {
  766. display: none;
  767. }
  768. `;
  769. const DropdownSidebarSection = styled(SidebarSection)<{
  770. hasNewNav?: boolean;
  771. isSuperuser?: boolean;
  772. }>`
  773. position: relative;
  774. margin: 0;
  775. padding: ${space(1)} ${space(2)};
  776. ${p =>
  777. p.isSuperuser &&
  778. css`
  779. &:before {
  780. content: '';
  781. position: absolute;
  782. inset: 0 ${space(1)};
  783. border-radius: ${p.theme.borderRadius};
  784. background: ${p.theme.superuserSidebar};
  785. }
  786. `}
  787. ${p => p.hasNewNav && `align-items: center;`}
  788. `;
  789. const SidebarCollapseItem = styled(SidebarItem)`
  790. @media (max-width: ${p => p.theme.breakpoints.medium}) {
  791. display: none;
  792. }
  793. `;