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

arshad 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 b5a2b54d39 AMBARI-26366 :: Ambari Web React: Hosts Summary Page (#4093)
b5a2b54d39 is described below

commit b5a2b54d39d96fe0ec28f768a9acc625e3c1d077
Author: Himanshu Maurya <[email protected]>
AuthorDate: Mon Dec 8 20:53:58 2025 +0530

    AMBARI-26366 :: Ambari Web React: Hosts Summary Page (#4093)
---
 ambari-web/latest/package.json                     |    2 +
 ambari-web/latest/src/constants.ts                 |   22 +
 ambari-web/latest/src/hooks/useDecommissionable.ts |  720 ++++++++++++
 .../latest/src/screens/Hosts/HostMetrics.tsx       |  127 +++
 .../latest/src/screens/Hosts/HostMetricsGraph.tsx  |  596 ++++++++++
 .../latest/src/screens/Hosts/HostSummary.tsx       | 1193 ++++++++++++++++++++
 ambari-web/latest/src/store/context.tsx            |    7 +-
 7 files changed, 2666 insertions(+), 1 deletion(-)

diff --git a/ambari-web/latest/package.json b/ambari-web/latest/package.json
index 2bb2f91177..024add53a2 100755
--- a/ambari-web/latest/package.json
+++ b/ambari-web/latest/package.json
@@ -17,6 +17,7 @@
     "@types/react-select": "^5.0.0",
     "axios": "^1.11.0",
     "bootstrap": "^5.3.6",
+    "chart.js": "^4.5.1",
     "classnames": "^2.5.1",
     "dayjs": "^1.11.13",
     "html-react-parser": "^5.2.6",
@@ -28,6 +29,7 @@
     "react": "^19.0.0",
     "react-bootstrap": "^2.10.10",
     "react-bootstrap-icons": "^1.11.6",
+    "react-chartjs-2": "^5.3.1",
     "react-dom": "^19.0.0",
     "react-hook-form": "^7.62.0",
     "react-hot-toast": "^2.5.2",
diff --git a/ambari-web/latest/src/constants.ts 
b/ambari-web/latest/src/constants.ts
index 401008d68e..4e39282312 100644
--- a/ambari-web/latest/src/constants.ts
+++ b/ambari-web/latest/src/constants.ts
@@ -15,6 +15,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
+
 export enum ClusterProgressStatus {
   PROVISIONING = "PROVISIONING",
   ENABLING_NAMENODE_HA = "ENABLING_NAMENODE_HA",
@@ -28,6 +29,26 @@ export enum ProgressStatus {
   FAILED = "FAILED",
 }
 
+export const serviceNameModelMapping: { [key: string]: string } = {
+  HDFS: "hdfs",
+  YARN: "yarn",
+  MAPREDUCE2: "mapreduce2",
+  TEZ: "tez",
+  HIVE: "hive",
+  HBASE: "hbase",
+  ZOOKEEPER: "zk",
+  AMBARI_METRICS: "ambari_metrics",
+  RANGER: "ranger",
+  RANGER_KMS: "ranger_kms",
+  KERBEROS: "kerberos",
+  SPARK3: "spark3",
+  SSM: "ssm",
+  TRINO: "trino",
+  SQOOP: "sqoop",
+  KYUUBI: "kyuubi",
+  TRINO_GATEWAY: "trino_gateway",
+  PINOT: "pinot",
+};
 
 export const serviceNameDisplayMapping = {
   HDFS: "HDFS",
@@ -47,4 +68,5 @@ export const serviceNameDisplayMapping = {
   SQOOP: "Sqoop",
   KYUUBI: "Kyuubi",
   TRINO_GATEWAY: "Trino Gateway",
+  PINOT: "Pinot",
 };
\ No newline at end of file
diff --git a/ambari-web/latest/src/hooks/useDecommissionable.ts 
b/ambari-web/latest/src/hooks/useDecommissionable.ts
new file mode 100644
index 0000000000..5e40d6b87b
--- /dev/null
+++ b/ambari-web/latest/src/hooks/useDecommissionable.ts
@@ -0,0 +1,720 @@
+/**
+ * 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 { useContext, useEffect, useState, useCallback, useRef } from "react";
+import { IHost } from "../models/host";
+import { get, isEmpty, set } from "lodash";
+import { ServiceContext } from "../store/ServiceContext";
+import { HostsApi } from "../api/hostsApi";
+import { AppContext } from "../store/context";
+import { IHostComponent } from "../models/hostComponent";
+import { getComponentName } from "../screens/Hosts/utils";
+import { ComponentStatus } from "../screens/Hosts/enums";
+import { serviceNameModelMapping } from "../constants";
+
+export const decommissionableComponents = [
+  "DATANODE",
+  "NODEMANAGER",
+  "HBASE_REGIONSERVER",
+  "TASKTRACKER",
+];
+const POLLING_INTERVAL = 6000;
+
+interface DecommissionState {
+  componentForCheckDecommission: string;
+  isComponentRecommissionAvailable: boolean;
+  isComponentDecommissionAvailable: boolean;
+  isComponentDecommissioning: boolean;
+}
+
+interface DecommissionableState {
+  DATANODE: DecommissionState;
+  NODEMANAGER: DecommissionState;
+  HBASE_REGIONSERVER: DecommissionState;
+  TASKTRACKER: DecommissionState;
+}
+
+abstract class BaseDecommissionableComponent {
+  protected serviceModels: any;
+  protected clusterName: string;
+  protected setDecommissionable: React.Dispatch<
+    React.SetStateAction<DecommissionableState>
+  >;
+  protected startPolling: (component: IHostComponent) => void;
+  protected stopPolling: (component: IHostComponent) => void;
+
+  constructor(
+    serviceModels: any,
+    clusterName: string,
+    setDecommissionable: React.Dispatch<
+      React.SetStateAction<DecommissionableState>
+    >,
+    startPolling: (component: IHostComponent) => void,
+    stopPolling: (component: IHostComponent) => void
+  ) {
+    this.serviceModels = serviceModels;
+    this.clusterName = clusterName;
+    this.setDecommissionable = setDecommissionable;
+    this.startPolling = startPolling;
+    this.stopPolling = stopPolling;
+  }
+
+  abstract loadDecommissionStatus(component: IHostComponent): Promise<void>;
+  abstract setDesiredAdminState(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void;
+
+  protected setStatusAs(status: string, component: IHostComponent): void {
+    const componentName = getComponentName(component);
+    const isStart = [
+      ComponentStatus.STARTED,
+      ComponentStatus.STARTING,
+    ].includes(get(component, "workStatus") as ComponentStatus);
+
+    this.setDecommissionable((prevState) => {
+      const updatedState: any = { ...prevState };
+
+      switch (status) {
+        case "INSERVICE":
+          updatedState[componentName] = {
+            ...updatedState[componentName],
+            isComponentRecommissionAvailable: false,
+            isComponentDecommissioning: false,
+            isComponentDecommissionAvailable: isStart,
+          };
+          break;
+
+        case "DECOMMISSIONING":
+          updatedState[componentName] = {
+            ...updatedState[componentName],
+            isComponentRecommissionAvailable: true,
+            isComponentDecommissioning: true,
+            isComponentDecommissionAvailable: false,
+          };
+          // Start polling when decommissioning
+          this.startPolling(component);
+          break;
+
+        case "DECOMMISSIONED":
+          updatedState[componentName] = {
+            ...updatedState[componentName],
+            isComponentRecommissionAvailable: true,
+            isComponentDecommissioning: false,
+            isComponentDecommissionAvailable: false,
+          };
+          // Stop polling when decommissioned
+          this.stopPolling(component);
+          break;
+
+        case "RS_DECOMMISSIONED":
+          updatedState[componentName] = {
+            ...updatedState[componentName],
+            isComponentRecommissionAvailable: true,
+            isComponentDecommissioning: isStart,
+            isComponentDecommissionAvailable: false,
+          };
+          break;
+      }
+
+      return updatedState;
+    });
+  }
+
+  protected async getDesiredAdminState(
+    component: IHostComponent
+  ): Promise<string | null> {
+    if (!component) return null;
+
+    try {
+      const response = await HostsApi.getSlaveDesiredAdminState(
+        this.clusterName,
+        get(component, "hostName"),
+        getComponentName(component)
+      );
+      const status = get(response, "HostRoles.desired_admin_state");
+      if (status) {
+        this.setDesiredAdminState(status, component);
+        return status;
+      }
+      return null;
+    } catch (error) {
+      return null;
+    }
+  }
+}
+
+class DataNodeComponent extends BaseDecommissionableComponent {
+  async loadDecommissionStatus(component: IHostComponent): Promise<void> {
+    await this.getDNDecommissionStatus(component);
+  }
+
+  setDesiredAdminState(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    this.setStatusAs(desiredAdminState, component);
+  }
+
+  private async getDNDecommissionStatus(
+    component: IHostComponent
+  ): Promise<void> {
+    const hdfs = get(this.serviceModels, "hdfs", {});
+    let activeNNHostNames = "";
+
+    if (
+      !get(hdfs, "snameNode") &&
+      get(hdfs, "activeNameNodes", []).length > 0
+    ) {
+      activeNNHostNames = get(hdfs, "activeNameNodes", [])
+        .map((nn: any) => get(nn, "hostName"))
+        .join(",");
+    } else {
+      activeNNHostNames = get(hdfs, "nameNode.hostName");
+    }
+
+    try {
+      const response = await HostsApi.getDecommissionStatusForDataNode(
+        this.clusterName,
+        activeNNHostNames
+      );
+      this.handleDecommissionStatusResponse(response, component);
+    } catch (error) {
+      console.error("Failed to get DataNode decommission status");
+    }
+  }
+
+  private handleDecommissionStatusResponse(
+    response: any,
+    component: IHostComponent
+  ): void {
+    if (response && response.items) {
+      const statusObjects = response.items.map((item: any) =>
+        get(item, "metrics.dfs.namenode")
+      );
+      this.computeStatus(statusObjects, component);
+    }
+  }
+
+  private computeStatus(metricObjects: any[], component: IHostComponent): void 
{
+    const hostName = get(component, "hostName");
+    let inServiceCount = 0;
+    let decommissioningCount = 0;
+    let decommissionedCount = 0;
+
+    metricObjects.forEach((curObj) => {
+      if (curObj) {
+        const liveNodesJson = JSON.parse(curObj.LiveNodes || "{}");
+        for (const hostPort in liveNodesJson) {
+          if (hostPort.indexOf(hostName) === 0) {
+            switch (liveNodesJson[hostPort].adminState) {
+              case "In Service":
+                inServiceCount++;
+                break;
+              case "Decommission In Progress":
+                decommissioningCount++;
+                break;
+              case "Decommissioned":
+                decommissionedCount++;
+                break;
+            }
+            return;
+          }
+        }
+      }
+    });
+
+    if (decommissioningCount) {
+      this.setStatusAs("DECOMMISSIONING", component);
+    } else if (inServiceCount && !decommissionedCount) {
+      this.setStatusAs("INSERVICE", component);
+    } else if (!inServiceCount && decommissionedCount) {
+      this.setStatusAs("DECOMMISSIONED", component);
+    } else {
+      // If namenodes are down, get desired_admin_state to decide if the user 
had issued a decommission
+      this.getDesiredAdminState(component);
+    }
+  }
+}
+
+class NodeManagerComponent extends BaseDecommissionableComponent {
+  async loadDecommissionStatus(component: IHostComponent): Promise<void> {
+    await this.getDesiredAdminState(component);
+  }
+
+  setDesiredAdminState(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    switch (desiredAdminState) {
+      case "INSERVICE":
+        this.setStatusAs(desiredAdminState, component);
+        break;
+      case "DECOMMISSIONED":
+        this.getDecommissionStatus(component);
+        break;
+    }
+  }
+
+  private async getDecommissionStatus(
+    component: IHostComponent
+  ): Promise<void> {
+    if (!component) return;
+
+    const serviceName = get(component, "serviceName");
+    const componentName = "RESOURCEMANAGER";
+
+    try {
+      const response = await HostsApi.getDecommissionStatus(
+        this.clusterName,
+        serviceName,
+        componentName
+      );
+      this.handleDecommissionStatusResponse(response, component);
+    } catch (error) {}
+  }
+
+  private handleDecommissionStatusResponse(
+    response: any,
+    component: IHostComponent
+  ): void {
+    const statusObject = get(response, "ServiceComponentInfo");
+    if (statusObject) {
+      set(
+        statusObject,
+        "component_state",
+        get(response, "host_components.[0].HostRoles.state")
+      );
+      this.setDecommissionStatusForNodeManager(statusObject, component);
+    }
+  }
+
+  private setDecommissionStatusForNodeManager(
+    curObj: any,
+    component: IHostComponent
+  ): void {
+    const hostName = get(component, "hostName");
+
+    const rmComponent = get(
+      this.serviceModels,
+      "yarn.masterComponents",
+      []
+    ).find((mc: any) => get(mc, "componentName") === "RESOURCEMANAGER");
+    if (rmComponent) {
+      set(rmComponent, "workStatus", curObj.component_state);
+    }
+
+    if (curObj.rm_metrics) {
+      // Update RESOURCEMANAGER status
+      const nodeManagersArray = JSON.parse(
+        curObj.rm_metrics.cluster.nodeManagers
+      );
+      if (nodeManagersArray.find((nm: any) => nm.HostName === hostName)) {
+        // decommissioning ..
+        this.setStatusAs("DECOMMISSIONING", component);
+      } else {
+        // decommissioned ..
+        this.setStatusAs("DECOMMISSIONED", component);
+      }
+    } else {
+      // in this case ResourceManager not started. Set status to Decommissioned
+      this.setStatusAs("DECOMMISSIONED", component);
+    }
+  }
+}
+
+class TaskTrackerComponent extends BaseDecommissionableComponent {
+  async loadDecommissionStatus(component: IHostComponent): Promise<void> {
+    await this.getDesiredAdminState(component);
+  }
+
+  setDesiredAdminState(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    switch (desiredAdminState) {
+      case "INSERVICE":
+        this.setStatusAs("INSERVICE", component);
+        break;
+      case "DECOMMISSIONED":
+        this.getDecommissionStatus(component);
+        break;
+    }
+  }
+
+  private async getDecommissionStatus(
+    component: IHostComponent
+  ): Promise<void> {
+    if (!component) return;
+
+    const serviceName = get(component, "serviceName");
+    const componentName = "JOBTRACKER";
+
+    try {
+      const response = await HostsApi.getDecommissionStatus(
+        this.clusterName,
+        serviceName,
+        componentName
+      );
+      this.handleDecommissionStatusResponse(response, component);
+    } catch (error) {}
+  }
+
+  private handleDecommissionStatusResponse(
+    response: any,
+    component: IHostComponent
+  ): void {
+    const statusObject = get(response, "ServiceComponentInfo");
+    if (statusObject) {
+      set(
+        statusObject,
+        "component_state",
+        get(response, "host_components.[0].HostRoles.state")
+      );
+      this.setDecommissionStatusForTaskTracker(statusObject, component);
+    }
+  }
+
+  private setDecommissionStatusForTaskTracker(
+    curObj: any,
+    component: IHostComponent
+  ): void {
+    const hostName = get(component, "hostName");
+
+    if (curObj) {
+      const aliveNodesArray = JSON.parse(curObj.AliveNodes || "[]");
+      if (aliveNodesArray && Array.isArray(aliveNodesArray)) {
+        if (aliveNodesArray.some((node) => node.hostname === hostName)) {
+          // decommissioning ..
+          this.setStatusAs("DECOMMISSIONING", component);
+        } else {
+          // decommissioned
+          this.setStatusAs("DECOMMISSIONED", component);
+        }
+      }
+    }
+  }
+}
+
+class RegionServerComponent extends BaseDecommissionableComponent {
+  async loadDecommissionStatus(component: IHostComponent): Promise<void> {
+    await this.getDesiredAdminState(component);
+  }
+
+  setDesiredAdminState(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    this.getRSDecommissionStatus(desiredAdminState, component);
+  }
+
+  private async getRSDecommissionStatus(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): Promise<void> {
+    const hostName = get(
+      this.serviceModels,
+      "hbase.masterComponents.[0].hostComponents.[0].HostRoles.host_name"
+    );
+    if (!hostName) {
+      return;
+    }
+
+    try {
+      const response = await HostsApi.getDecommissionStatusForRegionServer(
+        this.clusterName,
+        hostName
+      );
+      this.handleRSDecommissionStatusResponse(
+        response,
+        desiredAdminState,
+        component
+      );
+    } catch (error) {
+      this.setDesiredAdminStateDefault(desiredAdminState, component);
+    }
+  }
+
+  private handleRSDecommissionStatusResponse(
+    data: any,
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    const hostName = get(component, "hostName");
+
+    if (data) {
+      const liveRSHostsMetrics = get(
+        data,
+        "items.[0].metrics.hbase.master.liveRegionServersHosts"
+      );
+      const deadRSHostsMetrics = get(
+        data,
+        "items.[0].metrics.hbase.master.deadRegionServersHosts"
+      );
+
+      const liveRSHosts = this.parseRegionServersHosts(liveRSHostsMetrics);
+      const deadRSHosts = this.parseRegionServersHosts(deadRSHostsMetrics);
+
+      const isLiveRS = liveRSHosts.includes(hostName);
+      const isDeadRS = deadRSHosts.includes(hostName);
+
+      const isInServiceDesired = desiredAdminState === "INSERVICE";
+      const isDecommissionedDesired = desiredAdminState === "DECOMMISSIONED";
+
+      if (
+        liveRSHosts.length + deadRSHosts.length === 0 ||
+        (isInServiceDesired && isLiveRS) ||
+        (isDecommissionedDesired && isDeadRS)
+      ) {
+        this.setDesiredAdminStateDefault(desiredAdminState, component);
+      } else if (isInServiceDesired) {
+        this.setStatusAs("RS_DECOMMISSIONED", component);
+      } else if (isDecommissionedDesired) {
+        this.setStatusAs("INSERVICE", component);
+      }
+    } else {
+      this.setDesiredAdminStateDefault(desiredAdminState, component);
+    }
+  }
+
+  private parseRegionServersHosts(str: string): string[] {
+    const items = str ? str.split(";") : [];
+    return items.map((item) => item.split(",")[0]);
+  }
+
+  private setDesiredAdminStateDefault(
+    desiredAdminState: string,
+    component: IHostComponent
+  ): void {
+    switch (desiredAdminState) {
+      case "INSERVICE":
+        this.setStatusAs(desiredAdminState, component);
+        break;
+      case "DECOMMISSIONED":
+        this.setStatusAs("RS_DECOMMISSIONED", component);
+        break;
+    }
+  }
+}
+
+class DecommissionableComponentFactory {
+  static createComponent(
+    componentName: string,
+    serviceModels: any,
+    clusterName: string,
+    setDecommissionable: React.Dispatch<
+      React.SetStateAction<DecommissionableState>
+    >,
+    startPolling: (component: IHostComponent) => void,
+    stopPolling: (component: IHostComponent) => void
+  ): BaseDecommissionableComponent | null {
+    switch (componentName) {
+      case "DATANODE":
+        return new DataNodeComponent(
+          serviceModels,
+          clusterName,
+          setDecommissionable,
+          startPolling,
+          stopPolling
+        );
+      case "NODEMANAGER":
+        return new NodeManagerComponent(
+          serviceModels,
+          clusterName,
+          setDecommissionable,
+          startPolling,
+          stopPolling
+        );
+      case "HBASE_REGIONSERVER":
+        return new RegionServerComponent(
+          serviceModels,
+          clusterName,
+          setDecommissionable,
+          startPolling,
+          stopPolling
+        );
+      case "TASKTRACKER":
+        return new TaskTrackerComponent(
+          serviceModels,
+          clusterName,
+          setDecommissionable,
+          startPolling,
+          stopPolling
+        );
+      default:
+        return null;
+    }
+  }
+}
+
+// Main hook
+export const useDecommissionable = (host: IHost) => {
+  const { allServiceModels: serviceModels } = useContext(ServiceContext);
+  const { clusterName } = useContext(AppContext);
+  const [decommissionable, setDecommissionable] =
+    useState<DecommissionableState>({
+      DATANODE: {
+        componentForCheckDecommission: "NAMENODE",
+        isComponentRecommissionAvailable: false,
+        isComponentDecommissionAvailable: false,
+        isComponentDecommissioning: false,
+      },
+      NODEMANAGER: {
+        componentForCheckDecommission: "RESOURCEMANAGER",
+        isComponentRecommissionAvailable: false,
+        isComponentDecommissionAvailable: false,
+        isComponentDecommissioning: false,
+      },
+      HBASE_REGIONSERVER: {
+        componentForCheckDecommission: "HBASE_MASTER",
+        isComponentRecommissionAvailable: false,
+        isComponentDecommissionAvailable: false,
+        isComponentDecommissioning: false,
+      },
+      TASKTRACKER: {
+        componentForCheckDecommission: "JOBTRACKER",
+        isComponentRecommissionAvailable: false,
+        isComponentDecommissionAvailable: false,
+        isComponentDecommissioning: false,
+      },
+    });
+
+  const pollingTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
+
+  useEffect(() => {
+    return () => {
+      pollingTimers.current.forEach((timer) => {
+        clearTimeout(timer);
+      });
+      pollingTimers.current.clear();
+    };
+  }, []);
+
+  const startDecommissionStatusPolling = useCallback(
+    (component: IHostComponent) => {
+      const componentKey = `${get(component, "hostName")}_${getComponentName(
+        component
+      )}`;
+
+      if (!pollingTimers.current.has(componentKey)) {
+        const pollStatus = async () => {
+          try {
+            await loadComponentDecommissionStatus(component);
+            const timer = setTimeout(pollStatus, POLLING_INTERVAL);
+            pollingTimers.current.set(componentKey, timer);
+          } catch (error) {
+            console.error("Error during decommission status polling:", error);
+            pollingTimers.current.delete(componentKey);
+          }
+        };
+
+        const timer = setTimeout(pollStatus, POLLING_INTERVAL);
+        pollingTimers.current.set(componentKey, timer);
+      }
+    },
+    []
+  );
+
+  const stopDecommissionStatusPolling = useCallback(
+    (component: IHostComponent) => {
+      const componentKey = `${get(component, "hostName")}_${getComponentName(
+        component
+      )}`;
+      const timer = pollingTimers.current.get(componentKey);
+
+      if (timer) {
+        clearTimeout(timer);
+        pollingTimers.current.delete(componentKey);
+      }
+    },
+    []
+  );
+
+  const isComponentDecommissionDisable = (
+    component: IHostComponent
+  ): boolean => {
+    const componentName = getComponentName(component);
+    const masterComponentName = get(
+      decommissionable,
+      `${componentName}.componentForCheckDecommission`
+    );
+
+    if (!masterComponentName) return false;
+
+    const service = get(
+      serviceModels,
+      get(serviceNameModelMapping, component.serviceName)
+    );
+
+    const masterComponents = get(service, "masterComponents", []);
+    const masterComponent = masterComponents.find(
+      (mc: any) => get(mc, "componentName") === masterComponentName
+    );
+
+    if (masterComponent) {
+      const masterHostComponents = get(masterComponent, "hostComponents", []);
+      const hasStoppedMaster = masterHostComponents.some(
+        (hc: any) => get(hc, "state") !== ComponentStatus.STARTED
+      );
+      if (hasStoppedMaster) return true;
+    }
+
+    const serviceWorkStatus = get(service, "serviceState");
+    return serviceWorkStatus !== ComponentStatus.STARTED;
+  };
+
+  const loadComponentDecommissionStatus = async (
+    component: IHostComponent
+  ): Promise<void> => {
+    const componentName = getComponentName(component);
+    const componentHandler = DecommissionableComponentFactory.createComponent(
+      componentName,
+      serviceModels,
+      clusterName,
+      setDecommissionable,
+      startDecommissionStatusPolling,
+      stopDecommissionStatusPolling
+    );
+
+    if (componentHandler) {
+      await componentHandler.loadDecommissionStatus(component);
+    }
+  };
+
+  useEffect(() => {
+    if (!isEmpty(host) && !isEmpty(get(serviceModels, "hdfs.nameNode"))) {
+      get(host, "hostComponents", []).forEach(
+        (hostComponent: IHostComponent) => {
+          loadComponentDecommissionStatus(hostComponent);
+        }
+      );
+    }
+  }, [
+    JSON.stringify(host),
+    JSON.stringify(get(serviceModels, "hdfs.nameNode")),
+    JSON.stringify(get(serviceModels, "hbase.masterComponents")),
+  ]);
+
+  return {
+    decommissionable,
+    isComponentDecommissionDisable,
+    startDecommissionStatusPolling,
+    stopDecommissionStatusPolling,
+    loadComponentDecommissionStatus,
+  };
+};
diff --git a/ambari-web/latest/src/screens/Hosts/HostMetrics.tsx 
b/ambari-web/latest/src/screens/Hosts/HostMetrics.tsx
new file mode 100644
index 0000000000..7604c12cbb
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/HostMetrics.tsx
@@ -0,0 +1,127 @@
+/**
+ * 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 { Card, Dropdown } from "react-bootstrap";
+import { hostMetricsOption } from "./constants";
+import { get } from "lodash";
+import { getComponentName } from "./utils";
+import { IHost } from "../../models/host";
+//TODO: Enable these widgets when metrics are available
+// import NameNodeHeap from "../Dashboard/widgets/NameNodeHeap";
+// import NameNodeRpc from "../Dashboard/widgets/NameNodeRpc";
+// import NameNodeUptime from "../Dashboard/widgets/NameNodeUptime";
+import HostMetricsGraph from "./HostMetricsGraph";
+import { translate } from "../../Utils/Utility";
+// import NameNodeCpuPieChartView from 
"../Dashboard/widgets/NameNodeCpuPieChartView";
+
+type HostMetricsProps = {
+  metricsData: any;
+  allHostModels: IHost[];
+  selectedMetricsOption: string;
+  setSelectedMetricsOption: (option: string) => void;
+  setShowSelectTimeModal: (show: boolean) => void;
+};
+
+export const HostMetrics = ({
+  metricsData,
+  allHostModels,
+  selectedMetricsOption,
+  setSelectedMetricsOption,
+  setShowSelectTimeModal,
+}: HostMetricsProps) => {
+  const hasNameNode = () => {
+    return get(allHostModels, "[0].hostComponents", []).some(
+      (component: any) => getComponentName(component) === "NAMENODE"
+    );
+  };
+
+  return (
+    <Card className="w-50 rounded-0">
+      <div className="d-flex justify-content-between px-3 pt-3">
+        <h3 className="mt-2">{translate("hosts.host.summary.hostMetrics")}</h3>
+        <Dropdown>
+          <Dropdown.Toggle variant="transparent" className="btn-default">
+            <span className="me-2">{selectedMetricsOption}</span>
+          </Dropdown.Toggle>
+          <Dropdown.Menu className="rounded-0">
+            {hostMetricsOption.map((option) => (
+              <Dropdown.Item
+                key={option}
+                onClick={() => {
+                  setSelectedMetricsOption(option);
+                }}
+              >
+                {option}
+              </Dropdown.Item>
+            ))}
+            <Dropdown.Item onClick={() => setShowSelectTimeModal(true)}>
+              {translate("common.custom")}
+            </Dropdown.Item>
+          </Dropdown.Menu>
+        </Dropdown>
+      </div>
+      <hr />
+      <div>
+        <HostMetricsGraph
+          selectedMetricsOption={selectedMetricsOption}
+          metricsData={get(metricsData, "metrics", {})}
+        />
+        {hasNameNode() ? (
+          <div>
+            <div className="d-flex mb-4">
+              <Card className="widget-card h-100 border-light border-2 w-50 
mx-4 rounded-0">
+                <div className="px-4 pt-4">
+                  {translate("dashboard.widgets.NameNodeHeap")}
+                </div>
+                <div className="d-flex justify-content-center pb-4 pt-3 
text-muted">
+                  {/* <NameNodeHeap /> */}
+                </div>
+              </Card>
+              <Card className="widget-card h-100 border-light border-2 w-50 
mx-4 rounded-0">
+                <div className="px-4 pt-4">
+                  {translate("dashboard.widgets.NameNodeCpu")}
+                </div>
+                <div className="d-flex justify-content-center pb-4 pt-3 
text-muted">
+                  {/* <NameNodeCpuPieChartView /> */}
+                </div>
+              </Card>
+            </div>
+            <div className="d-flex mb-4">
+              <Card className="widget-card h-100 border-light border-2 w-50 
mx-4 rounded-0">
+                <div className="px-4 pt-4">
+                  {translate("dashboard.widgets.NameNodeRpc")}
+                </div>
+                <div className="px-4 pb-4 pt-3 text-center text-muted">
+                  {/* <NameNodeRpc /> */}
+                </div>
+              </Card>
+              <Card className="widget-card h-100 border-light border-2 w-50 
mx-4 rounded-0">
+                <div className="px-4 pt-4">
+                  {translate("dashboard.widgets.NameNodeUptime")}
+                </div>
+                <div className="px-4 pb-4 pt-3 text-center text-muted">
+                  {/* <NameNodeUptime /> */}
+                </div>
+              </Card>
+            </div>
+          </div>
+        ) : null}
+      </div>
+    </Card>
+  );
+};
diff --git a/ambari-web/latest/src/screens/Hosts/HostMetricsGraph.tsx 
b/ambari-web/latest/src/screens/Hosts/HostMetricsGraph.tsx
new file mode 100644
index 0000000000..b969279273
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/HostMetricsGraph.tsx
@@ -0,0 +1,596 @@
+/**
+ * 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 React from "react";
+import { Line } from "react-chartjs-2";
+import {
+  LineElement,
+  PointElement,
+  LinearScale,
+  Title,
+  Tooltip,
+  Legend,
+  CategoryScale,
+  Chart as ChartJs,
+  Filler,
+} from "chart.js";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faDownload } from "@fortawesome/free-solid-svg-icons";
+import { Alert, Dropdown } from "react-bootstrap";
+import { translate } from "../../Utils/Utility";
+import { isEmpty } from "lodash";
+
+ChartJs.register(
+  LineElement,
+  PointElement,
+  LinearScale,
+  Title,
+  Tooltip,
+  Legend,
+  CategoryScale,
+  Filler
+);
+
+interface MetricData {
+  [key: string]: {
+    [metricName: string]: Array<[number, number]>; // [value, timestamp] pairs
+  };
+}
+
+interface HostMetricsGraphProps {
+  metricsData: MetricData;
+  selectedMetricsOption?: string;
+}
+
+// Utility functions for data export
+const downloadFile = (content: string, filename: string, mimeType: string) => {
+  const blob = new Blob([content], { type: mimeType });
+  const url = URL.createObjectURL(blob);
+  const link = document.createElement("a");
+  link.href = url;
+  link.download = filename;
+  document.body.appendChild(link);
+  link.click();
+  document.body.removeChild(link);
+  URL.revokeObjectURL(url);
+};
+
+const formatSeriesNameWithUnit = (name: string, unit: string) => {
+  return unit ? `${name} (${unit})` : name;
+};
+
+const formatFilename = (metricName: string) => {
+  return metricName.toLowerCase().replace(/\s+/g, "_");
+};
+
+const generateCSVContent = (seriesData: any[], unit: string) => {
+  if (!seriesData || seriesData.length === 0) return "";
+
+  // Get all unique timestamps from all series
+  const timestampSet = new Set<number>();
+  seriesData.forEach((series) => {
+    series.data.forEach(([_, timestamp]: [number, number]) => {
+      timestampSet.add(timestamp);
+    });
+  });
+
+  const timestamps = Array.from(timestampSet).sort((a, b) => a - b);
+
+  // Create headers with Bytes unit for applicable metrics
+  const headers = ["Timestamp"];
+  seriesData.forEach((series) => {
+    const exportUnit = unit === "GB" || unit === "KB/s" ? "Bytes" : unit;
+    headers.push(formatSeriesNameWithUnit(series.name, exportUnit));
+  });
+
+  // Create data rows
+  const rows = [headers.join(",")];
+
+  timestamps.forEach((timestamp) => {
+    const row = [timestamp.toString()];
+
+    seriesData.forEach((series) => {
+      // Find the value for this timestamp in this series
+      const dataPoint = series.data.find(
+        ([_, ts]: [number, number]) => ts === timestamp
+      );
+
+      if (dataPoint) {
+        let value = dataPoint[0];
+
+        // Convert back to bytes for export
+        if (unit === "GB") {
+          value = value * Math.pow(2, 20);
+        } else if (unit === "KB/s") {
+          value = value * 1024;
+        }
+
+        row.push(value.toString());
+      } else {
+        row.push("");
+      }
+    });
+
+    rows.push(row.join(","));
+  });
+
+  return rows.join("\n");
+};
+
+const generateJSONContent = (seriesData: any[], unit: string) => {
+  return JSON.stringify(
+    seriesData.map((series) => {
+      const exportUnit = unit === "GB" || unit === "KB/s" ? "Bytes" : unit;
+      const exportData = series.data.map(
+        ([value, timestamp]: [number, number]) => {
+          let exportValue = value;
+
+          // Convert back to bytes for export
+          if (unit === "GB") {
+            exportValue = value * Math.pow(2, 20);
+          } else if (unit === "KB/s") {
+            exportValue = value * 1024;
+          }
+
+          return [exportValue, timestamp];
+        }
+      );
+
+      return {
+        name: formatSeriesNameWithUnit(series.name, exportUnit),
+        data: exportData,
+      };
+    }),
+    null,
+    2
+  );
+};
+
+const HostMetricsGraph: React.FC<HostMetricsGraphProps> = ({
+  metricsData,
+  selectedMetricsOption,
+}) => {
+  const renderChartJsChart = (metricName: string) => {
+    // Configuration for all metrics using Chart.js
+    const metricConfigs: Record<string, any> = {
+      "CPU Usage": {
+        calculation: (data: MetricData) => {
+          const cpuIdle = data.cpu?.cpu_idle || [];
+          const cpuUser = data.cpu?.cpu_user || [];
+          const cpuSystem = data.cpu?.cpu_system || [];
+          const cpuWio = data.cpu?.cpu_wio || [];
+          const cpuNice = data.cpu?.cpu_nice || [];
+
+          const series = [];
+          if (cpuUser.length > 0) {
+            series.push({
+              name: "CPU User",
+              data: cpuUser,
+              color: "#FF8000",
+            });
+          }
+          if (cpuSystem.length > 0) {
+            series.push({
+              name: "CPU System",
+              data: cpuSystem,
+              color: "#0066B3",
+            });
+          }
+          if (cpuWio.length > 0) {
+            series.push({
+              name: "CPU I/O Idle",
+              data: cpuWio,
+              color: "#FFCC00",
+            });
+          }
+          if (cpuNice.length > 0) {
+            series.push({
+              name: "CPU Nice",
+              data: cpuNice,
+              color: "#00CC00",
+            });
+          }
+          if (cpuIdle.length > 0) {
+            series.push({
+              name: "CPU Idle",
+              data: cpuIdle,
+              color: "#CFECEC",
+            });
+          }
+          return series;
+        },
+        unit: "%",
+      },
+      "Disk Usage": {
+        calculation: (data: MetricData) => {
+          const diskTotal = data.disk?.disk_total || [];
+          const diskFree = data.disk?.disk_free || [];
+
+          const series = [];
+          if (diskTotal.length > 0) {
+            series.push({
+              name: "Total",
+              data: diskTotal.map(([value, timestamp]) => [value, timestamp]),
+              color: "#0066B3",
+            });
+          }
+          if (diskFree.length > 0) {
+            series.push({
+              name: "Available",
+              data: diskFree.map(([value, timestamp]) => [value, timestamp]),
+              color: "#00CC00",
+            });
+          }
+          return series;
+        },
+        unit: "GB",
+      },
+      Load: {
+        calculation: (data: MetricData) => {
+          const loadOne = data.load?.load_one || [];
+          const loadFive = data.load?.load_five || [];
+          const loadFifteen = data.load?.load_fifteen || [];
+
+          const series = [];
+          if (loadOne.length > 0) {
+            series.push({
+              name: "1 Minute Load",
+              data: loadOne,
+              color: "#FF8000",
+            });
+          }
+          if (loadFive.length > 0) {
+            series.push({
+              name: "5 Minute Load",
+              data: loadFive,
+              color: "#0066B3",
+            });
+          }
+          if (loadFifteen.length > 0) {
+            series.push({
+              name: "15 Minute Load",
+              data: loadFifteen,
+              color: "#00CC00",
+            });
+          }
+          return series;
+        },
+        unit: "",
+      },
+      "Memory Usage": {
+        calculation: (data: MetricData) => {
+          const memFree = data.memory?.mem_free || [];
+          const memCached = data.memory?.mem_cached || [];
+          const memShared = data.memory?.mem_shared || [];
+          const swapFree = data.memory?.swap_free || [];
+
+          const series = [];
+          if (memFree.length > 0) {
+            series.push({
+              name: "Free",
+              data: memFree.map(([value, timestamp]) => [
+                value / Math.pow(2, 20),
+                timestamp,
+              ]),
+              color: "#0066B3",
+            });
+          }
+          if (memCached.length > 0) {
+            series.push({
+              name: "Cached",
+              data: memCached.map(([value, timestamp]) => [
+                value / Math.pow(2, 20),
+                timestamp,
+              ]),
+              color: "#00CC00",
+            });
+          }
+          if (memShared.length > 0) {
+            series.push({
+              name: "Shared",
+              data: memShared.map(([value, timestamp]) => [
+                value / Math.pow(2, 20),
+                timestamp,
+              ]),
+              color: "#FF8000",
+            });
+          }
+          if (swapFree.length > 0) {
+            series.push({
+              name: "Swap",
+              data: swapFree.map(([value, timestamp]) => [
+                value / Math.pow(2, 20),
+                timestamp,
+              ]),
+              color: "#FFCC00",
+            });
+          }
+          return series;
+        },
+        unit: "GB",
+      },
+      "Network Usage": {
+        calculation: (data: MetricData) => {
+          const bytesIn = data.network?.bytes_in || [];
+          const bytesOut = data.network?.bytes_out || [];
+          const pktsIn = data.network?.pkts_in || [];
+          const pktsOut = data.network?.pkts_out || [];
+
+          const series = [];
+          if (bytesIn.length > 0) {
+            series.push({
+              name: "Bytes In",
+              data: bytesIn.map(([value, timestamp]) => [
+                value / 1024,
+                timestamp,
+              ]),
+              color: "#00CC00",
+            });
+          }
+          if (bytesOut.length > 0) {
+            series.push({
+              name: "Bytes Out",
+              data: bytesOut.map(([value, timestamp]) => [
+                value / 1024,
+                timestamp,
+              ]),
+              color: "#0066B3",
+            });
+          }
+          if (pktsIn.length > 0) {
+            series.push({
+              name: "Packets In",
+              data: pktsIn,
+              color: "#FF8000",
+            });
+          }
+          if (pktsOut.length > 0) {
+            series.push({
+              name: "Packets Out",
+              data: pktsOut,
+              color: "#FFCC00",
+            });
+          }
+          return series;
+        },
+        unit: "KB/s",
+      },
+      Processes: {
+        calculation: (data: MetricData) => {
+          const procTotal = data.process?.proc_total || [];
+          const procRun = data.process?.proc_run || [];
+
+          const series = [];
+          if (procTotal.length > 0) {
+            series.push({
+              name: "Total Processes",
+              data: procTotal,
+              color: "#0066B3",
+            });
+          }
+          if (procRun.length > 0) {
+            series.push({
+              name: "Processes Run",
+              data: procRun,
+              color: "#00CC00",
+            });
+          }
+          return series;
+        },
+        unit: "",
+      },
+    };
+
+    const config = metricConfigs[metricName];
+    if (!config) return null;
+
+    if (isEmpty(metricsData)) {
+      if (selectedMetricsOption?.includes("CUSTOM")) {
+        return (
+          <div className="mx-4">
+            <Alert variant="info w-100">
+              {translate("graphs.noData.title")}
+              {": "}
+              {translate("graphs.noDataAtTime.message")}
+            </Alert>
+            <div className="d-flex justify-content-center">{metricName}</div>
+          </div>
+        );
+      }
+      return (
+        <div className="mx-4">
+          <Alert variant="info px-4 w-100">
+            {translate("graphs.noData.message")}
+          </Alert>
+          <div className="d-flex justify-content-center">{metricName}</div>
+        </div>
+      );
+    }
+
+    const seriesData = config.calculation(metricsData);
+
+    // Prepare Chart.js data
+    let labels: string[] = [];
+    const datasets: any[] = [];
+
+    seriesData.forEach((series: any) => {
+      if (!series.data || series.data.length === 0) return;
+
+      // Create labels from timestamps if not already created
+      if (labels.length === 0) {
+        labels = series.data.map(([_, timestamp]: [number, number]) =>
+          new Date(timestamp * 1000).toLocaleTimeString()
+        );
+      }
+
+      // Extract values for this series
+      const data = series.data.map(([value]: [number, number]) => value);
+
+      datasets.push({
+        label: series.name,
+        data,
+        fill: false,
+        backgroundColor: series.color + "40",
+        borderColor: series.color,
+        borderWidth: 1.5,
+        pointRadius: 0.5,
+        pointHoverRadius: 2,
+        tension: 0.1,
+      });
+    });
+
+    const chartData = {
+      labels,
+      datasets,
+    };
+
+    const options = {
+      responsive: true,
+      maintainAspectRatio: false,
+      scales: {
+        x: {
+          display: false, // Hide x-axis labels for compact view
+          grid: {
+            display: false,
+          },
+        },
+        y: {
+          beginAtZero: true,
+          grid: {
+            color: "rgba(0,0,0,0.1)",
+          },
+          ticks: {
+            font: {
+              size: 10,
+            },
+            callback: function (value: any) {
+              return value.toFixed(1) + (config.unit ? ` ${config.unit}` : "");
+            },
+          },
+        },
+      },
+      plugins: {
+        legend: {
+          display: false,
+        },
+        tooltip: {
+          mode: "index" as const,
+          intersect: false,
+          callbacks: {
+            label: function (context: any) {
+              return `${context.dataset.label}: ${context.parsed.y.toFixed(
+                2
+              )} ${config.unit}`;
+            },
+          },
+        },
+      },
+      interaction: {
+        mode: "nearest" as const,
+        axis: "x" as const,
+        intersect: false,
+      },
+    };
+
+    // Export handlers
+    const handleCSVExport = () => {
+      const csvContent = generateCSVContent(seriesData, config.unit);
+      if (csvContent) {
+        downloadFile(
+          csvContent,
+          `${formatFilename(metricName)}.csv`,
+          "text/csv"
+        );
+      }
+    };
+
+    const handleJSONExport = () => {
+      const jsonContent = generateJSONContent(seriesData, config.unit);
+      if (jsonContent) {
+        downloadFile(
+          jsonContent,
+          `${formatFilename(metricName)}.json`,
+          "application/json"
+        );
+      }
+    };
+
+    return (
+      <div className="p-1">
+        <div>
+          <Line data={chartData} options={options} />
+        </div>
+
+        <div className="d-flex justify-content-between">
+          <div className="pt-2 ps-4">{metricName}</div>
+          <Dropdown drop="down">
+            <Dropdown.Toggle
+              bsPrefix="custom"
+              variant="transparent border-0"
+              className="m-0 p-0 text-dark"
+              title="Export"
+            >
+              <FontAwesomeIcon icon={faDownload} />
+            </Dropdown.Toggle>
+            <Dropdown.Menu>
+              <Dropdown.Item onClick={handleCSVExport}>
+                Save as CSV
+              </Dropdown.Item>
+              <Dropdown.Item onClick={handleJSONExport}>
+                Save as JSON
+              </Dropdown.Item>
+            </Dropdown.Menu>
+          </Dropdown>
+        </div>
+      </div>
+    );
+  };
+
+  const metricNames = [
+    "CPU Usage",
+    "Disk Usage",
+    "Load",
+    "Memory Usage",
+    "Network Usage",
+    "Processes",
+  ];
+
+  const metricRows: string[][] = [];
+  for (let i = 0; i < metricNames.length; i += 2) {
+    metricRows.push(metricNames.slice(i, i + 2));
+  }
+
+  return (
+    <div className="px-4">
+      {metricRows.map((row, rowIndex) => (
+        <div
+          key={rowIndex}
+          className="d-flex justify-content-center mb-4 gap-2"
+        >
+          {row.map((metricName) => (
+            <div key={metricName} className="min-w-0">
+              {renderChartJsChart(metricName)}
+            </div>
+          ))}
+        </div>
+      ))}
+    </div>
+  );
+};
+
+export default HostMetricsGraph;
diff --git a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx 
b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
new file mode 100644
index 0000000000..3959dd3949
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
@@ -0,0 +1,1193 @@
+/**
+ * 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 {
+  useCallback,
+  useContext,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from "react";
+import { useNavigate, useParams } from "react-router-dom";
+import { HostsApi } from "../../api/hostsApi";
+import { Alert, Button, Card, Dropdown } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+  faCheckCircle,
+  faCog,
+  faEllipsis,
+  faMedkit,
+  faMinusCircle,
+  faPencil,
+  faPlus,
+  faQuestionCircle,
+  faRefresh,
+  faWarning,
+} from "@fortawesome/free-solid-svg-icons";
+import { get, isEmpty, startCase, uniq } from "lodash";
+import Modal from "../../components/Modal";
+import Table from "../../components/Table";
+import { ComponentStatus, ComponentType, PassiveStateOnFilters } from 
"./enums";
+import Tooltip from "../../components/Tooltip";
+import { getCmponentsToBeRestarted } from "./HostsList";
+import SelectTimeRangeModal from "../../components/SelectTimeRangeModal";
+import {
+  formatDate,
+  getCurrTimeInSec,
+  translate,
+  translateWithVariables,
+} from "../../Utils/Utility";
+import { durationMap } from "../../components/constants";
+import { Link } from "react-router-dom";
+import {
+  apiDataToHostComponentModel,
+  getClientCustomCommands,
+  getClusterComponentsCount,
+  getComponentDisplayName,
+  getComponentName,
+  getCustomCommands,
+  isActive,
+  isAddableToHost,
+  isDeletable,
+  isDeleteComponentDisabled,
+  isEnableHiveInteractive,
+  isInit,
+  isMoveComponentDisabled,
+  isOozieServerAddable,
+  isReassignable,
+  isRefreshConfigsAllowed,
+  isRestartable,
+  isRestartComponentDisabled,
+  isStart,
+  maxToInstall,
+} from "./utils";
+import {
+  checkNnLastCheckpointTime,
+  decommission,
+  recommission,
+  restartAllStaleConfigComponents,
+  restartComponent,
+  startComponent,
+  stopComponent,
+  executeCustomCommand,
+} from "./actions";
+import { AppContext } from "../../store/context";
+import IHost from "../../models/host";
+import { IHostStackVersion } from "../../models/hostStackVersion";
+import { IHostComponent } from "../../models/hostComponent";
+import Spinner from "../../components/Spinner";
+import modalManager from "../../store/ModalManager";
+import SetRackInfoModal from "./SetRackInfoModal";
+import {
+  useDecommissionable,
+  decommissionableComponents,
+} from "../../hooks/useDecommissionable";
+import { ServiceContext } from "../../store/ServiceContext";
+import { HostMetrics } from "./HostMetrics";
+import { hostMetricsOption } from "./constants";
+import usePolling from "../../hooks/usePolling";
+import classNames from "classnames";
+import { useAuth } from "../../hooks/useAuth";
+
+type HostSummaryProps = {
+  allHostModels: IHost[];
+  setAllHostModels: (
+    data: IHost[] | ((prevModels: IHost[]) => IHost[])
+  ) => void;
+  clusterComponents: any;
+};
+
+export default function HostsSummary({
+  allHostModels,
+  setAllHostModels,
+  clusterComponents,
+}: HostSummaryProps) {
+  const { clusterName, serviceComponentInfo, services, upgradeIsRunning, 
upgradeSuspended } =
+    useContext(AppContext);
+  const { allServiceModels: serviceModels } = useContext(ServiceContext);
+  const params = useParams();
+  const navigate = useNavigate();
+  const [loading, setLoading] = useState(true);
+  const [metricsData, setMetricsData] = useState({});
+  const [showSelectTimeModal, setShowSelectTimeModal] = useState(false);
+  const [showConfirmationModal, setShowConfirmationModal] = useState(false);
+  const [openDropdownId, setOpenDropdownId] = useState<string | null>(null);
+
+  //Note:- Below states should be part of the context
+  const [allComponents, setAllComponents] = useState<IHostComponent[]>([]);
+  const [addableComponents, setAddableComponents] = useState<any[]>([]);
+
+  const [summary, setSummary] = useState({
+    Hostname: "",
+    "IP Address": "",
+    Rack: "",
+    OS: "",
+    "Cores (CPU)": "",
+    Disk: "",
+    Memory: "",
+    "Load Avg": "",
+    Heartbeat: "",
+    "Current Version": "",
+    "JCE Unlimited": "",
+  });
+  const [selectedMetricsOption, setSelectedMetricsOption] = useState(
+    hostMetricsOption[0]
+  );
+
+  const selectedActionData = useRef({
+    component: {},
+    action: "",
+    data: {},
+    isCustom: false,
+    successCallback: (_component: any, _data: any): any => {
+      return -1;
+    },
+  });
+
+  const populateHostMetricesData = () => {
+    if (!selectedMetricsOption.toUpperCase().startsWith("CUSTOM")) {
+      const duration = selectedMetricsOption.split(" ").slice(1).join(" ");
+      const currTime = getCurrTimeInSec();
+      const startTime = currTime - durationMap[duration];
+      getHostMetrics(startTime, currTime);
+    }
+  };
+
+  const { pausePolling, resumePolling } = usePolling(
+    populateHostMetricesData,
+    15000
+  );
+
+  useEffect(() => {
+    if (!selectedMetricsOption.toUpperCase().startsWith("CUSTOM")) {
+      populateHostMetricesData();
+      resumePolling();
+    }
+  }, [selectedMetricsOption]);
+
+  useEffect(() => {
+    if (!isEmpty(serviceComponentInfo)) {
+      let allComponentsCopy: any[] = [];
+      get(serviceComponentInfo, "items", []).forEach((service: any) => {
+        allComponentsCopy = allComponentsCopy.concat(
+          get(service, "components", []).map((component: any) => {
+            return {
+              HostRoles: {
+                ...get(component, "StackServiceComponents"),
+                dependencies: get(component, "dependencies", []),
+              },
+            };
+          })
+        );
+      });
+      setAllComponents(apiDataToHostComponentModel(allComponentsCopy));
+    }
+  }, [serviceComponentInfo]);
+
+  useEffect(() => {
+    if (!isEmpty(allComponents)) {
+      getAddableComponents();
+    }
+  }, [allComponents, allHostModels, clusterComponents]);
+
+  useEffect(() => {
+    if (!isEmpty(allHostModels)) {
+      let tempSummary: any = {};
+      const host = get(allHostModels, "[0]");
+      tempSummary.Hostname = get(host, "hostName", "");
+      tempSummary["IP Address"] = get(host, "ip", "");
+      tempSummary.Rack = get(host, "rack", "");
+      tempSummary.OS =
+        get(host, "osType", "") + "(" + get(host, "osArch", "") + ")";
+      tempSummary["Cores (CPU)"] = host.coresFormatted();
+      tempSummary.Disk = get(host, "diskFree", "Data Unavailable");
+      tempSummary.Memory = host.memoryFormatted();
+      tempSummary["Load Avg"] = get(host, "loadOne", "");
+      tempSummary.Heartbeat = get(host, "lastHeartBeatTime", "")
+        ? "less than a minute ago"
+        : "";
+      tempSummary["Current Version"] = getCurrentVersion(host);
+      tempSummary["JCE Unlimited"] = get(host, "hasJcePolicy", true)
+        ? "true"
+        : "false";
+      setSummary(tempSummary);
+    }
+  }, [allHostModels]);
+
+  useEffect(() => {
+    if (!isEmpty(clusterComponents) && summary.Hostname) {
+      setLoading(false);
+    }
+  }, [clusterComponents, summary]);
+
+  const { decommissionable, isComponentDecommissionDisable } =
+    useDecommissionable(get(allHostModels, "[0]", {} as IHost));
+
+  // Authorization hooks - implementing Ember.js host component authorization 
patterns
+  const { hasAuthorization } = useAuth();
+
+  // Use computed upgrade properties instead of utility function
+  const isUpgradeInProgress = upgradeIsRunning && !upgradeSuspended;
+
+  // Check specific authorizations for host component operations
+  const canStartStopServices = hasAuthorization("SERVICE.START_STOP");
+  const canAddDeleteServices = hasAuthorization("HOST.ADD_DELETE_COMPONENTS");
+  const canModifyConfigs = hasAuthorization("SERVICE.MODIFY_CONFIGS");
+  const canManageHostComponents = hasAuthorization(
+    "HOST.ADD_DELETE_COMPONENTS"
+  );
+  const canMoveComponents = hasAuthorization("SERVICE.MOVE");
+
+  const canPerformActions =
+    canStartStopServices ||
+    canAddDeleteServices ||
+    canModifyConfigs ||
+    canManageHostComponents;
+
+  const getHostMetrics = async (startTime: number, endTime: number) => {
+    // Use a unique cache-busting parameter that includes the time range
+    const cacheBuster = `${startTime}_${endTime}_${getCurrTimeInSec()}`;
+    const response = await HostsApi.getHostData(
+      clusterName,
+      get(params, "hostname", ""),
+      
`metrics/cpu/cpu_user[${startTime},${endTime},15],metrics/cpu/cpu_wio[${startTime},${endTime},15],metrics/cpu/cpu_nice[${startTime},${endTime},15],metrics/cpu/cpu_aidle[${startTime},${endTime},15],metrics/cpu/cpu_system[${startTime},${endTime},15],metrics/cpu/cpu_idle[${startTime},${endTime},15],metrics/disk/disk_total[${startTime},${endTime},15],metrics/disk/disk_free[${startTime},${endTime},15],metrics/load/load_fifteen[${startTime},${endTime},15],metrics/load/load_one[${startTim
 [...]
+    );
+    setMetricsData(response);
+  };
+  //@ts-ignore
+  const getClusterHosts = () => {
+    let hosts: string[] = [];
+    get(clusterComponents, "items", []).forEach((component: any) => {
+      get(component, "host_components", []).forEach((host: any) => {
+        hosts.push(get(host, "HostRoles.host_name", ""));
+      });
+    });
+    return uniq(hosts);
+  };
+
+  const hasCardinalityConflict = (component: IHostComponent) => {
+    const totalCount = get(
+      getClusterComponentsCount(clusterComponents),
+      getComponentName(component),
+      0
+    );
+    const maxCount = maxToInstall(component);
+    return !(totalCount < maxCount);
+  };
+
+  const getAddableComponents = () => {
+    let components: any[] = [];
+    const installedComponents = get(
+      allHostModels,
+      "[0].hostComponents",
+      []
+    ).map((component) => getComponentName(component));
+    let installedServices: any[] = [];
+    get(clusterComponents, "items", []).forEach((component: any) => {
+      installedServices.push(
+        get(component, "ServiceComponentInfo.service_name", "")
+      );
+    });
+    installedServices = uniq(installedServices);
+    const addableToHostComponents = allComponents.filter((component) =>
+      isAddableToHost(component, serviceModels)
+    );
+
+    addableToHostComponents.forEach((component) => {
+      if (
+        installedServices.includes(get(component, "serviceName", "")) &&
+        !installedComponents.includes(getComponentName(component)) &&
+        !hasCardinalityConflict(component)
+      ) {
+        if (
+          (getComponentName(component) === "OOZIE_SERVER" &&
+            !isOozieServerAddable()) ||
+          (getComponentName(component) === "HIVE_SERVER_INTERACTIVE" &&
+            !isEnableHiveInteractive())
+        ) {
+          return;
+        }
+        components.push({
+          component_name: getComponentName(component),
+          service_name: get(component, "serviceName", ""),
+          display_name: get(component, "displayName", ""),
+          component_category: get(component, "componentCategory", ""),
+        });
+      }
+    });
+    setAddableComponents(components);
+  };
+
+  const getCurrentVersion = (hostData: IHost) => {
+    const stackVersions = get(hostData, "stackVersions", []);
+    const currentVersions = stackVersions.filter(
+      (version: IHostStackVersion) => get(version, "status") === "CURRENT"
+    );
+    return get(currentVersions, "[0].repoVersion", "");
+  };
+
+  const getStateIcon = (component: IHostComponent) => {
+    const state = get(component, "workStatus", "");
+    const type = get(component, "componentCategory", "");
+    const adminState = get(component, "adminState", "");
+    if (adminState === "DECOMMISSIONED") {
+      return (
+        <Tooltip message="Decommissioned">
+          <FontAwesomeIcon icon={faMinusCircle} className="text-orange" />
+        </Tooltip>
+      );
+    }
+    let message = "";
+    let icon = <div></div>;
+    switch (state) {
+      case ComponentStatus.UNKNOWN:
+        message = "Heartbeat Lost";
+        icon = (
+          <FontAwesomeIcon icon={faQuestionCircle} className="text-warning" />
+        );
+        break;
+      case ComponentStatus.INIT:
+        message = "Install Pending...";
+        icon = (
+          <FontAwesomeIcon icon={faQuestionCircle} className="text-warning" />
+        );
+        break;
+      case ComponentStatus.INSTALLING:
+        message = "Installing";
+        icon = (
+          <FontAwesomeIcon icon={faCog} className="text-info blinking-icon" />
+        );
+        break;
+      case ComponentStatus.STOPPING:
+        message = "Stopping";
+        icon = (
+          <FontAwesomeIcon
+            icon={faWarning}
+            className="text-danger blinking-icon"
+          />
+        );
+        break;
+      case ComponentStatus.STOPPED:
+        if (type === ComponentType.CLIENT) {
+          message = "Installed";
+          icon = (
+            <FontAwesomeIcon icon={faCheckCircle} className="text-success" />
+          );
+        } else {
+          message = "Stopped";
+          icon = <FontAwesomeIcon icon={faWarning} className="text-danger" />;
+        }
+        break;
+      case ComponentStatus.STARTING:
+        message = "Starting";
+        icon = (
+          <FontAwesomeIcon
+            icon={faCheckCircle}
+            className="success blinking-icon"
+          />
+        );
+        break;
+      case ComponentStatus.STARTED:
+        message = "Started";
+        icon = (
+          <FontAwesomeIcon icon={faCheckCircle} className="text-success" />
+        );
+        break;
+      case ComponentStatus.INSTALL_FAILED:
+        message = "Install Failed";
+        icon = <FontAwesomeIcon icon={faCog} className="text-danger" />;
+        break;
+    }
+    return <Tooltip message={message}>{icon}</Tooltip>;
+  };
+
+  const getStatusIcons = (component: IHostComponent) => {
+    const maintenanceState = get(component, "passiveState", "OFF");
+    const hasStaleConfigs = get(component, "staleConfigs", false);
+    return (
+      <div className="d-flex">
+        <div className="me-2">{getStateIcon(component)}</div>
+        {hasStaleConfigs ? (
+          <FontAwesomeIcon icon={faRefresh} className="text-warning me-2" />
+        ) : null}
+        {maintenanceState !== "OFF" ? (
+          <FontAwesomeIcon icon={faMedkit} className="text-dark me-2" />
+        ) : null}
+      </div>
+    );
+  };
+
+  const setSelectedActionData = (
+    component: any,
+    action: string,
+    isCustom: boolean,
+    successCallback: (component: any, data?: any) => any,
+    data?: any
+  ) => {
+    data = data || {};
+    selectedActionData.current.component = component;
+    selectedActionData.current.action = action;
+    selectedActionData.current.data = data;
+    selectedActionData.current.isCustom = isCustom;
+    selectedActionData.current.successCallback = successCallback;
+  };
+
+  const isComponentDecommissionAvailable = (component: IHostComponent) => {
+    return get(
+      decommissionable,
+      getComponentName(component) + ".isComponentDecommissionAvailable",
+      false
+    );
+  };
+
+  const isComponentRecommissionAvailable = (component: IHostComponent) => {
+    return get(
+      decommissionable,
+      getComponentName(component) + ".isComponentRecommissionAvailable",
+      false
+    );
+  };
+
+  const isToggleMaintenanceModeAvailable = (component: IHostComponent) => {
+    return (
+      isActive(component) ||
+      ![
+        PassiveStateOnFilters.IMPLIED_FROM_SERVICE,
+        PassiveStateOnFilters.IMPLIED_FROM_SERVICE_AND_HOST,
+      ].includes(get(component, "passiveState") as PassiveStateOnFilters)
+    );
+  };
+
+  const getActions = useCallback(
+    (component: IHostComponent) => {
+      const actions: React.ReactElement[] = [];
+      const state = get(component, "workStatus", "") as ComponentStatus;
+
+      //Actions of Clients
+      if (get(component, "componentCategory", "") === ComponentType.CLIENT) {
+        // Refresh configs - Requires SERVICE.MODIFY_CONFIGS authorization
+        if (canModifyConfigs) {
+          actions.push(
+            <div
+              key="refresh-configs"
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+            >
+              Refresh configs
+            </div>
+          );
+        }
+
+        // Install - Requires SERVICE.ADD_DELETE_SERVICES authorization
+        if (canAddDeleteServices) {
+          actions.push(
+            <div
+              key="install"
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+              className={isInit(component) ? "" : "disabled-btn"}
+            >
+              Install
+            </div>
+          );
+        }
+
+        // Re-Install - Requires SERVICE.ADD_DELETE_SERVICES authorization
+        if (state === ComponentStatus.INSTALL_FAILED && canAddDeleteServices) {
+          actions.push(
+            <div
+              key="re-install"
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+            >
+              Re-Install
+            </div>
+          );
+        }
+
+        // Custom commands - Requires SERVICE.START_STOP authorization
+        if (canStartStopServices) {
+          getClientCustomCommands(component).forEach(
+            (cmd: any, index: number) => {
+              actions.push(
+                <div
+                  key={`custom-${index}`}
+                  onClick={() => {
+                    executeCustomCommand(cmd, component);
+                  }}
+                >
+                  {get(cmd, "label", "")}
+                </div>
+              );
+            }
+          );
+        }
+      }
+      //Actions of Masters and Slaves
+      else {
+        // Decommission - Requires SERVICE.START_STOP authorization
+        if (
+          isComponentDecommissionAvailable(component) &&
+          canStartStopServices
+        ) {
+          actions.push(
+            <div
+              key="decommission"
+              onClick={() => {
+                if (!isComponentDecommissionDisable(component)) {
+                  const data = { clusterComponents };
+                  setSelectedActionData(
+                    component,
+                    "decommission",
+                    false,
+                    decommission,
+                    data
+                  );
+                  setShowConfirmationModal(true);
+                }
+              }}
+              className={
+                isComponentDecommissionDisable(component) ? "disabled-btn" : ""
+              }
+            >
+              Decommission
+            </div>
+          );
+        }
+
+        // Recommission - Requires SERVICE.START_STOP authorization
+        if (
+          isComponentRecommissionAvailable(component) &&
+          canStartStopServices
+        ) {
+          actions.push(
+            <div
+              key="recommission"
+              onClick={() => {
+                if (!isComponentDecommissionDisable(component)) {
+                  setSelectedActionData(
+                    component,
+                    "recommission",
+                    false,
+                    recommission
+                  );
+                  setShowConfirmationModal(true);
+                }
+              }}
+              className={
+                isComponentDecommissionDisable(component) ? "disabled-btn" : ""
+              }
+            >
+              Recommission
+            </div>
+          );
+        }
+
+        const isDecommissionableComponent = 
decommissionableComponents.includes(
+          getComponentName(component)
+        );
+        const canRestart = isDecommissionableComponent
+          ? isComponentDecommissionAvailable(component) &&
+          isRestartable(component)
+          : !isRestartComponentDisabled(component) && isRestartable(component);
+
+        // Restart - Requires SERVICE.START_STOP authorization
+        if (canRestart && canStartStopServices) {
+          actions.push(
+            <div
+              key="restart"
+              onClick={() => {
+                setSelectedActionData(
+                  component,
+                  "restart",
+                  false,
+                  restartComponent
+                );
+                setShowConfirmationModal(true);
+              }}
+            >
+              Restart
+            </div>
+          );
+        }
+
+        if (state !== ComponentStatus.INSTALLING) {
+            // Stop - Requires SERVICE.START_STOP authorization
+            if (isStart(component) && canStartStopServices) {
+                actions.push(
+                    <div
+                        key="stop"
+                        onClick={() => {
+                            setSelectedActionData(
+                                component,
+                                "stop",
+                                false,
+                                stopComponent
+                            );
+                            if (getComponentName(component) === "NAMENODE") {
+                                checkNnLastCheckpointTime(
+                                    () => setShowConfirmationModal(true),
+                                    get(component, "hostName", ""),
+                                    clusterName
+                                );
+                            } else {
+                                setShowConfirmationModal(true);
+                            }
+                        }}
+                    >
+                        Stop
+                    </div>
+                );
+            }
+
+            // Start - Requires SERVICE.START_STOP authorization
+            if (!isStart(component) && canStartStopServices) {
+                if (!isInit(component)) {
+                    if (
+                        ![
+                            ComponentStatus.UPGRADE_FAILED,
+                            ComponentStatus.INSTALL_FAILED,
+                        ].includes(state)
+                    ) {
+                        actions.push(
+                            <div
+                                key="start"
+                                onClick={() => {
+                                    setSelectedActionData(
+                                        component,
+                                        "start",
+                                        false,
+                                        startComponent
+                                    );
+                                    setShowConfirmationModal(true);
+                                }}
+                            >
+                                Start
+                            </div>
+                        );
+                    }
+                }
+            }
+
+            if (state === ComponentStatus.UPGRADE_FAILED) {
+                actions.push(<div key="retry-upgrade">Retry Upgrade</div>);
+            }
+
+            // Re-Install Failed - Requires SERVICE.ADD_DELETE_SERVICES 
authorization
+            if (
+                state === ComponentStatus.INSTALL_FAILED &&
+                canAddDeleteServices
+            ) {
+                actions.push(
+                    <div
+                        key="re-install-failed"
+                        onClick={() => {
+                            //TODO: Will be implemented in future PR
+                        }}
+                    >
+                        Re-Install
+                    </div>
+                );
+            }
+
+            // Move operations - Requires SERVICE.MOVE authorization
+            if (canMoveComponents && isReassignable(component, 
getClusterHosts().length)) {
+                actions.push(
+                    <div
+                        key="move"
+                        onClick={() => moveComponent(component)}
+                        className={
+                            isMoveComponentDisabled(
+                                component,
+                                getClusterHosts().length,
+                                get(clusterComponents, "items", [])
+                            )
+                                ? "disabled-btn"
+                                : ""
+                        }
+                    >
+                        Move
+                    </div>
+                );
+            }
+        }
+
+        // Maintenance Mode - Always available (no specific authorization 
required)
+        actions.push(
+          <div
+            key="maintenance-mode"
+            onClick={() => {
+              //TODO: Will be implemented in future PR
+            }}
+            className={
+              isToggleMaintenanceModeAvailable(component) ? "" : "disabled-btn"
+            }
+          >
+            {isActive(component)
+              ? "Turn On Maintenance Mode"
+              : "Turn Off Maintenance Mode"}
+          </div>
+        );
+
+        // Re-Install Init - Requires SERVICE.ADD_DELETE_SERVICES authorization
+        if (isInit(component) && canAddDeleteServices) {
+          actions.push(
+            <div
+              key="re-install-init"
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+            >
+              Re-Install
+            </div>
+          );
+        }
+
+        // Delete - Requires SERVICE.ADD_DELETE_SERVICES authorization
+        if (isDeletable(component, serviceModels) && canAddDeleteServices) {
+          actions.push(
+            <div
+              key="delete"
+              className={
+                isDeleteComponentDisabled(
+                  component,
+                  get(clusterComponents, "items", [])
+                )
+                  ? "disabled-btn"
+                  : ""
+              }
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+            >
+              Delete
+            </div>
+          );
+        }
+
+        // Refresh configs - Requires SERVICE.MODIFY_CONFIGS authorization
+        if (isRefreshConfigsAllowed(component) && canModifyConfigs) {
+          actions.push(
+            <div
+              key="refresh-component-configs"
+              onClick={() => {
+                //TODO: Will be implemented in future PR
+              }}
+            >
+              Refresh configs
+            </div>
+          );
+        }
+
+        // Custom commands - Requires SERVICE.START_STOP authorization
+        if (canStartStopServices) {
+          getCustomCommands(
+            component,
+            get(clusterComponents, "items", [])
+          ).forEach((cmd: any, index: number) => {
+            actions.push(
+              <div
+                key={`custom-master-${index}`}
+                onClick={() => {
+                  executeCustomCommand(cmd, component);
+                }}
+              >
+                {get(cmd, "label", "")}
+              </div>
+            );
+          });
+        }
+      }
+      return actions;
+    },
+    [
+      allComponents,
+      clusterComponents,
+      services,
+      JSON.stringify(allHostModels),
+      clusterName,
+      JSON.stringify(serviceModels),
+    ]
+  );
+
+  const moveComponent = (component: IHostComponent) => {
+    const modalProps = {
+      modalTitle: translate("popup.confirmation.commonHeader"),
+      modalBody: translateWithVariables("question.sure.move", {
+        "0": getComponentDisplayName(component),
+      }),
+      onClose: () => { },
+      successCallback: () => {
+        navigate(
+          "/main/service/reassign/" + getComponentName(component) + "/step1"
+        );
+        modalManager.hide();
+      },
+      options: {
+        buttonSize: "sm" as "sm" | "lg" | undefined,
+        cancelableViaIcon: true,
+        cancelableViaBtn: true,
+        okButtonVariant: "primary",
+      },
+    };
+    modalManager.show(modalProps);
+  };
+
+  const handleDropdownToggle = (
+    isOpen: boolean,
+    componentId: string,
+    isStarting: boolean
+  ) => {
+    if (isOpen && !isStarting) {
+      setOpenDropdownId(componentId);
+    } else {
+      setOpenDropdownId(null);
+    }
+  };
+
+  const componentActionsMap = useMemo(() => {
+    if (
+      !allHostModels ||
+      !allHostModels[0] ||
+      !allHostModels[0].hostComponents
+    ) {
+      return {};
+    }
+
+    const actionsMap: Record<string, React.ReactElement[]> = {};
+    allHostModels[0].hostComponents.forEach((component: IHostComponent) => {
+      const componentId = 
`${component.serviceName}-${component.componentName}-${component.hostName}`;
+      actionsMap[componentId] = getActions(component);
+    });
+    return actionsMap;
+  }, [getActions, allHostModels]);
+
+  const columnsInComponentsTable = useMemo(
+    () => [
+      {
+        header: "Status",
+        id: "status",
+        width: "12%",
+        cell: (info: any) => {
+          return getStatusIcons(get(info, "row.original", {}));
+        },
+      },
+      {
+        header: "Name",
+        id: "name",
+        width: "50%",
+        cell: (info: any) => {
+          const serviceName = get(info, "row.original.serviceName", "");
+          return (
+            <div className="d-flex">
+              <div className="me-2">
+                {get(info, "row.original.nnHAState", "") ? startCase(get(info, 
"row.original.nnHAState", "")) + " " : ""}
+                {get(info, "row.original.displayName", "")}
+                {" / "}
+              </div>
+              <Link
+                to={`/main/services/${serviceName}/summary`}
+                className="custom-link me-1"
+              >
+                <div>{startCase(serviceName.toLowerCase())}</div>
+              </Link>
+              <div>{get(info, "row.original.nnHAState", "") ? " - " + 
clusterName : ""}</div>
+            </div>
+          );
+        },
+      },
+      {
+        header: "Type",
+        id: "type",
+        width: "15%",
+        cell: (info: any) => {
+          return startCase(
+            get(info, "row.original.componentCategory", "").toLowerCase()
+          );
+        },
+      },
+      {
+        header: "Action",
+        id: "action",
+        width: "10%",
+        cell: (info: any) => {
+          const component = get(info, "row.original", {});
+          const componentId = 
`${component.serviceName}-${component.componentName}-${component.hostName}`;
+          const isStarting =
+            get(component, "workStatus", "") === ComponentStatus.STARTING;
+          const availableActions = componentActionsMap[componentId] || [];
+
+          // Hide dropdown if user has no access to any actions
+          if (availableActions.length === 0 || isUpgradeInProgress) {
+            return null;
+          }
+
+          return (!canPerformActions ? null : (
+            <Dropdown
+              drop="down-centered"
+              show={openDropdownId === componentId}
+              onToggle={(isOpen) =>
+                handleDropdownToggle(isOpen, componentId, isStarting)
+              }
+            >
+              <Dropdown.Toggle
+                variant="transparent border-0"
+                className={classNames("custom-link p-0 m-0", {
+                  "disabled-btn disabled": isStarting,
+                })}
+              >
+                <FontAwesomeIcon icon={faEllipsis} className="fs-6 me-1" />
+              </Dropdown.Toggle>
+              {!isStarting && (
+                <Dropdown.Menu className="rounded-0">
+                  {availableActions.map(
+                    (action: React.ReactElement, index: number) => (
+                      <Dropdown.Item key={index}>{action}</Dropdown.Item>
+                    )
+                  )}
+                </Dropdown.Menu>
+              )}
+            </Dropdown>
+          ));
+        },
+      },
+    ],
+    [getActions, componentActionsMap, openDropdownId]
+  );
+
+  if (loading) {
+    return <Spinner />;
+  }
+
+  return (
+    <div>
+      {showConfirmationModal ? (
+        <Modal
+          isOpen={showConfirmationModal}
+          onClose={() => setShowConfirmationModal(false)}
+          modalTitle={translate("popup.confirmation.commonHeader")}
+          modalBody={
+            selectedActionData.current.isCustom
+              ? translate("question.sure")
+              : `Are you sure you want to ${selectedActionData.current.action
+              } ${getComponentDisplayName(
+                selectedActionData.current.component as IHostComponent
+              )}?`
+          }
+          successCallback={async () => {
+            await selectedActionData.current.successCallback(
+              selectedActionData.current.component,
+              selectedActionData.current.data
+            );
+            setShowConfirmationModal(false);
+          }}
+          options={{
+            modalSize: "modal-sm",
+            cancelableViaIcon: true,
+            cancelableViaBtn: true,
+            okButtonVariant: "primary",
+          }}
+        />
+      ) : null}
+      {showSelectTimeModal ? (
+        <SelectTimeRangeModal
+          isOpen={showSelectTimeModal}
+          onClose={() => setShowSelectTimeModal(false)}
+          successCallback={(data) => {
+            pausePolling();
+            setSelectedMetricsOption(
+              "CUSTOM: " +
+                formatDate(new Date(data.startTime * 1000))
+                  .split("T")
+                  .join(" ")
+            );
+            getHostMetrics(data.startTime, data.endTime);
+            setShowSelectTimeModal(false);
+          }}
+        />
+      ) : null}
+      <div className="d-flex w-100 justify-content-center">
+        <div className="w-100 mx-5">
+          {getCmponentsToBeRestarted(get(allHostModels, "[0]", {} as IHost))
+            .length ? (
+            <div>
+              <Alert className="rounded-0" variant="warning">
+                <div className="d-flex justify-content-between">
+                  <div className="pt-2">
+                    <FontAwesomeIcon icon={faRefresh} className="me-1" />
+                    {translateWithVariables(
+                      "hosts.host.details.needToRestart",
+                      {
+                        "0": getCmponentsToBeRestarted(
+                          get(allHostModels, "[0]", {} as IHost)
+                        )?.length?.toString(),
+                        "1": String(
+                          translate("common.components")
+                        ).toLowerCase(),
+                      }
+                    )}
+                  </div>
+                  {/* Restart Button - Requires SERVICE.START_STOP 
authorization */}
+                  {canStartStopServices && !isUpgradeInProgress && (
+                    <Button
+                      variant="warning"
+                      className="text-light custom-btn"
+                      onClick={() => {
+                        const components = getCmponentsToBeRestarted(
+                          get(allHostModels, "[0]", {} as IHost)
+                        );
+                        const data = { clusterName: clusterName };
+                        setSelectedActionData(
+                          components,
+                          "",
+                          true,
+                          restartAllStaleConfigComponents,
+                          data
+                        );
+                        const nameNodeComponent = components.filter(
+                          (component: any) =>
+                            getComponentName(component) === "NAMENODE"
+                        )[0];
+                        if (
+                          nameNodeComponent &&
+                          get(nameNodeComponent, "workStatus", "") ===
+                            ComponentStatus.STARTED
+                        ) {
+                          checkNnLastCheckpointTime(
+                            () => setShowConfirmationModal(true),
+                            get(nameNodeComponent, "hostName", ""),
+                            clusterName
+                          );
+                        } else {
+                          setShowConfirmationModal(true);
+                        }
+                      }}
+                    >
+                      {String(translate("common.restart")).toUpperCase()}
+                    </Button>
+                  )}
+                </div>
+              </Alert>
+            </div>
+          ) : null}
+          <div className="d-flex w-100 mb-4">
+            <Card className="w-50 rounded-0 me-4">
+              <div className="d-flex justify-content-between px-3 pt-3">
+                <h3 className="mt-2">{translate("common.components")}</h3>
+                {/* Add Component Dropdown - Requires 
SERVICE.ADD_DELETE_SERVICES authorization */}
+                {canAddDeleteServices && !isUpgradeInProgress && (
+                  <Dropdown>
+                    <Dropdown.Toggle
+                      variant="transparent"
+                      className="btn-default"
+                    >
+                      <FontAwesomeIcon icon={faPlus} className="me-2" />
+                      <span className="me-2">
+                        {String(translate("common.add")).toUpperCase()}
+                      </span>
+                    </Dropdown.Toggle>
+                    <Dropdown.Menu className="rounded-0">
+                      {addableComponents.map((component) => (
+                        <Dropdown.Item
+                          key={component.component_name}
+                          onClick={() => {
+                            //TODO: Will be implemented in future PR
+                          }}
+                        >
+                          {component.display_name}
+                        </Dropdown.Item>
+                      ))}
+                    </Dropdown.Menu>
+                  </Dropdown>
+                )}
+              </div>
+              <hr />
+              <Table
+                data={get(allHostModels, "[0].hostComponents", [])}
+                columns={columnsInComponentsTable}
+                scrollable={false}
+              />
+            </Card>
+            <HostMetrics
+              metricsData={metricsData}
+              allHostModels={allHostModels}
+              selectedMetricsOption={selectedMetricsOption}
+              setSelectedMetricsOption={setSelectedMetricsOption}
+              setShowSelectTimeModal={setShowSelectTimeModal}
+            />
+          </div>
+          <div className="d-flex w-100 mb-4">
+            <Card className="w-50 rounded-0 me-4">
+              <div className="d-flex justify-content-between px-3 pt-3">
+                <h3 className="mt-2">{translate("common.summary")}</h3>
+              </div>
+              <hr />
+              <div className="pb-3">
+                {Object.keys(summary).map((key: string) => {
+                  return (
+                    <div className="d-flex" key={key}>
+                      <div className="d-flex justify-content-end mb-2 w-40">
+                        <div className="me-2 fw-bold">{key}:</div>
+                      </div>
+                      <div>{get(summary, key, "")}</div>
+                      {/* Edit Rack - Requires HOST.ADD_DELETE_COMPONENTS 
authorization */}
+                      {key === "Rack" &&
+                      get(summary, key) &&
+                      canManageHostComponents &&
+                      !isUpgradeInProgress ? (
+                        <FontAwesomeIcon
+                          icon={faPencil}
+                          className="ms-2 custom-link"
+                          onClick={() => {
+                            const data = {
+                              RequestInfo: {
+                                context: "Set Rack",
+                                query: 
`Hosts/host_name.in(${params.hostname})`,
+                              },
+                              Body: {
+                                Hosts: {
+                                  rack_info: get(summary, key, ""),
+                                },
+                              },
+                            };
+                            modalManager.show(
+                              <SetRackInfoModal
+                                clusterName={clusterName}
+                                data={data}
+                                callback={setAllHostModels}
+                                hostNames={[get(summary, "Hostname", "")]}
+                              />
+                            );
+                          }}
+                        />
+                      ) : null}
+                    </div>
+                  );
+                })}
+              </div>
+            </Card>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
diff --git a/ambari-web/latest/src/store/context.tsx 
b/ambari-web/latest/src/store/context.tsx
index 835916d506..d7a7618bf0 100644
--- a/ambari-web/latest/src/store/context.tsx
+++ b/ambari-web/latest/src/store/context.tsx
@@ -74,6 +74,7 @@ interface AppContextProps {
   sessionExists: boolean;
   clusterState: any;
   upgradeIsRunning: boolean;
+  upgradeSuspended: boolean;
 }
 
 export const AppContext = createContext<AppContextProps>({
@@ -110,6 +111,7 @@ export const AppContext = createContext<AppContextProps>({
   sessionsValidated: false,
   clusterState: {},
   upgradeIsRunning: false,
+  upgradeSuspended: false,
 });
 
 export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
@@ -151,7 +153,9 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
 
   const [allHostNames, setAllHostNames] = useState([]);
 
-  const upgradeIsRunning = false; // TODO: This will be implemented soon to 
check if upgrade is running
+  // TODO: These will be implemented soon to check upgrade status
+  const upgradeIsRunning = false;
+  const upgradeSuspended = false;
 
   const fetchClusterServices = async () => {
     try {
@@ -518,6 +522,7 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
         sessionsValidated,
         clusterState,
         upgradeIsRunning,
+        upgradeSuspended,
       }}
     >
       {children}


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


Reply via email to