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;
}
`;