Skip to content

Conversation

@chandrasekharan-zipstack
Copy link
Contributor

@chandrasekharan-zipstack chandrasekharan-zipstack commented Jan 29, 2026

What

  • Enhanced the sidebar to expand on hover
  • Added a pin at the bottom to leave it expanded

Why

  • Previously had a button that had to be clicked, not intuitive enough and involves friction
  • Newer UX allows easy navigation

How

  • Added flex layout styles for .ant-layout-sider-children wrapper
  • Wrapped content in .sidebar-content-wrapper with min-height: 0 for proper flex scrolling
  • Implemented pin toggle with localStorage persistence
  • Added mouse enter/leave handlers for auto-collapse when unpinned
  • Refactored collapse button from PageLayout to SideNavBar
  • Reduced pin container padding from 12px to 8px for better visual balance

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

No. Changes are isolated to sidebar styling and behavior. Collapse functionality is preserved and improved with new pin feature.

Notes on Testing

  • Test sidebar scroll when expanded
  • Test pin toggle and localStorage persistence
  • Test auto-collapse on mouse leave when unpinned
  • Test all sidebar navigation items still work correctly

Screenshots

image

Checklist

I have read and understood the Contribution Guidelines.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 29, 2026

Summary by CodeRabbit

  • New Features

    • Pin the sidebar to keep navigation permanently expanded
    • Sidebar auto-expands on hover and auto-collapses when not pinned
    • Sidebar collapsed/expanded state persists across sessions
    • Improved sidebar layout, scrolling, and pinned/hover visuals
  • Bug Fixes

    • More robust behavior when toggling collapse to avoid accidental collapses
  • Chores

    • Removed the separate collapse-toggle button from the main layout UI

Walkthrough

Adds a pinned/unpinned sidebar with hover-to-expand and a 300ms collapse debounce, a bottom pin toggle persisted via safe localStorage helpers, CSS scroll/flex wrappers for the sider, and moves collapse toggle responsibility from PageLayout to SideNavBar (SideNavBar now requires a setCollapsed prop).

Changes

Cohort / File(s) Summary
SideNavBar component
frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx
Introduces isPinned state persisted via safe localStorage helpers, hover enter/leave handlers with a 300ms collapse debounce, togglePin, pin button (icon + tooltip), conditional platform Popover behavior for disabled items, optional chaining guards, and a new required setCollapsed prop/PropTypes.
SideNavBar styles
frontend/src/components/navigations/side-nav-bar/SideNavBar.css
Adds flex wrappers for sider children, a scrollable content container, fixed bottom pin area with hover/pinned styles, overflow/height and collapse-aware rules, and pin icon/pinned-state styling.
LocalStorage helpers
frontend/src/helpers/localStorage.js
New safe localStorage utilities: isLocalStorageAvailable(), getLocalStorageValue(key, defaultValue), and setLocalStorageValue(key, value) to handle SSR/test environments and storage errors gracefully.
PageLayout component
frontend/src/layouts/page-layout/PageLayout.jsx
Removes inline collapse toggle UI and icon imports; initializes/persists collapsed via the new localStorage helpers; passes setCollapsed into SideNavBar and delegates toggle interactions to it.
PageLayout styles
frontend/src/layouts/page-layout/PageLayout.css
Removes the .collapse_btn CSS block associated with the removed collapse toggle button.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User as User
participant Sider as SideNavBar
participant Layout as PageLayout
participant Storage as LocalStorage

User->>Sider: mouse enter (hover)
Sider->>Layout: call setCollapsed(false)
Note right of Sider: expand UI shown
alt user clicks pin
    User->>Sider: click pin toggle
    Sider->>Storage: setLocalStorageValue("sidebarPinned", true/false)
    Sider->>Layout: ensure setCollapsed(false)
else hover leave while unpinned
    User->>Sider: mouse leave
    Sider->>Sider: start 300ms collapse timer
    Sider->>Layout: call setCollapsed(true) after timeout
