123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331 |
- import {Fragment, isValidElement} from 'react';
- // eslint-disable-next-line no-restricted-imports
- import {withRouter, WithRouterProps} from 'react-router';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import FeatureBadge from 'sentry/components/featureBadge';
- import HookOrDefault from 'sentry/components/hookOrDefault';
- import Link from 'sentry/components/links/link';
- import TextOverflow from 'sentry/components/textOverflow';
- import Tooltip from 'sentry/components/tooltip';
- import {Organization} from 'sentry/types';
- import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
- import localStorage from 'sentry/utils/localStorage';
- import {Theme} from 'sentry/utils/theme';
- import {SidebarOrientation} from './types';
- const LabelHook = HookOrDefault({
- hookName: 'sidebar:item-label',
- defaultComponent: ({children}) => <Fragment>{children}</Fragment>,
- });
- type Props = WithRouterProps & {
- /**
- * Icon to display
- */
- icon: React.ReactNode;
- /**
- * Key of the sidebar item. Used for label hooks
- */
- id: string;
- /**
- * Label to display (only when expanded)
- */
- label: React.ReactNode;
- /**
- * Sidebar is at "top" or "left" of screen
- */
- orientation: SidebarOrientation;
- /**
- * Is this sidebar item active
- */
- active?: boolean;
- /**
- * Additional badge to display after label
- */
- badge?: number;
- className?: string;
- /**
- * Is sidebar in a collapsed state
- */
- collapsed?: boolean;
- /**
- * Sidebar has a panel open
- */
- hasPanel?: boolean;
- href?: string;
- index?: boolean;
- /**
- * Additional badge letting users know a tab is in alpha.
- */
- isAlpha?: boolean;
- /**
- * Additional badge letting users know a tab is in beta.
- */
- isBeta?: boolean;
- /**
- * Additional badge letting users know a tab is new.
- */
- isNew?: boolean;
- /**
- * An optional prefix that can be used to reset the "new" indicator
- */
- isNewSeenKeySuffix?: string;
- onClick?: (id: string, e: React.MouseEvent<HTMLAnchorElement>) => void;
- /**
- * The current organization. Useful for analytics.
- */
- organization?: Organization;
- to?: string;
- };
- const SidebarItem = ({
- router,
- id,
- href,
- to,
- icon,
- label,
- badge,
- active,
- hasPanel,
- isNew,
- isBeta,
- isAlpha,
- collapsed,
- className,
- orientation,
- isNewSeenKeySuffix,
- organization,
- onClick,
- ...props
- }: Props) => {
- // label might be wrapped in a guideAnchor
- let labelString = label;
- if (isValidElement(label)) {
- labelString = label?.props?.children ?? label;
- }
- // If there is no active panel open and if path is active according to react-router
- const isActiveRouter =
- (!hasPanel && router && to && location.pathname.startsWith(to)) ||
- (labelString === 'Discover' && location.pathname.includes('/discover/')) ||
- (labelString === 'Dashboards' &&
- (location.pathname.includes('/dashboards/') ||
- location.pathname.includes('/dashboard/')) &&
- !location.pathname.startsWith('/settings/')) ||
- // TODO: this won't be necessary once we remove settingsHome
- (labelString === 'Settings' && location.pathname.startsWith('/settings/')) ||
- (labelString === 'Alerts' &&
- location.pathname.includes('/alerts/') &&
- !location.pathname.startsWith('/settings/'));
- const isActive = active || isActiveRouter;
- const isTop = orientation === 'top';
- const placement = isTop ? 'bottom' : 'right';
- const seenSuffix = isNewSeenKeySuffix ?? '';
- const isNewSeenKey = `sidebar-new-seen:${id}${seenSuffix}`;
- const showIsNew = isNew && !localStorage.getItem(isNewSeenKey);
- const recordAnalytics = () => {
- trackAdvancedAnalyticsEvent('growth.clicked_sidebar', {
- item: id,
- organization: organization || null,
- });
- };
- return (
- <Tooltip disabled={!collapsed} title={label} position={placement}>
- <StyledSidebarItem
- data-test-id={props['data-test-id']}
- id={`sidebar-item-${id}`}
- active={isActive ? 'true' : undefined}
- to={(to ? to : href) || '#'}
- className={className}
- onClick={(event: React.MouseEvent<HTMLAnchorElement>) => {
- !(to || href) && event.preventDefault();
- recordAnalytics();
- onClick?.(id, event);
- showIsNew && localStorage.setItem(isNewSeenKey, 'true');
- }}
- >
- <SidebarItemWrapper>
- <SidebarItemIcon>{icon}</SidebarItemIcon>
- {!collapsed && !isTop && (
- <SidebarItemLabel>
- <LabelHook id={id}>
- <TextOverflow>{label}</TextOverflow>
- {showIsNew && <FeatureBadge type="new" noTooltip />}
- {isBeta && <FeatureBadge type="beta" noTooltip />}
- {isAlpha && <FeatureBadge type="alpha" noTooltip />}
- </LabelHook>
- </SidebarItemLabel>
- )}
- {collapsed && showIsNew && <CollapsedFeatureBadge type="new" />}
- {collapsed && isBeta && <CollapsedFeatureBadge type="beta" />}
- {collapsed && isAlpha && <CollapsedFeatureBadge type="alpha" />}
- {badge !== undefined && badge > 0 && (
- <SidebarItemBadge collapsed={collapsed}>{badge}</SidebarItemBadge>
- )}
- </SidebarItemWrapper>
- </StyledSidebarItem>
- </Tooltip>
- );
- };
- export default withRouter(SidebarItem);
- const getActiveStyle = ({active, theme}: {active?: string; theme?: Theme}) => {
- if (!active) {
- return '';
- }
- return css`
- color: ${theme?.white};
- &:active,
- &:focus,
- &:hover {
- color: ${theme?.white};
- }
- &:before {
- background-color: ${theme?.active};
- }
- `;
- };
- const StyledSidebarItem = styled(Link)`
- display: flex;
- color: inherit;
- position: relative;
- cursor: pointer;
- font-size: 15px;
- line-height: 32px;
- height: 34px;
- flex-shrink: 0;
- transition: 0.15s color linear;
- &:before {
- display: block;
- content: '';
- position: absolute;
- top: 4px;
- left: -20px;
- bottom: 6px;
- width: 5px;
- border-radius: 0 3px 3px 0;
- background-color: transparent;
- transition: 0.15s background-color linear;
- }
- @media (max-width: ${p => p.theme.breakpoints.medium}) {
- margin: 0 4px;
- &:before {
- top: auto;
- left: 5px;
- bottom: -10px;
- height: 5px;
- width: auto;
- right: 5px;
- border-radius: 3px 3px 0 0;
- }
- }
- &:hover,
- &:focus {
- color: ${p => p.theme.white};
- }
- &.focus-visible {
- outline: none;
- background: #584c66;
- padding: 0 19px;
- margin: 0 -19px;
- &:before {
- left: 0;
- }
- }
- ${getActiveStyle};
- `;
- const SidebarItemWrapper = styled('div')`
- display: flex;
- align-items: center;
- width: 100%;
- `;
- const SidebarItemIcon = styled('span')`
- content: '';
- display: inline-flex;
- width: 32px;
- height: 22px;
- font-size: 20px;
- align-items: center;
- flex-shrink: 0;
- svg {
- display: block;
- margin: 0 auto;
- }
- `;
- const SidebarItemLabel = styled('span')`
- margin-left: 12px;
- white-space: nowrap;
- opacity: 1;
- flex: 1;
- display: flex;
- align-items: center;
- justify-content: space-between;
- `;
- const getCollapsedBadgeStyle = ({collapsed, theme}) => {
- if (!collapsed) {
- return '';
- }
- return css`
- text-indent: -99999em;
- position: absolute;
- right: 0;
- top: 1px;
- background: ${theme.red300};
- width: ${theme.sidebar.smallBadgeSize};
- height: ${theme.sidebar.smallBadgeSize};
- border-radius: ${theme.sidebar.smallBadgeSize};
- line-height: ${theme.sidebar.smallBadgeSize};
- box-shadow: ${theme.sidebar.boxShadow};
- `;
- };
- const SidebarItemBadge = styled(({collapsed: _, ...props}) => <span {...props} />)`
- display: block;
- text-align: center;
- color: ${p => p.theme.white};
- font-size: 12px;
- background: ${p => p.theme.red300};
- width: ${p => p.theme.sidebar.badgeSize};
- height: ${p => p.theme.sidebar.badgeSize};
- border-radius: ${p => p.theme.sidebar.badgeSize};
- line-height: ${p => p.theme.sidebar.badgeSize};
- ${getCollapsedBadgeStyle};
- `;
- const CollapsedFeatureBadge = styled(FeatureBadge)`
- position: absolute;
- top: 0;
- right: 0;
- `;
- CollapsedFeatureBadge.defaultProps = {
- variant: 'indicator',
- noTooltip: true,
- };
|