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

JiaLiangC pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/trunk by this push:
     new a01b6745cf AMBARI-26662: Fix HDFS NameNode/ZKFC summary labels and 
incorrect federation detection (#4223)
a01b6745cf is described below

commit a01b6745cf618b20accde2e670b7c78115c21367
Author: Himanshu Maurya <[email protected]>
AuthorDate: Wed Sep 23 04:50:22 2026 +0530

    AMBARI-26662: Fix HDFS NameNode/ZKFC summary labels and incorrect 
federation detection (#4223)
---
 .../latest/src/hooks/useHDFSConfigUpdater.ts       |  99 +++++++++++++------
 ambari-web/latest/src/locales/en/translation.json  |   1 +
 ambari-web/latest/src/locales/zh/translation.json  |   1 +
 .../screens/Services/ServiceComponents.test.tsx    |  91 +++++++++++++++++
 .../src/screens/Services/ServiceComponents.tsx     | 108 +++++++++------------
 ambari-web/latest/src/screens/messages.ts          |   1 +
 6 files changed, 211 insertions(+), 90 deletions(-)

diff --git a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
index 39a31a1a04..1135bd0bf9 100644
--- a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
@@ -82,6 +82,34 @@ export const useHDFSConfigUpdater = () => {
     }
   }, [JSON.stringify(configsData?.items)]);
 
+  // Group by the namespaces the installed NameNodes report, the way Ember's 
HDFS
+  // masterComponentGroups does. Deriving them from dfs.nameservices instead 
counts a
+  // declared-but-unpopulated nameservice, which flips the summary into the 
federated
+  // layout on a cluster that only added an observer NameNode.
+  const buildNameNodeGroups = (nameNodes: any[], nameSpacePath: string) => {
+    const groups: any[] = [];
+    nameNodes.forEach((nameNode: any) => {
+      const nameSpace = get(nameNode, nameSpacePath) || "default";
+      const hostName =
+        get(nameNode, "host_name") || get(nameNode, "HostRoles.host_name");
+      let group = find(groups, ["name", nameSpace]);
+      if (!group) {
+        group = {
+          name: nameSpace,
+          title: nameSpace,
+          hosts: [],
+          components: ["NAMENODE", "ZKFC"],
+          clusterId: "default",
+        };
+        groups.push(group);
+      }
+      if (hostName && !group.hosts.includes(hostName)) {
+        group.hosts.push(hostName);
+      }
+    });
+    return groups;
+  };
+
   function inferNamespace() {
     const hdfsModel = cloneDeep(allServiceModels["hdfs"]);
     const isHAEnabled = hdfsModel?.isNameNodeHaEnabled;
@@ -138,16 +166,10 @@ export const useHDFSConfigUpdater = () => {
               }
             });
 
