index.tsx 23 KB

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