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 eff2e9ebd0 AMBARI-26644: Bug fixes several issues across the cluster 
install wizard, host/service (#4201)
eff2e9ebd0 is described below

commit eff2e9ebd0b3d0f2b5afe34bde8107f3e796cffb
Author: Sandeep  Kumar <[email protected]>
AuthorDate: Fri Sep 4 13:53:08 2026 +0530

    AMBARI-26644: Bug fixes several issues across the cluster install wizard, 
host/service (#4201)
    
    configuration, alerts, and host management screens.
---
 .../src/Initializers/WizardConfigInitializer.ts    |  11 +-
 ambari-web/latest/src/Utils/configs.ts             |  13 +-
 ambari-web/latest/src/Utils/validators.ts          |  10 +
 ambari-web/latest/src/api/hostsApi.ts              |  33 ++-
 .../components/DependentConfigurationsModal.tsx    | 283 +++++++++++----------
 .../latest/src/components/Sidebar/Sidebar.tsx      |  47 +++-
 .../latest/src/components/Sidebar/SidebarItem.tsx  |  21 +-
 ambari-web/latest/src/hooks/useConfigSaver.tsx     |   7 +-
 ambari-web/latest/src/hooks/useDecommissionable.ts | 138 ++++++----
 ambari-web/latest/src/hooks/useEnhancedConfigs.ts  |   6 +
 .../latest/src/hooks/useHostConfigUpdater.ts       |  15 +-
 ambari-web/latest/src/layout/Dashboard.tsx         |  54 +---
 ambari-web/latest/src/locales/en/translation.json  |   6 +
 .../ClusterAdmin/StackAndVersions/ListVersion.tsx  |  27 +-
 .../screens/ClusterWizard/Step7/AccountsTab.tsx    |   5 +-
 .../screens/ClusterWizard/Step7/CredentialsTab.tsx |   6 +-
 .../src/screens/ClusterWizard/Step7/index.tsx      |  90 +++----
 .../latest/src/screens/ClusterWizard/Step8.tsx     |  10 +-
 .../src/screens/CommonConfigs/AdvancedConfigs.tsx  |  37 ++-
 .../src/screens/CommonConfigs/types/index.ts       |  24 ++
 .../screens/ConfigGroups/AddToConfigGroupModal.tsx |  16 +-
 .../latest/src/screens/Hosts/HostSummary.tsx       |  47 +++-
 ambari-web/latest/src/screens/Hosts/actions.tsx    | 105 +++++---
 ambari-web/latest/src/screens/Hosts/utils.tsx      |  59 +++--
 .../latest/src/screens/ServiceConfigs/index.tsx    |  32 ++-
 .../Services/highAvailibility/nameNode/Step3.tsx   |   5 +-
 ambari-web/latest/src/screens/messages.ts          |   6 +-
 ambari-web/latest/src/store/ServiceContext.tsx     |  78 +++++-
 ambari-web/latest/src/store/context.tsx            |  31 +++
 29 files changed, 830 insertions(+), 392 deletions(-)

diff --git a/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts 
b/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts
index 44a944087e..4265504e7d 100644
--- a/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts
+++ b/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts
@@ -255,15 +255,14 @@ function WizardConfigInitializer(
       return configProperty;
     },
     _initAsZookeeperServersList: (configProperty: any, localDB: any) => {
-      var zkHosts = map(
+      var zkHosts = uniq(map(
         filter(localDB.masterComponentHosts, ["component", 
"ZOOKEEPER_SERVER"]),
         "hostName"
-      );
-      var zkHostPort = zkHosts;
+      ));
+      var zkHostPort = zkHosts.slice();
       var regex = "\\w*:(\\d+)"; //regex to fetch the port
-      var portValue =
-        get(configProperty, "recommendedValue") &&
-        get(configProperty, "recommendedValue").match(new RegExp(regex));
+      var sourceValue = get(configProperty, "recommendedValue") || 
get(configProperty, "value") || "";
+      var portValue = sourceValue && sourceValue.match(new RegExp(regex));
       if (!portValue) {
         return configProperty;
       }
diff --git a/ambari-web/latest/src/Utils/configs.ts 
b/ambari-web/latest/src/Utils/configs.ts
index e8393213c1..0cb7a366e2 100644
--- a/ambari-web/latest/src/Utils/configs.ts
+++ b/ambari-web/latest/src/Utils/configs.ts
@@ -428,12 +428,13 @@ class NnHaConfigInitializer {
   }
 
   private _initDfsJnEditsDir(config: ConfigProperty, localDB: 
ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
-    console.log("Dependencies is",dependencies,localDB);
-    // if (localDB.installedServices.includes('HDFS')) {
-    //   const value = dependencies.serverConfigs.find(config => config.type 
=== 'hdfs-site')?.properties['dfs.journalnode.edits.dir'];
-    //   config.value = value || '';
-    //   config.recommendedValue = value || '';
-    // }
+    if (localDB.installedServices.includes('HDFS')) {
+      const value = dependencies.serverConfigs.find(config => config.type === 
'hdfs-site')?.properties['dfs.journalnode.edits.dir'];
+      if (value) {
+        config.value = value;
+        config.recommendedValue = value;
+      }
+    }
     return config;
   }
 
diff --git a/ambari-web/latest/src/Utils/validators.ts 
b/ambari-web/latest/src/Utils/validators.ts
index 7781843f7f..8125f1640e 100644
--- a/ambari-web/latest/src/Utils/validators.ts
+++ b/ambari-web/latest/src/Utils/validators.ts
@@ -191,6 +191,16 @@ export const configValidator = {
     return dbPattern.test(value);
   },
 
+  /**
+   * validate db user name (permissive: any non-whitespace, to allow hyphens 
and other special chars)
+   * @param value
+   * @returns {boolean}
+   */
+  isValidDbUserName: function(value: string): boolean {
+    const dbPattern = /^\S+$/;
+    return dbPattern.test(value);
+  },
+
   /**
    * validate key of configurations
    * allow spaces as prefix and suffix
diff --git a/ambari-web/latest/src/api/hostsApi.ts 
b/ambari-web/latest/src/api/hostsApi.ts
index 5f27f137d2..0f0b244ff1 100644
--- a/ambari-web/latest/src/api/hostsApi.ts
+++ b/ambari-web/latest/src/api/hostsApi.ts
@@ -685,7 +685,38 @@ export const HostsApi = {
       data: JSON.stringify({
         RequestInfo: {
           context: data.context,
-          command: "MAKEOBSERVER",
+          command: "TRANSITION_NAMENODE",
+          // Ambari forwards a top-level RequestInfo key named after the 
component
+          // into agent commandParams. namenode.py reads target_ha_state from 
it.
+          namenode: JSON.stringify({ target_ha_state: "observer" }),
+          operation_level: {
+            level: "HOST_COMPONENT",
+            cluster_name: clusterName,
+            host_name: data.hostName,
+            service_name: "HDFS",
+          },
+        },
+        "Requests/resource_filters": [
+          {
+            service_name: "HDFS",
+            component_name: "NAMENODE",
+            hosts: data.hostName,
+          },
+        ],
+      }),
+    });
+    return response.data;
+  },
+  transitionToStandby: async function (clusterName: string, data: any) {
+    const url = `/clusters/${clusterName}/requests`;
+    const response = await ambariApi.request({
+      url: url,
+      method: "POST",
+      data: JSON.stringify({
+        RequestInfo: {
+          context: data.context,
+          command: "TRANSITION_NAMENODE",
+          namenode: JSON.stringify({ target_ha_state: "standby" }),
           operation_level: {
             level: "HOST_COMPONENT",
             cluster_name: clusterName,
diff --git a/ambari-web/latest/src/components/DependentConfigurationsModal.tsx 
b/ambari-web/latest/src/components/DependentConfigurationsModal.tsx
index 685091224c..e712b51612 100644
--- a/ambari-web/latest/src/components/DependentConfigurationsModal.tsx
+++ b/ambari-web/latest/src/components/DependentConfigurationsModal.tsx
@@ -25,43 +25,108 @@ import Modal from "./Modal";
 type DependentConfigurationsModalProps = {
   isOpen: boolean;
   onClose: () => void;
-  dependentConfigs: any[];
+  /** editable recommendations (Ember.js recommendedChanges) */
+  recommendations?: any[];
+  /** non-editable required changes (Ember.js requiredChanges) */
+  requiredChanges?: any[];
+  /** legacy single-list prop, treated as editable recommendations */
+  dependentConfigs?: any[];
   onSave: (updatedConfigs: any[]) => void;
 };
 
+const columnsBase = [
+  {
+    accessorKey: "propertyName",
+    header: "Property",
+    width: "20%",
+  },
+  {
+    accessorKey: "serviceDisplayName",
+    header: "Service",
+    cell: ({ row }: { row: any }) =>
+      row.original.serviceDisplayName || row.original.serviceName,
+    width: "10%",
+  },
+  {
+    accessorKey: "configGroup",
+    header: "Config Group",
+    cell: ({ getValue }: { getValue: () => any }) => getValue() || "Default",
+    width: "10%",
+  },
+  {
+    accessorKey: "propertyFileName",
+    header: "File Name",
+    cell: ({ row }: { row: any }) =>
+      row.original.propertyFileName || row.original.fileName,
+    width: "15%",
+  },
+  {
+    accessorKey: "initialValue",
+    header: "Original Value",
+    cell: ({ getValue }: { getValue: () => any }) => {
+      const value = getValue();
+      return (
+        <div
+          style={{ maxWidth: "200px", wordBreak: "break-word", fontSize: 
"12px" }}
+          title={value === null ? "Property undefined" : String(value)}
+        >
+          {value === null ? "Property undefined" : String(value)}
+        </div>
+      );
+    },
+    width: "20%",
+  },
+  {
+    accessorKey: "recommendedValue",
+    header: "Recommended Value",
+    cell: ({ getValue }: { getValue: () => any }) => {
+      const value = getValue();
+      return (
+        <div
+          style={{ maxWidth: "200px", wordBreak: "break-word", fontSize: 
"12px" }}
+          title={value === null ? "Property removed" : String(value)}
+        >
+          {value === null ? "Property removed" : String(value)}
+        </div>
+      );
+    },
+    width: "20%",
+  },
+];
+
 export default function DependentConfigurationsModal({
   isOpen,
   onClose,
+  recommendations,
+  requiredChanges,
   dependentConfigs,
   onSave,
 }: DependentConfigurationsModalProps) {
+  const initialRecommended = recommendations || dependentConfigs || [];
+
   const [isAllChecked, setIsAllChecked] = useState(true);
-  const [configsToChange, setConfigsToChange] = useState<any[]>(
-    dependentConfigs || []
+  const [editableConfigs, setEditableConfigs] = useState<any[]>(
+    initialRecommended
   );
 
   useEffect(() => {
-    setConfigsToChange(dependentConfigs || []);
-    setIsAllChecked(
-      (dependentConfigs || []).every((config: any) => config.saveRecommended)
-    );
-  }, [dependentConfigs]);
+    const next = recommendations || dependentConfigs || [];
+    setEditableConfigs(next);
+    setIsAllChecked(next.every((config: any) => config.saveRecommended));
+  }, [recommendations, dependentConfigs]);
 
   useEffect(() => {
     setIsAllChecked(
-      configsToChange.every((config: any) => config.saveRecommended)
+      editableConfigs.length > 0 &&
+        editableConfigs.every((config: any) => config.saveRecommended)
     );
-  }, [configsToChange]);
+  }, [editableConfigs]);
 
   const applyValueRestoration = (config: any, isChecked: boolean) => {
     const updatedConfig = { ...config, saveRecommended: isChecked };
-    
-    if (isChecked) {
-      updatedConfig.currentValue = config.recommendedValue;
-    } else {
-      updatedConfig.currentValue = config.initialValue;
-    }
-    
+    updatedConfig.currentValue = isChecked
+      ? config.recommendedValue
+      : config.initialValue;
     return updatedConfig;
   };
 
