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

sandeepk318 pushed a commit to branch frontend-refactor
in repository https://gitbox.apache.org/repos/asf/ambari.git


The following commit(s) were added to refs/heads/frontend-refactor by this push:
     new f54410e35f AMBARI-26636 : Ambari Web React: Consolidate ServiceContext 
polling loops and fix stale UI state, stuck test-connection requests (#4178)
f54410e35f is described below

commit f54410e35f153bd7a7ba53ef055b4260a5cdbfbe
Author: Himanshu Maurya <[email protected]>
AuthorDate: Tue Aug 25 21:29:42 2026 +0530

    AMBARI-26636 : Ambari Web React: Consolidate ServiceContext polling loops 
and fix stale UI state, stuck test-connection requests (#4178)
---
 ambari-web/latest/src/api/cachedServiceApi.ts      | 185 ++------
 .../latest/src/api/centralizedServiceStateApi.ts   |  13 +
 ambari-web/latest/src/api/quicklinksApi.ts         |  35 +-
 .../latest/src/hooks/useHDFSConfigUpdater.ts       |   6 +-
 .../latest/src/hooks/useHbaseConfigUpdater.ts      |   4 +
 .../latest/src/hooks/usePinotConfigUpdater.ts      |   4 +
 .../latest/src/hooks/useRangerConfigUpdater.ts     |   4 +
 .../latest/src/hooks/useSpark3ConfigUpdater.ts     |   4 +
 .../latest/src/hooks/useTrinoConfigUpdater.tsx     |   4 +
 .../latest/src/hooks/useYarnConfigUpdater.ts       |   4 +
 ambari-web/latest/src/layout/Dashboard.tsx         |   2 +-
 .../src/screens/CommonConfigs/TestConnection.tsx   |  32 +-
 ambari-web/latest/src/screens/Hosts/HostsList.tsx  |  18 +-
 ambari-web/latest/src/screens/Hosts/index.tsx      |  14 +-
 ambari-web/latest/src/screens/Services/Actions.tsx |   5 +-
 .../latest/src/screens/Services/FlumeSummary.tsx   |   4 +-
 .../latest/src/screens/Services/RestartWarning.tsx |  83 ++--
 ambari-web/latest/src/store/ServiceContext.tsx     | 471 ++++++++++++++-------
 ambari-web/latest/src/store/context.tsx            |  32 --
 19 files changed, 506 insertions(+), 418 deletions(-)

diff --git a/ambari-web/latest/src/api/cachedServiceApi.ts 
b/ambari-web/latest/src/api/cachedServiceApi.ts
index 7ee76b75f8..2488024ee3 100644
--- a/ambari-web/latest/src/api/cachedServiceApi.ts
+++ b/ambari-web/latest/src/api/cachedServiceApi.ts
@@ -17,19 +17,17 @@
  */
 
 import { ServiceApi } from "./serviceApi";
-import { serviceCache } from "../Utils/cacheUtils";
 
 /**
  * Centralized Service Component API Manager
- * Similar to Ember.js approach - makes one consolidated API call for all 
components
- * and provides cached data to individual service hooks
+ * Mirrors Ember's updateServiceMetric pattern - makes one consolidated API 
call
+ * and provides data to all consumers. Polling is handled by usePolling in 
ServiceContext.
  */
 class CachedServiceApiManager {
   private static instance: CachedServiceApiManager;
-  private isPolling = false;
-  private pollingInterval: NodeJS.Timeout | null = null;
-  private subscribers = new Set<(data: any) => void>();
   private pendingRequest: Promise<any> | null = null;
+  private lastData: any = null;
+  private subscribers = new Set<(data: any) => void>();
 
   static getInstance(): CachedServiceApiManager {
     if (!CachedServiceApiManager.instance) {
@@ -39,62 +37,52 @@ class CachedServiceApiManager {
   }
 
   /**
-   * Subscribe to component data updates
+   * Subscribe to component data updates - notified whenever 
fetchAllServiceComponents
+   * returns fresh data, regardless of which caller initiated the request.
+   * If data is already available, immediately notify the new subscriber.
    */
   subscribe(callback: (data: any) => void): () => void {
     this.subscribers.add(callback);
+    if (this.lastData) {
+      callback(this.lastData);
+    }
     return () => this.subscribers.delete(callback);
   }
 
-  /**
-   * Notify all subscribers of data updates
-   */
   private notifySubscribers(data: any): void {
-    this.subscribers.forEach(callback => callback(data));
+    this.subscribers.forEach(cb => cb(data));
   }
 
   /**
-   * Get cached component data for a specific service
+   * Get all component data (last fetched)
    */
-  getServiceComponentData(serviceName: string): any {
-    return serviceCache.get(`components_${serviceName.toLowerCase()}`);
+  getAllComponentData(): any {
+    return this.lastData;
   }
 
   /**
-   * Get all cached component data
+   * Get component data for a specific service
    */
-  getAllComponentData(): any {
-    return serviceCache.get('all_components_data');
+  getServiceComponentData(serviceName: string): any {
+    if (!this.lastData?.items) return null;
+    return this.lastData.items.filter(
+      (item: any) => item.ServiceComponentInfo?.service_name === serviceName
+    );
   }
 
   /**
-   * Centralized API call for all service components
-   * Similar to Ember.js approach - one call for all services
-   * REQUEST DEDUPLICATION: If a request is already in progress, return the 
pending promise
+   * Centralized API call for all service components.
+   * No caching - always makes a real API call (like Ember).
+   * REQUEST DEDUPLICATION: If a request is already in progress, return the 
pending promise.
    */
-  async fetchAllServiceComponents(clusterName: string, forceRefresh: boolean = 
false): Promise<any> {
-    const cacheKey = 'all_components_data';
-
-    // Check cache first (unless force refresh)
-    if (!forceRefresh) {
-      const cachedData = serviceCache.get(cacheKey);
-      if (cachedData) {
-        return cachedData;
-      }
-    }
-
-    // REQUEST DEDUPLICATION: If a request is already pending, return that 
promise
-    // This prevents multiple simultaneous API calls
+  async fetchAllServiceComponents(clusterName: string): Promise<any> {
     if (this.pendingRequest) {
       return this.pendingRequest;
     }
 
     try {
-
-      // OPTIMIZED: Use the same comprehensive fields as the main 
ServiceContext for consistency
       const fields = 
`ServiceComponentInfo/service_name,host_components/HostRoles/display_name,host_components/HostRoles/host_name,host_components/HostRoles/public_host_name,host_components/HostRoles/state,host_components/HostRoles/maintenance_state,host_components/HostRoles/stale_configs,host_components/HostRoles/ha_state,host_components/HostRoles/desired_admin_state,host_components/metrics/jvm/memHeapUsedM,host_components/metrics/jvm/HeapMemoryMax,host_components/metrics/jvm/HeapMemory
 [...]
 
-      // Set pending request to prevent duplicate calls
       this.pendingRequest = 
ServiceApi.getAllServiceComponentsListAndInitialMetrics(
         clusterName,
         fields
@@ -103,19 +91,10 @@ class CachedServiceApiManager {
       const response = await this.pendingRequest;
 
       if (response?.data?.items) {
-
-        // Cache the consolidated data
-        serviceCache.set(cacheKey, response.data, 30000); // 30 second TTL
-
-        // Also cache by individual service for quick access
-        const serviceGroups = 
this.groupComponentsByService(response.data.items);
-        Object.entries(serviceGroups).forEach(([serviceName, data]) => {
-          serviceCache.set(`components_${serviceName.toLowerCase()}`, data, 
30000);
-        });
-
-        // Notify subscribers immediately
+        this.lastData = response.data;
+        // Notify all subscribers (including ServiceContext) so state updates 
flow
+        // regardless of which caller initiated this fetch
         this.notifySubscribers(response.data);
-
         return response.data;
       }
 
@@ -124,111 +103,10 @@ class CachedServiceApiManager {
       console.error('Error fetching service components:', error);
       return null;
     } finally {
-      // Clear pending request when done (success or error)
       this.pendingRequest = null;
     }
   }
 
-  /**
-   * Group component data by service name
-   */
-  private groupComponentsByService(items: any[]): Record<string, any[]> {
-    const groups: Record<string, any[]> = {};
-    
-    items.forEach(item => {
-      const serviceName = item.ServiceComponentInfo?.service_name;
-      if (serviceName) {
-        if (!groups[serviceName]) {
-          groups[serviceName] = [];
-        }
-        groups[serviceName].push(item);
-      }
-    });
-    
-    return groups;
-  }
-
-  /**
-   * Start centralized polling - similar to Ember.js approach
-   * Uses timeout-based polling to prevent overlapping requests
-   */
-  startPolling(clusterName: string, intervalMs: number = 5000): void {
-    if (this.isPolling) return;
-
-    this.isPolling = true;
-
-    // Timeout-based polling to prevent overlapping requests
-    const poll = async () => {
-      if (!this.isPolling) return;
-
-      try {
-        await this.fetchAllServiceComponents(clusterName);
-      } catch (error) {
-        console.error('Polling error:', error);
-      } finally {
-        // Schedule next poll ONLY after current request completes
-        if (this.isPolling) {
-          this.pollingInterval = setTimeout(poll, intervalMs);
-        }
-      }
-    };
-
-    // Start initial poll
-    poll();
-  }
-
-  /**
-   * Pause polling without clearing the interval
-   * Useful for temporarily stopping polling on specific pages
-   */
-  pausePolling(): void {
-    if (this.pollingInterval) {
-      clearTimeout(this.pollingInterval);
-      this.pollingInterval = null;
-    }
-  }
-
-  /**
-   * Resume polling after it was paused
-   */
-  resumePolling(clusterName?: string, intervalMs: number = 5000): void {
-    if (this.isPolling && !this.pollingInterval && clusterName) {
-      const poll = async () => {
-        if (!this.isPolling) return;
-
-        try {
-          await this.fetchAllServiceComponents(clusterName);
-        } catch (error) {
-          console.error('Polling error:', error);
-        } finally {
-          if (this.isPolling) {
-            this.pollingInterval = setTimeout(poll, intervalMs);
-          }
-        }
-      };
-
-      poll();
-    }
-  }
-
-  /**
-   * Stop polling completely
-   */
-  stopPolling(): void {
-    if (this.pollingInterval) {
-      clearTimeout(this.pollingInterval);
-      this.pollingInterval = null;
-    }
-    this.isPolling = false;
-  }
-
-  /**
-   * Check if specific service data is available and fresh
-   */
-  hasServiceData(serviceName: string): boolean {
-    return serviceCache.has(`components_${serviceName.toLowerCase()}`);
-  }
-
   /**
    * Get metrics for a specific service component
    */
@@ -236,17 +114,10 @@ class CachedServiceApiManager {
     const serviceData = this.getServiceComponentData(serviceName);
     if (!serviceData) return null;
 
-    return serviceData.find((item: any) => 
+    return serviceData.find((item: any) =>
       item.ServiceComponentInfo?.component_name === componentName
     );
   }
-
-  /**
-   * Clear all cached data
-   */
-  clearCache(): void {
-    serviceCache.clear();
-  }
 }
 
 // Export singleton instance
diff --git a/ambari-web/latest/src/api/centralizedServiceStateApi.ts 
b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
index 29b1da8c62..c52b31de0d 100644
--- a/ambari-web/latest/src/api/centralizedServiceStateApi.ts
+++ b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
@@ -21,6 +21,7 @@ import { ambariApi } from "./config/axiosConfig";
 interface ServiceStateData {
   serviceName: string;
   state: string;
+  maintenance_state: string;
   alertsCount: number;
   hasCriticalAlerts: boolean;
 }
@@ -121,12 +122,14 @@ class CentralizedServiceStateApi {
         response.data.items?.forEach((service: any) => {
           const serviceName = service.ServiceInfo.service_name;
           const state = service.ServiceInfo.state;
+          const maintenance_state = service.ServiceInfo.maintenance_state;
 
           const alertData = serviceAlertCounts.get(serviceName) || { 
alertsCount: 0, hasCriticalAlerts: false };
 
           newCache.set(serviceName, {
             serviceName,
             state,
+            maintenance_state,
             alertsCount: alertData.alertsCount,
             hasCriticalAlerts: alertData.hasCriticalAlerts,
           });
@@ -183,6 +186,16 @@ class CentralizedServiceStateApi {
     this.subscribers.forEach(callback => callback(this.cache));
   }
 
+  /**
+   * Set service state data directly from derived components data.
+   * Allows ServiceContext to populate the cache without a separate /services 
API call.
+   */
+  setDerivedServiceStates(data: Map<string, ServiceStateData>): void {
+    this.cache = data;
+    this.lastFetchTime = Date.now();
+    this.notifySubscribers();
+  }
+
   /**
    * Clear cache (useful for testing or forced refresh)
    */
diff --git a/ambari-web/latest/src/api/quicklinksApi.ts 
b/ambari-web/latest/src/api/quicklinksApi.ts
index 99f82e07d4..31ffe65661 100644
--- a/ambari-web/latest/src/api/quicklinksApi.ts
+++ b/ambari-web/latest/src/api/quicklinksApi.ts
@@ -18,14 +18,37 @@
 
 import { ambariApi } from "./config/axiosConfig";
 
+// The quicklinks config is immutable stack metadata (link templates, port/host
+// property names, protocol checks); the actual port/host VALUES come from a
+// separate service_config_versions call. Caching it per (stack, version,
+// service) for the session avoids refetching on every Quicklinks open / 
service
+// switch, which was making service switches feel slow.
+const quicklinksConfigCache = new Map<string, any>();
+const quicklinksConfigInflight = new Map<string, Promise<any>>();
+
 export const QuicklinksApi = {
     getQuicklinks: async (stackVersion: string,stackName:string, serviceName: 
string) => {
-        const url = 
`/stacks/${stackName}/versions/${stackVersion}/services/${serviceName}/quicklinks?QuickLinkInfo/default=true&fields=*&_=${Date.now()}`;
-        const response = await ambariApi.request({
-            url: url,
-            method: "GET",
-        });
-        return response;
+        const cacheKey = `${stackName}::${stackVersion}::${serviceName}`;
+        const cached = quicklinksConfigCache.get(cacheKey);
+        if (cached) {
+            return cached;
+        }
+        const pending = quicklinksConfigInflight.get(cacheKey);
+        if (pending) {
+            return pending;
+        }
+        const url = 
`/stacks/${stackName}/versions/${stackVersion}/services/${serviceName}/quicklinks?QuickLinkInfo/default=true&fields=*`;
+        const promise = ambariApi
+            .request({ url, method: "GET" })
+            .then((response) => {
+                quicklinksConfigCache.set(cacheKey, response);
+                return response;
+            })
+            .finally(() => {
+                quicklinksConfigInflight.delete(cacheKey);
+            });
+        quicklinksConfigInflight.set(cacheKey, promise);
+        return promise;
     },
     getPublicHostNames: async (clusterName: string, hostNames: string[]) => {
         const hosts = hostNames.map(encodeURIComponent).join(",");
diff --git a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
index 2a37d84c2a..74a4cb9b39 100644
--- a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
@@ -306,6 +306,10 @@ export const useHDFSConfigUpdater = () => {
     if (!allServiceModels["hdfs"]) {
       return;
     }
+
+    if (!items || items.length === 0) {
+      return;
+    }
     const isHAEnabled = allServiceModels["hdfs"]?.isNameNodeHaEnabled;
 
     const currentConfig = cloneDeep(allServiceModels["hdfs"]);
@@ -1162,7 +1166,7 @@ export const useHDFSConfigUpdater = () => {
 
   useEffect(() => {
     if (isEmpty(masterSlaveClientsData) && clusterName) {
-      cachedServiceApi.fetchAllServiceComponents(clusterName, true);
+      cachedServiceApi.fetchAllServiceComponents(clusterName);
     }
     findMasterSlaveClientComponents();
   }, [masterSlaveClientsData, clusterName]);
diff --git a/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
index 2f5e3de754..32192d2437 100644
--- a/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
@@ -117,6 +117,10 @@ export const useHbaseConfigUpdater = () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["hbase"]);
     //const hdfsServiceObj = currentConfig.getServiceObject();
     let masterComponents: any[] = [];
diff --git a/ambari-web/latest/src/hooks/usePinotConfigUpdater.ts 
b/ambari-web/latest/src/hooks/usePinotConfigUpdater.ts
index 7ddbcf07a4..f720d59eb0 100644
--- a/ambari-web/latest/src/hooks/usePinotConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/usePinotConfigUpdater.ts
@@ -105,6 +105,10 @@ export const usePinotConfigUpdater = () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["pinot"]);
     let masterComponents: any[] = [];
     let slaveComponents: any[] = [];
diff --git a/ambari-web/latest/src/hooks/useRangerConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useRangerConfigUpdater.ts
index e2a23e9665..be43c5c6ab 100644
--- a/ambari-web/latest/src/hooks/useRangerConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useRangerConfigUpdater.ts
@@ -103,6 +103,10 @@ export const useRangerConfigUpdater = () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["ranger"]);
 
     let masterComponents: any[] = [];
diff --git a/ambari-web/latest/src/hooks/useSpark3ConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useSpark3ConfigUpdater.ts
index 391166c752..1fda7b1a52 100644
--- a/ambari-web/latest/src/hooks/useSpark3ConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useSpark3ConfigUpdater.ts
@@ -200,6 +200,10 @@ export const useSpark3ConfigUpdater =  () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["spark3"]);
     let masterComponents: any[] = [];
     let slaveConponents: any[] = [];
diff --git a/ambari-web/latest/src/hooks/useTrinoConfigUpdater.tsx 
b/ambari-web/latest/src/hooks/useTrinoConfigUpdater.tsx
index a6bed45340..285d352509 100644
--- a/ambari-web/latest/src/hooks/useTrinoConfigUpdater.tsx
+++ b/ambari-web/latest/src/hooks/useTrinoConfigUpdater.tsx
@@ -207,6 +207,10 @@ export const useTrinoConfigUpdater = () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["trino"]);
     let masterComponents: any[] = [];
     let slaveConponents: any[] = [];
diff --git a/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts 
b/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
index 816ae3e71b..52475e67d4 100644
--- a/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
@@ -124,6 +124,10 @@ export const useYarnConfigUpdater = () => {
       return;
     }
 
+    if (!items || items.length === 0) {
+      return;
+    }
+
     const currentConfig = cloneDeep(allServiceModels["yarn"]);
     let masterComponents: any[] = [];
     let slaveComponents: any[] = [];
diff --git a/ambari-web/latest/src/layout/Dashboard.tsx 
b/ambari-web/latest/src/layout/Dashboard.tsx
index 63cb1df74f..1d428d40f2 100644
--- a/ambari-web/latest/src/layout/Dashboard.tsx
+++ b/ambari-web/latest/src/layout/Dashboard.tsx
@@ -366,7 +366,7 @@ const DashboardLayout = () => {
                   )}
                 </div>
               )}
-            <div style={{ paddingBottom: '80px' }}>
+            <div style={{ paddingBottom: location.pathname.includes('/views/') 
? '0' : '80px' }}>
               <Outlet></Outlet>
             </div>
           </div>
diff --git a/ambari-web/latest/src/screens/CommonConfigs/TestConnection.tsx 
b/ambari-web/latest/src/screens/CommonConfigs/TestConnection.tsx
index fc5f54cddc..722eb0c897 100644
--- a/ambari-web/latest/src/screens/CommonConfigs/TestConnection.tsx
+++ b/ambari-web/latest/src/screens/CommonConfigs/TestConnection.tsx
@@ -175,7 +175,7 @@ export default function TestConnection({
   const [errorMessage, setErrorMessage] = useState<any>(null);
   const [showErrorMessage, setShowErrorMessage] = useState(false);
 
-  const { services, ambariProperties, clusterName } = useContext(AppContext);
+  const { ambariProperties, clusterName, isClusterInstalled } = 
useContext(AppContext);
 
   const setSafeErrorMessage = (diagnostics: Record<string, unknown>) => {
     setErrorMessage(
@@ -266,10 +266,6 @@ export default function TestConnection({
     }
   }, [taskID, requestId, resumePolling]);
 
-  const installedServicesInCluster = services.map(
-    (service) => service.ServiceInfo.service_name
-  );
-
   const isDBACreds = (service: string): boolean => {
     if (service === "RANGER") {
       const createDbUser = 
configProperties[service]?.["ranger-env"]?.properties?.["create_db_dbuser"]?.value;
@@ -368,7 +364,16 @@ export default function TestConnection({
 
   const createCustomAction = async () => {
     setIsConnecting(true);
-    const isServiceInstalled = 
installedServicesInCluster.includes(serviceName);
+
+    // Route to the cluster-scoped endpoint whenever a cluster already exists.
+    // When a new service (e.g. Ranger Admin/KMS) is being added to an existing
+    // cluster, the service is not yet "installed" but a cluster IS present.
+    // Using the cluster-less POST /requests endpoint in that case produces a
+    // request with clusterID=-1 that the backend ActionScheduler cannot
+    // resolve, leaving it stuck in PENDING forever and blocking all future
+    // requests. Keying off cluster existence (not service install state)
+    // avoids creating the orphaned, cluster-less request.
+    const useClusterScopedAction = Boolean(clusterName) && 
Boolean(isClusterInstalled);
 
     const params = {
       action: "check_host",
@@ -380,16 +385,23 @@ export default function TestConnection({
       ),
     };
 
+    const targetHost = 
connectionSourceHosts(resolvedRequiredProperties.values);
+    if (!targetHost) {
+      failConnection(
+        null,
+        "No valid host is available to run the database connection check.",
+      );
+      return;
+    }
+
     const payload = {
       RequestInfo: {
         ...params,
       },
-      "Requests/resource_filters": [
-        { hosts: connectionSourceHosts(resolvedRequiredProperties.values) },
-      ],
+      "Requests/resource_filters": [{ hosts: targetHost }],
     };
 
-    if (isServiceInstalled) {
+    if (useClusterScopedAction) {
       try {
         const response = await ClusterApi.createClusterCustomAction(
           clusterName,
diff --git a/ambari-web/latest/src/screens/Hosts/HostsList.tsx 
b/ambari-web/latest/src/screens/Hosts/HostsList.tsx
index a82eb829c6..1c8101766e 100644
--- a/ambari-web/latest/src/screens/Hosts/HostsList.tsx
+++ b/ambari-web/latest/src/screens/Hosts/HostsList.tsx
@@ -93,7 +93,7 @@ export default function HostsList() {
     versionStatus?: string;
   }>();
   const { clusterName, serviceComponentInfo } = useContext(AppContext);
-  const { allServiceModels: serviceModels } = useContext(ServiceContext);
+  const { allServiceModels: serviceModels, polledHostComponentsData } = 
useContext(ServiceContext);
   const [loading, setLoading] = useState(true);
   const [paginationLoading, setPaginationLoading] = useState(false);
   const [showFilters, setShowFilters] = useState(false);
@@ -154,8 +154,18 @@ export default function HostsList() {
     order: "asc",
   });
 
+  // Reuse centralized polled data from CachedServiceApi instead of making a 
separate API call
+  // This eliminates the duplicate /components/ call that was previously 
polled independently
   useEffect(() => {
-    if (clusterName) {
+    if (polledHostComponentsData?.items) {
+      setClusterComponents(polledHostComponentsData);
+      setClusterLoadError(null);
+      setLoading(false);
+    }
+  }, [polledHostComponentsData]);
+
+  useEffect(() => {
+    if (clusterName && clusterRetryCount > 0) {
       void getClusterComponents();
     }
   }, [clusterName, clusterRetryCount]);
@@ -472,7 +482,7 @@ export default function HostsList() {
       const downMasters = get(hostData, "hostComponents", []).filter(
         (component: IHostComponent) =>
           get(component, "isMaster", false) &&
-          get(component, "HostRoles.state", "" as ComponentStatus) !==
+          get(component, "workStatus", "" as ComponentStatus) !==
             ComponentStatus.STARTED
       );
       return downMasters.map((component: IHostComponent) =>
@@ -488,7 +498,7 @@ export default function HostsList() {
         (component: IHostComponent) =>
           !get(component, "isMaster", false) &&
           !get(component, "isClient", false) &&
-          get(component, "HostRoles.state", "" as ComponentStatus) !==
+          get(component, "workStatus", "" as ComponentStatus) !==
             ComponentStatus.STARTED
       );
       return downSlaves.map((component: IHostComponent) =>
diff --git a/ambari-web/latest/src/screens/Hosts/index.tsx 
b/ambari-web/latest/src/screens/Hosts/index.tsx
index eef976a5b5..e5fda652a1 100644
--- a/ambari-web/latest/src/screens/Hosts/index.tsx
+++ b/ambari-web/latest/src/screens/Hosts/index.tsx
@@ -57,7 +57,7 @@ export function Hosts() {
     supports,
   } =
     useContext(AppContext);
-  const { allServiceModels: serviceModels } = useContext(ServiceContext);
+  const { allServiceModels: serviceModels, polledHostComponentsData } = 
useContext(ServiceContext);
   const [allHostModels, setAllHostModels] = useState<IHost[]>([]);
   const { stackVersion, stackVersionList } = useStackVersion();
   const [showHostCheck, setShowHostCheck] = useState(false);
@@ -137,8 +137,18 @@ export function Hosts() {
     }
   }, [params.tab]);
 
+  // Reuse centralized polled data from CachedServiceApi instead of making a 
separate API call
+  // This eliminates the duplicate /components/ call
   useEffect(() => {
-    if (clusterName && allHostModels.length) {
+    if (polledHostComponentsData?.items) {
+      setClusterComponents(polledHostComponentsData);
+      setClusterLoadError(null);
+      setLoading(false);
+    }
+  }, [polledHostComponentsData]);
+
+  useEffect(() => {
+    if (clusterName && allHostModels.length && clusterRetryCount > 0) {
       void getClusterComponents();
     }
   }, [clusterName, get(allHostModels, "[0].hostComponents", []).length, 
clusterRetryCount]);
diff --git a/ambari-web/latest/src/screens/Services/Actions.tsx 
b/ambari-web/latest/src/screens/Services/Actions.tsx
index 20e0db7da6..d686394446 100644
--- a/ambari-web/latest/src/screens/Services/Actions.tsx
+++ b/ambari-web/latest/src/screens/Services/Actions.tsx
@@ -171,7 +171,7 @@ const ActionsContent = ({ serviceName, className }: 
ActionsProps) => {
     backgroundOperations,
     fetchBackgroundOperationsSnapshot,
   } = useContext(AppContext);
-  const { allServiceModels, serviceStatesData } = useContext(ServiceContext);
+  const { allServiceModels, serviceStatesData, applyServiceMaintenanceChange } 
= useContext(ServiceContext);
 
   // Authorization hooks - implementing Ember.js App.isAuthorized patterns
   const { havePermissions, isAuthorized } = useAuthorizationPolicy();
@@ -1021,6 +1021,9 @@ const ActionsContent = ({ serviceName, className }: 
ActionsProps) => {
         ...currentState,
         maintenance_state: targetState,
       }));
+      // Update global state (serviceStatesData, allServiceModels, 
polledHostComponentsData) so
+      // dashboard component icons (passiveState) update immediately on all 
components
+      applyServiceMaintenanceChange(serviceName, targetState);
       setModalInfo({
         title: `Information`,
         body: `Maintenance Mode has been turned ${lowerCase(
diff --git a/ambari-web/latest/src/screens/Services/FlumeSummary.tsx 
b/ambari-web/latest/src/screens/Services/FlumeSummary.tsx
index 2d194b85f6..7681c2542f 100644
--- a/ambari-web/latest/src/screens/Services/FlumeSummary.tsx
+++ b/ambari-web/latest/src/screens/Services/FlumeSummary.tsx
@@ -133,7 +133,7 @@ function FlumeSummary() {
           />
         );
       } else {
-        await cachedServiceApi.fetchAllServiceComponents(clusterName, true);
+        await cachedServiceApi.fetchAllServiceComponents(clusterName);
         removePendingAgent(agent.id);
       }
     } catch (error) {
@@ -181,7 +181,7 @@ function FlumeSummary() {
           [pending.agent.id]: { ...pending, refreshing: true },
         }));
         void cachedServiceApi
-          .fetchAllServiceComponents(clusterName, true)
+          .fetchAllServiceComponents(clusterName)
           .finally(() => removePendingAgent(pending.agent.id));
       } else {
         removePendingAgent(pending.agent.id);
diff --git a/ambari-web/latest/src/screens/Services/RestartWarning.tsx 
b/ambari-web/latest/src/screens/Services/RestartWarning.tsx
index c2dc7afb26..4303202735 100644
--- a/ambari-web/latest/src/screens/Services/RestartWarning.tsx
+++ b/ambari-web/latest/src/screens/Services/RestartWarning.tsx
@@ -28,9 +28,9 @@ import { HostsApi } from "../../api/hostsApi";
 import { useCopyToClipboard } from "../../hooks/useCopyToClipboard";
 import BackgroundOperations from "../BackgroundOperations";
 import { RequestApi } from "../../api/requestApi";
-import usePolling from "../../hooks/usePolling";
 import useAuthorizationPolicy from "../../hooks/useAuthorizationPolicy";
 import { showRollingRestartPopup } from "../Hosts/batchUtils";
+import { ServiceContext } from "../../store/ServiceContext";
 
 
 type RestartWarningProps = {
@@ -60,6 +60,7 @@ function RestartWarning({ serviceName }: RestartWarningProps) 
{
   );
   const [, copy] = useCopyToClipboard();
   const { clusterName } = useContext(AppContext);
+  const { polledHostComponentsData } = useContext(ServiceContext);
 
   const { isAuthorized } = useAuthorizationPolicy();
   const canStartStopServices = isAuthorized("SERVICE.START_STOP");
@@ -80,56 +81,42 @@ function RestartWarning({ serviceName }: 
RestartWarningProps) {
     setComponentsInRestartState([]);
   }, [serviceName]);
 
-  async function loadStaleConfigs() {
-    // Capture current serviceName to prevent race conditions
-    const currentServiceName = serviceName;
-    
-    if (!currentServiceName || !clusterName) {
+  // Reuse centralized polled data from CachedServiceApi instead of making a 
separate API call
+  // CachedServiceApi already polls /components/ with all required fields 
(including stale_configs)
+  useEffect(() => {
+    if (!polledHostComponentsData?.items || !serviceName) {
       return;
     }
-    
-    try {
-      const response = await HostsApi.getClusterComponents(
-        clusterName,
-        
"ServiceComponentInfo/service_name,host_components/HostRoles/display_name,host_components/HostRoles/host_name,host_components/HostRoles/public_host_name,host_components/HostRoles/state,host_components/HostRoles/maintenance_state,host_components/HostRoles/stale_configs,host_components/HostRoles/ha_state,host_components/HostRoles/desired_admin_state,&minimal_response=true"
-      );
-      
-      // Check if serviceName is still the same (prevent race condition)
-      if (currentServiceName !== serviceName) {
-        return;
-      }
-      
-      for (const component of response.items) {
-        forEach(component.host_components, (comp: any) => {
-          set(
-            comp.HostRoles,
-            "serviceName",
-            component.ServiceComponentInfo.service_name
-          );
-        });
-      }
-      const allHostComponentsWithHostRoles = flatten(
-        response?.items?.map((host: any) => host.host_components)
-      );
-      const allHostComponents = flatten(
-        map(allHostComponentsWithHostRoles, "HostRoles")
-      );
-      const serviceComponents = filter(allHostComponents, [
-        "serviceName",
-        currentServiceName,
-      ]);
-      const componentsWithStaleConfigs = filter(serviceComponents, [
-        "stale_configs",
-        true,
-      ]);
-      setComponentsInRestartState(
-        groupPropertyValues(componentsWithStaleConfigs, "host_name")
-      );
-    } catch (error) {
-      console.error("Error loading stale configs for service:", 
currentServiceName, error);
+
+    const response = polledHostComponentsData;
+
+    for (const component of response.items) {
+      forEach(component.host_components, (comp: any) => {
+        set(
+          comp.HostRoles,
+          "serviceName",
+          component.ServiceComponentInfo.service_name
+        );
+      });
     }
-  }
-  usePolling(loadStaleConfigs, 5000);
+    const allHostComponentsWithHostRoles = flatten(
+      response?.items?.map((host: any) => host.host_components)
+    );
+    const allHostComponents = flatten(
+      map(allHostComponentsWithHostRoles, "HostRoles")
+    );
+    const serviceComponents = filter(allHostComponents, [
+      "serviceName",
+      serviceName,
+    ]);
+    const componentsWithStaleConfigs = filter(serviceComponents, [
+      "stale_configs",
+      true,
+    ]);
+    setComponentsInRestartState(
+      groupPropertyValues(componentsWithStaleConfigs, "host_name")
+    );
+  }, [polledHostComponentsData, serviceName]);
 
   useEffect(() => {
     const affectedHosts = [];
diff --git a/ambari-web/latest/src/store/ServiceContext.tsx 
b/ambari-web/latest/src/store/ServiceContext.tsx
index 71f8a1fd5e..6e1a932c72 100644
--- a/ambari-web/latest/src/store/ServiceContext.tsx
+++ b/ambari-web/latest/src/store/ServiceContext.tsx
@@ -16,10 +16,10 @@
  * limitations under the License.
  */
 
-import React, { useContext, useEffect, useState } from "react";
+import React, { useContext, useEffect, useRef, useState } from "react";
 import { useLocation } from "react-router-dom";
 import HDFSService from "../models/hdfs";
-import { cloneDeep, isEmpty, isEqual } from "lodash";
+import { cloneDeep, isEmpty } from "lodash";
 import OptimizedUpdater from "./OptimizedUpdater.tsx";
 import ZkService from "../models/zookeeper.ts";
 import HBaseService from "../models/hbase.ts";
@@ -37,7 +37,6 @@ import HiveService from "../models/hive.ts";
 import usePolling from "../hooks/usePolling.ts";
 import { AppContext } from "./context.tsx";
 import { useAlerts } from "./AlertsContext.tsx";
-import usePrevious from "../hooks/usePrevious.ts";
 import { ServiceApi } from "../api/serviceApi.ts";
 import { cachedServiceApi } from "../api/cachedServiceApi.ts";
 import { centralizedServiceStateApi } from 
"../api/centralizedServiceStateApi.ts";
@@ -52,10 +51,11 @@ interface ServiceContextType {
   allModelsLoaded: boolean;
   serviceModels: { [key: string]: any };
   updateRegistry: Function;
-  polledHostComponentsData: {};
+  polledHostComponentsData: any;
   quickLinksMapWithAPIResponse: Map<string, any>;
-  masterSlaveClientsData: {};
+  masterSlaveClientsData: any;
   serviceStatesData: Map<string, any>;
+  applyServiceMaintenanceChange: (serviceName: string, newState: string) => 
void;
 }
 
 export const ServiceContext = React.createContext<ServiceContextType>({
@@ -69,12 +69,58 @@ export const ServiceContext = 
React.createContext<ServiceContextType>({
   quickLinksMapWithAPIResponse: Map<string, any>,
   //@ts-ignore
   serviceStatesData: new Map(),
+  applyServiceMaintenanceChange: () => {},
 });
 
 interface ServiceProviderProps {
   children: any;
 }
 
+/**
+ * Compute alert counts per service from alert summary and definitions.
+ * Same logic as CentralizedServiceStateApi.calculateServiceAlertCounts but 
without the API call.
+ */
+function computeServiceAlertCounts(
+  alertSummary?: { alerts_summary_grouped: any[] },
+  alertDefinitions?: any[]
+): Map<string, { alertsCount: number; hasCriticalAlerts: boolean }> {
+  const serviceAlerts = new Map<string, { alertsCount: number; 
hasCriticalAlerts: boolean }>();
+
+  if (!alertSummary?.alerts_summary_grouped || !alertDefinitions) {
+    return serviceAlerts;
+  }
+
+  const definitionIdToService = new Map<number, string>();
+  alertDefinitions.forEach((def: any) => {
+    if (def.id && def.service_name) {
+      definitionIdToService.set(def.id, def.service_name);
+    }
+  });
+
+  alertSummary.alerts_summary_grouped.forEach((alert: any) => {
+    const definitionId = alert.definition_id;
+    if (!definitionId) return;
+
+    const serviceName = definitionIdToService.get(definitionId);
+    if (!serviceName) return;
+
+    const criticalCount = alert.summary?.CRITICAL?.count || 0;
+    const warningCount = alert.summary?.WARNING?.count || 0;
+    const totalCount = criticalCount + warningCount;
+    const hasCritical = criticalCount > 0;
+
+    if (!serviceAlerts.has(serviceName)) {
+      serviceAlerts.set(serviceName, { alertsCount: 0, hasCriticalAlerts: 
false });
+    }
+
+    const current = serviceAlerts.get(serviceName)!;
+    current.alertsCount += totalCount;
+    current.hasCriticalAlerts = current.hasCriticalAlerts || hasCritical;
+  });
+
+  return serviceAlerts;
+}
+
 const ServiceProvider: React.FC<ServiceProviderProps> = ({ children }) => {
   // const { services } = useContext(AppContext);
   // const vdpStackVersion = get(cluster, "version", "").split("-")[1];
@@ -84,19 +130,26 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
   const [polledHostComponentsData, setPolledHostComponentsData] = 
useState<any>(
     {}
   );
-  const previousHostComponentsData = usePrevious(polledHostComponentsData);
   const [quickLinksMapWithAPIResponse, setQuickLinksMapWithAPIResponse] =
     useState<any>(null);
 
   const [masterSlaveClientsData, setMasterSlaveClientsData] = 
useState<any>({});
-  const previousMasterSlaveClientsData = usePrevious(masterSlaveClientsData);
   const [serviceStatesData, setServiceStatesData] = useState<Map<string, 
any>>(new Map());
 
-  const { clusterName } = useContext(AppContext);
+  const { clusterName, parsedSocketMessages } = useContext(AppContext);
 
   // Alert data from AlertsContext, used to calculate service alert counts 
without a separate /alerts call
   const { alertSummary, alertDefinitions } = useAlerts();
-  
+
+  // Refs to avoid stale closures in subscriber callback and prevent useEffect 
re-runs
+  const alertSummaryRef = useRef(alertSummary);
+  const alertDefinitionsRef = useRef(alertDefinitions);
+  const allServiceModelsRef = useRef(allServiceModels);
+
+  useEffect(() => { alertSummaryRef.current = alertSummary; }, [alertSummary]);
+  useEffect(() => { alertDefinitionsRef.current = alertDefinitions; }, 
[alertDefinitions]);
+  useEffect(() => { allServiceModelsRef.current = allServiceModels; }, 
[allServiceModels]);
+
   const isOnClusterAdminPage = location.pathname.includes('/main/admin/');
 
   // const [quicklinks, setQuicklinks] = useState<Map<string, any>>(new Map());
@@ -158,12 +211,26 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
     setQuickLinksMapWithAPIResponse(quickLinksMap);
   };
 
+  const modelHasComponentData = (model: any): boolean =>
+    !!model &&
+    ((Array.isArray(model.masterComponents) && model.masterComponents.length > 
0) ||
+      (Array.isArray(model.slaveComponents) && model.slaveComponents.length > 
0) ||
+      (Array.isArray(model.clientComponents) && model.clientComponents.length 
> 0));
+
   const updateRegistry = (updatedModels: any) => {
-    if (updatedModels) {
-      const modelsCopy: any = cloneDeep(updatedModels);
-      if (JSON.stringify(updatedModels) !== JSON.stringify(allServiceModels))
-        setAllServiceModels(modelsCopy);
-    }
+    if (!updatedModels) return;
+    setAllServiceModels((prev: any) => {
+      const merged: any = { ...prev };
+      for (const key of Object.keys(updatedModels)) {
+        const incoming = updatedModels[key];
+        const existing = prev[key];
+        if (modelHasComponentData(existing) && 
!modelHasComponentData(incoming)) {
+          continue;
+        }
+        merged[key] = incoming;
+      }
+      return merged;
+    });
   };
   useEffect(() => {
     updateQuickLinksApiResponseForAllServices();
@@ -247,85 +314,37 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
           }
         });
 
-        // REACTIVE: Detect component-level maintenance mode changes
-        const componentMaintenanceChanges: string[] = [];
-        
-        if (previousHostComponentsData?.items) {
-          responseData.items?.forEach((currentItem: any) => {
-            const serviceName = currentItem.ServiceComponentInfo?.service_name;
-            const componentName = 
currentItem.ServiceComponentInfo?.component_name;
-            
-            if (!serviceName || !componentName) return;
-            
-            // Find the previous state of this component
-            const previousItem = 
previousHostComponentsData.items.find((prevItem: any) =>
-              prevItem.ServiceComponentInfo?.service_name === serviceName &&
-              prevItem.ServiceComponentInfo?.component_name === componentName
-            );
-            
-            if (previousItem) {
-              // Check if any host component maintenance state changed
-              const currentMaintenanceStates = 
currentItem.host_components?.map((hc: any) => 
-                `${hc.HostRoles?.host_name}:${hc.HostRoles?.maintenance_state}`
-              ).sort() || [];
-              
-              const previousMaintenanceStates = 
previousItem.host_components?.map((hc: any) => 
-                `${hc.HostRoles?.host_name}:${hc.HostRoles?.maintenance_state}`
-              ).sort() || [];
-              
-              // If maintenance states changed, track this service for alert 
refresh
-              if (!isEqual(currentMaintenanceStates, 
previousMaintenanceStates)) {
-                if (!componentMaintenanceChanges.includes(serviceName)) {
-                  componentMaintenanceChanges.push(serviceName);
-                }
-              }
-            }
-          });
-        }
-
-        // Update both polledHostComponentsData and masterSlaveClientsData 
from single response
-        if (
-          responseData?.items &&
-          (!isEqual(previousHostComponentsData?.items, responseData.items) ||
-           !isEqual(previousMasterSlaveClientsData?.items, responseData.items))
-        ) {
-          setPolledHostComponentsData(responseData);
-          setMasterSlaveClientsData(responseData.items);
-        }
+        // polledHostComponentsData and masterSlaveClientsData are now set by 
the CachedServiceApi subscriber
+        // No need to set them here - this function only processes stale 
config logic
 
         // Update registry only if there were changes
         if (hasUpdates) {
           updateRegistry(updatedModels);
         }
 
-        // REACTIVE: Immediately refresh alerts for services with component 
maintenance changes
-        if (componentMaintenanceChanges.length > 0) {
-          console.log('Component maintenance mode changed for services:', 
componentMaintenanceChanges);
-          // Force refresh of centralized service state data to get updated 
alerts
-          centralizedServiceStateApi.clearCache();
-          await 
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName, 
alertSummary, alertDefinitions);
-        }
+        // Service states (including alerts) are now recomputed in the 
CachedServiceApi subscriber
+        // whenever components data refreshes - no separate API call needed
       }
     } catch (error) {
       console.error('Error fetching optimized maintenance and stale data:', 
error);
     }
   };
 
-  // Service-level maintenance mode handling with reactive alert updates
+  // Service-level state and maintenance mode initial load (mirrors Ember's 
serviceMapper)
+  // Reads ServiceInfo.state and ServiceInfo.maintenance_state from backend - 
these are
+  // the AUTHORITATIVE source of truth (Ember does NOT derive workStatus from 
component counts).
   useEffect(() => {
     const fetchMaintenanceModeForService = async () => {
       try {
         const responseData = await ServiceApi.getAllServices(clusterName);
-        
+
         // Collect maintenance states like Ember's passiveStateMap
         const passiveStateMap: { [key: string]: string } = {};
-        const changedServices: string[] = [];
-        
+
         responseData.items.forEach((service: any) => {
           passiveStateMap[service.ServiceInfo.service_name] = 
service.ServiceInfo.maintenance_state;
         });
 
-        // Track which services had maintenance mode changes
         let hasMaintenanceUpdates = false;
         const updatedModels = cloneDeep(allServiceModels);
 
@@ -334,34 +353,44 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
           if (updatedModels[serviceModelKey]) {
             const currentMaintenanceValue = 
updatedModels[serviceModelKey].isInPassiveForService;
             const newMaintenanceValue = maintenanceState === "ON";
-            
-            // Only update if the maintenance state has actually changed
+
             if (currentMaintenanceValue !== newMaintenanceValue) {
               updatedModels[serviceModelKey].isInPassiveForService = 
newMaintenanceValue;
               hasMaintenanceUpdates = true;
-              changedServices.push(serviceName);
             }
           }
         });
 
-        // Update registry only if there were actual maintenance state changes
         if (hasMaintenanceUpdates) {
           updateRegistry(updatedModels);
-          
-          // REACTIVE: Immediately refresh alerts for services that changed 
maintenance mode
-          if (changedServices.length > 0) {
-            console.log('Maintenance mode changed for services:', 
changedServices);
-            // Force refresh of centralized service state data to get updated 
alerts
-            centralizedServiceStateApi.clearCache();
-            await 
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName, 
alertSummary, alertDefinitions);
-          }
         }
+
+        // Populate serviceStatesData with authoritative state from backend 
ServiceInfo.state
+        // (Ember's service_mapper.js: work_status: 'ServiceInfo.state')
+        setServiceStatesData((prev) => {
+          const updated = new Map(prev);
+          const serviceAlertCounts = 
computeServiceAlertCounts(alertSummaryRef.current, alertDefinitionsRef.current);
+          responseData.items.forEach((service: any) => {
+            const serviceName = service.ServiceInfo.service_name;
+            const alertData = serviceAlertCounts.get(serviceName) || { 
alertsCount: 0, hasCriticalAlerts: false };
+            const existing = updated.get(serviceName) || {};
+            updated.set(serviceName, {
+              ...existing,
+              serviceName,
+              state: service.ServiceInfo.state,
+              maintenance_state: service.ServiceInfo.maintenance_state,
+              alertsCount: alertData.alertsCount,
+              hasCriticalAlerts: alertData.hasCriticalAlerts,
+            });
+          });
+          centralizedServiceStateApi.setDerivedServiceStates(updated);
+          return updated;
+        });
       } catch (error) {
         console.error('Error fetching service maintenance mode:', error);
       }
     };
 
-    // Only fetch maintenance mode when models are loaded and stable
     if (allModelsLoaded && Object.keys(allServiceModels).length > 0) {
       fetchMaintenanceModeForService();
     }
@@ -370,90 +399,223 @@ const ServiceProvider: React.FC<ServiceProviderProps> = 
({ children }) => {
   // REMOVED: updateStaleConfigsForAllServices - now handled by 
fetchOptimizedMaintenanceAndStaleData
   // All stale config and maintenance state processing is consolidated in the 
optimized function
 
-  // Initialize centralized component API polling (Ember.js style)
-  useEffect(() => {
-    if (clusterName && allModelsLoaded) {
-      
-      // Start centralized polling for all service components
-      cachedServiceApi.startPolling(clusterName, 5000);
-      
-      // Subscribe to centralized data updates
-      const unsubscribe = cachedServiceApi.subscribe((data) => {
-        
-        // Update BOTH masterSlaveClientsData AND polledHostComponentsData 
with centralized data
-        // This ensures both component names AND status are available 
immediately
-        if (data?.items && !isEqual(previousMasterSlaveClientsData?.items, 
data.items)) {
-          setMasterSlaveClientsData(data.items);
-        }
-        
-        // Also update polledHostComponentsData to provide component status 
immediately
-        if (data?.items && !isEqual(previousHostComponentsData?.items, 
data.items)) {
-          setPolledHostComponentsData(data);
-        }
+  // Centralized component API polling using usePolling hook (mirrors Ember's 
updateServiceMetric)
+  // ONE poll drives all data updates - no separate /services poll needed
+  const clusterNameRef = useRef(clusterName);
+  useEffect(() => { clusterNameRef.current = clusterName; }, [clusterName]);
+
+  // Process components data and update derived state.
+  // Called both from the subscriber (when ANY caller fetches data) and from 
pollServiceComponents.
+  // IMPORTANT: This does NOT derive service `state` from component counts. 
Ember treats
+  // ServiceInfo.state as the authoritative source (set on initial /services 
load and via
+  // /events/services WebSocket). We preserve existing state and only refresh 
alerts +
+  // maintenance_state (which tracks the model's isInPassiveForService flag).
+  const processComponentsData = (data: any) => {
+    if (!data?.items) return;
+
+    setMasterSlaveClientsData(data.items);
+    setPolledHostComponentsData(data);
+
+    const presentServiceNames = new Set<string>();
+    data.items.forEach((item: any) => {
+      const serviceName = item.ServiceComponentInfo?.service_name;
+      if (serviceName) presentServiceNames.add(serviceName);
+    });
+
+    const serviceAlertCounts = 
computeServiceAlertCounts(alertSummaryRef.current, alertDefinitionsRef.current);
+
+    setServiceStatesData((prev) => {
+      const updated = new Map(prev);
+      presentServiceNames.forEach((serviceName) => {
+        const serviceModelKey = serviceNameModelMapping[serviceName];
+        const serviceModel = allServiceModelsRef.current[serviceModelKey];
+        const maintenance_state = serviceModel?.isInPassiveForService ? 'ON' : 
'OFF';
+        const alertData = serviceAlertCounts.get(serviceName) || { 
alertsCount: 0, hasCriticalAlerts: false };
+        const existing = updated.get(serviceName) || { serviceName, state: 
'INSTALLED' };
+
+        updated.set(serviceName, {
+          ...existing,
+          serviceName,
+          maintenance_state,
+          alertsCount: alertData.alertsCount,
+          hasCriticalAlerts: alertData.hasCriticalAlerts,
+        });
       });
+      centralizedServiceStateApi.setDerivedServiceStates(updated);
+      return updated;
+    });
+  };
 
-      // Subscribe to centralized service state updates
-      const unsubscribeServiceStates = 
centralizedServiceStateApi.subscribe((data) => {
-        setServiceStatesData(data);
-      });
+  // Subscribe to cachedServiceApi - notified whenever ANY caller (e.g. 
useHDFSConfigUpdater
+  // calling fetchAllServiceComponents directly) returns fresh data.
+  // This ensures state updates flow regardless of which code path initiated 
the fetch.
+  useEffect(() => {
+    const unsubscribe = cachedServiceApi.subscribe(processComponentsData);
+    return () => unsubscribe();
+  }, []);
 
-      // Start centralized service state and alerts polling with timeout-based 
approach
-      let serviceStateTimeout: NodeJS.Timeout | null = null;
-      let isPollingActive = true;
-
-      const pollServiceStates = async () => {
-        if (!isPollingActive) return;
-
-        try {
-          const statesData = await 
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName, 
alertSummary, alertDefinitions);
-          setServiceStatesData(statesData);
-        } catch (error) {
-          console.error('Error polling service states:', error);
-        } finally {
-          // Schedule next poll ONLY after current request completes
-          if (isPollingActive) {
-            serviceStateTimeout = setTimeout(pollServiceStates, 5000);
-          }
-        }
-      };
-
-      // Start initial poll
-      pollServiceStates();
-
-      return () => {
-        unsubscribe();
-        unsubscribeServiceStates();
-        cachedServiceApi.stopPolling();
-        isPollingActive = false;
-        if (serviceStateTimeout) {
-          clearTimeout(serviceStateTimeout);
-        }
-      };
+  const pollServiceComponents = async () => {
+    const currentClusterName = clusterNameRef.current;
+    if (!currentClusterName || !allModelsLoaded) return;
+
+    // fetchAllServiceComponents notifies subscribers internally - state will 
be updated via processComponentsData
+    await cachedServiceApi.fetchAllServiceComponents(currentClusterName);
+  };
+
+  // Eager first fetch - fires immediately when clusterName becomes available
+  useEffect(() => {
+    if (clusterName && allModelsLoaded) {
+      pollServiceComponents();
     }
   }, [clusterName, allModelsLoaded]);
 
-  // Control polling based on current route - pause on cluster admin pages
+  // Subsequent polling every 5s via usePolling hook
+  const pollingInterval = (clusterName && allModelsLoaded) ? 5000 : null;
+  //@ts-ignore
+  const { pausePolling, resumePolling } = usePolling(pollServiceComponents, 
pollingInterval);
+
+  // Pause on cluster admin pages, resume otherwise
   useEffect(() => {
     if (isOnClusterAdminPage) {
-      cachedServiceApi.pausePolling();
+      pausePolling();
     } else {
-      cachedServiceApi.resumePolling(clusterName);
+      resumePolling();
     }
-  }, [isOnClusterAdminPage, clusterName]);
+  }, [isOnClusterAdminPage]);
 
-  // Use the new optimized polling function that replaces multiple API calls
-  // This polling will be controlled by the pausePolling/resumePolling 
mechanism
-  const { pausePolling: pauseMaintenancePolling, resumePolling: 
resumeMaintenancePolling } = 
-    usePolling(fetchOptimizedMaintenanceAndStaleData, 5000);
+  // Process maintenance and stale config data reactively when polled data 
changes
+  // This replaces the independent 5s usePolling timer - no separate poll 
needed
+  useEffect(() => {
+    if (polledHostComponentsData?.items && allModelsLoaded) {
+      fetchOptimizedMaintenanceAndStaleData();
+    }
+  }, [polledHostComponentsData]);
+
+  // Apply a service-level maintenance state change to all derived state.
+  // Mirrors Ember's pattern of doing optimistic UI updates after the API call 
returns
+  // (see ui/app/controllers/main/service/item.js:1198 - 
self.set('content.passiveState', params.passive_state))
+  // Called both from the WebSocket /events/services handler and from 
optimistic UI updates
+  // in Actions.tsx after the toggleMaintenanceMode API call returns.
+  const applyServiceMaintenanceChange = (service_name: string, 
maintenance_state: string) => {
+    const serviceModelKey = serviceNameModelMapping[service_name];
+    if (serviceModelKey && allServiceModelsRef.current[serviceModelKey]) {
+      const updatedModels = cloneDeep(allServiceModelsRef.current);
+      updatedModels[serviceModelKey].isInPassiveForService = maintenance_state 
=== 'ON';
+      updateRegistry(updatedModels);
+    }
+    setServiceStatesData((prev) => {
+      const updated = new Map(prev);
+      const existing = updated.get(service_name) || { serviceName: 
service_name, state: 'INSTALLED', maintenance_state: 'OFF', alertsCount: 0, 
hasCriticalAlerts: false };
+      updated.set(service_name, { ...existing, maintenance_state });
+      centralizedServiceStateApi.setDerivedServiceStates(updated);
+      return updated;
+    });
+
+    // Cascade to all components of this service
+    const cascadeValue = maintenance_state === 'ON' ? 'IMPLIED_FROM_SERVICE' : 
'OFF';
+    setPolledHostComponentsData((prev: any) => {
+      if (!prev?.items) return prev;
+      const updated = cloneDeep(prev);
+      updated.items.forEach((item: any) => {
+        if (item.ServiceComponentInfo?.service_name !== service_name) return;
+        item.host_components?.forEach((hc: any) => {
+          if (hc.HostRoles) hc.HostRoles.maintenance_state = cascadeValue;
+        });
+      });
+      return updated;
+    });
+    setMasterSlaveClientsData((prev: any[]) => {
+      if (!Array.isArray(prev) || prev.length === 0) return prev;
+      const updated = cloneDeep(prev);
+      updated.forEach((item: any) => {
+        if (item.ServiceComponentInfo?.service_name !== service_name) return;
+        item.host_components?.forEach((hc: any) => {
+          if (hc.HostRoles) hc.HostRoles.maintenance_state = cascadeValue;
+        });
+      });
+      return updated;
+    });
+  };
 
-  // Control maintenance/stale config polling based on route
+  // WebSocket /events/hostcomponents and /events/services handler
+  // Mirrors Ember's hostComponentStatusMapper + serviceStateMapper: merges 
WebSocket events
+  // directly into polledHostComponentsData and allServiceModels so UI updates 
instantly
+  // without waiting for the next 5s poll.
+  const lastProcessedSocketMessageRef = useRef<any>(null);
   useEffect(() => {
-    if (isOnClusterAdminPage) {
-      pauseMaintenancePolling();
-    } else {
-      resumeMaintenancePolling();
+    if (!parsedSocketMessages?.length) return;
+
+    const latestMessage = parsedSocketMessages[0];
+    if (latestMessage === lastProcessedSocketMessageRef.current) return;
+    lastProcessedSocketMessageRef.current = latestMessage;
+
+    const destination = latestMessage?.destination;
+
+    // /events/hostcomponents - update host component state in 
polledHostComponentsData
+    // Ember mapping: workStatus<-currentState, staleConfigs<-staleConfigs, 
passiveState<-maintenanceState
+    if (destination === '/events/hostcomponents' && 
Array.isArray(latestMessage.hostComponents)) {
+      setPolledHostComponentsData((prev: any) => {
+        if (!prev?.items) return prev;
+        const updated = cloneDeep(prev);
+        latestMessage.hostComponents.forEach((evt: any) => {
+          const componentName = evt.componentName;
+          const hostName = evt.hostName;
+          const componentItem = updated.items.find(
+            (item: any) => item.ServiceComponentInfo?.component_name === 
componentName
+          );
+          if (!componentItem) return;
+          const hostComp = componentItem.host_components?.find(
+            (hc: any) => hc.HostRoles?.host_name === hostName
+          );
+          if (!hostComp) return;
+          if (evt.currentState !== undefined) hostComp.HostRoles.state = 
evt.currentState;
+          if (evt.staleConfigs !== undefined) hostComp.HostRoles.stale_configs 
= evt.staleConfigs;
+          if (evt.maintenanceState !== undefined) 
hostComp.HostRoles.maintenance_state = evt.maintenanceState;
+        });
+        return updated;
+      });
+      setMasterSlaveClientsData((prev: any[]) => {
+        if (!Array.isArray(prev) || prev.length === 0) return prev;
+        const updated = cloneDeep(prev);
+        latestMessage.hostComponents.forEach((evt: any) => {
+          const componentItem = updated.find(
+            (item: any) => item.ServiceComponentInfo?.component_name === 
evt.componentName
+          );
+          if (!componentItem) return;
+          const hostComp = componentItem.host_components?.find(
+            (hc: any) => hc.HostRoles?.host_name === evt.hostName
+          );
+          if (!hostComp) return;
+          if (evt.currentState !== undefined) hostComp.HostRoles.state = 
evt.currentState;
+          if (evt.staleConfigs !== undefined) hostComp.HostRoles.stale_configs 
= evt.staleConfigs;
+          if (evt.maintenanceState !== undefined) 
hostComp.HostRoles.maintenance_state = evt.maintenanceState;
+        });
+        return updated;
+      });
+    }
+
+    // /events/services - update service state and maintenance_state
+    // Ember mapping: workStatus<-state, passiveState<-maintenance_state
+    if (destination === '/events/services' && latestMessage.service_name) {
+      const { service_name, state, maintenance_state } = latestMessage;
+
+      // Apply maintenance state change (updates models, serviceStatesData, 
and cascades to components)
+      if (maintenance_state !== undefined) {
+        applyServiceMaintenanceChange(service_name, maintenance_state);
+      }
+
+      // Update state field independently
+      if (state !== undefined) {
+        setServiceStatesData((prev) => {
+          const updated = new Map(prev);
+          const existing = updated.get(service_name) || { serviceName: 
service_name, state: 'INSTALLED', maintenance_state: 'OFF', alertsCount: 0, 
hasCriticalAlerts: false };
+          updated.set(service_name, { ...existing, state });
+          centralizedServiceStateApi.setDerivedServiceStates(updated);
+          return updated;
+        });
+      }
     }
-  }, [isOnClusterAdminPage, pauseMaintenancePolling, 
resumeMaintenancePolling]);
+  }, [parsedSocketMessages]);
 
   return (
     <ServiceContext.Provider
@@ -466,6 +628,7 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({ 
children }) => {
         quickLinksMapWithAPIResponse,
         masterSlaveClientsData,
         serviceStatesData,
+        applyServiceMaintenanceChange,
       }}
     >
       <OptimizedUpdater />
diff --git a/ambari-web/latest/src/store/context.tsx 
b/ambari-web/latest/src/store/context.tsx
index d85c11d021..8f25ddf1bd 100644
--- a/ambari-web/latest/src/store/context.tsx
+++ b/ambari-web/latest/src/store/context.tsx
@@ -419,38 +419,6 @@ export const AppProvider: React.FC<{ children: 
React.ReactNode }> = ({
     }
   }, [clusterName, isClusterInstalled, isOnlyViewUser, initializationAttempt]);
 
-  useEffect(() => {
-    if (isOnlyViewUser || !clusterName || !isClusterInstalled) return;
-
-    let pollTimeout: NodeJS.Timeout | null = null;
-    let isPollingActive = true;
-
-    const poll = async () => {
-      if (!isPollingActive) return;
-
-      try {
-        await fetchBackgroundOperations();
-      } catch (error) {
-        console.error('Error polling background operations:', error);
-      } finally {
-        // Schedule next poll ONLY after current request completes
-        if (isPollingActive) {
-          pollTimeout = setTimeout(poll, 30000); // Poll every 30 seconds like 
Ember.js
-        }
-      }
-    };
-
-    // Start initial poll
-    poll();
-
-    return () => {
-      isPollingActive = false;
-      if (pollTimeout) {
-        clearTimeout(pollTimeout);
-      }
-    };
-  }, [clusterName, fetchBackgroundOperations, isClusterInstalled, 
isOnlyViewUser]);
-
   useEffect(() => {
     async function fetchStackConfigs() {
       const stack = get(cluster, "version", "").split("-")[0];


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

Reply via email to