This is an automated email from the ASF dual-hosted git repository.

sandeepk318 pushed a commit to branch frontend-refactor
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/frontend-refactor by this push:
     new 68f4b55616 AMBARI-26618 Ambari Web React: Implement 
authorization-aware Sidebar navigation (#4140)
68f4b55616 is described below

commit 68f4b556163a0b92a3479f24aac5a4ab2fa61eba
Author: Nikita Pande <[email protected]>
AuthorDate: Mon Jun 22 17:48:55 2026 +0530

    AMBARI-26618 Ambari Web React: Implement authorization-aware Sidebar 
navigation (#4140)
---
 .../Sidebar/{Sidebar.tsx => SideItem.tsx}          |  52 ++-
 .../latest/src/components/Sidebar/SideItemList.tsx | 275 ++++++++++++++
 .../latest/src/components/Sidebar/Sidebar.tsx      | 404 ++++++++++++++++++++-
 .../latest/src/components/Sidebar/SidebarItem.tsx  | 332 +++++++++++++++++
 .../components/Sidebar/SidebarItemCollapsed.tsx    | 112 ++++++
 5 files changed, 1127 insertions(+), 48 deletions(-)

diff --git a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx 
b/ambari-web/latest/src/components/Sidebar/SideItem.tsx
similarity index 53%
copy from ambari-web/latest/src/components/Sidebar/Sidebar.tsx
copy to ambari-web/latest/src/components/Sidebar/SideItem.tsx
index c1bbca5ab0..c4df0bf659 100644
--- a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
+++ b/ambari-web/latest/src/components/Sidebar/SideItem.tsx
@@ -15,37 +15,29 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+import { ReactNode } from "react";
 
-type SideBarProps = {
-  isRoot?: boolean;
-  isSidebarCollapsed: boolean;
-  setIsSidebarCollapsed: React.Dispatch<React.SetStateAction<boolean>>;
-  clusterExists?: boolean;
+type SideItem = {
+  id: string;
+  icon: ReactNode;
+  name: ReactNode;
+  path?: string;
+  children: SideItem[];
+  style?: unknown;
+  className?: string;
+  link?: string;
+  dataHref?: string;
+  noAlerts?: boolean;
+  hasCriticalAlerts?: boolean;
+  alertsCountDisplay?: string;
+  goToConfigs?: () => void;
+  isRestartRequired?: boolean;
+  restartRequiredMessage?: string;
+  isInPassive?: boolean;
+  maintenanceModeTooltip?: string;
+  dropDownIcon?: any;
+  sideItems?:boolean;
 };
 
-const SideBar = ({
-  isSidebarCollapsed,
-  setIsSidebarCollapsed,
-}: SideBarProps) => {
-  return isSidebarCollapsed ? (
-    <div className="sidebar-collapsed">
-      <button
-        className="toggle-button"
-        onClick={() => setIsSidebarCollapsed(false)}
-      >
-        &#9776;
-      </button>
-    </div>
-  ) : (
-    <div className="sidebar">
-      <button
-        className="toggle-button"
-        onClick={() => setIsSidebarCollapsed(true)}
-      >
-        &#9776;
-      </button>
-    </div>
-  );
-};
 
-export default SideBar;
+export default SideItem;
diff --git a/ambari-web/latest/src/components/Sidebar/SideItemList.tsx 
b/ambari-web/latest/src/components/Sidebar/SideItemList.tsx
new file mode 100644
index 0000000000..e98b5b3cb0
--- /dev/null
+++ b/ambari-web/latest/src/components/Sidebar/SideItemList.tsx
@@ -0,0 +1,275 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import SideItem from "./SideItem";
+import { Image } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+  faTachometerAlt,
+  faBriefcase,
+  faTasksAlt,
+  faBell,
+  faWrench 
+} from "@fortawesome/free-solid-svg-icons";
+import AmbariLogo from "../../assets/img/ambari-logo.png"
+
+enum SideItemLabels {
+  LOGO = "logo",
+  DASHBOARD = "dashboard",
+  SERVICES = "services",
+  HOSTS = "hosts",
+  ALERTS = "alerts",
+  CLUSTER_ADMIN = "cluster_admin",
+  STACK_AND_VERSIONS = "Stack and Versions",
+  SERVICE_ACCOUNTS = "Service Accounts",
+  KERBEROS = "kerberos",
+  SERVICE_AUTO_START = "Service Auto Start",
+}
+/**
+ * Generate sidebar items with authorization checks
+ * Based on Ember.js ui/app/views/main/admin.js authorization patterns
+ */
+const getSideItemList = (hasAuthorization: (auth: string) => boolean, 
upgradeInProgress: boolean = false, upgradeHolding: boolean = false): 
SideItem[] => {
+  const adminChildren: SideItem[] = [];
+  
+  // Stack and Versions - Requires CLUSTER.VIEW_STACK_DETAILS OR 
CLUSTER.UPGRADE_DOWNGRADE_STACK (matches Ember.js)
+  if (hasAuthorization('CLUSTER.VIEW_STACK_DETAILS, 
CLUSTER.UPGRADE_DOWNGRADE_STACK') || upgradeInProgress || upgradeHolding) {
+    adminChildren.push({
+      id: SideItemLabels.STACK_AND_VERSIONS,
+      icon: <></>,
+      name: "Stack And Versions",
+      path: "/main/admin/stack/services",
+      children: [],
+      style: {}
+    });
+  }
+  
+  // Service Accounts - Requires SERVICE.SET_SERVICE_USERS_GROUPS
+  if (hasAuthorization('SERVICE.SET_SERVICE_USERS_GROUPS') || 
upgradeInProgress || upgradeHolding) {
+    adminChildren.push({
+      id: SideItemLabels.SERVICE_ACCOUNTS,
+      icon: <></>,
+      name: "Service Accounts",
+      path: "/main/admin/serviceAccounts",
+      children: [],
+      style: {}
+    });
+  }
+  
+  // Kerberos - Requires CLUSTER.TOGGLE_KERBEROS
+  if (hasAuthorization('CLUSTER.TOGGLE_KERBEROS') || upgradeInProgress || 
upgradeHolding) {
+    adminChildren.push({
+      id: SideItemLabels.KERBEROS,
+      icon: <></>,
+      name: "Kerberos",
+      path: "/main/admin/kerberos",
+      children: [],
+      style: {}
+    });
+  }
+  
+  // Service Auto Start - Requires SERVICE.START_STOP authorization (matches 
ServiceAutoStart component)
+  if (hasAuthorization('SERVICE.START_STOP') || upgradeInProgress || 
upgradeHolding) {
+    adminChildren.push({
+      id: SideItemLabels.SERVICE_AUTO_START,
+      icon: <></>,
+      name: "Service Auto Start",
+      path: "/main/admin/serviceAutoStart",
+      children: [],
+      style: {}
+    });
+  }
+
+  const baseItems: SideItem[] = [
+    {
+      id: SideItemLabels.LOGO,
+      icon: (
+        <Image src={AmbariLogo} height={25} width={25} />
+      ),
+      name: <div className="fs-4">Ambari</div>,
+      path: "/main/dashboard",
+      children: [],
+      style: { background: "#313d54", height: "60px" },
+    },
+    {
+      id: SideItemLabels.DASHBOARD,
+      icon: <FontAwesomeIcon icon={faTachometerAlt} height={15} width={15} />,
+      name: "Dashboard",
+      path: "/main/dashboard",
+      children: [],
+      style: {},
+    },
+    {
+      id: SideItemLabels.SERVICES,
+      icon: <FontAwesomeIcon icon={faBriefcase} height={15} width={15} />,
+      name: "Services",
+      path: "/main/dashboard",
+      style: { position: "relative" },
+      sideItems: true,
+      children: [],
+    },
+    {
+      id: SideItemLabels.HOSTS,
+      icon: <FontAwesomeIcon icon={faTasksAlt} height={15} width={15} />,
+      name: "Hosts",
+      path: "/main/hosts",
+      children: [],
+      style: {}
+    },
+    {
+      id: SideItemLabels.ALERTS,
+      icon: <FontAwesomeIcon icon={faBell} height={15} width={15} />,
+      name: "Alerts",
+      path: "/main/alerts",
+      children: [],
+      style: {}
+    }
+  ];
+
+  // Only add Cluster Admin if user has any admin permissions
+  // This matches Ember.js ui/app/views/main/menu.js logic
+  const hasAnyAdminPermissions = hasAuthorization('CLUSTER.TOGGLE_KERBEROS') ||
+                                 hasAuthorization('CLUSTER.MODIFY_CONFIGS') ||
+                                 hasAuthorization('SERVICE.START_STOP') ||
+                                 
hasAuthorization('SERVICE.SET_SERVICE_USERS_GROUPS') ||
+                                 
hasAuthorization('CLUSTER.UPGRADE_DOWNGRADE_STACK') ||
+                                 
hasAuthorization('CLUSTER.VIEW_STACK_DETAILS') ||
+                                 upgradeInProgress || upgradeHolding;
+
+  if (hasAnyAdminPermissions && adminChildren.length > 0) {
+    baseItems.push({
+      id: SideItemLabels.CLUSTER_ADMIN,
+      icon: <FontAwesomeIcon icon={faWrench} height={15} width={15} />,
+      path: "/main/admin",
+      name: "Cluster Admin",
+      children: adminChildren,
+      sideItems: true,
+      style: {}
+    });
+  }
+
+  return baseItems;
+};
+
+// Default export for backward compatibility
+const SideItemList: SideItem[] = [
+  {
+    id: SideItemLabels.LOGO,
+    icon: (
+      <Image src={AmbariLogo} height={25} width={25} />
+    ),
+    name: <div className="fs-4">Ambari</div>,
+    path: "/dashboard",
+    children: [],
+    style: { background: "#313d54", height: "60px" },
+  },
+  {
+    id: SideItemLabels.DASHBOARD,
+    icon: <FontAwesomeIcon icon={faTachometerAlt} height={15} width={15} />,
+    name: "Dashboard",
+    path: "/main/dashboard",
+    children: [],
+    style: {},
+  },
+  {
+    id: SideItemLabels.SERVICES,
+    icon: <FontAwesomeIcon icon={faBriefcase} height={15} width={15} />,
+    name: "Services",
+    path: "/main/dashboard",
+    style: { position: "relative" },
+    sideItems: true,
+    children: [],
+  },
+  {
+    id: SideItemLabels.HOSTS,
+    icon: <FontAwesomeIcon icon={faTasksAlt} height={15} width={15} />,
+    name: "Hosts",
+    path: "/main/hosts",
+    children: [],
+    style: {}
+  },
+  {
+    id: SideItemLabels.ALERTS,
+    icon: <FontAwesomeIcon icon={faBell} height={15} width={15} />,
+    name: "Alerts",
+    path: "/main/alerts",
+    children: [],
+    style: {}
+  },
+  {
+    id: SideItemLabels.CLUSTER_ADMIN,
+    icon: <FontAwesomeIcon icon={faWrench} height={15} width={15} />,
+    path: "/main/admin",
+    name: "Cluster Admin",
+    children: [
+      {
+        id: SideItemLabels.STACK_AND_VERSIONS,
+        icon: <></>,
+        name: "Stack And Versions",
+        path: "/main/admin/stack/services",
+        children: [],
+        style: {}
+      },
+      {
+        id: SideItemLabels.SERVICE_ACCOUNTS,
+        icon: <></>,
+        name: "Service Accounts",
+        path: "/main/admin/serviceAccounts",
+        children: [],
+        style: {}
+      },
+      {
+        id: SideItemLabels.KERBEROS,
+        icon: <></>,
+        name: "Kerberos",
+        path: "/main/admin/kerberos",
+        children: [],
+        style: {}
+      },
+      {
+        id: SideItemLabels.SERVICE_AUTO_START,
+        icon: <></>,
+        name: "Service Auto Start",
+        path: "/main/admin/serviceAutoStart",
+        children: [],
+        style: {}
+      }
+    ],
+    sideItems: true,
+    style: {}
+  },
+];
+
+// const SideItemListComponent = () => {
+//   return (
+//     <nav className="sidebar-nav">
+//       <ul>
+//         {SideItemList.map((item, index) => (
+//           <li key={index}>
+//             <Link to="" className="sidebar-item">
+//               {item.icon}
+//               {item.name}
+//                 <span className="dropdown-icon">{item.dropDownIcon}</span>
+//             </Link>
+//           </li>
+//         ))}
+//       </ul>
+//     </nav>
+//   );
+// };
+
+export { SideItemList, SideItemLabels, getSideItemList };
diff --git a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx 
b/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
index c1bbca5ab0..718fcdb701 100644
--- a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
+++ b/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
@@ -15,7 +15,30 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-
+import React, { useEffect, useState } from "react";
+import SidebarItem from "./SidebarItem";
+import SideItem from "./SideItem.tsx";
+import SidebarItemCollapsed from "./SidebarItemCollapsed";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+  faAngleDoubleLeft,
+  faAngleDoubleRight,
+  faBriefcase,
+  faSync
+} from "@fortawesome/free-solid-svg-icons";
+import { SideItemLabels, getSideItemList } from "./SideItemList";
+import { AppContext } from "../../store/context.tsx";
+import { useContext } from "react";
+import { useAuth } from "../../hooks/useAuth";
+import { Collapse } from "react-bootstrap";
+import { ServiceContext } from "../../store/ServiceContext.tsx";
+import {
+  serviceNameModelMapping,
+  serviceNameDisplayMapping,
+} from "../../constants.ts";
+import { displayOrder } from "../../screens/ClusterWizard/constants";
+import { isEmpty } from "lodash";
+import { useLocation } from "react-router-dom";
 type SideBarProps = {
   isRoot?: boolean;
   isSidebarCollapsed: boolean;
@@ -27,25 +50,370 @@ const SideBar = ({
   isSidebarCollapsed,
   setIsSidebarCollapsed,
 }: SideBarProps) => {
-  return isSidebarCollapsed ? (
-    <div className="sidebar-collapsed">
-      <button
-        className="toggle-button"
-        onClick={() => setIsSidebarCollapsed(false)}
+  const { clusterName, services: contextServices, upgradeInProgress, 
upgradeHolding } = useContext(AppContext);
+  const [openOptions, setOpenOptions] = 
useState<string[]>([SideItemLabels.SERVICES]);
+  const [selectedOption, setSelectedOption] = useState<string>("");
+  const { allServiceModels  } = useContext(ServiceContext);
+  const location = useLocation();
+  
+  // Authorization hooks - implementing Ember.js menu authorization patterns
+  const { hasAuthorization } = useAuth();
+  
+  // Get authorization-aware sidebar items using computed upgrade properties
+  const authorizedSideItemList = getSideItemList(hasAuthorization, 
upgradeInProgress, upgradeHolding);
+  const [services, setServices] = useState<
+    {
+      name: string;
+      state: string;
+      alertsCountDisplay?: string;
+      noAlerts?: boolean;
+      hasCriticalAlerts?: boolean;
+      isClientOnlyService?: boolean;
+      isInPassiveForService?: boolean;
+      isRestartRequiredForService?: boolean;
+    }[]
+  >([]);
+
+  const isElementOpen = (id: string) => {
+    return openOptions.includes(id);
+  };
+
+  const handleSideItemClick = (itemId: string) => {
+    if (isElementOpen(itemId)) {
+      setOpenOptions(openOptions.filter((opt) => opt !== itemId));
+    } else {
+      setOpenOptions([...openOptions, itemId]);
+    }
+  };
+
+  useEffect(() => {
+    if (!clusterName || isEmpty(allServiceModels)) {
+      setServices([]);
+      return;
+    }
+
+    const processServices = () => {
+      
+      const processedServices = contextServices
+        ?.map((service: any) => {
+          const serviceName = service?.ServiceInfo?.service_name;
+        // Type assertion to tell TypeScript this is a valid key
+          const modelKey = serviceName && serviceNameModelMapping[serviceName 
as keyof typeof serviceNameModelMapping];
+          const currentServiceModel = allServiceModels[modelKey];
+
+          // Skip if model doesn't exist
+          if (!currentServiceModel) {
+            console.warn(
+              `No model found for service ${serviceName} with key ${modelKey}`
+            );
+            return null;
+          }
+
+          // Create the service object with proper state handling
+          const serviceData = {
+            name:
+              serviceNameDisplayMapping[
+                serviceName as keyof typeof serviceNameDisplayMapping
+              ] || serviceName,
+            serviceName: serviceName, // Keep original service name for sorting
+            state:
+              currentServiceModel["serviceState"] &&
+              currentServiceModel["serviceState"] !== ""
+                ? currentServiceModel["serviceState"]
+                : "UNKNOWN",
+            alertsCountDisplay:
+              currentServiceModel["alertsCount"] > 0
+                ? currentServiceModel["alertsCount"]
+                : undefined,
+            noAlerts: currentServiceModel["alertsCount"] === 0,
+            hasCriticalAlerts:
+              currentServiceModel["hasCriticalAlerts"] || false,
+            isClientOnlyService: currentServiceModel["isClientOnlyService"] || 
false,
+            isInPassiveForService: 
currentServiceModel["isInPassiveForService"] || false,
+            isRestartRequiredForService: 
currentServiceModel["isRestartRequiredForService"] || false
+          };
+
+          return serviceData;
+        })
+        .filter(Boolean)
+        // Sort services according to Ember.js displayOrder (matching 
App.StackService.displayOrder)
+        .sort((a: any, b: any) => {
+          const aIndex = displayOrder.indexOf(a.serviceName);
+          const bIndex = displayOrder.indexOf(b.serviceName);
+          
+          // If service is in displayOrder, use its index; otherwise, put it 
at the end
+          const aOrder = aIndex !== -1 ? aIndex : displayOrder.length;
+          const bOrder = bIndex !== -1 ? bIndex : displayOrder.length;
+          
+          return aOrder - bOrder;
+        }) as {
+          name: string;
+          serviceName: string;
+          state: string;
+          alertsCountDisplay?: string;
+          noAlerts?: boolean;
+          hasCriticalAlerts?: boolean;
+          isClientOnlyService?: boolean;
+          isInPassiveForService?: boolean;
+          isRestartRequiredForService?: boolean;
+        }[];; // Filter out any null values
+
+      setServices(processedServices);
+    };
+
+    processServices();
+  }, [JSON.stringify(allServiceModels), clusterName, contextServices]);
+
+  const getStateColor = (
+    state: string,
+    isClientOnlyService: boolean = false,
+  ): string | undefined => {
+    if (isClientOnlyService) {
+      return undefined; // Return undefined for client-only services to make 
the icon transparent
+    }
+    switch (state) {
+          case "STARTED":
+            return "#1eb475";
+          case "STOPPED" :
+          case "INSTALLED":
+            return "#ef6162";
+          default:
+            return "gray";
+    }   
+  };
+
+  const updatedSideItemList = authorizedSideItemList.map((item: SideItem) => {
+    if (item.id === SideItemLabels.SERVICES) {
+      return {
+        ...item,
+        children: services.map((service) => ({
+          id: service.name,
+          path: `/main/services/${service?.name?.replace(" 
","_")?.toUpperCase()}/summary`,
+          name: (
+            <div className="d-flex align-items-center w-100 pe-3">
+                <div
+                  className="rounded-circle me-2"
+                  style={{
+                    width: "8px",
+                    height: "8px",
+                    backgroundColor: getStateColor(
+                      service.state,
+                      service.isClientOnlyService ?? false,
+                    ),
+                  }}
+                ></div>
+              {/* Service name with conditional color based on state */}
+              <div className="flex-grow-1 text-truncate me-2" style={{ 
+                color: service.isClientOnlyService ? "inherit" : 
(service.state === "STOPPED" || service.state === "INSTALLED") ? "#ef6162" : 
"inherit",marginRight: "10px"}}>{service.name}</div>
+              {/* Maintenance icon (suitcase) - only show if service is in 
passive state */}
+              {service.isInPassiveForService === true && (
+                <FontAwesomeIcon icon={faBriefcase} className="me-2 " style={{ 
color: "#adb5bd" }} />
+              )}
+
+              {/* Refresh icon - only show if refresh is needed */}
+              {service.isRestartRequiredForService === true && (
+                <FontAwesomeIcon icon={faSync} className="me-2" style={{ 
color: "#adb5bd" }} />
+              )}
+              
+              {service.alertsCountDisplay &&
+                Number(service.alertsCountDisplay) > 0 && (
+                  <div className="ms-auto">
+                    <div
+                      className={`badge ${service.noAlerts ? "d-none" : ""} ${
+                        service.hasCriticalAlerts
+                          ? "bg-danger"
+                          : "bg-warning"
+                      } rounded-circle d-flex align-items-center 
justify-content-center`}
+                      style={{ width: "20px", height: "20px", fontSize: "10px" 
}}
+                    >
+                      {service.alertsCountDisplay}
+                    </div>
+                  </div>
+                )}
+            </div>
+          ),
+        })),
+      };
+    }
+    return item;
+  });
+
+  useEffect(() => {
+    const currentPath = location.pathname;
+    
+    const findMatchingItem = (items: any[]): string | null => {
+      let bestMatch: { id: string; pathLength: number } | null = null;
+      
+      for (const item of items) {
+        if (item.id === SideItemLabels.LOGO) {
+          continue;
+        }
+        
+        // Check if current path matches this item's path
+        if (item.path && currentPath.startsWith(item.path)) {
+          // Keep track of the best (longest) match
+          if (!bestMatch || item.path.length > bestMatch.pathLength) {
+            bestMatch = { id: item.id, pathLength: item.path.length };
+          }
+        }
+        
+        // Check children if they exist
+        if (item.children && item.children.length > 0) {
+          const childMatch = findMatchingItem(item.children);
+          if (childMatch) {
+            // If a child matches, also ensure parent is open
+            if (!openOptions.includes(item.id)) {
+              setOpenOptions(prev => [...prev, item.id]);
+            }
+            return childMatch;
+          }
+        }
+      }
+      
+      return bestMatch ? bestMatch.id : null;
+    };
+
+    // Find matching item in the updated sidebar list
+    const matchingItemId = findMatchingItem(updatedSideItemList);
+    
+    if (matchingItemId && matchingItemId !== selectedOption) {
+      setSelectedOption(matchingItemId);
+    }
+    
+    // Special handling for admin routes - ensure Cluster Admin is open when 
on admin pages
+    if (currentPath.startsWith('/main/admin') && 
!openOptions.includes(SideItemLabels.CLUSTER_ADMIN)) {
+      setOpenOptions(prev => [...prev, SideItemLabels.CLUSTER_ADMIN]);
+    }
+  }, [location.pathname, updatedSideItemList, selectedOption, openOptions]);
+
+  if (!isSidebarCollapsed) {
+    return (
+      <div
+        className="bg-secondary h-100 d-flex flex-column 
justify-content-between overflow-scroll no-scrollbar"
+        style={{ width: 230, position: "fixed", zIndex: 10 }}
       >
-        &#9776;
-      </button>
-    </div>
-  ) : (
-    <div className="sidebar">
-      <button
-        className="toggle-button"
-        onClick={() => setIsSidebarCollapsed(true)}
+        <div>
+          {updatedSideItemList.map((ele) => {
+            if (ele.children.length) {
+              return (
+                <div key={ele.id}>
+                  <SidebarItem
+                    isSelected={selectedOption === ele.id}
+                    ele={ele}
+                    isOpen={isElementOpen(ele.id)}
+                    hasChildren={ele.children.length > 0}
+                    onClick={() => {
+                      handleSideItemClick(ele.id);
+                    }}
+                  />
+                  <Collapse in={openOptions.includes(ele.id)}>
+                    <div>
+                      {ele.children.map((child: any) => {
+                        if (child.children && child.children.length > 0) {
+                          return (
+                            <div key={child.id}>
+                              <SidebarItem
+                                isSelected={selectedOption === child.id}
+                                ele={child}
+                                isOpen={isElementOpen(child.id)}
+                                hasChildren={child.children.length > 0}
+                                onClick={() => {
+                                  handleSideItemClick(child.id);
+                                }}
+                              />
+                              <Collapse in={openOptions.includes(child.id)}>
+                                <div>
+                                  {child.children.map((grandChild: any) => {
+                                    return (
+                                      <SidebarItem
+                                        key={grandChild.id}
+                                        isSelected={selectedOption === 
grandChild.id}
+                                        ele={grandChild}
+                                        onClick={() => {
+                                          setSelectedOption(grandChild.id);
+                                        }}
+                                      />
+                                    );
+                                  })}
+                                </div>
+                              </Collapse>
+                            </div>
+                          );
+                        } else {
+                          return (
+                            <SidebarItem
+                              key={child.id}
+                              isSelected={selectedOption === child.id}
+                              ele={child}
+                              onClick={() => {
+                                setSelectedOption(child.id);
+                              }}
+                            />
+                          );
+                        }
+                      })}
+                    </div>
+                  </Collapse>
+                </div>
+              );
+            } else {
+              return (
+                <SidebarItem
+                  key={ele.id}
+                  isSelected={selectedOption === ele.id}
+                  onClick={() => {
+                    setSelectedOption(ele.id);
+                  }}
+                  ele={ele}
+                />
+              );
+            }
+          })}
+        </div>
+        <div
+          className="py-3 d-flex justify-content-center text-primary"
+          style={{ background: "#313d54", cursor: "pointer" }}
+          onClick={() => {
+            setIsSidebarCollapsed(!isSidebarCollapsed);
+          }}
+        >
+          <FontAwesomeIcon icon={faAngleDoubleLeft} />
+        </div>
+      </div>
+    );
+  } else {
+    return (
+      <div
+        className="bg-secondary h-100 d-flex flex-column 
justify-content-between"
+        style={{ width: 60 }}
       >
-        &#9776;
-      </button>
-    </div>
-  );
+        <div>
+          {authorizedSideItemList.map((ele) => {
+            return (
+              <SidebarItemCollapsed
+                key={ele.id}
+                isSelected={selectedOption === ele.id}
+                ele={ele}
+                isOpen={isElementOpen(ele.id)}
+                childElements={ele.children}
+                setSelectedOption={setSelectedOption}
+                selectedOption={selectedOption}
+              />
+            );
+          })}
+        </div>
+        <div
+          className="py-3 d-flex justify-content-center text-primary"
+          style={{ background: "#313d54", cursor: "pointer" }}
+          onClick={() => {
+            setIsSidebarCollapsed(!isSidebarCollapsed);
+          }}
+        >
+          <FontAwesomeIcon icon={faAngleDoubleRight} />
+        </div>
+      </div>
+    );
+  }
 };
 
 export default SideBar;
diff --git a/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx 
b/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx
new file mode 100644
index 0000000000..f13db30b7c
--- /dev/null
+++ b/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx
@@ -0,0 +1,332 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+  faAdd,
+  faChevronDown,
+  faChevronRight,
+  faDownload,
+  faEllipsisH,
+  faPlay,
+  faRefresh,
+  faStop,
+} from "@fortawesome/free-solid-svg-icons";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { MouseEventHandler, useContext } from "react";
+import { Dropdown, DropdownButton } from "react-bootstrap";
+import { Link, useLocation, useNavigate } from "react-router-dom";
+import {
+  downloadClientConfigsCall,
+  restartAllRequired,
+  startAllServices,
+  stopAllServices,
+} from "../../Utils/taskUtils";
+import { AppContext } from "../../store/context";
+import { ServiceContext } from "../../store/ServiceContext";
+import showBackgroundModal from "../../Utils/showBg";
+import modalManager from "../../store/ModalManager";
+import { useAuth } from "../../hooks/useAuth";
+import { serviceNameModelMapping } from "../../constants";
+import RunAllServiceCheck from "./RunAllServiceCheck";
+
+// interface SidebarElement {
+//   id: string;
+//   path: string;
+//   className?: string;
+//   style?: React.CSSProperties;
+//   icon?: React.ReactNode;
+//   name: React.ReactNode;
+//   children?: Array<SidebarElement>;
+//   sideItems?: React.ReactNode;
+// }
+
+interface SidebarItemProps {
+  ele: any;
+  onClick?: MouseEventHandler<HTMLDivElement>;
+  isSelected: boolean;
+  isOpen?: boolean;
+  hasChildren?: boolean;
+}
+
+const SidebarItem = ({
+  ele,
+  onClick,
+  isSelected,
+  isOpen = false,
+  hasChildren = false,
+}: SidebarItemProps) => {
+  const navigate = useNavigate();
+  const {
+    clusterName,
+    upgradeIsRunning,
+    upgradeSuspended,
+    services: contextServices,
+  } = useContext(AppContext);
+  const { allServiceModels } = useContext(ServiceContext);
+  const location = useLocation();
+
+  // Authorization hooks - implementing Ember.js service menu authorization 
patterns
+  const { hasAuthorization } = useAuth();
+
+  // Check specific authorizations for service operations
+  const canAddDeleteServices = hasAuthorization("SERVICE.ADD_DELETE_SERVICES");
+  const canStartStopServices = hasAuthorization("SERVICE.START_STOP");
+  const canDownloadConfigs =
+    hasAuthorization("SERVICE.VIEW_CONFIGS") ||
+    hasAuthorization("CLUSTER.VIEW_CONFIGS");
+
+  // Check if upgrade is blocking operations (matches Ember.js logic)
+  const isUpgradeBlocking = upgradeIsRunning && !upgradeSuspended;
+
+  // Check service states for conditional button enabling/disabling
+  // Following Ember.js pattern where buttons are enabled based on actual 
service states
+  const getServiceStates = () => {
+    if (!contextServices || !allServiceModels) {
+      return {
+        hasStoppedServices: false,
+        hasStartedServices: false,
+        hasServicesRequiringRestart: false,
+        allServicesStarted: false,
+      };
+    }
+
+    let hasStoppedServices = false;
+    let hasStartedServices = false;
+    let hasServicesRequiringRestart = false;
+    let totalServices = 0;
+    let startedServicesCount = 0;
+    let stoppedServicesCount = 0;
+
+    contextServices.forEach((service: any) => {
+      const serviceName = service.ServiceInfo?.service_name;
+      const serviceState =
+        allServiceModels?.[serviceNameModelMapping[serviceName]]?.serviceState;
+
+      // Count total services (excluding client-only services for start/stop 
operations)
+      const isClientOnlyService = 
allServiceModels?.[serviceNameModelMapping[serviceName]]?.isClientOnlyService;
+      if (!isClientOnlyService) {
+        totalServices++;
+      }
+
+      // Following Ember.js logic: check for red status (stopped/installed 
services)
+      // Ember: stoppedServices = content.filter(_service => 
_service.get('healthStatus') === 'red')
+      if (
+        serviceState === "INSTALLED" ||
+        serviceState === "INIT" ||
+        serviceState === "INSTALL_FAILED" ||
+        serviceState === "STOPPED"
+      ) {
+        hasStoppedServices = true;
+        if (!isClientOnlyService) {
+          stoppedServicesCount++;
+        }
+      }
+
+      // Following Ember.js logic: check for green status (started services)
+      // Ember: !content.someProperty('healthStatus', 'green')
+      if (serviceState === "STARTED") {
+        hasStartedServices = true;
+        if (!isClientOnlyService) {
+          startedServicesCount++;
+        }
+      }
+
+      if 
(allServiceModels?.[serviceNameModelMapping[serviceName]]?.isRestartRequiredForService)
 {
+        hasServicesRequiringRestart = true;
+      }
+    });
+
+    // Check if ALL non-client-only services are started
+    const allServicesStarted = totalServices > 0 && startedServicesCount === 
totalServices;
+    
+    // Check if ALL non-client-only services are stopped
+    const allServicesStopped = totalServices > 0 && stoppedServicesCount === 
totalServices;
+
+    return {
+      hasStoppedServices,
+      hasStartedServices,
+      hasServicesRequiringRestart,
+      allServicesStarted,
+      allServicesStopped,
+    };
+  };
+
+  const {
+    hasServicesRequiringRestart,
+    allServicesStarted,
+    allServicesStopped,
+  } = getServiceStates();
+
+  // Check if user has any service operation permissions and upgrade is not 
blocking
+  const hasAnyServiceOperationPermissions =
+    (canAddDeleteServices || canStartStopServices) && !isUpgradeBlocking;
+  const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
+    // If this is the Cluster Admin item and it's currently closed, navigate 
to the first admin route
+    // Only navigate when opening the menu, not when closing it, and only if 
not already on an admin page
+    if (ele.id === "cluster_admin" && ele.sideItems && !isOpen && 
!location.pathname.startsWith("/main/admin")) {
+      e.preventDefault();
+      navigate("/main/admin");
+    }
+    // Call the original onClick handler
+    if (onClick) {
+      onClick(e);
+    }
+  };
+
+  return (
+    <Link
+      to={ele.sideItems ? location.pathname : ele.path}
+      className="text-decoration-none"
+    >
+      <div
+        className={`d-flex justify-content-between ${
+          ele.className
+        } sideitem align-items-center ${isSelected ? "selected-item" : ""}`}
+        style={{
+          ...(ele.style as any),
+          fontSize: 14,
+          cursor: "pointer",
+          padding: "10px 5px 10px 20px",
+          position: "relative",
+        }}
+        onClick={handleClick}
+      >
+        <div className="d-flex">
+          <div>{ele.icon}</div>
+          <div className="ms-2">{ele.name}</div>
+        </div>
+        <div className="d-flex align-items-center">
+          {ele.sideItems &&
+          ele.id === "services" &&
+          hasAnyServiceOperationPermissions ? (
+            <Dropdown>
+              <DropdownButton
+                variant="transparent"
+                onClick={(e) => {
+                  e.stopPropagation();
+                }}
+                className="service-dropdown"
+                title={
+                  <FontAwesomeIcon
+                    className="text-light me-1"
+                    icon={faEllipsisH}
+                  />
+                }
+              >
+                {/* Add Service - Requires SERVICE.ADD_DELETE_SERVICES 
authorization and no upgrade blocking */}
+                {canAddDeleteServices && !isUpgradeBlocking && (
+                  <Dropdown.Item
+                    onClick={() => {
+                      navigate("/main/service/add/step1");
+                    }}
+                  >
+                    <FontAwesomeIcon icon={faAdd} className="me-1" />
+                    Add Service
+                  </Dropdown.Item>
+                )}
+
+                {/* Start All - Disabled when all services are started 
(TLHASD-997 fix) */}
+                {canStartStopServices && (
+                  <Dropdown.Item
+                    disabled={allServicesStarted}
+                    onClick={async () => {
+                      if (!allServicesStarted) {
+                        await startAllServices(clusterName);
+                        showBackgroundModal();
+                      }
+                    }}
+                  >
+                    <FontAwesomeIcon icon={faPlay} className="me-1" />
+                    Start All
+                  </Dropdown.Item>
+                )}
+
+                {/* Stop All - Disabled when all services are stopped 
(TLHASD-997 consistency fix) */}
+                {canStartStopServices && (
+                  <Dropdown.Item
+                    disabled={allServicesStopped}
+                    onClick={async () => {
+                      if (!allServicesStopped) {
+                        await stopAllServices(clusterName);
+                        showBackgroundModal();
+                      }
+                    }}
+                  >
+                    <FontAwesomeIcon icon={faStop} className="me-1" />
+                    Stop All
+                  </Dropdown.Item>
+                )}
+
+                {/* Restart All Required - Disabled when no services require 
restart (following Ember.js isRestartAllRequiredDisabled logic) */}
+                {canStartStopServices && (
+                  <Dropdown.Item
+                    disabled={!hasServicesRequiringRestart}
+                    onClick={async () => {
+                      if (hasServicesRequiringRestart) {
+                        modalManager.show({
+                          modalTitle: "Confirmation",
+                          modalBody:
+                            "This will trigger alerts as the service is 
restarted. To suppress alerts, turn on Maintenance Mode for services listed 
above prior to running Restart All Required",
+                          successCallback: async () => {
+                            modalManager.hide();
+                            await restartAllRequired(clusterName);
+                            showBackgroundModal();
+                          },
+                          onClose: () => {
+                            modalManager.hide();
+                          },
+                          options: {},
+                        });
+                      }
+                    }}
+                  >
+                    <FontAwesomeIcon icon={faRefresh} className="me-1" />
+                    Restart All required
+                  </Dropdown.Item>
+                )}
+
+                {/* Download All Client Configs - Requires config view 
permissions */}
+                {canDownloadConfigs && (
+                  <Dropdown.Item
+                    onClick={() => {
+                      downloadClientConfigsCall({}, clusterName);
+                    }}
+                  >
+                    <FontAwesomeIcon icon={faDownload} className="me-1" />
+                    Download All Client Configs
+                  </Dropdown.Item>
+                )}
+                {/* Run All Service Check - Requires 
SERVICE.RUN_CUSTOM_COMMAND, SERVICE.RUN_SERVICE_CHECK, 
SERVICE.TOGGLE_MAINTENANCE, or SERVICE.ENABLE_HA */}
+                <RunAllServiceCheck />
+              </DropdownButton>
+            </Dropdown>
+          ) : null}
+          {hasChildren ? (
+            <>
+              <FontAwesomeIcon
+                icon={isOpen ? faChevronRight : faChevronDown}
+                className="me-1"
+              />
+            </>
+          ) : null}
+        </div>
+      </div>
+    </Link>
+  );
+};
+
+export default SidebarItem;
diff --git a/ambari-web/latest/src/components/Sidebar/SidebarItemCollapsed.tsx 
b/ambari-web/latest/src/components/Sidebar/SidebarItemCollapsed.tsx
new file mode 100644
index 0000000000..c5f1bcc79b
--- /dev/null
+++ b/ambari-web/latest/src/components/Sidebar/SidebarItemCollapsed.tsx
@@ -0,0 +1,112 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { MouseEventHandler, useState } from "react";
+import { Dropdown } from "react-bootstrap";
+import SidebarItem from "./SidebarItem";
+import { Link } from "react-router-dom";
+
+const SidebarItemCollapsed = ({
+  ele,
+  onClick,
+  isSelected,
+  childElements,
+  selectedOption,
+  setSelectedOption,
+}: {
+  ele: any;
+  onClick?: MouseEventHandler<HTMLDivElement>;
+  isSelected: boolean;
+  isOpen?: boolean;
+  hasChildren?: boolean;
+  childElements?: any[];
+  selectedOption?: string;
+  setSelectedOption?: any;
+}) => {
+  const [showDropdown, setShowDropdown] = useState(false);
+  if (childElements?.length) {
+    return (
+      <Dropdown
+        drop="end"
+        className="collapsed-sidebar-item"
+        onMouseLeave={() => setShowDropdown(false)}
+        onMouseOver={() => setShowDropdown(true)}
+        // style={{ width: "166px" }}
+      >
+        <Dropdown.Toggle as="div" className="main-style" id="dropdown-basic">
+          <div
+            className={`d-flex justify-content-between ${
+              ele.className
+            } sideitem align-items-center ${isSelected ? "selected-item" : 
""}`}
+            style={{
+              ...(ele.style as any),
+              fontSize: 14,
+              cursor: "pointer",
+              padding: "10px 5px 10px 20px",
+              position: "relative",
+            }}
+            onClick={onClick}
+          >
+            <div>{ele.icon}</div>
+          </div>
+        </Dropdown.Toggle>
+        {showDropdown ? (
+          <Dropdown.Menu
+            show={showDropdown}
+            style={{ width: "300px" }}
+            className="bg-secondary d-flex flex-column justify-content-between 
rounded-0 py-0"
+          >
+            {childElements?.map((ele) => {
+              return (
+                <SidebarItem
+                  onClick={() => {
+                    setSelectedOption(ele.id);
+                  }}
+                  ele={ele}
+                  isSelected={selectedOption === ele.id}
+                />
+              );
+            })}
+          </Dropdown.Menu>
+        ) : null}
+      </Dropdown>
+    );
+  } else {
+    return (
+      <div
+        className={`d-flex justify-content-between ${
+          ele.className
+        } sideitem align-items-center ${isSelected ? "selected-item" : ""}`}
+        style={{
+          ...(ele.style as any),
+          cursor: "pointer",
+          padding: "10px 5px 10px 20px",
+          position: "relative",
+        }}
+        onClick={() => {
+          setSelectedOption(ele.id);
+        }}
+      >
+        <Link to={ele.path} className="sideitem">
+          <div style={{ fontSize: 20 }}>{ele.icon}</div>
+        </Link>
+      </div>
+    );
+  }
+};
+
+export default SidebarItemCollapsed;


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to