@@ -69,136 +134,94 @@ export default function DependentConfigurationsModal({
     event: React.ChangeEvent<HTMLInputElement>
   ) => {
     const isChecked = event.target.checked;
-    
-    // Apply value restoration logic to all configs
-    const updatedConfigs = configsToChange.map((config: any) => 
-      applyValueRestoration(config, isChecked)
+    setEditableConfigs(
+      editableConfigs.map((config: any) =>
+        applyValueRestoration(config, isChecked)
+      )
     );
-    
-    setConfigsToChange(updatedConfigs);
   };
 
   const handleRowCheckboxChange = (index: number, event: any) => {
     const isChecked = event.target.checked;
-    const newConfigs = cloneDeep(configsToChange);
-    
-    // Apply value restoration logic for individual row
+    const newConfigs = cloneDeep(editableConfigs);
     newConfigs[index] = applyValueRestoration(newConfigs[index], isChecked);
-    
-    setConfigsToChange(newConfigs);
+    setEditableConfigs(newConfigs);
   };
 
   const handleSave = () => {
-    onSave(configsToChange);
+    const merged = editableConfigs.map((c: any) => ({
+      ...c,
+      saveRecommendedDefault: c.saveRecommended,
+    }));
+    onSave([...merged, ...(requiredChanges || [])]);
     onClose();
   };
 
+  const editableColumns = [
+    {
+      accessorKey: "saveRecommended",
+      header: () => (
+        <Form.Check
+          id="select-all-checkbox"
+          type="checkbox"
+          checked={isAllChecked}
+          onChange={handleHeaderCheckboxChange}
+        />
+      ),
+      cell: ({ row }: { row: any }) => (
+        <Form.Check
+          type="checkbox"
+          checked={row.original.saveRecommended}
+          onChange={(e) => handleRowCheckboxChange(row.index, e)}
+        />
+      ),
+      width: "5%",
+    },
+    ...columnsBase,
+  ];
+
   const getModalBody = () => {
-    if (!configsToChange || configsToChange.length === 0) {
+    const hasEditable = editableConfigs.length > 0;
+    const hasRequired = (requiredChanges || []).length > 0;
+
+    if (!hasEditable && !hasRequired) {
       return <div>No dependent configuration changes found.</div>;
     }
 
-    const columns = [
-      {
-        accessorKey: "saveRecommended",
-        header: () => (
-          <Form.Check
-            id="select-all-checkbox"
-            type="checkbox"
-            checked={isAllChecked}
-            onChange={handleHeaderCheckboxChange}
-          />
-        ),
-        cell: ({ row }: { row: any }) => (
-          <Form.Check
-            type="checkbox"
-            checked={row.original.saveRecommended}
-            onChange={(e) => {
-              handleRowCheckboxChange(row.index, e);
-            }}
-          />
-        ),
-        width: "5%",
-      },
-      {
-        accessorKey: "propertyName",
-        header: "Property",
-        width: "20%",
-      },
-      {
-        accessorKey: "serviceDisplayName",
-        header: "Service",
-        width: "10%",
-      },
-      {
-        accessorKey: "configGroup",
-        header: "Config Group",
-        cell: ({ getValue }: { getValue: () => any }) => getValue() || 
"Default",
-        width: "10%",
-      },
-      {
-        accessorKey: "propertyFileName",
-        header: "File Name",
-        width: "15%",
-      },
-      {
-        accessorKey: "initialValue",
-        header: "Original Value",
-        cell: ({ getValue }: { getValue: () => any }) => {
-          const value = getValue();
-          return (
-            <div 
-              style={{ 
-                maxWidth: '200px', 
-                wordBreak: 'break-word',
-                fontSize: '12px'
-              }}
-              title={value === null ? "Property undefined" : String(value)}
-            >
-              {value === null ? "Property undefined" : String(value)}
-            </div>
-          );
-        },
-        width: "20%",
-      },
-      {
-        accessorKey: "recommendedValue",
-        header: "Recommended Value",
-        cell: ({ getValue }: { getValue: () => any }) => {
-          const value = getValue();
-          return (
-            <div 
-              style={{ 
-                maxWidth: '200px', 
-                wordBreak: 'break-word',
-                fontSize: '12px'
-              }}
-              title={value === null ? "Property removed" : String(value)}
-            >
-              {value === null ? "Property removed" : String(value)}
-            </div>
-          );
-        },
-        width: "20%",
-      },
-    ];
-
     return (
       <div>
-        <h4>Dependent Configurations</h4>
-        <div className="alert alert-warning mb-3" style={{ fontSize: '14px' }}>
-          Based on the services you are adding, Ambari is recommending the 
following dependent configuration changes.<br/>
-          Ambari will update all checked configuration changes to the 
<strong>Recommended Value</strong>. 
-          Uncheck any configuration to retain the <strong>Current 
Value</strong>.
-        </div>
-
-        <Table
-          columns={columns}
-          data={configsToChange}
-          entityName="configuration"
-          hover
-          className="dependent-configs-table"
-        />
+        {hasEditable && (
+          <>
+            <h4>Dependent Configurations</h4>
+            <div className="alert alert-warning mb-3" style={{ fontSize: 
"14px" }}>
+              Based on the services you are adding, Ambari is recommending the 
following dependent configuration changes.<br />
+              Ambari will update all checked configuration changes to the 
<strong>Recommended Value</strong>.
+              Uncheck any configuration to retain the <strong>Current 
Value</strong>.
+            </div>
+            <Table
+              columns={editableColumns}
+              data={editableConfigs}
+              entityName="configuration"
+              hover
+              className="dependent-configs-table"
+            />
+          </>
+        )}
+        {hasRequired && (
+          <>
+            <h4 className={hasEditable ? "mt-4" : ""}>Required Changes</h4>
+            <div className="alert alert-info mb-3" style={{ fontSize: "14px" 
}}>
+              The following configuration changes are required and will always 
be applied.
+            </div>
+            <Table
+              columns={columnsBase}
+              data={requiredChanges || []}
+              entityName="required configuration"
+              hover
+              className="required-configs-table"
+            />
+          </>
+        )}
       </div>
     );
   };
@@ -222,4 +245,4 @@ export default function DependentConfigurationsModal({
       }}
     />
   );
-}
\ No newline at end of file
+}
diff --git a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx 
b/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
index b9e237623b..9f1e77d1ff 100644
--- a/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
+++ b/ambari-web/latest/src/components/Sidebar/Sidebar.tsx
@@ -57,9 +57,23 @@ const SideBar = ({
   } = useContext(AppContext);
   const [openOptions, setOpenOptions] = 
useState<string[]>([SideItemLabels.SERVICES]);
   const [selectedOption, setSelectedOption] = useState<string>("");
-  const { allServiceModels  } = useContext(ServiceContext);
+  const { allServiceModels, serviceStatesData, polledHostComponentsData } = 
useContext(ServiceContext);
   const location = useLocation();
-  
+
+  // Derive the set of service names that currently have any host component 
with
+  // stale_configs === true. This mirrors RestartWarning.tsx so the Sidebar 
restart
+  // icon stays in sync instead of relying on the isRestartRequiredForService 
flag
+  // on the (non-reactively-tracked) service models.
+  const servicesNeedingRestart = new Set<string>();
+  (polledHostComponentsData?.items ?? []).forEach((item: any) => {
+    const svc = item?.ServiceComponentInfo?.service_name;
+    if (!svc) return;
+    const hasStale = (item?.host_components ?? []).some(
+      (hc: any) => hc?.HostRoles?.stale_configs === true
+    );
+    if (hasStale) servicesNeedingRestart.add(svc);
+  });
+
   // Authorization hooks - implementing Ember.js menu authorization patterns
   const { havePermissions, isAuthorized } = useAuthorizationPolicy();
   
@@ -117,6 +131,14 @@ const SideBar = ({
             return null;
           }
 
+          // Alert counts from serviceStatesData (updated reactively from 
socket events).
+          // Ember's alertDefinitionSummaryMapper immediately updates 
service.alertsCount +
+          // service.hasCriticalAlerts on each /events/alerts socket push; 
allServiceModels
+          // instances are never updated so we must read from 
serviceStatesData.
+          const stateData = serviceStatesData.get(serviceName);
+          const alertsCount = stateData?.alertsCount ?? 
currentServiceModel["alertsCount"] ?? 0;
+          const hasCriticalAlerts = stateData?.hasCriticalAlerts ?? 
currentServiceModel["hasCriticalAlerts"] ?? false;
+
           // Create the service object with proper state handling
           const serviceData = {
             name:
@@ -130,15 +152,17 @@ const SideBar = ({
                 ? currentServiceModel["serviceState"]
                 : "UNKNOWN",
             alertsCountDisplay:
-              currentServiceModel["alertsCount"] > 0
-                ? currentServiceModel["alertsCount"]
+              alertsCount > 0
+                ? alertsCount
                 : undefined,
-            noAlerts: currentServiceModel["alertsCount"] === 0,
-            hasCriticalAlerts:
-              currentServiceModel["hasCriticalAlerts"] || false,
+            noAlerts: alertsCount === 0,
+            hasCriticalAlerts: hasCriticalAlerts,
             isClientOnlyService: currentServiceModel["isClientOnlyService"] || 
false,
             isInPassiveForService: 
currentServiceModel["isInPassiveForService"] || false,
-            isRestartRequiredForService: 
currentServiceModel["isRestartRequiredForService"] || false
+            isRestartRequiredForService:
+              servicesNeedingRestart.has(serviceName) ||
+              currentServiceModel["isRestartRequiredForService"] ||
+              false
           };
 
           return serviceData;
@@ -170,7 +194,12 @@ const SideBar = ({
     };
 
     processServices();
-  }, [JSON.stringify(allServiceModels), clusterName, contextServices]);
+  // serviceStatesData is a new Map reference on each update (from 
setServiceStatesData),
+  // so React detects changes immediately when alert counts update from socket 
events.
+  // polledHostComponentsData drives the restart-required indicator 
(stale_configs),
+  // updated by both the 5s poll and the WebSocket /events/hostcomponents 
handler.
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [JSON.stringify(allServiceModels), clusterName, contextServices, 
serviceStatesData, polledHostComponentsData]);
 
   const getStateColor = (
     state: string,
diff --git a/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx 
b/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx
index 2b3d82b396..0be5c82dbe 100644
--- a/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx
+++ b/ambari-web/latest/src/components/Sidebar/SidebarItem.tsx
@@ -89,9 +89,23 @@ const SidebarItem = ({
     supports,
     runningOperationsCount,
   } = useContext(AppContext);
-  const { allServiceModels } = useContext(ServiceContext);
+  const { allServiceModels, polledHostComponentsData } = 
useContext(ServiceContext);
   const location = useLocation();
 
+  // Service names that currently have any host component with stale_configs 
=== true.
+  // Mirrors RestartWarning.tsx / the Sidebar restart icon so "Restart All 
Required" is
+  // enabled whenever a restart is actually required, using the reactive 
polled data
+  // rather than the isRestartRequiredForService flag on the service models.
+  const servicesNeedingRestart = new Set<string>();
+  (polledHostComponentsData?.items ?? []).forEach((item: any) => {
+    const svc = item?.ServiceComponentInfo?.service_name;
+    if (!svc) return;
+    const hasStale = (item?.host_components ?? []).some(
+      (hc: any) => hc?.HostRoles?.stale_configs === true
+    );
+    if (hasStale) servicesNeedingRestart.add(svc);
+  });
+
   // Authorization hooks - implementing Ember.js service menu authorization 
patterns
   const { havePermissions, isAuthorized } = useAuthorizationPolicy();
 
@@ -143,7 +157,10 @@ const SidebarItem = ({
         hasStartedServices = true;
       }
 
-      if 
(allServiceModels?.[serviceNameModelMapping[serviceName]]?.isRestartRequiredForService)
 {
+      if (
+        servicesNeedingRestart.has(serviceName) ||
+        
allServiceModels?.[serviceNameModelMapping[serviceName]]?.isRestartRequiredForService
+      ) {
         hasServicesRequiringRestart = true;
       }
     });
diff --git a/ambari-web/latest/src/hooks/useConfigSaver.tsx 
b/ambari-web/latest/src/hooks/useConfigSaver.tsx
index d6f733c28f..574346b545 100644
--- a/ambari-web/latest/src/hooks/useConfigSaver.tsx
+++ b/ambari-web/latest/src/hooks/useConfigSaver.tsx
@@ -48,7 +48,8 @@ export const useConfigSaver = (
   configProperties: ConfigPropertiesType,
   serviceName: string,
   configGroupsData: any,
-  serviceConfigVersionNote: string
+  serviceConfigVersionNote: string,
+  onSaveComplete?: () => void
 ) => {
   const [saveInProgress, setSaveInProgress] = useState(false);
 
@@ -622,7 +623,9 @@ export const useConfigSaver = (
       popupOptions.urlParams,
       doConfigActions
     );
-    // clearAllRecommendations();
+    if (result.flag && onSaveComplete) {
+      onSaveComplete();
+    }
   };
 
   const getSaveConfigsPopupOptions = (result: any) => {
diff --git a/ambari-web/latest/src/hooks/useDecommissionable.ts 
b/ambari-web/latest/src/hooks/useDecommissionable.ts
index 92d571ba74..ab3bc36a58 100644
--- a/ambari-web/latest/src/hooks/useDecommissionable.ts
+++ b/ambari-web/latest/src/hooks/useDecommissionable.ts
@@ -107,8 +107,6 @@ abstract class BaseDecommissionableComponent {
             isComponentDecommissioning: true,
             isComponentDecommissionAvailable: false,
           };
-          // Start polling when decommissioning
-          this.startPolling(component);
           break;
 
         case "DECOMMISSIONED":
@@ -118,8 +116,6 @@ abstract class BaseDecommissionableComponent {
             isComponentDecommissioning: false,
             isComponentDecommissionAvailable: false,
           };
-          // Stop polling when decommissioned
-          this.stopPolling(component);
           break;
 
         case "RS_DECOMMISSIONED":
@@ -188,66 +184,87 @@ class DataNodeComponent extends 
BaseDecommissionableComponent {
       activeNNHostNames = get(hdfs, "nameNode.hostName");
     }
 
+    // Desired admin state is authoritative for the target; the live LiveNodes
+    // view only tells us whether that target has been reached. Reconciling 
both
+    // avoids settling on a transient live reading.
+    let liveMetrics: any[] = [];
+    let desiredAdminState: string | null = null;
     try {
       const response = await HostsApi.getDecommissionStatusForDataNode(
         this.clusterName,
         activeNNHostNames
       );
-      this.handleDecommissionStatusResponse(response, component);
+      if (response && response.items) {
+        liveMetrics = response.items.map((item: any) =>
+          get(item, "metrics.dfs.namenode")
+        );
+      }
     } catch (error) {
       console.error("Failed to get DataNode decommission status");
     }
-  }
-
-  private handleDecommissionStatusResponse(
-    response: any,
-    component: IHostComponent
-  ): void {
-    if (response && response.items) {
-      const statusObjects = response.items.map((item: any) =>
-        get(item, "metrics.dfs.namenode")
+    try {
+      const dResp = await HostsApi.getSlaveDesiredAdminState(
+        this.clusterName,
+        get(component, "hostName"),
+        getComponentName(component)
       );
-      this.computeStatus(statusObjects, component);
+      desiredAdminState = get(dResp, "HostRoles.desired_admin_state", null);
+    } catch (error) {
+      desiredAdminState = null;
     }
+
+    this.reconcileStatus(liveMetrics, desiredAdminState, component);
   }
 
-  private computeStatus(metricObjects: any[], component: IHostComponent): void 
{
-    const hostName = get(component, "hostName");
-    let inServiceCount = 0;
-    let decommissioningCount = 0;
-    let decommissionedCount = 0;
-
-    metricObjects.forEach((curObj) => {
-      if (curObj) {
-        const liveNodesJson = JSON.parse(curObj.LiveNodes || "{}");
-        for (const hostPort in liveNodesJson) {
-          if (hostPort.indexOf(hostName) === 0) {
-            switch (liveNodesJson[hostPort].adminState) {
-              case "In Service":
-                inServiceCount++;
-                break;
-              case "Decommission In Progress":
-                decommissioningCount++;
-                break;
-              case "Decommissioned":
-                decommissionedCount++;
-                break;
-            }
-            return;
-          }
+  private getLiveAdminState(
+    metricObjects: any[],
+    hostName: string
+  ): string | null {
+    for (const curObj of metricObjects) {
+      if (!curObj) continue;
+      const liveNodesJson = JSON.parse(curObj.LiveNodes || "{}");
+      for (const hostPort in liveNodesJson) {
+        if (hostPort.indexOf(hostName) === 0) {
+          return liveNodesJson[hostPort].adminState;
         }
       }
-    });
+    }
+    return null;
+  }
 
-    if (decommissioningCount) {
-      this.setStatusAs("DECOMMISSIONING", component);
-    } else if (inServiceCount && !decommissionedCount) {
-      this.setStatusAs("INSERVICE", component);
-    } else if (!inServiceCount && decommissionedCount) {
+  private reconcileStatus(
+    metricObjects: any[],
+    desiredAdminState: string | null,
+    component: IHostComponent
+  ): void {
+    const hostName = get(component, "hostName");
+    const liveAdminState = this.getLiveAdminState(metricObjects, hostName);
+
+    // Desired admin state reflects the operator's request and is what the icon
+    // shows. Polling keeps running so a later backend change is picked up.
+    if (desiredAdminState === "DECOMMISSIONED") {
       this.setStatusAs("DECOMMISSIONED", component);
-    } else {
-      // If namenodes are down, get desired_admin_state to decide if the user 
had issued a decommission
-      this.getDesiredAdminState(component);
+      return;
+    }
+
+    if (desiredAdminState === "INSERVICE") {
+      this.setStatusAs("INSERVICE", component);
+      return;
+    }
+
+    // No desired admin state - fall back to the live reading.
+    switch (liveAdminState) {
+      case "Decommission In Progress":
+        this.setStatusAs("DECOMMISSIONING", component);
+        break;
+      case "Decommissioned":
+        this.setStatusAs("DECOMMISSIONED", component);
+        break;
+      case "In Service":
+        this.setStatusAs("INSERVICE", component);
+        break;
+      default:
+        this.setStatusAs("INSERVICE", component);
     }
   }
 }
@@ -595,6 +612,11 @@ export const useDecommissionable = (host: IHost) => {
     });
 
   const pollingTimers = useRef<Map<string, NodeJS.Timeout>>(new Map());
+  // Active polling keys; a terminal state removes its key so the in-flight 
tick
+  // knows not to re-arm.
+  const activePolls = useRef<Set<string>>(new Set());
+  // Ref to the latest loader so the polling closure never reads a stale 
snapshot.
+  const loadStatusRef = useRef<((component: IHostComponent) => Promise<void>) 
| undefined>(undefined);
 
   useEffect(() => {
     return () => {
@@ -602,6 +624,7 @@ export const useDecommissionable = (host: IHost) => {
         clearTimeout(timer);
       });
       pollingTimers.current.clear();
+      activePolls.current.clear();
     };
   }, []);
 
@@ -611,15 +634,18 @@ export const useDecommissionable = (host: IHost) => {
         component
       )}`;
 
-      if (!pollingTimers.current.has(componentKey)) {
+      if (!activePolls.current.has(componentKey)) {
+        activePolls.current.add(componentKey);
         const pollStatus = async () => {
           try {
-            await loadComponentDecommissionStatus(component);
-            const timer = setTimeout(pollStatus, POLLING_INTERVAL);
-            pollingTimers.current.set(componentKey, timer);
+            await loadStatusRef.current?.(component);
           } catch (error) {
             console.error("Error during decommission status polling:", error);
-            pollingTimers.current.delete(componentKey);
+          }
+          // Re-arm while active; cleared only on unmount.
+          if (activePolls.current.has(componentKey)) {
+            const next = setTimeout(pollStatus, POLLING_INTERVAL);
+            pollingTimers.current.set(componentKey, next);
           }
         };
 
@@ -635,6 +661,7 @@ export const useDecommissionable = (host: IHost) => {
       const componentKey = `${get(component, "hostName")}_${getComponentName(
         component
       )}`;
+      activePolls.current.delete(componentKey);
       const timer = pollingTimers.current.get(componentKey);
 
       if (timer) {
@@ -696,8 +723,11 @@ export const useDecommissionable = (host: IHost) => {
     }
   };
   
+  // Keep the polling closure pointed at the current loader.
+  loadStatusRef.current = loadComponentDecommissionStatus;
+
   useEffect(() => {
-    if (!isEmpty(host)) {    
+    if (!isEmpty(host)) {
       get(host, "hostComponents", []).forEach((hostComponent: IHostComponent) 
=> {
         loadComponentDecommissionStatus(hostComponent);
       });
diff --git a/ambari-web/latest/src/hooks/useEnhancedConfigs.ts 
b/ambari-web/latest/src/hooks/useEnhancedConfigs.ts
index 4d97220734..afd7c15584 100644
--- a/ambari-web/latest/src/hooks/useEnhancedConfigs.ts
+++ b/ambari-web/latest/src/hooks/useEnhancedConfigs.ts
@@ -690,6 +690,11 @@ function useEnhancedConfigs(
     
     recommededConfigsRef.current = filteredRecommendations;
   }
+  function clearAllRecommendations() {
+    recommededConfigsRef.current = {};
+    setRecommendedChanges({});
+  }
+
   //@ts-ignore
   function clearRecommendationsByServiceName(serviceNames: string[]) {
     const filteredRecommendations: { [key: string]: any } = {};
@@ -933,6 +938,7 @@ function useEnhancedConfigs(
     loadAddServiceRecommendations,
     recommendedChanges,
     setRecommendedChanges,
+    clearAllRecommendations,
   };
 }
 export default useEnhancedConfigs;
diff --git a/ambari-web/latest/src/hooks/useHostConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useHostConfigUpdater.ts
index 2e8e0fe036..334014a1de 100644
--- a/ambari-web/latest/src/hooks/useHostConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHostConfigUpdater.ts
@@ -37,6 +37,19 @@ import {
 } from "../Utils/hosts";
 import VersionsApi from "../api/versionsApi";
 
+// cloneDeep strips class prototypes; rebuild instances so model methods 
survive.
+const cloneHostModels = (hosts: Host[]): Host[] =>
+  cloneDeep(hosts).map((host: Host) => {
+    const model = new Host(host as IHost);
+    model.hostComponents = get(host, "hostComponents", []).map(
+      (hc: IHostComponent) => new HostComponent(hc)
+    );
+    model.stackVersions = get(host, "stackVersions", []).map(
+      (sv: IHostStackVersion) => new HostStackVersion(sv)
+    );
+    return model;
+  });
+
 export const useHostConfigUpdater = (
   hostApiQueryParams: any,
   allHostModels: Host[],
@@ -154,7 +167,7 @@ export const useHostConfigUpdater = (
 
     if (get(response, "items", []).length) {
       // Use the ref to get the latest allHostModels value, avoiding stale 
closure
-      const allHostModelsCopy = cloneDeep(allHostModelsRef.current);
+      const allHostModelsCopy = cloneHostModels(allHostModelsRef.current);
       get(response, "items", []).forEach((host: any) => {
         const hostName = get(host, "Hosts.host_name", "");
         const hostModel = allHostModelsCopy.find(
diff --git a/ambari-web/latest/src/layout/Dashboard.tsx 
b/ambari-web/latest/src/layout/Dashboard.tsx
index 1d428d40f2..e0bc03528c 100644
--- a/ambari-web/latest/src/layout/Dashboard.tsx
+++ b/ambari-web/latest/src/layout/Dashboard.tsx
@@ -20,7 +20,6 @@ import { useEffect, useMemo, useRef, useState } from "react";
 import { Outlet, useParams } from "react-router-dom";
 import { useContext } from "react";
 import { AppContext } from "../store/context";
-import { AlertsApi } from "../api/alertsApi";
 import { useLocation } from "react-router-dom";
 import {
   map,
@@ -40,8 +39,8 @@ import Upgrade from 
"../screens/ClusterAdmin/StackAndVersions/Upgrade";
 import { getUpgradeRequestStatus, translate } from "../Utils/Utility";
 import { messages } from "../screens/messages";
 import ClusterApi from "../api/clusterApi";
-import { processData } from '../screens/Alerts/alertUtils';
 import useAuth from "../hooks/useAuth";
+import { useAlerts } from "../store/AlertsContext";
 import { isUpgradeRequest } from "../Utils/backgroundOperations";
 import { HostsApi } from "../api/hostsApi";
 import { useViewInstances } from "../screens/Views/ViewInstancesContext";
@@ -76,13 +75,22 @@ const DashboardLayout = () => {
   const [hostMaintenanceState, setHostMaintenanceState] = 
useState<string>("OFF");
 
   const isClusterInstalled = cluster?.provisioning_state === "INSTALLED";
-  // add a id and name map of alerts
-  const [alertLabels, setAlertLabels] = useState(new Map());
+  const { alertDefinitions, isLoading: alertsLoading } = useAlerts();
+  // Build id→label map from AlertsContext data — no separate API calls needed
+  const alertLabels = useMemo(() => {
+    const map = new Map();
+    alertDefinitions.forEach((def: any) => {
+      if (def.id && def.label) {
+        map.set(def.id, def.label);
+      }
+    });
+    return map;
+  }, [alertDefinitions]);
+  const alertLabelsLoaded = !alertsLoading;
   const { hostname } = useParams();
   //@ts-ignore
   const [clusterRequests, setClusterRequests] = useState<any[]>([]);
   const requestsRef = useRef<any>([]);
-  const [alertLabelsLoaded, setAlertLabelsLoaded] = useState(false);
 
   // Function to fetch host maintenance state
   const fetchHostMaintenanceState = async () => {
@@ -171,50 +179,14 @@ const DashboardLayout = () => {
   }, [parsedSocketMessages, hostname]);
 
   useEffect(() => {
-    const fetchAlertsLabel = async () => {
-      // TLHASD-745: Only fetch alerts if cluster is installed
-      if (!isClusterInstalled) {
-        setAlertLabelsLoaded(true);
-        return;
-      }
-      
-      try {
-        // Use the same API calls as the Alerts page
-        const [alertsResponse, summariesResponse] = await Promise.all([
-          AlertsApi.getAlerts(
-            clusterName,
-            
'AlertGroup/default,AlertGroup/definitions,AlertGroup/id,AlertGroup/name,AlertGroup/targets',
-            Date.now()
-          ),
-          AlertsApi.getAlertSummary(clusterName, Date.now())
-        ]);
-
-        const processedAlerts = processData(alertsResponse, 
summariesResponse);        
-        // Extract labels from the processed data
-        const alertLabelsCopy = new Map();
-        processedAlerts.forEach(alert => {
-          if (alert.alert_definition_id && alert.label) {
-            alertLabelsCopy.set(alert.alert_definition_id, alert.label);
-          }
-        });      
-        setAlertLabels(alertLabelsCopy); // Ensure state update
-        setAlertLabelsLoaded(true);
-      } catch (error) {
-        console.error("Failed to fetch alert labels:", error);
-        setAlertLabelsLoaded(true);
-      }
-    };
-    
     const getClusterRequestsConditional = async () => {
       if (!isClusterInstalled) {
         return;
       }
-      
       await getClusterRequests();
     };
 
     if (clusterName) {
-      fetchAlertsLabel();
       getClusterRequestsConditional();
       fetchHostMaintenanceState();
     }
diff --git a/ambari-web/latest/src/locales/en/translation.json 
b/ambari-web/latest/src/locales/en/translation.json
index 41a0247cc8..3d0d0f5b81 100644
--- a/ambari-web/latest/src/locales/en/translation.json
+++ b/ambari-web/latest/src/locales/en/translation.json
@@ -1980,6 +1980,12 @@
   "services.service.actions.run.rebalanceHdfsNodes.error":"Error during remote 
command: ",
   "services.service.actions.run.makeObserver.context":"Transition To Observer",
   "services.service.actions.run.makeObserver":"Transition To Observer",
+  "services.service.actions.run.makeObserver.error":"Error during transition 
to observer: ",
+  "question.sure.makeObserver":"Are you sure you want to transition {0} to 
Observer mode?",
+  "services.service.actions.run.makeStandby.context":"Transition To Standby",
+  "services.service.actions.run.makeStandby":"Transition To Standby",
+  "services.service.actions.run.makeStandby.error":"Error during transition to 
standby: ",
+  "question.sure.makeStandby":"Are you sure you want to transition {0} to 
Standby mode?",
   "services.service.actions.run.yarnRefreshQueues.title":"Refresh Queues 
ResourceManager",
   "services.service.actions.run.yarnRefreshQueues.menu":"Refresh YARN Capacity 
Scheduler",
   "services.service.actions.run.yarnRefreshQueues.context":"Refresh YARN 
Capacity Scheduler",
diff --git 
a/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx 
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx
index 6306d049e8..9121a7af43 100644
--- 
a/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx
+++ 
b/ambari-web/latest/src/screens/ClusterAdmin/StackAndVersions/ListVersion.tsx
@@ -103,7 +103,7 @@ export default function Versions() {
   const [slaveComponentFailures, setSlaveComponentFailures] = useState(false);
   const [serviceCheckFailures, setServiceCheckFailures] = useState(false);
   const [isRestoring, setIsRestoring] = useState(true);
-  const { clusterName, setUpgradeId, upgradeState, setIsPatchUpgrade, 
setUpgradeVersionDisplayName, allHostNames, upgradeVersionDisplayName, 
upgradeId, upgradeDirection, upgradeIsRunning, upgradeSuspended, supports, 
isNonWizardUser } = useContext(AppContext);
+  const { clusterName, setUpgradeId, upgradeState, setIsPatchUpgrade, 
setUpgradeVersionDisplayName, allHostNames, upgradeVersionDisplayName, 
upgradeAssociatedVersion, setUpgradeAssociatedVersion, upgradeId, 
upgradeDirection, upgradeIsRunning, upgradeSuspended, supports, isNonWizardUser 
} = useContext(AppContext);
   
   const packagesPayloadRef = useRef<any>({});
   
@@ -1269,6 +1269,9 @@ export default function Versions() {
       });
       setUpgradeCheckModal(false);
       setUpgradeConfirmationModal(false);
+      if (setUpgradeAssociatedVersion) {
+        setUpgradeAssociatedVersion(get(selectedStackRef.current, 
"repository_versions[0].RepositoryVersions.repository_version", ""));
+      }
       modalManager.show(<Upgrade upgradeId={newUpgradeId} />);
     } catch (error) {
       modalManager.show({
@@ -1324,16 +1327,25 @@ export default function Versions() {
   }
 
   function getUpgradeStatus(stackData: StackVersion) {
-    const stackDisplayName = 
stackData.repository_versions[0].RepositoryVersions.display_name;
-    
-    if (upgradeState !== "COMPLETED" && upgradeState !== "NOT_REQUIRED" && 
upgradeVersionDisplayName && stackDisplayName === upgradeVersionDisplayName) {
+    const { display_name: stackDisplayName, repository_version: 
stackRepositoryVersion } =
+      stackData.repository_versions[0].RepositoryVersions;
+
+    const isUpgradeActive =
+      upgradeState !== "COMPLETED" && upgradeState !== "NOT_REQUIRED";
+
+    // Prefer matching by repository_version (unambiguous across patches); 
fall back to display name.
+    const matchesUpgrade = upgradeAssociatedVersion
+      ? stackRepositoryVersion === upgradeAssociatedVersion
+      : !!upgradeVersionDisplayName && stackDisplayName === 
upgradeVersionDisplayName;
+
+    if (isUpgradeActive && matchesUpgrade) {
       return {
         isUpgradeInProgress: true,
         upgradeState: upgradeState,
-        statusText: getUpgradeRequestStatus(upgradeState, upgradeDirection == 
"DOWNGRADE"), // false for upgrade, true for downgrade
+        statusText: getUpgradeRequestStatus(upgradeState, upgradeDirection == 
"DOWNGRADE"),
       };
     }
-    
+
     return {
       isUpgradeInProgress: false,
       upgradeState: null,
@@ -1831,6 +1843,9 @@ export default function Versions() {
       ]).catch(() => {
         toast.error("The revert started, but its browser state could not be 
persisted");
       });
+      if (setUpgradeAssociatedVersion) {
+        setUpgradeAssociatedVersion(get(selectedStackRef.current, 
"repository_versions[0].RepositoryVersions.repository_version", ""));
+      }
       modalManager.show(<Upgrade upgradeId={upgradeId} />);
     } catch (error) {
       modalManager.show({
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step7/AccountsTab.tsx 
b/ambari-web/latest/src/screens/ClusterWizard/Step7/AccountsTab.tsx
index 42e5c928c3..80867d64e3 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/Step7/AccountsTab.tsx
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step7/AccountsTab.tsx
@@ -19,7 +19,8 @@
 import { cloneDeep, get, isEmpty, set } from "lodash";
 import { useEffect, useRef, useState } from "react";
 import { Card, Col, Container, Row } from "react-bootstrap";
-import { getDependentConfigChanges, isValidUserName } from "../utils";
+import { getDependentConfigChanges } from "../utils";
+import { configValidator } from "../../../Utils/validators";
 import Table from "../../../components/Table";
 import Modal from "../../../components/Modal";
 import TooltipInput from "../../../components/TooltipInput";
@@ -256,7 +257,7 @@ export default function AccountsTab({
                                   setShowWarning(true);
                                 }
                               },
-                              className: isValidUserName(config.value)
+                              className: 
configValidator.isValidDbUserName(config.value as string)
                                 ? "rounded-0"
                                 : "rounded-0 border-danger",
                             }}
diff --git 
a/ambari-web/latest/src/screens/ClusterWizard/Step7/CredentialsTab.tsx 
b/ambari-web/latest/src/screens/ClusterWizard/Step7/CredentialsTab.tsx
index 6af467d625..de3d4ccf60 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/Step7/CredentialsTab.tsx
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step7/CredentialsTab.tsx
@@ -27,8 +27,8 @@ import {
 import { Card, Form } from "react-bootstrap";
 import { ConfigPropertiesType } from "../../CommonConfigs/types";
 import { CredentialConfigType } from "../types/step7Types";
-import { isValidUserName } from "../utils";
 import TooltipInput from "../../../components/TooltipInput";
+import { configValidator } from "../../../Utils/validators";
 
 interface CredentialsTabProps {
   themes: object;
@@ -92,7 +92,7 @@ export default function CredentialsTab({
     const allCredentialsValid = allCredentials.every((config) => {
       return (
         (get(config, "usernameProperty")
-          ? isValidUserName(get(config, "usernameProperty.property_value", ""))
+          ? configValidator.isValidDbUserName(get(config, 
"usernameProperty.property_value", ""))
           : true) && isValidPassword(config)
       );
     });
@@ -118,7 +118,7 @@ export default function CredentialsTab({
     const isPasswordProperty = propertyType.toLowerCase().includes("password");
     const isInputValid = isPasswordProperty
       ? isValidPassword(config)
-      : isValidUserName(property_value);
+      : configValidator.isValidDbUserName(get(config, 
"usernameProperty.property_value", "") as string);
 
     let tooltipHeading = "";
     if (property_display_name && property_name) {
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step7/index.tsx 
b/ambari-web/latest/src/screens/ClusterWizard/Step7/index.tsx
index d6a77aade3..d3d601044f 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/Step7/index.tsx
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step7/index.tsx
@@ -271,6 +271,8 @@ export default function Step7({ wizardName = 
"clusterCreation" }: PropTypes) {
   const [dependentConfigsToShow, setDependentConfigsToShow] = useState<any[]>(
     []
   );
+  const [requiredConfigsToShow, setRequiredConfigsToShow] = 
useState<any[]>([]);
+  const initialRecommendationRequestedRef = useRef(false);
 
   const [configPropertiesLoaded, setConfigPropertiesLoaded] = useState(false);
   const [propertyValues, setPropertyValues] = useState<any>({});
@@ -580,23 +582,27 @@ export default function Step7({ wizardName = 
"clusterCreation" }: PropTypes) {
     setIsNextEnabled(true);
   }, [configProperties, selectedTab, themes, validationErrors]);
   useEffect(() => {
-    if (configPropertiesLoaded) {
-      if (wizardName === "clusterCreation") {
-        // Load recommendations for cluster creation - this was missing the 
proper call
-        loadConfigRecommendations();
-      } else if (wizardName === "addService") {
-        const newlyAddingServices = services.filter(service => 
-          !installedServices?.includes(service) && service !== "MISC"
-        );
-        
-        loadAddServiceRecommendations(
-          configProperties,
-          newlyAddingServices, // Pass only newly adding services
-          getValidationRequestBody()
-        );
+    if (!configPropertiesLoaded || initialRecommendationRequestedRef.current) {
+      return;
+    }
+    if (wizardName === "clusterCreation") {
+      initialRecommendationRequestedRef.current = true;
+      loadConfigRecommendations();
+    } else if (wizardName === "addService") {
+      const newlyAddingServices = services.filter(
+        (service) => !installedServices?.includes(service) && service !== 
"MISC"
+      );
+      if (newlyAddingServices.length === 0) {
+        return;
       }
+      initialRecommendationRequestedRef.current = true;
+      loadAddServiceRecommendations(
+        configProperties,
+        newlyAddingServices,
+        getValidationRequestBody()
+      );
     }
-  }, [configPropertiesLoaded]);
+  }, [configPropertiesLoaded, services, installedServices]);
 
   // Monitor recommended changes and prepare data for add service wizard
   // This implements the Ember.js filtering logic from changedProperties and 
filterRequiredChanges
@@ -629,38 +635,31 @@ export default function Step7({ wizardName = 
"clusterCreation" }: PropTypes) {
         }
       );
       
-      // Combine both types of changes (matching Ember.js 
showChangedDependentConfigs)
-      const allChanges = [...recommendedChanges_filtered, ...requiredChanges];
-      
-      if (allChanges.length > 0) {
-        // Further filter to only show changes for installed services 
(dependent configs)
-        const dependentChanges = allChanges.filter((change: any) => {
-          return installedServices.includes(change.serviceName);
-        });
+      const formatChange = (change: any) => ({
+        propertyName: change.propertyName,
+        serviceName: change.serviceName,
+        serviceDisplayName: change.serviceName,
+        configGroup: change.configGroup || "Default",
+        propertyFileName: change.fileName,
+        initialValue: change.initialValue,
+        originalValue: change.initialValue,
+        recommendedValue: change.recommendedValue,
+        saveRecommended: change.saveRecommended !== false,
+        isEditable: change.isEditable !== false,
+      });
 
-        if (dependentChanges.length > 0) {
-          const formattedChanges = dependentChanges.map((change: any) => ({
-            propertyName: change.propertyName,
-            serviceName: change.serviceName,
-            serviceDisplayName: change.serviceName,
-            configGroup: change.configGroup || "Default",
-            propertyFileName: change.fileName, // Use fileName instead of 
propertyFileName
-            initialValue: change.initialValue,
-            originalValue: change.initialValue, // Add originalValue for 
backward compatibility
-            recommendedValue: change.recommendedValue,
-            saveRecommended: change.saveRecommended !== false,
-            isEditable: change.isEditable !== false,
-          }));
-          
-          setDependentConfigsToShow(formattedChanges);
-        } else {
-          setDependentConfigsToShow([]);
-        }
-      } else {
-        setDependentConfigsToShow([]);
-      }
+      const dependentEditable = recommendedChanges_filtered
+        .filter((c: any) => installedServices.includes(c.serviceName))
+        .map(formatChange);
+      const dependentRequired = requiredChanges
+        .filter((c: any) => installedServices.includes(c.serviceName))
+        .map(formatChange);
+
+      setDependentConfigsToShow(dependentEditable);
+      setRequiredConfigsToShow(dependentRequired);
     } else {
       setDependentConfigsToShow([]);
+      setRequiredConfigsToShow([]);
     }
   }, [recommendedChanges, wizardName, installedServices]);
 
@@ -2778,7 +2777,8 @@ export default function Step7({ wizardName = 
"clusterCreation" }: PropTypes) {
         <DependentConfigurationsModal
           isOpen={showDependentConfigsModal}
           onClose={() => setShowDependentConfigsModal(false)}
-          dependentConfigs={dependentConfigsToShow}
+          recommendations={dependentConfigsToShow}
+          requiredChanges={requiredConfigsToShow}
           onSave={handleDependentConfigsModalCallback}
         />
       )}
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step8.tsx 
b/ambari-web/latest/src/screens/ClusterWizard/Step8.tsx
index bcb63c6f92..5becb885ad 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/Step8.tsx
+++ b/ambari-web/latest/src/screens/ClusterWizard/Step8.tsx
@@ -521,8 +521,9 @@ function Step8({ wizardName = "clusterCreation" }: 
Step8Props) {
 
   function renderRepos() {
     const operatingSystems = getStepData("VERSION", "operatingSystems");
-    const selectedStack = getStepData("VERSION", "selectedStack.id");
-    const addedOs = operatingSystems[selectedStack].filter(
+    // Use selectedVersion.id to match the key stored by Step1 (e.g. 
"3.4.1.0-13"), not selectedStack.id (e.g. "VDP-3.4")
+    const selectedVersionId = getStepData("VERSION", "selectedVersion.id");
+    const addedOs = operatingSystems[selectedVersionId].filter(
       (os: any) => os.isAdded
     );
     const allRepos = addedOs.map((currentOs: any) => {
@@ -758,12 +759,13 @@ function Step8({ wizardName = "clusterCreation" }: 
Step8Props) {
 
   async function getUpdateRepoOSInfoBody() {
     const usesRedhat = getStepData("VERSION", "redhatSatellite");
-    const selectedStack = getStepData("VERSION", "selectedStack.id");
+    // Use selectedVersion.id to match the key stored by Step1 (e.g. 
"3.4.1.0-13"), not selectedStack.id (e.g. "VDP-3.4")
+    const selectedVersionId = getStepData("VERSION", "selectedVersion.id");
     const operatingSystemsFromState = getStepData(
       "VERSION",
       `operatingSystems`
     );
-    const operatingSystems = operatingSystemsFromState[selectedStack];
+    const operatingSystems = operatingSystemsFromState[selectedVersionId];
     if (isArray(operatingSystems) && operatingSystems.length) {
       const selectedOperatingSystems = operatingSystems?.filter(
         (os: any) => os.isAdded
diff --git a/ambari-web/latest/src/screens/CommonConfigs/AdvancedConfigs.tsx 
b/ambari-web/latest/src/screens/CommonConfigs/AdvancedConfigs.tsx
index 216176c945..da30e5c146 100644
--- a/ambari-web/latest/src/screens/CommonConfigs/AdvancedConfigs.tsx
+++ b/ambari-web/latest/src/screens/CommonConfigs/AdvancedConfigs.tsx
@@ -157,6 +157,7 @@ function AdvancedConfigs({
   const [multiPropertyErrors, setMultiPropertyErrors] = useState<string[]>([]);
   const [isMultiPropertyMode, setIsMultiPropertyMode] = 
useState<boolean>(false);
   const [selectedPropertyTypes, setSelectedPropertyTypes] = 
useState<string[]>([]);
+  const [hostsModalProperty, setHostsModalProperty] = useState<PropertyType | 
null>(null);
 
   const configSectionNames = Object.keys(
     advancedConfigs?.[chosenService] || {},
@@ -479,10 +480,22 @@ function AdvancedConfigs({
           </div>
         );
       case InputType.HOSTS:
-        return !isEmpty(property?.value) && isArray(property?.value) ? (
-          <span>{property?.value?.join(", ")}</span>
+        if (isEmpty(property?.value) || !isArray(property?.value)) {
+          return "No host assigned";
+        }
+        return property.value.length > 1 ? (
+          <a
+            href="#"
+            onClick={(e) => {
+              e.preventDefault();
+              setHostsModalProperty(property);
+            }}
+          >
+            {property.value[0]} and {property.value.length - 1}{" "}
+            {property.value.length - 1 === 1 ? "other" : "others"}
+          </a>
         ) : (
-          "No host assigned"
+          <span>{property.value[0]}</span>
         );
 
       case InputType.RADIOBUTTON:
@@ -1437,6 +1450,24 @@ function AdvancedConfigs({
         successCallback={handleAddProperty}
         options={{}}
       ></Modal>
+      <Modal
+        isOpen={!!hostsModalProperty}
+        onClose={() => setHostsModalProperty(null)}
+        modalTitle={
+          hostsModalProperty?.propertyDisplayname ||
+          hostsModalProperty?.propertyName ||
+          "Hosts"
+        }
+        modalBody={
+          <ul className="list-unstyled mb-0">
+            {(hostsModalProperty?.value || []).map((host: string) => (
+              <li className="mt-2" key={host}>{host}</li>
+            ))}
+          </ul>
+        }
+        successCallback={() => setHostsModalProperty(null)}
+        options={{ cancelableViaIcon: true, cancelableViaBtn: false }}
+      ></Modal>
     </>
   );
 }
diff --git a/ambari-web/latest/src/screens/CommonConfigs/types/index.ts 
b/ambari-web/latest/src/screens/CommonConfigs/types/index.ts
index 4b972ed542..3386aa99c4 100644
--- a/ambari-web/latest/src/screens/CommonConfigs/types/index.ts
+++ b/ambari-web/latest/src/screens/CommonConfigs/types/index.ts
@@ -83,6 +83,11 @@ export type PropertyType = {
   isSecureConfig?: boolean;
   unit?: string; // Unit for the property value
   widget?: Record<string, any>;
+  initialValue?: string | null;
+  savedValue?: string | null;
+  group?: { name: string } | null;
+  hiddenBySection?: boolean;
+  [dynamicKey: string]: any;
 };
 
 export type ThemeType = {
@@ -173,6 +178,25 @@ export type configGroupOverrides = {
   [dynamicKey: string]: any;
 }
 
+export type Recommendation = {
+  saveRecommended: boolean;
+  saveRecommendedDefault: boolean;
+  isDeleted: boolean;
+  notDefined: boolean;
+  propertyName: string;
+  propertyFileName: string;
+  propertyTitle?: string;
+  propertyDescription?: string;
+  configGroup: string;
+  serviceName: string;
+  serviceDisplayName: string;
+  initialValue: string | null;
+  recommendedValue: string | null;
+  allowChangeGroup: boolean;
+  parentConfigs: string[];
+  isEditable: boolean;
+};
+
 export type TabErrorsType = {
   [key: string]: {
     errors: string;
diff --git 
a/ambari-web/latest/src/screens/ConfigGroups/AddToConfigGroupModal.tsx 
b/ambari-web/latest/src/screens/ConfigGroups/AddToConfigGroupModal.tsx
index 588c398eb9..3823b95116 100644
--- a/ambari-web/latest/src/screens/ConfigGroups/AddToConfigGroupModal.tsx
+++ b/ambari-web/latest/src/screens/ConfigGroups/AddToConfigGroupModal.tsx
@@ -48,7 +48,10 @@ export default function AddToConfigGroupModal({
 
   useEffect(() => {
     if (configGroupNames && configGroupNames.length > 0) {
-      setSelectedConfigGroup(configGroupNames[0]);
+      // Keep the current choice if still valid so it doesn't snap back to the 
first group.
+      setSelectedConfigGroup((prev) =>
+        prev && configGroupNames.includes(prev) ? prev : configGroupNames[0]
+      );
       setIsExistingConfigGroup(true);
     } else {
       // If no existing config groups, automatically select "Create new" option
@@ -57,6 +60,17 @@ export default function AddToConfigGroupModal({
     }
   }, [configGroupNames]);
 
+  // Reset fields on open; the modal stays mounted while hidden so state would 
otherwise persist.
+  useEffect(() => {
+    if (isOpen) {
+      setNewCofigGroup("");
+      setIsExistingConfigGroup(configGroupNames.length > 0);
+      setSelectedConfigGroup(
+        configGroupNames.length > 0 ? configGroupNames[0] : ""
+      );
+    }
+  }, [isOpen]);
+
   const createNewConfigGroup = async () => {
     const payload = {
       ConfigGroup: {
diff --git a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx 
b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
index 9de90ccbb5..4b8ffa99ad 100644
--- a/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostSummary.tsx
@@ -99,6 +99,7 @@ import {
   installComponent,
   executeCustomCommand,
   transitionToObserver,
+  transitionToStandby,
 } from "./actions";
 import { AppContext } from "../../store/context";
 import IHost from "../../models/host";
@@ -314,7 +315,7 @@ export default function HostsSummary({
     }
   }, [clusterComponents, summary]);
 
-  const { decommissionable, isComponentDecommissionDisable } =
+  const { decommissionable, isComponentDecommissionDisable, 
startDecommissionStatusPolling, loadComponentDecommissionStatus } =
     useDecommissionable(get(allHostModels, "[0]", {} as IHost));
 
   // Authorization hooks - implementing Ember.js host component authorization 
patterns
@@ -437,8 +438,9 @@ export default function HostsSummary({
   const getStateIcon = (component: IHostComponent) => {
     const state = get(component, "workStatus", "");
     const type = get(component, "componentCategory", "");
-    const adminState = get(component, "adminState", "");
-    if (adminState === "DECOMMISSIONED") {
+    const decommissionState = get(decommissionable, 
getComponentName(component));
+    const isRecommissionAvailable = get(decommissionState, 
"isComponentRecommissionAvailable");
+    if (isRecommissionAvailable) {
       return (
         <Tooltip message="Decommissioned">
           <FontAwesomeIcon icon={faMinusCircle} className="text-orange" />
@@ -677,7 +679,10 @@ export default function HostsSummary({
               key="decommission"
               onClick={() => {
                 if (!isComponentDecommissionDisable(component)) {
-                  const data = { clusterComponents };
+                  const data = {
+                    clusterComponents,
+                    callback: () => startDecommissionStatusPolling(component),
+                  };
                   setSelectedActionData(
                     component,
                     "decommission",
@@ -707,11 +712,19 @@ export default function HostsSummary({
               key="recommission"
               onClick={() => {
                 if (!isComponentDecommissionDisable(component)) {
+                  const data = {
+                    setAllHostModels,
+                    callback: () => {
+                      loadComponentDecommissionStatus(component);
+                      startDecommissionStatusPolling(component);
+                    },
+                  };
                   setSelectedActionData(
                     component,
                     "recommission",
                     false,
-                    recommission
+                    recommission,
+                    data
                   );
                   setShowConfirmationModal(true);
                 }
@@ -973,8 +986,8 @@ export default function HostsSummary({
             component,
             get(clusterComponents, "items", [])
           ).forEach((cmd: any, index: number) => {
-            // Handle special "Transition To Observer" command for NAMENODE
-            if (get(cmd, "command", "") === "MAKEOBSERVER" && 
+            // Handle special HA transition commands for NAMENODE
+            if (get(cmd, "command", "") === "MAKEOBSERVER" &&
                 getComponentName(component) === "NAMENODE") {
               actions.push(
                 <div
@@ -989,6 +1002,21 @@ export default function HostsSummary({
                   {get(cmd, "label", "")}
                 </div>
               );
+            } else if (get(cmd, "command", "") === "MAKESTANDBY" &&
+                getComponentName(component) === "NAMENODE") {
+              actions.push(
+                <div
+                  key={`transition-standby-${index}`}
+                  className={get(cmd, "disabled", false) ? "disabled-btn" : ""}
+                  onClick={() => {
+                    if (!get(cmd, "disabled", false)) {
+                      transitionToStandby(component);
+                    }
+                  }}
+                >
+                  {get(cmd, "label", "")}
+                </div>
+              );
             } else {
               actions.push(
                 <div
@@ -1022,6 +1050,9 @@ export default function HostsSummary({
       canRunCustomCommands,
       canStartStopServices,
       canToggleComponentMaintenance,
+      setAllHostModels,
+      startDecommissionStatusPolling,
+      loadComponentDecommissionStatus,
     ]
   );
 
@@ -1189,7 +1220,7 @@ export default function HostsSummary({
         },
       },
     ],
-    [getActions, componentActionsMap, openDropdownId]
+    [getActions, componentActionsMap, openDropdownId, decommissionable]
   );
 
   if (loading) {
diff --git a/ambari-web/latest/src/screens/Hosts/actions.tsx 
b/ambari-web/latest/src/screens/Hosts/actions.tsx
index 774d0a081d..8e46d1c3cb 100644
--- a/ambari-web/latest/src/screens/Hosts/actions.tsx
+++ b/ambari-web/latest/src/screens/Hosts/actions.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { capitalize, cloneDeep, get, set, uniq } from "lodash";
+import { capitalize, get, uniq } from "lodash";
 import { HostsApi } from "../../api/hostsApi";
 import {
   doDecommissionRegionServer,
@@ -48,7 +48,6 @@ import {
 import { addDeleteComponentsMap } from "../../Utils/Utility";
 import RecommendationModal from "../../components/RecommendationModal";
 import ConfirmationModal from "../../components/ConfirmationModal";
-import { IHost } from "../../models/host";
 import ConfigsApi from "../../api/configsApi";
 
 export const sendComponentCommand = async (
@@ -459,7 +458,7 @@ const runDecommission = (component: IHostComponent, data?: 
any) => {
   const clusterName = get(component, "clusterName", "");
   switch (svcName) {
     case "HDFS":
-      doDecommission(clusterName, hostName, svcName, "NAMENODE", "DATANODE", 
data);
+      doDecommission(clusterName, hostName, svcName, "NAMENODE", "DATANODE");
       break;
     case "YARN":
       doDecommission(
@@ -467,8 +466,7 @@ const runDecommission = (component: IHostComponent, data?: 
any) => {
         hostName,
         svcName,
         "RESOURCEMANAGER",
-        "NODEMANAGER",
-        data
+        "NODEMANAGER"
       );
       break;
     case "HBASE":
@@ -481,8 +479,7 @@ const doDecommission = async (
   hostName: string,
   serviceName: string,
   componentName: string,
-  slaveType: string,
-  data?: any
+  slaveType: string
 ) => {
   const contextNameString =
     "hosts.host." + slaveType.toLowerCase() + ".decommission";
@@ -498,26 +495,8 @@ const doDecommission = async (
   };
   const response = await HostsApi.decommissionSlave(clusterName, requestData);
   const requestId = get(response, "Requests.id", -1);
-  if((requestId != -1) && data && data.setAllHostModels){
-    data.setAllHostModels((prevModels: IHost[]) => {
-      return prevModels.map((host) => {
-        if (host.hostName === hostName) {
-          const hostModel = cloneDeep(host);
-          const hostComponents = get(hostModel, "hostComponents", []);
-          const updatedComponents = hostComponents.map((hc: IHostComponent) => 
{
-            if (getComponentName(hc) === slaveType) {
-              return { ...hc, adminState: "DECOMMISSIONED" };
-            }
-            return hc;
-          });
-          set(hostModel, "hostComponents", updatedComponents);
-          
-          return hostModel;
-        }
-        return host;
-      });
-    });
-  }
+  // Status is driven by the polled NameNode state (see useDecommissionable),
+  // not an optimistic adminState write here.
   defaultSuccessCallback(requestId);
 };
 
@@ -851,13 +830,15 @@ export const transitionToObserver = async (component: 
IHostComponent) => {
         }
       )}
       successCallback={async () => {
+        // Close the confirmation modal before showing the next modal.
+        modalManager.hide();
         try {
           const context = 
translate("services.service.actions.run.makeObserver.context");
           const response = await HostsApi.transitionToObserver(clusterName, {
             hostName: hostName,
             context: context,
           });
-          
+
           const requestId = get(response, "Requests.id", -1);
           if (requestId !== -1) {
             modalManager.show(
@@ -873,11 +854,77 @@ export const transitionToObserver = async (component: 
IHostComponent) => {
         } catch (error: any) {
           showAlertModal(
             translate("common.error"),
-            translate("services.service.actions.run.makeObserver.error") + 
+            translate("services.service.actions.run.makeObserver.error") +
             (error.message || "")
           );
         }
+      }}
+    />
+  );
+};
+
+export const transitionToStandby = async (component: IHostComponent) => {
+  const clusterName = get(component, "clusterName", "");
+  const hostName = get(component, "hostName", "");
+  const componentName = getComponentName(component);
+
+  if (componentName !== "NAMENODE") {
+    showAlertModal(
+      translate("common.error"),
+      "Transition to Standby is only available for NameNode components."
+    );
+    return;
+  }
+
+  const workStatus = get(component, "workStatus");
+  if (workStatus !== "STARTED") {
+    showAlertModal(
+      translate("common.error"),
+      "NameNode must be in STARTED state to transition to Standby."
+    );
+    return;
+  }
+
+  modalManager.show(
+    <ConfirmationModal
+      isOpen={true}
+      onClose={() => modalManager.hide()}
+      
modalTitle={translate("services.service.actions.run.makeStandby.context")}
+      modalBody={translateWithVariables(
+        "question.sure.makeStandby",
+        {
+          "0": get(component, "displayName", "NameNode"),
+        }
+      )}
+      successCallback={async () => {
+        // Close the confirmation modal before showing the next modal.
         modalManager.hide();
+        try {
+          const context = 
translate("services.service.actions.run.makeStandby.context");
+          const response = await HostsApi.transitionToStandby(clusterName, {
+            hostName: hostName,
+            context: context,
+          });
+
+          const requestId = get(response, "Requests.id", -1);
+          if (requestId !== -1) {
+            modalManager.show(
+              <BackgroundOperations
+                isOpen={true}
+                onClose={() => {
+                  modalManager.hide();
+                }}
+                requestId={requestId}
+              />
+            );
+          }
+        } catch (error: any) {
+          showAlertModal(
+            translate("common.error"),
+            translate("services.service.actions.run.makeStandby.error") +
+            (error.message || "")
+          );
+        }
       }}
     />
   );
diff --git a/ambari-web/latest/src/screens/Hosts/utils.tsx 
b/ambari-web/latest/src/screens/Hosts/utils.tsx
index 966409a603..842d201ac9 100644
--- a/ambari-web/latest/src/screens/Hosts/utils.tsx
+++ b/ambari-web/latest/src/screens/Hosts/utils.tsx
@@ -81,6 +81,12 @@ export const hostComponentCustomCommandMap = {
     context: translate("services.service.actions.run.makeObserver.context"),
     label: translate("services.service.actions.run.makeObserver"),
   },
+  MAKESTANDBY: {
+    action: "makeStandby",
+    customCommand: "MAKESTANDBY",
+    context: translate("services.service.actions.run.makeStandby.context"),
+    label: translate("services.service.actions.run.makeStandby"),
+  },
   IMMEDIATE_STOP_HAWQ_SERVICE: {
     action: "executeHawqCustomCommand",
     customCommand: "IMMEDIATE_STOP_HAWQ_SERVICE",
@@ -213,20 +219,16 @@ export const isClientUsingComponentName = (
   componentName: string,
   serviceComponentInfo: any
 ) => {
-  get(serviceComponentInfo, "items", []).forEach((item: any) => {
-    get(item, "components", []).forEach((component: any) => {
-      if (
-        get(component, "ServiceComponentInfo.component_name", "") ===
-        componentName
-      ) {
-        return (
-          get(component, "ServiceComponentInfo.component_category", "") ===
-          ComponentType.CLIENT
-        );
-      }
-    });
-  });
-  return false;
+  return get(serviceComponentInfo, "items", []).some((service: any) =>
+    get(service, "components", []).some((component: any) => {
+      const info = get(component, "StackServiceComponents", {});
+      return (
+        get(info, "component_name", "") === componentName &&
+        (get(info, "component_category", "") === ComponentType.CLIENT ||
+          get(info, "is_client", false) === true)
+      );
+    })
+  );
 };
 
 export const isSlave = (component: IHostComponent) => {
@@ -521,9 +523,29 @@ export const getCustomCommands = (
   component: IHostComponent,
   clusterComponents: any
 ) => {
-  const commands: string[] = get(component, "customCommands", []);
+  let commands: string[] = get(component, "customCommands", []);
   let customCommands: any[] = [];
 
+  // NAMENODE HA transition actions must reflect the node's current HA state.
+  // Strip all HA-transition commands first, then add back exactly one based on
+  // nnHAState: standby → MAKEOBSERVER, observer → MAKESTANDBY, active → none.
+  if (getComponentName(component) === "NAMENODE") {
+    commands = commands.filter(
+      (command: string) =>
+        command !== "TRANSITION_NAMENODE" &&
+        command !== "MAKEOBSERVER" &&
+        command !== "MAKESTANDBY"
+    );
+    if (get(component, "workStatus", "") === ComponentStatus.STARTED) {
+      const nnHAState = get(component, "nnHAState", "").toLowerCase();
+      if (nnHAState === "standby") {
+        commands = [...commands, "MAKEOBSERVER"];
+      } else if (nnHAState === "observer") {
+        commands = [...commands, "MAKESTANDBY"];
+      }
+    }
+  }
+
   commands.forEach((command: string) => {
     if (
       !isSlave(component) &&
@@ -1094,8 +1116,11 @@ export const getHostComponentsInfo = (
 };
 
 export const serviceActiveComponents = (hostComponents: IHostComponent[]) => {
-  return filter(hostComponents, (component: IHostComponent) =>
-    component.isActive()
+  // Exclude only components whose service is in maintenance, not host (Ember 
parity).
+  return filter(
+    hostComponents,
+    (component: IHostComponent) =>
+      get(component, "service.passiveState", "OFF") !== "ON"
   );
 };
 
diff --git a/ambari-web/latest/src/screens/ServiceConfigs/index.tsx 
b/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
index 7abfef8376..c7964956db 100644
--- a/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
+++ b/ambari-web/latest/src/screens/ServiceConfigs/index.tsx
@@ -16,7 +16,7 @@
  * limitations under the License.
  */
 
-import { useCallback, useContext, useEffect, useRef, useState, useTransition } 
from "react";
+import { useCallback, useContext, useEffect, useMemo, useRef, useState, 
useTransition } from "react";
 import { useBlocker, useLocation, useNavigate, useParams } from 
"react-router-dom";
 import ConfigsApi from "../../api/configsApi";
 import useAuthorizationPolicy from "../../hooks/useAuthorizationPolicy";
@@ -61,6 +61,7 @@ import ConfigsComparator from 
"../ConfigVersions/ConfigsComparator";
 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
 import { faExchangeAlt, faXmark } from "@fortawesome/free-solid-svg-icons";
 import { ServiceContext } from "../../store/ServiceContext";
+import { cachedServiceApi } from "../../api/cachedServiceApi";
 import { serviceNameModelMapping } from "../../constants";
 import useEnhancedConfigs from "../../hooks/useEnhancedConfigs";
 import useHostComponents from "../ClusterWizard/hooks/useHostComponents";
@@ -125,6 +126,19 @@ export default function ServiceConfigs({
   const [configProperties, setConfigProperties] =
     useState<ConfigPropertiesType>({});
   const [propertyValues, setPropertyValues] = useState<any>({});
+
+  // This service's non-default groups; memoized for a stable reference across 
renders.
+  const overrideConfigGroupNames = useMemo(
+    () =>
+      propertyValues?.items
+        ?.filter(
+          (item: any) =>
+            item.group_name !== "Default" && item.service_name === serviceName
+        )
+        .map((item: any) => item.group_name) || [],
+    [propertyValues, serviceName]
+  );
+
   const [defaultVersionNumber, setDefaultVersionNumber] = useState<string>();
   const [selectedVersion, setSelectedVersion] = useState<string>();
   const [configGroup, setConfigGroup] = useState<string>("Default");
@@ -1443,6 +1457,8 @@ export default function ServiceConfigs({
   }
 
   async function saveConfigs() {
+    // Disable submit immediately to prevent double-submission while the async 
save is in flight.
+    setIsSubmitDisabled(true);
     const saved = await saveStepConfigs();
     if (!saved) {
       return false;
@@ -1453,6 +1469,10 @@ export default function ServiceConfigs({
     setShowUnsaveChangesModal(false);
     setServiceConfigVersionNote("");
     await getPropertiesValues();
+    // Re-poll host components so stale_configs (and therefore the Sidebar
+    // restart icon / RestartWarning) update immediately after a save instead
+    // of waiting for the next 5s poll cycle.
+    cachedServiceApi.fetchAllServiceComponents(clusterName);
     if (blocker.state === "blocked") {
       blocker.proceed();
     }
@@ -1682,13 +1702,13 @@ export default function ServiceConfigs({
         isOpen={showAddToGroupModal}
         onClose={() => setShowAddToGroupModal(false)}
         serviceName={serviceName}
-        configGroupNames={
-          propertyValues?.items
-            ?.filter((item: any) => item.group_name !== "Default")
-            .map((item: any) => item.group_name) || []
-        }
+        configGroupNames={overrideConfigGroupNames}
         onConfigGroupSelect={(configGroup: string) => {
           setConfigGroup(configGroup);
+          setIsComparing(false);
+          setVersionCompared("");
+          // Refresh the dropdown's group list so a newly created/selected 
group is available.
+          setRefetchTrigger((prev) => prev + 1);
         }}
         setShowManageConfigGroupModal={setShowManageConfigGroupModal}
       />
diff --git 
a/ambari-web/latest/src/screens/Services/highAvailibility/nameNode/Step3.tsx 
b/ambari-web/latest/src/screens/Services/highAvailibility/nameNode/Step3.tsx
index 712497f65f..8e7289659f 100644
--- a/ambari-web/latest/src/screens/Services/highAvailibility/nameNode/Step3.tsx
+++ b/ambari-web/latest/src/screens/Services/highAvailibility/nameNode/Step3.tsx
@@ -80,6 +80,7 @@ function Step3() {
   } = useContext(EnableHighAvailibilityContext);
   const { clusterName, services } = useContext(AppContext);
   const serverConfigDataRef = useRef<any>([]);
+  const configTagsLoaded = useRef(false);
   const [clusterHostComponentsMapping, setClusterHostComponentsMapping] =
     useState<any>([]);
   const [stepConfigs, setStepConfigs] = useState<any>(null);
@@ -266,10 +267,12 @@ function Step3() {
   }
   useEffect(() => {
     if (
+      !configTagsLoaded.current &&
       clusterHostComponentsMapping.length &&
       configsData &&
-      !isEmpty(configsData) 
+      !isEmpty(configsData)
     ) {
+      configTagsLoaded.current = true;
       loadConfigsTags();
     }
   }, [clusterHostComponentsMapping, configsData]);
diff --git a/ambari-web/latest/src/screens/messages.ts 
b/ambari-web/latest/src/screens/messages.ts
index 37d4a17349..4484fb0cf5 100644
--- a/ambari-web/latest/src/screens/messages.ts
+++ b/ambari-web/latest/src/screens/messages.ts
@@ -451,7 +451,8 @@ const messages: any = {
     'question.sure.regenerateKeytab.service': 'Are you sure you want to 
regenerate keytab file operations for a {0} service?',
     'question.sure.regenerateKeytab.host': 'Are you sure you want to 
regenerate keytab file operations for a {0} host?',
     'question.sure.makeObserver': 'Are you sure you want to transition {0} to 
Observer mode?',
-  
+    'question.sure.makeStandby': 'Are you sure you want to transition {0} to 
Standby mode?',
+
     'popup.highlight':'click to highlight',
     'popup.confirmation.commonHeader':'Confirmation',
     'popup.confirmation.refreshYarnQueues.body':'It is strongly recommended to 
<strong>Refresh Yarn Queues</strong> after making a change to the capacity 
scheduler configuration. Would you like to proceed?',
@@ -2155,6 +2156,9 @@ const messages: any = {
     'services.service.actions.run.makeObserver.context':'Transition To 
Observer',
     'services.service.actions.run.makeObserver':'Transition To Observer',
     'services.service.actions.run.makeObserver.error':'Error during transition 
to observer: ',
+    'services.service.actions.run.makeStandby.context':'Transition To Standby',
+    'services.service.actions.run.makeStandby':'Transition To Standby',
+    'services.service.actions.run.makeStandby.error':'Error during transition 
to standby: ',
     'services.service.actions.run.yarnRefreshQueues.title':'Refresh Queues 
ResourceManager',
     'services.service.actions.run.yarnRefreshQueues.menu':'Refresh YARN 
Capacity Scheduler',
     'services.service.actions.run.yarnRefreshQueues.context':'Refresh YARN 
Capacity Scheduler',
diff --git a/ambari-web/latest/src/store/ServiceContext.tsx 
b/ambari-web/latest/src/store/ServiceContext.tsx
index 6e1a932c72..a5a6ab710b 100644
--- a/ambari-web/latest/src/store/ServiceContext.tsx
+++ b/ambari-web/latest/src/store/ServiceContext.tsx
@@ -130,16 +130,25 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
   const [polledHostComponentsData, setPolledHostComponentsData] = 
useState<any>(
     {}
   );
+  // Keep a ref in sync so fetchOptimizedMaintenanceAndStaleData always reads 
the
+  // latest polled/WebSocket-updated host component data (avoids stale 
closure).
+  const polledHostComponentsDataRef = useRef(polledHostComponentsData);
+  useEffect(() => {
+    polledHostComponentsDataRef.current = polledHostComponentsData;
+  }, [polledHostComponentsData]);
   const [quickLinksMapWithAPIResponse, setQuickLinksMapWithAPIResponse] =
     useState<any>(null);
 
   const [masterSlaveClientsData, setMasterSlaveClientsData] = 
useState<any>({});
   const [serviceStatesData, setServiceStatesData] = useState<Map<string, 
any>>(new Map());
 
-  const { clusterName, parsedSocketMessages } = useContext(AppContext);
+  const { clusterName, parsedSocketMessages, alertSummary: socketAlertSummary 
} = useContext(AppContext);
 
-  // Alert data from AlertsContext, used to calculate service alert counts 
without a separate /alerts call
-  const { alertSummary, alertDefinitions } = useAlerts();
+  // Boot-fetched summary from AlertsContext; prefer the synchronous socket 
summary when available.
+  // socketAlertSummary is set in the same render cycle as the socket arrival 
(context.tsx handler),
+  // avoiding the extra render cycle that parsedSocketMessages → AlertsContext 
useEffect requires.
+  const { alertSummary: bootAlertSummary, alertDefinitions } = useAlerts();
+  const alertSummary = socketAlertSummary ?? bootAlertSummary;
 
   // Refs to avoid stale closures in subscriber callback and prevent useEffect 
re-runs
   const alertSummaryRef = useRef(alertSummary);
@@ -150,6 +159,33 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
   useEffect(() => { alertDefinitionsRef.current = alertDefinitions; }, 
[alertDefinitions]);
   useEffect(() => { allServiceModelsRef.current = allServiceModels; }, 
[allServiceModels]);
 
+  // REACTIVE ALERT UPDATE: When alertSummary changes (from synchronous socket 
path or boot load),
+  // immediately recompute service alert counts and push to serviceStatesData.
+  // Mirrors Ember's alertDefinitionSummaryMapper which runs synchronously on 
socket events,
+  // instantly updating service.alertsCount + service.hasCriticalAlerts.
+  useEffect(() => {
+    if (!alertSummary || !alertDefinitions?.length) return;
+
+    const serviceAlertCounts = computeServiceAlertCounts(alertSummary, 
alertDefinitions);
+
+    setServiceStatesData((prev) => {
+      const updated = new Map(prev);
+      serviceAlertCounts.forEach(({ alertsCount, hasCriticalAlerts }, 
serviceName) => {
+        const existing = updated.get(serviceName);
+        if (existing) {
+          updated.set(serviceName, { ...existing, alertsCount, 
hasCriticalAlerts });
+        }
+      });
+      updated.forEach((data, serviceName) => {
+        if (!serviceAlertCounts.has(serviceName)) {
+          updated.set(serviceName, { ...data, alertsCount: 0, 
hasCriticalAlerts: false });
+        }
+      });
+      centralizedServiceStateApi.setDerivedServiceStates(updated);
+      return updated;
+    });
+  }, [alertSummary, alertDefinitions]);
+
   const isOnClusterAdminPage = location.pathname.includes('/main/admin/');
 
   // const [quicklinks, setQuicklinks] = useState<Map<string, any>>(new Map());
@@ -229,6 +265,10 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
         }
         merged[key] = incoming;
       }
+      // Keep the ref in sync synchronously so multiple updateRegistry calls 
within
+      // the same tick each clone the freshest models instead of a stale 
snapshot,
+      // which previously clobbered isRestartRequiredForService.
+      allServiceModelsRef.current = merged;
       return merged;
     });
   };
@@ -250,18 +290,23 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
    */
   const fetchOptimizedMaintenanceAndStaleData = async () => {
     try {
-      // USE CACHED DATA instead of making new API call
-      // CachedServiceApi is already polling this same endpoint
-      const cachedData = cachedServiceApi.getAllComponentData();
-
-      if (!cachedData) {
-        // If no cached data yet, fetch it once (will be cached for subsequent 
calls)
+      // Prefer the reactive polledHostComponentsData state: the WebSocket
+      // /events/hostcomponents handler updates it immediately with fresh
+      // stale_configs, whereas cachedServiceApi.getAllComponentData() is only
+      // refreshed by the 5s HTTP poll. Reading the cache here meant the 
Sidebar
+      // restart icon lagged until the next poll after a config save.
+      const polledData = polledHostComponentsDataRef.current;
+      const responseData =
+        polledData?.items?.length
+          ? polledData
+          : cachedServiceApi.getAllComponentData();
+
+      if (!responseData?.items?.length) {
+        // If no data yet, fetch it once (will be cached for subsequent calls)
         await cachedServiceApi.fetchAllServiceComponents(clusterName);
         return;
       }
 
-      const responseData = cachedData;
-
       if (!isEmpty(responseData)) {
         
         // Process stale configs for all services following Ember logic
@@ -297,9 +342,12 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
           serviceStaleStatus[serviceName] = staleHosts.length > 0;
         });
 
-        // Update service models with stale configs only (maintenance mode 
handled separately)
+        // Update service models with stale configs only (maintenance mode 
handled separately).
+        // Use the ref (not the closed-over allServiceModels) so we always 
read the latest
+        // models — this closure captures a stale value otherwise, preventing 
the
+        // Sidebar restart icon from updating after configs are saved.
         let hasUpdates = false;
-        const updatedModels = cloneDeep(allServiceModels);
+        const updatedModels = cloneDeep(allServiceModelsRef.current);
 
         Object.entries(serviceStaleStatus).forEach(([serviceName, 
hasStaleConfigs]) => {
           const serviceModelKey = serviceNameModelMapping[serviceName];
@@ -345,8 +393,10 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
           passiveStateMap[service.ServiceInfo.service_name] = 
service.ServiceInfo.maintenance_state;
         });
 
+        // Use the ref (not the closed-over allServiceModels) so this clone is 
not stale and
+        // does not clobber isRestartRequiredForService written by 
fetchOptimizedMaintenanceAndStaleData.
         let hasMaintenanceUpdates = false;
-        const updatedModels = cloneDeep(allServiceModels);
+        const updatedModels = cloneDeep(allServiceModelsRef.current);
 
         Object.entries(passiveStateMap).forEach(([serviceName, 
maintenanceState]) => {
           const serviceModelKey = serviceNameModelMapping[serviceName];
diff --git a/ambari-web/latest/src/store/context.tsx 
b/ambari-web/latest/src/store/context.tsx
index 8f25ddf1bd..cf904d2334 100644
--- a/ambari-web/latest/src/store/context.tsx
+++ b/ambari-web/latest/src/store/context.tsx
@@ -84,6 +84,8 @@ interface AppContextProps {
   setIsPatchUpgrade?: (isPatch: boolean) => void;
   upgradeVersionDisplayName?: string;
   setUpgradeVersionDisplayName?: (name: string) => void;
+  upgradeAssociatedVersion?: string;
+  setUpgradeAssociatedVersion?: (version: string) => void;
   upgradeIsFinalizeItem: boolean;
   setUpgradeIsFinalizeItem: (isFinalize: boolean) => void;
   userUrl?: string;
@@ -122,6 +124,9 @@ interface AppContextProps {
   serviceCheckSupportedMap: Record<string, boolean>;
   stackVersion: any;
   stackVersionList: any[];
+  // Alert summary updated synchronously from /events/alerts socket handler.
+  // Mirrors Ember's alertSummaryMapper: sidebar/service counts update in the 
same render cycle.
+  alertSummary: { alerts_summary_grouped: any[] } | null;
 }
 
 type BackgroundRequestPage = {
@@ -163,6 +168,8 @@ export const AppContext = createContext<AppContextProps>({
   setIsPatchUpgrade: () => {},
   upgradeVersionDisplayName: "",
   setUpgradeVersionDisplayName: () => {},
+  upgradeAssociatedVersion: "",
+  setUpgradeAssociatedVersion: () => {},
   sessionExists: false,
   sessionsValidated: false,
   clusterState: {},
@@ -198,6 +205,7 @@ export const AppContext = createContext<AppContextProps>({
   serviceCheckSupportedMap: {},
   stackVersion: undefined,
   stackVersionList: [],
+  alertSummary: null,
 });
 
 export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
@@ -210,6 +218,7 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
   const [initializationError, setInitializationError] = useState<string | 
null>(null);
   const [initializationAttempt, setInitializationAttempt] = useState(0);
   const [parsedSocketMessages, setParsedSocketMessages] = useState<any[]>([]);
+  const [alertSummary, setAlertSummary] = useState<{ alerts_summary_grouped: 
any[] } | null>(null);
   const [clusterName, setClusterName] = useState<string>("");
   const [isKerberosEnabled, setIsKerberosEnabled] = useState(false);
   const [cluster, setCluster] = useState<any>({});
@@ -224,6 +233,8 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
   const [isPatchUpgrade, setIsPatchUpgrade] = useState<boolean>(false);
   const [upgradeVersionDisplayName, setUpgradeVersionDisplayName] =
     useState<string>("");
+  const [upgradeAssociatedVersion, setUpgradeAssociatedVersion] =
+    useState<string>("");
   const [currentStackVersion, setCurrentStackVersion] = useState<string>("");
   const [ambariProperties, setAmbariProperties] = useState({});
   const [ambariServerVersion, setAmbariServerVersion] = useState("");
@@ -531,10 +542,18 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
         `items[${lastItemIndex}].Upgrade.direction`,
         "UPGRADE"
       );
+      const associatedVersion = get(
+        response,
+        `items[${lastItemIndex}].Upgrade.associated_version`,
+        ""
+      );
+      const hasActiveUpgrade =
+        upgradeState !== "NOT_REQUIRED" && upgradeState !== "COMPLETED";
       setUpgradeDirection(upgradeDirection);
       setUpgradeId(upgradeId);
       setUpgradeState(upgradeState);
       setUpgradeSuspend(upgradeSuspend);
+      setUpgradeAssociatedVersion(hasActiveUpgrade ? associatedVersion : "");
 
       const persistedUpgradeState = await Promise.allSettled([
         ClusterApi.getPersistData("isPatchUpgrade"),
@@ -703,6 +722,15 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
                 )
               ));
             }
+            // Mirrors Ember's alertSummaryMapper: update summary 
synchronously in the same
+            // render cycle so sidebar alert counts reflect the socket push 
immediately.
+            if (parsedMessage.summaries) {
+              const clusterId = parsedMessage.clusterId || 
Object.keys(parsedMessage.summaries)[0];
+              const clusterSummaries = parsedMessage.summaries[clusterId];
+              if (clusterSummaries) {
+                setAlertSummary({ alerts_summary_grouped: 
Object.values(clusterSummaries) });
+              }
+            }
             setParsedSocketMessages((current) => [parsedMessage, 
...current].slice(0, 200));
           } catch {
             console.error(`Ambari ignored a malformed STOMP message from 
${destination}.`);
@@ -796,6 +824,8 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
         setIsPatchUpgrade,
         upgradeVersionDisplayName,
         setUpgradeVersionDisplayName,
+        upgradeAssociatedVersion,
+        setUpgradeAssociatedVersion,
         userUrl,
         sessionExists: true,
         sessionsValidated: true,
@@ -831,6 +861,7 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
         serviceCheckSupportedMap,
         stackVersion,
         stackVersionList,
+        alertSummary,
       }}
     >
       {children}


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

Reply via email to