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]