123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706 |
- import * as React from 'react';
- import {browserHistory} from 'react-router';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import {Location} from 'history';
- import isEqual from 'lodash/isEqual';
- import * as queryString from 'query-string';
- import {hideSidebar, showSidebar} from 'app/actionCreators/preferences';
- import SidebarPanelActions from 'app/actions/sidebarPanelActions';
- import Feature from 'app/components/acl/feature';
- import GuideAnchor from 'app/components/assistant/guideAnchor';
- import HookOrDefault from 'app/components/hookOrDefault';
- import {extractSelectionParameters} from 'app/components/organizations/globalSelectionHeader/utils';
- import {
- IconActivity,
- IconChevron,
- IconGraph,
- IconIssues,
- IconLab,
- IconLightning,
- IconProject,
- IconReleases,
- IconSettings,
- IconSiren,
- IconStats,
- IconSupport,
- IconTelescope,
- } from 'app/icons';
- import {t} from 'app/locale';
- import ConfigStore from 'app/stores/configStore';
- import HookStore from 'app/stores/hookStore';
- import PreferencesStore from 'app/stores/preferencesStore';
- import SidebarPanelStore from 'app/stores/sidebarPanelStore';
- import space from 'app/styles/space';
- import {Organization} from 'app/types';
- import {getDiscoverLandingUrl} from 'app/utils/discover/urls';
- import theme from 'app/utils/theme';
- import withOrganization from 'app/utils/withOrganization';
- 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}) => <React.Fragment>{children({})}</React.Fragment>,
- });
- type ActivePanelType = SidebarPanelKey | '';
- type Props = {
- organization: Organization;
- activePanel: ActivePanelType;
- collapsed: boolean;
- location?: Location;
- children?: never;
- };
- type State = {
- horizontal: boolean;
- };
- class Sidebar extends React.Component<Props, State> {
- constructor(props: Props) {
- super(props);
- if (!window.matchMedia) {
- return;
- }
- // TODO(billy): We should consider moving this into a component
- this.mq = window.matchMedia(`(max-width: ${theme.breakpoints[1]})`);
- this.mq.addListener(this.handleMediaQueryChange);
- this.state.horizontal = this.mq.matches;
- }
- state: State = {
- horizontal: false,
- };
- componentDidMount() {
- document.body.classList.add('body-sidebar');
- this.checkHash();
- this.doCollapse(this.props.collapsed);
- }
- // Sidebar doesn't use children, so don't use it to compare
- // Also ignore location, will re-render when routes change (instead of query params)
- //
- // NOTE(epurkhiser): The comment above is why I added `children?: never` as a
- // type to this component. I'm not sure the implications of removing this so
- // I've just left it for now.
- shouldComponentUpdate(
- {children: _children, location: _location, ...nextPropsToCompare}: Props,
- nextState: State
- ) {
- const {
- children: _childrenCurrent,
- location: _locationCurrent,
- ...currentPropsToCompare
- } = this.props;
- return (
- !isEqual(currentPropsToCompare, nextPropsToCompare) ||
- !isEqual(this.state, nextState)
- );
- }
- componentDidUpdate(prevProps: Props) {
- const {collapsed, location} = this.props;
- // Close active panel if we navigated anywhere
- if (location?.pathname !== prevProps.location?.pathname) {
- this.hidePanel();
- }
- // Collapse
- if (collapsed !== prevProps.collapsed) {
- this.doCollapse(collapsed);
- }
- }
- componentWillUnmount() {
- document.body.classList.remove('body-sidebar');
- if (this.mq) {
- this.mq.removeListener(this.handleMediaQueryChange);
- this.mq = null;
- }
- }
- mq: MediaQueryList | null = null;
- sidebarRef = React.createRef<HTMLDivElement>();
- doCollapse(collapsed: boolean) {
- if (collapsed) {
- document.body.classList.add('collapsed');
- } else {
- document.body.classList.remove('collapsed');
- }
- }
- toggleSidebar = () => {
- const {collapsed} = this.props;
- if (!collapsed) {
- hideSidebar();
- } else {
- showSidebar();
- }
- };
- checkHash = () => {
- if (window.location.hash === '#welcome') {
- this.togglePanel(SidebarPanelKey.OnboardingWizard);
- }
- };
- handleMediaQueryChange = (changed: MediaQueryListEvent) => {
- this.setState({
- horizontal: changed.matches,
- });
- };
- togglePanel = (panel: SidebarPanelKey) => SidebarPanelActions.togglePanel(panel);
- hidePanel = () => SidebarPanelActions.hidePanel();
- // Keep the global selection querystring values in the path
- navigateWithGlobalSelection = (
- pathname: string,
- evt: React.MouseEvent<HTMLAnchorElement>
- ) => {
- const globalSelectionRoutes = [
- 'alerts',
- 'alerts/rules',
- 'dashboards',
- 'issues',
- 'releases',
- 'user-feedback',
- 'discover',
- 'discover/results', // Team plans do not have query landing page
- 'performance',
- ].map(route => `/organizations/${this.props.organization.slug}/${route}/`);
- // Only keep the querystring if the current route matches one of the above
- if (globalSelectionRoutes.includes(pathname)) {
- const query = extractSelectionParameters(this.props.location?.query);
- // Handle cmd-click (mac) and meta-click (linux)
- if (evt.metaKey) {
- const q = queryString.stringify(query);
- evt.currentTarget.href = `${evt.currentTarget.href}?${q}`;
- return;
- }
- evt.preventDefault();
- browserHistory.push({pathname, query});
- }
- this.hidePanel();
- };
- render() {
- const {activePanel, organization, collapsed} = this.props;
- const {horizontal} = this.state;
- const config = ConfigStore.getConfig();
- const user = ConfigStore.get('user');
- const hasPanel = !!activePanel;
- const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
- const sidebarItemProps = {
- orientation,
- collapsed,
- hasPanel,
- };
- const hasOrganization = !!organization;
- const projects = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- index
- onClick={this.hidePanel}
- icon={<IconProject size="md" />}
- label={<GuideAnchor target="projects">{t('Projects')}</GuideAnchor>}
- to={`/organizations/${organization.slug}/projects/`}
- id="projects"
- />
- );
- const issues = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/issues/`,
- evt
- )
- }
- icon={<IconIssues size="md" />}
- label={<GuideAnchor target="issues">{t('Issues')}</GuideAnchor>}
- to={`/organizations/${organization.slug}/issues/`}
- id="issues"
- />
- );
- const discover2 = hasOrganization && (
- <Feature
- hookName="feature-disabled:discover2-sidebar-item"
- features={['discover-basic']}
- organization={organization}
- >
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(getDiscoverLandingUrl(organization), evt)
- }
- icon={<IconTelescope size="md" />}
- label={<GuideAnchor target="discover">{t('Discover')}</GuideAnchor>}
- to={getDiscoverLandingUrl(organization)}
- id="discover-v2"
- />
- </Feature>
- );
- const performance = hasOrganization && (
- <Feature
- hookName="feature-disabled:performance-sidebar-item"
- features={['performance-view']}
- organization={organization}
- >
- <SidebarOverride id="performance-override">
- {(overideProps: Partial<React.ComponentProps<typeof SidebarItem>>) => (
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/performance/`,
- evt
- )
- }
- icon={<IconLightning size="md" />}
- label={<GuideAnchor target="performance">{t('Performance')}</GuideAnchor>}
- to={`/organizations/${organization.slug}/performance/`}
- id="performance"
- {...overideProps}
- />
- )}
- </SidebarOverride>
- </Feature>
- );
- const releases = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/releases/`,
- evt
- )
- }
- icon={<IconReleases size="md" />}
- label={<GuideAnchor target="releases">{t('Releases')}</GuideAnchor>}
- to={`/organizations/${organization.slug}/releases/`}
- id="releases"
- />
- );
- const userFeedback = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/user-feedback/`,
- evt
- )
- }
- icon={<IconSupport size="md" />}
- label={t('User Feedback')}
- to={`/organizations/${organization.slug}/user-feedback/`}
- id="user-feedback"
- />
- );
- const alerts = hasOrganization && (
- <Feature features={['incidents', 'alert-details-redesign']} requireAll={false}>
- {({features}) => {
- const hasIncidents = features.includes('incidents');
- const hasAlertList = features.includes('alert-details-redesign');
- const alertsPath =
- hasIncidents && !hasAlertList
- ? `/organizations/${organization.slug}/alerts/`
- : `/organizations/${organization.slug}/alerts/rules/`;
- return (
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) => this.navigateWithGlobalSelection(alertsPath, evt)}
- icon={<IconSiren size="md" />}
- label={t('Alerts')}
- to={alertsPath}
- id="alerts"
- />
- );
- }}
- </Feature>
- );
- const monitors = hasOrganization && (
- <Feature features={['monitors']} organization={organization}>
- <SidebarItem
- {...sidebarItemProps}
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/monitors/`,
- evt
- )
- }
- icon={<IconLab size="md" />}
- label={t('Monitors')}
- to={`/organizations/${organization.slug}/monitors/`}
- id="monitors"
- />
- </Feature>
- );
- const dashboards = hasOrganization && (
- <Feature
- hookName="feature-disabled:dashboards-sidebar-item"
- features={['discover', 'discover-query', 'dashboards-basic', 'dashboards-edit']}
- organization={organization}
- requireAll={false}
- >
- <SidebarItem
- {...sidebarItemProps}
- index
- onClick={(_id, evt) =>
- this.navigateWithGlobalSelection(
- `/organizations/${organization.slug}/dashboards/`,
- evt
- )
- }
- icon={<IconGraph size="md" />}
- label={t('Dashboards')}
- to={`/organizations/${organization.slug}/dashboards/`}
- id="customizable-dashboards"
- isNew
- />
- </Feature>
- );
- const activity = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={this.hidePanel}
- icon={<IconActivity size="md" />}
- label={t('Activity')}
- to={`/organizations/${organization.slug}/activity/`}
- id="activity"
- />
- );
- const stats = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={this.hidePanel}
- icon={<IconStats size="md" />}
- label={t('Stats')}
- to={`/organizations/${organization.slug}/stats/`}
- id="stats"
- />
- );
- const settings = hasOrganization && (
- <SidebarItem
- {...sidebarItemProps}
- onClick={this.hidePanel}
- icon={<IconSettings size="md" />}
- label={t('Settings')}
- to={`/settings/${organization.slug}/`}
- id="settings"
- />
- );
- return (
- <StyledSidebar ref={this.sidebarRef} collapsed={collapsed}>
- <SidebarSectionGroupPrimary>
- <SidebarSection>
- <SidebarDropdown
- orientation={orientation}
- collapsed={collapsed}
- org={organization}
- user={user}
- config={config}
- />
- </SidebarSection>
- <PrimaryItems>
- {hasOrganization && (
- <React.Fragment>
- <SidebarSection>
- {projects}
- {issues}
- {performance}
- {releases}
- {userFeedback}
- {alerts}
- {discover2}
- </SidebarSection>
- <SidebarSection>
- {dashboards}
- {monitors}
- </SidebarSection>
- <SidebarSection>
- {activity}
- {stats}
- </SidebarSection>
- <SidebarSection>{settings}</SidebarSection>
- </React.Fragment>
- )}
- </PrimaryItems>
- </SidebarSectionGroupPrimary>
- {hasOrganization && (
- <SidebarSectionGroup>
- <SidebarSection noMargin noPadding>
- <OnboardingStatus
- org={organization}
- currentPanel={activePanel}
- onShowPanel={() => this.togglePanel(SidebarPanelKey.OnboardingWizard)}
- hidePanel={this.hidePanel}
- {...sidebarItemProps}
- />
- </SidebarSection>
- <SidebarSection>
- {HookStore.get('sidebar:bottom-items').length > 0 &&
- HookStore.get('sidebar:bottom-items')[0]({
- organization,
- ...sidebarItemProps,
- })}
- <SidebarHelp
- orientation={orientation}
- collapsed={collapsed}
- hidePanel={this.hidePanel}
- organization={organization}
- />
- <Broadcasts
- orientation={orientation}
- collapsed={collapsed}
- currentPanel={activePanel}
- onShowPanel={() => this.togglePanel(SidebarPanelKey.Broadcasts)}
- hidePanel={this.hidePanel}
- organization={organization}
- />
- <ServiceIncidents
- orientation={orientation}
- collapsed={collapsed}
- currentPanel={activePanel}
- onShowPanel={() => this.togglePanel(SidebarPanelKey.StatusUpdate)}
- hidePanel={this.hidePanel}
- />
- </SidebarSection>
- {!horizontal && (
- <SidebarSection>
- <SidebarCollapseItem
- id="collapse"
- data-test-id="sidebar-collapse"
- {...sidebarItemProps}
- icon={<StyledIconChevron collapsed={collapsed} />}
- label={collapsed ? t('Expand') : t('Collapse')}
- onClick={this.toggleSidebar}
- />
- </SidebarSection>
- )}
- </SidebarSectionGroup>
- )}
- </StyledSidebar>
- );
- }
- }
- type ContainerProps = Omit<Props, 'collapsed' | 'activePanel'>;
- type ContainerState = {
- collapsed: boolean;
- activePanel: ActivePanelType;
- };
- type Preferences = typeof PreferencesStore.prefs;
- class SidebarContainer extends React.Component<ContainerProps, ContainerState> {
- state: ContainerState = {
- collapsed: PreferencesStore.getInitialState().collapsed,
- activePanel: '',
- };
- componentWillUnmount() {
- this.preferenceUnsubscribe();
- this.sidebarUnsubscribe();
- }
- preferenceUnsubscribe = PreferencesStore.listen(
- (preferences: Preferences) => this.onPreferenceChange(preferences),
- undefined
- );
- sidebarUnsubscribe = SidebarPanelStore.listen(
- (activePanel: ActivePanelType) => this.onSidebarPanelChange(activePanel),
- undefined
- );
- onPreferenceChange(preferences: Preferences) {
- if (preferences.collapsed === this.state.collapsed) {
- return;
- }
- this.setState({collapsed: preferences.collapsed});
- }
- onSidebarPanelChange(activePanel: ActivePanelType) {
- this.setState({activePanel});
- }
- render() {
- const {activePanel, collapsed} = this.state;
- return <Sidebar {...this.props} {...{activePanel, collapsed}} />;
- }
- }
- export default withOrganization(SidebarContainer);
- const responsiveFlex = css`
- display: flex;
- flex-direction: column;
- @media (max-width: ${theme.breakpoints[1]}) {
- flex-direction: row;
- }
- `;
- export const StyledSidebar = styled('div')<{collapsed: boolean}>`
- background: ${p => p.theme.sidebar.background};
- 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.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};
- ${responsiveFlex};
- ${p => p.collapsed && `width: ${p.theme.sidebar.collapsedWidth};`};
- @media (max-width: ${p => p.theme.breakpoints[1]}) {
- top: 0;
- left: 0;
- right: 0;
- height: ${p => p.theme.sidebar.mobileHeight};
- bottom: auto;
- width: auto;
- padding: 0 ${space(1)};
- align-items: center;
- }
- `;
- 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[1]}) {
- 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[1]}) {
- 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[1]}) {
- 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[0]}) {
- 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}) => (
- <IconChevron
- direction="left"
- size="md"
- isCircled
- css={[ExpandedIcon, collapsed && CollapsedIcon]}
- {...props}
- />
- ))``;
- const SidebarCollapseItem = styled(SidebarItem)`
- @media (max-width: ${p => p.theme.breakpoints[1]}) {
- display: none;
- }
- `;
|