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

jialiang 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 dab0db8a01 AMBARI:26377: Ambari Web React: Cluster Installation Wizard 
- Step5, assignMasters (#4051)
dab0db8a01 is described below

commit dab0db8a01d09766a137beffe46a37184c8dffb7
Author: Sandeep  Kumar <[email protected]>
AuthorDate: Thu Sep 11 06:12:47 2025 +0530

    AMBARI:26377: Ambari Web React: Cluster Installation Wizard - Step5, 
assignMasters (#4051)
---
 ambari-web/latest/package.json                     |   2 +
 ambari-web/latest/src/api/AssignMastersApi.ts      |  58 +++
 ambari-web/latest/src/components/AssignMasters.tsx | 580 +++++++++++++++++++++
 .../ClusterWizard/types/StackServiceComponent.ts   |  42 ++
 4 files changed, 682 insertions(+)

diff --git a/ambari-web/latest/package.json b/ambari-web/latest/package.json
index 692353828c..2bb2f91177 100755
--- a/ambari-web/latest/package.json
+++ b/ambari-web/latest/package.json
@@ -14,6 +14,7 @@
     "@fortawesome/react-fontawesome": "^0.2.2",
     "@stomp/stompjs": "^7.1.1",
     "@types/lodash": "^4.17.16",
+    "@types/react-select": "^5.0.0",
     "axios": "^1.11.0",
     "bootstrap": "^5.3.6",
     "classnames": "^2.5.1",
@@ -32,6 +33,7 @@
     "react-hot-toast": "^2.5.2",
     "react-i18next": "^15.5.1",
     "react-router-dom": "^7.6.0",
+    "react-select": "^5.10.2",
     "sass": "^1.88.0",
     "timeago.js": "^4.0.2"
   },
diff --git a/ambari-web/latest/src/api/AssignMastersApi.ts 
b/ambari-web/latest/src/api/AssignMastersApi.ts
new file mode 100644
index 0000000000..3b7bfd213b
--- /dev/null
+++ b/ambari-web/latest/src/api/AssignMastersApi.ts
@@ -0,0 +1,58 @@
+/**
+ * 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 { ambariApi } from "./config/axiosConfig";
+
+const AssignMastersApi = {
+  getCpuInfo: async function (HOSTS: any) {
+    const hostsParams = HOSTS.join(",");
+    const url = 
`/hosts?Hosts/host_name.in(${hostsParams})&fields=Hosts/cpu_count,Hosts/disk_info,Hosts/total_mem,Hosts/ip,Hosts/os_type,Hosts/os_arch,Hosts/public_host_name&minimal_response=true&_=1731567268225`;
+    const response = await ambariApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response;
+  },
+  postRecommendations: async function (
+    payload: any,
+    STACK: string,
+    VERSION: string
+  ) {
+    const url = `/stacks/${STACK}/versions/${VERSION}/recommendations`;
+    const response = await ambariApi.request({
+      url: url,
+      method: "POST",
+      data: payload,
+    });
+    return response.data;
+  },
+  postValidations: async function (
+    payload: any,
+    STACK: string,
+    VERSION: string
+  ) {
+    const url = `/stacks/${STACK}/versions/${VERSION}/validations`;
+    const response = await ambariApi.request({
+      url: url,
+      method: "POST",
+      data: payload,
+    });
+    return response.data;
+  },
+};
+export default AssignMastersApi;
diff --git a/ambari-web/latest/src/components/AssignMasters.tsx 
b/ambari-web/latest/src/components/AssignMasters.tsx
new file mode 100644
index 0000000000..e651265b7a
--- /dev/null
+++ b/ambari-web/latest/src/components/AssignMasters.tsx
@@ -0,0 +1,580 @@
+/**
+ * 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 { useEffect, useReducer, useState } from "react";
+import { Row, Col, Form, Card, Button, CardBody } from "react-bootstrap";
+import { Utility } from "../Utils/Utility.ts";
+import { misc } from "../Utils/misc.ts";
+import Spinner from "./Spinner.tsx";
+import _, { filter, get, map, uniq } from "lodash";
+import Select from "react-select";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
+import AssignMastersApi from "../api/AssignMastersApi.ts";
+import { ServicesResponse } from 
"../screens/ClusterWizard/types/StackServiceComponent.ts";
+import { ChooseServicesApi } from "../api/chooseServicesApi.ts";
+
+interface Host {
+  hostname: string;
+  cores: number;
+  memory: number;
+  components: string[];
+}
+
+interface Masters {
+  display_name: string;
+  component: string;
+  serviceId: string;
+  host_id: number;
+  hostName: string;
+  isInstalled?: boolean;
+}
+
+interface State {
+  hosts: { [key: string]: Host };
+}
+
+interface Action {
+  type: string;
+  payload: any;
+}
+
+const initialState: State = {
+  hosts: {},
+};
+
+function reducer(state: State, action: Action): State {
+  switch (action.type) {
+    case "SET_HOSTS_DATA":
+      return {
+        ...state,
+        hosts: action.payload,
+      };
+    case "UPDATE_COMPONENT_HOST": {
+      const { component, oldHost, newHost } = action.payload;
+      const updatedHosts = { ...state.hosts };
+
+      if (oldHost) {
+        updatedHosts[oldHost].components = updatedHosts[
+          oldHost
+        ].components.filter((c) => c !== component);
+      }
+
+      // Add component to new host if newHost is not null and it doesn't 
already exist
+      if (newHost && !updatedHosts[newHost].components.includes(component)) {
+        const index = updatedHosts[newHost].components.indexOf(component);
+        if (index !== -1) {
+          updatedHosts[newHost].components.splice(index + 1, 0, component);
+        } else {
+          updatedHosts[newHost].components.push(component);
+        }
+      }
+
+      return {
+        ...state,
+        hosts: updatedHosts,
+      };
+    }
+    default:
+      return state;
+  }
+}
+
+interface AssignMastersProps {
+  STACK: string;
+  VERSION: string;
+  superMasters: any;
+  hostsList: any;
+  services: string[];
+  setCanProceed: (canProcced: boolean) => void;
+  dispatch: any;
+  installedServices?: string[];
+  parentState?:any;
+}
+
+export default function AssignMasters({
+  STACK,
+  VERSION,
+  superMasters,
+  hostsList,
+  services,
+  dispatch: dispatchParent,
+  installedServices,
+  parentState,
+  setCanProceed,
+}: AssignMastersProps) {
+  const [state, dispatch] = useReducer(reducer, get(
+      parentState,
+      `clusterCreationSteps.MASTERS.data.state`,
+      undefined
+    ) || initialState);
+  const [loading, setLoading] = useState(false);
+  const [mastersData, setMastersData] = useState<Masters[]>([]);
+  const notMasters = ["MYSQL_SERVER", "HIVE_SERVER_INTERACTIVE"];
+
+  useEffect(() => {
+    async function getMastersData() {
+      setLoading(true);
+
+      const cpuResponse = await AssignMastersApi.getCpuInfo(hostsList);
+      const hostnames = cpuResponse.data.items.map(
+        (item: any) => item.Hosts.host_name
+      );
+
+      const recommendationPayload1 = Utility.recommendationPayload(
+        hostnames,
+        "host_groups",
+        [],
+        [],
+        services
+      );
+
+      const recommendationsResponse1 =
+        await AssignMastersApi.postRecommendations(
+          recommendationPayload1,
+          STACK,
+          VERSION
+        );
+
+      const firstBlueprintClusterBinding =
+        recommendationsResponse1.resources[0].recommendations
+          .blueprint_cluster_binding.host_groups;
+      const firstBlueprint =
+        recommendationsResponse1.resources[0].recommendations.blueprint
+          .host_groups;
+
+      const recommendationPayload2 = Utility.recommendationPayload(
+        hostnames,
+        "host_groups",
+        firstBlueprint,
+        firstBlueprintClusterBinding,
+        services
+      );
+
+      const recommendationsResponse2 =
+        await AssignMastersApi.postRecommendations(
+          recommendationPayload2,
+          STACK,
+          VERSION
+        );
+      const processRecommendations = (response: any) => {
+        const blueprintClusterBinding =
+          response.resources[0].recommendations.blueprint_cluster_binding
+            .host_groups;
+        const blueprint =
+          response.resources[0].recommendations.blueprint.host_groups;
+
+        return blueprintClusterBinding.reduce(
+          (acc: { [key: string]: Host }, hostGroup: any) => {
+            const hostname = hostGroup.hosts[0].fqdn;
+            const components = blueprint
+              .find((group: any) => group.name === hostGroup.name)
+              .components.map((component: any) => component.name);
+            acc[hostname] = {
+              hostname,
+              cores: 0,
+              memory: 0,
+              components,
+            };
+            return acc;
+          },
+          {}
+        );
+      };
+
+      const hostsData = processRecommendations(recommendationsResponse2);
+      // Add ZOOKEEPER_SERVER to all hosts by default
+      Object.keys(hostsData).forEach((hostname) => {
+        if (!hostsData[hostname].components.includes("ZOOKEEPER_SERVER")) {
+          hostsData[hostname].components.push("ZOOKEEPER_SERVER");
+        }
+      });
+      
+      cpuResponse.data.items.forEach((item: any) => {
+        const hostname = item.Hosts.host_name;
+        if (hostsData[hostname]) {
+          hostsData[hostname].cores = item.Hosts.cpu_count;
+          hostsData[hostname].memory = item.Hosts.total_mem;
+        }
+      });
+
+      // validations.
+      const validationPayload = {
+        hosts: hostnames,
+        validate: "host_groups",
+        recommendations: {
+          blueprint: {
+            configurations: null,
+            host_groups: firstBlueprint,
+          },
+          blueprint_cluster_binding: {
+            host_groups: firstBlueprintClusterBinding,
+          },
+        },
+        services: services,
+      };
+      AssignMastersApi.postValidations(validationPayload, STACK, VERSION);
+      //TODO: Check for validations response.
+
+      setCanProceed(true);
+      const servicesAndComponents: ServicesResponse =
+        await ChooseServicesApi.getServices(STACK, VERSION);
+
+      // Filter components based on is_master and ensure no duplicates on a 
single host
+      // except for superMaster components like ZOOKEEPER_SERVER which can be 
on multiple hosts
+      const assignedComponents = new Set<string>();
+      Object.keys(hostsData).forEach((hostname) => {
+        hostsData[hostname].components = hostsData[hostname].components.filter(
+          (component: any) => {
+            if(notMasters.includes(component)) {
+              return false;
+            }
+            const serviceComponent = servicesAndComponents.items
+              .flatMap((service: any) => service.components)
+              .find(
+                (comp: any) =>
+                  _.get(comp, "StackServiceComponents.component_name") ===
+                  component
+              );
+
+            // Allow superMaster components on multiple hosts
+            if (superMasters.includes(component)) {
+              return serviceComponent && 
+                _.get(serviceComponent, "StackServiceComponents.is_master");
+            }
+
+            if (
+              serviceComponent &&
+              _.get(serviceComponent, "StackServiceComponents.is_master") &&
+              !assignedComponents.has(component)
+            ) {
+              assignedComponents.add(component);
+              return true;
+            }
+            return false;
+          }
+        );
+      });
+
+      dispatch({ type: "SET_HOSTS_DATA", payload: hostsData });
+      dispatchParent({
+        hostsData,
+        mastersData: getTransformedMastersData(mastersData),
+        state,
+      });
+      setLoading(false);
+    }
+    getMastersData();
+  }, []);
+  
+  useEffect(() => {
+    dispatchParent({
+      mastersData: getTransformedMastersData(mastersData),
+      state,
+    });
+  }, [mastersData]);
+
+  const getTransformedMastersData = (mastersDataToBeTransformed: any) => {
+    const allHostnames = map(mastersDataToBeTransformed, "hostName");
+    const uniqueHostnames = uniq(allHostnames);
+    const transformedMasterMapping = [];
+    for (let hostname of uniqueHostnames) {
+      const hostObj = {
+        host_name: hostname,
+        masterServices: [],
+      };
+      const matchingServicesForHost = filter(mastersDataToBeTransformed, [
+        "hostName",
+        hostname,
+      ]);
+      hostObj.masterServices = matchingServicesForHost as any;
+      transformedMasterMapping.push(hostObj);
+    }
+    return transformedMasterMapping;
+  };
+
+  useEffect(() => {
+    async function setMasterComponentsData() {
+      const servicesAndComponents: ServicesResponse =
+        await ChooseServicesApi.getServices(STACK, VERSION);
+
+      
+      const mastersData: Masters[] = Object.keys(state.hosts).flatMap(
+        (hostname, index) =>
+          state.hosts[hostname].components
+          .filter(component => !notMasters.includes(component))
+          .map((component) => {
+            const serviceComponent = servicesAndComponents.items
+              .flatMap((service: any) => service.components)
+              .find(
+                (comp: any) =>
+                  _.get(comp, "StackServiceComponents.component_name") ===
+                  component
+              );
+            return {
+              display_name: _.get(
+                serviceComponent,
+                "StackServiceComponents.display_name"
+              ),
+              component: component,
+              serviceId: _.get(
+                serviceComponent,
+                "StackServiceComponents.service_name"
+              ),
+              isInstalled: installedServices?.includes(
+                _.get(serviceComponent, "StackServiceComponents.service_name")
+              ),
+              host_id: index + 1,
+              hostName: hostname,
+            };
+          })
+          // Sort components alphabetically by display_name
+          .sort((a, b) => (a.display_name || '').localeCompare(b.display_name 
|| ''))
+      );
+      dispatchParent({
+        mastersData: getTransformedMastersData(mastersData),
+        state,
+      });
+      setMastersData(mastersData);
+    }
+    setMasterComponentsData();
+  }, [state.hosts]);
+
+  const handleComponentChange = (
+    component: string,
+    oldHost: string,
+    newHost: string
+  ) => {
+    if (oldHost !== newHost) {
+      dispatch({
+        type: "UPDATE_COMPONENT_HOST",
+        payload: { component, oldHost, newHost, state },
+      });
+    }
+  };
+
+  const handleAddComponent = (component: string) => {
+    // Find the first host that doesn't already have this component
+    const availableHost = Object.keys(state.hosts).find((hostname) => {
+      return !_.get(state.hosts[hostname], "components", []).includes(
+        component as never
+      );
+    });
+
+    if (availableHost) {
+      dispatch({
+        type: "UPDATE_COMPONENT_HOST",
+        payload: { component, oldHost: null, newHost: availableHost, state },
+      });
+    }
+  };
+
+  const handleRemoveComponent = (component: string, hostname: string) => {
+    dispatch({
+      type: "UPDATE_COMPONENT_HOST",
+      payload: { component, oldHost: hostname, newHost: null, state },
+    });
+  };
+
+
+  return (
+    <>
+      <div className="step-title">Assign Masters</div>
+      <p className="step-description">
+        Assign master components to hosts you want to run them on.
+      </p>
+      {loading ? (
+        <Spinner />
+      ) : (
+        <Card>
+          <CardBody>
+            <Row>
+              <Col md={8}>
+                {(() => {
+                  // Get all components from all hosts
+                  const allComponentsWithHosts: Array<{component: string, 
hostname: string}> = [];
+                  
+                  // Collect all components with their assigned hosts
+                  Object.keys(state.hosts).forEach(hostname => {
+                    state.hosts[hostname].components.forEach(component => {
+                      allComponentsWithHosts.push({
+                        component,
+                        hostname
+                      });
+                    });
+                  });
+                  
+                  // Get display names for all components
+                  const componentDisplayNames: { [key: string]: string } = {};
+                  allComponentsWithHosts.forEach(({component, hostname}) => {
+                    const serviceComponent = mastersData.find(m => 
+                      m.component === component && m.hostName === hostname
+                    );
+                    if (serviceComponent) {
+                      // Use component as key to ensure uniqueness
+                      const key = `${component}-${hostname}`;
+                      componentDisplayNames[key] = 
serviceComponent.display_name || component;
+                    }
+                  });
+                  
+                  // Sort all components by their display names
+                  return allComponentsWithHosts
+                    // Filter out installed components
+                    .filter(({component, hostname}) => {
+                      const matchingMasterForInstall = mastersData.find(m => 
+                        m.component === component && m.hostName === hostname 
&& m.isInstalled
+                      );
+                      return !(matchingMasterForInstall && 
matchingMasterForInstall.isInstalled);
+                    })
+                    // Sort by display name
+                    .sort((a, b) => {
+                      const keyA = `${a.component}-${a.hostname}`;
+                      const keyB = `${b.component}-${b.hostname}`;
+                      const displayNameA = componentDisplayNames[keyA] || 
a.component;
+                      const displayNameB = componentDisplayNames[keyB] || 
b.component;
+                      return displayNameA.localeCompare(displayNameB);
+                    })
+                    // Render each component
+                    .map(({component, hostname}) => {
+                      const key = `${component}-${hostname}`;
+                      const displayName = componentDisplayNames[key] || 
component;
+                      
+                      return (
+                        <Row key={`${hostname}-${component}`} className="mb-3">
+                          <Col xs={4} className="text-end mt-3">
+                            <Form.Label className="fw-100">
+                              {displayName}:
+                            </Form.Label>
+                          </Col>
+                          <Col xs={4}>
+                            <Select
+                              id={`select-${component}`}
+                              value={{ label: hostname, value: hostname }}
+                              onChange={(selectedOption) => {
+                                if (selectedOption) {
+                                  handleComponentChange(
+                                    component,
+                                    hostname,
+                                    selectedOption.value
+                                  );
+                                }
+                              }}
+                              options={Object.keys(state.hosts)
+                                .filter(
+                                  (host) =>
+                                    host === hostname ||
+                                    !_.get(
+                                      state.hosts[host],
+                                      "components",
+                                      []
+                                    ).includes(component as never)
+                                )
+                                .map((host) => ({
+                                  label: host,
+                                  value: host,
+                                }))}
+                              className="w-100"
+                            />
+                          </Col>
+                          <Col xs={4} className="d-flex align-items-center">
+                            {superMasters.includes(component) && (
+                              <>
+                                {Object.keys(state.hosts).some(
+                                  (host) =>
+                                    !state.hosts[host].components.includes(
+                                      component
+                                    )
+                                ) ? (
+                                  <Button
+                                    variant="success"
+                                    size="sm"
+                                    onClick={() => 
handleAddComponent(component)}
+                                  >
+                                    <FontAwesomeIcon icon={faPlus} />
+                                  </Button>
+                                ) : (
+                                  <Button
+                                    variant="success"
+                                    size="sm"
+                                    onClick={() =>
+                                      handleRemoveComponent(component, 
hostname)
+                                    }
+                                  >
+                                    <FontAwesomeIcon icon={faMinus} />
+                                  </Button>
+                                )}
+                              </>
+                            )}
+                          </Col>
+                        </Row>
+                      );
+                    });
+                })()}
+              </Col>
+
+              <Col md={4}>
+                {Object.keys(state.hosts).map((hostname) => (
+                  <Card key={hostname} className="mb-3">
+                    <Card.Body>
+                      <Card.Title className="text-nowrap text-truncate small">
+                        {hostname}({" "}
+                        
{misc.formatBandwidth(state.hosts[hostname].memory)},{" "}
+                        {state.hosts[hostname].cores} cores)
+                      </Card.Title>
+                      <Card.Text>
+                        {/* Display components sorted alphabetically by 
display name */}
+                        {(() => {
+                          // Create display name mapping
+                          const displayNameMap: { [key: string]: string } = {};
+                          
+                          // Get display names for each component
+                          state.hosts[hostname].components.forEach(comp => {
+                            const master = mastersData.find(m => 
+                              m.component === comp && m.hostName === hostname
+                            );
+                            displayNameMap[comp] = master?.display_name || 
comp;
+                          });
+                          
+                          // Sort components alphabetically by display name
+                          return [...state.hosts[hostname].components]
+                            .sort((a, b) => 
+                              (displayNameMap[a] || 
a).localeCompare(displayNameMap[b] || b)
+                            )
+                            .map(comp => (
+                              <Button
+                                key={comp}
+                                size="sm"
+                                variant="success"
+                                className="me-1 mb-1 small"
+                              >
+                                {displayNameMap[comp] || comp}
+                              </Button>
+                            ));
+                        })()}
+                      </Card.Text>
+                    </Card.Body>
+                  </Card>
+                ))}
+              </Col>
+            </Row>
+          </CardBody>
+        </Card>
+      )}
+    </>
+  );
+}
diff --git 
a/ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts 
b/ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts
new file mode 100644
index 0000000000..2f0142ca5a
--- /dev/null
+++ b/ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts
@@ -0,0 +1,42 @@
+/**
+ * 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 StackServiceComponent {
+  componentName: string;
+  isDisabled: boolean;
+}
+
+export interface ServiceComponent {
+  StackServiceComponents: StackServiceComponent;
+}
+
+export interface Service {
+  StackServices: {
+    service_name: string;
+  };
+  components: ServiceComponent[];
+}
+
+export interface ServicesResponse {
+  items: Service[];
+}
+
+export interface SelectedService {
+  is_selected: boolean;
+  service_name: string;
+}


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

Reply via email to