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