index.tsx 26 KB

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