import {Fragment, useEffect} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {Location} from 'history'; import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences'; import Feature from 'sentry/components/acl/feature'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import HookOrDefault from 'sentry/components/hookOrDefault'; import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar'; import { IconChevron, IconDashboard, IconIssues, IconLab, IconLightning, IconList, IconPlay, IconProject, IconReleases, IconSettings, IconSiren, IconSpan, IconStats, IconSupport, IconTelescope, } from 'sentry/icons'; import {t} from 'sentry/locale'; import ConfigStore from 'sentry/stores/configStore'; import HookStore from 'sentry/stores/hookStore'; import PreferencesStore from 'sentry/stores/preferencesStore'; import SidebarPanelStore from 'sentry/stores/sidebarPanelStore'; import {useLegacyStore} from 'sentry/stores/useLegacyStore'; import space from 'sentry/styles/space'; import {Organization} from 'sentry/types'; import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls'; import theme from 'sentry/utils/theme'; import useMedia from 'sentry/utils/useMedia'; import Broadcasts from './broadcasts'; import SidebarHelp from './help'; import OnboardingStatus from './onboardingStatus'; import ServiceIncidents from './serviceIncidents'; import SidebarDropdown from './sidebarDropdown'; import SidebarItem from './sidebarItem'; import {SidebarOrientation, SidebarPanelKey} from './types'; const SidebarOverride = HookOrDefault({ hookName: 'sidebar:item-override', defaultComponent: ({children}) => {children({})}, }); type Props = { location?: Location; organization?: Organization; }; function activatePanel(panel: SidebarPanelKey) { SidebarPanelStore.activatePanel(panel); } function togglePanel(panel: SidebarPanelKey) { SidebarPanelStore.togglePanel(panel); } function hidePanel() { SidebarPanelStore.hidePanel(); } function Sidebar({location, organization}: Props) { const config = useLegacyStore(ConfigStore); const preferences = useLegacyStore(PreferencesStore); const activePanel = useLegacyStore(SidebarPanelStore); const collapsed = !!preferences.collapsed; const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`); const toggleCollapse = () => { const action = collapsed ? showSidebar : hideSidebar; action(); }; const bcl = document.body.classList; // Close panel on any navigation useEffect(() => void hidePanel(), [location?.pathname]); // Add classname to body useEffect(() => { bcl.add('body-sidebar'); return () => bcl.remove('body-sidebar'); }, [bcl]); // Add sidebar collapse classname to body useEffect(() => { if (collapsed) { bcl.add('collapsed'); } else { bcl.remove('collapsed'); } return () => bcl.remove('collapsed'); }, [collapsed, bcl]); // Trigger panels depending on the location hash useEffect(() => { if (location?.hash === '#welcome') { activatePanel(SidebarPanelKey.OnboardingWizard); } }, [location?.hash]); const hasPanel = !!activePanel; const hasOrganization = !!organization; const orientation: SidebarOrientation = horizontal ? 'top' : 'left'; const sidebarItemProps = { orientation, collapsed, hasPanel, organization, }; const projects = hasOrganization && ( } label={{t('Projects')}} to={`/organizations/${organization.slug}/projects/`} id="projects" /> ); const issues = hasOrganization && ( } label={{t('Issues')}} to={`/organizations/${organization.slug}/issues/`} id="issues" /> ); const discover2 = hasOrganization && ( } label={{t('Discover')}} to={getDiscoverLandingUrl(organization)} id="discover-v2" /> ); const performance = hasOrganization && ( {(overideProps: Partial>) => ( } label={{t('Performance')}} to={`/organizations/${organization.slug}/performance/`} id="performance" {...overideProps} /> )} ); const releases = hasOrganization && ( } label={{t('Releases')}} to={`/organizations/${organization.slug}/releases/`} id="releases" /> ); const userFeedback = hasOrganization && ( } label={t('User Feedback')} to={`/organizations/${organization.slug}/user-feedback/`} id="user-feedback" /> ); const alerts = hasOrganization && ( } label={t('Alerts')} to={`/organizations/${organization.slug}/alerts/rules/`} id="alerts" /> ); const monitors = hasOrganization && ( } label={t('Monitors')} to={`/organizations/${organization.slug}/monitors/`} id="monitors" /> ); const replays = hasOrganization && ( } label={t('Replays')} to={`/organizations/${organization.slug}/replays/`} id="replays" /> ); const dashboards = hasOrganization && ( } label={t('Dashboards')} to={`/organizations/${organization.slug}/dashboards/`} id="customizable-dashboards" /> ); const profiling = hasOrganization && ( } label={t('Profiling')} to={`/organizations/${organization.slug}/profiling/`} id="profiling" isAlpha /> ); const activity = hasOrganization && ( } label={t('Activity')} to={`/organizations/${organization.slug}/activity/`} id="activity" /> ); const stats = hasOrganization && ( } label={t('Stats')} to={`/organizations/${organization.slug}/stats/`} id="stats" /> ); const settings = hasOrganization && ( } label={t('Settings')} to={`/settings/${organization.slug}/`} id="settings" /> ); return ( {hasOrganization && ( {projects} {issues} {performance} {releases} {userFeedback} {alerts} {discover2} {dashboards} {profiling} {replays} {monitors} {activity} {stats} {settings} )} {hasOrganization && ( togglePanel(SidebarPanelKey.PerformanceOnboarding)} hidePanel={hidePanel} {...sidebarItemProps} /> togglePanel(SidebarPanelKey.OnboardingWizard)} hidePanel={hidePanel} {...sidebarItemProps} /> {HookStore.get('sidebar:bottom-items').length > 0 && HookStore.get('sidebar:bottom-items')[0]({ orientation, collapsed, hasPanel, organization, })} togglePanel(SidebarPanelKey.Broadcasts)} hidePanel={hidePanel} organization={organization} /> togglePanel(SidebarPanelKey.ServiceIncidents)} hidePanel={hidePanel} /> {!horizontal && ( } label={collapsed ? t('Expand') : t('Collapse')} onClick={toggleCollapse} /> )} )} ); } export default Sidebar; const responsiveFlex = css` display: flex; flex-direction: column; @media (max-width: ${theme.breakpoints.medium}) { flex-direction: row; } `; export const SidebarWrapper = styled('nav')<{collapsed: boolean}>` background: ${p => p.theme.sidebarGradient}; color: ${p => p.theme.sidebar.color}; line-height: 1; padding: 12px 0 2px; /* Allows for 32px avatars */ width: ${p => p.theme.sidebar[p.collapsed ? 'collapsedWidth' : 'expandedWidth']}; position: fixed; top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)}; left: 0; bottom: 0; justify-content: space-between; z-index: ${p => p.theme.zIndex.sidebar}; border-right: solid 1px ${p => p.theme.sidebarBorder}; ${responsiveFlex}; @media (max-width: ${p => p.theme.breakpoints.medium}) { top: 0; left: 0; right: 0; height: ${p => p.theme.sidebar.mobileHeight}; bottom: auto; width: auto; padding: 0 ${space(1)}; align-items: center; border-right: none; border-bottom: solid 1px ${p => p.theme.sidebarBorder}; } `; const SidebarSectionGroup = styled('div')` ${responsiveFlex}; flex-shrink: 0; /* prevents shrinking on Safari */ `; const SidebarSectionGroupPrimary = styled('div')` ${responsiveFlex}; /* necessary for child flexing on msedge and ff */ min-height: 0; min-width: 0; flex: 1; /* expand to fill the entire height on mobile */ @media (max-width: ${p => p.theme.breakpoints.medium}) { height: 100%; align-items: center; } `; const PrimaryItems = styled('div')` overflow: auto; flex: 1; display: flex; flex-direction: column; -ms-overflow-style: -ms-autohiding-scrollbar; @media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) { border-bottom: 1px solid ${p => p.theme.gray400}; padding-bottom: ${space(1)}; box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset; &::-webkit-scrollbar { background-color: transparent; width: 8px; } &::-webkit-scrollbar-thumb { background: ${p => p.theme.gray400}; border-radius: 8px; } } @media (max-width: ${p => p.theme.breakpoints.medium}) { overflow-y: visible; flex-direction: row; height: 100%; align-items: center; border-right: 1px solid ${p => p.theme.gray400}; padding-right: ${space(1)}; margin-right: ${space(0.5)}; box-shadow: rgba(0, 0, 0, 0.15) -10px 0px 10px inset; ::-webkit-scrollbar { display: none; } } `; const SidebarSection = styled(SidebarSectionGroup)<{ noMargin?: boolean; noPadding?: boolean; }>` ${p => !p.noMargin && `margin: ${space(1)} 0`}; ${p => !p.noPadding && 'padding: 0 19px'}; @media (max-width: ${p => p.theme.breakpoints.small}) { margin: 0; padding: 0; } &:empty { display: none; } `; const ExpandedIcon = css` transition: 0.3s transform ease; transform: rotate(270deg); `; const CollapsedIcon = css` transform: rotate(90deg); `; const StyledIconChevron = styled(({collapsed, ...props}) => ( ))``; const SidebarCollapseItem = styled(SidebarItem)` @media (max-width: ${p => p.theme.breakpoints.medium}) { display: none; } `;