index.tsx 23 KB

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