Browse Source

Nhsieh/navigationv2 (#76187)

https://getsentry.atlassian.net/browse/ALRT-210

Updates to the Navigation sidebar UI



https://github.com/user-attachments/assets/c3e02699-5213-4789-a3e2-0d89075730bc

TODO: tests
Nathan Hsieh 6 months ago
parent
commit
69a1806bad

+ 4 - 1
static/app/components/sidebar/help.tsx

@@ -13,7 +13,10 @@ import SidebarDropdownMenu from './sidebarDropdownMenu.styled';
 import SidebarMenuItem from './sidebarMenuItem';
 import type {CommonSidebarProps} from './types';
 
-type Props = Pick<CommonSidebarProps, 'collapsed' | 'hidePanel' | 'orientation'> & {
+type Props = Pick<
+  CommonSidebarProps,
+  'collapsed' | 'hidePanel' | 'orientation' | 'hasNewNav'
+> & {
   organization: Organization;
 };
 

+ 14 - 0
static/app/components/sidebar/index.spec.tsx

@@ -107,6 +107,20 @@ describe('Sidebar', function () {
     await userEvent.click(screen.getByTestId('sidebar-dropdown'));
   });
 
+  it('does not render collapse with navigation-sidebar-v2 flag', async function () {
+    renderSidebar({
+      organization: {...organization, features: ['navigation-sidebar-v2']},
+    });
+
+    // await for the page to be rendered
+    expect(await screen.findByText('Issues')).toBeInTheDocument();
+    // Check that the user name is no longer visible
+    expect(screen.queryByText(user.name)).not.toBeInTheDocument();
+    // Check that the organization name is no longer visible
+    expect(screen.queryByText(organization.name)).not.toBeInTheDocument();
+    expect(screen.queryByTestId('sidebar-collapse')).not.toBeInTheDocument();
+  });
+
   it('has can logout', async function () {
     const mock = MockApiClient.addMockResponse({
       url: '/auth/',

+ 82 - 31
static/app/components/sidebar/index.tsx

@@ -119,11 +119,23 @@ function Sidebar() {
   const activePanel = useLegacyStore(SidebarPanelStore);
   const organization = useOrganization({allowNull: true});
   const {shouldAccordionFloat} = useContext(ExpandedContext);
+  const hasNewNav = organization?.features.includes('navigation-sidebar-v2');
+  const hasOrganization = !!organization;
   const isSelfHostedErrorsOnly = ConfigStore.get('isSelfHostedErrorsOnly');
 
-  const collapsed = !!preferences.collapsed;
+  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');
@@ -176,16 +188,18 @@ function Sidebar() {
     return () => bcl.remove('collapsed');
   }, [collapsed]);
 
-  const hasPanel = !!activePanel;
-  const hasOrganization = !!organization;
-  const orientation: SidebarOrientation = horizontal ? 'top' : 'left';
+  // Add sidebar hasNewNav classname to body
+  useEffect(() => {
+    const bcl = document.body.classList;
 
-  const sidebarItemProps = {
-    orientation,
-    collapsed,
-    hasPanel,
-    organization,
-  };
+    if (hasNewNav) {
+      bcl.add('hasNewNav');
+    } else {
+      bcl.remove('hasNewNav');
+    }
+
+    return () => bcl.remove('hasNewNav');
+  }, [hasNewNav]);
 
   const sidebarAnchor = isDemoWalkthrough() ? (
     <GuideAnchor target="projects" disabled={!DemoWalkthroughStore.get('sidebar')}>
@@ -214,6 +228,7 @@ function Sidebar() {
       to={`/organizations/${organization.slug}/issues/`}
       search="?referrer=sidebar"
       id="issues"
+      hasNewNav={hasNewNav}
     />
   );
 
@@ -417,7 +432,11 @@ function Sidebar() {
       <SidebarItem
         {...sidebarItemProps}
         icon={<IconLightning />}
-        label={<GuideAnchor target="performance">{t('Performance')}</GuideAnchor>}
+        label={
+          <GuideAnchor target="performance">
+            {hasNewNav ? 'Perf.' : t('Performance')}
+          </GuideAnchor>
+        }
         to={`/organizations/${organization.slug}/performance/`}
         id="performance"
       />
@@ -521,7 +540,7 @@ function Sidebar() {
         {...sidebarItemProps}
         index
         icon={<IconDashboard />}
-        label={t('Dashboards')}
+        label={hasNewNav ? 'Dash.' : t('Dashboards')}
         to={`/organizations/${organization.slug}/dashboards/`}
         id="customizable-dashboards"
       />
@@ -591,6 +610,8 @@ function Sidebar() {
     </Feature>
   );
 
+  // Sidebar accordion includes a secondary list of nav items
+  // TODO: replace with a secondary panel
   const explore = (
     <SidebarAccordion
       {...sidebarItemProps}
@@ -608,13 +629,21 @@ function Sidebar() {
   );
 
   return (
-    <SidebarWrapper aria-label={t('Primary Navigation')} collapsed={collapsed}>
+    <SidebarWrapper
+      aria-label={t('Primary Navigation')}
+      collapsed={collapsed}
+      hasNewNav={hasNewNav}
+    >
       <ExpandedContextProvider>
         <SidebarSectionGroupPrimary>
           <DropdownSidebarSection
             isSuperuser={showSuperuserWarning() && !isExcludedOrg()}
+            hasNewNav={hasNewNav}
           >
-            <SidebarDropdown orientation={orientation} collapsed={collapsed} />
+            <SidebarDropdown
+              orientation={orientation}
+              collapsed={hasNewNav || collapsed}
+            />
 
             {showSuperuserWarning() && !isExcludedOrg() && (
               <Hook name="component:superuser-warning" organization={organization} />
@@ -624,19 +653,19 @@ function Sidebar() {
           <PrimaryItems>
             {hasOrganization && (
               <Fragment>
-                <SidebarSection>
+                <SidebarSection hasNewNav={hasNewNav}>
                   {issues}
                   {projects}
                 </SidebarSection>
 
                 {!isSelfHostedErrorsOnly && (
                   <Fragment>
-                    <SidebarSection>
+                    <SidebarSection hasNewNav={hasNewNav}>
                       {explore}
                       {insights}
                     </SidebarSection>
 
-                    <SidebarSection>
+                    <SidebarSection hasNewNav={hasNewNav}>
                       {performance}
                       {feedback}
                       {monitors}
@@ -649,7 +678,7 @@ function Sidebar() {
 
                 {isSelfHostedErrorsOnly && (
                   <Fragment>
-                    <SidebarSection>
+                    <SidebarSection hasNewNav={hasNewNav}>
                       {alerts}
                       {discover2}
                       {dashboards}
@@ -659,7 +688,7 @@ function Sidebar() {
                   </Fragment>
                 )}
 
-                <SidebarSection>
+                <SidebarSection hasNewNav={hasNewNav}>
                   {stats}
                   {settings}
                 </SidebarSection>
@@ -669,7 +698,8 @@ function Sidebar() {
         </SidebarSectionGroupPrimary>
 
         {hasOrganization && (
-          <SidebarSectionGroup>
+          <SidebarSectionGroup hasNewNav={hasNewNav}>
+            {/* What are the onboarding sidebars? */}
             <PerformanceOnboardingSidebar
               currentPanel={activePanel}
               onShowPanel={() => togglePanel(SidebarPanelKey.PERFORMANCE_ONBOARDING)}
@@ -700,7 +730,7 @@ function Sidebar() {
               hidePanel={hidePanel}
               {...sidebarItemProps}
             />
-            <SidebarSection noMargin noPadding>
+            <SidebarSection hasNewNav={hasNewNav} noMargin noPadding>
               <OnboardingStatus
                 org={organization}
                 currentPanel={activePanel}
@@ -710,7 +740,7 @@ function Sidebar() {
               />
             </SidebarSection>
 
-            <SidebarSection>
+            <SidebarSection hasNewNav={hasNewNav}>
               {HookStore.get('sidebar:bottom-items').length > 0 &&
                 HookStore.get('sidebar:bottom-items')[0]({
                   orientation,
@@ -741,8 +771,8 @@ function Sidebar() {
               />
             </SidebarSection>
 
-            {!horizontal && (
-              <SidebarSection>
+            {!horizontal && !hasNewNav && (
+              <SidebarSection hasNewNav={hasNewNav}>
                 <SidebarCollapseItem
                   id="collapse"
                   data-test-id="sidebar-collapse"
@@ -771,12 +801,19 @@ const responsiveFlex = css`
   }
 `;
 
-export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
+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.collapsed ? 'collapsedWidth' : 'expandedWidth']};
+  width: ${p =>
+    p.theme.sidebar[
+      p.hasNewNav
+        ? 'semiCollapsedWidth'
+        : p.collapsed
+          ? 'collapsedWidth'
+          : 'expandedWidth'
+    ]};
   position: fixed;
   top: ${p => (ConfigStore.get('demoMode') ? p.theme.demo.headerSize : 0)};
   left: 0;
@@ -800,10 +837,11 @@ export const SidebarWrapper = styled('nav')<{collapsed: boolean}>`
   }
 `;
 
-const SidebarSectionGroup = styled('div')`
+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')`
@@ -820,7 +858,8 @@ const SidebarSectionGroupPrimary = styled('div')`
 `;
 
 const PrimaryItems = styled('div')`
-  overflow: auto;
+  overflow-y: auto;
+  overflow-x: hidden;
   flex: 1;
   display: flex;
   flex-direction: column;
@@ -832,7 +871,8 @@ const PrimaryItems = styled('div')`
     box-shadow: rgba(0, 0, 0, 0.15) 0px -10px 10px inset;
   }
   @media (max-width: ${p => p.theme.breakpoints.medium}) {
-    overflow-y: visible;
+    overflow-y: hidden;
+    overflow-x: auto;
     flex-direction: row;
     height: 100%;
     align-items: center;
@@ -859,16 +899,25 @@ const SubitemDot = styled('div')<{collapsed: boolean}>`
 `;
 
 const SidebarSection = styled(SidebarSectionGroup)<{
+  hasNewNav?: boolean;
   noMargin?: boolean;
   noPadding?: boolean;
 }>`
-  ${p => !p.noMargin && `margin: ${space(1)} 0`};
-  ${p => !p.noPadding && `padding: 0 ${space(2)}`};
+  ${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;
+      }
+    `}
 
   &:empty {
     display: none;
@@ -876,6 +925,7 @@ const SidebarSection = styled(SidebarSectionGroup)<{
 `;
 
 const DropdownSidebarSection = styled(SidebarSection)<{
+  hasNewNav?: boolean;
   isSuperuser?: boolean;
 }>`
   position: relative;
@@ -893,6 +943,7 @@ const DropdownSidebarSection = styled(SidebarSection)<{
         background: ${p.theme.superuserSidebar};
       }
     `}
+  ${p => p.hasNewNav && `align-items: center;`}
 `;
 
 const SidebarCollapseItem = styled(SidebarItem)`

+ 77 - 26
static/app/components/sidebar/sidebarItem.tsx

@@ -70,6 +70,7 @@ export type SidebarItemProps = {
    * prop.
    */
   exact?: boolean;
+  hasNewNav?: boolean;
   /**
    * Sidebar has a panel open
    */
@@ -84,6 +85,7 @@ export type SidebarItemProps = {
    * Additional badge letting users know a tab is in beta.
    */
   isBeta?: boolean;
+
   /**
    * Is main item in a floating accordion
    */
@@ -93,7 +95,6 @@ export type SidebarItemProps = {
    * Is this item nested within another item
    */
   isNested?: boolean;
-
   /**
    * Specify the variant for the badge.
    */
@@ -143,6 +144,7 @@ function SidebarItem({
   isNested,
   isMainItem,
   isOpenInFloatingSidebar,
+  hasNewNav,
   ...props
 }: SidebarItemProps) {
   const {setExpandedItemId, shouldAccordionFloat} = useContext(ExpandedContext);
@@ -156,8 +158,13 @@ function SidebarItem({
   const isActiveRouter =
     !hasPanel && router && isItemActive({to, label: labelString}, exact);
 
-  const isInFloatingAccordion = (isNested || isMainItem) && shouldAccordionFloat;
+  // TODO: floating accordion should be transformed into secondary panel
+  let isInFloatingAccordion = (isNested || isMainItem) && shouldAccordionFloat;
+  if (hasNewNav) {
+    isInFloatingAccordion = false;
+  }
   const hasLink = Boolean(to);
+  const isInCollapsedState = (!isInFloatingAccordion && collapsed) || hasNewNav;
 
   const isActive = defined(active) ? active : isActiveRouter;
   const isTop = orientation === 'top' && !isInFloatingAccordion;
@@ -202,13 +209,12 @@ function SidebarItem({
     [href, to, id, onClick, recordAnalytics, showIsNew, isNewSeenKey, setExpandedItemId]
   );
 
-  const isInCollapsedState = !isInFloatingAccordion && collapsed;
-
   return (
     <Tooltip
       disabled={
         (!isInCollapsedState && !isTop) ||
-        (shouldAccordionFloat && isOpenInFloatingSidebar)
+        (shouldAccordionFloat && isOpenInFloatingSidebar) ||
+        hasNewNav
       }
       title={
         <Flex align="center">
@@ -229,10 +235,21 @@ function SidebarItem({
             className={className}
             aria-current={isActive ? 'page' : undefined}
             onClick={handleItemClick}
+            hasNewNav={hasNewNav}
           >
-            <InteractionStateLayer isPressed={isActive} color="white" higherOpacity />
-            <SidebarItemWrapper collapsed={isInCollapsedState}>
-              {!isInFloatingAccordion && <SidebarItemIcon>{icon}</SidebarItemIcon>}
+            {hasNewNav ? (
+              <StyledInteractionStateLayer
+                isPressed={isActive}
+                color="white"
+                higherOpacity
+              />
+            ) : (
+              <InteractionStateLayer isPressed={isActive} color="white" higherOpacity />
+            )}
+            <SidebarItemWrapper collapsed={isInCollapsedState} hasNewNav={hasNewNav}>
+              {!isInFloatingAccordion && (
+                <SidebarItemIcon hasNewNav={hasNewNav}>{icon}</SidebarItemIcon>
+              )}
               {!isInCollapsedState && !isTop && (
                 <SidebarItemLabel
                   isInFloatingAccordion={isInFloatingAccordion}
@@ -270,6 +287,12 @@ function SidebarItem({
                   {badge}
                 </SidebarItemBadge>
               )}
+              {!isInFloatingAccordion && hasNewNav && (
+                <LabelHook id={id}>
+                  <TruncatedLabel hasNewNav={hasNewNav}>{label}</TruncatedLabel>
+                  {additionalContent ?? badges}
+                </LabelHook>
+              )}
               {trailingItems}
             </SidebarItemWrapper>
           </StyledSidebarItem>
@@ -326,6 +349,7 @@ const getActiveStyle = ({
   isInFloatingAccordion,
 }: {
   active?: string;
+  hasNewNav?: boolean;
   isInFloatingAccordion?: boolean;
   theme?: Theme;
 }) => {
@@ -364,23 +388,33 @@ const StyledSidebarItem = styled(Link, {
   position: relative;
   cursor: pointer;
   font-size: 15px;
-  height: ${p => (p.isInFloatingAccordion ? '35px' : '30px')};
+  height: ${p => (p.isInFloatingAccordion ? '35px' : p.hasNewNav ? '40px' : '30px')};
   flex-shrink: 0;
   border-radius: ${p => p.theme.borderRadius};
   transition: none;
-
-  &:before {
-    display: block;
-    content: '';
-    position: absolute;
-    top: 4px;
-    left: calc(-${space(2)} - 1px);
-    bottom: 6px;
-    width: 5px;
-    border-radius: 0 3px 3px 0;
-    background-color: transparent;
-    transition: 0.15s background-color linear;
-  }
+  ${p => {
+    if (!p.hasNewNav) {
+      return css`
+        &:before {
+          display: block;
+          content: '';
+          position: absolute;
+          top: 4px;
+          left: calc(-${space(2)} - 1px);
+          bottom: 6px;
+          width: 5px;
+          border-radius: 0 3px 3px 0;
+          background-color: transparent;
+          transition: 0.15s background-color linear;
+        }
+      `;
+    }
+    return css`
+      margin: ${space(2)} 0;
+      width: 100px;
+      align-self: center;
+    `;
+  }}
 
   @media (max-width: ${p => p.theme.breakpoints.medium}) {
     &:before {
@@ -421,10 +455,11 @@ const StyledSidebarItem = styled(Link, {
   ${getActiveStyle};
 `;
 
-const SidebarItemWrapper = styled('div')<{collapsed?: boolean}>`
+const SidebarItemWrapper = styled('div')<{collapsed?: boolean; hasNewNav?: boolean}>`
   display: flex;
   align-items: center;
   justify-content: center;
+  ${p => p.hasNewNav && 'flex-direction: column;'}
   width: 100%;
 
   ${p => !p.collapsed && `padding-right: ${space(1)};`}
@@ -433,7 +468,7 @@ const SidebarItemWrapper = styled('div')<{collapsed?: boolean}>`
   }
 `;
 
-const SidebarItemIcon = styled('span')`
+const SidebarItemIcon = styled('span')<{hasNewNav?: boolean}>`
   display: flex;
   align-items: center;
   justify-content: center;
@@ -446,6 +481,13 @@ const SidebarItemIcon = styled('span')`
     width: 18px;
     height: 18px;
   }
+  ${p =>
+    p.hasNewNav &&
+    css`
+      @media (max-width: ${p.theme.breakpoints.medium}) {
+        display: none;
+      }
+    `};
 `;
 
 const SidebarItemLabel = styled('span')<{
@@ -461,8 +503,12 @@ const SidebarItemLabel = styled('span')<{
   overflow: hidden;
 `;
 
-const TruncatedLabel = styled(TextOverflow)`
-  margin-right: auto;
+const TruncatedLabel = styled(TextOverflow)<{hasNewNav?: boolean}>`
+  ${p =>
+    !p.hasNewNav &&
+    css`
+      margin-right: auto;
+    `}
 `;
 
 const getCollapsedBadgeStyle = ({collapsed, theme}) => {
@@ -503,3 +549,8 @@ const CollapsedFeatureBadge = styled(FeatureBadge)`
   top: 2px;
   right: 2px;
 `;
+
+const StyledInteractionStateLayer = styled(InteractionStateLayer)`
+  height: ${16 * 2 + 40}px;
+  width: 70px;
+`;

+ 4 - 0
static/app/components/sidebar/types.tsx

@@ -32,4 +32,8 @@ export type CommonSidebarProps = {
    * The orientation of the sidebar
    */
   orientation: SidebarOrientation;
+  /**
+   * Alternate collapsed state
+   */
+  hasNewNav?: boolean;
 };

+ 1 - 0
static/app/utils/theme.tsx

@@ -806,6 +806,7 @@ const commonTheme = {
     badgeSize: '22px',
     smallBadgeSize: '11px',
     collapsedWidth: '70px',
+    semiCollapsedWidth: '100px',
     expandedWidth: '220px',
     mobileHeight: '54px',
     menuSpacing: '15px',

+ 6 - 0
static/less/layout.less

@@ -7,6 +7,9 @@ body {
     &.collapsed {
       padding-left: @sidebar-collapsed-width;
     }
+    &.hasNewNav {
+      padding-left: @sidebar-semi-collapsed-width;
+    }
   }
 
   &.scroll-lock {
@@ -39,6 +42,9 @@ body.narrow {
     &.collapsed {
       padding-left: @sidebar-collapsed-width;
     }
+    &.hasNewNav {
+      padding-left: @sidebar-semi-collapsed-width;
+    }
   }
 
   &.dialog {

+ 1 - 0
static/less/variables.less

@@ -35,6 +35,7 @@
 // Sets up sidebar offsets
 
 @sidebar-collapsed-width: 70px;
+@sidebar-semi-collapsed-width: 100px;
 @sidebar-expanded-width: 220px;
 @sidebar-panel-width: 320px;
 @sidebar-mobile-height: 54px;