import {Fragment, useCallback, useContext, useEffect} from 'react';
import {css} from '@emotion/react';
import styled from '@emotion/styled';
import {hideSidebar, showSidebar} from 'sentry/actionCreators/preferences';
import Feature from 'sentry/components/acl/feature';
import GuideAnchor from 'sentry/components/assistant/guideAnchor';
import {Chevron} from 'sentry/components/chevron';
import FeatureFlagOnboardingSidebar from 'sentry/components/events/featureFlags/featureFlagOnboardingSidebar';
import FeedbackOnboardingSidebar from 'sentry/components/feedback/feedbackOnboarding/sidebar';
import Hook from 'sentry/components/hook';
import {OnboardingContext} from 'sentry/components/onboarding/onboardingContext';
import {getMergedTasks} from 'sentry/components/onboardingWizard/taskConfig';
import PerformanceOnboardingSidebar from 'sentry/components/performanceOnboarding/sidebar';
import ReplaysOnboardingSidebar from 'sentry/components/replaysOnboarding/sidebar';
import {
ExpandedContext,
ExpandedContextProvider,
} from 'sentry/components/sidebar/expandedContextProvider';
import {OnboardingStatus} from 'sentry/components/sidebar/onboardingStatus';
import {isDone} from 'sentry/components/sidebar/utils';
import {
IconDashboard,
IconGraph,
IconIssues,
IconLightning,
IconMegaphone,
IconProject,
IconReleases,
IconSearch,
IconSettings,
IconSiren,
IconStats,
IconSupport,
IconTelescope,
IconTimer,
} from 'sentry/icons';
import {t} from 'sentry/locale';
import ConfigStore from 'sentry/stores/configStore';
import DemoWalkthroughStore from 'sentry/stores/demoWalkthroughStore';
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 type {Organization} from 'sentry/types/organization';
import {isDemoModeEnabled} from 'sentry/utils/demoMode';
import {getDiscoverLandingUrl} from 'sentry/utils/discover/urls';
import {isActiveSuperuser} from 'sentry/utils/isActiveSuperuser';
import {hasCustomMetrics} from 'sentry/utils/metrics/features';
import theme from 'sentry/utils/theme';
import normalizeUrl from 'sentry/utils/url/normalizeUrl';
import {useLocation} from 'sentry/utils/useLocation';
import useMedia from 'sentry/utils/useMedia';
import useOrganization from 'sentry/utils/useOrganization';
import usePageFilters from 'sentry/utils/usePageFilters';
import useProjects from 'sentry/utils/useProjects';
import {
AI_LANDING_SUB_PATH,
AI_SIDEBAR_LABEL,
} from 'sentry/views/insights/pages/ai/settings';
import {
BACKEND_LANDING_SUB_PATH,
BACKEND_SIDEBAR_LABEL,
} from 'sentry/views/insights/pages/backend/settings';
import {
FRONTEND_LANDING_SUB_PATH,
FRONTEND_SIDEBAR_LABEL,
} from 'sentry/views/insights/pages/frontend/settings';
import {
MOBILE_LANDING_SUB_PATH,
MOBILE_SIDEBAR_LABEL,
} from 'sentry/views/insights/pages/mobile/settings';
import {
DOMAIN_VIEW_BASE_TITLE,
DOMAIN_VIEW_BASE_URL,
} from 'sentry/views/insights/pages/settings';
import MetricsOnboardingSidebar from 'sentry/views/metrics/ddmOnboarding/sidebar';
import {
getPerformanceBaseUrl,
platformToDomainView,
} from 'sentry/views/performance/utils';
import {ProfilingOnboardingSidebar} from '../profiling/profilingOnboardingSidebar';
import {Broadcasts} from './broadcasts';
import SidebarHelp from './help';
import ServiceIncidents from './serviceIncidents';
import {SidebarAccordion} from './sidebarAccordion';
import SidebarDropdown from './sidebarDropdown';
import SidebarItem from './sidebarItem';
import type {SidebarOrientation} from './types';
import {SidebarPanelKey} from './types';
function activatePanel(panel: SidebarPanelKey) {
SidebarPanelStore.activatePanel(panel);
}
function togglePanel(panel: SidebarPanelKey) {
SidebarPanelStore.togglePanel(panel);
}
function hidePanel(hash?: string) {
SidebarPanelStore.hidePanel(hash);
}
function useOpenOnboardingSidebar(organization?: Organization) {
const onboardingContext = useContext(OnboardingContext);
const {projects: project} = useProjects();
const location = useLocation();
const openOnboardingSidebar = (() => {
if (location?.hash === '#welcome') {
if (organization && !isDemoModeEnabled()) {
const tasks = getMergedTasks({
organization,
projects: project,
onboardingContext,
});
const allDisplayedTasks = tasks.filter(task => task.display);
const doneTasks = allDisplayedTasks.filter(isDone);
return !(doneTasks.length >= allDisplayedTasks.length);
}
return true;
}
return false;
})();
useEffect(() => {
if (openOnboardingSidebar) {
activatePanel(SidebarPanelKey.ONBOARDING_WIZARD);
}
}, [openOnboardingSidebar]);
}
function Sidebar() {
const location = useLocation();
const preferences = useLegacyStore(PreferencesStore);
const activePanel = useLegacyStore(SidebarPanelStore);
const organization = useOrganization({allowNull: true});
const {shouldAccordionFloat} = useContext(ExpandedContext);
const {selection} = usePageFilters();
const {projects: projectList} = useProjects();
const hasNewNav = organization?.features.includes('navigation-sidebar-v2');
const hasOrganization = !!organization;
const isSelfHostedErrorsOnly = ConfigStore.get('isSelfHostedErrorsOnly');
const collapsed = hasNewNav ? true : !!preferences.collapsed;
const horizontal = useMedia(`(max-width: ${theme.breakpoints.medium})`);
// Panel determines whether to highlight
const hasPanel = !!activePanel;
const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
const sidebarItemProps = {
orientation,
collapsed,
hasPanel,
organization,
hasNewNav,
};
// Avoid showing superuser UI on self-hosted instances
const showSuperuserWarning = () => {
return isActiveSuperuser() && !ConfigStore.get('isSelfHosted');
};
// Avoid showing superuser UI on certain organizations
const isExcludedOrg = () => {
return HookStore.get('component:superuser-warning-excluded')[0]?.(organization);
};
useOpenOnboardingSidebar();
const toggleCollapse = useCallback(() => {
if (collapsed) {
showSidebar();
} else {
hideSidebar();
}
}, [collapsed]);
// Close panel on any navigation
useEffect(() => void hidePanel(), [location?.pathname]);
// Add classname to body
useEffect(() => {
const bcl = document.body.classList;
bcl.add('body-sidebar');
return () => bcl.remove('body-sidebar');
}, []);
useEffect(() => {
Object.values(SidebarPanelKey).forEach(key => {
if (location?.hash === `#sidebar-${key}`) {
togglePanel(key);
}
});
}, [location?.hash]);
// Add sidebar collapse classname to body
useEffect(() => {
const bcl = document.body.classList;
if (collapsed) {
bcl.add('collapsed');
} else {
bcl.remove('collapsed');
}
return () => bcl.remove('collapsed');
}, [collapsed]);
// Add sidebar hasNewNav classname to body
useEffect(() => {
const bcl = document.body.classList;
if (hasNewNav) {
bcl.add('hasNewNav');
} else {
bcl.remove('hasNewNav');
}
return () => bcl.remove('hasNewNav');
}, [hasNewNav]);
const sidebarAnchor = isDemoModeEnabled() ? (
{t('Projects')}
) : (
{t('Projects')}
);
const projects = hasOrganization && (
}
label={sidebarAnchor}
to={`/organizations/${organization.slug}/projects/`}
id="projects"
/>
);
const issues = hasOrganization && (
}
label={{t('Issues')}}
to={`/organizations/${organization.slug}/issues/`}
search="?referrer=sidebar"
id="issues"
hasNewNav={hasNewNav}
/>
);
const discover = hasOrganization && (
: }
label={{t('Discover')}}
to={getDiscoverLandingUrl(organization)}
id="discover-v2"
/>
);
const traces = hasOrganization && (
{t('Traces')}}
to={`/organizations/${organization.slug}/traces/`}
id="performance-trace-explorer"
icon={}
/>
);
const logs = hasOrganization && (
{t('Logs')}}
to={`/organizations/${organization?.slug}/explore/logs/`}
id="ourlogs"
icon={}
/>
);
const hasPerfLandingRemovalFlag = organization?.features.includes(
'insights-performance-landing-removal'
);
const view = hasPerfLandingRemovalFlag
? platformToDomainView(projectList, selection.projects)
: undefined;
const performance = hasOrganization && (
}
label={
{hasNewNav ? 'Perf.' : t('Performance')}
}
to={`${getPerformanceBaseUrl(organization.slug, view)}/`}
id="performance"
/>
);
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 feedback = hasOrganization && (
}
label={t('User Feedback')}
variant="short"
to={`/organizations/${organization.slug}/feedback/`}
id="feedback"
/>
);
const alerts = hasOrganization && (
}
label={t('Alerts')}
to={`/organizations/${organization.slug}/alerts/rules/`}
id="alerts"
/>
);
const monitors = hasOrganization && (
}
label={t('Crons')}
to={`/organizations/${organization.slug}/crons/`}
id="crons"
/>
);
const replays = hasOrganization && (
}
label={t('Replays')}
to={`/organizations/${organization.slug}/replays/`}
id="replays"
/>
);
const metricsPath = `/organizations/${organization?.slug}/metrics/`;
const metrics = hasOrganization && hasCustomMetrics(organization) && (
}
label={t('Metrics')}
to={metricsPath}
search={location?.pathname === normalizeUrl(metricsPath) ? location.search : ''}
id="metrics"
badgeTitle={t(
'The Metrics beta will end and we will retire the current solution on October 7th, 2024'
)}
isBeta
/>
);
const dashboards = hasOrganization && (
}
label={hasNewNav ? 'Dash.' : t('Dashboards')}
to={`/organizations/${organization.slug}/dashboards/`}
id="customizable-dashboards"
/>
);
const profiling = hasOrganization && (
}
label={t('Profiles')}
to={`/organizations/${organization.slug}/profiling/`}
id="profiling"
/>
);
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"
/>
);
const performanceDomains = hasOrganization && (
}
label={DOMAIN_VIEW_BASE_TITLE}
id="insights-domains"
initiallyExpanded
exact={!shouldAccordionFloat}
>
}
/>
}
/>
}
/>
}
/>
);
// Sidebar accordion includes a secondary list of nav items
// TODO: replace with a secondary panel
const explore = (
}
label={{t('Explore')}}
id="explore"
exact={!shouldAccordionFloat}
>
{traces}
{logs}
{metrics}
{profiling}
{replays}
{discover}
);
return (
{showSuperuserWarning() && !isExcludedOrg() && (
)}
{hasOrganization && (
{issues}
{projects}
{!isSelfHostedErrorsOnly && (
{explore}
{performanceDomains}
{performance}
{feedback}
{monitors}
{alerts}
{dashboards}
{releases}
)}
{isSelfHostedErrorsOnly && (
{alerts}
{discover}
{dashboards}
{releases}
{userFeedback}
)}
{stats}
{settings}
)}
{hasOrganization && (
{/* What are the onboarding sidebars? */}
togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)}
hidePanel={() => hidePanel('performance-sidequest')}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.FEEDBACK_ONBOARDING)}
hidePanel={hidePanel}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.REPLAYS_ONBOARDING)}
hidePanel={hidePanel}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.FEATURE_FLAG_ONBOARDING)}
hidePanel={hidePanel}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.PROFILING_ONBOARDING)}
hidePanel={hidePanel}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.METRICS_ONBOARDING)}
hidePanel={hidePanel}
{...sidebarItemProps}
/>
togglePanel(SidebarPanelKey.ONBOARDING_WIZARD)}
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}
/>
togglePanel(SidebarPanelKey.SERVICE_INCIDENTS)}
hidePanel={hidePanel}
/>
{!horizontal && !hasNewNav && (
}
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; hasNewNav?: 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.hasNewNav
? 'semiCollapsedWidth'
: p.collapsed
? 'collapsedWidth'
: 'expandedWidth'
]};
position: fixed;
top: ${p => (isDemoModeEnabled() ? 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')<{hasNewNav?: boolean}>`
${responsiveFlex};
flex-shrink: 0; /* prevents shrinking on Safari */
gap: 1px;
${p => p.hasNewNav && `align-items: center;`}
`;
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-y: auto;
overflow-x: hidden;
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
-ms-overflow-style: -ms-autohiding-scrollbar;
scrollbar-color: ${p => p.theme.sidebar.scrollbarThumbColor}
${p => p.theme.sidebar.scrollbarColorTrack};
scrollbar-width: ${p => p.theme.sidebar.scrollbarWidth};
@media (max-height: 675px) and (min-width: ${p => p.theme.breakpoints.medium}) {
border-bottom: 1px solid ${p => p.theme.sidebarBorder};
padding-bottom: ${space(1)};
box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
}
@media (max-width: ${p => p.theme.breakpoints.medium}) {
overflow-y: hidden;
overflow-x: auto;
flex-direction: row;
height: 100%;
align-items: center;
border-right: 1px solid ${p => p.theme.sidebarBorder};
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 SubitemDot = styled('div')<{collapsed: boolean}>`
width: 3px;
height: 3px;
background: currentcolor;
border-radius: 50%;
opacity: ${p => (p.collapsed ? 1 : 0)};
@media (max-width: ${p => p.theme.breakpoints.medium}) {
opacity: 1;
}
`;
const SidebarSection = styled(SidebarSectionGroup)<{
centeredItems?: boolean;
hasNewNav?: boolean;
noMargin?: boolean;
noPadding?: boolean;
}>`
${p => !p.noMargin && !p.hasNewNav && `margin: ${space(1)} 0`};
${p => !p.noPadding && !p.hasNewNav && `padding: 0 ${space(2)}`};
@media (max-width: ${p => p.theme.breakpoints.small}) {
margin: 0;
padding: 0;
}
${p =>
p.hasNewNav &&
css`
@media (max-width: ${p.theme.breakpoints.medium}) {
margin: 0;
padding: 0;
}
`}
${p =>
p.centeredItems &&
css`
align-items: center;
`}
&:empty {
display: none;
}
`;
const DropdownSidebarSection = styled(SidebarSection)<{
hasNewNav?: boolean;
isSuperuser?: boolean;
}>`
position: relative;
margin: 0;
padding: ${space(1)} ${space(2)};
${p =>
p.isSuperuser &&
css`
&:before {
content: '';
position: absolute;
inset: 0 ${space(1)};
border-radius: ${p.theme.borderRadius};
background: ${p.theme.superuserSidebar};
}
`}
${p => p.hasNewNav && `align-items: center;`}
`;
const SidebarCollapseItem = styled(SidebarItem)`
@media (max-width: ${p => p.theme.breakpoints.medium}) {
display: none;
}
`;