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 = (
+
+
+
+ {!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);
- }
- }}
- >
-
- {!collapsed && (
-
-
- {el.title}
-
-
- {el.description}
-
-
- )}
-
-
- );
- })}
-
- {index < data.length - 1 && (
-
- )}
-
- ))}
+ })}
+
+ {index < data.length - 1 && (
+
+ )}
+
+ ))}
+
+
+
+ ) : (
+
+ )
+ }
+ />
+
);
};
SideNavBar.propTypes = {
collapsed: PropTypes.bool.isRequired,
+ setCollapsed: PropTypes.func.isRequired,
};
export default SideNavBar;
diff --git a/frontend/src/helpers/localStorage.js b/frontend/src/helpers/localStorage.js
new file mode 100644
index 0000000000..c734bdec1e
--- /dev/null
+++ b/frontend/src/helpers/localStorage.js
@@ -0,0 +1,35 @@
+/**
+ * Safe localStorage utilities with SSR/test environment support
+ */
+
+const isLocalStorageAvailable = () => {
+ try {
+ return typeof localStorage !== "undefined";
+ } catch {
+ return false;
+ }
+};
+
+export const getLocalStorageValue = (key, defaultValue) => {
+ if (!isLocalStorageAvailable()) return defaultValue;
+ try {
+ const item = localStorage.getItem(key);
+ return item ? JSON.parse(item) : defaultValue;
+ } catch {
+ try {
+ localStorage.removeItem(key);
+ } catch {
+ // Ignore storage errors
+ }
+ return defaultValue;
+ }
+};
+
+export const setLocalStorageValue = (key, value) => {
+ if (!isLocalStorageAvailable()) return;
+ try {
+ localStorage.setItem(key, JSON.stringify(value));
+ } catch {
+ // Ignore storage errors
+ }
+};
diff --git a/frontend/src/layouts/page-layout/PageLayout.css b/frontend/src/layouts/page-layout/PageLayout.css
index d0a18334ca..e31d920b15 100644
--- a/frontend/src/layouts/page-layout/PageLayout.css
+++ b/frontend/src/layouts/page-layout/PageLayout.css
@@ -14,12 +14,3 @@
.logoNoMargin {
margin-inline-end: 12px;
}
-
-.collapse_btn {
- width: 32px;
- height: 32px;
- margin-left: -10px;
- margin-top: -10px;
- position: fixed;
- z-index: 1000;
-}
diff --git a/frontend/src/layouts/page-layout/PageLayout.jsx b/frontend/src/layouts/page-layout/PageLayout.jsx
index b17589d163..5d45702e6c 100644
--- a/frontend/src/layouts/page-layout/PageLayout.jsx
+++ b/frontend/src/layouts/page-layout/PageLayout.jsx
@@ -1,5 +1,4 @@
-import { LeftOutlined, RightOutlined } from "@ant-design/icons";
-import { Button, Layout } from "antd";
+import { Layout } from "antd";
import { useEffect, useState } from "react";
import { Outlet } from "react-router-dom";
import PropTypes from "prop-types";
@@ -8,6 +7,10 @@ import "./PageLayout.css";
import SideNavBar from "../../components/navigations/side-nav-bar/SideNavBar.jsx";
import { TopNavBar } from "../../components/navigations/top-nav-bar/TopNavBar.jsx";
import { DisplayLogsAndNotifications } from "../../components/logs-and-notifications/DisplayLogsAndNotifications.jsx";
+import {
+ getLocalStorageValue,
+ setLocalStorageValue,
+} from "../../helpers/localStorage";
function PageLayout({
sideBarOptions,
@@ -15,29 +18,24 @@ function PageLayout({
showLogsAndNotifications = true,
hideSidebar = false,
}) {
- const initialCollapsedValue =
- JSON.parse(localStorage.getItem("collapsed")) || false;
- const [collapsed, setCollapsed] = useState(initialCollapsedValue);
+ const [collapsed, setCollapsed] = useState(() =>
+ getLocalStorageValue("collapsed", false)
+ );
useEffect(() => {
- localStorage.setItem("collapsed", JSON.stringify(collapsed));
+ setLocalStorageValue("collapsed", collapsed);
}, [collapsed]);
return (
{!hideSidebar && (
-
+
)}
- {!hideSidebar && (
- : }
- onClick={() => setCollapsed(!collapsed)}
- className="collapse_btn"
- />
- )}
{!hideSidebar && }
{showLogsAndNotifications && }