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]