end

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[MISC] Enhance sidebar UX to expand on hover' accurately reflects the main change—adding hover expansion to the sidebar with a pin feature—which is the core focus of this PR.
Description check ✅ Passed The PR description covers all required template sections including What, Why, How, breaking changes assessment, and testing notes with a screenshot, meeting repository standards.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/expandable-sidebar

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@chandrasekharan-zipstack chandrasekharan-zipstack changed the title [MISC] Fix sidebar scroll and add pin tooltip [MISC] Enhance sidebar UX to expand on hover Jan 29, 2026
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx (1)

196-211: Conditional hook calls violate React's rules of hooks.

The hooks inside the try/catch and if blocks at lines 199-201 and 208-210 are called conditionally, which violates React's hooks rules. Hooks must be called in the same order on every render—conditional calls lead to unpredictable behavior and bugs.

Since this appears to be pre-existing code (only the trailing comma formatting changed), consider addressing it in a follow-up. The typical fix is to call the hook unconditionally and handle the absence of the store gracefully:

// Call unconditionally, default to a no-op selector if store unavailable
const unstractSubscriptionPlan = unstractSubscriptionPlanStore?.useUnstractSubscriptionPlanStore?.(
  (state) => state?.unstractSubscriptionPlan
) ?? null;
🤖 Fix all issues with AI agents
In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 180-186: The collapse timeout started in handleMouseLeave (stored
on collapseTimeoutRef.current) isn't cleared on unmount; add a useEffect cleanup
that clears any pending timeout with clearTimeout(collapseTimeoutRef.current)
and optionally nulls collapseTimeoutRef.current to avoid calling setCollapsed
after the component has unmounted; ensure this effect runs once (empty deps) and
also consider clearing the timeout whenever isPinned changes or when collapsing
manually so setCollapsed won't be invoked on an unmounted component.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 162-164: The JSON.parse(localStorage.getItem("sidebarPinned"))
call can throw on corrupted data; wrap the read/parse in a try-catch inside the
SideNavBar component (where isPinned and setIsPinned are declared) so that if
JSON.parse throws you fall back to false (or a safe default) and optionally
clear the bad value from localStorage; update the initialization logic for
isPinned to use the safe parsed value from the try-catch and keep setIsPinned
usage the same.

Prevents calling setCollapsed after component unmounts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add try-catch for localStorage parsing to handle corrupted data
- Split timeout cleanup into separate useEffects for clearer unmount handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx (1)

224-239: ⚠️ Potential issue | 🟠 Major

Fix conditional hook calls to comply with React Rules of Hooks.

Both useUnstractSubscriptionPlanStore and useSelectedProductStore are called conditionally (lines 225 and 235). This violates React's Rules of Hooks: hooks must be called unconditionally at the top level of a component. If store availability changes between renders, the hook call order becomes inconsistent, causing bugs. Use a safe wrapper to ensure the hooks are always called.

🔧 Proposed fix (safe wrapper + unconditional call)
-  try {
-    if (unstractSubscriptionPlanStore?.useUnstractSubscriptionPlanStore) {
-      unstractSubscriptionPlan =
-        unstractSubscriptionPlanStore?.useUnstractSubscriptionPlanStore(
-          (state) => state?.unstractSubscriptionPlan,
-        );
-    }
-  } catch (error) {
-    // Do nothing
-  }
-
-  if (selectedProductStore?.useSelectedProductStore) {
-    selectedProduct = selectedProductStore.useSelectedProductStore(
-      (state) => state?.selectedProduct,
-    );
-  }
+  const useSafeUnstractSubscriptionPlanStore =
+    unstractSubscriptionPlanStore?.useUnstractSubscriptionPlanStore ??
+    ((selector) => selector?.({}));
+  const useSafeSelectedProductStore =
+    selectedProductStore?.useSelectedProductStore ?? ((selector) => selector?.({}));
+
+  try {
+    unstractSubscriptionPlan = useSafeUnstractSubscriptionPlanStore(
+      (state) => state?.unstractSubscriptionPlan,
+    );
+  } catch {
+    // Do nothing
+  }
+
+  selectedProduct = useSafeSelectedProductStore(
+    (state) => state?.selectedProduct,
+  );

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 157-179: The getSafeLocalStorageValue and SideNavBar useEffect
assume localStorage is always available—calling localStorage.removeItem in the
catch and localStorage.setItem in the effect can throw in SSR/tests or when
storage is blocked; add guards and safe wrappers: update
getSafeLocalStorageValue to first check typeof localStorage !== "undefined" and
only call getItem/removeItem when available (avoid calling removeItem
unconditionally in the catch), create a safeSetLocalStorage helper used in the
useEffect that wraps setItem in try/catch and no-ops when storage is unavailable
or errors, and replace direct localStorage calls in SideNavBar with these guards
(referencing getSafeLocalStorageValue, safeSetLocalStorage, and the useEffect
that persists sidebarPinned).
🧹 Nitpick comments (1)
frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx (1)

