This is an automated email from the ASF dual-hosted git repository.
JiaLiangC 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 b1f7fad510 AMBARI-26637: Ambari Web React: Implement observer Namenode
(#4181)
b1f7fad510 is described below
commit b1f7fad510131eb384f4f7d032acec841862e103
Author: Sandeep Kumar <[email protected]>
AuthorDate: Fri Aug 28 08:16:45 2026 +0530
AMBARI-26637: Ambari Web React: Implement observer Namenode (#4181)
---
ambari-web/latest/src/api/observerNameNodeApi.ts | 51 +++
ambari-web/latest/src/constants.ts | 1 +
ambari-web/latest/src/enums/ServiceActionEnums.ts | 1 +
ambari-web/latest/src/hooks/useLazyQuicklinks.ts | 2 +
ambari-web/latest/src/router/RoutesList.tsx | 15 +
ambari-web/latest/src/screens/Services/Actions.tsx | 5 +
.../screens/Services/ServiceActionsUrlMapping.tsx | 7 +
.../highAvailibility/observerNameNode/Step1.tsx | 171 +++++++++
.../highAvailibility/observerNameNode/Step2.tsx | 90 +++++
.../highAvailibility/observerNameNode/Step3.tsx | 405 +++++++++++++++++++++
.../highAvailibility/observerNameNode/Step4.tsx | 399 ++++++++++++++++++++
.../highAvailibility/observerNameNode/index.tsx | 79 ++++
.../observerNameNode/observer_nn_properties.ts | 205 +++++++++++
.../observerNameNode/store/context.tsx | 186 ++++++++++
.../observerNameNode/store/reducer.ts | 42 +++
.../observerNameNode/store/types.ts | 32 ++
.../observerNameNode/validateEnablement.tsx | 161 ++++++++
.../observerNameNode/wizardSteps.tsx | 64 ++++
ambari-web/latest/src/screens/messages.ts | 35 ++
19 files changed, 1951 insertions(+)
diff --git a/ambari-web/latest/src/api/observerNameNodeApi.ts
b/ambari-web/latest/src/api/observerNameNodeApi.ts
new file mode 100644
index 0000000000..6045eab7c3
--- /dev/null
+++ b/ambari-web/latest/src/api/observerNameNodeApi.ts
@@ -0,0 +1,51 @@
+/**
+ * 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 { set } from "lodash";
+import { ambariApi } from "./config/axiosConfig";
+
+async function postRequest(clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/requests`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload,
+ });
+ set(response, "data.status", response?.status);
+ return response.data;
+}
+
+const observerNameNodeApi = {
+ enterSafeMode: function (clusterName: string, payload: any) {
+ return postRequest(clusterName, payload);
+ },
+ saveNamespace: function (clusterName: string, payload: any) {
+ return postRequest(clusterName, payload);
+ },
+ leaveSafeMode: function (clusterName: string, payload: any) {
+ return postRequest(clusterName, payload);
+ },
+ refreshNamenodes: function (clusterName: string, payload: any) {
+ return postRequest(clusterName, payload);
+ },
+ transitionToObserver: function (clusterName: string, payload: any) {
+ return postRequest(clusterName, payload);
+ },
+};
+
+export default observerNameNodeApi;
diff --git a/ambari-web/latest/src/constants.ts
b/ambari-web/latest/src/constants.ts
index 83d82a8593..0faa8a2f67 100644
--- a/ambari-web/latest/src/constants.ts
+++ b/ambari-web/latest/src/constants.ts
@@ -220,6 +220,7 @@ export enum ClusterProgressStatus {
PROVISIONING = "PROVISIONING",
ENABLING_NAMENODE_HA = "ENABLING_NAMENODE_HA",
ENABLING_NAMENODE_FEDERATION = "ENABLING_NAMENODE_FEDERATION",
+ ADDING_OBSERVER_NAMENODE = "ADDING_OBSERVER_NAMENODE",
MANAGING_JOURNALNODES = "MANAGING_JOURNALNODES",
ADDING_HOST = "ADDING_HOST",
ADDING_SERVICE = "ADDING_SERVICE",
diff --git a/ambari-web/latest/src/enums/ServiceActionEnums.ts
b/ambari-web/latest/src/enums/ServiceActionEnums.ts
index a5c6a45820..300ec1b4ac 100644
--- a/ambari-web/latest/src/enums/ServiceActionEnums.ts
+++ b/ambari-web/latest/src/enums/ServiceActionEnums.ts
@@ -31,6 +31,7 @@ export const ServiceActionEnums = {
enableHighAvailibility: "Enable Namenode HA",
enableRmHighAvailability:"Enable ResourceManager HA",
enableNamenodeFederation: "Add New HDFS Namespace",
+ addObserverNamenode: "Add Observer Namenode",
addDfsRouter: "Add DFSRouter",
addHawqStandby: "Add HAWQ Standby",
removeHawqStandby: "Remove HAWQ Standby",
diff --git a/ambari-web/latest/src/hooks/useLazyQuicklinks.ts
b/ambari-web/latest/src/hooks/useLazyQuicklinks.ts
index c9226e3f42..5802b9c918 100644
--- a/ambari-web/latest/src/hooks/useLazyQuicklinks.ts
+++ b/ambari-web/latest/src/hooks/useLazyQuicklinks.ts
@@ -1366,6 +1366,8 @@ export const useLazyQuicklinks = (serviceName: string) =>
{
url: finalUrl,
hostName: nameNode.hostName,
componentName: link.component_name,
+ haState:
+ nameNode.haStatus === "observer" ? "Observer" : undefined,
namespace: namespace, // Add namespace for federation grouping
});
});
diff --git a/ambari-web/latest/src/router/RoutesList.tsx
b/ambari-web/latest/src/router/RoutesList.tsx
index 4cdb45cbe0..5091e09feb 100644
--- a/ambari-web/latest/src/router/RoutesList.tsx
+++ b/ambari-web/latest/src/router/RoutesList.tsx
@@ -191,6 +191,21 @@ const RoutesList: RouteObject[] = [
</HaPersistenceRouteGuard>
),
},
+ {
+ path: ":componentName/observerNamenode/:stepNumber",
+ element: (
+ <HaPersistenceRouteGuard>
+ <ProtectedRoute
+ requireAuthorization="SERVICE.ENABLE_HA"
+ redirectTo="/main/dashboard/metrics"
+ >
+ <ServiceOperationRouteGuard>
+ <ServiceLoader />
+ </ServiceOperationRouteGuard>
+ </ProtectedRoute>
+ </HaPersistenceRouteGuard>
+ ),
+ },
{
path:
":componentName/federation/routerBasedFederation/:stepNumber",
element: (
diff --git a/ambari-web/latest/src/screens/Services/Actions.tsx
b/ambari-web/latest/src/screens/Services/Actions.tsx
index 74305e1b9a..aaf9dc46a7 100644
--- a/ambari-web/latest/src/screens/Services/Actions.tsx
+++ b/ambari-web/latest/src/screens/Services/Actions.tsx
@@ -77,6 +77,7 @@ import { messages } from "../messages";
import { ServiceContext } from "../../store/ServiceContext";
import ManageJournalNodes from "./highAvailibility/journalNode/index";
import EnableNamenodeFederation from "./highAvailibility/Federation/index";
+import AddObserverNamenode from "./highAvailibility/observerNameNode/index";
import useComponentAddDelete from "../Hosts/hooks/useComponentAddDelete";
import { useConfigs } from "../../hooks/useConfigs";
import useStackServices from "../../hooks/useStackServices";
@@ -2307,6 +2308,10 @@ const ActionsContent = ({ serviceName, className }:
ActionsProps) => {
{canEnableHA && canPersistWorkflow && serviceName === "HDFS" && (
<EnableNamenodeFederation />
)}
+ {/* Add Observer Namenode - Requires SERVICE.ENABLE_HA authorization,
HDFS service, and NameNode HA enabled (matches Ember.js logic) */}
+ {canEnableHA && canPersistWorkflow && serviceName === "HDFS" &&
isHAEnabled() && (
+ <AddObserverNamenode />
+ )}
<WorkflowActions
serviceName={serviceName}
canEnableHighAvailability={canEnableHA}
diff --git
a/ambari-web/latest/src/screens/Services/ServiceActionsUrlMapping.tsx
b/ambari-web/latest/src/screens/Services/ServiceActionsUrlMapping.tsx
index c6cb244ef3..d3437c8075 100644
--- a/ambari-web/latest/src/screens/Services/ServiceActionsUrlMapping.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceActionsUrlMapping.tsx
@@ -20,6 +20,7 @@ import { matchPath, useLocation, useParams } from
"react-router-dom";
import EnableHighAvailibilityNameNode from "./highAvailibility/nameNode";
import ManageJournalNodes from "./highAvailibility/journalNode";
import EnableNamenodeFederation from "./highAvailibility/Federation";
+import AddObserverNamenode from "./highAvailibility/observerNameNode";
import EnableHighAvailibilityRangerAdmin from "./highAvailibility/rangerAdmin";
import EnableHighAvailibilityResourceManger from
"./highAvailibility/resourceManager";
import ReassignComponent from "./reassign";
@@ -47,6 +48,12 @@ function ServiceActionsUrlMapping({ serviceName }: {
serviceName: string }) {
if (location.pathname.includes("federation") && componentName ===
"NameNode") {
return <EnableNamenodeFederation isMappingOnly />;
}
+ if (
+ location.pathname.includes("observerNamenode") &&
+ componentName === "NameNode"
+ ) {
+ return <AddObserverNamenode isMappingOnly />;
+ }
if (
hawqMatch?.params.componentName?.toLowerCase() === "hawq" &&
hawqMode &&
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step1.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step1.tsx
new file mode 100644
index 0000000000..bda974a480
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step1.tsx
@@ -0,0 +1,171 @@
+/**
+ * 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 } from "react";
+import {
+ Alert,
+ Card,
+ CardBody,
+ Col,
+ FormControl,
+ Row,
+ Stack,
+} from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faMultiply } from "@fortawesome/free-solid-svg-icons";
+import classNames from "classnames";
+import WizardFooter from "../../../../components/StepWizard/WizardFooter";
+import { AddObserverNamenodeContext } from "./store/context";
+import { ActionTypes } from "./store/types";
+import { messages } from "../../../messages";
+import { get } from "lodash";
+import useConfigsTags from "../../../../hooks/useConfigsTags";
+
+function Step1() {
+ const [isNextEnabled, setIsNextEnabled] = useState(false);
+ const [nameServiceIds, setNameServiceIds] = useState<string[]>([]);
+ const [existingNameServiceId, setExistingNameServiceId] = useState("");
+ const [selectedNameServiceId, setSelectedNameServiceId] = useState("");
+ const [nameError, setNameError] = useState("");
+ const {
+ dispatch,
+ stepWizardUtilities: { currentStep, handleNextImperitive },
+ flushStateToDb,
+ } = useContext(AddObserverNamenodeContext);
+ const { configsData } = useConfigsTags();
+
+ useEffect(() => {
+ getExistingNameServiceIds();
+ }, [configsData]);
+
+ useEffect(() => {
+ if (!selectedNameServiceId) {
+ setNameError("");
+ setIsNextEnabled(false);
+ return;
+ }
+ if (nameServiceIds.includes(selectedNameServiceId)) {
+ setNameError("");
+ setIsNextEnabled(true);
+ } else {
+ setIsNextEnabled(false);
+ setNameError(
+ get(messages,
"admin.observerNameNode.wizard.step1.nameserviceid.error")
+ );
+ }
+ }, [selectedNameServiceId, nameServiceIds]);
+
+ const getExistingNameServiceIds = () => {
+ let nameService = "";
+ if (configsData && Array.isArray(configsData.items)) {
+ configsData.items.forEach((item: any) => {
+ if (
+ item.type === "hdfs-site" &&
+ item.properties &&
+ item.properties["dfs.nameservices"]
+ ) {
+ nameService = item.properties["dfs.nameservices"];
+ }
+ });
+ }
+ setExistingNameServiceId(nameService);
+ const ids = nameService
+ .split(",")
+ .map((id: string) => id.trim())
+ .filter((id: string) => id);
+ setNameServiceIds(ids);
+ };
+
+ return (
+ <>
+ <h2 className="step-title">
+ {get(messages, "admin.observerNameNode.wizard.step1.header")}
+ </h2>
+ <h3 className="step-description light-text">
+ {get(messages, "admin.observerNameNode.wizard.step1.body")}
+ </h3>
+ <Alert className="mt-2" variant="warning">
+ {get(messages, "admin.observerNameNode.wizard.step1.alert")}
+ </Alert>
+ <Card className="mt-2">
+ <CardBody>
+ <Row className="align-items-center mb-2">
+ <Col md={3} className="bolder">
+ {get(
+ messages,
+ "admin.observerNameNode.wizard.step1.nameserviceid.existing"
+ )}
+ :
+ </Col>
+ <Col md={4}>
+ <div>{existingNameServiceId}</div>
+ </Col>
+ </Row>
+ <Row className="align-items-center">
+ <Col md={3} className="bolder">
+ {get(
+ messages,
+ "admin.observerNameNode.wizard.step1.nameserviceid"
+ )}
+ :
+ </Col>
+ <Col md={4}>
+ <FormControl
+ type="text"
+ value={selectedNameServiceId}
+ className={classNames({ "is-invalid": nameError })}
+ onChange={(e) => setSelectedNameServiceId(e.target.value)}
+ />
+ </Col>
+ <Col>
+ {nameError ? (
+ <Stack direction="horizontal">
+ <FontAwesomeIcon icon={faMultiply} color="red" />
+ <div className="ms-2 text-muted
text-nowrap">{nameError}</div>
+ </Stack>
+ ) : null}
+ </Col>
+ </Row>
+ </CardBody>
+ </Card>
+ <WizardFooter
+ step={currentStep}
+ isNextEnabled={isNextEnabled}
+ onBack={() => {}}
+ onNext={() => {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ nameServiceId: selectedNameServiceId,
+ },
+ },
+ });
+ flushStateToDb("next");
+ handleNextImperitive();
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ />
+ </>
+ );
+}
+
+export default Step1;
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step2.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step2.tsx
new file mode 100644
index 0000000000..996fffbc5b
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step2.tsx
@@ -0,0 +1,90 @@
+/**
+ * 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, useState } from "react";
+import { AppContext } from "../../../../store/context";
+import { map } from "lodash";
+import { AddObserverNamenodeContext } from "./store/context";
+import { ActionTypes } from "./store/types";
+import WizardFooter from "../../../../components/StepWizard/WizardFooter";
+import AssignMastersAddable from "../../../../components/AssignMastersAddable";
+import { Card } from "react-bootstrap";
+import { getStepData } from "../../../../Utils/Utility";
+import { addObserverNamenodeSteps } from "./wizardSteps";
+
+function Step2() {
+ const { services } = useContext(AppContext);
+ const {
+ state,
+ dispatch,
+ flushStateToDb,
+ stepWizardUtilities: { handleNextImperitive, currentStep },
+ } = useContext(AddObserverNamenodeContext);
+ const [isNextEnabled] = useState(true);
+ const nameServiceId = getStepData(
+ state,
+ addObserverNamenodeSteps.GET_STARTED,
+ "nameServiceId",
+ "addObserverNamenodeSteps"
+ );
+ return (
+ <>
+ <div className="step-title">Select Hosts</div>
+ <div className="step-description">
+ {`Select hosts running the NameNodes for ${nameServiceId ||
""}`.trim()}
+ </div>
+ <Card className="mt-2">
+ <Card.Body>
+ <AssignMastersAddable
+ mastersToShow={["NAMENODE"]}
+ mastersToAdd={["NAMENODE"]}
+ mastersToCreate={[]}
+ showCurrentPrefix={["NAMENODE"]}
+ showAdditionalPrefix={["NAMENODE"]}
+ services={map(services, "ServiceInfo.service_name")}
+ dispatch={(payload: any) => {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: payload,
+ },
+ });
+ flushStateToDb();
+ }}
+ />
+ </Card.Body>
+ </Card>
+ <WizardFooter
+ step={currentStep}
+ isNextEnabled={isNextEnabled}
+ onBack={() => {
+ flushStateToDb("back");
+ }}
+ onNext={() => {
+ flushStateToDb("next");
+ handleNextImperitive();
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ />
+ </>
+ );
+}
+export default Step2;
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step3.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step3.tsx
new file mode 100644
index 0000000000..77e70fff9b
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step3.tsx
@@ -0,0 +1,405 @@
+/**
+ * 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, useRef, useState } from "react";
+import { AppContext } from "../../../../store/context";
+import { cloneDeep, find, get, isEmpty } from "lodash";
+import { AddObserverNamenodeContext } from "./store/context";
+import { getStepData } from "../../../../Utils/Utility";
+import { addObserverNamenodeSteps } from "./wizardSteps";
+import Spinner from "../../../../components/Spinner";
+import {
+ Accordion,
+ Alert,
+ Badge,
+ Card,
+ CardBody,
+ Col,
+ Form,
+ FormControl,
+ Row,
+} from "react-bootstrap";
+import WizardFooter from "../../../../components/StepWizard/WizardFooter";
+import { ActionTypes } from "./store/types";
+import useConfigsTags from "../../../../hooks/useConfigsTags";
+import ConfigsApi from "../../../../api/configsApi";
+import { reconfigureSites } from "../../../../Utils/taskUtils";
+import { messages } from "../../../messages";
+import { observerNnProperties } from "./observer_nn_properties";
+
+function Step3() {
+ const {
+ state,
+ dispatch,
+ stepWizardUtilities: { currentStep, handleNextImperitive },
+ flushStateToDb,
+ } = useContext(AddObserverNamenodeContext);
+ const { clusterName } = useContext(AppContext);
+ const serverConfigDataRef = useRef<any>([]);
+ const stepConfigs = useRef<any>(null);
+ const { configsData } = useConfigsTags();
+ const [overridenProperties, setOverridenProperties] = useState({});
+ const [isNextEnabled] = useState(true);
+ const [, forceRender] = useState(0);
+
+ function prepareDependencies() {
+ const ret: any = {};
+ const configsFromServer = serverConfigDataRef.current.items;
+
+ const nameNodes = getStepData(
+ state,
+ addObserverNamenodeSteps.SELECT_HOSTS,
+ "masterComponentHosts",
+ "addObserverNamenodeSteps"
+ ).filter((host: any) => host.component === "NAMENODE");
+
+ const hdfsSiteConfigs =
+ find(configsFromServer, ["type", "hdfs-site"])?.properties || {};
+
+ // Namespace id: prefer the nameservice chosen in Step1, else fall back to
+ // dfs.nameservices from configs (first entry for federated clusters).
+ const selectedNameServiceId = getStepData(
+ state,
+ addObserverNamenodeSteps.GET_STARTED,
+ "nameServiceId",
+ "addObserverNamenodeSteps"
+ );
+ ret.namespaceId =
+ selectedNameServiceId ||
+ (hdfsSiteConfigs["dfs.nameservices"] || "").split(",")[0];
+
+ // Existing namenodes list for this nameservice
+ const existingListRaw =
+ hdfsSiteConfigs[`dfs.ha.namenodes.${ret.namespaceId}`] || "";
+ const existingList = existingListRaw
+ .split(",")
+ .map((nn: string) => nn.trim())
+ .filter((nn: string) => nn);
+
+ // New namenode index continues from existing count (nn1,nn2 -> nn3)
+ ret.newNamenodeIndex = `nn${existingList.length + 1}`;
+ ret.listNameNodes = existingList.concat(ret.newNamenodeIndex).join(",");
+
+ // The host of the newly-selected (not yet installed) NameNode
+ ret.newNameNode = nameNodes.filter(
+ (node: any) => node.isInstalled === false
+ )[0]?.hostName;
+
+ if (ret.newNameNode === undefined) {
+ ret.newNameNode = "false";
+ }
+
+ // Ports (fall back to Ember defaults)
+ const dfsRpcA = hdfsSiteConfigs["dfs.namenode.rpc-address"];
+ ret.nnRpcPort = dfsRpcA ? dfsRpcA.split(":")[1] : "8020";
+
+ const dfsHttpA = hdfsSiteConfigs["dfs.namenode.http-address"];
+ ret.nnHttpPort = dfsHttpA ? dfsHttpA.split(":")[1] : "50070";
+
+ const dfsHttpsA = hdfsSiteConfigs["dfs.namenode.https-address"];
+ ret.nnHttpsPort = dfsHttpsA ? dfsHttpsA.split(":")[1] : "50470";
+
+ return ret;
+ }
+
+ function tweakServiceConfigs(configs: any) {
+ const dependencies = prepareDependencies();
+ const result: any[] = [];
+
+ configs.forEach((config: any) => {
+ const clone = { ...config };
+ clone.isOverridable = false;
+
+ const replaceTokens = (input: string) =>
+ input.replace(/\{\{(\w+)\}\}/g, (match: string, key: string) => {
+ return dependencies[key] !== undefined && dependencies[key] !== null
+ ? dependencies[key]
+ : match;
+ });
+
+ clone.name = replaceTokens(clone.name);
+ clone.displayName = replaceTokens(clone.displayName);
+ clone.value = replaceTokens(clone.value);
+ clone.recommendedValue = replaceTokens(clone.recommendedValue);
+ clone.changedValue = clone.value;
+
+ result.push(clone);
+ });
+
+ return result;
+ }
+
+ function renderServiceConfigs(_serviceConfig: any) {
+ const serviceConfig: any = {
+ serviceName: _serviceConfig.serviceName,
+ displayName: _serviceConfig.displayName,
+ configCategories: [],
+ showConfig: true,
+ configs: [],
+ };
+
+ _serviceConfig.configCategories.forEach((_configCategory: any) => {
+ serviceConfig.configCategories.push(_configCategory);
+ });
+
+ _serviceConfig.configs.forEach((_serviceConfigProperty: any) => {
+ serviceConfig.configs.push({
+ ..._serviceConfigProperty,
+ isEditable: _serviceConfigProperty.isReconfigurable,
+ });
+ });
+
+ stepConfigs.current = serviceConfig;
+ }
+
+ const loadStep = async () => {
+ await loadConfigsTags();
+ };
+
+ const loadConfigsTags = async () => {
+ try {
+ const configsTagApiResponsedata = await ConfigsApi.loadConfigTags(
+ clusterName
+ );
+ if (JSON.stringify(configsTagApiResponsedata) !== "{}") {
+ await onLoadConfigsTags(configsTagApiResponsedata);
+ }
+ } catch (error) {
+ console.error("Error loading config tags", error);
+ }
+ };
+
+ async function onLoadConfigsTags(data: any) {
+ const urlParams =
`(type=hdfs-site&tag=${data.Clusters.desired_configs["hdfs-site"].tag})`;
+ try {
+ const configsByTagApiResponseData = await ConfigsApi.getConfigsByTags(
+ clusterName,
+ urlParams
+ );
+ onLoadConfigs(configsByTagApiResponseData);
+ } catch (error) {
+ console.error("Error loading configurations", error);
+ }
+ }
+
+ const onLoadConfigs = (configsResponse: any) => {
+ serverConfigDataRef.current = configsResponse;
+
+ let observerConfigProperties =
observerNnProperties().observerNnConfig.configs;
+ const tweakedConfigs = tweakServiceConfigs(observerConfigProperties);
+ observerConfigProperties = tweakedConfigs;
+
+ // Build overridden properties (server configs + observer overrides) for
Step4 save
+ const overridenPropertiesCopy = cloneDeep(configsResponse);
+ for (const siteConfig of observerConfigProperties) {
+ const site = get(siteConfig, "filename", "");
+ const correspondingSite = find(
+ overridenPropertiesCopy.items,
+ (item: any) => item.type === site
+ );
+ if (correspondingSite) {
+ correspondingSite.properties = {
+ ...correspondingSite.properties,
+ [(siteConfig as any).name]:
+ (siteConfig as any).changedValue || (siteConfig as any).value,
+ };
+ }
+ }
+ setOverridenProperties(overridenPropertiesCopy);
+
+ const modifiedConfig = {
+ ...observerNnProperties().observerNnConfig,
+ configs: observerConfigProperties,
+ };
+ renderServiceConfigs(modifiedConfig);
+ forceRender((n) => n + 1);
+ };
+
+ useEffect(() => {
+ if (configsData && !isEmpty(configsData)) {
+ loadStep();
+ }
+ }, [configsData]);
+
+ function getMastersInfo() {
+ const step2Data = getStepData(
+ state,
+ addObserverNamenodeSteps.SELECT_HOSTS,
+ "masterComponentHosts",
+ "addObserverNamenodeSteps"
+ );
+
+ const currentNameNodes = step2Data.filter(
+ (host: any) => host.component === "NAMENODE" && host.isInstalled
+ );
+
+ const additionalNameNodes = step2Data.filter(
+ (host: any) => host.component === "NAMENODE" && !host.isInstalled
+ );
+
+ return { currentNameNodes, additionalNameNodes };
+ }
+
+ const { currentNameNodes, additionalNameNodes } = getMastersInfo();
+
+ if (!stepConfigs.current) {
+ return (
+ <div className="d-flex justify-content-center align-items-center p-5">
+ <Spinner />
+ </div>
+ );
+ }
+
+ return (
+ <>
+ <h2 className="step-title">Review</h2>
+ <h3 className="step-description light-text">
+ Confirm your host selections.
+ </h3>
+
+ <Card className="mt-3">
+ <CardBody>
+ {currentNameNodes.map((node: any, index: number) => (
+ <Row key={`current-${index}`} className="mb-2">
+ <Col md={3} className="bolder">
+ Current NameNode:
+ </Col>
+ <Col md={9}>{node.hostName}</Col>
+ </Row>
+ ))}
+
+ {additionalNameNodes.map((node: any, index: number) => (
+ <Row key={`additional-${index}`} className="mb-2">
+ <Col md={3} className="bolder">
+ Observer NameNode:
+ </Col>
+ <Col md={9}>
+ {node.hostName ? node.hostName : "false"}{" "}
+ <Badge bg="success">TO BE INSTALLED</Badge>
+ </Col>
+ </Row>
+ ))}
+ </CardBody>
+ </Card>
+
+ <Alert variant="info" className="mt-3">
+ <div className="bolder mb-2">Review Configuration Changes.</div>
+ <div>
+ The following lists the configuration changes that will be made by
the
+ Wizard to add the Observer NameNode. This information is for{" "}
+ <strong>review only</strong> and is not editable.
+ </div>
+ </Alert>
+
+ <Accordion defaultActiveKey="0" className="mt-3">
+ {stepConfigs.current?.configCategories?.map(
+ (category: any, categoryIndex: number) => {
+ const categoryConfigs = stepConfigs.current?.configs.filter(
+ (config: any) => config.category === category.name
+ );
+ if (!categoryConfigs?.length) return null;
+
+ return (
+ <Accordion.Item
+ eventKey={categoryIndex.toString()}
+ key={categoryIndex}
+ >
+ <Accordion.Header>{category.displayName}</Accordion.Header>
+ <Accordion.Body>
+ <Form>
+ {categoryConfigs.map((config: any, index: number) => (
+ <Form.Group as={Row} key={index} className="mb-3">
+ <Form.Label
+ column
+ sm={4}
+ className="text-break pe-3"
+ style={{
+ wordWrap: "break-word",
+ overflowWrap: "break-word",
+ }}
+ >
+ {config.displayName}
+ </Form.Label>
+ <Col sm={8}>
+ <FormControl
+ type="text"
+ value={config.changedValue || ""}
+ disabled={!config.isEditable}
+ readOnly
+ />
+ </Col>
+ </Form.Group>
+ ))}
+ </Form>
+ </Accordion.Body>
+ </Accordion.Item>
+ );
+ }
+ )}
+ </Accordion>
+
+ <WizardFooter
+ step={currentStep}
+ isNextEnabled={isNextEnabled}
+ onBack={() => {
+ flushStateToDb("back");
+ }}
+ onNext={async () => {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ overridenProperties,
+ },
+ },
+ });
+ // Apply the hdfs-site config changes here, mirroring the Ember
Observer
+ // NameNode wizard which saves configs in step3 (not as a step4
task).
+ try {
+ const note = get(
+ messages,
+ "admin.observerNameNode.wizard.step4.save.configuration.note"
+ );
+ const configs = [
+ {
+ Clusters: {
+ desired_config: reconfigureSites(
+ ["hdfs-site"],
+ overridenProperties,
+ note
+ ),
+ },
+ },
+ ];
+ await ConfigsApi.serviceMultiConfigurations(clusterName, configs);
+ } catch (error) {
+ console.error("Error saving observer namenode configs", error);
+ }
+ flushStateToDb("next");
+ handleNextImperitive();
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ />
+ </>
+ );
+}
+
+export default Step3;
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step4.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step4.tsx
new file mode 100644
index 0000000000..f2d3c5b56a
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/Step4.tsx
@@ -0,0 +1,399 @@
+/**
+ * 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 } from "react";
+import { Alert } from "react-bootstrap";
+import { AppContext } from "../../../../store/context";
+import { filter, find, map } from "lodash";
+import {
+ createInstallComponentTask,
+ updateComponent,
+} from "../../../../Utils/taskUtils";
+import { AddObserverNamenodeContext } from "./store/context";
+import { ServiceContext } from "../../../../store/ServiceContext";
+import { getStepData } from "../../../../Utils/Utility";
+import { addObserverNamenodeSteps } from "./wizardSteps";
+import observerNameNodeApi from "../../../../api/observerNameNodeApi";
+import WizardFooter from "../../../../components/StepWizard/WizardFooter";
+import OperationsProgress from "../../../../components/OperationsProgress";
+import useKDCSessionState from "../../../../hooks/useKDCSessionState";
+import { ActionTypes } from "./store/types";
+
+export function Step4() {
+ const {
+ state,
+ dispatch,
+ flushStateToDb,
+ stepWizardUtilities: { currentStep, jumpToStep },
+ } = useContext(AddObserverNamenodeContext);
+ const { clusterName } = useContext(AppContext);
+ const { serviceModels: allServiceModels }: any = useContext(ServiceContext);
+ const [completionStatus, setCompletionStatus] = useState(false);
+ const [stepOperations, setStepOperations] = useState<any>([]);
+ const { getKDCSessionState } = useKDCSessionState(() => {});
+
+ const masterComponentHosts = getStepData(
+ state,
+ addObserverNamenodeSteps.SELECT_HOSTS,
+ "masterComponentHosts",
+ "addObserverNamenodeSteps"
+ );
+
+ const newNameNodeHosts = () => {
+ return map(
+ filter(filter(masterComponentHosts, ["component", "NAMENODE"]), [
+ "isInstalled",
+ false,
+ ]),
+ "hostName"
+ );
+ };
+
+ const oldNameNodeHosts = () => {
+ return map(
+ filter(filter(masterComponentHosts, ["component", "NAMENODE"]), [
+ "isInstalled",
+ true,
+ ]),
+ "hostName"
+ );
+ };
+
+ const allDatanodeHosts = () => {
+ const dnComponent = find(allServiceModels["hdfs"]?.slaveComponents, [
+ "componentName",
+ "DATANODE",
+ ]);
+ if (dnComponent) {
+ return map(dnComponent?.hostComponents, "HostRoles.host_name");
+ }
+ return [];
+ };
+
+ function initializeTasks() {
+ let id = 0;
+ const allOps: any[] = [];
+
+ // NOTE: The hdfs-site config changes are applied in Step3 (Review) when
it loads,
+ // matching the Ember Observer NameNode wizard which has no "Reconfigure
Services"
+ // task in step4. The step4 task list here mirrors the Ember commands
exactly:
+ // installNameNode, installZKFC, enterSafeMode, saveNamespace,
leaveSafeMode,
+ // formatZKFC, bootstrapNameNode, startZKFC, startNameNode, refreshConfigs,
+ // refreshNamenodes, transitionToObserver.
+
+ // Install Additional NameNode
+ allOps.push({
+ id: ++id,
+ label: "Install Additional Namenode",
+ skippable: false,
+ callback: async () => {
+ return await createInstallComponentTask(
+ "NAMENODE",
+ newNameNodeHosts(),
+ "HDFS",
+ clusterName,
+ ["HDFS"],
+ allServiceModels["hdfs"],
+ getKDCSessionState
+ );
+ },
+ });
+
+ // Install ZKFC
+ allOps.push({
+ id: ++id,
+ label: "Install ZKFC",
+ skippable: false,
+ callback: async () => {
+ return await createInstallComponentTask(
+ "ZKFC",
+ newNameNodeHosts(),
+ "HDFS",
+ clusterName,
+ ["HDFS"],
+ allServiceModels["hdfs"],
+ getKDCSessionState
+ );
+ },
+ });
+
+ // Enter Safe Mode (on the standby / second existing NN)
+ allOps.push({
+ id: ++id,
+ label: "Enter Safe Mode",
+ skippable: false,
+ callback: async () => {
+ const host = oldNameNodeHosts()[1] || oldNameNodeHosts()[0];
+ const data = {
+ RequestInfo: { command: "ENTER_SAFEMODE", context: "Enter Safemode"
},
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "NAMENODE", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.enterSafeMode(clusterName, data);
+ },
+ });
+
+ // Save Namespace
+ allOps.push({
+ id: ++id,
+ label: "Save Namespace",
+ skippable: false,
+ callback: async () => {
+ const host = oldNameNodeHosts()[1] || oldNameNodeHosts()[0];
+ const data = {
+ RequestInfo: { command: "SAVE_NAMESPACE", context: "Save Namespace"
},
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "NAMENODE", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.saveNamespace(clusterName, data);
+ },
+ });
+
+ // Leave Safe Mode
+ allOps.push({
+ id: ++id,
+ label: "Leave Safe Mode",
+ skippable: false,
+ callback: async () => {
+ const host = oldNameNodeHosts()[1] || oldNameNodeHosts()[0];
+ const data = {
+ RequestInfo: { command: "LEAVE_SAFEMODE", context: "Leave Safemode"
},
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "NAMENODE", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.leaveSafeMode(clusterName, data);
+ },
+ });
+
+ // Format ZKFC (on the new host)
+ allOps.push({
+ id: ++id,
+ label: "Format ZKFC",
+ skippable: false,
+ callback: async () => {
+ const host = newNameNodeHosts()[0];
+ const data = {
+ RequestInfo: { command: "FORMAT", context: "Format ZKFC" },
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "ZKFC", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.enterSafeMode(clusterName, data);
+ },
+ });
+
+ // Bootstrap Additional NameNode
+ allOps.push({
+ id: ++id,
+ label: "Bootstrap Additional Namenode",
+ skippable: false,
+ callback: async () => {
+ const host = newNameNodeHosts()[0];
+ const data = {
+ RequestInfo: {
+ command: "BOOTSTRAP_STANDBY",
+ context: "Bootstrap NameNode",
+ },
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "NAMENODE", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.saveNamespace(clusterName, data);
+ },
+ });
+
+ // Start ZKFC (new host)
+ allOps.push({
+ id: ++id,
+ label: "Start ZKFC",
+ skippable: false,
+ callback: async () => {
+ const host = newNameNodeHosts()[0];
+ return await updateComponent(
+ clusterName,
+ "ZKFC",
+ host,
+ "HDFS",
+ "Start",
+ id
+ );
+ },
+ });
+
+ // Start New NameNode
+ allOps.push({
+ id: ++id,
+ label: "Start New Namenode",
+ skippable: false,
+ callback: async () => {
+ const host = newNameNodeHosts()[0];
+ return await updateComponent(
+ clusterName,
+ "NAMENODE",
+ host,
+ "HDFS",
+ "Start",
+ id
+ );
+ },
+ });
+
+ // Refresh configs on DataNodes
+ allOps.push({
+ id: ++id,
+ label: "Refresh configs",
+ skippable: false,
+ callback: async () => {
+ const data = {
+ RequestInfo: { command: "CONFIGURE", context: "refresh configs" },
+ "Requests/resource_filters": [
+ {
+ service_name: "HDFS",
+ component_name: "DATANODE",
+ hosts: allDatanodeHosts().join(","),
+ },
+ ],
+ };
+ return await observerNameNodeApi.refreshNamenodes(clusterName, data);
+ },
+ });
+
+ // Refresh Namenodes on DataNodes
+ allOps.push({
+ id: ++id,
+ label: "Refresh Namenodes",
+ skippable: false,
+ callback: async () => {
+ const data = {
+ RequestInfo: {
+ command: "REFRESH_NAMENODE",
+ context: "Refresh Namenode",
+ },
+ "Requests/resource_filters": [
+ {
+ service_name: "HDFS",
+ component_name: "DATANODE",
+ hosts: allDatanodeHosts().join(","),
+ },
+ ],
+ };
+ return await observerNameNodeApi.refreshNamenodes(clusterName, data);
+ },
+ });
+
+ // Transition to Observer (the key final step)
+ allOps.push({
+ id: ++id,
+ label: "Transition to Observer",
+ skippable: false,
+ callback: async () => {
+ const host = newNameNodeHosts()[0];
+ const data = {
+ RequestInfo: {
+ command: "TRANSITION_NAMENODE",
+ context: "Transition Namenode",
+ },
+ "Requests/resource_filters": [
+ { service_name: "HDFS", component_name: "NAMENODE", hosts: host },
+ ],
+ };
+ return await observerNameNodeApi.transitionToObserver(
+ clusterName,
+ data
+ );
+ },
+ });
+
+ return allOps;
+ }
+
+ const savedOperationsState = getStepData(
+ state,
+ addObserverNamenodeSteps.CONFIGURE_COMPONENTS,
+ "operationsState",
+ "addObserverNamenodeSteps"
+ );
+
+ useEffect(() => {
+ const operations = initializeTasks();
+ const finalOperations = (() => {
+ if (savedOperationsState && Array.isArray(savedOperationsState)) {
+ return operations.map((originalOp) => {
+ const savedOp = savedOperationsState.find(
+ (saved: any) => saved.id === originalOp.id
+ );
+ return savedOp
+ ? { ...originalOp, ...savedOp, callback: originalOp.callback }
+ : originalOp;
+ });
+ }
+ return operations;
+ })();
+ setStepOperations(finalOperations);
+ }, [JSON.stringify(savedOperationsState)]);
+
+ if (!stepOperations || stepOperations.length === 0) {
+ return <div>Loading...</div>;
+ }
+
+ return (
+ <>
+ {completionStatus && (
+ <Alert variant="success" className="mb-3">
+ Observer Namenode has been enabled successfully.
+ </Alert>
+ )}
+ <OperationsProgress
+ title=""
+ description=""
+ setCompletionStatus={setCompletionStatus}
+ operations={stepOperations as any}
+ dispatch={(operationsState: any) => {
+ dispatch({
+ type: ActionTypes.STORE_INFORMATION,
+ payload: {
+ step: currentStep.name,
+ data: {
+ operationsState,
+ },
+ },
+ });
+ }}
+ />
+ <WizardFooter
+ step={currentStep}
+ isNextEnabled={completionStatus}
+ onNext={() => {
+ flushStateToDb("cancel"); // Clear the wizard state on completion
+ window.location.href = "/#/main/services/HDFS/summary";
+ window.location.reload();
+ }}
+ onBack={() => {
+ flushStateToDb("back");
+ jumpToStep(2);
+ }}
+ onCancel={() => {
+ flushStateToDb("cancel");
+ }}
+ />
+ </>
+ );
+}
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/index.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/index.tsx
new file mode 100644
index 0000000000..aef6ca685f
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/index.tsx
@@ -0,0 +1,79 @@
+/**
+ * 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 { Dropdown } from "react-bootstrap";
+import { ServiceActionEnums } from "../../../../enums/ServiceActionEnums";
+import { useContext, useEffect, useState } from "react";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
+import ValidateEnablement from "./validateEnablement";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faSitemap } from "@fortawesome/free-solid-svg-icons";
+import { ServiceContext } from "../../../../store/ServiceContext";
+
+function AddObserverNamenode({ isMappingOnly }: { isMappingOnly?: boolean }) {
+ const [shouldStartEnableFlow, setShouldStartEnableFlow] = useState(false);
+ const { componentName } = useParams();
+ const location = useLocation();
+ const navigate = useNavigate();
+ const { allServiceModels } = useContext(ServiceContext);
+
+ useEffect(() => {
+ if (
+ location.pathname.includes("observerNamenode") &&
+ componentName === "NameNode"
+ ) {
+ setShouldStartEnableFlow(true);
+ }
+ }, []);
+
+ // Observer NameNode requires NameNode HA to be enabled (matches Ember.js
App.get('isHaEnabled') logic)
+ const isHAEnabled = () => {
+ const hdfsModel = allServiceModels["hdfs"];
+ const hasSNameNode = hdfsModel?.["masterComponents"]?.some(
+ (component: any) => {
+ return (
+ component.component_name === "SECONDARY_NAMENODE" ||
+ component.componentName === "SECONDARY_NAMENODE"
+ );
+ }
+ );
+ return !hasSNameNode; // HA is enabled when there's no Secondary NameNode
+ };
+
+ return (
+ <>
+ {shouldStartEnableFlow ? <ValidateEnablement /> : null}
+ {!isMappingOnly ? (
+ <Dropdown.Item
+ onClick={() => {
+ if (!isHAEnabled()) {
+ return;
+ }
+ navigate(`/main/services/NameNode/observerNamenode/step1`);
+ }}
+ disabled={!isHAEnabled()}
+ >
+ <FontAwesomeIcon className="text-secondary me-2" icon={faSitemap} />
+ {ServiceActionEnums.addObserverNamenode}
+ </Dropdown.Item>
+ ) : null}
+ </>
+ );
+}
+
+export default AddObserverNamenode;
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/observer_nn_properties.ts
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/observer_nn_properties.ts
new file mode 100644
index 0000000000..9662feb42b
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/observer_nn_properties.ts
@@ -0,0 +1,205 @@
+/**
+ * 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.
+ */
+
+interface ConfigProperty {
+ name: string;
+ displayName: string;
+ description?: string;
+ isReconfigurable: boolean;
+ recommendedValue: string;
+ value: string;
+ displayType?: string;
+ category: string;
+ filename: string;
+ serviceName: string;
+ isRequired?: boolean;
+}
+
+class ServiceConfigCategory {
+ name: string;
+ displayName: string;
+
+ constructor(options: { name: string; displayName: string }) {
+ this.name = options.name;
+ this.displayName = options.displayName;
+ }
+
+ static create(options: {
+ name: string;
+ displayName: string;
+ }): ServiceConfigCategory {
+ return new ServiceConfigCategory(options);
+ }
+}
+
+export const observerNnProperties = () => {
+ const observerNnConfig: {
+ serviceName: string;
+ displayName: string;
+ configCategories: ServiceConfigCategory[];
+ sites: string[];
+ configs: ConfigProperty[];
+ } = {
+ serviceName: "MISC",
+ displayName: "MISC",
+ configCategories: [
+ ServiceConfigCategory.create({ name: "HDFS", displayName: "HDFS" }),
+ ],
+ sites: ["hdfs-site", "hdfs-client"],
+ configs: [
+ /********************************************** HDFS
***************************************/
+ {
+ name: "dfs.ha.namenodes.{{namespaceId}}",
+ displayName: "dfs.ha.namenodes.{{namespaceId}}",
+ description:
+ "The prefix for a given nameservice, contains a comma-separated list
of namenodes for a given nameservice.",
+ isReconfigurable: false,
+ recommendedValue: "nn1,nn2,nn3",
+ value: "{{listNameNodes}}",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.namenode.rpc-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ displayName:
+ "dfs.namenode.rpc-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ description: "RPC address that handles all clients requests for the
new NameNode.",
+ isReconfigurable: false,
+ recommendedValue: "0.0.0.0:8020",
+ value: "{{newNameNode}}:{{nnRpcPort}}",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.namenode.http-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ displayName:
+ "dfs.namenode.http-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ description: "The fully-qualified HTTP address for the new NameNode.",
+ isReconfigurable: false,
+ recommendedValue: "0.0.0.0:50070",
+ value: "{{newNameNode}}:{{nnHttpPort}}",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name:
"dfs.namenode.https-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ displayName:
+ "dfs.namenode.https-address.{{namespaceId}}.{{newNamenodeIndex}}",
+ description: "The fully-qualified HTTPS address for the new NameNode.",
+ isReconfigurable: false,
+ recommendedValue: "0.0.0.0:50470",
+ value: "{{newNameNode}}:{{nnHttpsPort}}",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.client.failover.proxy.provider.{{namespaceId}}",
+ displayName: "dfs.client.failover.proxy.provider.{{namespaceId}}",
+ description:
+ "The prefix for a given nameservice, contains a list of the RPC
addresses for the namenodes.",
+ isReconfigurable: false,
+ recommendedValue:
+
"org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider",
+ value:
+
"org.apache.hadoop.hdfs.server.namenode.ha.ObserverReadProxyProvider",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.client.failover.observer.auto-msync-period.{{namespaceId}}",
+ displayName:
+ "dfs.client.failover.observer.auto-msync-period.{{namespaceId}}",
+ description:
+ "The auto msync period for observer reads, controlling how often the
client syncs with the active NameNode.",
+ isReconfigurable: false,
+ recommendedValue: "500ms",
+ value: "500ms",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.namenode.state.context.enabled",
+ displayName: "dfs.namenode.state.context.enabled",
+ description:
+ "Enables the NameNode to include state context in RPC responses,
required for observer reads.",
+ isReconfigurable: false,
+ recommendedValue: "true",
+ value: "true",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.ha.tail-edits.in-progress",
+ displayName: "dfs.ha.tail-edits.in-progress",
+ description:
+ "Enables tailing of in-progress edit log segments, required for
low-latency observer reads.",
+ isReconfigurable: false,
+ recommendedValue: "true",
+ value: "true",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.ha.tail-edits.period",
+ displayName: "dfs.ha.tail-edits.period",
+ description:
+ "How often the standby/observer NameNode should tail edits from the
JournalNodes.",
+ isReconfigurable: false,
+ recommendedValue: "0ms",
+ value: "0ms",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.ha.tail-edits.period.backoff-max",
+ displayName: "dfs.ha.tail-edits.period.backoff-max",
+ description:
+ "The maximum backoff period between edit tailing attempts when no
new edits are available.",
+ isReconfigurable: false,
+ recommendedValue: "10s",
+ value: "10s",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ {
+ name: "dfs.journalnode.edit-cache-size.bytes",
+ displayName: "dfs.journalnode.edit-cache-size.bytes",
+ description:
+ "The size of the in-memory cache of edits on the JournalNode, used
to serve edits to observer/standby NameNodes.",
+ isReconfigurable: false,
+ recommendedValue: "1048576",
+ value: "1048576",
+ category: "HDFS",
+ filename: "hdfs-site",
+ serviceName: "MISC",
+ },
+ ],
+ };
+
+ return { observerNnConfig };
+};
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/context.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/context.tsx
new file mode 100644
index 0000000000..d9d93daed3
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/context.tsx
@@ -0,0 +1,186 @@
+/**
+ * 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, {
+ createContext,
+ Dispatch,
+ useEffect,
+ useReducer,
+ useRef,
+ useState,
+} from "react";
+import { State, Action, ActionTypes } from "./types";
+import { reducer, initialState } from "./reducer";
+import ClusterApi from "../../../../../api/clusterApi";
+import { get, isEmpty } from "lodash";
+import { ClusterProgressStatus } from "../../../../../constants";
+import modalManager from "../../../../../store/ModalManager";
+
+interface AddObserverNamenodeContextProps {
+ state: State;
+ dispatch: Dispatch<Action>;
+ stepWizardUtilities?: any;
+ flushStateToDb?: any;
+}
+
+export const AddObserverNamenodeContext =
+ createContext<AddObserverNamenodeContextProps>({
+ state: initialState,
+ dispatch: () => undefined,
+ flushStateToDb: () => undefined,
+ });
+
+export const AddObserverNamenodeProvider: React.FC<{
+ stepWizardUtilities: any;
+ children: React.ReactNode;
+}> = ({ stepWizardUtilities, children }) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const [currStepData, setCurrStepData] = useState({});
+
+ const isDataPersisted = useRef(false);
+
+ useEffect(() => {
+ syncUserPersistedData();
+ }, []);
+
+ useEffect(() => {
+ if (isDataPersisted.current) {
+ flushCurrentData();
+ }
+ }, [state.addObserverNamenodeSteps, currStepData]);
+
+ async function syncUserPersistedData() {
+ try {
+ const persistedData = await ClusterApi.getPersistData(
+ "OBSERVER_NAMENODE"
+ );
+ if (!isEmpty(get(persistedData, "addObserverNamenodeSteps", {}))) {
+ dispatch({
+ type: ActionTypes.SYNC_STATE,
+ payload: persistedData,
+ });
+ }
+ if (get(persistedData, "activeStep", "")) {
+ try {
+ const activeStepName = get(persistedData, "activeStep");
+ setCurrStepData({
+ progressStatus: ClusterProgressStatus.ADDING_OBSERVER_NAMENODE,
+ stepName: activeStepName,
+ });
+ const activeStepNumber = Object.keys(
+ stepWizardUtilities.wizardSteps
+ ).find((stepName) => {
+ return (
+ stepWizardUtilities.wizardSteps?.[stepName]?.name ===
+ activeStepName
+ );
+ });
+ stepWizardUtilities.jumpToStep(Number(activeStepNumber), true);
+ } catch (err) {
+ console.error("Error while jumping to step", err);
+ }
+ } else {
+ stepWizardUtilities.jumpToStep(0, true);
+ }
+ } finally {
+ isDataPersisted.current = true;
+ }
+ }
+
+ async function flushCurrentData() {
+ await ClusterApi.postPersistData(
+ JSON.stringify({
+ OBSERVER_NAMENODE: JSON.stringify({
+ ...state,
+ activeStep: get(currStepData, "stepName", ""),
+ }),
+ CLUSTER_STATE: JSON.stringify(currStepData),
+ })
+ );
+ }
+
+ async function flushOnCancel() {
+ await ClusterApi.postPersistData(
+ JSON.stringify({
+ OBSERVER_NAMENODE: JSON.stringify(initialState),
+ CLUSTER_STATE: JSON.stringify({}),
+ })
+ );
+ modalManager.hide();
+ window.location.href = "/#/main/services/HDFS/summary";
+ window.location.reload();
+ }
+
+ async function flushOnStepChange(nextStep: number) {
+ if (nextStep >= 0) {
+ const nextStepDetails = stepWizardUtilities.wizardSteps?.[nextStep];
+ if (nextStepDetails?.keysToRemove) {
+ nextStepDetails.keysToRemove.forEach((key: string) => {
+ if (state?.addObserverNamenodeSteps?.[key]) {
+ dispatch({
+ type: ActionTypes.REMOVE_KEY,
+ payload: { key },
+ });
+ }
+ });
+ }
+ setCurrStepData({
+ progressStatus: ClusterProgressStatus.ADDING_OBSERVER_NAMENODE,
+ stepName: stepWizardUtilities?.wizardSteps?.[nextStep]?.name,
+ });
+ }
+ }
+
+ function flushStateToDb(
+ operation: string = "default",
+ jumpStep: number = -1
+ ) {
+ const activeStep = Object.keys(stepWizardUtilities.wizardSteps).find(
+ (stepName) => {
+ return (
+ stepWizardUtilities.wizardSteps?.[stepName]?.name ===
+ stepWizardUtilities.currentStep.name
+ );
+ }
+ );
+ switch (operation) {
+ case "cancel":
+ flushOnCancel();
+ break;
+ case "back":
+ flushOnStepChange(Number(activeStep) - 1);
+ break;
+ case "next":
+ flushOnStepChange(Number(activeStep) + 1);
+ break;
+ case "jump":
+ flushOnStepChange(jumpStep);
+ break;
+ default:
+ flushCurrentData();
+ }
+ }
+
+ return (
+ <AddObserverNamenodeContext.Provider
+ value={{ state, dispatch, stepWizardUtilities, flushStateToDb }}
+ >
+ {children}
+ </AddObserverNamenodeContext.Provider>
+ );
+};
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/reducer.ts
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/reducer.ts
new file mode 100644
index 0000000000..af881a19a0
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/reducer.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.
+ */
+
+import { State, Action, ActionTypes } from "./types";
+
+export const initialState: State = { addObserverNamenodeSteps: {} };
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case ActionTypes.STORE_INFORMATION: {
+ const stateCopy = { ...state };
+ const addObserverNamenodeSteps = { ...stateCopy.addObserverNamenodeSteps
};
+ addObserverNamenodeSteps[action.payload.step] = action.payload;
+ return { ...state, addObserverNamenodeSteps };
+ }
+ case ActionTypes.SYNC_STATE:
+ return { ...action.payload };
+ case ActionTypes.REMOVE_KEY: {
+ const newState = { ...state };
+ const newSteps = { ...newState.addObserverNamenodeSteps };
+ delete newSteps[action.payload.key];
+ return { ...newState, addObserverNamenodeSteps: newSteps };
+ }
+ default:
+ return state;
+ }
+};
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/types.ts
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/types.ts
new file mode 100644
index 0000000000..735719a996
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/store/types.ts
@@ -0,0 +1,32 @@
+/**
+ * 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 State {
+ addObserverNamenodeSteps: any;
+}
+
+export enum ActionTypes {
+ STORE_INFORMATION = "STORE INFORMATION",
+ SYNC_STATE = "SYNC STATE",
+ REMOVE_KEY = "REMOVE_KEY",
+}
+
+export type Action =
+ | { type: ActionTypes.STORE_INFORMATION; payload: any }
+ | { type: ActionTypes.SYNC_STATE; payload: any }
+ | { type: ActionTypes.REMOVE_KEY; payload: { key: string } };
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/validateEnablement.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/validateEnablement.tsx
new file mode 100644
index 0000000000..a9a5e76602
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/validateEnablement.tsx
@@ -0,0 +1,161 @@
+/**
+ * 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 } from "react";
+import useHostComponents from "../../../ClusterWizard/hooks/useHostComponents";
+import Modal from "../../../../components/Modal";
+import { find, flatten, get, map } from "lodash";
+import Spinner from "../../../../components/Spinner";
+import useStepWizard from "../../../../hooks/useStepWizard";
+import wizardSteps from "./wizardSteps";
+import { AddObserverNamenodeProvider } from "./store/context";
+import StepWizard from "../../../../components/StepWizard";
+import ClusterApi from "../../../../api/clusterApi";
+import { LocalStorageOps } from "../../../../Utils/LocalStorageOps";
+import { AppContext } from "../../../../store/context";
+import { messages } from "../../../messages";
+import { ServiceContext } from "../../../../store/ServiceContext";
+
+function ValidateEnablement() {
+ const { services } = useContext(AppContext);
+ const { masterSlaveClientsData } = useContext(ServiceContext);
+ const { hostComponents: serviceHostComponents, serviceComponents } =
+ useHostComponents(map(services, "ServiceInfo.service_name"));
+ const stepWizardUtilities = useStepWizard(wizardSteps, 0);
+ const [canStartEnablement, setCanStartEnablement] = useState(false);
+ const [validationErrors, setValidationErrors] = useState<string[]>([]);
+ const [checkingForEnablement, setCheckingForEnablement] = useState(true);
+ const [showModal, setShowModal] = useState(true);
+
+ const getModalBodyContent = () => {
+ if (checkingForEnablement) {
+ return (
+ <div className="d-flex justify-content-center align-items-center
flex-column p-4">
+ <Spinner />
+ <small className="text-muted mt-2">
+ Validating the components for Observer Namenode enablement...
+ </small>
+ </div>
+ );
+ }
+ if (validationErrors.length) {
+ return (
+ <>
+ <strong className="text-danger">Errors:</strong>
+ {validationErrors.map((error) => (
+ <div key={error} className="mt-2">
+ {error}
+ </div>
+ ))}
+ </>
+ );
+ }
+ return (
+ <AddObserverNamenodeProvider stepWizardUtilities={stepWizardUtilities}>
+ <StepWizard wizardUtilities={stepWizardUtilities} />
+ </AddObserverNamenodeProvider>
+ );
+ };
+
+ const validateCanEnable = () => {
+ const errorMessages = [];
+ const hostComponentsRoles = flatten(
+ map(serviceHostComponents, "host_components")
+ );
+ const hostComponents = flatten(map(hostComponentsRoles, "HostRoles"));
+
+ if (
+ get(
+ find(hostComponents, ["component_name", "ZOOKEEPER_SERVER"]),
+ "state"
+ ) !== "STARTED"
+ ) {
+ errorMessages.push(
+ get(messages, "admin.observerNameNode.wizard.required.zookeepers")
+ );
+ }
+
+ const journalNodes: any[] = Object.values(masterSlaveClientsData).filter(
+ (item: any) => {
+ return item.ServiceComponentInfo.component_name === "JOURNALNODE";
+ }
+ );
+
+ if (
+ journalNodes.length > 0 &&
+ journalNodes[0]?.ServiceComponentInfo.total_count !==
+ journalNodes[0].ServiceComponentInfo.started_count
+ ) {
+ errorMessages.push(
+ get(messages, "admin.observerNameNode.wizard.required.journalnodes")
+ );
+ }
+
+ if (!errorMessages.length) {
+ setCanStartEnablement(true);
+ setValidationErrors([]);
+ setCheckingForEnablement(false);
+ } else {
+ setValidationErrors(errorMessages);
+ setCheckingForEnablement(false);
+ }
+ };
+
+ useEffect(() => {
+ if (serviceHostComponents.length && serviceComponents.length) {
+ validateCanEnable();
+ }
+ }, [serviceHostComponents.length, serviceComponents.length]);
+
+ return (
+ <>
+ {showModal ? (
+ <Modal
+ isOpen={showModal}
+ onClose={async () => {
+ await ClusterApi.postPersistData(
+ JSON.stringify({
+ USER_REDIRECTION_URL: "",
+ })
+ );
+ setShowModal(false);
+ LocalStorageOps.setItem(
+ "lastVisitedURL",
+ "/#/main/services/HDFS/summary"
+ );
+ window.location.href = "/#/main/services/HDFS/summary";
+ }}
+ modalTitle={get(messages, "admin.observerNameNode.button.enable")}
+ modalBody={getModalBodyContent()}
+ successCallback={() => {
+ setShowModal(false);
+ window.location.href = "/#/main/services/HDFS/summary";
+ }}
+ options={{
+ shouldShowFooter:
+ checkingForEnablement || canStartEnablement ? false : true,
+ modalSize: "modal-wizard",
+ cancelableViaIcon: true,
+ }}
+ />
+ ) : null}
+ </>
+ );
+}
+
+export default ValidateEnablement;
diff --git
a/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/wizardSteps.tsx
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/wizardSteps.tsx
new file mode 100644
index 0000000000..70153ec96b
--- /dev/null
+++
b/ambari-web/latest/src/screens/Services/highAvailibility/observerNameNode/wizardSteps.tsx
@@ -0,0 +1,64 @@
+/**
+ * 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 Step1 from "./Step1";
+import Step2 from "./Step2";
+import Step3 from "./Step3";
+import { Step4 } from "./Step4";
+
+export enum addObserverNamenodeSteps {
+ GET_STARTED = "GET_STARTED",
+ SELECT_HOSTS = "SELECT_HOSTS",
+ REVIEW = "REVIEW",
+ CONFIGURE_COMPONENTS = "CONFIGURE_COMPONENTS",
+}
+
+export default {
+ 0: {
+ label: "Get Started",
+ completed: false,
+ Component: <Step1 />,
+ canGoBack: false,
+ isNextEnabled: false,
+ name: addObserverNamenodeSteps.GET_STARTED,
+ },
+ 1: {
+ label: "Select Hosts",
+ completed: false,
+ Component: <Step2 />,
+ canGoBack: true,
+ isNextEnabled: false,
+ name: addObserverNamenodeSteps.SELECT_HOSTS,
+ },
+ 2: {
+ label: "Review",
+ completed: false,
+ Component: <Step3 />,
+ canGoBack: true,
+ isNextEnabled: false,
+ name: addObserverNamenodeSteps.REVIEW,
+ },
+ 3: {
+ label: "Configure Components",
+ completed: false,
+ Component: <Step4 />,
+ canGoBack: true,
+ isNextEnabled: true,
+ name: addObserverNamenodeSteps.CONFIGURE_COMPONENTS,
+ },
+};
diff --git a/ambari-web/latest/src/screens/messages.ts
b/ambari-web/latest/src/screens/messages.ts
index 38aa9623cf..37d4a17349 100644
--- a/ambari-web/latest/src/screens/messages.ts
+++ b/ambari-web/latest/src/screens/messages.ts
@@ -1715,6 +1715,41 @@ const messages: any = {
'admin.nameNodeFederation.wizard.step4.task15.title': 'Start ZKFC',
'admin.nameNodeFederation.wizard.step4.task16.title': 'Start NameNode',
'admin.nameNodeFederation.wizard.step4.task19.title': 'Restart Required
Services',
+
+ 'admin.observerNameNode.button.enable': 'Add Observer Namenode',
+ 'admin.observerNameNode.wizard.header': 'Add Observer Namenode',
+ 'admin.observerNameNode.closePopup': 'Are you sure you want to quit?',
+ 'admin.observerNameNode.closePopup2': 'Add Observer Namenode Wizard is in
progress. You must allow the wizard to complete for Ambari to be in usable
state. If you choose to quit, you must follow manual instructions to complete
or revert Add Observer Namenode as documented in the Ambari User Guide. Are you
sure you want to exit the wizard?',
+ 'admin.observerNameNode.wizard.required.zookeepers': 'All ZooKeeper
Servers should be up',
+ 'admin.observerNameNode.wizard.required.journalnodes': 'All JournalNodes
should be up',
+ 'admin.observerNameNode.wizard.step1.header': 'Get Started',
+ 'admin.observerNameNode.wizard.step1.body': 'This wizard will walk you
through adding observer namenode on your cluster. Once enabled, you will be
running a Observer Namenode in addition to your Active and Standby Namenode.
This allows for an Active-Standby Namenode configuration that automatically
performs failover.',
+ 'admin.observerNameNode.wizard.step1.alert': 'You should have atleast one
Standby and one Active Namenode in your cluster',
+ 'admin.observerNameNode.wizard.step1.nameserviceid.existing': 'Existing
Nameservice ID',
+ 'admin.observerNameNode.wizard.step1.nameserviceid': 'Please choose a
Nameservice ID',
+ 'admin.observerNameNode.wizard.step1.nameserviceid.error': 'Must be one of
the existing Nameservice',
+ 'admin.observerNameNode.wizard.step2.header': 'Select Hosts',
+ 'admin.observerNameNode.wizard.step2.body': 'Select hosts running the
NameNodes for the observer.',
+ 'admin.observerNameNode.wizard.step3.confirm.host.body': 'Confirm your
host selections.',
+ 'admin.observerNameNode.wizard.step3.currentNN': 'Current Namenodes',
+ 'admin.observerNameNode.wizard.step3.additionalNN': 'Additional Namenodes',
+ 'admin.observerNameNode.wizard.step3.header': 'Review',
+ 'admin.observerNameNode.wizard.step4.header': 'Configure Components',
+ 'admin.observerNameNode.wizard.step4.save.configuration.note': 'This
configuration is created by Add Observer Namenode wizard',
+ 'admin.observerNameNode.wizard.step4.notice.inProgress': 'Please wait
while your Observer Namenode is being deployed.',
+ 'admin.observerNameNode.wizard.step4.notice.completed': 'Observer Namenode
has been enabled successfully.',
+ 'admin.observerNameNode.wizard.step4.task0.title': 'Install Additional
Namenode',
+ 'admin.observerNameNode.wizard.step4.task1.title': 'Install ZKFC',
+ 'admin.observerNameNode.wizard.step4.task2.title': 'Enter Safe Mode',
+ 'admin.observerNameNode.wizard.step4.task3.title': 'Save Namespace',
+ 'admin.observerNameNode.wizard.step4.task4.title': 'Leave Safe Mode',
+ 'admin.observerNameNode.wizard.step4.task5.title': 'Format ZKFC',
+ 'admin.observerNameNode.wizard.step4.task6.title': 'Bootstrap Additional
Namenode',
+ 'admin.observerNameNode.wizard.step4.task7.title': 'Start ZKFC',
+ 'admin.observerNameNode.wizard.step4.task8.title': 'Start New Namenode',
+ 'admin.observerNameNode.wizard.step4.task9.title': 'Refresh configs',
+ 'admin.observerNameNode.wizard.step4.task10.title': 'Refresh Namenodes',
+ 'admin.observerNameNode.wizard.step4.task11.title': 'Transition to
Observer',
'admin.multipleNameNode.wizard.header': 'Add Multiple Namenode',
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]