diff --git a/frontend/src/components/navigations/side-nav-bar/SideNavBar.css b/frontend/src/components/navigations/side-nav-bar/SideNavBar.css index eed76f741f..29bca37208 100644 --- a/frontend/src/components/navigations/side-nav-bar/SideNavBar.css +++ b/frontend/src/components/navigations/side-nav-bar/SideNavBar.css @@ -1,6 +1,61 @@ .side-bar { background-color: #0d3a63 !important; + overflow: hidden; + height: 100%; +} + +/* Ant Design's internal wrapper - needs flex layout for scroll */ +.side-bar .ant-layout-sider-children { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +/* Wrapper for scrollable content */ +.sidebar-content-wrapper { + flex: 1; + min-height: 0; overflow-y: auto; + overflow-x: hidden; +} + +/* Hide scrollbar when sidebar is collapsed */ +.side-bar.ant-layout-sider-collapsed .sidebar-content-wrapper { + overflow-y: hidden; +} + +/* Pin container - fixed at bottom, centered */ +.sidebar-pin-container.ant-btn { + display: flex; + justify-content: center; + align-items: center; + padding: 8px; + height: auto; + border: none; + border-top: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 0; + flex-shrink: 0; + background: transparent; + width: 100%; +} + +.sidebar-pin-container.ant-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +.sidebar-pin-icon { + color: rgba(255, 255, 255, 0.6); + font-size: 16px; + transition: color 0.2s; +} + +.sidebar-pin-icon:hover { + color: white; +} + +.sidebar-pin-icon.pinned { + color: #1890ff; } .secondary-list-wrapper { diff --git a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx index 4caf155219..22a83986fd 100644 --- a/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx +++ b/frontend/src/components/navigations/side-nav-bar/SideNavBar.jsx @@ -1,6 +1,11 @@ -import { useMemo } from "react"; -import { BranchesOutlined } from "@ant-design/icons"; +import { useMemo, useState, useEffect, useRef } from "react"; import { + BranchesOutlined, + PushpinOutlined, + PushpinFilled, +} from "@ant-design/icons"; +import { + Button, Divider, Image, Layout, @@ -13,6 +18,10 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useSessionStore } from "../../../store/session-store"; +import { + getLocalStorageValue, + setLocalStorageValue, +} from "../../../helpers/localStorage"; import Workflows from "../../../assets/Workflows.svg"; import apiDeploy from "../../../assets/api-deployments.svg"; import CustomTools from "../../../assets/custom-tools-icon.svg"; @@ -150,11 +159,51 @@ SettingsPopoverContent.propTypes = { navigate: PropTypes.func.isRequired, }; -const SideNavBar = ({ collapsed }) => { +const SideNavBar = ({ collapsed, setCollapsed }) => { const navigate = useNavigate(); const { sessionDetails } = useSessionStore(); const { orgName, flags } = sessionDetails; + const [isPinned, setIsPinned] = useState(() => + getLocalStorageValue("sidebarPinned", false) + ); + const collapseTimeoutRef = useRef(null); + + const clearCollapseTimeout = () => { + if (collapseTimeoutRef.current) { + clearTimeout(collapseTimeoutRef.current); + collapseTimeoutRef.current = null; + } + }; + + useEffect(() => { + setLocalStorageValue("sidebarPinned", isPinned); + if (isPinned) { + clearCollapseTimeout(); + setCollapsed(false); + } + return clearCollapseTimeout; + }, [isPinned, setCollapsed]); + + const handleMouseEnter = () => { + clearCollapseTimeout(); + if (!isPinned) setCollapsed(false); + }; + + const handleMouseLeave = () => { + if (!isPinned) { + collapseTimeoutRef.current = setTimeout(() => setCollapsed(true), 300); + } + }; + + const togglePin = () => { + const newPinned = !isPinned; + setIsPinned(newPinned); + if (newPinned) { + setCollapsed(false); + } + }; + try { if (unstractSubscriptionPlanStore?.useUnstractSubscriptionPlanStore) { unstractSubscriptionPlan = @@ -354,8 +403,8 @@ const SideNavBar = ({ collapsed }) => { return unstractSubscriptionPlan?.remainingDays < 0; }, [unstractSubscriptionPlan]); - data.forEach((mainMenuItem) => { - mainMenuItem.subMenu.forEach((subMenuItem) => { + data?.forEach((mainMenuItem) => { + mainMenuItem?.subMenu?.forEach((subMenuItem) => { subMenuItem.disable = shouldDisableAll; }); }); @@ -368,33 +417,98 @@ const SideNavBar = ({ collapsed }) => { className="side-bar" width={240} collapsedWidth={65} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > -
-
- {data?.map((item, index) => ( -
- {!collapsed && ( - - {item.mainTitle} - - )} - - {item.subMenu.map((el) => { - // Platform item has a hover menu and click navigates to platform settings - if (el.id === 3.6) { - const handlePlatformClick = () => { - if (!el.disable) { - navigate(el.path); +
+
+
+ {data?.map((item, index) => ( +
+ {!collapsed && ( + + {item?.mainTitle} + + )} + + {item?.subMenu?.map((el) => { + // Platform item has a hover menu and click navigates to platform settings + if (el.id === 3.6) { + const handlePlatformClick = () => { + if (!el.disable) { + navigate(el.path); + } + }; + + const platformContent = ( + + + side_icon + {!collapsed && ( +
+ + {el.title} + + + {el.description} + +
+ )} +
+
+ ); + + // Don't show popover when disabled + if (el.disable) { + return
{platformContent}
; } - }; - const platformContent = ( - + return ( + + } + trigger="hover" + placement="rightTop" + arrow={false} + overlayClassName="settings-popover-overlay" + > + {platformContent} + + ); + } + + return ( + { + if (!el.disable) { + navigate(el.path); + } + }} + data-testid={`sidebar-${el.title + ?.toLowerCase() + ?.replace(/\s+/g, "-")}`} > { ); - - // Don't show popover when disabled - if (el.disable) { - return
{platformContent}
; - } - - return ( - - } - trigger="hover" - placement="rightTop" - arrow={false} - overlayClassName="settings-popover-overlay" - > - {platformContent} - - ); - } - - return ( - - { - if (!el.disable) { - navigate(el.path); - } - }} - > - side_icon - {!collapsed && ( -
- - {el.title} - - - {el.description} - -
- )} -
-
- ); - })} -
- {index < data.length - 1 && ( - - )} -
- ))} + })} + + {index < data.length - 1 && ( + + )} +
+ ))} +
+ +