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 ffbce1ff34 AMBARI:-26571: Ambari Web React: Component actions for 
clients - Install / Re-Install (#4105)
ffbce1ff34 is described below

commit ffbce1ff34a8d0adfa47f499146d2085d2ad54d3
Author: Sandeep  Kumar <[email protected]>
AuthorDate: Thu Jan 29 18:32:21 2026 +0530

    AMBARI:-26571: Ambari Web React: Component actions for clients - Install / 
Re-Install (#4105)
---
 .../latest/src/screens/Hosts/HostSummary.tsx       |  34 ++-
 ambari-web/latest/src/screens/Hosts/actions.tsx    | 184 ++++++++++++-
 ambari-web/latest/src/screens/Hosts/utils.tsx      | 303 +++++++++++++++++++++
 .../src/screens/Hosts/utils/ComponentDependency.ts |  43 +++
 4 files changed, 560 insertions(+), 4 deletions(-)

diff --git a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx 
b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
index 3959dd3949..a5dd2e7109 100644
--- a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
@@ -86,6 +86,7 @@ import {
   startComponent,
   stopComponent,
   executeCustomCommand,
+  installClients,
 } from "./actions";
 import { AppContext } from "../../store/context";
 import IHost from "../../models/host";
@@ -501,7 +502,23 @@ export default function HostsSummary({
             <div
               key="install"
               onClick={() => {
-                //TODO: Will be implemented in future PR
+                if (isInit(component)) {
+                  const data = {
+                    allComponents,
+                    clusterComponents,
+                    services,
+                    // getKDCSessionState, TODO: will be added in future PR.
+                    host: allHostModels[0],
+                  };
+                  setSelectedActionData(
+                    [component],
+                    "install",
+                    false,
+                    installClients,
+                    data
+                  );
+                  setShowConfirmationModal(true);
+                }
               }}
               className={isInit(component) ? "" : "disabled-btn"}
             >
@@ -516,7 +533,20 @@ export default function HostsSummary({
             <div
               key="re-install"
               onClick={() => {
-                //TODO: Will be implemented in future PR
+                  const data = {
+                  allComponents,
+                  clusterComponents,
+                  services,
+                  // getKDCSessionState, TODO: will be added in future PR.
+                };
+                setSelectedActionData(
+                  [component],
+                  "re-install",
+                  false,
+                  installClients,
+                  data
+                );
+                setShowConfirmationModal(true);
               }}
             >
               Re-Install
diff --git a/ambari-web/latest/src/screens/Hosts/actions.tsx 
b/ambari-web/latest/src/screens/Hosts/actions.tsx
index 9fb82d38dc..86f95ddfe8 100644
--- a/ambari-web/latest/src/screens/Hosts/actions.tsx
+++ b/ambari-web/latest/src/screens/Hosts/actions.tsx
@@ -16,13 +16,14 @@
  * limitations under the License.
  */
 
-import { capitalize, cloneDeep, get, set } from "lodash";
+import { capitalize, cloneDeep, get, set, uniq } from "lodash";
 import { HostsApi } from "../../api/hostsApi";
 import {
   doDecommissionRegionServer,
   doRecommissionAndStart,
   getComponentDisplayName,
   getComponentName,
+  installHostComponentCall,
   parseNnCheckPointTime,
   showHbaseActiveWarning,
   showRegionServerWarning,
@@ -40,6 +41,7 @@ import {
 import ConfirmationModal from "../../components/ConfirmationModal";
 import { IHost } from "../../models/host";
 import { t } from "i18next";
+import { CompatibleComponent, ComponentDependency } from 
"./utils/ComponentDependency";
 
 export const sendComponentCommand = async (
   component: IHostComponent,
@@ -436,7 +438,185 @@ export const toggleMaintenanceMode = async (component: 
IHostComponent) => {
     data
   );
 };
-export const refreshConfigs = async (component: IHostComponent) => {
+function assert(condition: any, message: any) {
+  if (!condition) {
+    throw new Error(message);
+  }
+}
+
+const checkComponentDependencies = (
+  data: any,
+  component: IHostComponent,
+  opt: any
+) => {
+  var opt = opt || {};
+  opt.scope = opt.scope || "*";
+  var installedComponents;
+  switch (opt.scope) {
+    case "host":
+      assert(
+        "You should pass at least `hostName` or `installedComponents` to 
options.",
+        opt.hostName || opt.installedComponents
+      );
+      installedComponents = opt.installedComponents || [];
+      break;
+    default:
+      installedComponents = opt.installedComponents || [];
+      break;
+  }
+  return missingDependencies(data, component, installedComponents, opt)?.map(
+    (componentDependency: { chooseCompatible: (arg0: any) => any }) => {
+      return componentDependency.chooseCompatible(data.services);
+    }
+  );
+};
+
+const missingDependencies = (
+  data: any,
+  component: IHostComponent,
+  installedComponents: any,
+  opt: any
+) => {
+  opt = opt || {};
+  opt.scope = opt.scope || "*";
+  var dependencies: any = get(component, "dependencies", []);
+  dependencies =
+    opt.scope === "*"
+      ? dependencies
+      : dependencies.filter((item: any) => {
+          return item.Dependencies.scope === opt.scope;
+        });
+  if (dependencies.length === 0) return [];
+
+  var missingComponents = dependencies.filter((dependency: any) => {
+    return !installedComponents.some((installedComponent: IHostComponent) => {
+      const dependencyComponent = data.allComponents.find(
+        (host: IHostComponent) => {
+          return host.componentName === dependency.Dependencies.component_name;
+        }
+      );
+      return compatibleWith(
+        installedComponent,
+        dependencyComponent.componentName,
+        dependencyComponent.componentType
+      );
+    });
+  });
+  return missingComponents.map((missingComponent: any) => {
+    var componentFound = data.allComponents.find(
+      (hostComponent: IHostComponent) => {
+        return (
+          hostComponent.componentName ===
+          missingComponent.Dependencies.component_name
+        );
+      }
+    );
+    const compatibleComponents: CompatibleComponent[] = componentFound
+      ? [
+          {
+            componentName: componentFound.componentName,
+            serviceName: componentFound.serviceName,
+          },
+        ]
+      : [];
+
+    return new ComponentDependency(
+      missingComponent.Dependencies.component_name,
+      compatibleComponents
+    );
+  });
+};
+
+const compatibleWith = (component: any, compName: string, compType: string) => 
{
+  return (
+    component.componentName === compName ||
+    (component.componentType && component.componentType === compType)
+  );
+};
+
+export const installClients = async (
+  components: IHostComponent[],
+  data: any
+) => {
+  var clientsToInstall: IHostComponent[] = [],
+    clientsToAdd: IHostComponent[] = [],
+    missedComponents: any = [],
+    dependentComponents: any = [];
+
+  components.forEach((component) => {
+    if (["INIT", "INSTALL_FAILED"].includes(get(component, "workStatus"))) {
+      clientsToInstall.push(component);
+    } else if (typeof get(component, "workStatus") == "undefined") {
+      clientsToAdd.push(component);
+    }
+  });
+  clientsToAdd.forEach((component, _index, array) => {
+    var dependencies;
+    try {
+      dependencies = checkComponentDependencies(data, component, {
+        scope: "host",
+        installedComponents: get(data, "host.hostComponents", []),
+      });
+    } catch (error) {
+      dependencies = array.map((component) => {
+        get(component, "componentName").includes(getComponentName(component));
+      });
+    }
+    if (dependencies && dependencies.length > 0) {
+      missedComponents.push(dependencies);
+      dependentComponents.push(getComponentDisplayName(component));
+    }
+  });
+
+  missedComponents = uniq(missedComponents);
+  if (missedComponents && missedComponents.length) {
+    var popupMessage = t(
+      "host.host.addComponent.popup.clients.dependedComponents.body"
+    )
+      .replace("{0}", dependentComponents.join(", "))
+      .replace(
+        "{1}",
+        missedComponents
+          .map((component: IHostComponent) => {
+            getComponentDisplayName(component);
+          })
+          .join(", ")
+      );
+    showAlertModal(
+      t("host.host.addComponent.popup.dependedComponents.header"),
+      popupMessage
+    );
+  } else {
+    await data.getKDCSessionState(async () => {
+      var sendInstallCommand = function () {
+        if (clientsToInstall && clientsToInstall.length) {
+          sendComponentCommand(
+            clientsToInstall[0],
+            t("host.host.details.installClients"),
+            "INSTALLED"
+          );
+        }
+      };
+
+      if (clientsToAdd && clientsToAdd.length) {
+        // var message = clientsToAdd.map((component: IHostComponent) => {
+        //     return getComponentDisplayName(component)
+        //   }).join(", ");
+        // var componentObject = Object.create({
+        //   displayName: message
+        // });
+
+        // popup for add component modal.
+        sendInstallCommand();
+        clientsToAdd.forEach((component: IHostComponent) => {
+          installHostComponentCall(get(component, "hostName"), component, 
data, data?.setAllHostModels);
+        });
+      } else {
+        sendInstallCommand();
+      }
+    });
+  }
+};export const refreshConfigs = async (component: IHostComponent) => {
   const message = t("rollingrestart.context.ClientOnSelectedHost")
     .replace("{0}", getComponentDisplayName(component))
     .replace("{1}", get(component, "hostName"));
diff --git a/ambari-web/latest/src/screens/Hosts/utils.tsx 
b/ambari-web/latest/src/screens/Hosts/utils.tsx
index c9170705df..26a70dc26c 100644
--- a/ambari-web/latest/src/screens/Hosts/utils.tsx
+++ b/ambari-web/latest/src/screens/Hosts/utils.tsx
@@ -38,6 +38,8 @@ import {
 //TODO: Uncomment the below import and its usage once BackgroundOperations 
component is available
 // import BackgroundOperations from "../BackgroundOperations"; 
 import { IHost } from "../../models/host.ts";
+import { HostsApi } from "../../api/hostsApi.ts";
+import { defaultSuccessCallbackWithoutReload } from "./batchUtils.tsx";
 
 export const hostComponentCustomCommandMap = {
   REFRESHQUEUES: {
@@ -184,6 +186,50 @@ export const addDeleteComponentsMap: any = {
   },
 };
 
+const serviceComponentMetrics = [
+  "host_components/metrics/jvm/memHeapUsedM",
+  "host_components/metrics/jvm/HeapMemoryMax",
+  "host_components/metrics/jvm/HeapMemoryUsed",
+  "host_components/metrics/jvm/memHeapCommittedM",
+  "host_components/metrics/mapred/jobtracker/trackers_decommissioned",
+  "host_components/metrics/cpu/cpu_wio",
+  "host_components/metrics/rpc/client/RpcQueueTime_avg_time",
+  "host_components/metrics/dfs/FSNamesystem/*",
+  "host_components/metrics/dfs/namenode/Version",
+  "host_components/metrics/dfs/namenode/LiveNodes",
+  "host_components/metrics/dfs/namenode/DeadNodes",
+  "host_components/metrics/dfs/namenode/DecomNodes",
+  "host_components/metrics/dfs/namenode/TotalFiles",
+  "host_components/metrics/dfs/namenode/UpgradeFinalized",
+  "host_components/metrics/dfs/namenode/Safemode",
+  "host_components/metrics/runtime/StartTime",
+];
+
+const serviceSpecificParams = {
+  FLUME: "host_components/processes/HostComponentProcess",
+  YARN:
+    "host_components/metrics/yarn/Queue," +
+    "host_components/metrics/yarn/ClusterMetrics/NumActiveNMs," +
+    "host_components/metrics/yarn/ClusterMetrics/NumLostNMs," +
+    "host_components/metrics/yarn/ClusterMetrics/NumUnhealthyNMs," +
+    "host_components/metrics/yarn/ClusterMetrics/NumRebootedNMs," +
+    "host_components/metrics/yarn/ClusterMetrics/NumDecommissionedNMs",
+  HBASE:
+    "host_components/metrics/hbase/master/IsActiveMaster," +
+    "host_components/metrics/hbase/master/MasterStartTime," +
+    "host_components/metrics/hbase/master/MasterActiveTime," +
+    "host_components/metrics/hbase/master/AverageLoad," +
+    "host_components/metrics/master/AssignmentManager/ritCount",
+  STORM:
+    
"metrics/api/v1/cluster/summary,metrics/api/v1/topology/summary,metrics/api/v1/nimbus/summary",
+  HDFS: "host_components/metrics/dfs/namenode/ClusterId",
+  SSM: "host_components/processes/HostComponentProcess",
+};
+
+var requestsRunningStatus = {
+  updateServiceMetric: false,
+};
+
 export const populateHostComponentModels = (hostComponent: any) => {
   const hostComponentModel = new HostComponent({} as IHostComponent);
   (
@@ -1177,4 +1223,261 @@ export const validateInteger = (
 
 export const getClusterUpgradeStatusForHost = (upgradeState: string) => {
   return upgradeState === "IN_PROGRESS" || upgradeState.includes("HOLDING");
+};
+
+
+export const installHostComponentCall = async (
+  hostName: any,
+  component: IHostComponent,
+  data: any,
+  setAllHostModels?: (
+      data: IHost[] | ((prevModels: IHost[]) => IHost[])
+    ) => void
+) => {
+  const componentName = getComponentName(component);
+  const displayName = getComponentDisplayName(component);
+  const clusterName = get(component, "clusterName", "");
+
+  // Ensure the component has the correct hostname before proceeding
+  const updatedComponent = { ...component, hostName: hostName };
+
+  try {
+    updateAndCreateServiceComponent(componentName, data, clusterName);
+    const payload = {
+      RequestInfo: {
+        context:
+          translate("requestInfo.installHostComponent") + " " + displayName,
+      },
+      Body: {
+        host_components: [
+          {
+            HostRoles: {
+              component_name: componentName,
+            },
+          },
+        ],
+      },
+    };
+    const res = await HostsApi.hostComponentAddNewComponent(
+      clusterName,
+      hostName,
+      payload
+    );
+    addNewComponentSuccessCallback(res, {}, { component: updatedComponent }, 
setAllHostModels);
+  } catch (error) {
+    console.log("error in updating and creating service component", error);
+  }
+};
+
+const addNewComponentSuccessCallback = async (
+  _data: any,
+  _opt: any,
+  params: any,
+  setAllHostModels?: (
+      data: IHost[] | ((prevModels: IHost[]) => IHost[])
+    ) => void
+) => {
+  const component = cloneDeep(params.component);
+  const hostName = get(component, "hostName");
+  const componentName = getComponentName(component);
+  const clusterName = get(component, "clusterName");
+  const serviceName = get(component, "serviceName");
+  const displayName = get(component, "displayName");
+  const context =
+    translate("requestInfo.installNewHostComponent") + " " + displayName;
+  const urlParams = "HostRoles/state=INIT";
+  const HostRoles = {
+    state: "INSTALLED",
+  };
+
+  const payload = {
+    RequestInfo: {
+      context: context,
+      operation_level: {
+        level: "HOST_COMPONENT",
+        cluster_name: clusterName,
+        host_name: hostName,
+        service_name: serviceName || null,
+      },
+    },
+    Body: {
+      HostRoles: HostRoles,
+    },
+  };
+  var response: any = await HostsApi.commonHostComponentUpdate(
+    clusterName,
+    hostName,
+    componentName,
+    urlParams,
+    payload
+  );
+  if (typeof response === "string") {
+    response = JSON.parse(response);
+  }
+  if (!response || !response.Requests || !response.Requests.id) {
+    return false;
+  }
+
+  if (setAllHostModels) {
+    setAllHostModels((prevModels: IHost[]) => {
+      return prevModels.map((host: IHost) => {
+        if (get(host, "hostName") === hostName) {
+          const hostModel = cloneDeep(host);
+          const hostComponents = get(
+            hostModel,
+            "hostComponents",
+            [] as IHostComponent[]
+          );
+          hostComponents.push(component);
+          set(
+            hostModel,
+            "hostComponents",
+            sortBasedOnMasterSlave(hostComponents, "componentCategory")
+          );
+          return hostModel;
+        }
+        return host;
+      });
+    });
+  }
+
+  const requestId = get(response, "Requests.id", -1);
+  defaultSuccessCallbackWithoutReload(requestId);
+};
+
+const updateAndCreateServiceComponent = async(
+  componentName: string,
+  data: any,
+  clusterName: string
+) => {
+  var url =
+    "/components/?fields=ServiceComponentInfo/service_name," +
+    
"ServiceComponentInfo/category,ServiceComponentInfo/installed_count,ServiceComponentInfo/started_count,ServiceComponentInfo/init_count,ServiceComponentInfo/install_failed_count,ServiceComponentInfo/unknown_count,ServiceComponentInfo/total_count,ServiceComponentInfo/display_name,host_components/HostRoles/host_name&minimal_response=true";
+  try {
+    await HostsApi.updateComponentsState(clusterName, url);
+    updateServiceMetric(
+      componentName,
+      data,
+      createServiceComponent,
+      clusterName
+    );
+  } catch (error) {
+    console.log("error in updating and creating service component", error);
+  }
+};
+
+const getConditionalFields = (data: any) => {
+  let conditionalFields = serviceComponentMetrics.slice(0);
+  let serviceParams = cloneDeep(serviceSpecificParams);
+  set(serviceParams, "ONEFS", "metrics/*,");
+
+  data.services.forEach((service: any) => {
+    const urlParams = get(serviceParams, service.ServiceInfo.service_name);
+    if (urlParams) {
+      conditionalFields.push(urlParams);
+    }
+  });
+
+  return conditionalFields;
+};
+
+const isComponentPresent = (
+  componentName: string,
+  allServiceComponents: any
+) => {
+  return allServiceComponents.items?.some((item: any) => {
+    return get(item, "ServiceComponentInfo.component_name") === componentName;
+  });
+};
+
+const updateServiceMetric = async (
+  componentName: string,
+  data: any,
+  callback: Function,
+  clusterName: string
+) => {
+  const isATSPresent = isComponentPresent(
+    "APP_TIMELINE_SERVER",
+    data.clusterComponents
+  );
+  const isHaEnabled = false;
+
+  const conditionalFields = getConditionalFields(data);
+  const conditionalFieldsString =
+    conditionalFields.length > 0 ? "," + conditionalFields.join(",") : "";
+  const isFlumeInstalled = data.services.filter(
+    (service: any) => service.ServiceInfo.service_name === "FLUME"
+  );
+  const isATSInstalled =
+    data.services.filter(
+      (service: any) => service.ServiceInfo.service_name === "YARN"
+    ) && isATSPresent;
+  const flumeHandlerParam = isFlumeInstalled
+    ? "ServiceComponentInfo/component_name=FLUME_HANDLER|"
+    : "";
+  const atsHandlerParam = isATSInstalled
+    ? "ServiceComponentInfo/component_name=APP_TIMELINE_SERVER|"
+    : "";
+  const haComponents = isHaEnabled
+    ? 
"ServiceComponentInfo/component_name=JOURNALNODE|ServiceComponentInfo/component_name=ZKFC|"
+    : "";
+  const url =
+    "/components/?" +
+    flumeHandlerParam +
+    atsHandlerParam +
+    haComponents +
+    "ServiceComponentInfo/category.in(MASTER,CLIENT)&fields=" +
+    "ServiceComponentInfo/service_name," +
+    "host_components/HostRoles/display_name," +
+    "host_components/HostRoles/host_name," +
+    "host_components/HostRoles/public_host_name," +
+    "host_components/HostRoles/state," +
+    "host_components/HostRoles/maintenance_state," +
+    "host_components/HostRoles/stale_configs," +
+    "host_components/HostRoles/ha_state," +
+    "host_components/HostRoles/desired_admin_state," +
+    conditionalFieldsString +
+    "&minimal_response=true";
+
+  if (!requestsRunningStatus.updateServiceMetric) {
+    requestsRunningStatus.updateServiceMetric = true;
+    try {
+      await HostsApi.updateServiceMetric(clusterName, url);
+      requestsRunningStatus.updateServiceMetric = false;
+      callback(componentName, data, clusterName);
+    } catch (error) {
+      console.log("error in updating service metric", error);
+    }
+  } else {
+    callback(componentName, data, clusterName);
+  }
+};
+
+const createServiceComponent = (
+  componentName: string,
+  data: any,
+  clusterName: string
+) => {
+  const allServiceComponents = data.clusterComponents;
+
+  if (
+    allServiceComponents &&
+    isComponentPresent(componentName, allServiceComponents)
+  ) {
+    return;
+  } else {
+    const payload = {
+      components: [
+        {
+          ServiceComponentInfo: {
+            component_name: componentName,
+          },
+        },
+      ],
+    };
+    const serviceName = allServiceComponents.items.find((item: any) => {
+      return item.ServiceComponentInfo.component_name === componentName;
+    }).ServiceComponentInfo.service_name;
+    HostsApi.commonCreateComponent(clusterName, serviceName, payload);
+  }
 };
\ No newline at end of file
diff --git a/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts 
b/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts
new file mode 100644
index 0000000000..d23400752c
--- /dev/null
+++ b/ambari-web/latest/src/screens/Hosts/utils/ComponentDependency.ts
@@ -0,0 +1,43 @@
+/**
+ * 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.
+ */
+
+export interface CompatibleComponent {
+  componentName: string;
+  serviceName: string;
+}
+
+export class ComponentDependency {
+  componentName: string;
+  compatibleComponents: CompatibleComponent[];
+
+  constructor(componentName: string, compatibleComponents: 
CompatibleComponent[] = []) {
+    this.componentName = componentName;
+    this.compatibleComponents = compatibleComponents;
+  }
+
+  /**
+   * Find the first compatible component which belongs to a service that is 
installed
+   */
+  chooseCompatible(services: any) {
+    const compatibleComponent = this.compatibleComponents.find(component => {
+      return services.some((service: any) => service.ServiceInfo.service_name 
=== component.serviceName);
+    });
+
+    return (compatibleComponent || this.compatibleComponents[0]).componentName;
+  }
+}
\ No newline at end of file


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

Reply via email to