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 bca23ad2ad AMBARI-26635 : Ambari Web React: Reduce redundant API calls
and polling in dashboard, alerts, and services pages (#4177)
bca23ad2ad is described below
commit bca23ad2ad10e1243acb57d79558af1e79907363
Author: Himanshu Maurya <[email protected]>
AuthorDate: Tue Aug 25 13:11:21 2026 +0530
AMBARI-26635 : Ambari Web React: Reduce redundant API calls and polling in
dashboard, alerts, and services pages (#4177)
* AMBARI-26635 : Ambari Web React: Reduce redundant API calls and polling
in dashboard, alerts, and services pages
---
ambari-web/latest/src/AppLoader.tsx | 9 +-
.../latest/src/api/centralizedServiceStateApi.ts | 104 ++++++----
ambari-web/latest/src/api/configsApi.ts | 2 +-
ambari-web/latest/src/components/Navbar.tsx | 96 +++------
.../src/components/Sidebar/RunAllServiceCheck.tsx | 33 +--
.../latest/src/hooks/useHDFSConfigUpdater.ts | 22 +-
.../latest/src/hooks/useHbaseConfigUpdater.ts | 35 +---
ambari-web/latest/src/hooks/useStackVersion.ts | 66 +-----
.../latest/src/hooks/useYarnConfigUpdater.ts | 40 +---
.../src/screens/Alerts/AlertDefinitionDetails.tsx | 25 +--
ambari-web/latest/src/screens/Alerts/Alerts.tsx | 18 +-
ambari-web/latest/src/screens/Hosts/details.tsx | 3 -
ambari-web/latest/src/screens/Services/Actions.tsx | 48 ++---
.../latest/src/screens/Services/ServiceSummary.tsx | 113 +++++------
ambari-web/latest/src/store/AlertsContext.tsx | 221 +++++++++++++++++++++
ambari-web/latest/src/store/ServiceContext.tsx | 10 +-
ambari-web/latest/src/store/context.tsx | 85 +++++++-
pom.xml | 1 +
18 files changed, 525 insertions(+), 406 deletions(-)
diff --git a/ambari-web/latest/src/AppLoader.tsx
b/ambari-web/latest/src/AppLoader.tsx
index 8007a21bca..f08df1d10e 100644
--- a/ambari-web/latest/src/AppLoader.tsx
+++ b/ambari-web/latest/src/AppLoader.tsx
@@ -20,6 +20,7 @@ import { useContext, useEffect, useState } from "react";
import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom";
import { Alert, Button, ProgressBar } from "react-bootstrap";
import { AppContext, AppProvider } from "./store/context";
+import { AlertsProvider } from "./store/AlertsContext";
import { ModalProvider } from "./store/ModalContext";
import { useAuth } from "./hooks/useAuth";
import useAuthorizationPolicy from "./hooks/useAuthorizationPolicy";
@@ -107,9 +108,11 @@ export function AuthenticatedApplication() {
return (
<AppProvider>
- <ModalProvider>
- <ApplicationLoader />
- </ModalProvider>
+ <AlertsProvider>
+ <ModalProvider>
+ <ApplicationLoader />
+ </ModalProvider>
+ </AlertsProvider>
</AppProvider>
);
}
diff --git a/ambari-web/latest/src/api/centralizedServiceStateApi.ts
b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
index 3c3d03ed32..29b1da8c62 100644
--- a/ambari-web/latest/src/api/centralizedServiceStateApi.ts
+++ b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
@@ -33,11 +33,68 @@ class CentralizedServiceStateApi {
private pendingRequest: Promise<Map<string, ServiceStateData>> | null = null;
/**
- * Fetches all service states and alerts in a single API call (Ember.js
pattern)
- * This replaces individual ServiceApi.getServiceState() calls
+ * Calculate alert counts per service from alert summary and definitions
(EmberJS pattern)
+ * Matches alert_definition_summary_mapper.js logic
+ *
+ * @param alertSummary - Alert summary with grouped alerts (by definition_id)
+ * @param alertDefinitions - Alert definitions with service_name mapping
+ */
+ private calculateServiceAlertCounts(
+ 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;
+ }
+
+ // Create map of definition_id -> service_name
+ const definitionIdToService = new Map<number, string>();
+ alertDefinitions.forEach((def: any) => {
+ if (def.id && def.service_name) {
+ definitionIdToService.set(def.id, def.service_name);
+ }
+ });
+
+ // Group alerts by service_name and count CRITICAL + WARNING
+ 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;
+ }
+
+ /**
+ * Fetches service states and calculates alert counts (EmberJS pattern)
+ *
+ * Alert counts come from /alerts?format=groupedSummary via AlertsContext
(useAlerts hook),
+ * not from a separate /alerts API call here - alertSummary/alertDefinitions
carry that data.
+ *
* REQUEST DEDUPLICATION: If a request is already in progress, return the
pending promise
*/
- async fetchAllServiceStatesAndAlerts(clusterName: string):
Promise<Map<string, ServiceStateData>> {
+ async fetchAllServiceStatesAndAlerts(
+ clusterName: string,
+ alertSummary?: { alerts_summary_grouped: any[] },
+ alertDefinitions?: any[]
+ ): Promise<Map<string, ServiceStateData>> {
const now = Date.now();
// Return cached data if still fresh
@@ -57,52 +114,21 @@ class CentralizedServiceStateApi {
method: "GET",
});
- // Get alerts with proper maintenance state filtering (following Ember
pattern)
- // This API call will return NO items for services/components in
maintenance mode
- const alertsResponse = await ambariApi.request({
- url:
`/clusters/${clusterName}/alerts?fields=Alert/service_name,Alert/state&Alert/state.in(CRITICAL,WARNING)&Alert/maintenance_state.in(OFF)&minimal_response=true`,
- method: "GET",
- });
-
const newCache = new Map<string, ServiceStateData>();
- // Count alerts per service - API already filters out maintenance mode
alerts
- const serviceAlertsCount: { [key: string]: { critical: number;
warning: number } } = {};
-
- alertsResponse.data.items?.forEach((alert: any) => {
- const serviceName = alert.Alert?.service_name;
- const alertState = alert.Alert?.state;
-
- if (serviceName && alertState) {
- if (!serviceAlertsCount[serviceName]) {
- serviceAlertsCount[serviceName] = { critical: 0, warning: 0 };
- }
-
- if (alertState === 'CRITICAL') {
- serviceAlertsCount[serviceName].critical++;
- } else if (alertState === 'WARNING') {
- serviceAlertsCount[serviceName].warning++;
- }
- }
- });
+ const serviceAlertCounts =
this.calculateServiceAlertCounts(alertSummary, alertDefinitions);
response.data.items?.forEach((service: any) => {
const serviceName = service.ServiceInfo.service_name;
const state = service.ServiceInfo.state;
- // Use API-filtered alert counts (already excludes maintenance mode
alerts)
- const serviceAlerts = serviceAlertsCount[serviceName] || { critical:
0, warning: 0 };
- const criticalAlerts = serviceAlerts.critical;
- const warningAlerts = serviceAlerts.warning;
-
- const alertsCount = criticalAlerts + warningAlerts;
- const hasCriticalAlerts = criticalAlerts > 0;
+ const alertData = serviceAlertCounts.get(serviceName) || {
alertsCount: 0, hasCriticalAlerts: false };
newCache.set(serviceName, {
serviceName,
state,
- alertsCount,
- hasCriticalAlerts,
+ alertsCount: alertData.alertsCount,
+ hasCriticalAlerts: alertData.hasCriticalAlerts,
});
});
@@ -114,7 +140,7 @@ class CentralizedServiceStateApi {
return this.cache;
} catch (error) {
- console.error('Error fetching service states and alerts:', error);
+ console.error('Error fetching service states:', error);
// Return existing cache on error
return this.cache;
} finally {
diff --git a/ambari-web/latest/src/api/configsApi.ts
b/ambari-web/latest/src/api/configsApi.ts
index 2838e3674f..1fd3d5fe33 100644
--- a/ambari-web/latest/src/api/configsApi.ts
+++ b/ambari-web/latest/src/api/configsApi.ts
@@ -26,7 +26,7 @@ const ConfigsApi = {
verison: string,
services: string
) {
- const url =
`stacks/${stack}/versions/${verison}/services?StackServices/service_name.in(${services})&fields=configurations/*,configurations/dependencies/*,StackServices/config_types/*`;
+ const url =
`stacks/${stack}/versions/${verison}/services?StackServices/service_name.in(${services})&fields=configurations/*,configurations/dependencies/*,StackServices/config_types/*,StackServices/service_check_supported`;
const response = await ambariApi.request({
url: url,
method: "GET",
diff --git a/ambari-web/latest/src/components/Navbar.tsx
b/ambari-web/latest/src/components/Navbar.tsx
index d23d87b090..02b762484f 100644
--- a/ambari-web/latest/src/components/Navbar.tsx
+++ b/ambari-web/latest/src/components/Navbar.tsx
@@ -36,12 +36,9 @@ import {
faMedkit,
} from "@fortawesome/free-solid-svg-icons";
import { AppContext } from "../store/context.tsx";
-import { Notifications } from "../screens/Alerts/types";
import { useLocation, useNavigate } from "react-router-dom";
import AmbariAboutModal from "../AmbariAboutModal.tsx";
import "../styles/app.scss";
-import usePolling from "../hooks/usePolling.ts";
-import { AlertsApi } from "../api/alertsApi.ts";
import {
classicExperienceUrl,
redirectToAdminView,
@@ -50,6 +47,7 @@ import modalManager from "../store/ModalManager.ts";
import BackgroundOperations from "../screens/BackgroundOperations/index.tsx";
import { useCallback } from "react";
import { useAuth } from "../hooks/useAuth";
+import { useAlerts } from "../store/AlertsContext";
import useAuthorizationPolicy from "../hooks/useAuthorizationPolicy";
import { clusterNavigationEnabled } from "../Utils/authPolicy";
import { openViewInstance, ViewInstance } from "../Utils/viewUtils";
@@ -85,27 +83,30 @@ export default function NavBar({
hostname,
enableDigitalClock,
}: NavBarProps) {
- const [notifications, setNotifications] = useState<Notifications[]>([]);
const [showUserSettingsModal, setShowUserSettingsModal] = useState(false);
- const [filteredNotifications, setFilteredNotifications] = useState<
- Notifications[]
- >([]);
+ const [filteredNotifications, setFilteredNotifications] =
useState<any[]>([]);
const [alertCounts, setAlertCounts] = useState({
all: 0,
critical: 0,
warning: 0,
});
+
+ // FOLLOWING EMBERJS PATTERN: Get alert data from useAlerts hook (WebSocket
updates)
+ // EmberJS: mainAlertDefinitionsController.unhealthyAlertInstancesCount
+ // - Computed from [email protected] (App.AlertDefinition.find())
+ // - Updated via WebSocket /events/alerts
+ // - NO POLLING for navbar alerts
+ const { unhealthyAlertInstances } = useAlerts();
+
const location = useLocation();
const [selectedFilter, setSelectedFilter] = useState("all");
const {
clusterName,
- cluster,
serverClock,
userTimezone,
} = useContext(AppContext);
const [showAmbariAboutModal, setShowAmbariAboutModal] = useState(false);
-
- const isClusterInstalled = cluster?.provisioning_state === "INSTALLED";
+
const navigate = useNavigate();
const clusterNavigation = clusterNavigationEnabled(
@@ -113,74 +114,43 @@ export default function NavBar({
location.pathname,
);
- const fetchAlerts = async () => {
- // TLHASD-745: Only fetch alerts if cluster is installed
- if (!clusterName) {
- pausePolling(); // Pause polling if clusterName is not available
- return;
- } else {
- resumePolling(); // Resume polling if clusterName is available
- }
-
- if (!isClusterInstalled) {
- pausePolling(); // Pause polling if cluster is not installed
- return;
- }
-
- resumePolling(); // Resume polling if clusterName is available and cluster
is installed
-
- try {
- const fields =
-
"Alert/component_name,Alert/definition_id,Alert/definition_name,Alert/host_name,Alert/id,Alert/instance,Alert/label,Alert/latest_timestamp,Alert/maintenance_state,Alert/original_timestamp,Alert/scope,Alert/service_name,Alert/state,Alert/text,Alert/repeat_tolerance,Alert/repeat_tolerance_remaining&Alert/state.in(CRITICAL,WARNING)&Alert/maintenance_state.in(OFF)&from=0&page_size=100";
- const time = Date.now();
- const data = await AlertsApi.getAlertsNotifications(
- clusterName,
- fields,
- time
- );
- setNotifications(data.items);
- calculateAlertCounts(data.items);
- } catch (error) {
- console.error("Error fetching alerts:", error);
- }
- };
-
- const { stopPolling, pausePolling, resumePolling } = usePolling(
- fetchAlerts,
- 30000
- );
+ // REMOVED fetchAlerts and polling - using WebSocket data from useAlerts hook
+ // EmberJS pattern: navbar alerts come from mainAlertDefinitionsController
computed properties
+ // which are based on alert definitions summary (updated via WebSocket
/events/alerts)
+ // Update local state when unhealthyAlertInstances changes (from WebSocket)
useEffect(() => {
- if (clusterName && isClusterInstalled) {
- fetchAlerts();
+ if (unhealthyAlertInstances && unhealthyAlertInstances.length > 0) {
+ calculateAlertCounts(unhealthyAlertInstances);
+ } else {
+ setAlertCounts({ all: 0, critical: 0, warning: 0 });
+ setFilteredNotifications([]);
}
- }, [clusterName, isClusterInstalled]);
+ }, [unhealthyAlertInstances]);
useEffect(() => {
filterNotifications();
- }, [selectedFilter, notifications]);
+ }, [selectedFilter, unhealthyAlertInstances]);
- const calculateAlertCounts = (alerts: Notifications[]) => {
+ const calculateAlertCounts = (alerts: any[]) => {
const counts = { all: alerts.length, critical: 0, warning: 0 };
alerts.forEach((alert) => {
- if (alert.Alert.state === "CRITICAL") counts.critical++;
- if (alert.Alert.state === "WARNING") counts.warning++;
+ const state = alert.Alert?.state;
+ if (state === "CRITICAL") counts.critical++;
+ if (state === "WARNING") counts.warning++;
});
setAlertCounts(counts);
};
- useEffect(() => {
- filterNotifications();
- }, [selectedFilter, notifications]);
-
const filterNotifications = () => {
+ const alerts = unhealthyAlertInstances || [];
if (selectedFilter === "all") {
- setFilteredNotifications(notifications);
+ setFilteredNotifications(alerts);
} else {
setFilteredNotifications(
- notifications.filter(
- (notification) =>
- notification.Alert.state === selectedFilter.toUpperCase()
+ alerts.filter(
+ (notification: any) =>
+ notification.Alert?.state === selectedFilter.toUpperCase()
)
);
}
@@ -195,10 +165,10 @@ export default function NavBar({
const { isAuthorized } = useAuthorizationPolicy();
const handleSignOut = useCallback(async () => {
- stopPolling();
+ // REMOVED stopPolling() - no longer polling for navbar alerts (using
WebSocket)
await logout();
navigate("/login", { replace: true });
- }, [logout, navigate, stopPolling]);
+ }, [logout, navigate]);
const canManageAmbari = isAuthorized(
"AMBARI.ADD_DELETE_CLUSTERS, AMBARI.ASSIGN_ROLES, AMBARI.EDIT_STACK_REPOS,
AMBARI.MANAGE_GROUPS, AMBARI.MANAGE_STACK_VERSIONS, AMBARI.MANAGE_USERS,
AMBARI.MANAGE_VIEWS, AMBARI.RENAME_CLUSTER"
diff --git a/ambari-web/latest/src/components/Sidebar/RunAllServiceCheck.tsx
b/ambari-web/latest/src/components/Sidebar/RunAllServiceCheck.tsx
index 2d074cb553..e0885e8d1c 100644
--- a/ambari-web/latest/src/components/Sidebar/RunAllServiceCheck.tsx
+++ b/ambari-web/latest/src/components/Sidebar/RunAllServiceCheck.tsx
@@ -28,10 +28,9 @@ import { ActionsApi } from "../../api/actionsApi.ts";
import BackgroundOperations from "../../screens/BackgroundOperations";
import modalManager from "../../store/ModalManager.ts";
import { get, map } from "lodash";
-import { ServiceApi } from "../../api/serviceApi.ts";
const RunAllServiceCheck = () => {
- const { clusterName, services, cluster } = useContext(AppContext);
+ const { clusterName, services, upgradeIsRunning, upgradeSuspended,
serviceCheckSupportedMap } = useContext(AppContext);
const { allServiceModels } = useContext(ServiceContext);
const { isAuthorized } = useAuthorizationPolicy();
const [showConfirmation, setShowConfirmation] = useState(false);
@@ -39,14 +38,13 @@ const RunAllServiceCheck = () => {
const canRunServiceCheck = isAuthorized("SERVICE.RUN_SERVICE_CHECK");
- if (!canRunServiceCheck) {
+ // Block during active upgrade (not suspended)
+ const isUpgradeBlocking = upgradeIsRunning && !upgradeSuspended;
+
+ if (!canRunServiceCheck || isUpgradeBlocking) {
return null;
}
- const stackInfo = get(cluster, "version", "").split("-");
- const stackName = stackInfo[0];
- const stackVersion = stackInfo[1];
-
const runAllServiceChecks = async () => {
setIsRunning(true);
setShowConfirmation(false);
@@ -57,28 +55,11 @@ const RunAllServiceCheck = () => {
// Build array of service check promises to execute in parallel
const serviceCheckPromises = installedServiceNames.map(async
(serviceName) => {
try {
- // Check if service supports service check
- let isServiceCheckSupported = false;
- try {
- const response = await ServiceApi.isServiceCheckSupported(
- clusterName,
- serviceName,
- stackName,
- stackVersion
- );
- isServiceCheckSupported = get(
- response.data,
- "StackServices.service_check_supported",
- false
- );
- } catch (error) {
- console.error(`Error checking service check support for
${serviceName}`, error);
- return null; // Skip this service if we can't determine support
- }
+ // Use cached service_check_supported from initial stack configs
fetch (no per-service API call)
+ const isServiceCheckSupported =
serviceCheckSupportedMap[serviceName] || false;
// Skip if service doesn't support service check
if (!isServiceCheckSupported) {
- console.log(`Skipping ${serviceName} - service check not
supported`);
return null;
}
diff --git a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
index 88937ed2c6..2a37d84c2a 100644
--- a/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHDFSConfigUpdater.ts
@@ -52,13 +52,12 @@ export const useHDFSConfigUpdater = () => {
const { configsData } = useHDFSConfigsTags();
// @ts-ignore
- const { clusterName } = useContext(AppContext);
+ const { clusterName, clockDistance } = useContext(AppContext);
// @ts-ignore
const { parsedSocketMessages } = useContext(AppContext);
// @ts-ignore
const { allServiceModels, updateRegistry } = useContext(ServiceContext);
- const [serverClockTime, setServerClockTime] = useState<any>();
//const vdpStackVersion = get(cluster, "version", "").split("-")[1];
const hasNameNodeHAEnabledUseEffectRunOnce = useRef(false);
const [isHAEnabledForNamenode, setIsHAEnabledForNamenode] = useState(false);
@@ -143,17 +142,6 @@ export const useHDFSConfigUpdater = () => {
updateRegistry(allServiceModels);
}
};
- const fetchServerCLockTime = async () => {
- const fields = "?fields=RootServiceComponents/server_clock";
- const responseData = await ServiceApi.ambariService(fields);
- const serverClock = get(
- responseData,
- "RootServiceComponents.server_clock",
- null
- );
- setServerClockTime(serverClock);
- };
-
function inferNamespace() {
const hdfsModel = cloneDeep(allServiceModels["hdfs"]);
const isHAEnabled = hdfsModel?.isNameNodeHaEnabled;
@@ -523,12 +511,6 @@ export const useHDFSConfigUpdater = () => {
const calclateNamenodeUptime = (startTime: number) => {
const currentConfig = cloneDeep(allServiceModels["hdfs"]);
const hdfsServiceObj = currentConfig.getServiceObject();
- let clientClock = Date.now();
-
- let serverClock = serverClockTime;
- serverClock = serverClock.toString();
- serverClock = serverClock.length < 13 ? serverClock + "000" : serverClock;
- const clockDistance = serverClock - clientClock;
const uptime = startTime;
if (uptime && uptime > 0) {
const appDateTime = Date.now() + clockDistance;
@@ -1171,7 +1153,6 @@ export const useHDFSConfigUpdater = () => {
updateHDFSMasterComponents();
findMasterSlaveClientComponents();
calcDiskUsagePartandPercent();
- fetchServerCLockTime();
}
}, [polledHostComponentsData]);
@@ -1187,7 +1168,6 @@ export const useHDFSConfigUpdater = () => {
}, [masterSlaveClientsData, clusterName]);
useEffect(() => {
- fetchServerCLockTime();
//findMasterSlaveClientComponents();
updateWorkStatusValues();
}, []);
diff --git a/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
b/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
index e1389a2964..2f5e3de754 100644
--- a/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useHbaseConfigUpdater.ts
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { useContext, useEffect, useState } from "react";
+import { useContext, useEffect } from "react";
import { cloneDeep, find, get, isEmpty, isEqual } from "lodash";
import { ServiceApi } from "../api/serviceApi";
import { cachedServiceApi } from "../api/cachedServiceApi";
@@ -38,30 +38,18 @@ export const useHbaseConfigUpdater = () => {
} = useContext(ServiceContext);
// @ts-ignore
- const { services, clusterName, parsedSocketMessages } =
useContext(AppContext);
-
+ const { services, clusterName, parsedSocketMessages, clockDistance } =
useContext(AppContext);
+
// Early return if HBASE service is not installed
- const isHbaseInstalled = services && Array.isArray(services) &&
+ const isHbaseInstalled = services && Array.isArray(services) &&
services.some((service: any) => service.ServiceInfo.service_name ===
"HBASE");
-
+
if (!isHbaseInstalled) {
return;
}
-
+
//@ts-ignore
const { allServiceModels, updateRegistry } = useContext(ServiceContext);
- const [serverClockTime, setServerClockTime] = useState<any>();
-
- const fetchServerCLockTime = async () => {
- const fields = "?fields=RootServiceComponents/server_clock";
- const responseData = await ServiceApi.ambariService(fields);
- const serverClock = get(
- responseData,
- "RootServiceComponents.server_clock",
- null
- );
- setServerClockTime(serverClock);
- };
const fetchHbaseMasterSlaveClientsData = async () => {
let hbaseComponentsData =
cachedServiceApi.getServiceComponentData("HBASE");
@@ -246,16 +234,6 @@ export const useHbaseConfigUpdater = () => {
const calculateHbaseMasterUptime = (startOrActiveTime: number) => {
const currentConfig = cloneDeep(allServiceModels["hbase"]);
const hbaseServiceObj = currentConfig?.getServiceObject();
- let clientClock = Date.now();
-
- let serverClock = serverClockTime;
- if (!serverClock) {
- return null; // Return null if serverClockTime is not available
- }
-
- serverClock = serverClock.toString();
- serverClock = serverClock.length < 13 ? serverClock + "000" : serverClock;
- const clockDistance = serverClock - clientClock;
const uptime = startOrActiveTime;
if (uptime && uptime > 0) {
const appDateTime = Date.now() + clockDistance;
@@ -532,7 +510,6 @@ export const useHbaseConfigUpdater = () => {
useEffect(() => {
updateHbaseHostComponentsData();
- fetchServerCLockTime();
findMasterSlaveClientComponents();
parseAlertsWebSocketMessages();
}, []);
diff --git a/ambari-web/latest/src/hooks/useStackVersion.ts
b/ambari-web/latest/src/hooks/useStackVersion.ts
index 89a0b7a63e..128fd81c37 100644
--- a/ambari-web/latest/src/hooks/useStackVersion.ts
+++ b/ambari-web/latest/src/hooks/useStackVersion.ts
@@ -16,73 +16,11 @@
* limitations under the License.
*/
-import { useContext, useEffect, useState } from "react";
-import VersionsApi from "../api/versionsApi";
-import { forEach, get } from "lodash";
+import { useContext } from "react";
import { AppContext } from "../store/context";
const useStackVersion = () => {
- const { clusterName } = useContext(AppContext);
- const [stackVersion, setStackVersion] = useState();
- const [stackVersionList, setStackVersionList] = useState([]);
-
- const getStackVersion = async () => {
- const response = await VersionsApi.getServices(clusterName);
- setStackVersion(response);
- };
-
- useEffect(() => {
- getStackVersion();
- }, []);
-
- useEffect(() => {
- if (stackVersion) {
- let stackVersionListCopy: any = [];
- forEach(get(stackVersion, "items", []), (item: any) => {
- const repoVersionId = get(
- item,
- "ClusterStackVersions.repository_version"
- );
- const repoVersion = get(item, "repository_versions", []).find(
- (repo: any) => get(repo, "RepositoryVersions.id") === repoVersionId
- )?.RepositoryVersions?.repository_version;
- stackVersionListCopy.push({
- id: get(item, "ClusterStackVersions.id"),
- cluster_name: get(item, "ClusterStackVersions.cluster_name"),
- stack: get(item, "ClusterStackVersions.stack"),
- version: get(item, "ClusterStackVersions.version"),
- state: get(item, "ClusterStackVersions.state"),
- displayName: get(item,
"repository_versions.[0].RepositoryVersions.display_name"),
- not_installed_hosts: get(
- item,
- "ClusterStackVersions.host_states.NOT_REQUIRED"
- ),
- installing_hosts: get(
- item,
- "ClusterStackVersions.host_states.INSTALLING"
- ),
- installed_hosts: get(
- item,
- "ClusterStackVersions.host_states.INSTALLED"
- ),
- install_failed_hosts: get(
- item,
- "ClusterStackVersions.host_states.INSTALL_FAILED"
- ),
- out_of_sync_hosts: get(
- item,
- "ClusterStackVersions.host_states.OUT_OF_SYNC"
- ),
- current_hosts: get(item, "ClusterStackVersions.host_states.CURRENT"),
- supports_revert: get(item, "ClusterStackVersions.supports_revert"),
- repository_version_id: repoVersionId,
- repository_version: repoVersion,
- });
- });
- setStackVersionList(stackVersionListCopy);
- }
- }, [stackVersion]);
-
+ const { stackVersion, stackVersionList } = useContext(AppContext);
return { stackVersion, stackVersionList };
};
diff --git a/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
b/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
index 55e98738cd..816ae3e71b 100644
--- a/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
+++ b/ambari-web/latest/src/hooks/useYarnConfigUpdater.ts
@@ -16,9 +16,8 @@
* limitations under the License.
*/
-import { useContext, useEffect, useRef, useState } from "react";
+import { useContext, useEffect, useRef } from "react";
import { cloneDeep, find, get, isEmpty, isEqual } from "lodash";
-import { ServiceApi } from "../api/serviceApi";
import { cachedServiceApi } from "../api/cachedServiceApi";
import { centralizedServiceStateApi } from "../api/centralizedServiceStateApi";
import { ServiceComponentMetricsEnums } from
"../enums/ServiceComponentMetricsEnums";
@@ -37,32 +36,20 @@ export const useYarnConfigUpdater = () => {
} = useContext(ServiceContext);
// @ts-ignore
- const { services, clusterName, parsedSocketMessages } =
useContext(AppContext);
-
+ const { services, clusterName, parsedSocketMessages, clockDistance } =
useContext(AppContext);
+
// Early return if YARN service is not installed
- const isYarnInstalled = services && Array.isArray(services) &&
+ const isYarnInstalled = services && Array.isArray(services) &&
services.some((service: any) => service.ServiceInfo.service_name ===
"YARN");
-
+
if (!isYarnInstalled) {
return;
}
// @ts-ignore
const { allServiceModels, updateRegistry } = useContext(ServiceContext);
- const [serverClockTime, setServerClockTime] = useState<any>();
const hasResourceManagerHAEnabledUseEffectRunOnce = useRef(false);
- const fetchServerCLockTime = async () => {
- const fields = "?fields=RootServiceComponents/server_clock";
- const responseData = await ServiceApi.ambariService(fields);
- const serverClock = get(
- responseData,
- "RootServiceComponents.server_clock",
- null
- );
- setServerClockTime(serverClock);
- };
-
const fetchYARNMasterSlaveClientsData = async () => {
let yarnComponentsData = cachedServiceApi.getServiceComponentData("YARN");
@@ -280,16 +267,6 @@ export const useYarnConfigUpdater = () => {
const calclateResourceManagereUptime = (startTime: number) => {
const currentConfig = cloneDeep(allServiceModels["yarn"]);
const yarnServiceObj = currentConfig.getServiceObject();
- let clientClock = Date.now();
-
- let serverClock = serverClockTime;
- if (!serverClock) {
- return null; // Return null if serverClockTime is not available
- }
-
- serverClock = serverClock.toString();
- serverClock = serverClock.length < 13 ? serverClock + "000" : serverClock;
- const clockDistance = serverClock - clientClock;
const uptime = startTime;
if (uptime && uptime > 0) {
const appDateTime = Date.now() + clockDistance;
@@ -806,7 +783,6 @@ export const useYarnConfigUpdater = () => {
updateYARNData();
updateYARNMasterComponents();
findMasterSlaveClientComponents();
- fetchServerCLockTime();
calcDiskUsagePartandPercent();
}
}, [polledHostComponentsData]);
@@ -822,12 +798,6 @@ export const useYarnConfigUpdater = () => {
hasResourceManagerHAEnabledUseEffectRunOnce.current = true;
}, [allServiceModels]);
- useEffect(() => {
- fetchServerCLockTime();
- //isRMAEnabled();
- //findMasterSlaveClientComponents();
- }, []);
-
useEffect(() => {
updateAlertsAndServiceStateData();
}, [allServiceModels, serviceStatesData]);
diff --git a/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
b/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
index 8f6d6719ad..b709749448 100644
--- a/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
+++ b/ambari-web/latest/src/screens/Alerts/AlertDefinitionDetails.tsx
@@ -30,9 +30,12 @@ import { AppContext } from "../../store/context";
import { AlertEditorHandle, AlertStatusObject, MergedAlert } from './types';
import { buildAlertDefinitionDetails } from '../../Utils/alertDefinitions';
import Modal from '../../components/Modal';
+import { useAlerts } from '../../store/AlertsContext';
const AlertDefinitionDetails = () => {
const { clusterName } = useContext(AppContext);
+ // Alert groups + summary already loaded/kept in sync by AlertsContext -
no need to refetch them here.
+ const { alertGroups, alertSummary } = useAlerts();
const params = useParams<{ alertId?: string }>();
const [isLoaded, setIsLoaded] = useState(false);
const [alertDefinition, setAlertDefinition] = useState<MergedAlert |
null>(null);
@@ -106,22 +109,20 @@ const AlertDefinitionDetails = () => {
try {
const alertDefinitionId = params.alertId;
if (alertDefinitionId) {
- const [groupsResponse, summariesResponse,
alertDefinitionResponse] = await Promise.all([
- AlertsApi.getAlerts(
- clusterName,
-
'AlertGroup/default,AlertGroup/definitions,AlertGroup/id,AlertGroup/name,AlertGroup/targets',
- Date.now()
- ),
-
AlertsApi.getGroupFormattedAlertsNotifications(clusterName, Date.now()),
- AlertsApi.getAlertDefinitionById(clusterName,
alertDefinitionId, Date.now())
- ]);
+ // Alert groups + summary come from AlertsContext (shared,
WebSocket-updated) -
+ // only the full per-definition detail
(source/config/help_url) needs its own fetch.
+ const alertDefinitionResponse = await
AlertsApi.getAlertDefinitionById(
+ clusterName,
+ alertDefinitionId,
+ Date.now(),
+ );
const definition =
alertDefinitionResponse?.AlertDefinition ||
alertDefinitionResponse?.items?.[0]?.AlertDefinition;
if (!definition) throw new Error('Alert definition not
found');
const details = buildAlertDefinitionDetails(
definition,
- groupsResponse?.items || [],
- summariesResponse?.alerts_summary_grouped || [],
+ alertGroups || [],
+ alertSummary?.alerts_summary_grouped || [],
);
setAlertDefinition(details);
setStatuses(details.statuses);
@@ -137,7 +138,7 @@ const AlertDefinitionDetails = () => {
};
fetchAlertDetails();
- }, [params.alertId, clusterName, retryTrigger]);
+ }, [params.alertId, clusterName, retryTrigger, alertGroups, alertSummary]);
if (!isLoaded) {
return <Spinner />;
diff --git a/ambari-web/latest/src/screens/Alerts/Alerts.tsx
b/ambari-web/latest/src/screens/Alerts/Alerts.tsx
index c38ded03ad..e8c85c65a3 100644
--- a/ambari-web/latest/src/screens/Alerts/Alerts.tsx
+++ b/ambari-web/latest/src/screens/Alerts/Alerts.tsx
@@ -37,7 +37,6 @@ import {SortingState} from "@tanstack/react-table";
import MenuBar from './MenuBar'
import {formatAlertStatusDisplay} from "./alertStatus";
import useAuthorizationPolicy from '../../hooks/useAuthorizationPolicy';
-import usePolling from '../../hooks/usePolling';
const DEFAULT_ALERT_SORTING: SortingState = [{ id: 'statuses', desc: true }];
@@ -69,7 +68,7 @@ const Alerts = () => {
const [alertDefinitions, setAlertDefinitions] =
useState<AlertDefinition[]>([]);
const [searchFilters, setSearchFilters] =
useState<SearchFilter[]>(initialViewState.searchFilters);
const [filteredAlerts, setFilteredAlerts] = useState<MergedAlert[]>([]);
- const [isModalOpen, setIsModalOpen] = useState(false);
+ const [, setIsModalOpen] = useState(false);
const [listLoadError, setListLoadError] = useState('');
const [definitionLoadError, setDefinitionLoadError] = useState('');
const listRequestId = useRef(0);
@@ -177,11 +176,11 @@ const Alerts = () => {
}
}, [clusterName]);
- // Use usePolling hook with pause/resume based on modal state
- const { pausePolling, resumePolling } = usePolling(fetchData, 30000);
-
+ // Load once on mount - alert data updates are pushed via WebSocket
(/events/alerts),
+ // not polled, matching the EmberJS pattern (no independent 30s poll per
page).
useEffect(() => {
if (clusterName) {
+ fetchData();
fetchAlertDefinitions();
}
}, [clusterName, fetchAlertDefinitions]);
@@ -199,15 +198,6 @@ const Alerts = () => {
}));
}, [clusterName, searchFilters, sorting]);
- // Control polling based on modal state
- useEffect(() => {
- if (isModalOpen) {
- pausePolling();
- } else {
- resumePolling();
- }
- }, [isModalOpen, pausePolling, resumePolling]);
-
// Fetch alert groups when needed
const handleSearch = (filters: SearchFilter[]) => {
diff --git a/ambari-web/latest/src/screens/Hosts/details.tsx
b/ambari-web/latest/src/screens/Hosts/details.tsx
index 81b40d9a8a..07bc3bdcdd 100644
--- a/ambari-web/latest/src/screens/Hosts/details.tsx
+++ b/ambari-web/latest/src/screens/Hosts/details.tsx
@@ -196,9 +196,6 @@ const hostPassiveModeRequest = async (
const updateHost = (state: string) => {
infoPassiveState(state);
- // setTimeout(() => {
- // window.location.reload();
- // }, 2000);
};
const doStartAllComponents = (context: any) => {
diff --git a/ambari-web/latest/src/screens/Services/Actions.tsx
b/ambari-web/latest/src/screens/Services/Actions.tsx
index e61b6c17e3..20e0db7da6 100644
--- a/ambari-web/latest/src/screens/Services/Actions.tsx
+++ b/ambari-web/latest/src/screens/Services/Actions.tsx
@@ -167,10 +167,11 @@ const ActionsContent = ({ serviceName, className }:
ActionsProps) => {
isClusterInstalled,
supports,
wizardIsNotFinished,
+ serviceCheckSupportedMap,
backgroundOperations,
fetchBackgroundOperationsSnapshot,
} = useContext(AppContext);
- const { allServiceModels } = useContext(ServiceContext);
+ const { allServiceModels, serviceStatesData } = useContext(ServiceContext);
// Authorization hooks - implementing Ember.js App.isAuthorized patterns
const { havePermissions, isAuthorized } = useAuthorizationPolicy();
@@ -389,46 +390,31 @@ const ActionsContent = ({ serviceName, className }:
ActionsProps) => {
return false;
}
- async function fetchServiceState() {
+ // OPTIMIZED: Use cached service state data instead of making individual
API call
+ // EmberJS uses App.Service.find() which loads from Ember Data store
+ // Modern UI now uses serviceStatesData from ServiceContext (centralized
polling)
+ // This eliminates unnecessary /services/{serviceName} API calls
+ function loadServiceStateFromCache() {
if (
clusterName &&
serviceName &&
installedServiceNames.includes(serviceName)
) {
- const response = await ServiceApi.getServiceState(
- clusterName,
- serviceName.toUpperCase()
- );
- setServiceState(response.data.ServiceInfo);
+ // Get service state from centralized cache (already being polled)
+ const cachedServiceState =
serviceStatesData.get(serviceName.toUpperCase());
+ if (cachedServiceState) {
+ setServiceState(cachedServiceState);
+ }
}
}
- async function fetchServiceCheckSupported() {
+ function fetchServiceCheckSupported() {
if (!allServiceModels ||
!allServiceModels[serviceNameModelMapping[serviceName]]) {
return;
}
- // Match Ember.js logic exactly: check if service supports service check
from stack definition
- let isServiceCheckSupportedFromStack = false;
-
- try {
- const response = await ServiceApi.isServiceCheckSupported(
- clusterName,
- //@ts-ignore
- serviceName,
- stackName,
- stackVersion
- );
- isServiceCheckSupportedFromStack = get(
- response.data,
- "StackServices.service_check_supported",
- false
- );
- } catch (error) {
- console.error("Error fetching service check support", error);
- // Default to false if we can't determine support
- isServiceCheckSupportedFromStack = false;
- }
+ // Use cached service_check_supported from initial stack configs fetch
(like Ember's App.services.supportsServiceCheck)
+ const isServiceCheckSupportedFromStack =
serviceCheckSupportedMap[serviceName] || false;
// Apply Ember.js isSmokeTestDisabled logic
let isSmokeTestDisabled = false;
@@ -452,9 +438,9 @@ const ActionsContent = ({ serviceName, className }:
ActionsProps) => {
}
//fetchClusterName();
- fetchServiceState();
+ loadServiceStateFromCache(); // OPTIMIZED: Load from cache instead of API
call
fetchServiceCheckSupported();
- }, [serviceName, clusterName, allServiceModels, serviceModels]);
+ }, [serviceName, clusterName, allServiceModels, serviceModels,
serviceStatesData]);
useEffect(() => {
const message = socketMessages[0];
diff --git a/ambari-web/latest/src/screens/Services/ServiceSummary.tsx
b/ambari-web/latest/src/screens/Services/ServiceSummary.tsx
index 47b4c57efc..90064205ac 100644
--- a/ambari-web/latest/src/screens/Services/ServiceSummary.tsx
+++ b/ambari-web/latest/src/screens/Services/ServiceSummary.tsx
@@ -22,13 +22,10 @@ import ServiceComponents from "./ServiceComponents";
import ServiceMetrics from "./ServiceMetrics";
import OptimizedServiceQuicklinks from "./OptimizedServiceQuicklinks";
import HDFSFederationSummary from "./HDFSFederationSummary";
-import { filter, find, get, isNumber, isObject, map } from "lodash";
-import { AlertsApi } from "../../api/alertsApi";
+import { find, isNumber, isObject } from "lodash";
import { useContext, useEffect, useState } from "react";
-import { AppContext } from "../../store/context";
import { ServiceContext } from "../../store/ServiceContext";
-import usePolling from "../../hooks/usePolling";
-import { centralizedServiceStateApi } from
"../../api/centralizedServiceStateApi";
+import { useAlerts } from "../../store/AlertsContext";
type SummaryProps = {
serviceName: string;
@@ -37,9 +34,15 @@ type SummaryProps = {
function ServiceSummary({ serviceName, selectedTab }: SummaryProps) {
const [alerts, setAlerts] = useState<any>([]);
const [alertsCount, setAlertsCount] = useState<number>(0);
- const { clusterName, isClusterInstalled } = useContext(AppContext);
const { allServiceModels } = useContext(ServiceContext);
+ // FOLLOWING EMBERJS PATTERN: Get alert data from useAlerts hook (WebSocket
updates)
+ // EmberJS: App.AlertDefinition.find() from Ember Data store
+ // - Loaded once via updateAlertDefinitions() and
updateAlertDefinitionSummary()
+ // - Updated via WebSocket /events/alerts
+ // - NO POLLING on service summary page
+ const { alertDefinitions, alertSummary } = useAlerts();
+
const STATUS_PRIORITY_ORDER = [
"CRITICAL",
"OK",
@@ -48,38 +51,39 @@ function ServiceSummary({ serviceName, selectedTab }:
SummaryProps) {
"NONE",
];
- async function getAlerts() {
- const alertDefinitionsFields =
`AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/repeat_tolerance,AlertDefinition/repeat_tolerance_enabled,AlertDefinition/id,AlertDefinition/ignore_host,AlertDefinition/interval,AlertDefinition/label,AlertDefinition/name,AlertDefinition/scope,AlertDefinition/service_name,AlertDefinition/source,AlertDefinition/help_url`;
- const alertDefinitions = await AlertsApi.getAlertDefinition(
- clusterName,
- alertDefinitionsFields,
- Date.now()
+ // REMOVED getAlerts() and polling - using WebSocket data from useAlerts hook
+ // EmberJS pattern: service summary uses App.AlertDefinition.find() from
store
+ // which is populated once and updated via WebSocket /events/alerts
+
+ // Process alerts when alertDefinitions or alertSummary changes (from
WebSocket)
+ useEffect(() => {
+ if (!alertDefinitions || !alertSummary || !serviceName) {
+ setAlerts([]);
+ return;
+ }
+
+ const allGroupedAlerts = alertSummary?.alerts_summary_grouped || [];
+
+ // Filter alert definitions for this service
+ const alertsForSelectedService = alertDefinitions.filter(
+ (def: any) => def.service_name === serviceName
);
- const { items } = alertDefinitions;
-
- // FIXED: Get ALL alerts (including maintenance mode) to properly display
them with maintenance styling
- const { alerts_summary_grouped: allGroupedAlerts } =
- await AlertsApi.getGroupFormattedAlertsNotifications(clusterName);
-
- const alertsForSelectedService = filter(items, [
- "AlertDefinition.service_name",
- serviceName,
- ]);
-
- const inferredAlerts = map(alertsForSelectedService, (alert: any) => {
+
+ // Map definitions to alerts with summary data
+ const inferredAlerts = alertsForSelectedService.map((alert: any) => {
const matchingAlert = find(allGroupedAlerts, [
"definition_id",
- get(alert, "AlertDefinition.id", ""),
+ alert.id,
]);
return {
- label: get(alert, "AlertDefinition.label", ""),
- name: get(alert, "AlertDefinition.name", ""),
- description: get(alert, "AlertDefinition.description", ""),
- id: get(alert, "AlertDefinition.id", ""),
- component_name: get(alert, "AlertDefinition.component_name", ""),
+ label: alert.label || "",
+ name: alert.name || "",
+ description: alert.description || "",
+ id: alert.id || "",
+ component_name: alert.component_name || "",
summary: matchingAlert
- ? matchingAlert?.summary
+ ? matchingAlert.summary
: {
NONE: { count: 1, maintenance_count: 0 },
CRITICAL: { count: 0, maintenance_count: 0 },
@@ -90,59 +94,46 @@ function ServiceSummary({ serviceName, selectedTab }:
SummaryProps) {
highestStatus: "",
};
});
+
+ // Determine highest status for each alert
for (const alert of inferredAlerts) {
const statuses =
alert && isObject(alert) && alert.summary
- ? Object.keys(alert?.summary).map((status: any) => ({
+ ? Object.keys(alert.summary).map((status: any) => ({
status,
count: alert.summary[status].count,
maintenance_count: alert.summary[status].maintenance_count,
}))
: [];
-
- // FIXED: Determine highest status based on both regular and maintenance
alerts
- // This preserves the actual alert status even when in maintenance mode
+
let highestStatus = "none";
const statusOrder = ["critical", "warning", "ok", "unknown"];
-
+
for (const priorityStatus of statusOrder) {
- const statusItem = statuses.find(s => s.status.toLowerCase() ===
priorityStatus);
- if (statusItem && (statusItem.count > 0 ||
statusItem.maintenance_count > 0)) {
+ const statusItem = statuses.find(
+ (s) => s.status.toLowerCase() === priorityStatus
+ );
+ if (
+ statusItem &&
+ (statusItem.count > 0 || statusItem.maintenance_count > 0)
+ ) {
highestStatus = priorityStatus;
break;
}
}
-
+
alert.highestStatus = highestStatus.toUpperCase();
}
+
+ // Sort by status priority
const sortedInferredAlerts = STATUS_PRIORITY_ORDER.map((status) => {
return inferredAlerts.filter(
(alert: any) => alert.highestStatus === status
);
}).flat();
- setAlerts(sortedInferredAlerts);
- }
- usePolling(getAlerts, 30000);
-
- useEffect(() => {
- if (isClusterInstalled) {
- setAlertsCount(0);
- getAlerts();
- }
- }, [serviceName, isClusterInstalled]);
- useEffect(() => {
- if (!clusterName || !serviceName) return;
-
- const unsubscribe =
centralizedServiceStateApi.subscribe((serviceStatesData) => {
- const serviceStateData = serviceStatesData.get(serviceName);
- if (serviceStateData) {
- getAlerts();
- }
- });
-
- return unsubscribe;
- }, [clusterName, serviceName]);
+ setAlerts(sortedInferredAlerts);
+ }, [alertDefinitions, alertSummary, serviceName]);
useEffect(() => {
if (alerts.length) {
let inferredAlertsCount = 0;
diff --git a/ambari-web/latest/src/store/AlertsContext.tsx
b/ambari-web/latest/src/store/AlertsContext.tsx
new file mode 100644
index 0000000000..77b5e5d420
--- /dev/null
+++ b/ambari-web/latest/src/store/AlertsContext.tsx
@@ -0,0 +1,221 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * AlertsContext - centralized alert data management with WebSocket updates.
+ *
+ * Single source of truth for alert groups/definitions/summary so consumers
+ * (Navbar, ServiceSummary, Alerts, AlertDefinitionDetails, ServiceContext)
+ * don't each independently poll the alerts APIs.
+ *
+ * Pattern, matching EmberJS:
+ * - Initial load once: alert groups -> alert definitions -> alert summary ->
unhealthy alerts
+ * - WebSocket updates on /events/alerts refresh the summary and unhealthy
alerts
+ * - Polling only for unhealthy alerts, and only while on the /main/alerts
route
+ */
+
+import React, { createContext, useState, useEffect, useCallback, useContext,
useRef } from 'react';
+import { AlertsApi } from '../api/alertsApi';
+import { getCurrTimeInSec } from '../Utils/Utility';
+import { AppContext } from './context';
+import { useLocation } from 'react-router-dom';
+
+interface AlertsContextType {
+ alertGroups: any[];
+ alertDefinitions: any[];
+ alertSummary: any;
+ unhealthyAlertInstances: any[];
+ isLoading: boolean;
+ refreshUnhealthyAlerts: () => Promise<void>;
+}
+
+const AlertsContext = createContext<AlertsContextType | undefined>(undefined);
+
+const UNHEALTHY_ALERTS_POLL_INTERVAL = 10000; // 10 seconds - matching EmberJS
+
+export const AlertsProvider: React.FC<{ children: React.ReactNode }> = ({
children }) => {
+ const { clusterName, parsedSocketMessages } = useContext(AppContext);
+ const location = useLocation();
+
+ const [alertGroups, setAlertGroups] = useState<any[]>([]);
+ const [alertDefinitions, setAlertDefinitions] = useState<any[]>([]);
+ const [alertSummary, setAlertSummary] = useState<any>(null);
+ const [unhealthyAlertInstances, setUnhealthyAlertInstances] =
useState<any[]>([]);
+ const [isLoading, setIsLoading] = useState(true);
+
+ const initialLoadComplete = useRef(false);
+ const pollIntervalRef = useRef<NodeJS.Timeout | null>(null);
+
+ const isAlertsRoute = location.pathname.includes('/main/alerts');
+
+ const loadAlertGroups = useCallback(async () => {
+ if (!clusterName) return;
+
+ try {
+ const currTime = getCurrTimeInSec();
+ const response = await AlertsApi.getAlerts(
+ clusterName,
+
'AlertGroup/default,AlertGroup/definitions,AlertGroup/id,AlertGroup/name,AlertGroup/targets',
+ currTime
+ );
+
+ if (response?.items) {
+ setAlertGroups(response.items);
+ }
+ } catch (error) {
+ console.error('[AlertsContext] Error loading alert groups:', error);
+ }
+ }, [clusterName]);
+
+ const loadAlertDefinitions = useCallback(async () => {
+ if (!clusterName) return;
+
+ try {
+ const currTime = getCurrTimeInSec();
+ const response = await AlertsApi.getAlertDefinition(
+ clusterName,
+
'AlertDefinition/component_name,AlertDefinition/description,AlertDefinition/enabled,AlertDefinition/id,AlertDefinition/label,AlertDefinition/name,AlertDefinition/service_name',
+ currTime
+ );
+
+ if (response?.items) {
+ const definitions = response.items.map((item: any) => ({
+ ...item.AlertDefinition,
+ label: item.AlertDefinition.label || item.AlertDefinition.name,
+ component_name: item.AlertDefinition.component_name || 'N/A'
+ }));
+ setAlertDefinitions(definitions);
+ }
+ } catch (error) {
+ console.error('[AlertsContext] Error loading alert definitions:', error);
+ }
+ }, [clusterName]);
+
+ const loadAlertDefinitionSummary = useCallback(async () => {
+ if (!clusterName) return;
+
+ try {
+ const currTime = getCurrTimeInSec();
+ const response = await
AlertsApi.getGroupFormattedAlertsNotifications(clusterName, currTime);
+
+ setAlertSummary(response);
+ } catch (error) {
+ console.error('[AlertsContext] Error loading alert summary:', error);
+ }
+ }, [clusterName]);
+
+ const loadUnhealthyAlertInstances = useCallback(async () => {
+ if (!clusterName) return;
+
+ try {
+ const response = await AlertsApi.getAlertsListDetailed(clusterName);
+
+ if (response?.items) {
+ setUnhealthyAlertInstances(response.items);
+ }
+ } catch (error) {
+ console.error('[AlertsContext] Error loading unhealthy alerts:', error);
+ }
+ }, [clusterName]);
+
+ // Initial load - once per application session (like EmberJS's
cluster_controller loadAlerts())
+ const performInitialLoad = useCallback(async () => {
+ if (!clusterName || initialLoadComplete.current) return;
+
+ setIsLoading(true);
+
+ try {
+ await loadAlertGroups();
+ await loadAlertDefinitions();
+ await loadAlertDefinitionSummary();
+ await loadUnhealthyAlertInstances();
+
+ initialLoadComplete.current = true;
+ } catch (error) {
+ console.error('[AlertsContext] Error during initial load:', error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [clusterName, loadAlertGroups, loadAlertDefinitions,
loadAlertDefinitionSummary, loadUnhealthyAlertInstances]);
+
+ useEffect(() => {
+ performInitialLoad();
+ }, [performInitialLoad]);
+
+ // Poll unhealthy alerts only while on the alerts route
+ useEffect(() => {
+ if (pollIntervalRef.current) {
+ clearInterval(pollIntervalRef.current);
+ pollIntervalRef.current = null;
+ }
+
+ if (isAlertsRoute && initialLoadComplete.current) {
+ pollIntervalRef.current = setInterval(() => {
+ loadUnhealthyAlertInstances();
+ }, UNHEALTHY_ALERTS_POLL_INTERVAL);
+ }
+
+ return () => {
+ if (pollIntervalRef.current) {
+ clearInterval(pollIntervalRef.current);
+ pollIntervalRef.current = null;
+ }
+ };
+ }, [isAlertsRoute, loadUnhealthyAlertInstances]);
+
+ // WebSocket updates for alert summary; refresh unhealthy alerts to stay in
sync
+ useEffect(() => {
+ if (!initialLoadComplete.current || parsedSocketMessages.length === 0) {
+ return;
+ }
+
+ const latestMessage = parsedSocketMessages[0];
+
+ if (latestMessage?.destination === '/events/alerts' &&
latestMessage.summaries) {
+ const clusterId = latestMessage.clusterId ||
Object.keys(latestMessage.summaries)[0];
+ const clusterSummaries = latestMessage.summaries[clusterId];
+
+ if (clusterSummaries) {
+ const updatedSummary = { alerts_summary_grouped:
Object.values(clusterSummaries) };
+ setAlertSummary(updatedSummary);
+
+ // Keep the detailed unhealthy alert list in sync with the new summary
+ loadUnhealthyAlertInstances();
+ }
+ }
+ }, [parsedSocketMessages, loadUnhealthyAlertInstances]);
+
+ const value: AlertsContextType = {
+ alertGroups,
+ alertDefinitions,
+ alertSummary,
+ unhealthyAlertInstances,
+ isLoading,
+ refreshUnhealthyAlerts: loadUnhealthyAlertInstances,
+ };
+
+ return <AlertsContext.Provider
value={value}>{children}</AlertsContext.Provider>;
+};
+
+export const useAlerts = (): AlertsContextType => {
+ const context = useContext(AlertsContext);
+ if (!context) {
+ throw new Error('useAlerts must be used within an AlertsProvider');
+ }
+ return context;
+};
diff --git a/ambari-web/latest/src/store/ServiceContext.tsx
b/ambari-web/latest/src/store/ServiceContext.tsx
index 9ed8bae806..71f8a1fd5e 100644
--- a/ambari-web/latest/src/store/ServiceContext.tsx
+++ b/ambari-web/latest/src/store/ServiceContext.tsx
@@ -36,6 +36,7 @@ import YARNService from "../models/yarn.ts";
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";
@@ -92,6 +93,9 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({
children }) => {
const [serviceStatesData, setServiceStatesData] = useState<Map<string,
any>>(new Map());
const { clusterName } = useContext(AppContext);
+
+ // Alert data from AlertsContext, used to calculate service alert counts
without a separate /alerts call
+ const { alertSummary, alertDefinitions } = useAlerts();
const isOnClusterAdminPage = location.pathname.includes('/main/admin/');
@@ -299,7 +303,7 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({
children }) => {
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);
+ await
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName,
alertSummary, alertDefinitions);
}
}
} catch (error) {
@@ -349,7 +353,7 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({
children }) => {
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);
+ await
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName,
alertSummary, alertDefinitions);
}
}
} catch (error) {
@@ -401,7 +405,7 @@ const ServiceProvider: React.FC<ServiceProviderProps> = ({
children }) => {
if (!isPollingActive) return;
try {
- const statesData = await
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName);
+ const statesData = await
centralizedServiceStateApi.fetchAllServiceStatesAndAlerts(clusterName,
alertSummary, alertDefinitions);
setServiceStatesData(statesData);
} catch (error) {
console.error('Error polling service states:', error);
diff --git a/ambari-web/latest/src/store/context.tsx
b/ambari-web/latest/src/store/context.tsx
index 6ea9741ad6..d85c11d021 100644
--- a/ambari-web/latest/src/store/context.tsx
+++ b/ambari-web/latest/src/store/context.tsx
@@ -31,8 +31,9 @@ import { Client } from "@stomp/stompjs";
import ClusterApi from "../api/clusterApi";
import { ChooseServicesApi } from "../api/chooseServicesApi";
import { ServicesApi } from "../api/servicesApi";
-import { get, isEmpty, isUndefined, map, set } from "lodash";
+import { forEach, get, isEmpty, isUndefined, map, set } from "lodash";
import ConfigsApi from "../api/configsApi";
+import VersionsApi from "../api/versionsApi";
import { mapStackConfigProperties } from "../Utils/Utility";
import useAuth from "../hooks/useAuth";
import { parsePersistedValue, persistedPayload } from
"../Utils/persistedSettings";
@@ -117,6 +118,10 @@ interface AppContextProps {
wizardUser: string;
isClusterInstalled?: boolean;
loginName: string;
+ clockDistance: number;
+ serviceCheckSupportedMap: Record<string, boolean>;
+ stackVersion: any;
+ stackVersionList: any[];
}
type BackgroundRequestPage = {
@@ -189,6 +194,10 @@ export const AppContext = createContext<AppContextProps>({
wizardUser: "",
isClusterInstalled: false,
loginName: "",
+ clockDistance: 0,
+ serviceCheckSupportedMap: {},
+ stackVersion: undefined,
+ stackVersionList: [],
});
export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
@@ -243,10 +252,65 @@ export const AppProvider: React.FC<{ children:
React.ReactNode }> = ({
}));
const [services, setServices] = useState([]);
const [stackConfigurations, setStackConfigurations] = useState([]);
+ // Service check supported map - static stack property, loaded once from
initial stack configs fetch
+ const [serviceCheckSupportedMap, setServiceCheckSupportedMap] =
useState<Record<string, boolean>>({});
+
+ // Stack versions - fetched once at startup, refreshed on /events/upgrade
(like Ember's DS.Model store)
+ const [stackVersion, setStackVersion] = useState<any>(undefined);
+ const [stackVersionList, setStackVersionList] = useState<any[]>([]);
+
+ const fetchStackVersionList = async () => {
+ try {
+ const response = await VersionsApi.getServices(clusterName);
+ setStackVersion(response);
+ const items = get(response, "items", []);
+ const list: any[] = [];
+ forEach(items, (item: any) => {
+ const repoVersionId = get(item,
"ClusterStackVersions.repository_version");
+ const repoVersion = get(item, "repository_versions", []).find(
+ (repo: any) => get(repo, "RepositoryVersions.id") === repoVersionId
+ )?.RepositoryVersions?.repository_version;
+ list.push({
+ id: get(item, "ClusterStackVersions.id"),
+ cluster_name: get(item, "ClusterStackVersions.cluster_name"),
+ stack: get(item, "ClusterStackVersions.stack"),
+ version: get(item, "ClusterStackVersions.version"),
+ state: get(item, "ClusterStackVersions.state"),
+ displayName: get(item,
"repository_versions.[0].RepositoryVersions.display_name"),
+ not_installed_hosts: get(item,
"ClusterStackVersions.host_states.NOT_REQUIRED"),
+ installing_hosts: get(item,
"ClusterStackVersions.host_states.INSTALLING"),
+ installed_hosts: get(item,
"ClusterStackVersions.host_states.INSTALLED"),
+ install_failed_hosts: get(item,
"ClusterStackVersions.host_states.INSTALL_FAILED"),
+ out_of_sync_hosts: get(item,
"ClusterStackVersions.host_states.OUT_OF_SYNC"),
+ current_hosts: get(item, "ClusterStackVersions.host_states.CURRENT"),
+ supports_revert: get(item, "ClusterStackVersions.supports_revert"),
+ repository_version_id: repoVersionId,
+ repository_version: repoVersion,
+ });
+ });
+ setStackVersionList(list);
+ } catch (error) {
+ console.error("Failed to fetch stack versions:", error);
+ }
+ };
const [userBgPreferences, setUserBgPreferences] = useState(true);
const [userTimezone, setUserTimezone] = useState(detectUserTimezone());
const [allHostNames, setAllHostNames] = useState([]);
+ // Clock distance (server - client offset), derived from serverClock
(fetched once by
+ // getAmbariProperties) instead of a separate
RootServiceComponents/server_clock request.
+ const [clockDistance, setClockDistance] = useState<number>(0);
+
+ useEffect(() => {
+ if (serverClock === null) {
+ return;
+ }
+ const clientClock = Date.now();
+ let serverClockMs = serverClock.toString();
+ serverClockMs = serverClockMs.length < 13 ? serverClockMs + "000" :
serverClockMs;
+ setClockDistance(parseInt(serverClockMs, 10) - clientClock);
+ }, [serverClock]);
+
// Background Operations - persistent cache like Ember.js singleton
const [backgroundOperations, setBackgroundOperations] =
useState<BackgroundRequest[]>([]);
const [backgroundOperationsPageSize, setBackgroundOperationsPageSizeState] =
useState(20);
@@ -349,6 +413,7 @@ export const AppProvider: React.FC<{ children:
React.ReactNode }> = ({
fetchClusterServices();
fetchAllHostNames();
fetchUpgradeStates();
+ fetchStackVersionList();
} else if (!isUndefined(isClusterInstalled) && !isClusterInstalled) {
setAppLoaded(true);
}
@@ -399,6 +464,19 @@ export const AppProvider: React.FC<{ children:
React.ReactNode }> = ({
);
const stackConfigs = mapStackConfigProperties(response);
setStackConfigurations(stackConfigs);
+
+ // Extract service_check_supported map (static stack property, like
Ember's App.services.supportsServiceCheck)
+ const checkSupportedMap: Record<string, boolean> = {};
+ if (response?.items) {
+ response.items.forEach((item: any) => {
+ const svcName = get(item, "StackServices.service_name", "");
+ const supported = get(item,
"StackServices.service_check_supported", false);
+ if (svcName) {
+ checkSupportedMap[svcName] = supported;
+ }
+ });
+ }
+ setServiceCheckSupportedMap(checkSupportedMap);
}
}
fetchStackConfigs();
@@ -614,6 +692,7 @@ export const AppProvider: React.FC<{ children:
React.ReactNode }> = ({
}
}
fetchUpgradeStates();
+ fetchStackVersionList();
}
}, [parsedSocketMessages]);
@@ -780,6 +859,10 @@ export const AppProvider: React.FC<{ children:
React.ReactNode }> = ({
wizardUser,
isClusterInstalled,
loginName: loginName || "",
+ clockDistance,
+ serviceCheckSupportedMap,
+ stackVersion,
+ stackVersionList,
}}
>
{children}
diff --git a/pom.xml b/pom.xml
index 17c19becc8..4796b3e953 100644
--- a/pom.xml
+++ b/pom.xml
@@ -284,6 +284,7 @@
<exclude>**/hdp_mon_nagios_addons.conf</exclude>
<exclude>**/*.json</exclude>
<exclude>**/*.svg</exclude>
+ <exclude>**/*.csv</exclude>
<exclude>derby.log</exclude>
<exclude>CHANGES.txt</exclude>
<exclude>pass.txt</exclude>
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]