-            // Create masterComponentGroups for federation detection
-            const masterComponentGroups = nameSpaces
-              .filter((ns: any) => ns && ns.nameSpace && ns.hostNames)
-              .map((ns: any) => ({
-                name: ns.nameSpace,
-                title: ns.nameSpace,
-                hosts: ns.hostNames.filter((host: string) => host), // Filter 
out null/undefined hosts
-                components: ["NAMENODE", "ZKFC"],
-                clusterId: "default"
-              }));
+            const masterComponentGroups = buildNameNodeGroups(
+              allNameNodes,
+              "haNameSpace"
+            );
 
             hdfsModel.updateConfig({
               namespaces: nameSpaces,
@@ -314,34 +336,51 @@ export const useHDFSConfigUpdater = () => {
     currentConfig[ServiceComponentFields.HDFS.journalNodes] =
       componentHosts("JOURNALNODE");
 
-    // Update masterComponentGroups when master components change
+    // Rebuilt on every poll, so it has to use the same NameNode-derived 
grouping as
+    // inferNamespace or it reintroduces the nameservice that was just 
discarded.
     if (isHAEnabled && configsData?.items) {
       const hdfsSiteConfigs = find(configsData?.items, ["type", "hdfs-site"]);
       if (hdfsSiteConfigs) {
         const properties = get(hdfsSiteConfigs, "properties", {});
         const nameSpaceProperty = properties["dfs.nameservices"];
         if (nameSpaceProperty) {
-          const nameSpaces = nameSpaceProperty.split(",");
-          const masterComponentGroups = nameSpaces.map((nameSpace: string) => {
-            const nameNodeIdsProperty = 
properties[`dfs.ha.namenodes.${nameSpace}`];
-            let hosts: string[] = [];
-            if (nameNodeIdsProperty) {
-              const nameNodeIds = nameNodeIdsProperty.split(",");
-              hosts = nameNodeIds.map((id: any) => {
-                const propertyValue = 
properties[`dfs.namenode.http-address.${nameSpace}.${id}`];
-                const matches = propertyValue && 
propertyValue.match(/([\D\d]+)\:\d+$/);
-                return matches && matches[1];
-              }).filter((host: string) => host); // Filter out null/undefined 
hosts
+          const nameSpaces = nameSpaceProperty
+            .split(",")
+            .map((nameSpace: string) => {
+              const nameNodeIdsProperty = 
properties[`dfs.ha.namenodes.${nameSpace}`];
+              let hostNames: string[] = [];
+              if (nameNodeIdsProperty) {
+                const nameNodeIds = nameNodeIdsProperty.split(",");
+                hostNames = nameNodeIds.map((id: any) => {
+                  const propertyValue = 
properties[`dfs.namenode.http-address.${nameSpace}.${id}`];
+                  const matches = propertyValue && 
propertyValue.match(/([\D\d]+)\:\d+$/);
+                  return matches && matches[1];
+                }).filter((host: string) => host); // Filter out 
null/undefined hosts
+              }
+              return { nameSpace, hostNames };
+            });
+
+          const nameNodeComponent = masterComponents.find(
+            (comp: any) => comp.componentName === "NAMENODE"
+          );
+          const allNameNodes = map(
+            nameNodeComponent?.hostComponents,
+            "HostRoles"
+          );
+          allNameNodes.forEach((component: any) => {
+            const nameSpaceObject = nameSpaces.find(
+              (ns: any) =>
+                ns && ns.hostNames && 
ns.hostNames.includes(component.host_name)
+            );
+            if (nameSpaceObject) {
+              set(component, "haNameSpace", nameSpaceObject.nameSpace);
             }
-            return {
-              name: nameSpace,
-              title: nameSpace,
-              hosts: hosts,
-              components: ["NAMENODE", "ZKFC"],
-              clusterId: "default"
-            };
           });
-          currentConfig.federationNamespaces = masterComponentGroups;
+
+          currentConfig.federationNamespaces = buildNameNodeGroups(
+            allNameNodes,
+            "haNameSpace"
+          );
         }
       }
     } else {
diff --git a/ambari-web/latest/src/locales/en/translation.json 
b/ambari-web/latest/src/locales/en/translation.json
index cbb313a808..8dce984461 100644
--- a/ambari-web/latest/src/locales/en/translation.json
+++ b/ambari-web/latest/src/locales/en/translation.json
@@ -2155,6 +2155,7 @@
   "services.service.summary.nameNode":"NameNode Web UI",
   "services.service.summary.nameNode.active":"Active NameNode",
   "services.service.summary.nameNode.standby":"Standby NameNode",
+  "services.service.summary.nameNode.observer":"Observer NameNode",
   "services.service.summary.jobTracker":"JobTracker",
   "services.service.summary.jobTrackerWebUI":"JobTracker Web UI",
   "services.service.summary.hbaseMaster":"HBase Master Web UI",
diff --git a/ambari-web/latest/src/locales/zh/translation.json 
b/ambari-web/latest/src/locales/zh/translation.json
index f9de83342d..cb2d1c4794 100644
--- a/ambari-web/latest/src/locales/zh/translation.json
+++ b/ambari-web/latest/src/locales/zh/translation.json
@@ -2029,6 +2029,7 @@
   "services.service.summary.nameNode": "NameNode Web UI",
   "services.service.summary.nameNode.active": "活动 NameNode",
   "services.service.summary.nameNode.standby": "备用 NameNode",
+  "services.service.summary.nameNode.observer": "观察者 NameNode",
   "services.service.summary.jobTracker": "JobTracker",
   "services.service.summary.jobTrackerWebUI": "JobTracker Web UI",
   "services.service.summary.hbaseMaster": "HBase Master Web UI",
diff --git a/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx 
b/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
index f6b3254a14..737c161568 100644
--- a/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceComponents.test.tsx
@@ -104,6 +104,41 @@ function renderComponents(data: any[]) {
   );
 }
 
+function renderHDFSSummary(hdfsModel: any) {
+  return render(
+    <MemoryRouter>
+      <HostsListStateProvider>
+        <ServiceContext.Provider
+          value={
+            {
+              allServiceModels: { hdfs: hdfsModel },
+            } as unknown as ComponentProps<
+              typeof ServiceContext.Provider
+            >["value"]
+          }
+        >
+          <ServiceComponents serviceName="HDFS" alerts={[]} />
+        </ServiceContext.Provider>
+      </HostsListStateProvider>
+    </MemoryRouter>
+  );
+}
+
+function nameNodeHostComponent(hostName: string, haStatus: string) {
+  return {
+    HostRoles: { host_name: hostName, component_name: "NAMENODE" },
+    state: "STARTED",
+    haStatus,
+  };
+}
+
+function zkfcHostComponent(hostName: string) {
+  return {
+    HostRoles: { host_name: hostName, component_name: "ZKFC" },
+    state: "STARTED",
+  };
+}
+
 describe("generic service summary", () => {
   afterEach(cleanup);
 
@@ -123,3 +158,59 @@ describe("generic service summary", () => {
     expect(screen.getByText("No components to display")).toBeTruthy();
   });
 });
+
+describe("HDFS NameNode/ZKFC summary", () => {
+  afterEach(cleanup);
+
+  it("labels each NameNode by HA state and pairs it with its host's ZKFC", () 
=> {
+    renderHDFSSummary({
+      isNameNodeHaEnabled: true,
+      federationNamespaces: [{ name: "default", title: "default", hosts: [], 
components: [], clusterId: "default" }],
+      masterComponents: [
+        {
+          componentName: "NAMENODE",
+          hostComponents: [
+            nameNodeHostComponent("nn1.example.com", "active"),
+            nameNodeHostComponent("nn2.example.com", "observer"),
+          ],
+        },
+      ],
+      slaveComponents: [
+        {
+          componentName: "ZKFC",
+          displayName: "ZKFailoverController",
+          hostComponents: [
+            zkfcHostComponent("nn1.example.com"),
+            zkfcHostComponent("nn2.example.com"),
+          ],
+        },
+      ],
+    });
+
+    expect(screen.getByText("Active NameNode")).toBeTruthy();
+    expect(screen.getByText("Observer NameNode")).toBeTruthy();
+    expect(screen.getAllByText("ZKFailoverController")).toHaveLength(2);
+  });
+
+  it("stays out of the federated layout when only one namespace has an 
installed NameNode", () => {
+    renderHDFSSummary({
+      isNameNodeHaEnabled: true,
+      // A declared-but-unpopulated second namespace must not flip isFederated.
+      federationNamespaces: [
+        { name: "ns1", title: "ns1", hosts: ["nn1.example.com"], components: 
["NAMENODE", "ZKFC"], clusterId: "default" },
+      ],
+      masterComponents: [
+        {
+          componentName: "NAMENODE",
+          hostComponents: [nameNodeHostComponent("nn1.example.com", "active")],
+        },
+      ],
+      slaveComponents: [
+        { componentName: "ZKFC", displayName: "ZKFailoverController", 
hostComponents: [] },
+      ],
+    });
+
+    expect(screen.getByText("Active NameNode")).toBeTruthy();
+    expect(screen.queryByText(/Namespace:/)).toBeNull();
+  });
+});
diff --git a/ambari-web/latest/src/screens/Services/ServiceComponents.tsx 
b/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
index 42b8b8e6e5..2a17ef215c 100644
--- a/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceComponents.tsx
@@ -20,6 +20,7 @@ import {
   cloneDeep,
   filter,
   find,
+  get,
   isEmpty,
   isObject,
   lowerCase,
@@ -34,7 +35,7 @@ import { Badge, Col, Row, Stack } from "react-bootstrap";
 import { useContext, useEffect } from "react";
 import { ServiceContext } from "../../store/ServiceContext";
 import Spinner from "../../components/Spinner";
-import { pluralize } from "../../Utils/Utility";
+import { pluralize, translate } from "../../Utils/Utility";
 import modalManager from "../../store/ModalManager";
 import { AlertsModal } from "./ServiceAlerts";
 import useClusterNavigate from "../../hooks/useClusterNavigate";
@@ -73,6 +74,20 @@ const getComponentDisplayName = (componentName: string): 
string => {
   return componentDisplayNames[componentName] || componentName;
 };
 
+// Mirrors Ember's display_name_advanced; observer is a third HA state.
+const getNameNodeLabel = (haStatus?: string) => {
+  switch ((haStatus || "").toLowerCase()) {
+    case "active":
+      return translate("services.service.summary.nameNode.active");
+    case "standby":
+      return translate("services.service.summary.nameNode.standby");
+    case "observer":
+      return translate("services.service.summary.nameNode.observer");
+    default:
+      return getComponentDisplayName("NAMENODE");
+  }
+};
+
 const TOOLTIP_MESSAGES = {
   GENERAL: {
     MAINTENANCE_MODE: 'Service is in maintenance mode',
@@ -103,6 +118,30 @@ function HDFSSummary({ alerts }: { alerts: any }) {
     if (!hdfsModel) {
       return <Spinner />;
     }
+
+    // Each ZKFC follows the NameNode on its host, as Ember's
+    // getGroupedMasterComponents orders them.
+    const zkfcHostComponents = get(
+      find(slaveComponents, ["componentName", "ZKFC"]),
+      "hostComponents",
+      []
+    );
+    const nameNodeGroupComponents: any[] = [];
+    get(
+      find(masterComponents, ["componentName", "NAMENODE"]),
+      "hostComponents",
+      []
+    ).forEach((hostComponent: any) => {
+      nameNodeGroupComponents.push(hostComponent);
+      const zkfc = find(zkfcHostComponents, (candidate: any) =>
+        get(candidate, "HostRoles.host_name") ===
+        get(hostComponent, "HostRoles.host_name")
+      );
+      if (zkfc) {
+        nameNodeGroupComponents.push(zkfc);
+      }
+    });
+
     return (
       <>
         {isFederated && (
@@ -113,10 +152,7 @@ function HDFSSummary({ alerts }: { alerts: any }) {
           />
         )}
         <Row>
-          {!isFederated && find(masterComponents, [
-            "componentName",
-            "NAMENODE",
-          ])?.hostComponents?.map((hostComponent: any) => {
+          {!isFederated && nameNodeGroupComponents.map((hostComponent: any) => 
{
             const component = hostComponent.HostRoles.component_name;
             const icon =
               hostComponent.passiveState == "OFF"
@@ -180,7 +216,13 @@ function HDFSSummary({ alerts }: { alerts: any }) {
                         );
                       }}
                     >
-                      {hostComponent.haStatus} NAMENODE
+                      {get(hostComponent, "HostRoles.component_name") === 
"ZKFC"
+                        ? get(
+                            find(slaveComponents, ["componentName", "ZKFC"]),
+                            "displayName",
+                            "ZKFC"
+                          )
+                        : getNameNodeLabel(hostComponent.haStatus)}
                     </div>
                   </Tooltip>
                 </Stack>
@@ -261,60 +303,6 @@ function HDFSSummary({ alerts }: { alerts: any }) {
               </Col>
             );
           })}
-          {!isFederated && find(slaveComponents, [
-            "componentName",
-            "ZKFC",
-          ])?.hostComponents?.map((hostComponent: any) => {
-            const icon =
-              hostComponent.passiveState == "OFF"
-                ? statusIconMap[lowerCase(hostComponent?.state)]
-                : hostComponent?.passiveState
-                ? statusIconMap["Maintenance"]
-                : null;
-            return (
-              <Col md={2}>
-                <Stack>
-                  <Stack direction="horizontal">
-                    <Tooltip
-                      message={hostComponent?.passiveState ? 
TOOLTIP_MESSAGES.GENERAL.MAINTENANCE_MODE : 
TOOLTIP_MESSAGES.GENERAL.COMPONENT_HEALTH}
-                      heading="Component Status"
-                      placement="top"
-                    >
-                      <FontAwesomeIcon
-                        icon={icon?.icon}
-                        className={`me-1 fw-bold fs-12 text-${icon?.color}`}
-                      />
-                    </Tooltip>
-                    <h3 className="text-dark mb-0">
-                      {startCase(hostComponent?.state?.toLowerCase()) ===
-                      "Installed"
-                        ? "Stopped"
-                        : startCase(hostComponent?.state?.toLowerCase())}
-                    </h3>
-                  </Stack>
-
-                  <Tooltip
-                    message={hostComponent.HostRoles.host_name}
-                    placement="top"
-                  >
-                  <div 
-                    className="custom-link text-uppercase fs-12 text-nowrap 
mt-2"
-                    onClick={() => {
-                      navigate(
-                        
`/main/hosts/${hostComponent.HostRoles.host_name}/summary`
-                      );
-                    }}
-                  >
-                    {
-                      find(slaveComponents, ["componentName", "ZKFC"])
-                        ?.displayName
-                    }
-                  </div>
-                  </Tooltip>
-                </Stack>
-              </Col>
-            );
-          })}
         </Row>
         <Row>
           {slaveComponents.map((slaveComponent: any) => {
diff --git a/ambari-web/latest/src/screens/messages.ts 
b/ambari-web/latest/src/screens/messages.ts
index 6203f8e8cc..dbf3a52501 100644
--- a/ambari-web/latest/src/screens/messages.ts
+++ b/ambari-web/latest/src/screens/messages.ts
@@ -2070,6 +2070,7 @@ const messages: any = {
     'services.service.summary.nameNode':'NameNode Web UI',
     'services.service.summary.nameNode.active':'Active NameNode',
     'services.service.summary.nameNode.standby':'Standby NameNode',
+    'services.service.summary.nameNode.observer':'Observer NameNode',
     'services.service.summary.jobTracker':'JobTracker',
     'services.service.summary.jobTrackerWebUI':'JobTracker Web UI',
     'services.service.summary.hbaseMaster':'HBase Master Web UI',


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

Reply via email to