553-571: Prefer a semantic button for the pin control (with aria-pressed).

♻️ Suggested refactor
-      <Tooltip title={isPinned ? "Unpin sidebar" : "Keep expanded"}>
-        <div
-          className="sidebar-pin-container"
-          onClick={togglePin}
-          onKeyDown={(e) => {
-            if (e.key === "Enter" || e.key === " ") {
-              e.preventDefault();
-              togglePin();
-            }
-          }}
-          role="button"
-          tabIndex={0}
-        >
+      <Tooltip title={isPinned ? "Unpin sidebar" : "Keep expanded"}>
+        <button
+          type="button"
+          className="sidebar-pin-container"
+          onClick={togglePin}
+          aria-pressed={isPinned}
+          aria-label={isPinned ? "Unpin sidebar" : "Keep sidebar expanded"}
+        >
           {isPinned ? (
             <PushpinFilled className="sidebar-pin-icon pinned" />
           ) : (
             <PushpinOutlined className="sidebar-pin-icon" />
           )}
-        </div>
+        </button>
       </Tooltip>

- Create clearCollapseTimeout helper to eliminate duplication
- Combine 3 useEffects into 1 for localStorage, pin change, and unmount
- Simplify handleMouseEnter and handleMouseLeave

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 538-556: Replace the non-semantic div used for the pin control
with a native button element: in the block rendering the pin control (where
Tooltip wraps the element, and components/symbols involved include togglePin,
isPinned, PushpinFilled, PushpinOutlined, and the "sidebar-pin-container" /
"sidebar-pin-icon" classes), change the div to a <button type="button">, remove
role="button", tabIndex and the onKeyDown keyboard handling (the native button
handles Enter/Space), keep the onClick calling togglePin, preserve Tooltip,
className and conditional icon rendering, and add an explicit accessible name
via aria-label that reflects state (e.g., aria-label={isPinned ? "Unpin sidebar"
: "Keep expanded"}).

Menu items now have data-testid attributes (e.g., sidebar-workflows,
sidebar-api-deployments) that persist regardless of collapsed state,
allowing UI tests to find elements without relying on visible text.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Guard localStorage for SSR/tests with isLocalStorageAvailable check
- Wrap all localStorage operations in try-catch
- Replace div with native <button> for pin toggle (accessibility)
- Add aria-pressed and aria-label for screen reader support
- Update CSS to reset button default styles

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx`:
- Around line 194-229: The sidebar can remain collapsed when isPinned is loaded
true because useEffect only clears timeouts; update the isPinned change handling
to force expansion: inside the useEffect watching isPinned (the effect that
calls setSafeLocalStorageValue), when isPinned is true call setCollapsed(false)
(and clearCollapseTimeout via clearCollapseTimeout()) so any persisted pinned
state immediately expands; also ensure togglePin (which flips isPinned) keeps
the existing behavior of clearing timeouts and calling setCollapsed(false) when
newPinned is true. Reference functions/vars: useEffect (dependency [isPinned]),
setSafeLocalStorageValue, clearCollapseTimeout, setCollapsed, togglePin,
setIsPinned, collapseTimeoutRef.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace native button with Ant Design Button component
for consistency with rest of codebase.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Call setCollapsed(false) in useEffect when isPinned is true to ensure
  sidebar expands on initial load with persisted pinned state
- Add optional chaining to data iteration and item property access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create helpers/localStorage.js with getLocalStorageValue and
  setLocalStorageValue functions
- Update SideNavBar.jsx to use the common utility
- Update PageLayout.jsx to use the common utility with safe access

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 3, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants