You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ambari.apache.org by vi...@apache.org on 2026/05/18 09:00:56 UTC
(ambari) branch frontend-refactor updated: AMBARI-26369 : Ambari Web React: Hosts Configs Page (#4130)
This is an automated email from the ASF dual-hosted git repository.
vishalsuvagia 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 00830d8e3e AMBARI-26369 : Ambari Web React: Hosts Configs Page (#4130)
00830d8e3e is described below
commit 00830d8e3e90033d621443e48dc325c3206b4a13
Author: Himanshu Maurya <54...@users.noreply.github.com>
AuthorDate: Mon May 18 14:30:51 2026 +0530
AMBARI-26369 : Ambari Web React: Hosts Configs Page (#4130)
Co-authored-by : Kowshic-V
---
ambari-web/latest/package.json | 1 +
.../Initializers/AddComponentConfigInitializer.ts | 556 ++++
.../AddHiveComponentsInitializer.ts} | 27 +-
.../AddZooKeeperComponentsInitializer.ts | 58 +
.../latest/src/Initializers/ConfigInitializer.ts | 128 +
.../src/Initializers/ControlFlowInitializer.ts | 136 +
.../HaConfigInitializer.ts} | 33 +-
.../src/Initializers/HostsBasedInitializer.ts | 331 +++
.../Initializers/MoveComponentConfigInitializer.ts | 259 ++
.../MoveHiveComponentConfigInitializer.ts | 99 +
.../src/Initializers/MoveHmConfigInitializer.ts | 143 +
.../src/Initializers/MoveHsConfigInitializer.ts | 72 +
.../Initializers/MoveNameNodeConfigInitializer.ts | 243 ++
.../src/Initializers/MoveOSConfigInitializer.ts | 69 +
.../src/Initializers/MoveRmConfigInitializer.ts | 195 ++
.../src/Initializers/MoveWsConfigInitializer.ts | 72 +
.../src/Initializers/UtilitiesForInitialziers.ts | 168 ++
.../src/Initializers/WizardConfigInitializer.ts | 632 ++++
ambari-web/latest/src/Utils/StringUtilsObj.ts | 173 ++
ambari-web/latest/src/Utils/Utility.ts | 554 +++-
ambari-web/latest/src/Utils/adminViewRedirect.ts | 50 +
ambari-web/latest/src/Utils/cacheUtils.ts | 86 +
ambari-web/latest/src/Utils/clusterConfigUtils.ts | 121 +
ambari-web/latest/src/Utils/configs.ts | 456 +++
ambari-web/latest/src/Utils/credentialsUtils.ts | 153 +
.../DirectoriesTab.tsx => Utils/jvmFormatUtils.ts} | 16 +-
ambari-web/latest/src/Utils/sslProtocolUtils.ts | 287 ++
ambari-web/latest/src/Utils/unitConversionUtils.ts | 358 +++
ambari-web/latest/src/api/AssignMastersApi.ts | 58 -
.../src/api/{ActionsApi.ts => actionsApi.ts} | 0
ambari-web/latest/src/api/adminApi.ts | 73 +
ambari-web/latest/src/api/assignMastersApi.ts | 50 +
ambari-web/latest/src/api/cachedServiceApi.ts | 216 ++
.../latest/src/api/centralizedServiceStateApi.ts | 153 +
ambari-web/latest/src/api/chooseServicesApi.ts | 1 +
ambari-web/latest/src/api/clusterApi.ts | 24 +-
ambari-web/latest/src/api/clusterDeployment.ts | 67 +
.../src/api/{validations.ts => componentApi.ts} | 16 +-
ambari-web/latest/src/api/config/axiosConfig.ts | 1 +
.../src/api/{LogApi.ts => configHistoryApi.ts} | 20 +-
ambari-web/latest/src/api/credentialsApi.ts | 97 +
ambari-web/latest/src/api/hostsApi.ts | 42 +
ambari-web/latest/src/api/kerberosApi.ts | 68 +-
ambari-web/latest/src/api/{LogApi.ts => logApi.ts} | 1 +
ambari-web/latest/src/api/loginApi.ts | 1 +
ambari-web/latest/src/api/metricsApi.ts | 164 +-
ambari-web/latest/src/api/nameNodeFederationApi.ts | 56 +
.../src/api/{validations.ts => persistanceApi.ts} | 9 +-
.../{recommendationsApi.ts => quicklinksApi.ts} | 24 +-
ambari-web/latest/src/api/recommendationsApi.ts | 1 +
ambari-web/latest/src/api/requestApi.ts | 10 +-
.../src/api/{ServiceApi.ts => serviceApi.ts} | 1 +
ambari-web/latest/src/api/serviceConfigApi.ts | 38 +
ambari-web/latest/src/api/validations.ts | 1 +
.../src/api/{VersionsApi.ts => versionsApi.ts} | 257 +-
.../latest/src/api/{LogApi.ts => viewApi.tsx} | 21 +-
ambari-web/latest/src/api/wizardApi.ts | 2 +-
ambari-web/latest/src/components/AssignMasters.tsx | 534 ++--
.../latest/src/components/AssignMastersAddable.tsx | 1670 +++++++++++
.../latest/src/components/ConfirmationModal.tsx | 7 +-
ambari-web/latest/src/components/CustomSlider.tsx | 109 +
.../components/DependentConfigurationsModal.tsx | 225 ++
.../latest/src/components/InvalidKdcPopup.tsx | 111 +
.../OverlayBackdrop.tsx} | 17 +-
ambari-web/latest/src/components/RepoModal.tsx | 2 +-
ambari-web/latest/src/constants.ts | 405 +--
.../src/data/configs/kerberos_ui_properties.ts | 1 +
.../Step7/DatabasesTab.tsx => enums/Categories.ts} | 12 +-
ambari-web/latest/src/enums/ComponentNameEnums.ts | 86 +
ambari-web/latest/src/enums/ServiceActionEnums.ts | 66 +
.../src/enums/ServiceComponentMetricsEnums.ts | 306 ++
ambari-web/latest/src/hooks/useAuth.ts | 1 +
ambari-web/latest/src/hooks/useConfigSaver.tsx | 694 +++++
ambari-web/latest/src/hooks/useConfigs.ts | 3 +-
ambari-web/latest/src/hooks/useConfigsTags.ts | 305 ++
ambari-web/latest/src/hooks/useCopyToClipboard.ts | 1 +
ambari-web/latest/src/hooks/useDebounce.ts | 1 +
ambari-web/latest/src/hooks/useDecommissionable.ts | 14 +-
ambari-web/latest/src/hooks/useEnhancedConfigs.ts | 1110 +++++++
ambari-web/latest/src/hooks/useHostChecks.tsx | 10 +-
.../latest/src/hooks/useHostConfigUpdater.ts | 92 +-
ambari-web/latest/src/hooks/useKDCSessionState.tsx | 74 +-
ambari-web/latest/src/hooks/useLazyQuicklinks.ts | 2025 +++++++++++++
ambari-web/latest/src/hooks/usePagination.ts | 1 +
ambari-web/latest/src/hooks/usePolling.ts | 5 +-
ambari-web/latest/src/hooks/usePrevious.ts | 3 +-
ambari-web/latest/src/hooks/useServerValidation.ts | 211 ++
.../latest/src/hooks/useServiceAvailability.ts | 93 +
ambari-web/latest/src/hooks/useServiceCache.ts | 86 +
ambari-web/latest/src/hooks/useServiceDeletion.tsx | 1949 ++++++++++++
ambari-web/latest/src/hooks/useStackServices.ts | 5 +-
ambari-web/latest/src/hooks/useStackVersion.ts | 2 +-
ambari-web/latest/src/hooks/useStepWizard.ts | 3 +-
ambari-web/latest/src/hooks/useUpgrade.ts | 4 +-
.../latest/src/screens/Alerts/AlertFilters.tsx | 2 +-
.../src/screens/Alerts/HostStackVersions.tsx | 2 +-
.../ClusterAdmin/StackAndVersions/ListStack.tsx | 2 +-
.../ClusterAdmin/StackAndVersions/ListVersion.tsx | 2 +-
.../ClusterAdmin/StackAndVersions/Upgrade.tsx | 2 +-
.../StackAndVersions/UpgradeHistory.tsx | 2 +-
.../latest/src/screens/ClusterWizard/Step0.tsx | 1 -
.../latest/src/screens/ClusterWizard/Step1.tsx | 5 +-
.../latest/src/screens/ClusterWizard/Step2.tsx | 16 +-
.../latest/src/screens/ClusterWizard/Step3.tsx | 71 +-
.../latest/src/screens/ClusterWizard/Step4.tsx | 4 +-
.../latest/src/screens/ClusterWizard/Step5.tsx | 127 +
.../screens/ClusterWizard/Step7/AccountsTab.tsx | 1 -
.../screens/ClusterWizard/Step7/CredentialsTab.tsx | 1 -
.../screens/ClusterWizard/Step7/RestAllTabs.tsx | 70 +
.../src/screens/ClusterWizard/Step7/index.tsx | 2316 +++++++++++++++
.../latest/src/screens/ClusterWizard/Step8.tsx | 9 +-
.../latest/src/screens/ClusterWizard/Step9.tsx | 6 +-
.../screens/ClusterWizard/clusterStore/context.tsx | 188 ++
.../screens/ClusterWizard/clusterStore/reducer.ts | 43 +
.../screens/ClusterWizard/clusterStore/types.ts | 14 +
.../latest/src/screens/ClusterWizard/constants.ts | 1 -
.../ClusterWizard/hooks/useHostComponents.ts | 75 +
.../ClusterWizard/hooks/useServiceComponents.ts | 3 +-
.../latest/src/screens/ClusterWizard/index.tsx | 7 +-
...ckServiceComponent.ts => AssignMastersTypes.ts} | 58 +-
.../ClusterWizard/types/StackServiceComponent.ts | 47 +-
...{VersionDefinition.ts => VersionsDefinition.ts} | 0
.../src/screens/ClusterWizard/types/step7Types.ts | 17 +
.../latest/src/screens/ClusterWizard/utils.ts | 5 +-
.../src/screens/ClusterWizard/wizardSteps.tsx | 276 +-
.../src/screens/CommonConfigs/AdvancedConfigs.tsx | 1411 +++++++++
.../screens/CommonConfigs/ChooseConfigGroup.tsx | 236 ++
.../latest/src/screens/CommonConfigs/Config.tsx | 3141 ++++++++++++++++++++
.../src/screens/CommonConfigs/ConfigUtils.ts | 1795 +++++++++++
.../src/screens/CommonConfigs/TestConnection.tsx | 580 ++++
.../latest/src/screens/CommonConfigs/index.tsx | 187 ++
.../src/screens/CommonConfigs/types/index.ts | 203 ++
.../src/screens/Dashboard/DashboardHeatmaps.tsx | 10 +-
ambari-web/latest/src/screens/Heatmaps/index.tsx | 10 +-
.../AddHostWizard/wizardDataStore/context.tsx | 2 +-
.../latest/src/screens/Hosts/HostConfigs.tsx | 889 ++++++
ambari-web/latest/src/screens/Hosts/utils.tsx | 2 +-
.../src/screens/Kerberos/Kerberos_identitites.ts | 165 +
ambari-web/latest/src/store/ServiceContext.tsx | 9 +
ambari-web/latest/tsconfig.app.json | 5 +-
140 files changed, 28126 insertions(+), 1083 deletions(-)
diff --git a/ambari-web/latest/package.json b/ambari-web/latest/package.json
index 4d1a5418dd..e4379b2021 100755
--- a/ambari-web/latest/package.json
+++ b/ambari-web/latest/package.json
@@ -27,6 +27,7 @@
"isomorphic-dompurify": "^2.26.0",
"js-cookie": "^3.0.5",
"lodash": "^4.17.21",
+ "rc-slider": "^11.1.9",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-bootstrap-icons": "^1.11.6",
diff --git a/ambari-web/latest/src/Initializers/AddComponentConfigInitializer.ts b/ambari-web/latest/src/Initializers/AddComponentConfigInitializer.ts
new file mode 100644
index 0000000000..6a5394bfe3
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/AddComponentConfigInitializer.ts
@@ -0,0 +1,556 @@
+/**
+ * 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.
+ */
+
+import HaConfigInitializer from "./HaConfigInitializer";
+import { HostsBasedInitializer } from "./HostsBasedInitializer";
+import ControlFlowInitializer from "./ControlFlowInitializer";
+import UtilitiesForInitialziers from "./UtilitiesForInitialziers.ts";
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+//@ts-ignore
+class AddComponentConfigInitializer extends HaConfigInitializer {
+ protected defaultInitializers: Record<string, any>;
+ protected defaultUniqueInitializers: Record<string, any>;
+ public initializers: Record<string, any>;
+ public uniqueInitializers: Record<string, any>;
+ private __copyInitializers: Record<string, any> = {};
+ private __copyUniqueInitializers: Record<string, any> = {};
+ //@ts-ignore
+ private utilitiesForInitialziers: any;
+
+ constructor() {
+ super();
+
+ this.defaultInitializers = {
+ "zookeeper.connect": this.getHDPStackOnlyHostsPortConfig(
+ "2.2",
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "ha.zookeeper.quorum": this.getHDPStackOnlyHostsPortConfig(
+ "2.2",
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "hbase.zookeeper.quorum":
+ HostsBasedInitializer.getHostsListComponentConfig(
+ "ZOOKEEPER_SERVER",
+ true
+ ),
+ "instance.zookeeper.host": this.getHostsWithPortConfig(
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "templeton.zookeeper.hosts": this.getHostsWithPortConfig(
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "hive.cluster.delegation.token.store.zookeeper.connectString":
+ this.getHostsWithPortConfig(
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "hive.zookeeper.quorum": this.getHDPStackOnlyHostsPortConfig(
+ "2.2",
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "hadoop.registry.zk.quorum": this.getHDPStackOnlyHostsPortConfig(
+ "2.2",
+ "ZOOKEEPER_SERVER",
+ "",
+ "",
+ ",",
+ "zkClientPort",
+ true
+ ),
+ "hadoop.proxyuser.{{hiveUser}}.hosts": this.getComponentsHostsConfig(
+ [
+ "HIVE_SERVER",
+ "WEBHCAT_SERVER",
+ "HIVE_METASTORE",
+ "HIVE_SERVER_INTERACTIVE",
+ ],
+ false,
+ true
+ ),
+ "hive.metastore.uris": this.getHostsWithPortConfig(
+ "HIVE_METASTORE",
+ "thrift://",
+ "",
+ ",thrift://",
+ "hiveMetastorePort",
+ true
+ ),
+ };
+
+ this.defaultUniqueInitializers = {
+ "yarn.resourcemanager.zk-address": this.initYarnRMZkAddress,
+ "templeton.hive.properties": this._initTempletonHiveProperties,
+ "atlas.graph.index.search.solr.zookeeper-url":
+ this._initAtlasGraphIndexSearchSolrZkUrl,
+ };
+
+ this.initializers = {};
+ this.uniqueInitializers = {};
+ }
+
+ setup(): void;
+
+ setup(settings?: Record<string, any>) {
+ if (!settings) {
+ this._updateInitializers();
+ } else {
+ //@ts-ignore
+ this._updateInitializers(settings);
+ }
+ }
+
+ updateSiteObj(
+ siteConfigs: Record<string, any>,
+ configProperty: ConfigProperty
+ ): boolean {
+ if (!siteConfigs || !configProperty) return false;
+
+ const initializer = this.initializers[configProperty.name];
+ const isArray = !!(
+ initializer &&
+ (initializer.type === "json_stringified_value" ||
+ (Array.isArray(initializer) &&
+ initializer.some(
+ (item: any) => item.type === "json_stringified_value"
+ )))
+ );
+
+ UtilitiesForInitialziers.updateHostsListValue(
+ siteConfigs,
+ //@ts-ignore
+ configProperty.filename,
+ configProperty.name,
+ //@ts-ignore
+ configProperty.value,
+ isArray
+ );
+ return true;
+ }
+
+ getNameNodeHAOnlyHostsConfig(components: string[], asArray: boolean): any[] {
+ return [
+ ControlFlowInitializer.getNameNodeHAControl(),
+ //@ts-ignore
+ this.getComponentsHostsConfig(components, asArray),
+ ];
+ }
+
+ // type ComponentsHostsConfig = {
+ // type: string;
+ // components: string[];
+ // asArray: boolean;
+ // isInstalled?: boolean;
+ // };
+
+ getComponentsHostsConfig(
+ components: string | string[],
+ asArray: boolean = false,
+ isInstalled?: boolean
+ ) {
+ const ret = {
+ type: "hosts_with_components",
+ components: Array.isArray(components) ? components : [components],
+ asArray,
+ isInstalled,
+ };
+ return ret;
+ }
+
+ getHostsWithPortConfig(
+ component: string,
+ prefix: string,
+ suffix: string,
+ delimiter: string,
+ port: string,
+ portFromDependencies: boolean
+ ): {
+ type: string;
+ component: string;
+ modifier: {
+ prefix: string;
+ suffix: string;
+ delimiter: string;
+ };
+ portKey?: string;
+ port?: string;
+ componentExists?: boolean | undefined;
+ } {
+ const ret = HostsBasedInitializer.getHostsWithPortConfig(
+ component,
+ prefix,
+ suffix,
+ delimiter,
+ port,
+ portFromDependencies
+ ) as {
+ type: string;
+ component: string;
+ modifier: {
+ prefix: string;
+ suffix: string;
+ delimiter: string;
+ };
+ portKey?: string;
+ port?: string;
+ componentExists?: boolean;
+ };
+ ret.componentExists = true;
+ return ret;
+ }
+
+ getNameNodeHAOnlyHostsPortConfig(
+ component: string,
+ prefix: string,
+ suffix: string,
+ delimiter: string,
+ port: string,
+ portFromDependencies: boolean
+ ): any[] {
+ return [
+ ControlFlowInitializer.getNameNodeHAControl(),
+ HostsBasedInitializer.getHostsWithPortConfig(
+ component,
+ prefix,
+ suffix,
+ delimiter,
+ port,
+ portFromDependencies
+ ),
+ ];
+ }
+
+ getResourceManagerHAOnlyHostsPortConfig(
+ component: string,
+ prefix: string,
+ suffix: string,
+ delimiter: string,
+ port: string,
+ portFromDependencies: boolean
+ ): any[] {
+ return [
+ ControlFlowInitializer.getResourceManagerHAControl(),
+ this.getHostsWithPortConfig(
+ component,
+ prefix,
+ suffix,
+ delimiter,
+ port,
+ portFromDependencies
+ ),
+ ];
+ }
+
+ getHostsListComponentJSONStringifiedConfig(
+ component: string,
+ componentExists: boolean,
+ //@ts-ignore
+ delimiter: string
+ ): any[] {
+ return [
+ //@ts-ignore
+ this.getHostsListComponentConfig(component, componentExists),
+ //@ts-ignore
+ this.getJSONStringifiedValueConfig(),
+ ];
+ }
+
+ getHDPStackOnlyHostsPortConfig(
+ minStackVersion: string,
+ component: string,
+ prefix: string,
+ suffix: string,
+ delimiter: string,
+ port: string,
+ portFromDependencies: boolean
+ ): any[] {
+ return [
+ ControlFlowInitializer.getHDPStackVersionControl(minStackVersion),
+ this.getHostsWithPortConfig(
+ component,
+ prefix,
+ suffix,
+ delimiter,
+ port,
+ portFromDependencies
+ ),
+ ];
+ }
+
+ cleanup() {
+ this._restoreInitializers();
+ }
+
+ // private _updateInitializers(settings: Record<string, any>): void {
+ // this.__copyInitializers = { ...this.initializers };
+ // this.__copyUniqueInitializers = { ...this.uniqueInitializers };
+ //
+ // this.initializers = this._updateNames(this.initializers, settings);
+ // this.uniqueInitializers = this._updateNames(this.uniqueInitializers, settings);
+ // }
+
+ private deepCopy(obj: any): any {
+ return JSON.parse(JSON.stringify(obj));
+ }
+
+ // Updates the initializers and uniqueInitializers based on settings
+
+ private _updateInitializers(): void;
+
+ private _updateInitializers(settings?: Record<string, any>) {
+ if (!settings) {
+ this._bootstrapInitializers(Object.keys(this.defaultInitializers));
+ } else {
+ this.__copyInitializers = this.deepCopy(this.defaultInitializers);
+ const updatedInitializers = this._updateNames(
+ "defaultInitializers",
+ settings
+ );
+ this._setForComputed("defaultInitializers", updatedInitializers);
+
+ this.__copyUniqueInitializers = this.deepCopy(
+ this.defaultUniqueInitializers
+ );
+ const updatedUniqueInitializers = this._updateNames(
+ "defaultUniqueInitializers",
+ settings
+ );
+ this._setForComputed(
+ "defaultUniqueInitializers",
+ updatedUniqueInitializers
+ );
+ }
+ }
+
+ // private _restoreInitializers() {
+ // this.initializers = { ...this.defaultInitializers };
+ // this.uniqueInitializers = { ...this.defaultUniqueInitializers };
+ // }
+
+ _updateNames(sourceKey: any, settings: any) {
+ settings = settings || {};
+ //@ts-ignore
+ var source = this[sourceKey];
+ let updatedSource: Record<string, any> = {};
+ Object.keys(source).forEach((configName) => {
+ const initializer = source[configName];
+
+ // Use regex to replace any placeholder in the format {{...}} with the settings string
+ const updatedName = configName.replace(/{{.*?}}/g, settings);
+ updatedSource[updatedName] = initializer;
+ });
+ return updatedSource;
+ }
+
+ // Updates the initializers and uniqueInitializers based on settings
+ // public _updateInitializers(settings: Record<string, any> = {}): void {
+ // this.__copyInitializers = this.deepCopy(this.initializers);
+ // const updatedInitializers = this._updateNames('initializers', settings);
+ // console.log("time to set computed properties for default initializers");
+ // this._setForComputed('initializers', updatedInitializers);
+ //
+ // this.__copyUniqueInitializers = this.deepCopy(this.uniqueInitializers);
+ // const updatedUniqueInitializers = this._updateNames('uniqueInitializers', settings);
+ // this._setForComputed('uniqueInitializers', updatedUniqueInitializers);
+ // }
+
+ // Restores the initializers to their original state
+ public _restoreInitializers(): void {
+ this.__copyInitializers = this.deepCopy(this.defaultInitializers);
+ this.__copyUniqueInitializers = this.deepCopy(
+ this.defaultUniqueInitializers
+ );
+
+ if (this.__copyInitializers) {
+ this._setForComputed(
+ "defaultInitializers",
+ this.deepCopy(this.__copyInitializers)
+ );
+ }
+ if (this.__copyUniqueInitializers) {
+ this._setForComputed(
+ "defaultUniqueInitializers",
+ this.deepCopy(this.__copyUniqueInitializers)
+ );
+ }
+ }
+
+ // Sets a value for a computed property
+ private _setForComputed(key: string, value: any): void {
+ //@ts-ignore
+ this[key] = value;
+ }
+
+ // /**
+ // * Initializes the Yarn ResourceManager ZooKeeper address.
+ // * @param configProperty The configuration property to be initialized.
+ // * @param localDB The local database containing master component hosts.
+ // * @param dependencies The dependencies containing the ZooKeeper client port.
+ // * @returns The updated configuration property with the ZooKeeper address.
+ // */
+ // private _initYarnRMZkAddress(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ // const value = localDB.masterComponentHosts
+ // .filter((host: any) => host.component === 'ZOOKEEPER_SERVER' && host.isInstalled)
+ // .map((host: any) => `${host.hostName}:${dependencies.zkClientPort}`)
+ // .join(',');
+ //
+ // configProperty.value = value;
+ // configProperty.recommendedValue = value;
+ //
+ // return configProperty;
+ // }
+
+ private initYarnRMZkAddress(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ //@ts-ignore
+ return HostsBasedInitializer._initAsHostsWithPort(
+ //@ts-ignore
+ configProperty,
+ localDB,
+ dependencies,
+ {
+ component: "ZOOKEEPER_SERVER",
+ componentExists: true,
+ modifier: {
+ prefix: "",
+ suffix: "",
+ delimiter: ",",
+ },
+ portKey: "zkClientPort",
+ }
+ );
+ }
+
+ private _initTempletonHiveProperties(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ) {
+ const hostNames = localDB.masterComponentHosts
+ .filter(
+ (host: any) => host.component === "HIVE_METASTORE" && host.isInstalled
+ )
+ .map(
+ (host: any) =>
+ `thrift://${host.hostName}:${dependencies.hiveMetastorePort}`
+ )
+ .join(",");
+ configProperty.value = hostNames;
+ configProperty.recommendedValue = hostNames;
+ return configProperty;
+ }
+
+ private _initAtlasGraphIndexSearchSolrZkUrl(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ) {
+ const solr = dependencies.infraSolrZnode;
+ const hostNames = localDB.masterComponentHosts
+ .filter(
+ (host: any) => host.component === "ZOOKEEPER_SERVER" && host.isInstalled
+ )
+ .map(
+ (host: any) => `${host.hostName}:${dependencies.zkClientPort}${solr}`
+ )
+ .join(",");
+ configProperty.value = hostNames;
+ configProperty.recommendedValue = hostNames;
+ return configProperty;
+ }
+
+ //@ts-ignore
+ private _bootstrapInitializers(properties: string[] | null) {
+ const initializers: Record<string, any> = {};
+ const uniqueInitializers: Record<string, any> = {};
+ const defaultInitializers = this.defaultInitializers;
+ const defaultUniqueInitializers = this.defaultUniqueInitializers;
+
+ if (!properties) {
+ this.initializers = { ...defaultInitializers };
+ this.uniqueInitializers = { ...defaultUniqueInitializers };
+ } else {
+ properties.forEach((propertyName) => {
+ if (defaultInitializers[propertyName]) {
+ initializers[propertyName] = defaultInitializers[propertyName];
+ } else if (defaultUniqueInitializers[propertyName]) {
+ uniqueInitializers[propertyName] =
+ defaultUniqueInitializers[propertyName];
+ }
+ });
+ this.initializers = initializers;
+ this.uniqueInitializers = uniqueInitializers;
+ }
+ }
+}
+//@ts-ignore
+// Apply mixins
+interface AddComponentConfigInitializer
+ extends HostsBasedInitializer,
+ ControlFlowInitializer {}
+Object.assign(
+ AddComponentConfigInitializer.prototype,
+ HostsBasedInitializer.prototype
+);
+Object.assign(
+ AddComponentConfigInitializer.prototype,
+ ControlFlowInitializer.prototype
+);
+
+export default AddComponentConfigInitializer;
diff --git a/ambari-web/latest/src/hooks/useKDCSessionState.tsx b/ambari-web/latest/src/Initializers/AddHiveComponentsInitializer.ts
similarity index 51%
copy from ambari-web/latest/src/hooks/useKDCSessionState.tsx
copy to ambari-web/latest/src/Initializers/AddHiveComponentsInitializer.ts
index a57a7b8faa..78f502e03d 100644
--- a/ambari-web/latest/src/hooks/useKDCSessionState.tsx
+++ b/ambari-web/latest/src/Initializers/AddHiveComponentsInitializer.ts
@@ -16,14 +16,27 @@
* limitations under the License.
*/
-//TODO: This will be implemented soon to fetch KDC session state
+import AddComponentConfigInitializer from "./AddComponentConfigInitializer.ts";
-function useKDCSessionState(_cancelHandler: Function) {
- const getKDCSessionState = async (callback: Function) => {
- callback();
- };
+class AddHiveComponentsInitializer extends AddComponentConfigInitializer {
+ //@ts-ignore
+ private initializeForProperties: string[];
- return { getKDCSessionState };
+ constructor() {
+ super();
+ this.initializeForProperties = [
+ "hive.metastore.uris",
+ "templeton.hive.properties",
+ "hadoop.proxyuser.{{hiveUser}}.hosts",
+ ];
+
+ this.defaultInitializers = Object.keys(this.defaultInitializers)
+ .filter((key) => this.initializeForProperties.includes(key))
+ .reduce((filteredInitializers, key) => {
+ filteredInitializers[key] = this.defaultInitializers[key];
+ return filteredInitializers;
+ }, {} as Record<string, any>);
+ }
}
-export default useKDCSessionState;
+export default AddHiveComponentsInitializer;
diff --git a/ambari-web/latest/src/Initializers/AddZooKeeperComponentsInitializer.ts b/ambari-web/latest/src/Initializers/AddZooKeeperComponentsInitializer.ts
new file mode 100644
index 0000000000..510c190ff5
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/AddZooKeeperComponentsInitializer.ts
@@ -0,0 +1,58 @@
+/**
+ * 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.
+ */
+
+import AddComponentConfigInitializer from "./AddComponentConfigInitializer.ts";
+class AddZooKeeperComponentsInitializer extends AddComponentConfigInitializer {
+ //@ts-ignore
+ private initializeForProperties: any;
+ constructor() {
+ super();
+ //@ts-ignore
+ this.initializeForProperties = [
+ 'zookeeper.connect',
+ 'ha.zookeeper.quorum',
+ 'hbase.zookeeper.quorum',
+ 'instance.zookeeper.host',
+ 'templeton.zookeeper.hosts',
+ 'hive.cluster.delegation.token.store.zookeeper.connectString',
+ 'yarn.resourcemanager.zk-address',
+ 'hive.zookeeper.quorum',
+ 'storm.zookeeper.servers',
+ 'hadoop.registry.zk.quorum',
+ 'atlas.audit.hbase.zookeeper.quorum',
+ 'atlas.graph.index.search.solr.zookeeper-url',
+ 'atlas.graph.storage.hostname',
+ 'atlas.kafka.zookeeper.connect'
+ ]
+ this.defaultInitializers = Object.keys(this.defaultInitializers)
+ .filter((key) => this.initializeForProperties.includes(key))
+ .reduce((filteredInitializers, key) => {
+ filteredInitializers[key] = this.defaultInitializers[key];
+ return filteredInitializers;
+ }, {} as Record<string, any>);
+
+ this.defaultUniqueInitializers = Object.keys(this.defaultUniqueInitializers)
+ .filter((key) => this.initializeForProperties.includes(key))
+ .reduce((filteredInitializers, key) => {
+ filteredInitializers[key] = this.defaultUniqueInitializers[key];
+ return filteredInitializers;
+ }, {} as Record<string, any>);
+ }
+}
+
+export default AddZooKeeperComponentsInitializer;
diff --git a/ambari-web/latest/src/Initializers/ConfigInitializer.ts b/ambari-web/latest/src/Initializers/ConfigInitializer.ts
new file mode 100644
index 0000000000..6ffa620f5f
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/ConfigInitializer.ts
@@ -0,0 +1,128 @@
+/**
+ * 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.
+ */
+
+import { HostsBasedInitializer } from "./HostsBasedInitializer.ts";
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type InitializerType = {
+ name: string;
+ method: string;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+class ConfigInitializer extends HostsBasedInitializer {
+ constructor() {
+ super(); // Call the superclass constructor
+ this.initializerTypes = [
+ // ...super.initializerTypes, // Include initializerTypes from the superclass
+ { name: "host_with_port", method: "_initAsHostWithPort" },
+ { name: "hosts_with_port", method: "_initAsHostsWithPort" },
+ { name: "host_with_component", method: "_initAsHostWithComponent" },
+ { name: "hosts_with_components", method: "_initAsHostsWithComponents" },
+ {
+ name: "hosts_list_with_component",
+ method: "_initAsHostsListWithComponent",
+ },
+ ];
+ }
+
+ private _initializerFlowCode = {
+ next: 0,
+ skipNext: 1,
+ skipAll: 2,
+ };
+
+ private defaultInitializers: Record<string, any> = {};
+ private defaultUniqueInitializers: Record<string, any> = {};
+ public initializerTypes: InitializerType[] = [];
+
+ public flowNext(): number {
+ return this._initializerFlowCode.next;
+ }
+
+ public flowSkipNext(): number {
+ return this._initializerFlowCode.skipNext;
+ }
+
+ public flowSkipAll(): number {
+ return this._initializerFlowCode.skipAll;
+ }
+
+ public initialValue(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const configName = configProperty.name;
+ const initializer = this.defaultInitializers[configName];
+
+ if (initializer) {
+ return this._defaultInitializer(configProperty, localDB, dependencies);
+ }
+
+ const uniqueInitializer = this.defaultUniqueInitializers[configName];
+ if (uniqueInitializer) {
+ uniqueInitializer(configProperty, localDB, dependencies);
+ }
+
+ configProperty.recommendedValue = configProperty.value;
+ return configProperty;
+ }
+
+ private _defaultInitializer(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const initializer = this.defaultInitializers[configProperty.name];
+ if (initializer) {
+ const initializerArray = Array.isArray(initializer)
+ ? initializer
+ : [initializer];
+ for (const init of initializerArray) {
+ const initializerType = this.initializerTypes.find(
+ (type) => type.name === init.type
+ );
+ if (initializerType) {
+ const method = HostsBasedInitializer[
+ initializerType.method as keyof typeof HostsBasedInitializer
+ ] as any;
+ if (typeof method === "function") {
+ configProperty = method(
+ configProperty,
+ localDB,
+ dependencies,
+ init
+ );
+ }
+ }
+ }
+ }
+ return configProperty;
+ }
+}
+
+export default ConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/ControlFlowInitializer.ts b/ambari-web/latest/src/Initializers/ControlFlowInitializer.ts
new file mode 100644
index 0000000000..cc37aec919
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/ControlFlowInitializer.ts
@@ -0,0 +1,136 @@
+/**
+ * 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.
+ */
+
+type ConfigProperty = {
+ value: string | number;
+ recommendedValue?: string | number;
+};
+
+type TopologyLocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+type Initializer = {
+ type: string;
+ isChecker: boolean;
+ stackVersion?: string;
+};
+
+class ControlFlowInitializer {
+ //@ts-ignore
+ private initializerTypes = [
+ { name: "namenode_ha_enabled", method: "_initNameNodeHACheck" },
+ {
+ name: "resourcemanager_ha_enabled",
+ method: "_initResourceManagerHACheck",
+ },
+ { name: "hdp_stack_version_checker", method: "_initHDPStackVersionCheck" },
+ ];
+
+ public static getHDPStackVersionControl(
+ minStackVersionNumber: string
+ ): Initializer {
+ return {
+ type: "hdp_stack_version_checker",
+ isChecker: true,
+ stackVersion: minStackVersionNumber,
+ };
+ }
+
+ //@ts-ignore
+ private _initHDPStackVersionCheck(
+ //@ts-ignore
+ configProperty: ConfigProperty,
+ //@ts-ignore
+ localDB: TopologyLocalDB,
+ //@ts-ignore
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): number {
+ return this.compareVersions(
+ this.getCurrentStackVersion(),
+ initializer.stackVersion || ""
+ ) > -1
+ ? this.flowNext()
+ : this.flowSkipAll();
+ }
+
+ public static getNameNodeHAControl(): Initializer {
+ return { type: "namenode_ha_enabled", isChecker: true };
+ }
+
+ //@ts-ignore
+ private _initNameNodeHACheck(
+ //@ts-ignore
+ configProperty: ConfigProperty,
+ //@ts-ignore
+ localDB: TopologyLocalDB,
+ //@ts-ignore
+ dependencies: Dependencies
+ ): number {
+ return this.isHaEnabled() ? this.flowNext() : this.flowSkipNext();
+ }
+
+ public static getResourceManagerHAControl(): Initializer {
+ return { type: "resourcemanager_ha_enabled", isChecker: true };
+ }
+
+ //@ts-ignore
+ private _initResourceManagerHACheck(
+ //@ts-ignore
+ configProperty: ConfigProperty,
+ //@ts-ignore
+ localDB: TopologyLocalDB,
+ //@ts-ignore
+ dependencies: Dependencies
+ ): number {
+ return this.isRMHaEnabled() ? this.flowNext() : this.flowSkipNext();
+ }
+
+ private flowNext(): number {
+ return 0; // Represents "next"
+ }
+
+ private flowSkipNext(): number {
+ return 1; // Represents "skipNext"
+ }
+
+ private flowSkipAll(): number {
+ return 2; // Represents "skipAll"
+ }
+
+ private compareVersions(version1: string, version2: string): number {
+ // Add logic to compare versions
+ return version1.localeCompare(version2, undefined, { numeric: true });
+ }
+
+ private getCurrentStackVersion(): string {
+ // Replace with logic to get the current stack version
+ return "3.0.0";
+ }
+
+ private isHaEnabled(): boolean {
+ // Replace with logic to check if HA is enabled
+ return true;
+ }
+
+ private isRMHaEnabled(): boolean {
+ // Replace with logic to check if ResourceManager HA is enabled
+ return true;
+ }
+}
+
+export default ControlFlowInitializer;
\ No newline at end of file
diff --git a/ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts b/ambari-web/latest/src/Initializers/HaConfigInitializer.ts
similarity index 63%
copy from ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts
copy to ambari-web/latest/src/Initializers/HaConfigInitializer.ts
index 2f0142ca5a..cdcd886644 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/types/StackServiceComponent.ts
+++ b/ambari-web/latest/src/Initializers/HaConfigInitializer.ts
@@ -16,27 +16,18 @@
* limitations under the License.
*/
-export interface StackServiceComponent {
- componentName: string;
- isDisabled: boolean;
-}
-
-export interface ServiceComponent {
- StackServiceComponents: StackServiceComponent;
-}
+import ConfigInitializer from "./ConfigInitializer";
+//@ts-ignore
+import NnHaConfigInitializer from "../Utils/configs.ts";
-export interface Service {
- StackServices: {
- service_name: string;
- };
- components: ServiceComponent[];
+class HaConfigInitializer extends ConfigInitializer {
+ constructor() {
+ super();
+ // this.initializerTypes = [
+ // { name: "host_with_port", method: "_initAsHostWithPort" },
+ // { name: "hosts_with_port", method: "_initAsHostsWithPort" },
+ // ];
+ }
}
-export interface ServicesResponse {
- items: Service[];
-}
-
-export interface SelectedService {
- is_selected: boolean;
- service_name: string;
-}
+export default HaConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/HostsBasedInitializer.ts b/ambari-web/latest/src/Initializers/HostsBasedInitializer.ts
new file mode 100644
index 0000000000..1d5f9a6604
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/HostsBasedInitializer.ts
@@ -0,0 +1,331 @@
+/**
+ * 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.
+ */
+
+import {ExtendedTopologyLocalDB, NnHaConfigDependencies} from "../Utils/configs.ts";
+
+type ConfigProperty = {
+ value: string;
+ recommendedValue: string;
+};
+
+type TopologyLocalDB = {
+ masterComponentHosts: {
+ component: string;
+ hostName: string;
+ isInstalled?: boolean;
+ }[];
+};
+
+type Initializer = {
+ component: string | string[];
+ componentExists?: boolean;
+ modifier?: {
+ prefix?: string;
+ suffix?: string;
+ delimiter?: string;
+ regex?: string;
+ };
+ asArray?: boolean;
+ components?: string[];
+ portKey?: string;
+ port?: string;
+};
+
+type Dependencies = Record<string, string | number>;
+
+export class HostsBasedInitializer {
+ static _initAsHostWithPort(
+ configProperty: ConfigProperty,
+ localDB: TopologyLocalDB,
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): ConfigProperty {
+ const host = localDB.masterComponentHosts
+ .filter((host) => host.component === initializer.component)
+ .find((host) => host.isInstalled === initializer.componentExists);
+
+ const hostName = host?.hostName || "";
+ const port = HostsBasedInitializer.__getPort(dependencies, initializer);
+ const value =
+ (initializer.modifier?.prefix || "") +
+ hostName +
+ (port ? `:${port}` : "") +
+ (initializer.modifier?.suffix || "");
+
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+
+ return configProperty;
+ }
+
+ static getComponentsHostsConfig(
+ components: string | string[],
+ asArray: boolean = false,
+ isInstalled?: boolean
+ ) {
+ return {
+ type: "hosts_with_components",
+ components: Array.isArray(components) ? components : [components],
+ asArray,
+ isInstalled,
+ };
+ }
+
+ static getSimpleComponentConfig(
+ component: string,
+ withModifier: boolean = true
+ ) {
+ const config: any = {
+ type: "host_with_component",
+ component,
+ };
+ if (withModifier) {
+ config.modifier = {
+ type: "regexp",
+ regex: "([\\w|\\.]*)(?=:)",
+ };
+ }
+ return config;
+ }
+
+ static getComponentConfigWithAffixes(
+ component: string,
+ prefix: string = "",
+ suffix: string = ""
+ ) {
+ return {
+ type: "host_with_component",
+ component,
+ modifier: {
+ type: "regexp",
+ regex: ":\\/\\/([\\w|\\.]*)(?=:)",
+ prefix,
+ suffix,
+ },
+ };
+ }
+
+ static getHostWithPortConfig(
+ component: string,
+ componentExists: boolean,
+ prefix: string = "",
+ suffix: string = "",
+ port: string,
+ portFromDependencies: boolean = false
+ ) {
+ const config: any = {
+ type: "host_with_port",
+ component,
+ componentExists,
+ modifier: {
+ prefix,
+ suffix,
+ },
+ };
+ if (portFromDependencies) {
+ config.portKey = port;
+ } else {
+ config.port = port;
+ }
+ return config;
+ }
+
+ static getHostsWithPortConfig(
+ component: string,
+ prefix: string = "",
+ suffix: string = "",
+ delimiter: string = ",",
+ port: string,
+ portFromDependencies: boolean = false
+ ): {
+ type: string;
+ component: string;
+ modifier: {
+ prefix: string;
+ suffix: string;
+ delimiter: string;
+ };
+ portKey?: string;
+ port?: string;
+ componentExists?: boolean | undefined;
+ } {
+ const ret: {
+ type: string;
+ component: string;
+ modifier: {
+ prefix: string;
+ suffix: string;
+ delimiter: string;
+ };
+ portKey?: string;
+ port?: string;
+ componentExists?: boolean | undefined;
+ } = {
+ type: "hosts_with_port",
+ component,
+ modifier: {
+ prefix: prefix || "",
+ suffix: suffix || "",
+ delimiter: delimiter || ",",
+ },
+ };
+
+ if (portFromDependencies) {
+ ret.portKey = port;
+ } else {
+ ret.port = port;
+ }
+
+ return ret;
+ }
+
+ static _initAsHostWithComponent(
+ configProperty: ConfigProperty,
+ localDB: TopologyLocalDB,
+ //@ts-ignore
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): ConfigProperty {
+ const component = localDB.masterComponentHosts.find(
+ (host) => host.component === initializer.component
+ );
+ if (!component) {
+ return configProperty;
+ }
+ if (initializer.modifier) {
+ const replaceWith =
+ (initializer.modifier.prefix || "") +
+ component.hostName +
+ (initializer.modifier.suffix || "");
+ configProperty.value = replaceWith;
+ configProperty.recommendedValue = replaceWith;
+ } else {
+ configProperty.value = component.hostName;
+ configProperty.recommendedValue = component.hostName;
+ }
+ return configProperty;
+ }
+
+ static _initAsHostsWithComponents(
+ configProperty: ConfigProperty,
+ localDB: TopologyLocalDB,
+ //@ts-ignore
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): ConfigProperty {
+ // const hostNames = localDB.masterComponentHosts
+ // .filter((host) => initializer.components?.includes(host.component))
+ // //@ts-ignore
+ // .filter((host) => initializer.isInstalled === undefined || host.isInstalled === initializer.isInstalled)
+ // .map((host) => host.hostName)
+ // .sort();
+
+ const hostNames = Array.from(
+ new Set(
+ localDB.masterComponentHosts
+ .filter((host) => initializer.components?.includes(host.component))
+ //@ts-ignore
+ .filter(
+ (host) =>
+ //@ts-ignore
+ initializer.isInstalled === undefined ||
+ //@ts-ignore
+ host.isInstalled === initializer.isInstalled
+ )
+ .map((host) => host.hostName)
+ )
+ ).sort();
+
+ const value = initializer.asArray ? hostNames : hostNames.join(",");
+ //@ts-ignore
+ configProperty.value = value;
+ //@ts-ignore
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ static _initAsHostsWithPort(
+ configProperty: ConfigProperty,
+ localDB: TopologyLocalDB,
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): ConfigProperty {
+ const hostNames = localDB.masterComponentHosts
+ .filter((host) => host.component === initializer.component)
+ .filter(
+ (host) =>
+ initializer.componentExists === undefined ||
+ host.isInstalled === initializer.componentExists
+ )
+ .map((host) => host.hostName);
+
+ const port = HostsBasedInitializer.__getPort(dependencies, initializer);
+ const value =
+ (initializer.modifier?.prefix || "") +
+ hostNames
+ .map((hostName) => `${hostName}${port ? `:${port}` : ""}`)
+ .join(initializer.modifier?.delimiter || ",") +
+ (initializer.modifier?.suffix || "");
+
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ //@ts-ignore
+ static _initAsHostsListWithComponent(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: any): ConfigProperty {
+ const hostNames = localDB.masterComponentHosts
+ .filter(host => host.component === initializer.component && host.isInstalled === initializer.componentExists)
+ .map(host => host.hostName)
+ .join(initializer.modifier.delimiter);
+ config.value = hostNames;
+ config.recommendedValue = hostNames;
+ return config;
+ }
+
+ static getHostsListComponentConfig(
+ component: string,
+ componentExists: boolean,
+ delimiter?: string
+ ): {
+ type: string;
+ component: string;
+ componentExists: boolean;
+ modifier: {
+ delimiter: string;
+ };
+ } {
+ return {
+ type: "hosts_list_with_component",
+ component: component,
+ componentExists: componentExists,
+ modifier: {
+ delimiter: delimiter == null ? "," : delimiter, // Check for null or undefined
+ },
+ };
+ }
+
+ static __getPort(
+ dependencies: Dependencies,
+ initializer: Initializer
+ ): string | number | undefined {
+ return initializer.portKey
+ ? dependencies[initializer.portKey]
+ : initializer.port;
+ }
+}
diff --git a/ambari-web/latest/src/Initializers/MoveComponentConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveComponentConfigInitializer.ts
new file mode 100644
index 0000000000..756635ea89
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveComponentConfigInitializer.ts
@@ -0,0 +1,259 @@
+/**
+ * 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.
+ */
+
+import ConfigInitializer from './ConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+export interface ReassignComponentDependencies {
+ targetHostName: string;
+ sourceHostName: string;
+ namespaceId?: string;
+ [key: string]: any;
+}
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+export interface MoveInitializer {
+ type: string;
+ component?: string;
+ port?: string | number;
+}
+
+/**
+ * Basic class for all Initializers used for configs which are affected by some component's moving from
+ * one host to another
+ */
+export class MoveComponentConfigInitializer extends ConfigInitializer {
+ protected initializers: Record<string, MoveInitializer | MoveInitializer[]> = {};
+ protected uniqueInitializers: Record<string, string> = {};
+ private __copyInitializers?: Record<string, MoveInitializer | MoveInitializer[]>;
+ private __copyUniqueInitializers?: Record<string, string>;
+
+ constructor() {
+ super();
+ this.initializerTypes = [
+ ...this.initializerTypes,
+ { name: 'hosts_with_component', method: '_initAsHostsWithComponentConsideringMoving' },
+ { name: 'target_host', method: '_initAsTargetHost' }
+ ];
+ }
+
+ /**
+ * Setup method to update initializers with settings
+ */
+ public setup(settings: any): void {
+ this._updateInitializers(settings);
+ }
+
+ /**
+ * Cleanup method to restore initializers
+ */
+ public cleanup(): void {
+ this._restoreInitializers();
+ }
+
+ /**
+ * Get initial value for a config property
+ */
+ public initialValue(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const configName = configProperty.name;
+ const initializer = this.initializers[configName];
+
+ if (initializer) {
+ return this._moveDefaultInitializer(configProperty, localDB, dependencies);
+ }
+
+ const uniqueInitializerMethod = this.uniqueInitializers[configName];
+ if (uniqueInitializerMethod && typeof (this as any)[uniqueInitializerMethod] === 'function') {
+ return (this as any)[uniqueInitializerMethod](configProperty, localDB, dependencies);
+ }
+
+ configProperty.recommendedValue = configProperty.value;
+ return configProperty;
+ }
+
+ /**
+ * Initializer for configs with value equal to the hostName where component will be moved (with port)
+ */
+ protected _initAsTargetHost(
+ configProperty: ConfigProperty,
+ _localDB: LocalDB,
+ dependencies: Dependencies,
+ initializer: MoveInitializer
+ ): ConfigProperty {
+ const hostName = dependencies.targetHostName;
+ const port = initializer.port || '';
+ const value = `${hostName}${port ? ':' + port : ''}`;
+
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ /**
+ * Initializer for configs with value equal to the hosts list where needed component exists
+ * Value considers component-moving, so targetHostName will be added to the list
+ * and sourceHostName will be removed
+ */
+ protected _initAsHostsWithComponentConsideringMoving(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies,
+ initializer: MoveInitializer
+ ): ConfigProperty {
+ if (!initializer.component) {
+ return configProperty;
+ }
+
+ let hosts = localDB.masterComponentHosts
+ .filter((host: any) => host.component === initializer.component)
+ .map((host: any) => host.hostName);
+
+ // Remove source host and add target host
+ hosts = hosts.filter((host: string) => host !== dependencies.sourceHostName);
+ if (!hosts.includes(dependencies.targetHostName)) {
+ hosts.push(dependencies.targetHostName);
+ }
+
+ const uniqueHosts = Array.from(new Set(hosts));
+ const value = uniqueHosts.sort().join(',');
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ /**
+ * Default initializer that processes the initializer configuration
+ */
+ private _moveDefaultInitializer(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const initializer = this.initializers[configProperty.name];
+ if (initializer) {
+ const initializerArray = Array.isArray(initializer) ? initializer : [initializer];
+
+ for (const init of initializerArray) {
+ const initializerType = this.initializerTypes.find(type => type.name === init.type);
+ if (initializerType) {
+ const method = (this as any)[initializerType.method];
+ if (typeof method === 'function') {
+ configProperty = method.call(this, configProperty, localDB, dependencies, init);
+ }
+ }
+ }
+ }
+ return configProperty;
+ }
+
+ /**
+ * Setup initializers with settings to replace placeholders like {{namespaceId}}, {{suffix}}
+ * This allows config names with dynamic parts to be resolved at runtime
+ *
+ * Example:
+ * - Original: 'dfs.namenode.http-address.{{namespaceId}}.{{suffix}}'
+ * - With settings {namespaceId: 'mycluster', suffix: 'nn1'}
+ * - Becomes: 'dfs.namenode.http-address.mycluster.nn1'
+ */
+ private _updateInitializers(settings: any): void {
+ settings = settings || {};
+
+ // Backup original initializers
+ this.__copyInitializers = JSON.parse(JSON.stringify(this.initializers));
+ this.__copyUniqueInitializers = JSON.parse(JSON.stringify(this.uniqueInitializers));
+
+ // Update initializers with placeholder replacement
+ this.initializers = this._updateNames(this.initializers, settings);
+ this.uniqueInitializers = this._updateNames(this.uniqueInitializers, settings);
+ }
+
+ /**
+ * Restore initializers to their original state before placeholder replacement
+ */
+ private _restoreInitializers(): void {
+ if (this.__copyInitializers) {
+ this.initializers = JSON.parse(JSON.stringify(this.__copyInitializers));
+ this.__copyInitializers = undefined;
+ }
+ if (this.__copyUniqueInitializers) {
+ this.uniqueInitializers = JSON.parse(JSON.stringify(this.__copyUniqueInitializers));
+ this.__copyUniqueInitializers = undefined;
+ }
+ }
+
+ /**
+ * Replace placeholders in object keys with actual values from settings
+ *
+ * @param source - Object with keys that may contain placeholders
+ * @param settings - Object with replacement values
+ * @returns Updated object with placeholders replaced
+ */
+ private _updateNames<T>(source: Record<string, T>, settings: any): Record<string, T> {
+ settings = settings || {};
+ const result: Record<string, T> = {};
+
+ Object.keys(source).forEach(configName => {
+ const initializer = source[configName];
+ let updatedConfigName = configName;
+
+ // Replace each placeholder with its value from settings
+ Object.keys(settings).forEach(key => {
+ const replaceWith = String(settings[key]);
+ const toReplace = `{{${key}}}`;
+ updatedConfigName = updatedConfigName.replace(new RegExp(toReplace.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), replaceWith);
+ });
+
+ result[updatedConfigName] = initializer;
+ });
+ return result;
+ }
+
+ /**
+ * Static method to get target host config
+ */
+ public static getTargetHostConfig(port?: string | number): MoveInitializer {
+ return {
+ type: 'target_host',
+ port: port || ''
+ };
+ }
+
+ /**
+ * Static method to get hosts with component config
+ */
+ public static getHostsWithComponentConfig(component: string): MoveInitializer {
+ return {
+ type: 'hosts_with_component',
+ component: component
+ };
+ }
+}
+
+export default MoveComponentConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveHiveComponentConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveHiveComponentConfigInitializer.ts
new file mode 100644
index 0000000000..57f109f993
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveHiveComponentConfigInitializer.ts
@@ -0,0 +1,99 @@
+/**
+ * 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.
+ */
+
+import { MoveComponentConfigInitializer, MoveInitializer } from './MoveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+export interface HiveComponentInitializer extends MoveInitializer {
+ components: string[];
+ movedComponent: string;
+}
+
+/**
+ * Common class for Initializers which are used when some HIVE's component is moved from one host to another
+ */
+export class MoveHiveComponentConfigInitializer extends MoveComponentConfigInitializer {
+ constructor() {
+ super();
+ this.initializerTypes = [
+ ...this.initializerTypes,
+ { name: 'hive_hosts_with_components', method: '_initAsHostsWithComponentsConsideringMovedComponent' }
+ ];
+ }
+
+ /**
+ * Initializer for configs with value equal to the list of hosts where some components exist
+ * This list is affected by component's moving.
+ * Example: movedComponent is 'Component1', list of needed components is ['Component1', 'Component2']
+ * So, when hosts for each needed component will be mapped, host where 'Component1' was before moving ('host1') will be skipped
+ * and host where 'Component1' is moved will be added ('host2')
+ * But, if 'Component2' exists of the 'host1', 'host1' will be added to the hosts list
+ */
+ protected _initAsHostsWithComponentsConsideringMovedComponent(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies,
+ initializer: HiveComponentInitializer
+ ): ConfigProperty {
+ let allHosts: string[] = [];
+
+ initializer.components.forEach(component => {
+ let hosts = localDB.masterComponentHosts
+ .filter((host: any) => host.component === component)
+ .map((host: any) => host.hostName);
+
+ if (component === initializer.movedComponent) {
+ hosts = hosts.filter((host: string) => host !== dependencies.sourceHostName);
+ if (!hosts.includes(dependencies.targetHostName)) {
+ hosts.push(dependencies.targetHostName);
+ }
+ }
+ allHosts = allHosts.concat(hosts);
+ });
+
+ const uniqueHosts = Array.from(new Set(allHosts));
+ const value = uniqueHosts.sort().join(',');
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ /**
+ * Settings for hive_hosts_with_components-initializer
+ * Used for configs with value equal to the hosts list where needed components exist.
+ * This list is affected by component's moving
+ */
+ public static getHostsWithComponentsConfig(neededComponents: string | string[], movedComponent: string): HiveComponentInitializer {
+ return {
+ type: 'hive_hosts_with_components',
+ components: Array.isArray(neededComponents) ? neededComponents : [neededComponents],
+ movedComponent: movedComponent
+ };
+ }
+}
+
+export default MoveHiveComponentConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveHmConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveHmConfigInitializer.ts
new file mode 100644
index 0000000000..a9fb4d2e00
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveHmConfigInitializer.ts
@@ -0,0 +1,143 @@
+/**
+ * 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.
+ */
+
+import { MoveHiveComponentConfigInitializer } from './MoveHiveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+/**
+ * Initializer for configs which should be affected when Hive Metastore is moved from one host to another
+ */
+export class MoveHmConfigInitializer extends MoveHiveComponentConfigInitializer {
+ private static instance: MoveHmConfigInitializer;
+
+ constructor() {
+ super();
+
+ this.initializers = {
+ 'hadoop.proxyuser.{{hiveUser}}.hosts': MoveHiveComponentConfigInitializer.getHostsWithComponentsConfig(
+ ['HIVE_SERVER', 'HIVE_METASTORE', 'HIVE_SERVER_INTERACTIVE'],
+ 'HIVE_METASTORE'
+ )
+ };
+
+ this.uniqueInitializers = {
+ 'hive.metastore.uris': '_initHiveMetastoreUris',
+ 'templeton.hive.properties': '_initTempletonHiveProperties'
+ };
+ }
+
+ /**
+ * Unique initializer for hive.metastore.uris-config
+ * Value example: 'thrift://host1:1234,thrift://host2:1234,thrift://host3:1234'
+ */
+ protected _initHiveMetastoreUris(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const hiveMSHosts = this._getHmHostsConsideringMoved(localDB, dependencies);
+ const value = String(configProperty.value);
+
+ // Extract port from existing value
+ const portMatch = value.match(/:[0-9]{2,4}/);
+ const port = portMatch ? portMatch[0].slice(1) : '9083';
+
+ const newValue = hiveMSHosts
+ .filter((host, index, arr) => arr.indexOf(host) === index) // unique hosts
+ .map(hiveMSHost => `thrift://${hiveMSHost}:${port}`)
+ .join(',');
+
+ configProperty.value = newValue;
+ configProperty.recommendedValue = newValue;
+ return configProperty;
+ }
+
+ /**
+ * Unique initializer for templeton.hive.properties-config
+ * Replace existing hosts with new
+ * Value example: 'hive.metastore.local=false,hive.metastore.uris=thrift://host1:9083,hive.metastore.sasl.enabled=false,hive.metastore.execute.setugi=true'
+ */
+ protected _initTempletonHiveProperties(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const hiveMSHosts = this._getHmHostsConsideringMoved(localDB, dependencies);
+ let value = String(configProperty.value);
+
+ // Replace the thrift URIs in the properties string
+ const hostsString = hiveMSHosts.join('\\,');
+ value = value.replace(/thrift.+[0-9]{2,},/i, `${hostsString},`);
+
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ return configProperty;
+ }
+
+ /**
+ * Get list of hosts where HIVE_METASTORE exists considering component's moving
+ * (host where it was is removed and host where it will be is added)
+ */
+ private _getHmHostsConsideringMoved(localDB: LocalDB, dependencies: Dependencies): string[] {
+ let hiveMSHosts = localDB.masterComponentHosts
+ .filter((host: any) => host.component === 'HIVE_METASTORE')
+ .map((host: any) => host.hostName);
+
+ // Remove source host and add target host
+ hiveMSHosts = hiveMSHosts.filter((host: string) => host !== dependencies.sourceHostName);
+ if (!hiveMSHosts.includes(dependencies.targetHostName)) {
+ hiveMSHosts.push(dependencies.targetHostName);
+ }
+
+ return Array.from(new Set(hiveMSHosts)); // ensure unique hosts
+ }
+
+ // Singleton pattern for compatibility with Step3
+ public static getInstance(): MoveHmConfigInitializer {
+ if (!MoveHmConfigInitializer.instance) {
+ MoveHmConfigInitializer.instance = new MoveHmConfigInitializer();
+ }
+ return MoveHmConfigInitializer.instance;
+ }
+
+ public static setup(settings: any): void {
+ const instance = MoveHmConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ public static cleanup(): void {
+ const instance = MoveHmConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ public static initialValue(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ const instance = MoveHmConfigInitializer.getInstance();
+ return instance.initialValue(configProperty, localDB, dependencies);
+ }
+}
+
+export default MoveHmConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveHsConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveHsConfigInitializer.ts
new file mode 100644
index 0000000000..bbc3aa1d0a
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveHsConfigInitializer.ts
@@ -0,0 +1,72 @@
+/**
+ * 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.
+ */
+
+import { MoveHiveComponentConfigInitializer } from './MoveHiveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+/**
+ * Initializer for configs which should be affected when Hive Server is moved from one host to another
+ */
+export class MoveHsConfigInitializer extends MoveHiveComponentConfigInitializer {
+ private static instance: MoveHsConfigInitializer;
+
+ constructor() {
+ super();
+
+ this.initializers = {
+ 'hadoop.proxyuser.{{hiveUser}}.hosts': MoveHiveComponentConfigInitializer.getHostsWithComponentsConfig(
+ ['HIVE_SERVER', 'HIVE_METASTORE', 'HIVE_SERVER_INTERACTIVE'],
+ 'HIVE_SERVER'
+ )
+ };
+ }
+
+ // Singleton pattern for compatibility with Step3
+ public static getInstance(): MoveHsConfigInitializer {
+ if (!MoveHsConfigInitializer.instance) {
+ MoveHsConfigInitializer.instance = new MoveHsConfigInitializer();
+ }
+ return MoveHsConfigInitializer.instance;
+ }
+
+ public static setup(settings: any): void {
+ const instance = MoveHsConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ public static cleanup(): void {
+ const instance = MoveHsConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ public static initialValue(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ const instance = MoveHsConfigInitializer.getInstance();
+ return instance.initialValue(configProperty, localDB, dependencies);
+ }
+}
+
+export default MoveHsConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveNameNodeConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveNameNodeConfigInitializer.ts
new file mode 100644
index 0000000000..ccd61b86ee
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveNameNodeConfigInitializer.ts
@@ -0,0 +1,243 @@
+/**
+ * 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.
+ */
+
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+
+import {
+ MoveComponentConfigInitializer,
+ ReassignComponentDependencies,
+} from "./MoveComponentConfigInitializer";
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+/**
+ * Initializer for configs which should be affected when NameNode is moved from one host to another
+ * If NameNode HA-mode is already activated, several configs are also updated
+ */
+export class MoveNameNodeConfigInitializer extends MoveComponentConfigInitializer {
+ private static instance: MoveNameNodeConfigInitializer;
+
+ constructor() {
+ super();
+
+ // Initialize the standard initializers for NameNode configs
+ this.initializers = {
+ "dfs.namenode.http-address.{{namespaceId}}.{{suffix}}":
+ MoveComponentConfigInitializer.getTargetHostConfig(50070),
+ "dfs.namenode.https-address.{{namespaceId}}.{{suffix}}":
+ MoveComponentConfigInitializer.getTargetHostConfig(50470),
+ "dfs.namenode.rpc-address.{{namespaceId}}.{{suffix}}":
+ MoveComponentConfigInitializer.getTargetHostConfig(8020),
+ "dfs.namenode.servicerpc-address.{{namespaceId}}.{{suffix}}":
+ MoveComponentConfigInitializer.getTargetHostConfig(8021),
+ "dfs.namenode.lifeline.rpc-address.{{namespaceId}}.{{suffix}}":
+ MoveComponentConfigInitializer.getTargetHostConfig(8050),
+
+ // 'dfs.namenode.http-address': MoveComponentConfigInitializer.getTargetHostConfig(50070),
+ // 'dfs.namenode.https-address': MoveComponentConfigInitializer.getTargetHostConfig(50470),
+ // 'dfs.namenode.rpc-address': MoveComponentConfigInitializer.getTargetHostConfig(8020),
+ // 'fs.defaultFS': MoveComponentConfigInitializer.getTargetHostConfig(8020, 'hdfs://')
+ };
+
+ // Initialize unique initializers for special handling
+ this.uniqueInitializers = {
+ "instance.volumes": "_initInstanceVolumes",
+ "instance.volumes.replacements": "_initInstanceVolumesReplacements",
+ "hbase.rootdir": "_initHbaseRootDir",
+ hawq_dfs_url: "_initHawqDfsUrl",
+ };
+ }
+
+ /**
+ * Static method to get singleton instance
+ */
+ public static getInstance(): MoveNameNodeConfigInitializer {
+ if (!MoveNameNodeConfigInitializer.instance) {
+ MoveNameNodeConfigInitializer.instance =
+ new MoveNameNodeConfigInitializer();
+ }
+ return MoveNameNodeConfigInitializer.instance;
+ }
+
+ /**
+ * Static setup method for compatibility with Step3
+ */
+ public static setup(settings: any): void {
+ const instance = MoveNameNodeConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ /**
+ * Static cleanup method for compatibility with Step3
+ */
+ public static cleanup(): void {
+ const instance = MoveNameNodeConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ /**
+ * Static method to get initial value for compatibility
+ */
+ public static initialValue(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const instance = MoveNameNodeConfigInitializer.getInstance();
+ const result = instance.initialValue(configProperty, localDB, dependencies);
+
+ return result;
+ }
+
+ /**
+ * Unique initializer for instance.volumes config
+ * Value example: 'hdfs://host1:8020/apps/accumulo/data'
+ */
+
+ //@ts-ignore
+ private _initInstanceVolumes(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const reassignDeps = dependencies as ReassignComponentDependencies;
+
+ // Process regardless of HA status
+ if (this._isServiceInstalled(localDB, "ACCUMULO")) {
+ let value = String(configProperty.value);
+ value = value.replace(
+ /\/\/[^\/]*/,
+ "//" + reassignDeps.targetHostName + ":8020"
+ );
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ }
+ return configProperty;
+ }
+
+ /**
+ * Unique initializer for instance.volumes.replacements config
+ * Value example: 'hdfs://host1:8020/apps/accumulo/data hdfs://host2:8020/apps/accumulo/data'
+ */
+ //@ts-ignore
+ private _initInstanceVolumesReplacements(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const reassignDeps = dependencies as ReassignComponentDependencies;
+
+ // Process regardless of HA status
+ if (this._isServiceInstalled(localDB, "ACCUMULO")) {
+ const target =
+ "hdfs://" +
+ reassignDeps.targetHostName +
+ ":8020" +
+ "/apps/accumulo/data";
+ const source =
+ "hdfs://" +
+ reassignDeps.sourceHostName +
+ ":8020" +
+ "/apps/accumulo/data";
+ const value = source + " " + target;
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ }
+ return configProperty;
+ }
+
+ /**
+ * Unique initializer for hbase.rootdir config (for HBASE service)
+ */
+ //@ts-ignore
+ private _initHbaseRootDir(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const reassignDeps = dependencies as ReassignComponentDependencies;
+
+ // Process regardless of HA status
+ if (
+ this._isServiceInstalled(localDB, "HBASE") &&
+ configProperty.filename === "hbase-site"
+ ) {
+ let value = String(configProperty.value);
+ value = value.replace(
+ /\/\/[^\/]*/,
+ "//" + reassignDeps.targetHostName + ":8020"
+ );
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ }
+ return configProperty;
+ }
+
+ /**
+ * Unique initializer for hawq_dfs_url config (for HAWQ service)
+ */
+ // @ts-ignore
+ private _initHawqDfsUrl(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies
+ ): ConfigProperty {
+ const reassignDeps = dependencies as ReassignComponentDependencies;
+
+ // Process regardless of HA status
+ if (
+ this._isServiceInstalled(localDB, "HAWQ") &&
+ configProperty.filename === "hawq-site"
+ ) {
+ let value = String(configProperty.value);
+ value = value.replace(/(.*):/, reassignDeps.targetHostName + ":");
+ configProperty.value = value;
+ configProperty.recommendedValue = value;
+ }
+ return configProperty;
+ }
+
+ /**
+ * Helper method to check if HA is enabled
+ */
+ //@ts-ignore
+ private _isHaEnabled(localDB: LocalDB): boolean {
+ // Check if HA is enabled by looking at the localDB
+ return localDB.isHaEnabled || false;
+ }
+
+ /**
+ * Helper method to check if a service is installed
+ */
+ private _isServiceInstalled(localDB: LocalDB, serviceName: string): boolean {
+ return (
+ localDB.installedServices &&
+ localDB.installedServices.includes(serviceName)
+ );
+ }
+}
+
+export default MoveNameNodeConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveOSConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveOSConfigInitializer.ts
new file mode 100644
index 0000000000..45aaf3c146
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveOSConfigInitializer.ts
@@ -0,0 +1,69 @@
+/**
+ * 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.
+ */
+
+import { MoveComponentConfigInitializer } from './MoveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+/**
+ * Initializer for configs which should be affected when Oozie Server is moved from one host to another
+ */
+export class MoveOSConfigInitializer extends MoveComponentConfigInitializer {
+ private static instance: MoveOSConfigInitializer;
+
+ constructor() {
+ super();
+
+ this.initializers = {
+ 'hadoop.proxyuser.{{oozieUser}}.hosts': MoveComponentConfigInitializer.getHostsWithComponentConfig('OOZIE_SERVER')
+ };
+ }
+
+ // Singleton pattern for compatibility with Step3
+ public static getInstance(): MoveOSConfigInitializer {
+ if (!MoveOSConfigInitializer.instance) {
+ MoveOSConfigInitializer.instance = new MoveOSConfigInitializer();
+ }
+ return MoveOSConfigInitializer.instance;
+ }
+
+ public static setup(settings: any): void {
+ const instance = MoveOSConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ public static cleanup(): void {
+ const instance = MoveOSConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ public static initialValue(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ const instance = MoveOSConfigInitializer.getInstance();
+ return instance.initialValue(configProperty, localDB, dependencies);
+ }
+}
+
+export default MoveOSConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveRmConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveRmConfigInitializer.ts
new file mode 100644
index 0000000000..4d49b4ec8d
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveRmConfigInitializer.ts
@@ -0,0 +1,195 @@
+/**
+ * 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.
+ */
+
+import { MoveComponentConfigInitializer, MoveInitializer } from './MoveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+export interface RmHaInitializer extends MoveInitializer {
+ rmHaShouldBeEnabled: boolean;
+}
+
+/**
+ * Initializer for configs which should be affected when Resource Manager is moved from one host to another
+ * If Resource Manager HA-mode is already activated, several configs are also updated
+ */
+export class MoveRmConfigInitializer extends MoveComponentConfigInitializer {
+ private static instance: MoveRmConfigInitializer;
+
+ constructor() {
+ super();
+ this.initializerTypes = [
+ ...this.initializerTypes,
+ { name: 'rm_ha_depended', method: '_initAsRmHaDepended' },
+ { name: 'rm_ha_hawq', method: '_initAsRmHaHawq' }
+ ];
+
+ this.initializers = {
+ 'yarn.resourcemanager.hostname.{{suffix}}': MoveRmConfigInitializer.getRmHaDependedConfig(true),
+ 'yarn.resourcemanager.webapp.address.{{suffix}}': MoveRmConfigInitializer.getRmHaDependedConfig(true),
+ 'yarn.resourcemanager.webapp.https.address.{{suffix}}': MoveRmConfigInitializer.getRmHaDependedConfig(true),
+ 'yarn.resourcemanager.resource-tracker.address.{{suffix}}': MoveRmConfigInitializer.getRmHaDependedConfig(true),
+ 'yarn.resourcemanager.address.{{suffix}}': MoveRmConfigInitializer.getRmHaDependedConfig(true),
+ 'yarn.resourcemanager.ha': MoveRmConfigInitializer.getRmHaHawqConfig(true),
+ 'yarn.resourcemanager.scheduler.ha': MoveRmConfigInitializer.getRmHaHawqConfig(true)
+ };
+ }
+
+ /**
+ * Check if ResourceManager HA is enabled
+ */
+ private _isRMHaEnabled(localDB: LocalDB): boolean {
+ // Check if there are multiple ResourceManager instances
+ const rmHosts = localDB.masterComponentHosts
+ .filter((host: any) => host.component === 'RESOURCEMANAGER')
+ .map((host: any) => host.hostName);
+ return rmHosts.length > 1;
+ }
+
+ /**
+ * Initializer for configs with value equal to the target hostName
+ * and based on ResourceManager HA status
+ * Value example: 'host1:port1'
+ */
+ protected _initAsRmHaDepended(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies,
+ initializer: RmHaInitializer
+ ): ConfigProperty {
+ const isRMHaEnabled = this._isRMHaEnabled(localDB);
+
+ if (isRMHaEnabled === initializer.rmHaShouldBeEnabled) {
+ const value = String(configProperty.value);
+ const parts = value.split(':');
+ parts[0] = dependencies.targetHostName;
+ const newValue = parts.join(':');
+ configProperty.value = newValue;
+ configProperty.recommendedValue = newValue;
+ }
+ return configProperty;
+ }
+
+ /**
+ * This function generates "rm1_host:port,rm2_host:port" for HAWQ configuration (RM HA specific)
+ *
+ * configProperty = {
+ * filename: "yarn-client",
+ * name: "yarn.resourcemanager.ha",
+ * value: "ip-10-32-36-162.ore1.vpc.pivotal.io:8050"
+ * }
+ *
+ * dependencies = {
+ * rm1: "ip-10-32-38-225.ore1.vpc.pivotal.io",
+ * rm2: "ip-10-32-38-162.ore1.vpc.pivotal.io",
+ * sourceHostName: "ip-10-32-36-162.ore1.vpc.pivotal.io",
+ * targetHostName: "ip-10-32-38-34.ore1.vpc.pivotal.io"
+ * }
+ */
+ protected _initAsRmHaHawq(
+ configProperty: ConfigProperty,
+ localDB: LocalDB,
+ dependencies: Dependencies,
+ initializer: RmHaInitializer
+ ): ConfigProperty {
+ const isRMHaEnabled = this._isRMHaEnabled(localDB);
+
+ if (isRMHaEnabled === initializer.rmHaShouldBeEnabled) {
+ // Which of rm1 and rm2 is changed?
+ let rm1: string, rm2: string;
+
+ if (dependencies.rm1 === dependencies.sourceHostName) {
+ rm1 = dependencies.targetHostName;
+ rm2 = dependencies.rm2;
+ } else {
+ rm1 = dependencies.rm1;
+ rm2 = dependencies.targetHostName;
+ }
+
+ // Default ports from Hadoop YARN configuration
+ const DEFAULT_RM_PORT = 8032;
+ const DEFAULT_RM_SCHEDULER_PORT = 8030;
+
+ let newValue: string;
+ if (configProperty.name === 'yarn.resourcemanager.ha') {
+ newValue = `${rm1}:${DEFAULT_RM_PORT},${rm2}:${DEFAULT_RM_PORT}`;
+ } else {
+ newValue = `${rm1}:${DEFAULT_RM_SCHEDULER_PORT},${rm2}:${DEFAULT_RM_SCHEDULER_PORT}`;
+ }
+
+ configProperty.value = newValue;
+ configProperty.recommendedValue = newValue;
+ }
+ return configProperty;
+ }
+
+ /**
+ * Settings for rm_ha_depended-initializer
+ * Used for configs with value equal to the host name (host where component is moved)
+ */
+ public static getRmHaDependedConfig(rmHaShouldBeEnabled: boolean): RmHaInitializer {
+ return {
+ type: 'rm_ha_depended',
+ rmHaShouldBeEnabled: Boolean(rmHaShouldBeEnabled)
+ };
+ }
+
+ /**
+ * Settings for rm_ha_hawq-initializer
+ * Used for HAWQ RM HA configurations
+ */
+ public static getRmHaHawqConfig(rmHaShouldBeEnabled: boolean): RmHaInitializer {
+ return {
+ type: 'rm_ha_hawq',
+ rmHaShouldBeEnabled: Boolean(rmHaShouldBeEnabled)
+ };
+ }
+
+ // Singleton pattern for compatibility with Step3
+ public static getInstance(): MoveRmConfigInitializer {
+ if (!MoveRmConfigInitializer.instance) {
+ MoveRmConfigInitializer.instance = new MoveRmConfigInitializer();
+ }
+ return MoveRmConfigInitializer.instance;
+ }
+
+ public static setup(settings: any): void {
+ const instance = MoveRmConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ public static cleanup(): void {
+ const instance = MoveRmConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ public static initialValue(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ const instance = MoveRmConfigInitializer.getInstance();
+ return instance.initialValue(configProperty, localDB, dependencies);
+ }
+}
+
+export default MoveRmConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/MoveWsConfigInitializer.ts b/ambari-web/latest/src/Initializers/MoveWsConfigInitializer.ts
new file mode 100644
index 0000000000..e95eb1058a
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/MoveWsConfigInitializer.ts
@@ -0,0 +1,72 @@
+/**
+ * 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.
+ */
+
+import { MoveHiveComponentConfigInitializer } from './MoveHiveComponentConfigInitializer';
+
+type ConfigProperty = {
+ name: string;
+ value: string | number;
+ filename: string;
+ recommendedValue?: string | number;
+};
+
+type LocalDB = Record<string, any>;
+type Dependencies = Record<string, any>;
+
+/**
+ * Initializer for configs which should be affected when WebHCat Server is moved from one host to another
+ */
+export class MoveWsConfigInitializer extends MoveHiveComponentConfigInitializer {
+ private static instance: MoveWsConfigInitializer;
+
+ constructor() {
+ super();
+
+ this.initializers = {
+ 'hadoop.proxyuser.{{webhcatUser}}.hosts': MoveHiveComponentConfigInitializer.getHostsWithComponentsConfig(
+ ['WEBHCAT_SERVER'],
+ 'WEBHCAT_SERVER'
+ )
+ };
+ }
+
+ // Singleton pattern for compatibility with Step3
+ public static getInstance(): MoveWsConfigInitializer {
+ if (!MoveWsConfigInitializer.instance) {
+ MoveWsConfigInitializer.instance = new MoveWsConfigInitializer();
+ }
+ return MoveWsConfigInitializer.instance;
+ }
+
+ public static setup(settings: any): void {
+ const instance = MoveWsConfigInitializer.getInstance();
+ instance.setup(settings);
+ }
+
+ public static cleanup(): void {
+ const instance = MoveWsConfigInitializer.getInstance();
+ instance.cleanup();
+ }
+
+ public static initialValue(configProperty: ConfigProperty, localDB: LocalDB, dependencies: Dependencies): ConfigProperty {
+ const instance = MoveWsConfigInitializer.getInstance();
+ return instance.initialValue(configProperty, localDB, dependencies);
+ }
+}
+
+export default MoveWsConfigInitializer;
diff --git a/ambari-web/latest/src/Initializers/UtilitiesForInitialziers.ts b/ambari-web/latest/src/Initializers/UtilitiesForInitialziers.ts
new file mode 100644
index 0000000000..7b758d91db
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/UtilitiesForInitialziers.ts
@@ -0,0 +1,168 @@
+/**
+ * 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.
+ */
+
+type SiteConfigs = Record<string, string>;
+//@ts-ignore
+type UniqueHostsListParser = {
+ type: string;
+ propertyName: string;
+ method: (value: string) => string;
+};
+
+interface MasterComponent {
+ component: string;
+ hostName: string;
+ isInstalled: boolean;
+}
+
+interface HiveHostsParams {
+ fromDeleteHost: boolean;
+ deleteHiveMetaStore: boolean;
+ deleteHiveServer: boolean;
+ deleteWebHCatServer: boolean;
+ hiveMetastoreHost: string | null;
+ contentHostName: string;
+ bootstrapHostsMapping: (componentName: string) => MasterComponent[];
+}
+
+class UtilitiesForInitialziers {
+ static updateHostsListValue(
+ siteConfigs: SiteConfigs,
+ propertyType: string,
+ propertyName: string,
+ hostsList: string,
+ isArray: boolean
+ ): string {
+ const uniqueHostsListParsers = [
+ {
+ propertyName: "templeton.hive.properties",
+ type: "webhcat-site",
+ method: "getTempletonHiveHosts",
+ },
+ ];
+ let value = hostsList;
+ let propertyHosts = siteConfigs[propertyName] || "";
+ let hostsToSet = hostsList;
+
+ const parser = uniqueHostsListParsers.find(
+ (property) =>
+ property.type === propertyType && property.propertyName === propertyName
+ );
+
+ if (parser) {
+ //@ts-ignore
+ propertyHosts = parser.method(propertyHosts);
+ //@ts-ignore
+ hostsToSet = parser.method(hostsToSet);
+ } else {
+ if (isArray) {
+ const pattern = /(^\[|]$)/g;
+ propertyHosts = propertyHosts.replace(pattern, "");
+ hostsToSet = hostsToSet.replace(pattern, "");
+ }
+ propertyHosts = propertyHosts.split(",").join(",");
+ hostsToSet = hostsToSet.split(",").join(",");
+ }
+
+ if (siteConfigs[propertyName]) {
+ const diffLength = propertyHosts
+ .split(",")
+ .filter((hostName) => !hostsToSet.split(",").includes(hostName)).length;
+
+ if (
+ diffLength === 0 &&
+ propertyHosts.split(",").length === hostsToSet.split(",").length
+ ) {
+ value = siteConfigs[propertyName];
+ }
+ }
+
+ siteConfigs[propertyName] = value;
+ return value;
+ }
+ static getHiveHosts(params: HiveHostsParams): MasterComponent[] {
+ const {
+ fromDeleteHost,
+ deleteHiveMetaStore,
+ deleteHiveServer,
+ deleteWebHCatServer,
+ hiveMetastoreHost,
+ // webhcatServerHost,
+ // bootstrapHostsMapping,
+ } = params;
+
+ let removePerformed =
+ fromDeleteHost ||
+ deleteHiveMetaStore ||
+ deleteHiveServer ||
+ deleteWebHCatServer;
+
+ const hiveMasterComponents = ["HIVE_METASTORE", "HIVE_SERVER"];
+ let masterComponentsMap = hiveMasterComponents
+ .map((componentName) =>
+ UtilitiesForInitialziers.bootstrapHostsMapping(componentName)
+ )
+ .reduce((p, c) => p.concat(c), []);
+
+ if (removePerformed) {
+ masterComponentsMap = masterComponentsMap.map((masterComponent) => {
+ //@ts-ignore
+ masterComponent.isInstalled =
+ //@ts-ignore
+ masterComponent.hostName !== contentHostName;
+ return masterComponent;
+ });
+ }
+
+ if (hiveMetastoreHost) {
+ masterComponentsMap.push({
+ component: "HIVE_METASTORE",
+ hostName: hiveMetastoreHost,
+ isInstalled: !removePerformed,
+ });
+ }
+
+ // if (webhcatServerHost) {
+ // masterComponentsMap.push({
+ // component: 'WEBHCAT_SERVER',
+ // hostName: webhcatServerHost,
+ // isInstalled: !removePerformed,
+ // });
+ // }
+
+ return masterComponentsMap;
+ }
+
+ //@ts-ignore
+ static bootstrapHostsMapping(
+ componentName: string,
+ hostNames?: string[]
+ //@ts-ignore
+ ): HostMapping[] {
+ if (!hostNames) {
+ hostNames = [];
+ }
+ return hostNames.map((hostName) => ({
+ component: componentName,
+ hostName: hostName,
+ isInstalled: true,
+ }));
+ }
+}
+
+export default UtilitiesForInitialziers;
diff --git a/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts b/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts
new file mode 100644
index 0000000000..f75f8e1798
--- /dev/null
+++ b/ambari-web/latest/src/Initializers/WizardConfigInitializer.ts
@@ -0,0 +1,632 @@
+/**
+ * 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.
+ */
+
+import {
+ cloneDeep,
+ filter,
+ find,
+ get,
+ isFunction,
+ map,
+ sortBy,
+ toArray,
+ uniq,
+} from "lodash";
+import stringUtils from "../Utils/StringUtilsObj";
+
+function getZKBasedConfig() {
+ return {
+ type: "zookeeper_based",
+ };
+}
+const hostWithPort = "([\\w|\\.]*)(?=:)";
+const hostWithPrefix = "://" + hostWithPort;
+
+function WizardConfigInitializer(
+ configProperty: any,
+ _localDB: any,
+ _dependencies: any
+) {
+ const obj: any = {
+ _setRangerAdminPassword: () => {
+ var value = "P1!q" + stringUtils.getRandomString(12);
+ configProperty = {
+ ...configProperty,
+ ...{ value: value, recommendedValue: value, retypedPassword: value, confirmPassword: value },
+ };
+ return configProperty;
+ },
+
+ initializerTypes: [
+ {
+ name: "zookeeper_based",
+ method: "_initAsZookeeperServersList",
+ },
+ {
+ name: "host_with_component",
+ method: "_initAsHostWithComponent",
+ },
+ {
+ name: "hosts_with_components",
+ method: "_initAsHostsWithComponents",
+ },
+
+ {
+ name: "hosts_list_with_component",
+ method: "_initAsHostsListWithComponent",
+ },
+ ],
+ _initAsHostsListWithComponent: (
+ configProperty: any,
+ localDB: any,
+ _dependencies: any,
+ initializer?: any
+ ) => {
+ var hostNames = map(
+ filter(
+ filter(localDB.masterComponentHosts, [
+ "component",
+ initializer?.component,
+ ]),
+ ["isInstalled", initializer?.componentExists]
+ ),
+ "hostName"
+ ).join(initializer?.modifier?.delimiter);
+ configProperty = {
+ ...configProperty,
+ ...{
+ value: hostNames,
+ recommendedValue: hostNames,
+ },
+ };
+
+ return configProperty;
+ },
+ _initAsHostsWithComponents: (
+ configProperty: any,
+ localDB: any,
+ _dependencies: any,
+ initializer?: any
+ ) => {
+ let hostNames: any = map(
+ sortBy(
+ localDB.masterComponentHosts.filter(function (masterComponent: any) {
+ var hasFound = initializer?.components?.includes(
+ masterComponent.component
+ );
+ if (initializer?.isInstalled) {
+ return hasFound;
+ }
+ return (
+ hasFound &&
+ masterComponent?.isInstalled === initializer?.isInstalled
+ );
+ }),
+ "hostName"
+ ),
+ "hostName"
+ );
+ if (!initializer) {
+ hostNames = uniq(hostNames).join(",");
+ }
+ configProperty = {
+ ...configProperty,
+ ...{
+ value: hostNames,
+ recommendedValue: hostNames,
+ },
+ };
+ return configProperty;
+ },
+ _initAsHostWithComponent: (
+ configProperty: any,
+ localDB: any,
+ _dependencies: any,
+ initializer?: any
+ ) => {
+ var component = find(localDB.masterComponentHosts, [
+ "component",
+ initializer?.component,
+ ]);
+ if (!component) {
+ return configProperty;
+ }
+ if (initializer.modifier) {
+ let replaceWith =
+ (initializer.modifier.prefix || "") +
+ component.hostName +
+ (initializer.modifier.suffix || "");
+ setRecommendedValue(
+ configProperty,
+ initializer.modifier.regex,
+ replaceWith
+ );
+ } else {
+ const changedValue = configProperty.value.split(":");
+ changedValue[0] = component.hostName;
+ configProperty = {
+ ...configProperty,
+ ...{
+ recommendedValue: component.hostName,
+ value: changedValue.join(":"),
+ },
+ };
+ }
+
+ return configProperty;
+ },
+ _initHiveDatabaseValue: (configProperty: any) => {
+ var newMySQLDBOption = find(get(configProperty, "options"), [
+ "displayName",
+ "New MySQL Database",
+ ]);
+ // if (newMySQLDBOption) {
+ // var isNewMySQLDBOptionHidden =
+ // get("supports.alwaysEnableManagedMySQLForHive") &&
+ // App.get("router.currentState.name") !== "configs" &&
+ // !App.get("isManagedMySQLForHiveEnabled");
+ // if (
+ // isNewMySQLDBOptionHidden &&
+ // Em.get(configProperty, "value") === "New MySQL Database"
+ // ) {
+ // Em.set(configProperty, "value", "Existing MySQL Database");
+ // }
+ // Em.set(newMySQLDBOption, "hidden", isNewMySQLDBOptionHidden);
+ // }
+ console.log("newMySQLDBOption", newMySQLDBOption);
+
+ return configProperty;
+ },
+ _initTempletonHiveProperties: (
+ configProperty: any,
+ localDB: any,
+ dependencies: any
+ ) => {
+ var hiveMSUris = self
+ .getHiveMetastoreUris(
+ localDB.masterComponentHosts,
+ dependencies["hive.metastore.uris"]
+ )
+ .replace(",", "\\,");
+ if (/\/\/localhost:/g.test(get(configProperty, "value"))) {
+ configProperty.recommendedValue =
+ get(configProperty, "value") + ",hive.metastore.execute.setugi=true";
+ }
+ setRecommendedValue(
+ configProperty,
+ "(hive\\.metastore\\.uris=)([^\\,]+)",
+ "$1" + hiveMSUris
+ );
+ return configProperty;
+ },
+ _initHBaseZookeeperQuorum: (configProperty: any, localDB: any) => {
+ if ("hbase-site.xml" === get(configProperty, "fileName")) {
+ const zkHosts = uniq(map(
+ filter(localDB.masterComponentHosts, [
+ "component",
+ "ZOOKEEPER_SERVER",
+ ]),
+ "hostName"
+ ));
+ setRecommendedValue(configProperty, "(.*)", zkHosts.join(","));
+ }
+ return configProperty;
+ },
+ _initRangerHost: (configProperty: any, localDB: any) => {
+ var rangerAdminHost = find(localDB.masterComponentHosts, [
+ "component",
+ "RANGER_ADMIN",
+ ]);
+ if (rangerAdminHost) {
+ configProperty = {
+ ...configProperty,
+ ...{
+ value: rangerAdminHost.hostName,
+ recommendedValue: rangerAdminHost.hostName,
+ },
+ };
+ } else {
+ configProperty = {
+ ...configProperty,
+ ...{
+ isVisible: "false",
+ isRequired: "false",
+ },
+ };
+ }
+ return configProperty;
+ },
+ _initAsZookeeperServersList: (configProperty: any, localDB: any) => {
+ var zkHosts = map(
+ filter(localDB.masterComponentHosts, ["component", "ZOOKEEPER_SERVER"]),
+ "hostName"
+ );
+ var zkHostPort = zkHosts;
+ var regex = "\\w*:(\\d+)"; //regex to fetch the port
+ var portValue =
+ get(configProperty, "recommendedValue") &&
+ get(configProperty, "recommendedValue").match(new RegExp(regex));
+ if (!portValue) {
+ return configProperty;
+ }
+ if (portValue[1]) {
+ for (var i = 0; i < zkHosts.length; i++) {
+ zkHostPort[i] = zkHosts[i] + ":" + portValue[1];
+ }
+ }
+ setRecommendedValue(configProperty, "(.*)", zkHostPort);
+ return configProperty;
+ },
+ _initYarnRMzkAddress: (
+ configProperty: any,
+ localDB: any,
+ dependencies: any
+ ) => {
+ const value = filter(localDB.masterComponentHosts, [
+ "component",
+ "ZOOKEEPER_SERVER",
+ ])
+ .map(function (component) {
+ return component.hostName + ":" + dependencies.clientPort;
+ })
+ .join(",");
+
+ configProperty = {
+ ...configProperty,
+ ...{
+ value: value,
+ recommendedValue: value,
+ },
+ };
+
+ return configProperty;
+ },
+ _initHiveMetastoreUris: (
+ configProperty: any,
+ localDB: any,
+ dependencies: any
+ ) => {
+ const fileName = get(configProperty, "fileName");
+ const isHiveSite = fileName.endsWith(".xml")
+ ? fileName.slice(0, -4)
+ : fileName;
+ if (isHiveSite) {
+ var hiveMSUris = self.getHiveMetastoreUris(
+ localDB.masterComponentHosts,
+ dependencies["hive.metastore.uris"]
+ );
+ if (hiveMSUris) {
+ setRecommendedValue(configProperty, "(.*)", hiveMSUris);
+ }
+ }
+ return configProperty;
+ },
+
+ initAtlasRestAddress: (
+ configProperty: any,
+ localDB: any,
+ dependencies: any
+ ) => {
+ var atlasTls = dependencies["atlas.enableTLS"];
+ var httpPort = dependencies["atlas.server.http.port"];
+ var httpsPort = dependencies["atlas.server.https.port"];
+ var protocol = atlasTls ? "https" : "http";
+ var port = atlasTls ? httpsPort : httpPort;
+ var value = filter(localDB.masterComponentHosts, [
+ "component",
+ "ZOOKEEPER_SERVER",
+ ])
+ .map(function (component) {
+ return protocol + "://" + component.hostName + ":" + port;
+ })
+ .join(",");
+ configProperty = {
+ ...configProperty,
+ ...{
+ value,
+ recommendedValue: value,
+ },
+ };
+ return configProperty;
+ },
+ getHiveMetastoreUris: (hosts: any, recommendedValue: any) => {
+ var hiveMSHosts = map(
+ filter(hosts, ["component", "HIVE_METASTORE"]),
+ "hostName"
+ ),
+ hiveMSUris = hiveMSHosts,
+ regex = "\\w*:(\\d+)",
+ portValue =
+ recommendedValue && recommendedValue.match(new RegExp(regex));
+
+ if (!portValue) {
+ return "";
+ }
+ if (portValue[1]) {
+ for (var i = 0; i < hiveMSHosts.length; i++) {
+ hiveMSUris[i] = "thrift://" + hiveMSHosts[i] + ":" + portValue[1];
+ }
+ }
+ return hiveMSUris.join(",");
+ },
+
+ getSimpleComponentConfig: (component: any, withModifier?: any) => {
+ if (arguments.length === 1) {
+ withModifier = true;
+ }
+ let config: any = {
+ type: "host_with_component",
+ component: component,
+ };
+ if (withModifier) {
+ config.modifier = {
+ type: "regexp",
+ regex: hostWithPort,
+ };
+ }
+ return config;
+ },
+
+ getComponentConfigWithAffixes: (
+ component: any,
+ prefix: any,
+ suffix?: any
+ ) => {
+ prefix = prefix || "";
+ suffix = suffix || "";
+ return {
+ type: "host_with_component",
+ component: component,
+ modifier: {
+ type: "regexp",
+ regex: hostWithPrefix,
+ prefix: prefix,
+ suffix: suffix,
+ },
+ };
+ },
+
+ getComponentsHostsConfig: (
+ components: any,
+ asArray: any = false,
+ isInstalled?: any
+ ) => {
+ if (1 === arguments.length) {
+ asArray = false;
+ }
+ return {
+ type: "hosts_with_components",
+ components: toArray(components),
+ asArray: asArray,
+ isInstalled: isInstalled,
+ };
+ },
+ };
+ let self: any = obj;
+
+ const uniqueInitializers: any = {
+ ranger_admin_password: "_setRangerAdminPassword",
+ hive_database: "_initHiveDatabaseValue",
+ "templeton.hive.properties": "_initTempletonHiveProperties",
+ "hbase.zookeeper.quorum": "_initHBaseZookeeperQuorum",
+ "yarn.resourcemanager.zk-address": "_initYarnRMzkAddress",
+ RANGER_HOST: "_initRangerHost",
+ "hive.metastore.uris": "_initHiveMetastoreUris",
+ };
+ const initializers = {
+ "dfs.namenode.rpc-address": self.getSimpleComponentConfig("NAMENODE"),
+ "dfs.http.address": self.getSimpleComponentConfig("NAMENODE"),
+ "dfs.namenode.http-address": self.getSimpleComponentConfig("NAMENODE"),
+ "dfs.https.address": self.getSimpleComponentConfig("NAMENODE"),
+ "dfs.namenode.https-address": self.getSimpleComponentConfig("NAMENODE"),
+ "dfs.secondary.http.address":
+ self.getSimpleComponentConfig("SECONDARY_NAMENODE"),
+ "dfs.namenode.secondary.http-address":
+ self.getSimpleComponentConfig("SECONDARY_NAMENODE"),
+ "yarn.resourcemanager.hostname": self.getSimpleComponentConfig(
+ "RESOURCEMANAGER",
+ false
+ ),
+ "yarn.resourcemanager.resource-tracker.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.resourcemanager.webapp.https.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.resourcemanager.webapp.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.resourcemanager.scheduler.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.resourcemanager.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.resourcemanager.admin.address":
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "yarn.timeline-service.webapp.address": self.getSimpleComponentConfig(
+ "APP_TIMELINE_SERVER"
+ ),
+ "yarn.timeline-service.webapp.https.address": self.getSimpleComponentConfig(
+ "APP_TIMELINE_SERVER"
+ ),
+ "yarn.timeline-service.address": self.getSimpleComponentConfig(
+ "APP_TIMELINE_SERVER"
+ ),
+ "mapred.job.tracker": self.getSimpleComponentConfig("JOBTRACKER"),
+ "mapred.job.tracker.http.address":
+ self.getSimpleComponentConfig("JOBTRACKER"),
+ "mapreduce.history.server.http.address":
+ self.getSimpleComponentConfig("HISTORYSERVER"),
+ "oozie.base.url": self.getComponentConfigWithAffixes("OOZIE_SERVER", "://"),
+ hawq_dfs_url: self.getSimpleComponentConfig("NAMENODE"),
+ hawq_rm_yarn_address: self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ hawq_rm_yarn_scheduler_address:
+ self.getSimpleComponentConfig("RESOURCEMANAGER"),
+ "fs.default.name": self.getComponentConfigWithAffixes("NAMENODE", "://"),
+ "fs.defaultFS": self.getComponentConfigWithAffixes("NAMENODE", "://"),
+ "hbase.rootdir": self.getComponentConfigWithAffixes("NAMENODE", "://"),
+ "instance.volumes": self.getComponentConfigWithAffixes("NAMENODE", "://"),
+ "yarn.log.server.url": self.getComponentConfigWithAffixes(
+ "HISTORYSERVER",
+ "://"
+ ),
+ "mapreduce.jobhistory.webapp.address":
+ self.getSimpleComponentConfig("HISTORYSERVER"),
+ "mapreduce.jobhistory.address":
+ self.getSimpleComponentConfig("HISTORYSERVER"),
+ "kafka.ganglia.metrics.host": self.getSimpleComponentConfig(
+ "GANGLIA_SERVER",
+ false
+ ),
+ hive_master_hosts: self.getComponentsHostsConfig([
+ "HIVE_METASTORE",
+ "HIVE_SERVER",
+ ]),
+ hadoop_host: self.getSimpleComponentConfig("NAMENODE", false),
+ "nimbus.host": self.getSimpleComponentConfig("NIMBUS", false),
+ "nimbus.seeds": self.getComponentsHostsConfig("NIMBUS", true),
+ "storm.zookeeper.servers": self.getComponentsHostsConfig(
+ "ZOOKEEPER_SERVER",
+ true
+ ),
+ hawq_master_address_host: self.getSimpleComponentConfig(
+ "HAWQMASTER",
+ false
+ ),
+ hawq_standby_address_host: self.getSimpleComponentConfig(
+ "HAWQSTANDBY",
+ false
+ ),
+
+ "*.broker.url": {
+ type: "host_with_component",
+ component: "FALCON_SERVER",
+ modifier: {
+ type: "regexp",
+ regex: "localhost",
+ },
+ },
+
+ "zookeeper.connect": getZKBasedConfig(),
+ "hive.zookeeper.quorum": getZKBasedConfig(),
+ "templeton.zookeeper.hosts": getZKBasedConfig(),
+ "hadoop.registry.zk.quorum": getZKBasedConfig(),
+ "hive.cluster.delegation.token.store.zookeeper.connectString":
+ getZKBasedConfig(),
+ "instance.zookeeper.host": getZKBasedConfig(),
+ };
+ function _defaultInitializer(
+ configProperty: any,
+ _localDB: any,
+ _dependencies: any
+ ) {
+ let args = [].slice.call(arguments);
+ var localInitializers: any = initializers;
+ var initializer = localInitializers[get(configProperty, "propertyName")];
+
+ if (initializer) {
+ let init = initializer;
+ let _args: any = [].slice.call(args);
+ const inferredVariabled = init?.type ? init.type : init;
+ let type = find(self.initializerTypes, ["name", inferredVariabled]);
+ // add initializer-settings
+ if (type) {
+ _args.push(init);
+ let methodName: any = type.method;
+ if (!isFunction(obj[methodName])) {
+ console.log(methodName, "is not a function");
+ // throw new Error("Method " + method + " is not defined");
+ }
+ if (init.isChecker) {
+ // method(_args);
+ // if (result === flowSkipNext()) {
+ // i++; // skip next
+ // }
+ // else {
+ // if (result === self.flowSkipAll()) {
+ // break;
+ // }
+ // }
+ } else {
+ configProperty = obj[methodName]?.(..._args); // call method with args
+ }
+ }
+ }
+ return configProperty;
+ }
+ function setRecommendedValue(
+ configProperty: any,
+ regex: any,
+ replaceWith: any
+ ) {
+
+ var recommendedValue = get(configProperty, "recommendedValue");
+ recommendedValue = !recommendedValue ? (configProperty.value || "") : recommendedValue;
+ var re = new RegExp(regex);
+
+
+ recommendedValue = recommendedValue.replace(re, replaceWith);
+
+
+
+ configProperty.recommendedValue = recommendedValue;
+ let value = !get(configProperty, "recommendedValue")
+ ? ""
+ : recommendedValue;
+
+ // If final value is empty/undefined, fall back to current value
+ if (!value && configProperty.value) {
+ value = configProperty.value;
+ configProperty.recommendedValue = configProperty.value;
+
+ }
+
+
+ configProperty.value = value;
+ configProperty.initialValue = value;
+ return configProperty;
+ }
+ const initialValue = (
+ configPropertyCopy: any,
+ localDB: any,
+ dependencies: any
+ ) => {
+ let configProperty = cloneDeep(configPropertyCopy);
+ var configName = get(configProperty, "propertyName");
+ var definedInitializers: any = initializers;
+ var initializer = definedInitializers[configName];
+ if (initializer) {
+ return _defaultInitializer(configProperty, localDB, dependencies);
+ }
+
+ var uniqueInitializerName: any = uniqueInitializers[configName];
+ if (uniqueInitializerName && typeof obj[uniqueInitializerName] === 'function') {
+ return obj[uniqueInitializerName](configProperty, localDB, dependencies);
+ }
+
+
+ configProperty = {
+ ...configProperty,
+ ...{
+ initialValue: get(configProperty, "value"),
+ },
+ };
+ return configProperty;
+ };
+ return {
+ initialValue,
+ };
+}
+
+export default WizardConfigInitializer;
diff --git a/ambari-web/latest/src/Utils/StringUtilsObj.ts b/ambari-web/latest/src/Utils/StringUtilsObj.ts
new file mode 100644
index 0000000000..819c85efa4
--- /dev/null
+++ b/ambari-web/latest/src/Utils/StringUtilsObj.ts
@@ -0,0 +1,173 @@
+/**
+ * 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.
+ */
+
+interface StringUtils {
+ pad(str: string, len?: number, pad?: string, dir?: number): string;
+ underScoreToCamelCase(name: string): string;
+ getCamelCase(name: string | null): string | null;
+ compareVersions(first: string, second: string): number;
+ isSingleLine(string: string): boolean;
+ arrayToCSV(array: Array<Record<string, any>>): string;
+ getFileFromPath(path: string): string;
+ getPath(path: string): string;
+ getFormattedStringFromArray(array: string[], endSeparator?: string): string;
+ pluralize(count: number, singular: string, plural?: string): string;
+ htmlEntities(string: string): string;
+ escapeRegExp(str: string): string;
+ getRandomString(len: number, allowed?: string): string;
+ upperUnderscoreToText(string: string): string;
+ unicodeEscape(string: string, regexp?: RegExp): string;
+}
+
+const STR_PAD_LEFT = 1;
+const STR_PAD_RIGHT = 2;
+const STR_PAD_BOTH = 3;
+
+const stringUtilsObj: StringUtils = {
+ pad(str: string, len: number = 0, pad: string = ' ', dir: number = STR_PAD_RIGHT): string {
+ if (len + 1 >= str.length) {
+ switch (dir) {
+ case STR_PAD_LEFT:
+ str = Array(len + 1 - str.length).join(pad) + str;
+ break;
+ case STR_PAD_BOTH:
+ const padlen = len - str.length;
+ const right = Math.ceil(padlen / 2);
+ const left = padlen - right;
+ str = Array(left + 1).join(pad) + str + Array(right + 1).join(pad);
+ break;
+ default:
+ str = str + Array(len + 1 - str.length).join(pad);
+ break;
+ }
+ }
+ return str;
+ },
+
+ underScoreToCamelCase(name: string): string {
+ return name.replace(/_\w/g, (str) => str[1].toUpperCase());
+ },
+
+ getCamelCase(name: string | null): string | null {
+ if (name != null) {
+ return name.toLowerCase().replace(/(\b\w)/g, (f) => f.toUpperCase());
+ }
+ return name;
+ },
+
+ compareVersions(first: string, second: string): number {
+ if (!(typeof first === 'string' && typeof second === 'string')) {
+ return -1;
+ }
+ if (first === '' || second === '') {
+ return -1;
+ }
+ const firstNumbers = first.split(/[\.-]/);
+ const secondNumbers = second.split(/[\.-]/);
+ const length = Math.max(firstNumbers.length, secondNumbers.length);
+
+ for (let i = 0; i < length; i++) {
+ const firstNum = parseInt(firstNumbers[i] || '0');
+ const secondNum = parseInt(secondNumbers[i] || '0');
+
+ if (firstNum > secondNum) return 1;
+ if (firstNum < secondNum) return -1;
+ }
+ return 0;
+ },
+
+ isSingleLine(string: string): boolean {
+ return String(string).trim().indexOf("\n") === -1;
+ },
+
+ arrayToCSV(array: Array<Record<string, any>>): string {
+ return array.map(item =>
+ Object.values(item).join(',')
+ ).join('\n');
+ },
+
+ getFileFromPath(path: string): string {
+ if (!path || typeof path !== 'string') {
+ return '';
+ }
+ return path.replace(/^.*[\/]/, '');
+ },
+
+ getPath(path: string): string {
+ if (!path || typeof path !== 'string' || path[0] !== '/') {
+ return '';
+ }
+ const lastSlash = path.lastIndexOf('/');
+ return lastSlash !== 0 ? path.substr(0, lastSlash) : '/';
+ },
+
+ getFormattedStringFromArray(array: string[], endSeparator: string = 'and'): string {
+ if (array.length === 0) return '';
+ if (array.length === 1) return array[0];
+
+ const lastElement = array[array.length - 1];
+ const otherElements = array.slice(0, -1);
+
+ return `${otherElements.join(', ')} ${endSeparator} ${lastElement}`;
+ },
+
+ pluralize(count: number, singular: string, plural?: string): string {
+ const _plural = plural || `${singular}s`; // Simple pluralization if not provided
+ return count > 1 ? _plural : singular;
+ },
+
+ htmlEntities(string: string): string {
+ if (typeof string !== 'string') return "";
+ const div = document.createElement('div');
+ div.textContent = string;
+ return div.innerHTML;
+ },
+
+ escapeRegExp(str: string): string {
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ },
+
+ getRandomString(len: number, allowed?: string): string {
+ if (len <= 0) {
+ throw new Error('len should be defined and more than 0');
+ }
+
+ const chars = allowed || "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
+ return Array.from({ length: len }, () =>
+ chars.charAt(Math.floor(Math.random() * chars.length))
+ ).join('');
+ },
+
+ upperUnderscoreToText(string: string): string {
+ if (typeof string !== 'string') {
+ return '';
+ }
+ return string
+ .split('_')
+ .map(word => word.toLowerCase().replace(/^\w/, c => c.toUpperCase()))
+ .join(' ');
+ },
+
+ unicodeEscape(string: string, regexp: RegExp = /[\s\S]/g): string {
+ return string.replace(regexp, (escape) =>
+ '\\u' + ('0000' + escape.charCodeAt(0).toString(16)).slice(-4)
+ );
+ }
+};
+
+export default stringUtilsObj;
\ No newline at end of file
diff --git a/ambari-web/latest/src/Utils/Utility.ts b/ambari-web/latest/src/Utils/Utility.ts
index 1dc0bb16c4..6786ae65e5 100644
--- a/ambari-web/latest/src/Utils/Utility.ts
+++ b/ambari-web/latest/src/Utils/Utility.ts
@@ -15,6 +15,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import { ReactNode } from "react";
import { normalizeName } from "../screens/Hosts/helpers";
import _, { startCase, get, has, isEmpty } from "lodash";
import { messages } from "../screens/messages";
@@ -48,7 +50,6 @@ import DOMPurify from "isomorphic-dompurify";
import parse from 'html-react-parser';
import { t } from "i18next";
import { HostsApi } from "../api/hostsApi";
-import { JSX } from "react";
export const padNumber = (num: number) => (num < 10 ? `0${num}` : num);
const components: any = {
@@ -1508,7 +1509,7 @@ export const initialUpgradeMethods = [
isCheckComplete: false,
isCheckRequestInProgress: false,
precheckResultsMessage: "",
- preCheckResultsModalContent: null as JSX.Element | null,
+ preCheckResultsModalContent: null as ReactNode,
precheckResultsTitle: "",
action: "",
isWizardRestricted: false,
@@ -1529,7 +1530,7 @@ export const initialUpgradeMethods = [
isCheckComplete: false,
isCheckRequestInProgress: false,
precheckResultsMessage: "",
- preCheckResultsModalContent: null as JSX.Element | null,
+ preCheckResultsModalContent: null as ReactNode,
precheckResultsTitle: "",
action: "",
isWizardRestricted: false,
@@ -1547,9 +1548,554 @@ export const initialUpgradeMethods = [
isCheckComplete: false,
isCheckRequestInProgress: false,
precheckResultsMessage: "",
- preCheckResultsModalContent: null as JSX.Element | null,
+ preCheckResultsModalContent: null as ReactNode,
precheckResultsTitle: "",
action: "",
cantBeStarted: true,
},
];
+
+export const createFallbackWidgetsForService = (
+ serviceName: string,
+ userName: string,
+) => {
+ const baseWidgets = [];
+
+ // Service-specific widget configurations
+ switch (serviceName.toUpperCase()) {
+ case "AMBARI_METRICS":
+ baseWidgets.push(
+ // Master Average Load
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-master-average-load",
+ widget_name: "Average load",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "AverageLoad",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/master/AverageLoad",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Average Load",
+ value: "${AverageLoad}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase master average load",
+ },
+ },
+ // RegionServer Regions
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-region-server-regions",
+ widget_name: "Number of Regions",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "regions",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/regionserver/regions",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Regions",
+ value: "${regions}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase RegionServer regions",
+ },
+ },
+ // RegionServer Store Files
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-region-server-store-files",
+ widget_name: "Number of StoreFiles",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "storefiles",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/regionserver/storefiles",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "StoreFiles",
+ value: "${storefiles}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase RegionServer store files",
+ },
+ },
+ // RegionServer Requests
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-region-server-requests",
+ widget_name: "Total Request Count",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "requests._rate",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/regionserver/requests._rate",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Request Rate",
+ value: "${requests._rate}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase RegionServer requests",
+ },
+ },
+ // RegionServer Block Cache Hit Percent
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-region-server-block-cache-hit-percent",
+ widget_name: "Block Cache Hit Percent",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "blockCacheHitPercent",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/regionserver/blockCacheHitPercent",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Block Cache Hit %",
+ value: "${blockCacheHitPercent}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase RegionServer block cache hit percent",
+ },
+ },
+ // RegionServer Compaction Queue Size
+ {
+ WidgetInfo: {
+ id: "service-metrics-ambari-metrics-region-server-compaction-queue-size",
+ widget_name: "Compaction Queue Size",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "compactionQueueSize",
+ service_name: "AMBARI_METRICS",
+ component_name: "METRICS_COLLECTOR",
+ metric_path: "metrics/hbase/regionserver/compactionQueueSize",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Compaction Queue",
+ value: "${compactionQueueSize}",
+ },
+ ]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase RegionServer compaction queue size",
+ },
+ },
+ );
+ break;
+
+ case "HDFS":
+ baseWidgets.push(
+ // NameNode GC count
+ {
+ WidgetInfo: {
+ id: "hdfs-namenode-gc-count",
+ widget_name: "NameNode GC count",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "jvm.JvmMetrics.GcCount._rate",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/jvm/gcCount._rate",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ {
+ name: "jvm.JvmMetrics.GcCountG1 Old Generation._rate",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/jvm/GcCountG1 Old Generation._rate",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "GC total count",
+ value: "${jvm.JvmMetrics.GcCount._rate}",
+ },
+ {
+ name: "GC count of type major collection",
+ value: "${jvm.JvmMetrics.GcCountG1 Old Generation._rate}",
+ },
+ ]),
+ properties: JSON.stringify({
+ graph_type: "LINE",
+ time_range: "1",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Count of total garbage collections and count of major type garbage collections of the JVM.",
+ },
+ },
+ // NameNode GC time
+ {
+ WidgetInfo: {
+ id: "hdfs-namenode-gc-time",
+ widget_name: "NameNode GC time",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "jvm.JvmMetrics.GcTimeMillisG1 Old Generation._rate",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/jvm/GcTimeMillisG1 Old Generation._rate",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "GC time in major collection",
+ value: "${jvm.JvmMetrics.GcTimeMillisG1 Old Generation._rate}",
+ },
+ ]),
+ properties: JSON.stringify({
+ display_unit: "ms",
+ graph_type: "LINE",
+ time_range: "1",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Total time taken by major type garbage collections in milliseconds.",
+ },
+ },
+ // NN Connection Load
+ {
+ WidgetInfo: {
+ id: "hdfs-nn-connection-load",
+ widget_name: "NN Connection Load",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "rpc.rpc.client.NumOpenConnections",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/rpc/client/NumOpenConnections",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ {
+ name: "rpc.rpc.datanode.NumOpenConnections",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/rpc/datanode/NumOpenConnections",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Open Client Connections",
+ value: "${rpc.rpc.client.NumOpenConnections}",
+ },
+ {
+ name: "Open Datanode Connections",
+ value: "${rpc.rpc.datanode.NumOpenConnections}",
+ },
+ ]),
+ properties: JSON.stringify({
+ graph_type: "LINE",
+ time_range: "1",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Number of open RPC connections being managed by NameNode.",
+ },
+ },
+ // NameNode Heap
+ {
+ WidgetInfo: {
+ id: "hdfs-namenode-heap",
+ widget_name: "NameNode Heap",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "jvm.JvmMetrics.MemHeapCommittedM",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/jvm/memHeapCommittedM",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ {
+ name: "jvm.JvmMetrics.MemHeapUsedM",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/jvm/memHeapUsedM",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "JVM heap committed",
+ value: "${jvm.JvmMetrics.MemHeapCommittedM}",
+ },
+ {
+ name: "JVM heap used",
+ value: "${jvm.JvmMetrics.MemHeapUsedM}",
+ },
+ ]),
+ properties: JSON.stringify({
+ display_unit: "MB",
+ graph_type: "LINE",
+ time_range: "1",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Heap memory committed and Heap memory used with respect to time.",
+ },
+ },
+ // Failed disk volumes
+ {
+ WidgetInfo: {
+ id: "hdfs-failed-disk-volumes",
+ widget_name: "Failed disk volumes",
+ widget_type: "NUMBER",
+ metrics: JSON.stringify([
+ {
+ name: "FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.NumFailedVolumes._sum",
+ service_name: "HDFS",
+ component_name: "DATANODE",
+ metric_path: "metrics/dfs/datanode/NumFailedVolumes",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Failed disk volumes",
+ value:
+ "${FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.NumFailedVolumes._sum}",
+ },
+ ]),
+ properties: JSON.stringify({
+ display_unit: "",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Number of Failed disk volumes across all DataNodes. Its indicative of HDFS bad health.",
+ },
+ },
+ // Blocks With Corrupted Replicas
+ {
+ WidgetInfo: {
+ id: "hdfs-blocks-corrupted-replicas",
+ widget_name: "Blocks With Corrupted Replicas",
+ widget_type: "NUMBER",
+ metrics: JSON.stringify([
+ {
+ name: "Hadoop:service=NameNode,name=FSNamesystem.CorruptBlocks",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/dfs/FSNamesystem/CorruptBlocks",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Blocks With Corrupted Replicas",
+ value:
+ "${Hadoop:service=NameNode,name=FSNamesystem.CorruptBlocks}",
+ },
+ ]),
+ properties: JSON.stringify({
+ warning_threshold: "0",
+ error_threshold: "50",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Number represents data blocks with at least one corrupted replica (but not all of them). Its indicative of HDFS bad health.",
+ },
+ },
+ // Under Replicated Blocks
+ {
+ WidgetInfo: {
+ id: "hdfs-under-replicated-blocks",
+ widget_name: "Under Replicated Blocks",
+ widget_type: "NUMBER",
+ metrics: JSON.stringify([
+ {
+ name: "Hadoop:service=NameNode,name=FSNamesystem.UnderReplicatedBlocks",
+ service_name: "HDFS",
+ component_name: "NAMENODE",
+ metric_path: "metrics/dfs/FSNamesystem/UnderReplicatedBlocks",
+ host_component_criteria:
+ "host_components/metrics/dfs/FSNamesystem/HAState=active",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "Under Replicated Blocks",
+ value:
+ "${Hadoop:service=NameNode,name=FSNamesystem.UnderReplicatedBlocks}",
+ },
+ ]),
+ properties: JSON.stringify({
+ warning_threshold: "0",
+ error_threshold: "50",
+ }),
+ scope: "USER",
+ author: userName,
+ description:
+ "Number represents file blocks that does not meet the replication factor criteria. Its indicative of HDFS bad health.",
+ },
+ },
+ // HDFS Space Utilization
+ {
+ WidgetInfo: {
+ id: "hdfs-space-utilization",
+ widget_name: "HDFS Space Utilization",
+ widget_type: "GAUGE",
+ metrics: JSON.stringify([
+ {
+ name: "FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.Remaining",
+ service_name: "HDFS",
+ component_name: "DATANODE",
+ metric_path:
+ "metrics/FSDatasetState/org/apache/hadoop/hdfs/server/datanode/fsdataset/impl/FsDatasetImpl/Remaining",
+ },
+ {
+ name: "FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.Capacity",
+ service_name: "HDFS",
+ component_name: "DATANODE",
+ metric_path: "metrics/dfs/datanode/Capacity",
+ },
+ ]),
+ values: JSON.stringify([
+ {
+ name: "HDFS Space Utilization",
+ value:
+ "${(FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.Capacity - FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.Remaining)/FSDatasetState.org.apache.hadoop.hdfs.server.datanode.fsdataset.impl.FsDatasetImpl.Capacity}",
+ },
+ ]),
+ properties: JSON.stringify({
+ warning_threshold: "0.75",
+ error_threshold: "0.9",
+ }),
+ scope: "USER",
+ author: userName,
+ description: "Percentage of available space used in the DFS.",
+ },
+ },
+ );
+ break;
+
+ case "YARN":
+ baseWidgets.push({
+ WidgetInfo: {
+ id: "yarn-resourcemanager-heap",
+ widget_name: "ResourceManager Heap",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "resourcemanager_heap",
+ service_name: "YARN",
+ component_name: "RESOURCEMANAGER",
+ metric_path: "metrics/jvm/HeapMemoryUsed",
+ },
+ ]),
+ values: JSON.stringify([]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "ResourceManager Heap Memory",
+ },
+ });
+ break;
+
+ case "HBASE":
+ baseWidgets.push({
+ WidgetInfo: {
+ id: "hbase-master-heap",
+ widget_name: "HBase Master Heap",
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "hbase_master_heap",
+ service_name: "HBASE",
+ component_name: "HBASE_MASTER",
+ metric_path: "metrics/jvm/HeapMemoryUsed",
+ },
+ ]),
+ values: JSON.stringify([]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: "HBase Master Heap Memory",
+ },
+ });
+ break;
+
+ default:
+ baseWidgets.push({
+ WidgetInfo: {
+ id: `${serviceName.toLowerCase()}-default-widget`,
+ widget_name: `${serviceName} Default Widget`,
+ widget_type: "GRAPH",
+ metrics: JSON.stringify([
+ {
+ name: "default_metric",
+ service_name: serviceName,
+ component_name: "DEFAULT_COMPONENT",
+ metric_path: "metrics/jvm/HeapMemoryUsed",
+ },
+ ]),
+ values: JSON.stringify([]),
+ properties: JSON.stringify({}),
+ scope: "USER",
+ author: userName,
+ description: `Default ${serviceName} widget`,
+ },
+ });
+ }
+
+ return baseWidgets;
+};
\ No newline at end of file
diff --git a/ambari-web/latest/src/Utils/adminViewRedirect.ts b/ambari-web/latest/src/Utils/adminViewRedirect.ts
new file mode 100644
index 0000000000..764a39600d
--- /dev/null
+++ b/ambari-web/latest/src/Utils/adminViewRedirect.ts
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+import { ServiceApi } from '../api/serviceApi';
+
+export async function redirectToAdminView(adminPage = "") {
+// if (!localStorage.getItem('app.isAuthorized.CLUSTER.UPGRADE_DOWNGRADE_STACK')) {
+// window.location.replace('/#/login');
+// return;
+// }
+
+ try {
+ const data = await ServiceApi.getAmbariServerVersion();
+ const components = data?.components || [];
+
+ if (Array.isArray(components)) {
+ const mappedVersions = components
+ .filter(component =>
+ component?.RootServiceComponents?.component_name === 'AMBARI_SERVER' &&
+ component?.RootServiceComponents?.component_version
+ )
+ .map(component => component.RootServiceComponents.component_version);
+
+ if (mappedVersions.length > 0) {
+ const sortedVersions = mappedVersions.sort();
+ const latestVersion = sortedVersions[sortedVersions.length - 1].replace(/[^\d.-]/g, '');
+ const appRoot = window.location.origin;
+ window.location.replace(`${appRoot}/views/ADMIN_VIEW/${latestVersion}/INSTANCE/#/${adminPage}`);
+ }
+ }
+ } catch (error) {
+ console.error('Error getting admin version:', error);
+ window.location.replace('/#/main/views/index');
+ }
+}
\ No newline at end of file
diff --git a/ambari-web/latest/src/Utils/cacheUtils.ts b/ambari-web/latest/src/Utils/cacheUtils.ts
new file mode 100644
index 0000000000..63b3a6f0e1
--- /dev/null
+++ b/ambari-web/latest/src/Utils/cacheUtils.ts
@@ -0,0 +1,86 @@
+/**
+ * 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.
+ */
+
+interface CacheItem<T> {
+ data: T;
+ timestamp: number;
+ ttl: number;
+}
+
+class SimpleCache {
+ private cache = new Map<string, CacheItem<any>>();
+
+ set<T>(key: string, data: T, ttlMs: number = 30000): void {
+ this.cache.set(key, {
+ data,
+ timestamp: Date.now(),
+ ttl: ttlMs,
+ });
+ }
+
+ get<T>(key: string): T | null {
+ const item = this.cache.get(key);
+ if (!item) return null;
+
+ const now = Date.now();
+ if (now - item.timestamp > item.ttl) {
+ this.cache.delete(key);
+ return null;
+ }
+
+ return item.data;
+ }
+
+ has(key: string): boolean {
+ const item = this.cache.get(key);
+ if (!item) return false;
+
+ const now = Date.now();
+ if (now - item.timestamp > item.ttl) {
+ this.cache.delete(key);
+ return false;
+ }
+
+ return true;
+ }
+
+ clear(): void {
+ this.cache.clear();
+ }
+
+ delete(key: string): boolean {
+ return this.cache.delete(key);
+ }
+
+ // Clean expired entries
+ cleanup(): void {
+ const now = Date.now();
+ for (const [key, item] of this.cache.entries()) {
+ if (now - item.timestamp > item.ttl) {
+ this.cache.delete(key);
+ }
+ }
+ }
+}
+
+export const serviceCache = new SimpleCache();
+
+// Cleanup expired entries every 5 minutes
+setInterval(() => {
+ serviceCache.cleanup();
+}, 5 * 60 * 1000);
diff --git a/ambari-web/latest/src/Utils/clusterConfigUtils.ts b/ambari-web/latest/src/Utils/clusterConfigUtils.ts
new file mode 100644
index 0000000000..999ab24913
--- /dev/null
+++ b/ambari-web/latest/src/Utils/clusterConfigUtils.ts
@@ -0,0 +1,121 @@
+/**
+ * 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.
+ */
+
+import ClusterApi from "../api/clusterApi";
+import ConfigsApi from "../api/configsApi";
+import { ambariApi } from "../api/config/axiosConfig";
+
+/**
+ * Safely update cluster-env configuration while preserving existing properties
+ * This prevents the auto-start settings from being accidentally reset
+ */
+export const safeUpdateClusterEnvConfig = async (
+ clusterName: string,
+ newProperties: Record<string, any>,
+ serviceConfigVersionNote?: string
+): Promise<any> => {
+ try {
+
+ // Get current cluster configuration
+ const response = await ClusterApi.getCluster(clusterName);
+ const desiredConfigs = response?.Clusters?.desired_configs;
+
+ if (!desiredConfigs || !desiredConfigs["cluster-env"]) {
+ throw new Error('cluster-env configuration not found');
+ }
+
+ const clusterEnvTag = desiredConfigs["cluster-env"].tag;
+
+ // Get current cluster-env configuration with all properties
+ const configUrl = `clusters/${clusterName}/configurations?type=cluster-env&tag=${clusterEnvTag}&fields=*`;
+ const configResponse = await ambariApi.request({
+ url: configUrl,
+ method: "GET",
+ });
+
+ if (!configResponse.data || !configResponse.data.items || configResponse.data.items.length === 0) {
+ throw new Error('Failed to get current cluster-env configuration');
+ }
+
+ const clusterEnvConfig = configResponse.data.items[0];
+
+ // Get ALL existing properties from the current configuration
+ const existingProperties = clusterEnvConfig.properties || {};
+
+
+ // Create updated properties by merging existing properties with the new values
+ // This prevents data loss by preserving all existing configuration including recovery_enabled
+ const updatedProperties = {
+ ...existingProperties,
+ ...newProperties
+ };
+
+
+ // Save the updated configuration using the proper service config API structure
+ const configData = {
+ Clusters: {
+ desired_config: {
+ type: "cluster-env",
+ tag: `version${Date.now()}`,
+ properties: updatedProperties, // Send ALL properties, not just the changed ones
+ service_config_version_note: serviceConfigVersionNote || "Updated cluster-env configuration"
+ }
+ }
+ };
+
+ const saveResponse = await ConfigsApi.saveConfigs(clusterName, configData);
+
+ return saveResponse;
+ } catch (error) {
+ console.error("Error safely updating cluster-env config:", error);
+ throw error;
+ }
+};
+
+/**
+ * Get current cluster-env properties
+ */
+export const getClusterEnvProperties = async (clusterName: string): Promise<Record<string, any>> => {
+ try {
+ const response = await ClusterApi.getCluster(clusterName);
+ const desiredConfigs = response?.Clusters?.desired_configs;
+
+ if (!desiredConfigs || !desiredConfigs["cluster-env"]) {
+ throw new Error('cluster-env configuration not found');
+ }
+
+ const clusterEnvTag = desiredConfigs["cluster-env"].tag;
+
+ // Get current cluster-env configuration
+ const configUrl = `clusters/${clusterName}/configurations?type=cluster-env&tag=${clusterEnvTag}&fields=*`;
+ const configResponse = await ambariApi.request({
+ url: configUrl,
+ method: "GET",
+ });
+
+ if (!configResponse.data || !configResponse.data.items || configResponse.data.items.length === 0) {
+ throw new Error('Failed to get current cluster-env configuration');
+ }
+
+ const clusterEnvConfig = configResponse.data.items[0];
+ return clusterEnvConfig.properties || {};
+ } catch (error) {
+ console.error("Error getting cluster-env properties:", error);
+ throw error;
+ }
+};
diff --git a/ambari-web/latest/src/Utils/configs.ts b/ambari-web/latest/src/Utils/configs.ts
new file mode 100644
index 0000000000..5f179ef961
--- /dev/null
+++ b/ambari-web/latest/src/Utils/configs.ts
@@ -0,0 +1,456 @@
+/**
+ * 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.
+ */
+
+// NnHaConfigInitializer.tsx
+// interfaces.ts
+export interface ConfigProperty {
+ name: string;
+ displayName: string;
+ value: string;
+ recommendedValue: string;
+ isVisible?: boolean;
+ isOverridable?: boolean;
+ filename?: string;
+}
+
+export interface ExtendedTopologyLocalDB {
+ installedServices: string[];
+ masterComponentHosts: MasterComponentHost[];
+ nameserviceId: string;
+}
+
+export interface MasterComponentHost {
+ component: string;
+ hostName: string;
+ isInstalled: boolean;
+}
+
+export interface NnHaConfigDependencies {
+ namespaceId: string;
+ serverConfigs: ServerConfig[];
+ isHadoopWindowsStack?: boolean;
+}
+
+export interface ServerConfig {
+ type: string;
+ properties: { [key: string]: string };
+}
+
+export interface Initializer {
+ type: any;
+ toReplace?: string;
+ modifier?: {
+ prefix?: string;
+ suffix?: string;
+ regex?: string;
+ delimiter?: string;
+ };
+ component?: any;
+ components?: any;
+ componentExists?: boolean;
+ portKey?: string;
+ port?: string;
+ asArray?: boolean;
+ isInstalled?: boolean;
+}
+// NnHaConfigInitializer.tsx
+
+// NnHaConfigInitializer.tsx
+
+class NnHaConfigInitializer {
+ static getRenameWithNamespaceConfig(toReplace: string): Initializer {
+ return {
+ type: 'rename',
+ toReplace: toReplace
+ };
+ }
+
+ static getNamespaceConfig(prefix: string = '', suffix: string = ''): Initializer {
+ return {
+ type: 'namespace',
+ modifier: {
+ prefix: prefix,
+ suffix: suffix
+ }
+ };
+ }
+
+ static getReplaceNamespaceConfig(toReplace: string): Initializer {
+ return {
+ type: 'replace_namespace',
+ toReplace: toReplace
+ };
+ }
+
+ static getHostWithPortConfig(component: string, componentExists: boolean, prefix: string = '', suffix: string = '', port: string, portFromDependencies: boolean = false): Initializer {
+ return {
+ type: 'host_with_port',
+ component: component,
+ componentExists: componentExists,
+ modifier: {
+ prefix: prefix,
+ suffix: suffix
+ },
+ portKey: portFromDependencies ? port : undefined,
+ port: portFromDependencies ? undefined : port
+ };
+ }
+
+ static getHostsWithPortConfig(component: string | string[], prefix: string = '', suffix: string = '', delimiter: string = ',', port: string, portFromDependencies: boolean = false): Initializer {
+ return {
+ type: 'hosts_with_port',
+ component: component,
+ modifier: {
+ prefix: prefix,
+ suffix: suffix,
+ delimiter: delimiter
+ },
+ portKey: portFromDependencies ? port : undefined,
+ port: portFromDependencies ? undefined : port
+ };
+ }
+
+ static getComponentsHostsConfig(components: string | string[], asArray: boolean = false, isInstalled: boolean | undefined = undefined): Initializer {
+ return {
+ type: 'hosts_with_components',
+ components: Array.isArray(components) ? components : [components],
+ asArray: asArray,
+ isInstalled: isInstalled
+ };
+ }
+
+ static getSimpleComponentConfig(component: string, withModifier: boolean = true): Initializer {
+ return {
+ type: 'host_with_component',
+ component: component,
+ modifier: withModifier ? {
+ type: 'regexp',
+ regex: "([\\w|\\.]*)(?=:)"
+ } : undefined
+ } as any;
+ }
+
+ static getComponentConfigWithAffixes(component: string, prefix: string = '', suffix: string = ''): Initializer {
+ return {
+ type: 'host_with_component',
+ component: component,
+ modifier: {
+ type: 'regexp',
+ regex: ":\\/\\/" + "([\\w|\\.]*)(?=:)",
+ prefix: prefix,
+ suffix: suffix
+ } as any
+ };
+ }
+
+ static getHostsListComponentConfig(component: string, componentExists: boolean, delimiter: string = ','): Initializer {
+ return {
+ type: 'hosts_list_with_component',
+ component: component,
+ componentExists: componentExists,
+ modifier: {
+ delimiter: delimiter
+ }
+ };
+ }
+
+ uniqueInitializers: { [key: string]: (config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies) => ConfigProperty };
+
+ constructor() {
+ this.uniqueInitializers = {
+ 'hbase.rootdir': this._initHbaseRootDir.bind(this),
+ 'hawq_dfs_url': this._initHawqDfsUrl.bind(this),
+ 'instance.volumes': this._initInstanceVolumes.bind(this),
+ 'instance.volumes.replacements': this._initInstanceVolumesReplacements.bind(this),
+ 'dfs.journalnode.edits.dir': this._initDfsJnEditsDir.bind(this),
+ 'xasecure.audit.destination.hdfs.dir': this._initXasecureAuditDestinationHdfsDir.bind(this)
+ };
+ }
+
+ initialValue(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+
+ const initializers: { [key: string]: Initializer[] } = {
+ 'dfs.ha.namenodes.${dfs.nameservices}': [NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')],
+ 'dfs.namenode.rpc-address.${dfs.nameservices}.nn1': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', true, '', '', 'nnRpcPort', true),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.namenode.rpc-address.${dfs.nameservices}.nn2': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', false, '', '', '8020', false),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.namenode.http-address.${dfs.nameservices}.nn1': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', true, '', '', 'nnHttpPort', true),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.namenode.http-address.${dfs.nameservices}.nn2': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', false, '', '', '50070', false),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.namenode.https-address.${dfs.nameservices}.nn1': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', true, '', '', 'nnHttpsPort', true),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.namenode.https-address.${dfs.nameservices}.nn2': [
+ NnHaConfigInitializer.getHostWithPortConfig('NAMENODE', false, '', '', '50470', false),
+ NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'dfs.client.failover.proxy.provider.${dfs.nameservices}': [NnHaConfigInitializer.getRenameWithNamespaceConfig('${dfs.nameservices}')],
+ 'dfs.nameservices': [NnHaConfigInitializer.getNamespaceConfig()],
+ 'dfs.internal.nameservices': [NnHaConfigInitializer.getNamespaceConfig()],
+ 'fs.defaultFS': [NnHaConfigInitializer.getNamespaceConfig('hdfs://')],
+ 'dfs.namenode.shared.edits.dir': [
+ NnHaConfigInitializer.getHostsWithPortConfig('JOURNALNODE', 'qjournal://', '/${dfs.nameservices}', ';', '8485', false),
+ NnHaConfigInitializer.getReplaceNamespaceConfig('${dfs.nameservices}')
+ ],
+ 'ha.zookeeper.quorum': [NnHaConfigInitializer.getHostsWithPortConfig('ZOOKEEPER_SERVER', '', '', ',', 'zkClientPort', true)]
+ };
+
+ const uniqueInitializer = {
+ 'hbase.rootdir': this._initHbaseRootDir.bind(this),
+ 'hawq_dfs_url': this._initHawqDfsUrl.bind(this),
+ 'instance.volumes': this._initInstanceVolumes.bind(this),
+ 'instance.volumes.replacements': this._initInstanceVolumesReplacements.bind(this),
+ 'dfs.journalnode.edits.dir': this._initDfsJnEditsDir.bind(this),
+ 'xasecure.audit.destination.hdfs.dir': this._initXasecureAuditDestinationHdfsDir.bind(this)
+ }[config.name];
+ if (uniqueInitializer) {
+ uniqueInitializer(config, localDB, dependencies);
+ }
+
+ const initializersForConfig = initializers[config.name];
+ if (initializersForConfig) {
+ initializersForConfig.forEach(initializer => {
+ switch (initializer.type) {
+ case 'rename':
+ this._initWithRename(config, localDB, dependencies, initializer);
+ break;
+ case 'namespace':
+ this._initAsNamespace(config, localDB, dependencies, initializer);
+ break;
+ case 'replace_namespace':
+ this._initWithNamespace(config, localDB, dependencies, initializer);
+ break;
+ case 'host_with_port':
+ this._initAsHostWithPort(config, localDB, dependencies, initializer);
+ break;
+ case 'hosts_with_port':
+ this._initAsHostsWithPort(config, localDB, dependencies, initializer);
+ break;
+ case 'hosts_with_components':
+ this._initAsHostsWithComponents(config, localDB, dependencies, initializer);
+ break;
+ case 'host_with_component':
+ this._initAsHostWithComponent(config, localDB, dependencies, initializer);
+ break;
+ case 'hosts_list_with_component':
+ this._initAsHostsListWithComponent(config, localDB, dependencies, initializer);
+ break;
+ default:
+ break;
+ }
+ });
+ }
+
+ config.isOverridable = false;
+ return config;
+ }
+
+ private _initWithRename(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: Initializer): ConfigProperty {
+ console.log("Local DB is",localDB)
+ const replaceWith = dependencies.namespaceId;
+ const toReplace:any = initializer.toReplace;
+ if (!replaceWith) {
+ throw new Error('`dependencies.namespaceId` should be not empty string');
+ }
+ config.name = config.name.replace(toReplace, replaceWith);
+ config.displayName = config.displayName.replace(toReplace, replaceWith);
+ return config;
+ }
+
+ private _initAsNamespace(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: any): ConfigProperty {
+ console.log("Local DB is",localDB)
+ const value = dependencies.namespaceId;
+ if (!value) {
+ throw new Error('`dependencies.namespaceId` should be not empty string');
+ }
+ config.value = initializer.modifier.prefix + value + initializer.modifier.suffix;
+ config.recommendedValue = config.value;
+ return config;
+ }
+
+ private _initWithNamespace(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: Initializer): ConfigProperty {
+ console.log("Local DB is",localDB)
+ const replaceWith = dependencies.namespaceId;
+ const toReplace:any = initializer.toReplace;
+ if (!replaceWith) {
+ throw new Error('`dependencies.namespaceId` should be not empty string');
+ }
+ config.value = config.value.replace(toReplace, replaceWith);
+ config.recommendedValue = config.recommendedValue.replace(toReplace, replaceWith);
+ return config;
+ }
+
+ private _initAsHostWithPort(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: any, initializer: any): ConfigProperty {
+ const hostName = localDB.masterComponentHosts.find(host => host.component === initializer.component && host.isInstalled === initializer.componentExists)?.hostName;
+ const port = initializer.portKey ? dependencies[initializer.portKey] : initializer.port;
+ config.value = initializer.modifier.prefix + (hostName || '') + (port ? ':' + port : '') + initializer.modifier.suffix;
+ config.recommendedValue = config.value;
+ return config;
+ }
+
+ private _initAsHostsWithPort(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: any, initializer: any): ConfigProperty {
+ const hostNames = localDB.masterComponentHosts
+ .filter(host => initializer.component.includes(host.component) && (initializer.componentExists === undefined || host.isInstalled === initializer.componentExists))
+ .map(host => host.hostName);
+ const port = initializer.portKey ? dependencies[initializer.portKey] : initializer.port;
+ config.value = initializer.modifier.prefix + hostNames.map(host => host + (port ? ':' + port : '')).join(initializer.modifier.delimiter) + initializer.modifier.suffix;
+ config.recommendedValue = config.value;
+ return config;
+ }
+
+ private _initAsHostsWithComponents(config: any, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: Initializer): ConfigProperty {
+ console.log("Dependencies is",dependencies);
+ const hostNames = localDB.masterComponentHosts
+ .filter(host => initializer.components.includes(host.component) && (initializer.isInstalled === undefined || host.isInstalled === initializer.isInstalled))
+ .map(host => host.hostName);
+ config.value = initializer.asArray ? hostNames : hostNames.join(',');
+ config.recommendedValue = config.value;
+ return config;
+ }
+
+ private _initAsHostWithComponent(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: any): ConfigProperty {
+ console.log("Dependencies is",dependencies);
+ const component = localDB.masterComponentHosts.find(host => host.component === initializer.component);
+ if (!component) {
+ return config;
+ }
+ if (initializer.modifier) {
+ const replaceWith = initializer.modifier.prefix + component.hostName + initializer.modifier.suffix;
+ config.value = config.value.replace(initializer.modifier.regex, replaceWith);
+ config.recommendedValue = config.recommendedValue.replace(initializer.modifier.regex, replaceWith);
+ } else {
+ config.value = component.hostName;
+ config.recommendedValue = component.hostName;
+ }
+ return config;
+ }
+
+ private _initAsHostsListWithComponent(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies, initializer: any): ConfigProperty {
+ console.log("Dependencies is",dependencies);
+ const hostNames = localDB.masterComponentHosts
+ .filter(host => host.component === initializer.component && host.isInstalled === initializer.componentExists)
+ .map(host => host.hostName)
+ .join(initializer.modifier.delimiter);
+ config.value = hostNames;
+ config.recommendedValue = hostNames;
+ return config;
+ }
+
+ private _initHbaseRootDir(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ const fileName = config.filename;
+ if (fileName === 'hbase-site') {
+ return this._initHbaseRootDirForHbase(config, localDB, dependencies);
+ }
+ if (fileName === 'ams-hbase-site') {
+ return this._initHbaseRootDirForAMS(config, localDB, dependencies);
+ }
+ return config;
+ }
+
+ private _initHbaseRootDirForHbase(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('HBASE')) {
+ const value = dependencies.serverConfigs.find(config => config.type === 'hbase-site')?.properties['hbase.rootdir'].replace(/\/\/[^\/]*/, '//' + dependencies.namespaceId);
+ config.value = value || '';
+ config.recommendedValue = value || '';
+ }
+ return config;
+ }
+
+ private _initHbaseRootDirForAMS(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('AMBARI_METRICS')) {
+ const value = dependencies.serverConfigs.find(config => config.type === 'ams-hbase-site')?.properties['hbase.rootdir'];
+ const currentNameNodeHost = localDB.masterComponentHosts.find(host => host.component === 'NAMENODE' && host.isInstalled)?.hostName;
+ if (value && value.includes('hdfs://' + currentNameNodeHost)) {
+ config.value = value.replace(/\/\/[^\/]*/, '//' + dependencies.namespaceId);
+ config.recommendedValue = config.value;
+ }
+ else{
+ config.value=value as any,
+ config.recommendedValue=value as any
+ }
+ }
+ return config;
+ }
+
+ private _initHawqDfsUrl(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('HAWQ')) {
+ const value = dependencies.serverConfigs.find(config => config.type === 'hawq-site')?.properties['hawq_dfs_url'].replace(/(^.*:[0-9]+)(?=\/)/, dependencies.namespaceId);
+ config.value = value || '';
+ config.recommendedValue = value || '';
+ }
+ return config;
+ }
+
+ private _initInstanceVolumes(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('ACCUMULO')) {
+ const oldValue = dependencies.serverConfigs.find(config => config.type === 'accumulo-site')?.properties['instance.volumes'];
+ const value = oldValue?.replace(/\/\/[^\/]*/, '//' + dependencies.namespaceId);
+ config.value = value || '';
+ config.recommendedValue = value || '';
+ }
+ return config;
+ }
+
+ private _initInstanceVolumesReplacements(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('ACCUMULO')) {
+ const oldValue = dependencies.serverConfigs.find(config => config.type === 'accumulo-site')?.properties['instance.volumes'];
+ const value = oldValue?.replace(/\/\/[^\/]*/, '//' + dependencies.namespaceId);
+ const replacements = oldValue + " " + value;
+ config.value = replacements || '';
+ config.recommendedValue = replacements || '';
+ }
+ return config;
+ }
+
+ 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 || '';
+ // }
+ return config;
+ }
+
+ private _initXasecureAuditDestinationHdfsDir(config: ConfigProperty, localDB: ExtendedTopologyLocalDB, dependencies: NnHaConfigDependencies): ConfigProperty {
+ if (localDB.installedServices.includes('RANGER')) {
+ const oldValue = dependencies.serverConfigs.find(config => config.type === 'ranger-env')?.properties['xasecure.audit.destination.hdfs.dir'];
+ const valueArray = oldValue?.split('/');
+ if (valueArray && valueArray.length > 2) {
+ valueArray[2] = dependencies.namespaceId;
+ const newValue = valueArray.join('/');
+ config.value = newValue;
+ config.recommendedValue = newValue;
+ }
+ }
+ return config;
+ }
+}
+
+export default NnHaConfigInitializer;
\ No newline at end of file
diff --git a/ambari-web/latest/src/Utils/credentialsUtils.ts b/ambari-web/latest/src/Utils/credentialsUtils.ts
new file mode 100644
index 0000000000..a0c9b08770
--- /dev/null
+++ b/ambari-web/latest/src/Utils/credentialsUtils.ts
@@ -0,0 +1,153 @@
+/**
+ * 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.
+ */
+
+import { find, map } from "lodash";
+import CredentialsApi from "../api/credentialsApi";
+
+const credentialsUtils = {
+ STORE_TYPES: {
+ TEMPORARY: "temporary",
+ PERSISTENT: "persisted",
+ PERSISTENT_KEY: "persistent",
+ TEMPORARY_KEY: "temporary",
+ PERSISTENT_PATH: "storage.persistent",
+ TEMPORARY_PATH: "storage.temporary",
+ },
+ ALIAS: {
+ KDC_CREDENTIALS: "kdc.admin.credential",
+ },
+ createCredentials: async function (
+ clusterName: string,
+ alias: string,
+ resource: string
+ ) {
+ return await CredentialsApi.createCredentials(clusterName, alias, {
+ resource,
+ });
+ },
+ getCredential: async function (
+ clusterName: string,
+ alias: string,
+ successCallback: any,
+ errorCallback: any
+ ) {
+ try {
+ const data = await CredentialsApi.getCredentials(clusterName, alias);
+ if (successCallback) {
+ successCallback(data);
+ }
+ } catch {
+ errorCallback();
+ }
+ },
+ updateCredentials: async function (
+ clusterName: string,
+ alias: string,
+ resource: string
+ ) {
+ return await CredentialsApi.updateCredentials(clusterName, alias, {
+ resource,
+ });
+ },
+ createOrUpdateCredentials: async function (
+ clusterName: string,
+ alias: string,
+ resource: string
+ ) {
+ const self = this;
+ self.getCredential(
+ clusterName,
+ alias,
+ async () => {
+ await self.updateCredentials(clusterName, alias, resource);
+ var status = arguments[1];
+ var result = arguments[2];
+ if (status === "success") {
+ return result;
+ }
+ },
+ async () => {
+ await self.createCredentials(clusterName, alias, resource);
+ var status = arguments[1];
+ var result = arguments[2];
+ if (status === "success") {
+ return result;
+ }
+ }
+ );
+ },
+ credentials: async function (clusterName: string, callback: Function) {
+ const data = await CredentialsApi.listCredentials(clusterName);
+ callback(map(data?.items, "Credential"));
+ },
+ removeCredentials: async function (clusterName: string, alias: string) {
+ return await CredentialsApi.deleteCredentials(clusterName, alias);
+ },
+ storageInfo: async function (clusterName: string, callback: Function) {
+ const json = await CredentialsApi.credentialsStoreInfo(clusterName);
+ if (json.Clusters) {
+ const storage = json?.Clusters?.credential_store_properties ?? {};
+ let storeTypesObject: any = {};
+ storeTypesObject[this.STORE_TYPES.PERSISTENT_KEY] =
+ storage[this.STORE_TYPES.PERSISTENT_PATH] === "true";
+ storeTypesObject[this.STORE_TYPES.TEMPORARY_KEY] =
+ storage[this.STORE_TYPES.TEMPORARY_PATH] === "true";
+ callback(storeTypesObject);
+ } else {
+ callback(null);
+ }
+ },
+ isStorePersisted: function (clusterName: string) {
+ return this.storeTypeStatus(clusterName, this.STORE_TYPES.PERSISTENT_KEY);
+ },
+ isStoreTemporary: function (clusterName: string) {
+ return this.storeTypeStatus(clusterName, this.STORE_TYPES.TEMPORARY_KEY);
+ },
+ storeTypeStatus: function (clusterName: string, type: string) {
+ this.storageInfo(clusterName, function (storage: any) {
+ return storage?.[type];
+ });
+ },
+ createCredentialResource: function (
+ principal: string,
+ key: string,
+ type: string
+ ) {
+ return {
+ principal: principal,
+ key: key,
+ type: type,
+ };
+ },
+ isKDCCredentialsPersisted: function (credentials: any) {
+ var kdcCredentials = find(credentials, [
+ "alias",
+ this.ALIAS.KDC_CREDENTIALS,
+ ]);
+ if (kdcCredentials) {
+ const type =
+ kdcCredentials.type !== undefined
+ ? kdcCredentials.type
+ : this.STORE_TYPES.TEMPORARY;
+ return type === this.STORE_TYPES.PERSISTENT;
+ }
+ return false;
+ },
+};
+
+export default credentialsUtils;
diff --git a/ambari-web/latest/src/screens/ClusterWizard/Step7/DirectoriesTab.tsx b/ambari-web/latest/src/Utils/jvmFormatUtils.ts
similarity index 70%
rename from ambari-web/latest/src/screens/ClusterWizard/Step7/DirectoriesTab.tsx
rename to ambari-web/latest/src/Utils/jvmFormatUtils.ts
index b5dd7572a8..b6c741b0c2 100644
--- a/ambari-web/latest/src/screens/ClusterWizard/Step7/DirectoriesTab.tsx
+++ b/ambari-web/latest/src/Utils/jvmFormatUtils.ts
@@ -16,12 +16,14 @@
* limitations under the License.
*/
-import { Card } from "react-bootstrap";
+export const formatParamsForDisplay = (_value: string, _displayType?: string): string => {
+ return "In Progress";
+}
-export default function DirectoriesTab() {
- return (
- <Card>
- <h5 className="p-4">Directories Tab</h5>
- </Card>
- );
+export const formatParamsForSave = (_value: string): string => {
+ return "In Progress";
+}
+
+export const shouldUseMultilineFormatting = (_value: string, _displayType?: string): string => {
+ return "In Progress";
}
\ No newline at end of file
diff --git a/ambari-web/latest/src/Utils/sslProtocolUtils.ts b/ambari-web/latest/src/Utils/sslProtocolUtils.ts
new file mode 100644
index 0000000000..a34d89c991
--- /dev/null
+++ b/ambari-web/latest/src/Utils/sslProtocolUtils.ts
@@ -0,0 +1,287 @@
+/**
+ * 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.
+ */
+
+/**
+ * SSL Protocol Utilities - Following Ember.js setProtocol patterns
+ * Implements sophisticated SSL detection logic for different services and components
+ */
+
+interface ProtocolConfig {
+ type?: string;
+ checks?: Array<{
+ site: string;
+ property: string;
+ desired: string;
+ }>;
+}
+
+interface ConfigProperty {
+ type: string;
+ properties: { [key: string]: string };
+}
+
+/**
+ * Services that support HTTPS with specific SSL configurations
+ * Following Ember.js servicesSupportsHttps pattern
+ */
+export const SERVICES_SUPPORTS_HTTPS = ["HDFS", "HBASE", "PINOT"];
+
+/**
+ * Check if desired condition is met for SSL configuration
+ * Following Ember.js meetDesired pattern
+ */
+const meetDesired = (
+ configProperties: ConfigProperty[],
+ configType: string,
+ property: string,
+ desiredState: string
+): boolean => {
+ const currentConfig = configProperties.find(config => config.type === configType);
+
+ if (!currentConfig) {
+ return false;
+ }
+
+ const currentPropertyValue = currentConfig.properties[property];
+
+ if ('NOT_EXIST' === desiredState) {
+ return currentPropertyValue == null || currentPropertyValue === undefined;
+ }
+ if ('EXIST' === desiredState) {
+ return currentPropertyValue != null && currentPropertyValue !== undefined;
+ }
+ return desiredState === currentPropertyValue;
+};
+
+/**
+ * Reverse protocol type for fallback logic
+ * Following Ember.js reverseType pattern
+ */
+const reverseType = (type: string): string => {
+ if ('https' === type) {
+ return 'http';
+ }
+ if ('http' === type) {
+ return 'https';
+ }
+ return '';
+};
+
+/**
+ * Main protocol determination function following Ember.js setProtocol logic
+ * Determines protocol based on service configuration and SSL settings
+ */
+export const setProtocol = (
+ configProperties: ConfigProperty[],
+ protocolConfig?: ProtocolConfig
+): string => {
+ // Check global Hadoop SSL setting
+ let hadoopSslEnabled = false;
+
+ if (configProperties && configProperties.length > 0) {
+ const hdfsSite = configProperties.find(config => config.type === 'hdfs-site');
+ hadoopSslEnabled = !!(hdfsSite &&
+ hdfsSite.properties &&
+ hdfsSite.properties['dfs.http.policy'] === 'HTTPS_ONLY');
+ }
+
+ // If no protocol config, use global Hadoop SSL setting
+ if (!protocolConfig) {
+ return hadoopSslEnabled ? 'https' : 'http';
+ }
+
+ const protocolType = protocolConfig.type;
+
+ // Explicit protocol type overrides
+ if ('HTTPS_ONLY' === protocolType) {
+ return 'https';
+ }
+ if ('HTTP_ONLY' === protocolType) {
+ return 'http';
+ }
+
+ // If no checks defined, use global Hadoop SSL setting
+ const checks = protocolConfig.checks;
+ if (!checks) {
+ return hadoopSslEnabled ? 'https' : 'http';
+ }
+
+ // Process protocol checks
+ const protocolTypeLower = protocolType?.toLowerCase() || 'http';
+ let failedChecks = 0;
+
+ checks.forEach(check => {
+ const configType = check.site;
+ const property = check.property;
+ const desiredState = check.desired;
+ const checkMet = meetDesired(configProperties, configType, property, desiredState);
+
+ if (!checkMet) {
+ failedChecks++;
+ }
+ });
+
+ // If any checks failed, reverse the protocol type
+ return failedChecks > 0 ? reverseType(protocolTypeLower) : protocolTypeLower;
+};
+
+/**
+ * Service-specific SSL configuration patterns
+ * Based on Ember.js quick_links.js FIXTURES
+ */
+export const SERVICE_SSL_CONFIGS = {
+ HDFS: {
+ supportsHttps: true,
+ httpConfig: 'dfs.namenode.http-address',
+ httpsConfig: 'dfs.namenode.https-address',
+ site: 'hdfs-site',
+ defaultHttpPort: 50070,
+ defaultHttpsPort: 50470
+ },
+ YARN: {
+ supportsHttps: true,
+ httpConfig: 'yarn.resourcemanager.webapp.address',
+ httpsConfig: 'yarn.resourcemanager.webapp.https.address',
+ site: 'yarn-site',
+ defaultHttpPort: 8088,
+ defaultHttpsPort: 8090
+ },
+ MAPREDUCE2: {
+ supportsHttps: true,
+ httpConfig: 'mapreduce.jobhistory.webapp.address',
+ httpsConfig: 'mapreduce.jobhistory.webapp.https.address',
+ site: 'mapred-site',
+ defaultHttpPort: 19888,
+ defaultHttpsPort: 19888
+ },
+ HBASE: {
+ supportsHttps: false,
+ httpConfig: 'hbase.master.info.port',
+ site: 'hbase-site',
+ defaultHttpPort: 60010
+ },
+ RANGER: {
+ supportsHttps: true,
+ httpConfig: 'http.service.port',
+ httpsConfig: 'https.service.port',
+ site: 'ranger-site',
+ defaultHttpPort: 6080,
+ defaultHttpsPort: 6182,
+ specialExternalUrl: 'policymgr_external_url', // Special Ranger handling
+ externalUrlSite: 'admin-properties'
+ },
+ ATLAS: {
+ supportsHttps: true,
+ httpConfig: 'atlas.server.http.port',
+ httpsConfig: 'atlas.server.https.port',
+ site: 'application-properties',
+ defaultHttpPort: 21000,
+ defaultHttpsPort: 21443
+ },
+ AMBARI_METRICS: {
+ supportsHttps: false,
+ httpConfig: 'port',
+ site: 'ams-grafana-ini',
+ defaultHttpPort: 3000,
+ defaultHttpsPort: 3000
+ },
+ ACCUMULO: {
+ supportsHttps: true,
+ httpConfig: 'monitor.port.client',
+ httpsConfig: 'monitor.port.client', // Same property
+ site: 'accumulo-site',
+ defaultHttpPort: 50095,
+ defaultHttpsPort: 50095
+ },
+ LOGSEARCH: {
+ supportsHttps: true,
+ httpConfig: 'logsearch_ui_port',
+ httpsConfig: 'logsearch_ui_port', // Same property
+ site: 'logsearch-env',
+ defaultHttpPort: 61888,
+ defaultHttpsPort: 61888
+ },
+ STORM: {
+ supportsHttps: false,
+ httpConfig: 'ui.port',
+ site: 'storm-site',
+ defaultHttpPort: 8744
+ },
+ FALCON: {
+ supportsHttps: false,
+ httpConfig: 'falcon_port',
+ site: 'falcon-env',
+ defaultHttpPort: 15000
+ },
+ SPARK: {
+ supportsHttps: false,
+ httpConfig: 'spark.history.ui.port',
+ site: 'spark-defaults',
+ defaultHttpPort: 18080
+ },
+ PINOT: {
+ supportsHttps: true,
+ httpConfig: 'controller.access.protocols.http.port',
+ httpsConfig: 'controller.access.protocols.https.port',
+ site: 'pinot-controller-conf',
+ defaultHttpPort: 9000,
+ defaultHttpsPort: 8444,
+ sslCheckProperty: 'enable.tls',
+ sslCheckSite: 'pinot-common-conf'
+ }
+};
+
+/**
+ * Get SSL configuration for a specific service
+ */
+export const getServiceSSLConfig = (serviceName: string) => {
+ return SERVICE_SSL_CONFIGS[serviceName as keyof typeof SERVICE_SSL_CONFIGS] || null;
+};
+
+/**
+ * Determine if service supports HTTPS
+ */
+export const serviceSupportsHttps = (serviceName: string): boolean => {
+ const config = getServiceSSLConfig(serviceName);
+ return config?.supportsHttps || false;
+};
+
+/**
+ * Get appropriate protocol for a service based on its configuration
+ * Following Ember.js service-specific protocol logic
+ */
+export const getServiceProtocol = (
+ serviceName: string,
+ configProperties: ConfigProperty[],
+ protocolConfig?: ProtocolConfig
+): string => {
+ const serviceConfig = getServiceSSLConfig(serviceName);
+
+ if (!serviceConfig) {
+ // Fallback to global SSL detection
+ return setProtocol(configProperties, protocolConfig);
+ }
+
+ // If service doesn't support HTTPS, always return HTTP
+ if (!serviceConfig.supportsHttps) {
+ return 'http';
+ }
+
+ // Use Ember.js setProtocol logic for services that support HTTPS
+ return setProtocol(configProperties, protocolConfig);
+};
diff --git a/ambari-web/latest/src/Utils/unitConversionUtils.ts b/ambari-web/latest/src/Utils/unitConversionUtils.ts
new file mode 100644
index 0000000000..8fb3c2e5d1
--- /dev/null
+++ b/ambari-web/latest/src/Utils/unitConversionUtils.ts
@@ -0,0 +1,358 @@
+/**
+ * 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.
+ */
+export const convertMapTable = {
+ size: {
+ b: 1024,
+ kb: 1024,
+ mb: 1024,
+ gb: 1024,
+ tb: 1024,
+ pb: 1024
+ },
+ time: {
+ milliseconds: 1,
+ seconds: 1000,
+ minutes: 60,
+ hours: 60,
+ days: 24
+ },
+ percent: {
+ percent_int: {
+ int: 1,
+ percent: 1
+ },
+ percent_float: {
+ float: 1,
+ percent: 0.01
+ }
+ },
+ dimensionless: {
+ int: 1,
+ float: 1
+ }
+};
+
+// Unit label mapping for display
+export const unitLabelMap: Record<string, string> = {
+ percent: '%',
+ int: '',
+ float: '',
+ b: 'B',
+ kb: 'KB',
+ mb: 'MB',
+ gb: 'GB',
+ tb: 'TB',
+ pb: 'PB',
+ milliseconds: 'ms',
+ seconds: 's',
+ minutes: 'min',
+ hours: 'h',
+ days: 'd'
+};
+
+// Data type units that should not display units
+const DATA_TYPES = ['int', 'float'];
+
+/**
+ * Check if a unit is a data type (int/float)
+ */
+export function isDataType(unit: string): boolean {
+ return DATA_TYPES.includes(unit?.toLowerCase());
+}
+
+/**
+ * Get dimension table map for unit conversion
+ */
+function getUnitTable(unit: string, currentDimensionType?: string): Record<string, any> {
+ let unitType: string;
+
+ if (currentDimensionType) {
+ unitType = currentDimensionType;
+ } else {
+ // Find the dimension type by looking for the unit in conversion tables
+ unitType = Object.keys(convertMapTable).find(item => {
+ const table = convertMapTable[item as keyof typeof convertMapTable];
+ return Object.keys(table).includes(unit?.toLowerCase());
+ }) || 'dimensionless';
+ }
+
+ return convertMapTable[unitType as keyof typeof convertMapTable] || convertMapTable.dimensionless;
+}
+
+/**
+ * Convert single value between units
+ */
+function convertToSingleValue(value: number, fromUnit: string, toUnit: string, currentDimensionType?: string): number {
+ const convertTable = getUnitTable(fromUnit, currentDimensionType);
+ const units = Object.keys(convertTable);
+ const fromUnitIndex = units.indexOf(fromUnit.toLowerCase());
+ const toUnitIndex = units.indexOf(toUnit.toLowerCase());
+
+ if (fromUnitIndex === -1) {
+ console.warn(`Invalid value unit type: ${fromUnit}`);
+ return value;
+ }
+
+ if (toUnitIndex === -1) {
+ console.warn(`Invalid desired unit type: ${toUnit}`);
+ return value;
+ }
+
+ if (fromUnitIndex === toUnitIndex) {
+ return value;
+ }
+
+ const range = [fromUnitIndex + 1, toUnitIndex + 1];
+ const processedUnits = units.slice(...(range[0] < range[1] ? range : range.slice().reverse()));
+
+ const factor = processedUnits.reduce((p, unit) => p * convertTable[unit], 1);
+
+ if (range[0] < range[1]) {
+ value /= factor;
+ } else {
+ value *= factor;
+ }
+
+ return Number.isInteger(value) ? value : parseFloat(value.toFixed(3));
+}
+
+/**
+ * Main conversion function - converts value between different unit types
+ */
+export function convertValue(
+ value: number | number[],
+ fromUnit: string | string[],
+ toUnit: string | string[],
+ isObjectOutput: boolean = false,
+ currentDimensionType?: string
+): number | Array<{value: number, type: string}> {
+ const valueIsArray = Array.isArray(value);
+
+ if (!valueIsArray) {
+ value = +value;
+ }
+
+ // If units are the same and no object output requested, return as is
+ if ((fromUnit === toUnit || !toUnit) && !isObjectOutput) {
+ return value as number;
+ }
+
+ if (isNaN(value as number) && !valueIsArray) {
+ return 0;
+ }
+
+ // Convert toUnit to array if it's a string
+ const toUnitArray = Array.isArray(toUnit) ? toUnit : toUnit.split(',');
+
+ // Process multi unit format or object output
+ if (toUnitArray.length > 1 || isObjectOutput) {
+ let remainingValue = value as number;
+ return toUnitArray.map(unit => {
+ const convertedValue = Math.floor(convertToSingleValue(remainingValue, fromUnit as string, unit, currentDimensionType));
+ remainingValue -= convertToSingleValue(convertedValue, unit, fromUnit as string, currentDimensionType);
+ return {
+ value: convertedValue,
+ type: unit
+ };
+ });
+ } else {
+ // Single unit format
+ if (!valueIsArray) {
+ return convertToSingleValue(value as number, fromUnit as string, toUnitArray[0], currentDimensionType);
+ } else {
+ // Handle array input - sum all values after converting to target unit
+ if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object' && value[0] !== null && 'value' in value[0]) {
+ const arrayValue = value as unknown as Array<{value: number, type: string}>;
+ return arrayValue
+ .map(item => convertToSingleValue(item.value, item.type, toUnitArray[0], currentDimensionType))
+ .reduce((sum, val) => sum + val, 0);
+ } else {
+ // Fallback for simple number arrays or invalid input
+ return 0;
+ }
+ }
+ }
+}
+
+/**
+ * Get unit label for display
+ */
+export function getUnitLabel(unit: string): string {
+ return unitLabelMap[unit?.toLowerCase()] || unit || '';
+}
+
+/**
+ * Check if unit should be displayed (hide for int/float)
+ */
+export function shouldDisplayUnit(unit: string): boolean {
+ return Boolean(unit) && !isDataType(unit) && unit.toLowerCase() !== 'dimensionless';
+}
+
+/**
+ * Get display unit label (empty for int/float)
+ */
+export function getDisplayUnitLabel(unit: string): string {
+ return shouldDisplayUnit(unit) ? getUnitLabel(unit) : '';
+}
+
+/**
+ * Get config unit information from property
+ */
+export function getConfigUnitInfo(property: any): {
+ configUnit: string;
+ widgetUnit: string;
+ dimensionType: string;
+ configType: string;
+} {
+ // Get config unit from property attributes
+ const configUnit = property?.propertyAttributes?.unit || property?.propertyAttributes?.type || 'int';
+
+ // Get widget unit from widget definition or fall back to config unit
+ const widgetUnit = property?.widget?.units?.[0]?.['unit-name'] ||
+ property?.widget?.units?.[0]?.unit ||
+ configUnit;
+
+ // Determine dimension type
+ let dimensionType = '';
+ if (['int', 'float'].includes(configUnit.toLowerCase()) && widgetUnit.toLowerCase() === 'percent') {
+ dimensionType = `percent.percent_${configUnit.toLowerCase()}`;
+ } else {
+ // Auto-detect dimension type
+ dimensionType = Object.keys(convertMapTable).find(type => {
+ const table = convertMapTable[type as keyof typeof convertMapTable];
+ return Object.keys(table).includes(widgetUnit.toLowerCase());
+ }) || 'dimensionless';
+ }
+
+ return {
+ configUnit: configUnit.toLowerCase(),
+ widgetUnit: widgetUnit.toLowerCase(),
+ dimensionType,
+ configType: property?.propertyAttributes?.type || 'int'
+ };
+}
+
+/**
+ * Convert config value to widget value
+ */
+export function widgetValueByConfigAttributes(
+ value: number,
+ configUnit: string,
+ widgetUnit: string,
+ dimensionType?: string
+): number {
+ if (!value || configUnit === widgetUnit) return value;
+ return convertValue(value, configUnit, widgetUnit, false, dimensionType) as number;
+}
+
+/**
+ * Convert widget value back to config value
+ */
+export function configValueByWidget(
+ value: number,
+ widgetUnit: string,
+ configUnit: string,
+ configType: string,
+ dimensionType?: string
+): number {
+ if (!value || widgetUnit === configUnit) return value;
+ const converted = convertValue(value, widgetUnit, configUnit, false, dimensionType) as number;
+ return configType === 'int' ? Math.round(converted) : converted;
+}
+
+/**
+ * Format tick label for sliders (matches Ember formatTickLabel exactly)
+ */
+export function formatTickLabel(tick: number, unit: string, separator: string = ''): string {
+ if (!shouldDisplayUnit(unit)) {
+ return tick.toString();
+ }
+
+ let valueLabel = tick;
+ let unitLabel = getUnitLabel(unit);
+
+ // Handle size units with auto-scaling (matches Ember logic exactly)
+ const sizeUnits = ['B', 'KB', 'MB', 'GB', 'TB'];
+ const unitLabelIndex = sizeUnits.indexOf(unitLabel);
+
+ if (unitLabelIndex > -1) {
+ let scaledTick = tick;
+ let scaledUnitIndex = unitLabelIndex;
+
+
+ while (scaledTick >= 1024 && scaledUnitIndex < sizeUnits.length - 1) {
+ scaledTick /= 1024;
+ scaledUnitIndex++;
+ }
+
+ unitLabel = sizeUnits[scaledUnitIndex];
+ valueLabel = parseFloat(scaledTick.toFixed(3));
+ }
+
+ return valueLabel + separator + unitLabel;
+}
+
+/**
+ * Round number to 3 decimal places
+ */
+export function extraRound(value: number): number {
+ return parseFloat(value.toFixed(3));
+}
+
+/**
+ * Parse time interval from config value to appropriate time units (days, hours, minutes, seconds)
+ */
+export function parseTimeInterval(value: number, configUnit: string): {days: number, hours: number, minutes: number, seconds: number} {
+ // Convert to milliseconds first
+ const milliseconds = convertValue(value, configUnit, 'milliseconds') as number;
+
+ const totalSeconds = Math.floor(milliseconds / 1000);
+ const days = Math.floor(totalSeconds / (24 * 60 * 60));
+ const hours = Math.floor((totalSeconds % (24 * 60 * 60)) / (60 * 60));
+ const minutes = Math.floor((totalSeconds % (60 * 60)) / 60);
+ const seconds = totalSeconds % 60;
+
+ return { days, hours, minutes, seconds };
+}
+
+/**
+ * Compose time interval from time components back to config unit
+ */
+export function composeTimeInterval(days: number, hours: number, minutes: number, seconds: number, configUnit: string): number {
+ const totalMilliseconds = ((days * 24 * 60 * 60) + (hours * 60 * 60) + (minutes * 60) + seconds) * 1000;
+ return convertValue(totalMilliseconds, 'milliseconds', configUnit) as number;
+}
+
+/**
+ * Get parse function based on unit type
+ */
+export function getParseFunction(unitType: string): (value: string) => number {
+ return unitType === 'int' ? parseInt : parseFloat;
+}
+
+/**
+ * Check if widget unit is higher precision than config unit
+ */
+export function isWidgetUnitHigher(configUnit: string, widgetUnit: string): boolean {
+ const units = ['b', 'kb', 'mb', 'gb', 'tb', 'pb'];
+ const configIndex = units.indexOf(configUnit.toLowerCase());
+ const widgetIndex = units.indexOf(widgetUnit.toLowerCase());
+
+ if (configIndex === -1 || widgetIndex === -1) return false;
+ return widgetIndex > configIndex;
+}
diff --git a/ambari-web/latest/src/api/AssignMastersApi.ts b/ambari-web/latest/src/api/AssignMastersApi.ts
deleted file mode 100644
index aa3b246b3c..0000000000
--- a/ambari-web/latest/src/api/AssignMastersApi.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-/**
- * 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.
- */
-
-import { ambariApi } from "./config/axiosConfig";
-
-const AssignMastersApi = {
- getCpuInfo: async function (HOSTS: any) {
- const hostsParams = HOSTS.join(",");
- const url = `/hosts?Hosts/host_name.in(${hostsParams})&fields=Hosts/cpu_count,Hosts/disk_info,Hosts/total_mem,Hosts/ip,Hosts/os_type,Hosts/os_arch,Hosts/public_host_name&minimal_response=true`;
- const response = await ambariApi.request({
- url: url,
- method: "GET",
- });
- return response;
- },
- postRecommendations: async function (
- payload: any,
- STACK: string,
- VERSION: string
- ) {
- const url = `/stacks/${STACK}/versions/${VERSION}/recommendations`;
- const response = await ambariApi.request({
- url: url,
- method: "POST",
- data: payload,
- });
- return response.data;
- },
- postValidations: async function (
- payload: any,
- STACK: string,
- VERSION: string
- ) {
- const url = `/stacks/${STACK}/versions/${VERSION}/validations`;
- const response = await ambariApi.request({
- url: url,
- method: "POST",
- data: payload,
- });
- return response.data;
- },
-};
-export default AssignMastersApi;
diff --git a/ambari-web/latest/src/api/ActionsApi.ts b/ambari-web/latest/src/api/actionsApi.ts
similarity index 100%
rename from ambari-web/latest/src/api/ActionsApi.ts
rename to ambari-web/latest/src/api/actionsApi.ts
diff --git a/ambari-web/latest/src/api/adminApi.ts b/ambari-web/latest/src/api/adminApi.ts
new file mode 100644
index 0000000000..5aca8acf4f
--- /dev/null
+++ b/ambari-web/latest/src/api/adminApi.ts
@@ -0,0 +1,73 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+
+const AdminApi = {
+
+ getNnCheckPointStatus: async function (clusterName:string, hostName:string) {
+ const url = `/clusters/${clusterName}/hosts/${hostName}/host_components/NAMENODE`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getNnCheckPointStatuses: async function (clusterName:string, hostNames:string) {
+ const url = `/clusters/${clusterName}/host_components?HostRoles/component_name=NAMENODE&HostRoles/host_name.in(${hostNames})&fields=HostRoles/desired_state,metrics/dfs/namenode&minimal_response=true`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getJnCheckPointStatus:async function(clusterName:string, hostName:string){
+ const url = `/clusters/${clusterName}/hosts/${hostName}/host_components/JOURNALNODE?fields=metrics`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getSecurityStatus:async function(clusterName:string){
+ const url=`/clusters/${clusterName}?fields=Clusters/security_type`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getSecurityType:async function(clusterName:string){
+ const url=`/clusters/${clusterName}/configurations/service_config_versions?service_name=KERBEROS&is_current=true`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getKerberosSessionState:async function(clusterName:string){
+ const url=`/clusters/${clusterName}/services/KERBEROS?fields=Services/attributes/kdc_validation_result,Services/attributes/kdc_validation_failure_details`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ }
+};
+
+export default AdminApi
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/assignMastersApi.ts b/ambari-web/latest/src/api/assignMastersApi.ts
new file mode 100644
index 0000000000..b07f944cd7
--- /dev/null
+++ b/ambari-web/latest/src/api/assignMastersApi.ts
@@ -0,0 +1,50 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+
+const AssignMastersApi = {
+ getCpuInfo: async function(HOSTS: any) {
+ const hostsParams = HOSTS.join(',');
+ const url = `/hosts?Hosts/host_name.in(${hostsParams})&fields=Hosts/cpu_count,Hosts/disk_info,Hosts/total_mem,Hosts/ip,Hosts/os_type,Hosts/os_arch,Hosts/public_host_name&minimal_response=true&_=1731567268225`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET"
+ });
+ return response;
+ },
+ postRecommendations: async function (payload: any, STACK: string, VERSION: string) {
+ const url = `/stacks/${STACK}/versions/${VERSION}/recommendations`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ return response.data;
+ },
+ postValidations: async function (payload: any, STACK: string, VERSION: string) {
+ const url = `/stacks/${STACK}/versions/${VERSION}/validations`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ return response.data;
+ }
+}
+export default AssignMastersApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/cachedServiceApi.ts b/ambari-web/latest/src/api/cachedServiceApi.ts
new file mode 100644
index 0000000000..0b9811aef4
--- /dev/null
+++ b/ambari-web/latest/src/api/cachedServiceApi.ts
@@ -0,0 +1,216 @@
+/**
+ * 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.
+ */
+
+import { ServiceApi } from "./serviceApi";
+import { serviceCache } from "../Utils/cacheUtils";
+
+/**
+ * Centralized Service Component API Manager
+ * Similar to Ember.js approach - makes one consolidated API call for all components
+ * and provides cached data to individual service hooks
+ */
+class CachedServiceApiManager {
+ private static instance: CachedServiceApiManager;
+ private isPolling = false;
+ private pollingInterval: NodeJS.Timeout | null = null;
+ private subscribers = new Set<(data: any) => void>();
+
+ static getInstance(): CachedServiceApiManager {
+ if (!CachedServiceApiManager.instance) {
+ CachedServiceApiManager.instance = new CachedServiceApiManager();
+ }
+ return CachedServiceApiManager.instance;
+ }
+
+ /**
+ * Subscribe to component data updates
+ */
+ subscribe(callback: (data: any) => void): () => void {
+ this.subscribers.add(callback);
+ return () => this.subscribers.delete(callback);
+ }
+
+ /**
+ * Notify all subscribers of data updates
+ */
+ private notifySubscribers(data: any): void {
+ this.subscribers.forEach(callback => callback(data));
+ }
+
+ /**
+ * Get cached component data for a specific service
+ */
+ getServiceComponentData(serviceName: string): any {
+ return serviceCache.get(`components_${serviceName.toLowerCase()}`);
+ }
+
+ /**
+ * Get all cached component data
+ */
+ getAllComponentData(): any {
+ return serviceCache.get('all_components_data');
+ }
+
+ /**
+ * Centralized API call for all service components
+ * Similar to Ember.js approach - one call for all services
+ */
+ async fetchAllServiceComponents(clusterName: string, forceRefresh: boolean = false): Promise<any> {
+ const cacheKey = 'all_components_data';
+
+ // Check cache first (unless force refresh)
+ if (!forceRefresh) {
+ const cachedData = serviceCache.get(cacheKey);
+ if (cachedData) {
+ return cachedData;
+ }
+ }
+
+ try {
+
+ // OPTIMIZED: Use the same comprehensive fields as the main ServiceContext for consistency
+ const fields = `ServiceComponentInfo/service_name,host_components/HostRoles/display_name,host_components/HostRoles/host_name,host_components/HostRoles/public_host_name,host_components/HostRoles/state,host_components/HostRoles/maintenance_state,host_components/HostRoles/stale_configs,host_components/HostRoles/ha_state,host_components/HostRoles/desired_admin_state,host_components/metrics/jvm/memHeapUsedM,host_components/metrics/jvm/HeapMemoryMax,host_components/metrics/jvm/HeapMemory [...]
+
+ const response = await ServiceApi.getAllServiceComponentsListAndInitialMetrics(
+ clusterName,
+ fields
+ );
+
+ if (response?.data?.items) {
+
+ // Cache the consolidated data
+ serviceCache.set(cacheKey, response.data, 30000); // 30 second TTL
+
+ // Also cache by individual service for quick access
+ const serviceGroups = this.groupComponentsByService(response.data.items);
+ Object.entries(serviceGroups).forEach(([serviceName, data]) => {
+ serviceCache.set(`components_${serviceName.toLowerCase()}`, data, 30000);
+ });
+
+ // Notify subscribers immediately
+ this.notifySubscribers(response.data);
+
+ return response.data;
+ }
+ } catch (error) {
+ return null;
+ }
+ }
+
+ /**
+ * Group component data by service name
+ */
+ private groupComponentsByService(items: any[]): Record<string, any[]> {
+ const groups: Record<string, any[]> = {};
+
+ items.forEach(item => {
+ const serviceName = item.ServiceComponentInfo?.service_name;
+ if (serviceName) {
+ if (!groups[serviceName]) {
+ groups[serviceName] = [];
+ }
+ groups[serviceName].push(item);
+ }
+ });
+
+ return groups;
+ }
+
+ /**
+ * Start centralized polling - similar to Ember.js approach
+ */
+ startPolling(clusterName: string, intervalMs: number = 5000): void {
+ if (this.isPolling) return;
+
+ this.isPolling = true;
+
+ // Initial fetch
+ this.fetchAllServiceComponents(clusterName);
+
+ // Set up polling interval
+ this.pollingInterval = setInterval(() => {
+ this.fetchAllServiceComponents(clusterName);
+ }, intervalMs);
+
+ }
+
+ /**
+ * Pause polling without clearing the interval
+ * Useful for temporarily stopping polling on specific pages
+ */
+ pausePolling(): void {
+ if (this.pollingInterval) {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ }
+ }
+
+ /**
+ * Resume polling after it was paused
+ */
+ resumePolling(clusterName?: string, intervalMs: number = 5000): void {
+ if (this.isPolling && !this.pollingInterval && clusterName) {
+ this.pollingInterval = setInterval(() => {
+ this.fetchAllServiceComponents(clusterName);
+ }, intervalMs);
+ }
+ }
+
+ /**
+ * Stop polling completely
+ */
+ stopPolling(): void {
+ if (this.pollingInterval) {
+ clearInterval(this.pollingInterval);
+ this.pollingInterval = null;
+ }
+ this.isPolling = false;
+ }
+
+ /**
+ * Check if specific service data is available and fresh
+ */
+ hasServiceData(serviceName: string): boolean {
+ return serviceCache.has(`components_${serviceName.toLowerCase()}`);
+ }
+
+ /**
+ * Get metrics for a specific service component
+ */
+ getServiceMetrics(serviceName: string, componentName: string): any {
+ const serviceData = this.getServiceComponentData(serviceName);
+ if (!serviceData) return null;
+
+ return serviceData.find((item: any) =>
+ item.ServiceComponentInfo?.component_name === componentName
+ );
+ }
+
+ /**
+ * Clear all cached data
+ */
+ clearCache(): void {
+ serviceCache.clear();
+ }
+}
+
+// Export singleton instance
+export const cachedServiceApi = CachedServiceApiManager.getInstance();
+
+// Export the class for testing
+export { CachedServiceApiManager };
diff --git a/ambari-web/latest/src/api/centralizedServiceStateApi.ts b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
new file mode 100644
index 0000000000..8de31317f5
--- /dev/null
+++ b/ambari-web/latest/src/api/centralizedServiceStateApi.ts
@@ -0,0 +1,153 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+
+interface ServiceStateData {
+ serviceName: string;
+ state: string;
+ alertsCount: number;
+ hasCriticalAlerts: boolean;
+}
+
+class CentralizedServiceStateApi {
+ private cache: Map<string, ServiceStateData> = new Map();
+ private lastFetchTime: number = 0;
+ private readonly CACHE_DURATION = 5000; // 5 seconds cache
+ private subscribers: ((data: Map<string, ServiceStateData>) => void)[] = [];
+
+ /**
+ * Fetches all service states and alerts in a single API call (Ember.js pattern)
+ * This replaces individual ServiceApi.getServiceState() calls
+ */
+ async fetchAllServiceStatesAndAlerts(clusterName: string): Promise<Map<string, ServiceStateData>> {
+ const now = Date.now();
+
+ // Return cached data if still fresh
+ if (now - this.lastFetchTime < this.CACHE_DURATION && this.cache.size > 0) {
+ return this.cache;
+ }
+
+ try {
+ const response = await ambariApi.request({
+ url: `/clusters/${clusterName}/services?fields=ServiceInfo/state,ServiceInfo/maintenance_state&minimal_response=true`,
+ 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++;
+ }
+ }
+ });
+
+ 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;
+
+ newCache.set(serviceName, {
+ serviceName,
+ state,
+ alertsCount,
+ hasCriticalAlerts,
+ });
+ });
+
+ this.cache = newCache;
+ this.lastFetchTime = now;
+
+ // Notify subscribers
+ this.notifySubscribers();
+
+ return this.cache;
+ } catch (error) {
+ // Return existing cache on error
+ return this.cache;
+ }
+ }
+
+ /**
+ * Get service state data for a specific service
+ */
+ getServiceStateData(serviceName: string): ServiceStateData | null {
+ return this.cache.get(serviceName) || null;
+ }
+
+ /**
+ * Subscribe to service state updates
+ */
+ subscribe(callback: (data: Map<string, ServiceStateData>) => void): () => void {
+ this.subscribers.push(callback);
+
+ // Return unsubscribe function
+ return () => {
+ const index = this.subscribers.indexOf(callback);
+ if (index > -1) {
+ this.subscribers.splice(index, 1);
+ }
+ };
+ }
+
+ /**
+ * Notify all subscribers of data updates
+ */
+ private notifySubscribers(): void {
+ this.subscribers.forEach(callback => callback(this.cache));
+ }
+
+ /**
+ * Clear cache (useful for testing or forced refresh)
+ */
+ clearCache(): void {
+ this.cache.clear();
+ this.lastFetchTime = 0;
+ }
+}
+
+// Export singleton instance
+export const centralizedServiceStateApi = new CentralizedServiceStateApi();
diff --git a/ambari-web/latest/src/api/chooseServicesApi.ts b/ambari-web/latest/src/api/chooseServicesApi.ts
index ce7b9c9a55..c728f9cf7f 100644
--- a/ambari-web/latest/src/api/chooseServicesApi.ts
+++ b/ambari-web/latest/src/api/chooseServicesApi.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
export const ChooseServicesApi = {
diff --git a/ambari-web/latest/src/api/clusterApi.ts b/ambari-web/latest/src/api/clusterApi.ts
index 2d7c1cb549..dca1291b8e 100644
--- a/ambari-web/latest/src/api/clusterApi.ts
+++ b/ambari-web/latest/src/api/clusterApi.ts
@@ -15,6 +15,8 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+import { set } from "lodash";
import { ambariApi, supressErrorAmbariApi } from "./config/axiosConfig";
const ClusterApi = {
@@ -50,6 +52,7 @@ const ClusterApi = {
method:"PUT",
data
})
+ set(response, "data.status", response?.status);
return response.data
},
getCluster: async function (clusterName:string) {
@@ -62,7 +65,7 @@ const ClusterApi = {
},
getAllClusters: async function () {
const url = `/clusters`;
- const response = await ambariApi.request({
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "GET",
});
@@ -86,7 +89,7 @@ const ClusterApi = {
},
getRequestById: async function (clusterName: string,requestId:number|string) {
const url = `/clusters/${clusterName}/requests/${requestId}?fields=*,tasks/Tasks/request_id,tasks/Tasks/command,tasks/Tasks/command_detail,tasks/Tasks/ops_display_name,tasks/Tasks/host_name,tasks/Tasks/id,tasks/Tasks/role,tasks/Tasks/status&minimal_response=true`;
- const response = await ambariApi.request({
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "GET",
});
@@ -116,7 +119,6 @@ const ClusterApi = {
method: "GET"
});
const clusterName = response?.data?.items[0]?.Clusters?.cluster_name;
- console.log("CLUSTER NAME", response.data.items[0].Clusters.cluster_name)
return clusterName;
},
getClusterData: async function () {
@@ -127,8 +129,8 @@ const ClusterApi = {
});
return response.data;
},
- getPersistData: async function (key:any) {
- const url = `/persist/${key}`;
+ getPersistData: async function (key?: any) {
+ const url = key ? `/persist/${key}` : '/persist';
const response = await supressErrorAmbariApi.request({
url: url,
method: "GET",
@@ -197,7 +199,17 @@ const ClusterApi = {
data: payload,
});
return response.data;
+ },
+
+ getClusterMetrics: async function (clusterName: string, fields: string) {
+ const url = `/clusters/${clusterName}?fields=${fields}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+
}
- }
+}
export default ClusterApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/clusterDeployment.ts b/ambari-web/latest/src/api/clusterDeployment.ts
new file mode 100644
index 0000000000..1e6d1cba10
--- /dev/null
+++ b/ambari-web/latest/src/api/clusterDeployment.ts
@@ -0,0 +1,67 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+const ClusterDeploymentApi = {
+ createCluster:async function createCluster(clusterName:string,data:any){{
+ const url=`/clusters/${clusterName}`;
+ const response=await ambariApi.request({
+ url,
+ method:"POST",
+ data
+ })
+ return response.data
+ }},
+ createSelectedServices:async function createCluster(clusterName:string,data:any){{
+ const url=`/clusters/${clusterName}/services`;
+ return ambariApi.request({
+ url,
+ method:"POST",
+ data
+ })
+ }},
+ addRequestToCreateComponent:async function addRequestToCreateComponent(clusterName:string,serviceName:string,data:any){{
+ const url=`/clusters/${clusterName}/services?ServiceInfo/service_name=${serviceName}`
+ const response=await ambariApi.request({
+ url,
+ method:"POST",
+ data
+ })
+ return response.data
+ }},
+ registerHostToCluster:async function registerHostToCluster(clusterName:string,data:any){{
+ const url=`/clusters/${clusterName}/hosts`
+ const response=await ambariApi.request({
+ url,
+ method:"POST",
+ data
+ })
+ return response.data
+ }},
+ applyClusterConfigs: async function(clusterName:string, applyConfigsPaylpoad: any) {
+ const url = `/clusters/${clusterName}`;
+ return ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: applyConfigsPaylpoad
+ });
+
+ }
+};
+
+export default ClusterDeploymentApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/validations.ts b/ambari-web/latest/src/api/componentApi.ts
similarity index 80%
copy from ambari-web/latest/src/api/validations.ts
copy to ambari-web/latest/src/api/componentApi.ts
index 27ea68e03b..de622077ff 100644
--- a/ambari-web/latest/src/api/validations.ts
+++ b/ambari-web/latest/src/api/componentApi.ts
@@ -15,18 +15,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
-const ValidationsApi = {
- validateMapping: async (stack:string,version:string,data:any) => {
- const url = `/stacks/${stack}/versions/${version}/validations`;
+const ComponentApi = {
+
+ editComponent: async function (clusterName:string,data:any) {
+ const url = `/clusters/${clusterName}/components`;
const response = await ambariApi.request({
- url,
- data,
- method: "POST",
+ url: url,
+ method: "PUT",
+ data
});
return response.data;
},
};
-export default ValidationsApi;
\ No newline at end of file
+export default ComponentApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/config/axiosConfig.ts b/ambari-web/latest/src/api/config/axiosConfig.ts
index af5cbffc43..8f98a7cab2 100644
--- a/ambari-web/latest/src/api/config/axiosConfig.ts
+++ b/ambari-web/latest/src/api/config/axiosConfig.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import axios from "axios";
import { toast } from "react-hot-toast";
import { get } from "lodash";
diff --git a/ambari-web/latest/src/api/LogApi.ts b/ambari-web/latest/src/api/configHistoryApi.ts
similarity index 57%
copy from ambari-web/latest/src/api/LogApi.ts
copy to ambari-web/latest/src/api/configHistoryApi.ts
index 0011202dc7..1546d0ae10 100644
--- a/ambari-web/latest/src/api/LogApi.ts
+++ b/ambari-web/latest/src/api/configHistoryApi.ts
@@ -15,18 +15,26 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ambariApi } from "./config/axiosConfig";
-const LogApi = {
+import { ambariApi } from "./config/axiosConfig";
- getLogData: async function (clusterName:string, requestId:string) {
- const url = `/clusters/${clusterName}/requests/${requestId}?fields=tasks/Tasks/command,tasks/Tasks/command_detail,tasks/Tasks/ops_display_name,tasks/Tasks/exit_code,tasks/Tasks/start_time,tasks/Tasks/end_time,tasks/Tasks/host_name,tasks/Tasks/id,tasks/Tasks/role,tasks/Tasks/status&minimal_response=true`;
+const ConfigHistoryApi = {
+ fetchPageSize: async (clusterName: string) => {
+ const url = `/clusters/${clusterName}/configurations/service_config_versions?page_size=1&minimal_response=true`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ fetchConfigHistory: async (clusterName: string, parameters: string) => {
+ const url = `/clusters/${clusterName}/configurations/service_config_versions?${parameters}fields=service_config_version,user,group_id,group_name,is_current,createtime,service_name,hosts,service_config_version_note,is_cluster_compatible,stack_id&minimal_response=true`;
const response = await ambariApi.request({
- url: url,
+ url,
method: "GET",
});
return response.data;
},
};
-export default LogApi;
\ No newline at end of file
+export default ConfigHistoryApi;
diff --git a/ambari-web/latest/src/api/credentialsApi.ts b/ambari-web/latest/src/api/credentialsApi.ts
new file mode 100644
index 0000000000..6db96bb46b
--- /dev/null
+++ b/ambari-web/latest/src/api/credentialsApi.ts
@@ -0,0 +1,97 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+
+const CredentialsApi = {
+ createCredentials: async function (
+ clusterName: string,
+ alias: string,
+ data: { resource: string }
+ ) {
+ const url = `/clusters/${clusterName}/credentials/${alias}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: {
+ Credential: data.resource,
+ },
+ });
+ return response.data;
+ },
+ getCredentials: async function (
+ clusterName: string,
+ alias: string,
+ ) {
+ const url = `/clusters/${clusterName}/credentials/${alias}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ deleteCredentials: async function (
+ clusterName: string,
+ alias: string,
+ ) {
+ const url = `/clusters/${clusterName}/credentials/${alias}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "DELETE",
+ });
+ return response.data;
+ },
+ updateCredentials: async function (
+ clusterName: string,
+ alias: string,
+ data:{resource:string}
+ ) {
+ const url = `/clusters/${clusterName}/credentials/${alias}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data:{
+ Credential:data.resource
+ }
+ });
+ return response.data;
+ },
+ listCredentials: async function (
+ clusterName: string,
+ ) {
+ const url = `/clusters/${clusterName}/credentials?fields=Credential/*`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ credentialsStoreInfo: async function (
+ clusterName: string,
+ ) {
+ const url = `/clusters/${clusterName}?fields=Clusters/credential_store_properties`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+
+};
+
+export default CredentialsApi
diff --git a/ambari-web/latest/src/api/hostsApi.ts b/ambari-web/latest/src/api/hostsApi.ts
index 789c6b4c53..7f73173107 100644
--- a/ambari-web/latest/src/api/hostsApi.ts
+++ b/ambari-web/latest/src/api/hostsApi.ts
@@ -16,6 +16,7 @@
* limitations under the License.
*/
+import { set } from "lodash";
import { ambariApi } from "./config/axiosConfig";
export const HostsApi = {
@@ -181,6 +182,7 @@ export const HostsApi = {
},
},
});
+ set(response, "data.status", response.status);
return response.data;
},
updateHostComponents: async function (
@@ -209,6 +211,7 @@ export const HostsApi = {
},
}),
});
+ set(response, "data.status", response?.status)
return response.data;
},
deleteHostComponent: async function (
@@ -221,6 +224,7 @@ export const HostsApi = {
url: url,
method: "DELETE",
});
+ set(response, "data.status", response.status);
return response.data;
},
deleteHostComponents: async function (data: any, clusterName: string) {
@@ -239,6 +243,7 @@ export const HostsApi = {
method: "POST",
data: data,
});
+ set(response, "data.status", response?.status)
return response.data;
},
getHostsData: async function (fields: string) {
@@ -477,6 +482,16 @@ export const HostsApi = {
});
return response.data;
},
+ commonServiceConfigurationsMove: async function (clusterName: string, data: any) {
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data,
+ });
+ set(response, "data.status", response?.status)
+ return response.data;
+ },
updateServicePassiveState: async function (
clusterName: string,
serviceName: string,
@@ -658,4 +673,31 @@ export const HostsApi = {
});
return response.data;
},
+ transitionToObserver: 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: "MAKEOBSERVER",
+ 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;
+ },
};
diff --git a/ambari-web/latest/src/api/kerberosApi.ts b/ambari-web/latest/src/api/kerberosApi.ts
index 9107ed9cd9..9d736c95e7 100644
--- a/ambari-web/latest/src/api/kerberosApi.ts
+++ b/ambari-web/latest/src/api/kerberosApi.ts
@@ -16,7 +16,7 @@
* limitations under the License.
*/
-import { ambariApi } from "./config/axiosConfig";
+import { ambariApi, supressErrorAmbariApi } from "./config/axiosConfig";
const KerberosApi = {
getKerberosDescriptorProperties: async function (
@@ -57,38 +57,65 @@ const KerberosApi = {
});
return response.data;
},
- saveKerberosData: async function (clusterName: string, payload: any) {
- const url = `clusters/${clusterName}/artifacts/kerberos_descriptor`;
- const response = await ambariApi.request({
+ saveKerberosData: async function(clusterName: string, payload: any) {
+ const url = `clusters/${clusterName}/artifacts/kerberos_descriptor`
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "POST",
- data: payload,
- });
+ data: payload
+ })
+ return response.data;
+ },
+ saveAndEditKerberosData: async function(clusterName: string, payload: any) {
+ const url = `clusters/${clusterName}/artifacts/kerberos_descriptor`
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: "PUT",
+ data: payload
+ })
return response.data;
},
postKDCAdminCredentials: async function (clusterName: string, payload: any) {
- const url = `clusters/${clusterName}/credentials/kdc.admin.credential`;
- const response = await ambariApi.request({
+ const url = `clusters/${clusterName}/credentials/kdc.admin.credential`
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "POST",
- data: payload,
- });
+ data: payload
+ })
+ return response.data;
+ },
+ postKDCAdminCredentialsSupress: async function (clusterName: string, payload: any) {
+ const url = `clusters/${clusterName}/credentials/kdc.admin.credential`
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ })
+ return response.data;
+ },
+ submitKDCAdminCredentials: async function (clusterName: string, payload: any, method: string) {
+ const url = `clusters/${clusterName}/credentials/kdc.admin.credential`
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: method,
+ data: payload
+ })
return response.data;
},
deleteKDCAdminCredentials: async function (clusterName: string) {
- const url = `clusters/${clusterName}/credentials/kdc.admin.credential`;
+ const url = `clusters/${clusterName}/credentials/kdc.admin.credential`
const response = await ambariApi.request({
url: url,
method: "DELETE",
- });
+ })
return response.data;
},
getKDCAdminCredentials: async function (clusterName: string) {
- const url = `clusters/${clusterName}/credentials?fields=Credential/*`;
- const response = await ambariApi.request({
+ const url = `clusters/${clusterName}/credentials?fields=Credential/*`
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "GET",
- });
+ })
return response.data;
},
@@ -110,7 +137,7 @@ const KerberosApi = {
serviceName: string
) {
const url = `clusters/${clusterName}/services/${serviceName}`;
- const response = await ambariApi.request({
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "DELETE",
});
@@ -126,6 +153,15 @@ const KerberosApi = {
});
return response.data;
},
+
+ getCredentialStoreInfo: async function (clusterName: string) {
+ const url = `clusters/${clusterName}?fields=Clusters/credential_store_properties`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
};
export default KerberosApi;
diff --git a/ambari-web/latest/src/api/LogApi.ts b/ambari-web/latest/src/api/logApi.ts
similarity index 98%
copy from ambari-web/latest/src/api/LogApi.ts
copy to ambari-web/latest/src/api/logApi.ts
index 0011202dc7..8ecec1b9ad 100644
--- a/ambari-web/latest/src/api/LogApi.ts
+++ b/ambari-web/latest/src/api/logApi.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
const LogApi = {
diff --git a/ambari-web/latest/src/api/loginApi.ts b/ambari-web/latest/src/api/loginApi.ts
index 97cbc5e66c..593c0cda82 100644
--- a/ambari-web/latest/src/api/loginApi.ts
+++ b/ambari-web/latest/src/api/loginApi.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import {ambariApi} from "./config/axiosConfig.ts";
import {misc} from "../Utils/misc.ts";
import { db } from "../Utils/db";
diff --git a/ambari-web/latest/src/api/metricsApi.ts b/ambari-web/latest/src/api/metricsApi.ts
index 0c6413ca1b..203f4f9752 100644
--- a/ambari-web/latest/src/api/metricsApi.ts
+++ b/ambari-web/latest/src/api/metricsApi.ts
@@ -15,58 +15,97 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ambariApi } from "./config/axiosConfig";
-const metricsApi = {
+import { ambariApi, supressErrorAmbariApi } from "./config/axiosConfig";
+
+const MetricsApi = {
getWidgets: async function (userName: string, urlParams: string ) {
+ const url = `/users/${userName}/activeWidgetLayouts?${urlParams}`;
+
try {
- const url = `/users/${userName}/activeWidgetLayouts?${urlParams}`;
const response = await ambariApi.request({
url: url,
method: "GET",
});
return response.data;
} catch (error) {
- console.warn("Error fetching active widgets from API, using fallback data:", error);
- // Extract service name from urlParams (format: WidgetLayoutInfo/section_name=SERVICE_SUMMARY)
- const serviceMatch = urlParams.match(/section_name=(\w+)_SUMMARY/);
- const serviceName = serviceMatch ? serviceMatch[1] : "HDFS";
-
- // Return a fallback structure that matches the expected format
- return {
- items: [
- {
- WidgetLayoutInfo: {
- id: "fallback-layout",
- layout_name: `${serviceName.toLowerCase()}_dashboard`,
- section_name: `${serviceName}_SUMMARY`,
- scope: "USER",
- widgets: [
- {
- WidgetInfo: {
- id: "fallback-active-graph",
- widget_name: `${serviceName} Metrics Graph`,
- widget_type: "GRAPH",
- metrics: JSON.stringify([
- {
- name: "active_metric_graph",
- service_name: serviceName,
- component_name: serviceName === "HDFS" ? "NAMENODE" : "RESOURCEMANAGER",
- metric_path: "metrics/jvm/HeapMemoryMax",
- }
- ]),
- values: JSON.stringify([]),
- properties: JSON.stringify({}),
- scope: "USER",
- author: userName,
- description: "Active graph widget (fallback)"
- }
- }
- ]
- }
- }
- ]
- };
+ console.warn("Error fetching active widgets from API:", error);
+ throw error; // Let the caller handle the fallback logic
+ }
+ },
+
+ getDefaultWidgetLayoutByName: async function (layoutName: string) {
+ try {
+ const urlParams = `WidgetLayoutInfo/layout_name=${layoutName}`;
+ const url = `/widget_layouts?${urlParams}`;
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ } catch (error) {
+ console.warn("Error fetching default widget layout:", error);
+ throw error;
+ }
+ },
+
+ createUserWidgetLayout: async function (clusterName: string, widgetLayoutData: any) {
+ try {
+ const url = `/clusters/${clusterName}/widget_layouts`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: widgetLayoutData,
+ });
+ return response.data;
+ } catch (error) {
+ console.warn("Error creating user widget layout:", error);
+ throw error;
+ }
+ },
+
+ getAllActiveWidgetLayouts: async function (userName: string) {
+ try {
+ const url = `/users/${userName}/activeWidgetLayouts`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ } catch (error) {
+ console.warn("Error fetching all active widget layouts:", error);
+ throw error;
+ }
+ },
+
+ saveActiveWidgetLayouts: async function (userName: string, activeWidgetLayouts: any) {
+ try {
+ const url = `/users/${userName}/activeWidgetLayouts`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: activeWidgetLayouts,
+ });
+ return response.data;
+ } catch (error) {
+ console.warn("Error saving active widget layouts:", error);
+ throw error;
+ }
+ },
+
+ getWidgetsByService: async function (clusterName: string, serviceName: string) {
+ try {
+ // Get widgets for the specific service
+ const urlParams = `WidgetInfo/widget_type.in(GRAPH,NUMBER,GAUGE)&WidgetInfo/scope=CLUSTER&WidgetInfo/metrics.matches(.*"service_name":"${serviceName}".*)&fields=*`;
+ const url = `/clusters/${clusterName}/widgets?${urlParams}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ } catch (error) {
+ console.warn("Error fetching widgets by service:", error);
+ throw error;
}
},
getAllSharedWidgets: async function (clusterName: string) {
@@ -281,30 +320,21 @@ const metricsApi = {
hostComponentCriteria: string,
metricsPath: string
) {
+ // Don't encode the metrics path - let axios handle it properly
+ // encodeURIComponent breaks comma-separated field lists
+ const url = `/clusters/${clusterName}/host_components?HostRoles/component_name=${componentName}&${hostComponentCriteria}&fields=${metricsPath}&format=null_padding`;
+
try {
- const url = `/clusters/${clusterName}/host_components?HostRoles/component_name=${componentName}&${hostComponentCriteria}&fields=${metricsPath}&format=null_padding`;
const response = await ambariApi.request({
url: url,
method: "GET",
});
return response.data;
} catch (error) {
- console.warn("Error fetching host component metrics, returning fallback data:", error);
- // Return a fallback structure with empty metrics data
+ console.error("Error fetching host component metrics:", error);
+ // Return empty structure to avoid displaying incorrect data
return {
- items: [
- {
- href: "#",
- HostRoles: {
- component_name: componentName,
- host_name: "localhost"
- },
- metrics: {
- dfs: { namenode: { ClusterId: "fallback-cluster-id" } },
- jvm: { HeapMemoryMax: 1073741824, HeapMemoryUsed: 536870912 }
- }
- }
- ]
+ items: []
};
}
},
@@ -314,25 +344,25 @@ const metricsApi = {
serviceName: string,
metricsPath: string
) {
+ // Don't encode the metrics path - let axios handle it properly
+ // encodeURIComponent breaks comma-separated field lists
+ const url = `/clusters/${clusterName}/services/${serviceName}/components/${componentName}?fields=${metricsPath}&format=null_padding`;
+
try {
- const url = `/clusters/${clusterName}/services/${serviceName}/components/${componentName}?fields=${metricsPath}&format=null_padding`;
const response = await ambariApi.request({
url: url,
method: "GET",
});
return response.data;
} catch (error) {
- console.warn("Error fetching service component metrics, returning fallback data:", error);
- // Return a fallback structure with empty metrics data
+ console.error("Error fetching service component metrics:", error);
+ // Return empty structure to avoid displaying incorrect data
return {
ServiceComponentInfo: {
component_name: componentName,
service_name: serviceName
},
- metrics: {
- dfs: { namenode: { ClusterId: "fallback-cluster-id" } },
- jvm: { HeapMemoryMax: 1073741824, HeapMemoryUsed: 536870912 }
- }
+ metrics: {}
};
}
},
@@ -407,4 +437,4 @@ const metricsApi = {
},
};
-export default metricsApi;
+export default MetricsApi;
diff --git a/ambari-web/latest/src/api/nameNodeFederationApi.ts b/ambari-web/latest/src/api/nameNodeFederationApi.ts
new file mode 100644
index 0000000000..fac2ea648e
--- /dev/null
+++ b/ambari-web/latest/src/api/nameNodeFederationApi.ts
@@ -0,0 +1,56 @@
+/**
+ * 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.
+ */
+
+import { set } from "lodash";
+import { ambariApi } from "./config/axiosConfig";
+
+const NameNodeFederationApi = {
+ formatNameNode : async function(clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/requests`
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ set(response, "data.status", response?.status)
+ return response.data;
+ },
+
+ formatZKFC : async function(clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/requests`
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ set(response, "data.status", response?.status)
+ return response.data;
+ },
+ bootStrapNameNode : async function(clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/requests`
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ set(response, "data.status", response?.status)
+ return response.data;
+ }
+}
+
+export default NameNodeFederationApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/validations.ts b/ambari-web/latest/src/api/persistanceApi.ts
similarity index 83%
copy from ambari-web/latest/src/api/validations.ts
copy to ambari-web/latest/src/api/persistanceApi.ts
index 27ea68e03b..a5dec99aaa 100644
--- a/ambari-web/latest/src/api/validations.ts
+++ b/ambari-web/latest/src/api/persistanceApi.ts
@@ -15,11 +15,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
-const ValidationsApi = {
- validateMapping: async (stack:string,version:string,data:any) => {
- const url = `/stacks/${stack}/versions/${version}/validations`;
+const PersistanceApi = {
+ validateMapping: async (data:any) => {
+ const url = `/persist`;
const response = await ambariApi.request({
url,
data,
@@ -29,4 +30,4 @@ const ValidationsApi = {
},
};
-export default ValidationsApi;
\ No newline at end of file
+export default PersistanceApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/recommendationsApi.ts b/ambari-web/latest/src/api/quicklinksApi.ts
similarity index 67%
copy from ambari-web/latest/src/api/recommendationsApi.ts
copy to ambari-web/latest/src/api/quicklinksApi.ts
index e2f0f7a707..c97d482776 100644
--- a/ambari-web/latest/src/api/recommendationsApi.ts
+++ b/ambari-web/latest/src/api/quicklinksApi.ts
@@ -15,18 +15,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { ambariApi } from "./config/axiosConfig";
-const RecommendationsApi = {
- loadRecommendations: async (stack:string,version:string,data:any) => {
- const url = `/stacks/${stack}/versions/${version}/recommendations`;
- const response = await ambariApi.request({
- url,
- data,
- method: "POST",
- });
- return response.data;
- },
-};
+import { ambariApi } from "./config/axiosConfig";
-export default RecommendationsApi;
\ No newline at end of file
+export const QuicklinksApi = {
+ getQuicklinks: async (stackVersion: string,stackName:string, serviceName: string) => {
+ const url = `/stacks/${stackName}/versions/${stackVersion}/services/${serviceName}/quicklinks?QuickLinkInfo/default=true&fields=*&_=${Date.now()}'`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/recommendationsApi.ts b/ambari-web/latest/src/api/recommendationsApi.ts
index e2f0f7a707..4a9aaf71a3 100644
--- a/ambari-web/latest/src/api/recommendationsApi.ts
+++ b/ambari-web/latest/src/api/recommendationsApi.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
const RecommendationsApi = {
diff --git a/ambari-web/latest/src/api/requestApi.ts b/ambari-web/latest/src/api/requestApi.ts
index 536d4cc948..f0a2847aa5 100644
--- a/ambari-web/latest/src/api/requestApi.ts
+++ b/ambari-web/latest/src/api/requestApi.ts
@@ -15,12 +15,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { set } from "lodash";
-import { ambariApi } from "./config/axiosConfig";
+import { ambariApi, supressErrorAmbariApi } from "./config/axiosConfig";
export const RequestApi = {
getRequestStatus: async function (clusterName: string,requestId:string) {
const url = `/clusters/${clusterName}/requests/${requestId}?fields=*,tasks/Tasks/request_id,tasks/Tasks/command,tasks/Tasks/command_detail,tasks/Tasks/ops_display_name,tasks/Tasks/host_name,tasks/Tasks/id,tasks/Tasks/role,tasks/Tasks/status&minimal_response=true`;
- const response = await ambariApi.request({
+ const response = await supressErrorAmbariApi.request({
url: url,
method: "GET",
});
@@ -44,6 +45,7 @@ export const RequestApi = {
"Content-Type":"text/plain"
}
});
+ set(response, "data.status", response.status);
return response.data;
},
startServices: async function (clusterName: string, payload: any, params: string, method="PUT"){
@@ -68,6 +70,7 @@ export const RequestApi = {
"Content-Type":"text/plain"
}
})
+ set(response, "data.status", response.status);
return response.data;
},
postRequest: async function (clusterName: string, payload: any, method="POST") {
@@ -77,6 +80,7 @@ export const RequestApi = {
method: method,
data: payload,
})
+ set(response, "data.status", response.status);
return response.data;
},
getServices: async function (clusterName: string, payload: any, params: string, method="PUT"){
@@ -119,6 +123,7 @@ export const RequestApi = {
"Content-Type":"text/plain"
}
})
+ set(response, "data.status", response.status);
return response.data;
},
regenerateKeytabs: async function (clusterName: string, payload: any, params: string) {
@@ -131,6 +136,7 @@ export const RequestApi = {
"Content-Type": "text/plain"
}
})
+ set(response, "data.status", response.status);
return response.data;
},
kerberosDescriptor: async function (clusterName: string, payload: any) {
diff --git a/ambari-web/latest/src/api/ServiceApi.ts b/ambari-web/latest/src/api/serviceApi.ts
similarity index 99%
rename from ambari-web/latest/src/api/ServiceApi.ts
rename to ambari-web/latest/src/api/serviceApi.ts
index 7aeb4ddf82..e297196183 100644
--- a/ambari-web/latest/src/api/ServiceApi.ts
+++ b/ambari-web/latest/src/api/serviceApi.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { set } from "lodash";
import { ambariApi } from "./config/axiosConfig";
diff --git a/ambari-web/latest/src/api/serviceConfigApi.ts b/ambari-web/latest/src/api/serviceConfigApi.ts
new file mode 100644
index 0000000000..a66adcd0fd
--- /dev/null
+++ b/ambari-web/latest/src/api/serviceConfigApi.ts
@@ -0,0 +1,38 @@
+/**
+ * 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.
+ */
+
+import { ambariApi } from "./config/axiosConfig";
+export const ServiceConfigApi = {
+ getServiceConfig: async function (clusterName: string, serviceName: string) {
+ const url = `/clusters/${clusterName}/configurations/service_config_versions?service_name=${serviceName}&fields=service_config_version,user,hosts,group_id,group_name,is_current,createtime,service_name,service_config_version_note,stack_id,is_cluster_compatible&sortBy=service_config_version.desc&minimal_response=true`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response;
+ },
+ setIsCurrent: async function (clusterName: string, selectedServices: string[]) {
+ const servicesQuery = selectedServices.join(',');
+ const url = `/clusters/${clusterName}/configurations/service_config_versions?service_name.in(${servicesQuery})&is_current=true&fields=*&_=${Date.now()}\`;`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response;
+ },
+};
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/validations.ts b/ambari-web/latest/src/api/validations.ts
index 27ea68e03b..5c41e59a6c 100644
--- a/ambari-web/latest/src/api/validations.ts
+++ b/ambari-web/latest/src/api/validations.ts
@@ -15,6 +15,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { ambariApi } from "./config/axiosConfig";
const ValidationsApi = {
diff --git a/ambari-web/latest/src/api/VersionsApi.ts b/ambari-web/latest/src/api/versionsApi.ts
similarity index 68%
rename from ambari-web/latest/src/api/VersionsApi.ts
rename to ambari-web/latest/src/api/versionsApi.ts
index 5f0def0033..f36ac21fc0 100644
--- a/ambari-web/latest/src/api/VersionsApi.ts
+++ b/ambari-web/latest/src/api/versionsApi.ts
@@ -1,3 +1,4 @@
+
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -18,6 +19,171 @@
import { ambariApi } from "./config/axiosConfig";
const VersionsApi = {
+ versionsList: async function (repoVersion: string, clusterName: string) {
+ const url = `/clusters/${clusterName}/stack_versions?fields=*&ClusterStackVersions/repository_version=${repoVersion}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getRepos: async function () {
+ const url = `/stacks?fields=versions/repository_versions/RepositoryVersions`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getClusterInfo: async function () {
+ const url = `/clusters?fields=Clusters/cluster_id`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getVersionDefinitions: async function () {
+ const url = `/version_definitions?fields=VersionDefinition/stack_default,VersionDefinition/stack_repo_update_link_exists,VersionDefinition/max_jdk,VersionDefinition/min_jdk,operating_systems/repositories/Repositories/*,operating_systems/OperatingSystems/*,VersionDefinition/stack_services,VersionDefinition/repository_version&VersionDefinition/show_available=true&VersionDefinition/stack_name=VDP&_=1727429673209`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getAllVersionDefinitions: async function () {
+ const url = `/version_definitions`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getVersionOperatingSystems: async function (versionName: string) {
+ const url = `/stacks/VDP/versions/${versionName}?fields=operating_systems/repositories/Repositories`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ readVersionInfo: async function (
+ payload: any,
+ headers = {},
+ isDryRun = true
+ ) {
+ const url = `/version_definitions?skip_url_check=true${
+ isDryRun ? "&dry_run=true" : ""
+ }`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ headers: { ...headers },
+ data: payload,
+ });
+ return response.data;
+ },
+ validateRepos: async function name(
+ stack: string,
+ stackVersion: string,
+ os: string,
+ repo: string,
+ payload: { base_url: string; repo_name: string }
+ ) {
+ const url = `/stacks/${stack}/versions/${stackVersion}/operating_systems/${os}/repositories/${repo}?validate_only=true`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: { Repositories: payload },
+ });
+ return response.data;
+ },
+ saveRepoVersions: async function name(
+ stack: string,
+ stackVersion: string,
+ version_id: string,
+ payload: any
+ ) {
+ const url = `/stacks/${stack}/versions/${stackVersion}/repository_versions/${version_id}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: payload,
+ });
+ return response.data;
+ },
+ getRepoDetails: async (stack: string, version: string) => {
+ const url = `/stacks/${stack}/versions?fields=repository_versions/operating_systems/repositories/*,repository_versions/operating_systems/OperatingSystems/*,repository_versions/RepositoryVersions/*&repository_versions/RepositoryVersions/repository_version=${version}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ deleteRepositoryVersion: async function deleteRepositoryVersion(
+ stack: string,
+ stackVersion: string,
+ repositoryVersion: string
+ ) {
+ const url = `/stacks/${stack}/versions/${stackVersion}/repository_versions/${repositoryVersion}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "DELETE",
+ });
+ return response.data;
+ },
+ postVersionDefinitionFile: async function postVersionDefinitionFile(
+ data: any
+ ) {
+ const url = `/version_definitions`;
+ const response = await ambariApi.request({
+ url,
+ method: "POST",
+ data,
+ });
+ return response.data;
+ },
+ getVersionDefinition: async function getVersionDefinition() {
+ const url = `/version_definitions`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ updateRepoOSInfo: async function updateRepoOSInfo(
+ stackName: string,
+ stackVersion: string,
+ repoVersionId: string,
+ data: any
+ ) {
+ {
+ const url = `/stacks/${stackName}/versions/${stackVersion}/repository_versions/${repoVersionId}`;
+ const response = await ambariApi.request({
+ url,
+ method: "PUT",
+ data,
+ });
+ return response.data;
+ }
+ },
+ updateOSInfo: async function updateRepoOSInfo(
+ stackName: string,
+ stackVersion: string,
+ operatingSystem: string,
+ repoVersionId: string,
+ data: any
+ ) {
+ {
+ const url = `/stacks/${stackName}/versions/${stackVersion}/operating_systems/${operatingSystem}/repositories/${repoVersionId}`;
+ const response = await ambariApi.request({
+ url,
+ method: "PUT",
+ data,
+ });
+ return response.data;
+ }
+ },
getServices: async function (clusterName: string) {
const url = `/clusters/${clusterName}/stack_versions?fields=*,repository_versions/*`;
const response = await ambariApi.request({
@@ -33,7 +199,7 @@ const VersionsApi = {
method: "GET",
});
return response.data;
- },
+ },
get_supported_upgradeTypes: async function (
stackName: string,
stackVersion: string,
@@ -75,32 +241,6 @@ const VersionsApi = {
});
return response.data;
},
- getUpgradeItem: async function (
- upgradeId: number,
- groupId: number,
- stageId: number,
- clusterName: string
- ) {
- const url =
- `/clusters/${clusterName}/upgrades/${upgradeId}/upgrade_groups/${groupId}/upgrade_items/${stageId}?fields=` +
- [
- "UpgradeItem/group_id",
- "UpgradeItem/stage_id",
- "tasks/Tasks/command_detail",
- "tasks/Tasks/host_name",
- "tasks/Tasks/role",
- "tasks/Tasks/request_id",
- "tasks/Tasks/stage_id",
- "tasks/Tasks/status",
- "tasks/Tasks/structured_out",
- ].join(",") +
- "&minimal_response=true";
- const response = await ambariApi.request({
- url: url,
- method: "GET",
- });
- return response.data;
- },
getTasksList: async function (
upgradeId: number,
groupId: number,
@@ -141,6 +281,32 @@ const VersionsApi = {
});
return response.data;
},
+ getUpgradeItem: async function (
+ upgradeId: number,
+ groupId: number,
+ stageId: number,
+ clusterName: string
+ ) {
+ const url =
+ `/clusters/${clusterName}/upgrades/${upgradeId}/upgrade_groups/${groupId}/upgrade_items/${stageId}?fields=` +
+ [
+ "UpgradeItem/group_id",
+ "UpgradeItem/stage_id",
+ "tasks/Tasks/command_detail",
+ "tasks/Tasks/host_name",
+ "tasks/Tasks/role",
+ "tasks/Tasks/request_id",
+ "tasks/Tasks/stage_id",
+ "tasks/Tasks/status",
+ "tasks/Tasks/structured_out",
+ ].join(",") +
+ "&minimal_response=true";
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
abortUpgrade: async function (clusterName: string, upgradeId: number) {
const url = `/clusters/${clusterName}/upgrades/${upgradeId}`;
const response = await ambariApi.request({
@@ -182,14 +348,6 @@ const VersionsApi = {
});
return response.data;
},
- getRepoDetails: async (stack: string, version: string) => {
- const url = `/stacks/${stack}/versions?fields=repository_versions/operating_systems/repositories/*,repository_versions/operating_systems/OperatingSystems/*,repository_versions/RepositoryVersions/*&repository_versions/RepositoryVersions/repository_version=${version}`;
- const response = await ambariApi.request({
- url: url,
- method: "GET",
- });
- return response.data;
- },
getUpgradeHistory: async function (clusterName: string) {
const url = `/clusters/${clusterName}/upgrades?fields=Upgrade`;
const response = await ambariApi.request({
@@ -198,35 +356,6 @@ const VersionsApi = {
});
return response.data;
},
- validateRepos: async function name(
- stack: string,
- stackVersion: string,
- os: string,
- repo: string,
- payload: { base_url: string; repo_name: string }
- ) {
- const url = `/stacks/${stack}/versions/${stackVersion}/operating_systems/${os}/repositories/${repo}?validate_only=true`;
- const response = await ambariApi.request({
- url: url,
- method: "POST",
- data: { Repositories: payload },
- });
- return response.data;
- },
- saveRepoVersions: async function name(
- stack: string,
- stackVersion: string,
- version_id: string,
- payload: any
- ) {
- const url = `/stacks/${stack}/versions/${stackVersion}/repository_versions/${version_id}`;
- const response = await ambariApi.request({
- url: url,
- method: "PUT",
- data: payload,
- });
- return response.data;
- },
updateUpgrade: async function (upgradeId: number, data: any, clusterName: string) {
const url = `/clusters/${clusterName}/upgrades/${upgradeId}`;
const response = await ambariApi.request({
diff --git a/ambari-web/latest/src/api/LogApi.ts b/ambari-web/latest/src/api/viewApi.tsx
similarity index 67%
rename from ambari-web/latest/src/api/LogApi.ts
rename to ambari-web/latest/src/api/viewApi.tsx
index 0011202dc7..6c9d31db7a 100644
--- a/ambari-web/latest/src/api/LogApi.ts
+++ b/ambari-web/latest/src/api/viewApi.tsx
@@ -17,16 +17,23 @@
*/
import { ambariApi } from "./config/axiosConfig";
-const LogApi = {
-
- getLogData: async function (clusterName:string, requestId:string) {
- const url = `/clusters/${clusterName}/requests/${requestId}?fields=tasks/Tasks/command,tasks/Tasks/command_detail,tasks/Tasks/ops_display_name,tasks/Tasks/exit_code,tasks/Tasks/start_time,tasks/Tasks/end_time,tasks/Tasks/host_name,tasks/Tasks/id,tasks/Tasks/role,tasks/Tasks/status&minimal_response=true`;
+/**
+ * Fetches a list of instance names from the Ambari API.
+ *
+ * @returns {Promise<any[]>} A promise that resolves to an array of instance names.
+ */
+const ViewApi = {
+ getInstances: async function() {
+ const url = `/views?fields=versions/ViewVersionInfo/version,versions/instances/ViewInstanceInfo,versions/*&versions/ViewVersionInfo/system=false`;
const response = await ambariApi.request({
url: url,
method: "GET",
});
- return response.data;
- },
+
+
+ return response.data
+
+ }
};
-export default LogApi;
\ No newline at end of file
+export default ViewApi;
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/wizardApi.ts b/ambari-web/latest/src/api/wizardApi.ts
index c737250aea..1a141bed16 100644
--- a/ambari-web/latest/src/api/wizardApi.ts
+++ b/ambari-web/latest/src/api/wizardApi.ts
@@ -64,7 +64,7 @@ const WizardApi = {
method: "GET",
});
return response.data;
- },
+ }
};
export default WizardApi;
diff --git a/ambari-web/latest/src/components/AssignMasters.tsx b/ambari-web/latest/src/components/AssignMasters.tsx
index fdb89f39c5..51f03e074b 100644
--- a/ambari-web/latest/src/components/AssignMasters.tsx
+++ b/ambari-web/latest/src/components/AssignMasters.tsx
@@ -17,42 +17,19 @@
*/
import { useEffect, useReducer, useState } from "react";
-import { Row, Col, Form, Card, Button, CardBody } from "react-bootstrap";
+import { Row, Col, Form, Card, Button, CardBody, Alert } from "react-bootstrap";
+import { ChooseServicesApi } from "../api/chooseServicesApi";
+import AssignMastersApi from "../api/assignMastersApi";
import { Utility } from "../Utils/Utility.ts";
import { misc } from "../Utils/misc.ts";
import Spinner from "./Spinner.tsx";
-import _, { filter, get, map, uniq } from "lodash";
+import { filter, get, map, uniq } from "lodash";
import Select from "react-select";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
-import AssignMastersApi from "../api/AssignMastersApi.ts";
-import { ServicesResponse } from "../screens/ClusterWizard/types/StackServiceComponent.ts";
-import { ChooseServicesApi } from "../api/chooseServicesApi.ts";
-
-interface Host {
- hostname: string;
- cores: number;
- memory: number;
- components: string[];
-}
-
-interface Masters {
- display_name: string;
- component: string;
- serviceId: string;
- host_id: number;
- hostName: string;
- isInstalled?: boolean;
-}
-
-interface State {
- hosts: { [key: string]: Host };
-}
-
-interface Action {
- type: string;
- payload: any;
-}
+import { blueprintUtils } from "../screens/ClusterWizard/utils.ts";
+import { maxToInstall, isMultipleAllowed } from "../screens/Hosts/utils.tsx";
+import { AssignMastersProps, Host, Masters, State, Action, ServicesResponse } from "../screens/ClusterWizard/types/AssignMastersTypes.ts";
const initialState: State = {
hosts: {},
@@ -77,12 +54,9 @@ function reducer(state: State, action: Action): State {
// Add component to new host if newHost is not null and it doesn't already exist
if (newHost && !updatedHosts[newHost].components.includes(component)) {
- const index = updatedHosts[newHost].components.indexOf(component);
- if (index !== -1) {
- updatedHosts[newHost].components.splice(index + 1, 0, component);
- } else {
- updatedHosts[newHost].components.push(component);
- }
+ updatedHosts[newHost].components.push(component);
+ // Note: We don't sort here as the sorting will be handled in the data preparation phase
+ // The components array in state.hosts is just for tracking which components are on which hosts
}
return {
@@ -95,22 +69,9 @@ function reducer(state: State, action: Action): State {
}
}
-interface AssignMastersProps {
- STACK: string;
- VERSION: string;
- superMasters: any;
- hostsList: any;
- services: string[];
- setCanProceed: (canProcced: boolean) => void;
- dispatch: any;
- installedServices?: string[];
- parentState?:any;
-}
-
export default function AssignMasters({
STACK,
VERSION,
- superMasters,
hostsList,
services,
dispatch: dispatchParent,
@@ -125,6 +86,9 @@ export default function AssignMasters({
) || initialState);
const [loading, setLoading] = useState(false);
const [mastersData, setMastersData] = useState<Masters[]>([]);
+ const [servicesAndComponents, setServicesAndComponents] = useState<ServicesResponse | null>(null);
+ const [generalErrorMessages, setGeneralErrorMessages] = useState<string[]>([]);
+ const [generalWarningMessages, setGeneralWarningMessages] = useState<string[]>([]);
const notMasters = ["MYSQL_SERVER", "HIVE_SERVER_INTERACTIVE"];
useEffect(() => {
@@ -213,6 +177,14 @@ export default function AssignMasters({
}
});
+ // Sort hosts alphabetically and create a new sorted object
+ const sortedHostsData: { [key: string]: Host } = {};
+ Object.keys(hostsData)
+ .sort()
+ .forEach(hostname => {
+ sortedHostsData[hostname] = hostsData[hostname];
+ });
+
// validations.
const validationPayload = {
hosts: hostnames,
@@ -228,15 +200,21 @@ export default function AssignMasters({
},
services: services,
};
- AssignMastersApi.postValidations(validationPayload, STACK, VERSION);
- //TODO: Check for validations response.
-
- setCanProceed(true);
- const servicesAndComponents: ServicesResponse =
+
+ try {
+ const validationResponse = await AssignMastersApi.postValidations(validationPayload, STACK, VERSION);
+ updateValidationsSuccessCallback(validationResponse);
+ } catch (error) {
+ console.error('Validation API call failed:', error);
+ setCanProceed(false);
+ }
+ const servicesAndComponentsData: ServicesResponse =
await ChooseServicesApi.getServices(STACK, VERSION);
+
+ setServicesAndComponents(servicesAndComponentsData);
// Filter components based on is_master and ensure no duplicates on a single host
- // except for superMaster components like ZOOKEEPER_SERVER which can be on multiple hosts
+ // except for components that allow multiple instances (using isMasterAddableInstallerWizard)
const assignedComponents = new Set<string>();
Object.keys(hostsData).forEach((hostname) => {
hostsData[hostname].components = hostsData[hostname].components.filter(
@@ -244,25 +222,25 @@ export default function AssignMasters({
if(notMasters.includes(component)) {
return false;
}
- const serviceComponent = servicesAndComponents.items
+ const serviceComponent = servicesAndComponentsData.items
.flatMap((service: any) => service.components)
.find(
(comp: any) =>
- _.get(comp, "StackServiceComponents.component_name") ===
+ get(comp, "StackServiceComponents.component_name") ===
component
);
- // Allow superMaster components on multiple hosts
- if (superMasters.includes(component)) {
- return serviceComponent &&
- _.get(serviceComponent, "StackServiceComponents.is_master");
+ if (!serviceComponent || !get(serviceComponent, "StackServiceComponents.is_master")) {
+ return false;
+ }
+
+ const stackComponent = get(serviceComponent, "StackServiceComponents");
+ if (isMasterAddableInstallerWizard(stackComponent)) {
+ return true;
}
- if (
- serviceComponent &&
- _.get(serviceComponent, "StackServiceComponents.is_master") &&
- !assignedComponents.has(component)
- ) {
+ // For single-instance components, ensure no duplicates
+ if (!assignedComponents.has(component)) {
assignedComponents.add(component);
return true;
}
@@ -271,9 +249,9 @@ export default function AssignMasters({
);
});
- dispatch({ type: "SET_HOSTS_DATA", payload: hostsData });
+ dispatch({ type: "SET_HOSTS_DATA", payload: sortedHostsData });
dispatchParent({
- hostsData,
+ hostsData: sortedHostsData,
mastersData: getTransformedMastersData(mastersData),
state,
});
@@ -283,12 +261,89 @@ export default function AssignMasters({
}, []);
useEffect(() => {
+ const transformedMastersData = getTransformedMastersData(mastersData);
dispatchParent({
- mastersData: getTransformedMastersData(mastersData),
+ mastersData: transformedMastersData,
state,
});
+ validateChange(transformedMastersData)
}, [mastersData]);
+ const isMasterAddableOnlyOnHA = (component: any) => {
+ return ["NAMENODE", "RESOURCEMANAGER", "RANGER_ADMIN"].includes(
+ get(component, "component_name", "")
+ );
+ }
+
+ const isNotAddableOnlyInInstall = (component: any) => {
+ return [
+ "HIVE_METASTORE",
+ "HIVE_SERVER",
+ "RANGER_KMS_SERVER",
+ "OOZIE_SERVER",
+ "TIMELINE_READER",
+ "YARN_REGISTRY_DNS",
+ ].includes(get(component, "component_name", ""));
+ };
+
+ const isMasterAddableInstallerWizard = (component: any) => {
+ return (
+ get(component, "is_master", false) &&
+ isMultipleAllowed(component) &&
+ !isMasterAddableOnlyOnHA(component) &&
+ !isNotAddableOnlyInInstall(component)
+ );
+ };
+
+ const validateChange = async (transformedMastersData: any) => {
+ try {
+ const allHostnames = map(transformedMastersData, "host_name");
+ const hostnames = uniq(allHostnames);
+ const validationResponse = await AssignMastersApi.postValidations(
+ {
+ hosts: hostnames,
+ services,
+ validate: "host_groups",
+ recommendations: getValidationRequestBody(transformedMastersData),
+ },
+ STACK,
+ VERSION,
+ );
+ updateValidationsSuccessCallback(validationResponse);
+ } catch (error) {
+ console.error('Validation API call failed:', error);
+ setCanProceed(false);
+ }
+ };
+
+ const getValidationRequestBody = (transformedMastersData: any) => {
+ const allHostnames = map(transformedMastersData, "host_name");
+ const hostnames = uniq(allHostnames);
+ const masterBlueprint = blueprintUtils.getBlueprint(
+ hostnames,
+ getSelectedMastersGroupedMapping(transformedMastersData)
+ );
+
+ return masterBlueprint;
+ };
+
+ const getSelectedMastersGroupedMapping = (transformedMastersData: any) => {
+ const hostComponentMapping: any = [];
+ transformedMastersData.forEach((selectedMaster: any) => {
+ hostComponentMapping.push({
+ hostname: selectedMaster.host_name,
+ components: selectedMaster.masterServices.map(
+ (selectedComponent: any) => {
+ return {
+ name: selectedComponent.component,
+ };
+ }
+ ),
+ });
+ });
+ return hostComponentMapping;
+ };
+
const getTransformedMastersData = (mastersDataToBeTransformed: any) => {
const allHostnames = map(mastersDataToBeTransformed, "hostName");
const uniqueHostnames = uniq(allHostnames);
@@ -309,11 +364,12 @@ export default function AssignMasters({
};
useEffect(() => {
- async function setMasterComponentsData() {
- const servicesAndComponents: ServicesResponse =
- await ChooseServicesApi.getServices(STACK, VERSION);
+ function setMasterComponentsData() {
+ // Only proceed if we have cached services data
+ if (!servicesAndComponents) {
+ return;
+ }
-
const mastersData: Masters[] = Object.keys(state.hosts).flatMap(
(hostname, index) =>
state.hosts[hostname].components
@@ -323,37 +379,41 @@ export default function AssignMasters({
.flatMap((service: any) => service.components)
.find(
(comp: any) =>
- _.get(comp, "StackServiceComponents.component_name") ===
+ get(comp, "StackServiceComponents.component_name") ===
component
);
return {
- display_name: _.get(
+ display_name: get(
serviceComponent,
"StackServiceComponents.display_name"
),
component: component,
- serviceId: _.get(
+ serviceId: get(
serviceComponent,
"StackServiceComponents.service_name"
),
isInstalled: installedServices?.includes(
- _.get(serviceComponent, "StackServiceComponents.service_name")
+ get(serviceComponent, "StackServiceComponents.service_name")
),
host_id: index + 1,
hostName: hostname,
};
})
- // Sort components alphabetically by display_name
- .sort((a, b) => (a.display_name || '').localeCompare(b.display_name || ''))
);
+
+ // Sort all mastersData by display_name to ensure consistent ordering
+ const sortedMastersData = mastersData.sort((a, b) =>
+ (a.display_name || a.component).localeCompare(b.display_name || b.component)
+ );
+
dispatchParent({
- mastersData: getTransformedMastersData(mastersData),
+ mastersData: getTransformedMastersData(sortedMastersData),
state,
});
- setMastersData(mastersData);
+ setMastersData(sortedMastersData);
}
setMasterComponentsData();
- }, [state.hosts]);
+ }, [state.hosts, servicesAndComponents]);
const handleComponentChange = (
component: string,
@@ -367,11 +427,36 @@ export default function AssignMasters({
});
}
};
+ const getMaxNumberOfMasters = (componentName: string) => {
+ if (!servicesAndComponents) return 1;
+
+ const serviceComponent = servicesAndComponents.items
+ .flatMap((service: any) => service.components)
+ .find(
+ (comp: any) =>
+ get(comp, "StackServiceComponents.component_name") === componentName
+ );
+
+ if (!serviceComponent) return 1;
+
+ const stackComponent = get(serviceComponent, "StackServiceComponents");
+ const maxToInstallValue = maxToInstall(stackComponent);
+ const hostsNumber = Object.keys(state.hosts).length;
+
+ return Math.min(maxToInstallValue, hostsNumber);
+ };
const handleAddComponent = (component: string) => {
+ const maxNumMasters = getMaxNumberOfMasters(component);
+ const currentMasters = mastersData.filter(master => master.component === component);
+
+ if (currentMasters.length >= maxNumMasters) {
+ return false;
+ }
+
// Find the first host that doesn't already have this component
const availableHost = Object.keys(state.hosts).find((hostname) => {
- return !_.get(state.hosts[hostname], "components", []).includes(
+ return !get(state.hosts[hostname], "components", []).includes(
component as never
);
});
@@ -381,16 +466,89 @@ export default function AssignMasters({
type: "UPDATE_COMPONENT_HOST",
payload: { component, oldHost: null, newHost: availableHost, state },
});
+ return true;
}
+ return false;
};
const handleRemoveComponent = (component: string, hostname: string) => {
+ const currentMasters = mastersData.filter(master => master.component === component);
+
+ // Don't allow removal if only one instance exists
+ if (currentMasters.length <= 1) {
+ return false;
+ }
+
dispatch({
type: "UPDATE_COMPONENT_HOST",
payload: { component, oldHost: hostname, newHost: null, state },
});
+ return true;
};
+ /**
+ * Remove validation messages for components which are already installed
+ */
+ const filterNotInstalledComponents = (validationData: any) => {
+ return validationData.resources[0].items.filter((item: any) => {
+ const host = state.hosts[item.host];
+ return !host || !host.components.includes(item['component-name']);
+ });
+ };
+
+ /**
+ * Process validation response
+ */
+ const updateValidationsSuccessCallback = (data: any) => {
+ const newGeneralErrorMessages: string[] = [];
+ const newGeneralWarningMessages: string[] = [];
+
+ // Clear existing validation messages from mastersData
+ const updatedMastersData = mastersData.map(master => {
+ const { warnMessage, errorMessage, ...rest } = master;
+ return {
+ ...rest,
+ warnMessage: "",
+ errorMessage: "",
+ };
+ });
+
+ let anyErrors = false;
+
+ // Process validation data - filter out installed components
+ const validationData = filterNotInstalledComponents(data);
+ validationData
+ .filter((item: any) => item.type === 'host-component')
+ .forEach((item: any) => {
+ // Find the master component that matches this validation item
+ const masterIndex = updatedMastersData.findIndex(master =>
+ master.component === item['component-name'] &&
+ master.hostName === item.host
+ );
+
+ if (masterIndex !== -1) {
+ if (item.level === 'ERROR') {
+ anyErrors = true;
+ generalErrorMessages.push(item.message);
+ updatedMastersData[masterIndex] = {
+ ...updatedMastersData[masterIndex],
+ errorMessage: item.message
+ };
+ } else if (item.level === 'WARN') {
+ generalWarningMessages.push(item.message);
+ updatedMastersData[masterIndex] = {
+ ...updatedMastersData[masterIndex],
+ warnMessage: item.message
+ };
+ }
+ }
+ });
+
+ setGeneralErrorMessages(newGeneralErrorMessages);
+ setGeneralWarningMessages(newGeneralWarningMessages);
+
+ setCanProceed(!anyErrors);
+ };
return (
<>
@@ -398,6 +556,30 @@ export default function AssignMasters({
<p className="step-description">
Assign master components to hosts you want to run them on.
</p>
+
+ {/* Display general validation messages */}
+ {generalErrorMessages.length > 0 && (
+ <Alert variant="danger" className="mb-3">
+ <strong>Validation Errors:</strong>
+ <ul className="mb-0 mt-2">
+ {generalErrorMessages.map((message, index) => (
+ <li key={index}>{message}</li>
+ ))}
+ </ul>
+ </Alert>
+ )}
+
+ {generalWarningMessages.length > 0 && (
+ <Alert variant="warning" className="mb-3">
+ <strong>Validation Warnings:</strong>
+ <ul className="mb-0 mt-2">
+ {generalWarningMessages.map((message, index) => (
+ <li key={index}>{message}</li>
+ ))}
+ </ul>
+ </Alert>
+ )}
+
{loading ? (
<Spinner />
) : (
@@ -405,57 +587,18 @@ export default function AssignMasters({
<CardBody>
<Row>
<Col md={8}>
- {(() => {
- // Get all components from all hosts
- const allComponentsWithHosts: Array<{component: string, hostname: string}> = [];
-
- // Collect all components with their assigned hosts
- Object.keys(state.hosts).forEach(hostname => {
- state.hosts[hostname].components.forEach(component => {
- allComponentsWithHosts.push({
- component,
- hostname
- });
- });
- });
-
- // Get display names for all components
- const componentDisplayNames: { [key: string]: string } = {};
- allComponentsWithHosts.forEach(({component, hostname}) => {
- const serviceComponent = mastersData.find(m =>
- m.component === component && m.hostName === hostname
- );
- if (serviceComponent) {
- // Use component as key to ensure uniqueness
- const key = `${component}-${hostname}`;
- componentDisplayNames[key] = serviceComponent.display_name || component;
- }
- });
-
- // Sort all components by their display names
- return allComponentsWithHosts
- // Filter out installed components
- .filter(({component, hostname}) => {
- const matchingMasterForInstall = mastersData.find(m =>
- m.component === component && m.hostName === hostname && m.isInstalled
- );
- return !(matchingMasterForInstall && matchingMasterForInstall.isInstalled);
- })
- // Sort by display name
- .sort((a, b) => {
- const keyA = `${a.component}-${a.hostname}`;
- const keyB = `${b.component}-${b.hostname}`;
- const displayNameA = componentDisplayNames[keyA] || a.component;
- const displayNameB = componentDisplayNames[keyB] || b.component;
- return displayNameA.localeCompare(displayNameB);
- })
- // Render each component
- .map(({component, hostname}) => {
- const key = `${component}-${hostname}`;
- const displayName = componentDisplayNames[key] || component;
-
- return (
- <Row key={`${hostname}-${component}`} className="mb-3">
+ {/* Render components using pre-sorted mastersData */}
+ {mastersData
+ // Filter out installed components
+ .filter((master) => !master.isInstalled)
+ // Render each component
+ .map((master) => {
+ const { component, hostName: hostname, display_name, errorMessage, warnMessage } = master;
+ const displayName = display_name || component;
+
+ return (
+ <div key={`${hostname}-${component}`}>
+ <Row className="mb-3">
<Col xs={4} className="text-end mt-3">
<Form.Label className="fw-100">
{displayName}:
@@ -465,7 +608,7 @@ export default function AssignMasters({
<Select
id={`select-${component}`}
value={{ label: hostname, value: hostname }}
- onChange={(selectedOption: any) => {
+ onChange={(selectedOption) => {
if (selectedOption) {
handleComponentChange(
component,
@@ -478,7 +621,7 @@ export default function AssignMasters({
.filter(
(host) =>
host === hostname ||
- !_.get(
+ !get(
state.hosts[host],
"components",
[]
@@ -492,39 +635,96 @@ export default function AssignMasters({
/>
</Col>
<Col xs={4} className="d-flex align-items-center">
- {superMasters.includes(component) && (
- <>
- {Object.keys(state.hosts).some(
- (host) =>
- !state.hosts[host].components.includes(
- component
- )
- ) ? (
- <Button
- variant="success"
- size="sm"
- onClick={() => handleAddComponent(component)}
- >
- <FontAwesomeIcon icon={faPlus} />
- </Button>
- ) : (
- <Button
- variant="success"
- size="sm"
- onClick={() =>
- handleRemoveComponent(component, hostname)
- }
- >
- <FontAwesomeIcon icon={faMinus} />
- </Button>
- )}
- </>
- )}
+ {(() => {
+ // Get the service component to check if it's addable in installer wizard
+ const serviceComponent = servicesAndComponents?.items
+ .flatMap((service: any) => service.components)
+ .find(
+ (comp: any) =>
+ get(comp, "StackServiceComponents.component_name") === component
+ );
+
+ if (!serviceComponent) return null;
+ const stackComponent = get(serviceComponent, "StackServiceComponents");
+ const isAddableInInstallerWizard = isMasterAddableInstallerWizard(stackComponent);
+ if (!isAddableInInstallerWizard) {
+ return null;
+ }
+
+ const componentCount = Object.keys(state.hosts).filter(hostname =>
+ state.hosts[hostname].components.includes(component)
+ ).length;
+
+ const totalHosts = Object.keys(state.hosts).length;
+
+ // Logic based on Ember.js implementation:
+ // 1. Show add control if current count < max allowed masters
+ // 2. Show remove control if current count > 1 (for non-installed components)
+ // 3. Respect the maxToInstall cardinality from stack definition
+ const maxNumMasters = getMaxNumberOfMasters(component);
+ let showPlusButton = false;
+ let showMinusButton = false;
+
+ // Show add button if we haven't reached the maximum
+ if (componentCount < maxNumMasters) {
+ showPlusButton = true;
+ }
+
+ // Show remove button if we have more than 1 instance
+ if (componentCount > 1) {
+ showMinusButton = true;
+ }
+
+ return (
+ <div className="d-flex gap-1">
+ {showPlusButton && (
+ <Button
+ variant="success"
+ size="sm"
+ className="h15"
+ onClick={() => handleAddComponent(component)}
+ title={`Add ${component} (${componentCount}/${totalHosts})`}
+ >
+ <FontAwesomeIcon icon={faPlus} />
+ </Button>
+ )}
+ {showMinusButton && (
+ <Button
+ variant="secondary"
+ size="sm"
+ onClick={() => handleRemoveComponent(component, hostname)}
+ title={`Remove ${component} (${componentCount}/${totalHosts})`}
+ >
+ <FontAwesomeIcon icon={faMinus} />
+ </Button>
+ )}
+ </div>
+ );
+ })()}
</Col>
</Row>
- );
- });
- })()}
+ {/* Display validation messages */}
+ {errorMessage && (
+ <Row className="mb-2">
+ <Col xs={12}>
+ <Alert variant="danger" className="py-2 small">
+ <strong>Error:</strong> {errorMessage}
+ </Alert>
+ </Col>
+ </Row>
+ )}
+ {warnMessage && (
+ <Row className="mb-2">
+ <Col xs={12}>
+ <Alert variant="warning" className="py-2 small">
+ <strong>Warning:</strong> {warnMessage}
+ </Alert>
+ </Col>
+ </Row>
+ )}
+ </div>
+ );
+ })}
</Col>
<Col md={4}>
diff --git a/ambari-web/latest/src/components/AssignMastersAddable.tsx b/ambari-web/latest/src/components/AssignMastersAddable.tsx
new file mode 100644
index 0000000000..9b8a4fc082
--- /dev/null
+++ b/ambari-web/latest/src/components/AssignMastersAddable.tsx
@@ -0,0 +1,1670 @@
+/**
+ * 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.
+ */
+
+import { useContext, useEffect, useRef, useState } from "react";
+import { AppContext } from "../store/context";
+import { HostsApi } from "../api/hostsApi";
+import useHostComponents from "../screens/ClusterWizard/hooks/useHostComponents";
+import Spinner from "./Spinner";
+import {
+ cloneDeep,
+ filter,
+ find,
+ flatten,
+ forEach,
+ get,
+ isEmpty,
+ map,
+ merge,
+ reject,
+ set,
+ some,
+ sortBy,
+ uniq,
+} from "lodash";
+import { groupPropertyValues } from "../Utils/dataUtils";
+import Select from "react-select";
+import AssignMastersApi from "../api/assignMastersApi";
+import { isShownOnAddServiceAssignMasterPage, role } from "../Utils/Utility";
+import { displayOrder } from "../screens/ClusterWizard/constants";
+import {
+ isMasterAddableInstallerWizard,
+ maxToInstall,
+} from "../screens/Hosts/utils";
+import { Badge, Button, Col, Row, Stack, Alert } from "react-bootstrap";
+import { misc } from "../Utils/misc";
+import classNames from "classnames";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faMinus, faPlus } from "@fortawesome/free-solid-svg-icons";
+
+type AssignMastersAddableProps = {
+ services: string[];
+ mastersToCreate?: string[];
+ mastersToShow?: string[];
+ mastersToMove?: string[];
+ mastersToAdd?: string[];
+ showCurrentPrefix?: string[];
+ showAdditionalPrefix?: string[];
+ mastersAddableInHA?: string[];
+ showInstalledMastersFirst?: boolean;
+ dispatch: any;
+ isInstallFlow?: boolean;
+ showJournalNode?: boolean;
+ wizardName?: string;
+ servicesData?: any;
+};
+
+function AssignMastersAddable({
+ services,
+ mastersToCreate = [],
+ mastersToShow = [],
+ mastersToMove = [],
+ mastersToAdd = [],
+ showCurrentPrefix = [],
+ showAdditionalPrefix = [],
+ mastersAddableInHA = [],
+ showInstalledMastersFirst = false,
+ isInstallFlow = false,
+ dispatch,
+ showJournalNode = false,
+ wizardName = "",
+ servicesData = {},
+}: AssignMastersAddableProps) {
+ const [hosts, setHosts] = useState([]);
+ const {
+ clusterName,
+ cluster: { versionNum, stack },
+ } = useContext(AppContext);
+ const [recommendations, setRecommendations] = useState<any>([]);
+ const [recommededHostsForComponents, setRecommendedHostsForComponents] =
+ useState<any>({});
+ const { hostComponents: serviceHostComponents, serviceComponents } =
+ useHostComponents(services, showJournalNode);
+ const [selectedServicesMasters, setSelectedServicesMasters] = useState<any>(
+ []
+ );
+ const [inferredMasterHosts, setInferredMasterHosts] = useState([]);
+ const [inferredSlaveHosts, setInferredSlaveHosts] = useState([]);
+ const [addableMasters, setAddableMasters] = useState<any>([]);
+ const [servicesMasters, setServicesMasters] = useState<any>([]);
+ const [masterHostsMapping, setMasterHostsMapping] = useState<any>([]);
+ const [inferredMasterComponents, setInferredMasterComponents] = useState<any>(
+ []
+ );
+
+ // Function to get warning message for hosts not running master services
+ function getHostWarningMessage() {
+ if (!hosts.length || !inferredMasterHosts.length) return null;
+
+ // Get all hosts in the cluster
+ const allHostNames = hosts.map((host: any) => host.Hosts.host_name);
+
+ // Get hosts that are currently running master services from inferredMasterHosts
+ // These are the hosts that actually have installed master components
+ const currentHostsRunningMasters = uniq(
+ inferredMasterHosts
+ .filter((masterHost: any) => masterHost.isInstalled)
+ .map((masterHost: any) => masterHost.hostName)
+ );
+
+ // Get hosts that will be running master services after the move operation
+ // This includes both current hosts and new target hosts from moves
+ const futureHostsRunningMasters = uniq([
+ ...currentHostsRunningMasters,
+ // Add target hosts from move operations
+ ...addableMasters
+ .filter((master: any) => master.movedHost) // Only moving components
+ .map((master: any) => master.movedHost),
+ // Add any other selected hosts that will have masters
+ ...addableMasters
+ .filter((master: any) => !master.isInstalled) // New components
+ .map((master: any) => master.selectedHost)
+ ]);
+
+ // Find hosts not running master services (considering future state)
+ const hostsNotRunningMasters = allHostNames.filter(
+ (hostName: string) => !futureHostsRunningMasters.includes(hostName)
+ );
+
+ if (hostsNotRunningMasters.length > 0) {
+ return `${hostsNotRunningMasters.length} hosts not running master services`;
+ }
+
+ return null;
+ }
+
+
+ // Function to enhance inferred master hosts with isMoving information
+ function getEnhancedInferredMasterHosts() {
+ return inferredMasterHosts.map((masterHost: any) => {
+ // Find matching component in selectedServicesMasters that has movedHost and isMoving
+ const matchingServiceMaster = selectedServicesMasters.find((serviceMaster: any) =>
+ serviceMaster.component_name === masterHost.component &&
+ serviceMaster.hostName === masterHost.hostName &&
+ serviceMaster.movedHost &&
+ serviceMaster.isMoving === true
+ );
+
+ // Find matching component in addableMasters that has movedHost and isMoving
+ const matchingAddableMaster = addableMasters.find((addableMaster: any) =>
+ addableMaster.component_name === masterHost.component &&
+ addableMaster.hostName === masterHost.hostName &&
+ addableMaster.movedHost &&
+ addableMaster.isMoving === true
+ );
+
+ const matchingComponent = matchingServiceMaster || matchingAddableMaster;
+
+ return {
+ ...masterHost,
+ isMoving: !!matchingComponent,
+ movedHost: matchingComponent ? matchingComponent.movedHost : undefined
+ };
+ });
+ }
+ const initiallyLoaded = useRef(false);
+ const selectedServicesMastersRef = useRef<any>([]);
+ const servicesMastersRef = useRef<any>([]);
+ const [, setComponentToRebalance] = useState("");
+ const [, setLastChangedComponent] = useState("");
+ const [, setRebalanceHostCounter] = useState(0);
+
+ useEffect(() => {
+ const flattenedHostComponents = flatten(
+ map(serviceHostComponents, "host_components")
+ );
+ const flattenedHostRoles = flatten(
+ map(flattenedHostComponents, "HostRoles")
+ );
+ const groupedHosts = groupPropertyValues(flattenedHostRoles, "host_name");
+ const hostComponentsRoles = flatten(
+ map(serviceHostComponents, "host_components")
+ );
+ const hostComponents = flatten(map(hostComponentsRoles, "HostRoles"));
+ const masterComponentHostsToDispatch = flatten([
+ ...getEnhancedInferredMasterHosts(),
+ filter(inferredMasterComponents, ["isInstalled", false]),
+ ]);
+
+ dispatch({
+ masterHostsMapping,
+ hosts: groupedHosts,
+ masterHosts: inferredMasterHosts,
+ enhancedMasterHosts: getEnhancedInferredMasterHosts(),
+ slaveHosts: inferredSlaveHosts,
+ mastersData: masterHostsMapping,
+ masterComponentHosts: masterComponentHostsToDispatch,
+ slaveComponentHosts: null,
+ hostComponents,
+ });
+ }, [masterHostsMapping]);
+
+ function createComponentInstallationObject(
+ fullComponent: any,
+ hostName: string,
+ savedComponent?: any
+ ) {
+ fullComponent.component_name =
+ fullComponent.componentName || fullComponent.component_name;
+ fullComponent.component =
+ fullComponent.componentName || fullComponent.component_name;
+ const componentName: any = fullComponent.component_name,
+ resultingHostName = savedComponent ? savedComponent.hostName : hostName;
+ let componentObj: any = {};
+ componentObj.component_name = componentName;
+ componentObj.component = componentName;
+ componentObj.display_name = role(fullComponent.component_name, false);
+ componentObj.serviceId = fullComponent.service_name;
+ componentObj.isServiceCoHost = !mastersToMove.includes(
+ componentName as never
+ );
+ componentObj.selectedHost = resultingHostName;
+ componentObj.hostName = resultingHostName;
+ componentObj.isInstalled = savedComponent
+ ? savedComponent.isInstalled ||
+ !mastersToCreate.includes(fullComponent.component_name as never)
+ : false;
+ //Check for namenode federation
+ // if (this.get('content.controllerName') === 'reassignMasterController' && componentName === 'NAMENODE' && App.get('hasNameNodeFederation')) {
+ // componentObj.nameSpace = App.HostComponent.find(`${componentName}_${resultingHostName}`).get('haNameSpace');
+ // }
+ return componentObj;
+ }
+
+ function getHostForMaster(master: any, allMasters: any[]) {
+ const masterHostList: any = [];
+ allMasters.forEach((component: any) => {
+ if (component.component_name === master) {
+ masterHostList.push(component.selectedHost);
+ }
+ });
+ const recommendedHostsForMaster =
+ recommededHostsForComponents[master] || [];
+ for (let k = 0; k < recommendedHostsForMaster.length; k++) {
+ if (!masterHostList.includes(recommendedHostsForMaster[k])) {
+ return recommendedHostsForMaster[k];
+ }
+ }
+ let usedHosts = map(
+ filter(allMasters, ["component_name", master]),
+ "selectedHost"
+ );
+ var allHosts = hosts;
+ for (var i = 0; i < allHosts.length; i++) {
+ if (!usedHosts.includes(get(allHosts[i], "Hosts.host_name"))) {
+ return get(allHosts[i], "Hosts.host_name");
+ }
+ }
+
+ return false;
+ }
+
+ function masterHostMapping() {
+ let mapping: any = [],
+ mappingObject,
+ mappedHosts,
+ hostObj: any;
+ const allHosts = [
+ ...map(selectedServicesMasters, "selectedHost"),
+ ...map(selectedServicesMasters, "movedHost"),
+ ];
+ mappedHosts = uniq(allHosts);
+ mappedHosts.forEach(function (item) {
+ hostObj = find(hosts, ["Hosts.host_name", item]);
+ // User may input invalid host name (this is handled in hostname checker). Here we just skip it
+ if (!hostObj) return;
+ let masterServices = filter(
+ selectedServicesMasters,
+ (masterService: any) => {
+ return masterService.movedHost
+ ? masterService.movedHost === item
+ : masterService.selectedHost === item;
+ }
+ );
+ let masterServicesToDisplay: any = [];
+ uniq(map(masterServices, "display_name")).forEach(function (n) {
+ masterServicesToDisplay.push(find(masterServices, ["display_name", n]));
+ });
+ mappingObject = {
+ host_name: item,
+ hostInfo: hostObj.host_info,
+ masterServices: masterServices,
+ masterServicesToDisplay: masterServicesToDisplay,
+ };
+
+ mapping.push(mappingObject);
+ });
+ setMasterHostsMapping(mapping);
+ }
+
+ function sortMasterComponents(masters: any[]) {
+ return [].concat(
+ filter(masters, ["isInstalled", true]) as any,
+ filter(masters, ["isInstalled", false]) as any
+ );
+ }
+
+ function servicesMastersToShow() {
+ let result: any = [];
+ if (!mastersToShow.length) {
+ result = servicesMasters;
+ } else {
+ mastersToShow.forEach(function (master) {
+ result = result.concat(
+ filter(servicesMasters, ["component_name", master])
+ );
+ });
+ }
+ if (showInstalledMastersFirst) {
+ result = sortMasterComponents(result);
+ }
+
+ const installedServices = map(
+ filter(
+ getFlattenedcomponents(),
+ (component: any) => servicesData[component.service_name]?.installed
+ ),
+ "service_name"
+ );
+
+ for (const item of result) {
+ const stackComponent =
+ find(getFlattenedcomponents(), [
+ "component_name",
+ item.component_name,
+ ]) || {};
+ const isMasterWithMultipleInstances = get(
+ stackComponent,
+ "isMasterWithMultipleInstances",
+ false
+ );
+ const showControl =
+ !installedServices.includes(get(stackComponent, "service_name", "")) ||
+ mastersAddableInHA.includes(item.component_name);
+
+ // PRIMARY RULE: In move/reassign scenarios, NEVER show add/remove controls for ANY component
+ // This is the most important rule and overrides all other logic
+ if (mastersToMove.length > 0) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ }
+ // In reassign/move scenarios, installed components should NEVER show add/remove controls
+ // This is a secondary rule for additional safety
+ else if (item.isInstalled && mastersToMove.includes(item.component_name)) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ }
+ // Special handling for NameNode Federation - never show controls for additional NameNodes
+ // This matches the ui version behavior where NAMENODE controls are not shown in federation wizard
+ else if (
+ item.component_name === "NAMENODE" &&
+ showAdditionalPrefix.includes("NAMENODE") &&
+ !item.isInstalled
+ ) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ }
+ // Special handling for NameNode in HA mode - never show controls
+ else if (
+ item.component_name === "NAMENODE" &&
+ mastersToShow.includes("NAMENODE") &&
+ mastersToShow.includes("JOURNALNODE")
+ ) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ }
+ // Special handling for ResourceManager in HA mode - never show controls (matching Ember)
+ else if (
+ item.component_name === "RESOURCEMANAGER" &&
+ mastersToShow.includes("RESOURCEMANAGER") &&
+ mastersToShow.length === 1
+ ) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ } else if (item.isInstalled) {
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ } else if (isMasterWithMultipleInstances && showControl) {
+ // For non-installed multiple instance masters, show add/remove based on count
+ const mastersCount = filter(result, [
+ "component_name",
+ item.component_name,
+ ]).length;
+ set(item, "showRemoveControl", mastersCount > 1);
+ set(
+ item,
+ "showAddControl",
+ mastersCount < getMaxNumberOfMasters(item.component_name)
+ );
+ } else {
+ // Default to no controls for components not in mastersAddableInHA
+ set(item, "showRemoveControl", false);
+ set(item, "showAddControl", false);
+ }
+ set(item, "isMoving", mastersToMove.includes(item.component));
+ }
+
+ // Special handling for JournalNodes - apply the Ember logic exactly
+ // This applies to both NameNode HA wizard and Manage JournalNodes wizard
+ if (mastersToShow.includes("JOURNALNODE") && mastersToMove.length === 0) {
+ const JOURNALNODES_COUNT_MINIMUM = 3;
+ const journalNodes = filter(result, ["component_name", "JOURNALNODE"]);
+ const maxNumMasters = getMaxNumberOfMasters("JOURNALNODE");
+
+ // TLHASD-1164: Implement exact Ember logic from showHideJournalNodesAddRemoveControl
+ // Remove control is only shown when there are MORE than minimum required JournalNodes
+ const showRemoveControl = journalNodes.length > JOURNALNODES_COUNT_MINIMUM;
+ const showAddControl = journalNodes.length < maxNumMasters;
+
+ // Set all JournalNodes to not show add control initially
+ journalNodes.forEach(function (item: any) {
+ set(item, "showAddControl", false);
+ set(item, "showRemoveControl", showRemoveControl);
+ });
+
+ // Only the last JournalNode should show add control (matching Ember: jns.set('lastObject.showAddControl', showAddControl))
+ if (journalNodes.length > 0) {
+ set(
+ journalNodes[journalNodes.length - 1],
+ "showAddControl",
+ showAddControl
+ );
+ }
+ }
+
+ setAddableMasters(result);
+ }
+
+ function getServiceByMaster(master: any) {
+ const allComponents = flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ const mappedService = find(allComponents, ["component_name", master]);
+ return get(mappedService, "service_name", "");
+ }
+
+ function addNewMasters(masterComponents: any) {
+ if (!mastersToAdd.length && isInstallFlow) {
+ const notMasters = ["MYSQL_SERVER", "HIVE_SERVER_INTERACTIVE"];
+ const allMasterComponents = map(
+ filter(flatten(map(serviceComponents, "components")), [
+ "StackServiceComponents.is_master",
+ true,
+ ]),
+ "StackServiceComponents.component_name"
+ );
+ const installedMasters: string[] = map(
+ serviceHostComponents,
+ "ServiceComponentInfo.component_name"
+ );
+ let missingMasters = filter(
+ allMasterComponents,
+ function (masterName: string) {
+ return (
+ !installedMasters.includes(masterName) &&
+ !notMasters.includes(masterName)
+ );
+ }
+ );
+ let updatedMissingMasters = [];
+ if (wizardName === "addService") {
+ for (const master of missingMasters) {
+ const serviceOfMaster = getServiceByMaster(master);
+ if (servicesData[serviceOfMaster]?.installed === false) {
+ updatedMissingMasters.push(master);
+ }
+ }
+ }
+ mastersToAdd =
+ wizardName === "addService" ? updatedMissingMasters : missingMasters;
+ }
+ mastersToAdd.forEach(function (masterName: string) {
+ var toBeAddedNumber = mastersToAdd.filter(function (name) {
+ return name === masterName;
+ }).length,
+ alreadyAddedNumber = reject(
+ filter(masterComponents, ["component_name", masterName]),
+ "isInstalled"
+ ).length;
+ if (toBeAddedNumber > alreadyAddedNumber) {
+ var hostName = getHostForMaster(masterName, masterComponents),
+ serviceName = getServiceByMaster(masterName);
+ masterComponents.push(
+ createComponentInstallationObject(
+ {
+ componentName: masterName,
+ serviceName: serviceName,
+ },
+ hostName
+ )
+ );
+ }
+ });
+ return masterComponents;
+ }
+
+ function sortComponentsByServiceName(components: any) {
+ const componentsOrderForService: any = {
+ HAWQ: ["HAWQMASTER", "HAWQSTANDBY"],
+ };
+ let indexForUnordered = Math.max(displayOrder.length, components.length);
+ return components.sort(function (a: any, b: any) {
+ if (
+ a.serviceId === b.serviceId &&
+ a.serviceId in componentsOrderForService
+ )
+ return (
+ componentsOrderForService[a.serviceId].indexOf(a.component_name) -
+ componentsOrderForService[b.serviceId].indexOf(b.component_name)
+ );
+ var aValue =
+ displayOrder.indexOf(a.serviceId) != -1
+ ? displayOrder.indexOf(a.serviceId)
+ : indexForUnordered;
+ var bValue =
+ displayOrder.indexOf(b.serviceId) != -1
+ ? displayOrder.indexOf(b.serviceId)
+ : indexForUnordered;
+ return aValue - bValue;
+ });
+ }
+
+ async function renderComponents(masterComponents: any) {
+ let result: any = [];
+ //@ts-ignore
+ let serviceComponentId, previousComponentName;
+ addNewMasters(masterComponents);
+ //@ts-ignore
+ const allComponents = flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ masterComponents.forEach(function (item: any) {
+ const allComponents = flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ const masterComponent: any = find(allComponents, [
+ "component_name",
+ item.component_name,
+ ]);
+ setInferredMasterComponents(masterComponents);
+ if (masterComponent) {
+ let componentObj = { ...item };
+ if (item.nameSpace) {
+ componentObj = {
+ allMasters: result,
+ namespace: (function () {
+ // Todo: Logic to get namespace
+ })(),
+ };
+ }
+ let showRemoveControl;
+ if (masterComponent.isMasterWithMultipleInstances) {
+ showRemoveControl =
+ filter(masterComponents, function (masterComponent: any) {
+ masterComponent.component_name === item.component_name &&
+ !masterComponent.isInstalled;
+ }).length > 1;
+ previousComponentName = item.component_name;
+ set(
+ componentObj,
+ "serviceComponentId",
+ filter(result, ["component_name", item.component_name]).length + 1
+ );
+ if (componentObj.isInstalled) {
+ set(componentObj, "showRemoveControl", false);
+ } else {
+ set(componentObj, "showRemoveControl", showRemoveControl);
+ }
+ }
+ set(componentObj, "isHostNameValid", true);
+ // Handle HA state display for ResourceManager (matching Ember logic)
+ if (
+ item.component_name === "RESOURCEMANAGER" &&
+ mastersToShow.includes("RESOURCEMANAGER")
+ ) {
+ const flattenedHostComponents = flatten(
+ map(serviceHostComponents, "host_components")
+ );
+ const flattenedHostRoles = flatten(
+ map(flattenedHostComponents, "HostRoles")
+ );
+ const hostComponent = find(
+ flattenedHostRoles,
+ (hc: any) =>
+ hc.component_name === "RESOURCEMANAGER" &&
+ hc.host_name === item.selectedHost
+ );
+ const haState = get(hostComponent, "ha_state", "");
+
+ if (haState === "ACTIVE") {
+ set(componentObj, "showCurrentPrefix", false);
+ set(componentObj, "showAdditionalPrefix", false);
+ set(componentObj, "display_name", "Active ResourceManager");
+ } else if (
+ haState === "STANDBY" ||
+ (!item.isInstalled && haState !== "ACTIVE")
+ ) {
+ set(componentObj, "showCurrentPrefix", false);
+ set(componentObj, "showAdditionalPrefix", false);
+ set(
+ componentObj,
+ "display_name",
+ item.isInstalled
+ ? "Standby ResourceManager"
+ : "Additional ResourceManager"
+ );
+ } else {
+ // Fallback to original logic
+ set(
+ componentObj,
+ "showCurrentPrefix",
+ showCurrentPrefix.includes(item.component_name as never) &&
+ item.isInstalled
+ );
+ set(
+ componentObj,
+ "showAdditionalPrefix",
+ showAdditionalPrefix.includes(item.component_name as never) &&
+ !item.isInstalled
+ );
+ }
+ } else {
+ set(
+ componentObj,
+ "showCurrentPrefix",
+ showCurrentPrefix.includes(item.component_name as never) &&
+ item.isInstalled
+ );
+ set(
+ componentObj,
+ "showAdditionalPrefix",
+ showAdditionalPrefix.includes(item.component_name as never) &&
+ !item.isInstalled
+ );
+ }
+ if (mastersToMove.includes(item.component_name as never)) {
+ set(componentObj, "isInstalled", false);
+ }
+ result.push(componentObj);
+ }
+ });
+ result = sortComponentsByServiceName(result);
+ setSelectedServicesMasters(result);
+ setServicesMasters(result);
+ setInferredMasterComponents(result);
+ selectedServicesMastersRef.current = result;
+ servicesMastersRef.current = result;
+
+ // JournalNode controls will be handled in servicesMastersToShow()
+ }
+
+ function createComponentInstallationObjects() {
+ let stackMasterComponentsMap: any = {},
+ resultComponents: any = [],
+ multipleComponentHasBeenAdded: any = {},
+ hostGroupsMap: any = {};
+
+ const allComponents = flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ const journalNode: any = find(allComponents, [
+ "component_name",
+ "JOURNALNODE",
+ ]);
+ if (journalNode && showJournalNode) {
+ journalNode.is_master = true;
+ }
+ const masterComponents = map(
+ filter(allComponents, ["is_master", true]),
+ "component_name"
+ );
+ const slaveComponents = map(
+ filter(allComponents, ["component_category", "SLAVE"]),
+ "component_name"
+ );
+ serviceHostComponents.forEach(function (serviceComponent: any) {
+ serviceComponent.host_components.map((hostComponent: any) => {
+ set(
+ hostComponent,
+ "HostRoles.service_name",
+ get(serviceComponent, "ServiceComponentInfo.service_name")
+ );
+ });
+ });
+ const flattenedHostComponents = flatten(
+ map(serviceHostComponents, "host_components")
+ );
+ const flattenedHostRoles = flatten(
+ map(flattenedHostComponents, "HostRoles")
+ );
+ let masterHosts: any = [];
+ let slaveHosts: any = [];
+ for (const masterComponent of masterComponents) {
+ const hostsWithMasterComponent = filter(flattenedHostRoles, [
+ "component_name",
+ masterComponent,
+ ]);
+
+ const transformedMasterHosts = map(
+ hostsWithMasterComponent,
+ function (masterComponentHost: any) {
+ return {
+ component: masterComponent,
+ hostName: masterComponentHost.host_name,
+ isInstalled: true,
+ serviceId: get(masterComponentHost, "service_name", ""),
+ display_name: get(masterComponentHost, "display_name", ""),
+ };
+ }
+ );
+ masterHosts = [...masterHosts, ...transformedMasterHosts];
+ setInferredMasterHosts(masterHosts);
+ }
+ for (const slaveComponent of slaveComponents) {
+ const hostsWithSlaveComponent = filter(flattenedHostRoles, [
+ "component_name",
+ slaveComponent,
+ ]);
+
+ const transformedSlaveHosts = map(
+ hostsWithSlaveComponent,
+ function (slaveComponentHost: any) {
+ return {
+ component: slaveComponent,
+ hostName: slaveComponentHost.host_name,
+ isInstalled: true,
+ serviceId: get(slaveComponentHost, "service_name", ""),
+ display_name: get(slaveComponentHost, "display_name", ""),
+ };
+ }
+ );
+ slaveHosts = [...slaveHosts, ...transformedSlaveHosts];
+ setInferredSlaveHosts(masterHosts);
+ }
+ // const allStackServices = flatten(
+ // map(serviceHostComponents, "host_components.[0].HostRoles")
+ // );
+ const allStackServices = getFlattenedcomponents();
+ const isHFDSInstalled = services.includes("HDFS");
+ const servicesToAdd: any = [];
+ const isHAEnabled =
+ isHFDSInstalled &&
+ !some(allStackServices, ["component_name", "SECONDARY_NAMENODE"]);
+ allStackServices.forEach((component: any) => {
+ // let component=componentHere;
+ // const matchingComponent=find(getFlattenedcomponents(), ["component_name", componentHere?.component_name]);
+ // if(matchingComponent){
+ // component=matchingComponent
+ // }
+ if (component) {
+ const isMasterCreateOnConfig = mastersToCreate.includes(
+ get(component, "component_name", "") as never
+ );
+ //@ts-ignore
+ // const isMasterComponent=getFlattenedcomponents().find((c:any)=>c.component_name===component.component_name)?.is_master ;
+ if (
+ isShownOnAddServiceAssignMasterPage(
+ component.component_name,
+ component.is_master,
+ stack,
+ isHAEnabled
+ ) ||
+ mastersToShow.includes(component.component_name as never) ||
+ isMasterCreateOnConfig
+ ) {
+ stackMasterComponentsMap[get(component, "component_name", "")] =
+ component;
+ }
+ }
+ });
+ recommendations.blueprint_cluster_binding.host_groups.forEach(function (
+ group: any
+ ) {
+ hostGroupsMap[group.name] = group;
+ });
+ recommendations.blueprint.host_groups.forEach(function (host_group: any) {
+ let currentHosts = hostGroupsMap[host_group.name]
+ ? hostGroupsMap[host_group.name].hosts
+ : [];
+ currentHosts.forEach(function (host: any) {
+ host_group.components.forEach(function (component: any) {
+ component.component_name = component.name;
+ let willBeDisplayed = true;
+ let stackMasterComponent =
+ stackMasterComponentsMap[component.component_name];
+ if (stackMasterComponent) {
+ let isMasterCreateOnConfig = mastersToCreate.includes(
+ component.component_name as never
+ );
+ // If service is already installed and not being added as a new service then render on UI only those master components
+ // that have already installed hostComponents.
+ // NOTE: On upgrade there might be a prior installed service with non-installed newly introduced serviceComponent
+ if (
+ !servicesToAdd.includes(
+ get(stackMasterComponent, "serviceName")
+ ) &&
+ !isMasterCreateOnConfig
+ ) {
+ willBeDisplayed = some(masterHosts, [
+ "component",
+ component.component_name,
+ ]);
+ }
+
+ if (willBeDisplayed) {
+ let savedComponents = filter(masterHosts, [
+ "component",
+ component.component_name,
+ ]);
+ const multipleComponents = map(
+ filter(allStackServices, [
+ "isMasterWithMultipleInstances",
+ true,
+ ]),
+ "component_name"
+ );
+ if (
+ multipleComponents.includes(
+ component.component_name as never
+ ) &&
+ savedComponents.length > 0
+ ) {
+ if (!multipleComponentHasBeenAdded[component.component_name]) {
+ multipleComponentHasBeenAdded[component.component_name] =
+ true;
+
+ savedComponents.forEach(function (saved) {
+ resultComponents.push(
+ createComponentInstallationObject(
+ stackMasterComponent,
+ host.fqdn.toLowerCase(),
+ saved
+ )
+ );
+ });
+ }
+ } else {
+ var savedComponent = find(masterHosts, [
+ "component",
+ component.name,
+ ]);
+ resultComponents.push(
+ createComponentInstallationObject(
+ stackMasterComponent,
+ host.fqdn.toLowerCase(),
+ savedComponent
+ )
+ );
+ }
+ }
+ }
+ });
+ });
+ });
+
+ return resultComponents;
+ }
+ function formatRecommendComponents(components: any) {
+ const res: any = [];
+ if (!components) return [];
+ components.forEach(function (component: any) {
+ var componentName = get(component, "component_name", "");
+ if (get(component, "hosts.length")) {
+ get(component, "hosts").forEach(function (hostName: string) {
+ res.push({
+ componentName: componentName,
+ hostName: hostName,
+ });
+ });
+ }
+ });
+ return res;
+ }
+ function getComponentsBlueprint(components: any) {
+ const uniqueHosts = uniq([
+ ...map(components, "hostName"),
+ ...map(hosts, "Hosts.host_name"),
+ ]);
+ const mappedComponents = groupPropertyValues(components, "hostName");
+ const res = {
+ blueprint: { host_groups: [] },
+ blueprint_cluster_binding: { host_groups: [] },
+ };
+ uniqueHosts.forEach(function (host, i) {
+ var group_name = "host-group-" + (i + 1);
+
+ res.blueprint.host_groups.push({
+ name: group_name,
+ components: mappedComponents[host]
+ ? mappedComponents[host].map(function (c: any) {
+ return { name: get(c, "componentName") };
+ })
+ : [],
+ } as never);
+
+ res.blueprint_cluster_binding.host_groups.push({
+ name: group_name,
+ hosts: [{ fqdn: host }],
+ } as never);
+ });
+ return res;
+ }
+ function getRecommendationRequestData(opts: any) {
+ return {
+ recommend: "host_groups",
+ hosts: opts.hosts,
+ services,
+ recommendations:
+ opts.blueprint || getComponentsBlueprint(opts.components),
+ };
+ }
+ async function getRecommendedHosts() {
+ const payloadObject = {
+ services: [],
+ hosts: map(hosts, "Hosts.host_name"),
+ components: [],
+ blueprint: null,
+ };
+ payloadObject.components = formatRecommendComponents(serviceHostComponents);
+ const payload = getRecommendationRequestData(payloadObject);
+ try {
+ const data = await AssignMastersApi.postRecommendations(
+ payload,
+ stack,
+ versionNum
+ );
+ // const recommendationsServer: any = get(
+ // data,
+ // "resources[0].recommendations",
+ // []
+ // );
+ const payloadWithComponentsObject = {
+ services: [],
+ hosts: map(hosts, "Hosts.host_name"),
+ components: [],
+ blueprint: get(data, "resources[0].recommendations.blueprint", []),
+ };
+ const payloadWithComponents = getRecommendationRequestData(
+ payloadWithComponentsObject
+ );
+ const dataWithComponents = await AssignMastersApi.postRecommendations(
+ payloadWithComponents,
+ stack,
+ versionNum
+ );
+ const recommendationsServerWithComponents: any = get(
+ dataWithComponents,
+ "resources[0].recommendations",
+ []
+ );
+ setRecommendations(recommendationsServerWithComponents);
+ const recommendedHostsForComponent: any = {};
+ const hostsForHostGroup: any = {};
+ recommendationsServerWithComponents.blueprint_cluster_binding.host_groups.forEach(
+ function (hostGroup: any) {
+ hostsForHostGroup[hostGroup.name] = map(hostGroup.hosts, "fqdn");
+ }
+ );
+
+ recommendationsServerWithComponents.blueprint.host_groups.forEach(
+ function (hostGroup: any) {
+ var components = map(hostGroup.components, "name");
+ components.forEach(function (componentName: string) {
+ var hostList = recommendedHostsForComponent[componentName] || [];
+ var hostNames = hostsForHostGroup[hostGroup.name] || [];
+ hostList = [...hostList, ...hostNames];
+ recommendedHostsForComponent[componentName] = hostList;
+ });
+ }
+ );
+ setRecommendedHostsForComponents(recommendedHostsForComponent);
+ } catch (err) {
+ console.error("Could not load Recommendations");
+ }
+ }
+ async function fetchHosts() {
+ try {
+ const allHosts = await HostsApi.getHostComponentsDetails(
+ clusterName,
+ "fields=Hosts/cpu_count,Hosts/disk_info,Hosts/total_mem,Hosts/maintenance_state&minimal_response=true"
+ );
+ allHosts?.items.forEach((host: any) => {
+ host.host_info = {
+ cpu_count: host.Hosts.cpu_count,
+ disk_info: host.Hosts.disk_info,
+ total_mem: host.Hosts.total_mem,
+ };
+ });
+ setHosts(allHosts?.items);
+ } catch (err) {
+ console.error("Could Not Load Hosts");
+ }
+ }
+
+ function getFlattenedcomponents() {
+ return flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ }
+
+ function getMaxNumberOfMasters(componentName: string) {
+ const maxByCardinality = maxToInstall(
+ find(getFlattenedcomponents(), ["component_name", componentName])!
+ );
+ const hostsNumber = hosts.length;
+ return Math.min(maxByCardinality, hostsNumber);
+ }
+
+ function updateComponent(componentName: string) {
+ const selectedServicesMastersCopy = cloneDeep(
+ selectedServicesMastersRef.current
+ );
+ const component: any = filter(selectedServicesMastersCopy, [
+ "component_name",
+ componentName,
+ ])?.at(-1);
+ if (!component) {
+ return;
+ }
+
+ // PRIMARY RULE: In move/reassign scenarios, NEVER show add/remove controls
+ if (mastersToMove.length > 0) {
+ set(component, "showAddControl", false);
+ set(component, "showRemoveControl", false);
+ setSelectedServicesMasters(selectedServicesMastersCopy);
+ setServicesMasters(selectedServicesMastersCopy);
+ return;
+ }
+
+ // Get component stack service installation status
+ const stackComponent =
+ find(getFlattenedcomponents(), ["component_name", componentName]) || {};
+ const isServiceInstalled =
+ servicesData[get(stackComponent, "service_name", "")]?.installed;
+
+ // Show control if service is not installed or component is addable in HA
+ let showControl =
+ !isServiceInstalled ||
+ mastersAddableInHA.includes(componentName as never);
+ if (showControl) {
+ let mastersLength = filter(selectedServicesMastersRef.current, [
+ "component_name",
+ componentName,
+ ]).length;
+ if (mastersLength < getMaxNumberOfMasters(componentName)) {
+ set(component, "showAddControl", true);
+ } else {
+ if (component.isInstalled) {
+ set(component, "showRemoveControl", false);
+ } else {
+ set(component, "showRemoveControl", mastersLength != 1);
+ }
+ }
+ }
+ setSelectedServicesMasters(selectedServicesMastersCopy);
+ setServicesMasters(selectedServicesMastersCopy);
+ const addableMastersCopy = cloneDeep(addableMasters);
+ const currentComponentInAddableMasters = filter(
+ addableMastersCopy,
+ function (addableMaster: any) {
+ return addableMaster.component_name === componentName;
+ }
+ );
+ forEach(currentComponentInAddableMasters, function (addableMaster: any) {
+ const matchingComponentFromServiceCopy = find(
+ selectedServicesMastersCopy,
+ function (serviceMaster: any) {
+ return (
+ serviceMaster.component_name === addableMaster.component_name &&
+ serviceMaster.selectedHost === addableMaster.selectedHost
+ );
+ }
+ );
+ if (matchingComponentFromServiceCopy) {
+ merge(addableMaster, matchingComponentFromServiceCopy);
+ }
+ });
+ setAddableMasters(addableMastersCopy);
+ selectedServicesMastersRef.current = selectedServicesMastersCopy;
+ servicesMastersRef.current = selectedServicesMastersCopy;
+ }
+
+ function thereIsNoMasters() {
+ return !filter(selectedServicesMasters, ["isInstalled", false]).length;
+ }
+
+ async function loadStepCallback(components: any) {
+ const allComponents = flatten(
+ map(
+ flatten(map(serviceComponents, "components")),
+ "StackServiceComponents"
+ )
+ );
+ renderComponents(components);
+ const addableMasterInstallerWizard = (function () {
+ return map(
+ filter(allComponents, (component: any) => {
+ return isMasterAddableInstallerWizard(component);
+ }),
+ "component_name"
+ );
+ })();
+ const addableComponents = uniq([
+ ...addableMasterInstallerWizard,
+ ...mastersAddableInHA,
+ ]);
+ addableComponents.forEach((componentName: any) => {
+ updateComponent(componentName);
+ });
+
+ // Special handling for JOURNALNODE in HA mode (matching Ember implementation)
+ // In Ember, JOURNALNODE is handled by the showHideJournalNodesAddRemoveControl function
+ // even when it's not in mastersAddableInHA
+ if (mastersToShow.includes("JOURNALNODE")) {
+ updateComponent("JOURNALNODE");
+ }
+
+ if (thereIsNoMasters() && !mastersToCreate.length) {
+ //Enable Next
+ }
+ }
+
+ useEffect(() => {
+ fetchHosts();
+ }, []);
+
+ useEffect(() => {
+ if (serviceHostComponents.length && hosts.length) {
+ getRecommendedHosts();
+ }
+ }, [serviceHostComponents.length, hosts.length]);
+
+ useEffect(() => {
+ if (servicesMasters.length && !initiallyLoaded.current) {
+ initiallyLoaded.current = true;
+ servicesMastersToShow();
+ }
+ }, [servicesMasters.length]);
+
+ useEffect(() => {
+ if (addableMasters.length) {
+ masterHostMapping();
+ }
+ }, [addableMasters, servicesMasters]);
+
+ useEffect(() => {
+ if (!isEmpty(recommededHostsForComponents) && !isEmpty(recommendations)) {
+ loadStepCallback(createComponentInstallationObjects());
+ }
+ }, [!isEmpty(recommededHostsForComponents), !isEmpty(recommendations)]);
+
+ if (
+ !hosts.length ||
+ !serviceHostComponents.length ||
+ !masterHostsMapping.length
+ ) {
+ return <Spinner />;
+ }
+
+ const hostsOptions = hosts.map((host: any) => ({
+ label: host.Hosts.host_name,
+ value: host.Hosts.host_name,
+ }));
+
+ function changeHostNameForMaster(
+ componentIndex: number,
+ selectedHost: { label: string; value: string },
+ isMoving = false,
+ serviceComponentId?: string
+ ) {
+ const addableMastersCopy = cloneDeep(addableMasters);
+ if (isMoving) {
+ addableMastersCopy[componentIndex].movedHost = selectedHost.value;
+ addableMastersCopy[componentIndex].isMoving=true;
+ } else {
+ addableMastersCopy[componentIndex].selectedHost = selectedHost.value;
+ addableMastersCopy[componentIndex].hostName = selectedHost.value;
+ }
+ //change inferred masters as well
+ const inferrredCopy = cloneDeep(inferredMasterComponents);
+ const inferredComponent = find(inferrredCopy, function (comp) {
+ return (
+ comp.component_name ===
+ addableMastersCopy[componentIndex].component_name &&
+ comp.serviceComponentId === serviceComponentId &&
+ comp.isInstalled === false
+ );
+ });
+ if (isMoving) {
+ inferredComponent.movedHost = selectedHost.value;
+ inferredComponent.isMoving=true;
+ } else {
+ inferredComponent.selectedHost = selectedHost.value;
+ inferredComponent.hostName = selectedHost.value;
+ }
+ setInferredMasterComponents(inferrredCopy);
+ setSelectedServicesMasters(inferrredCopy);
+ setAddableMasters(addableMastersCopy);
+ }
+
+ function getHostOptionsFor(componentIndex: number) {
+ const selectedMaster = addableMasters[componentIndex];
+ const allMasters = cloneDeep(addableMasters);
+ const matchingMasters = allMasters.filter(
+ (master: any) => master.component_name === selectedMaster.component_name
+ );
+
+ // For reassign scenarios, we need to consider both selectedHost and movedHost
+ // to prevent duplicate host assignments
+ const usedHosts: string[] = [];
+ matchingMasters.forEach((master: any, index: number) => {
+ // Skip the current component being edited to allow it to select any host
+ if (index === componentIndex) {
+ return;
+ }
+
+ // For other components of the same type, add their hosts to exclusion list
+ // Add the current selected host
+ if (master.selectedHost) {
+ usedHosts.push(master.selectedHost);
+ }
+ // For moving components, also add the moved host if it exists
+ if (master.movedHost) {
+ usedHosts.push(master.movedHost);
+ }
+ });
+
+ // Remove duplicates
+ const uniqueUsedHosts = uniq(usedHosts);
+
+ // For move scenarios, always allow the original host (selectedHost) to be available
+ // so users can revert back to the current host
+ const availableHosts = reject(hostsOptions, (hostOption: any) =>
+ uniqueUsedHosts.includes(hostOption.value)
+ );
+
+ return availableHosts;
+ }
+ function addComponent(componentName: string) {
+ const maxNumMasters = getMaxNumberOfMasters(componentName);
+ const currentMasters = sortBy(
+ filter(addableMasters, ["component_name", componentName])
+ );
+ let newMaster = null;
+ let masterHosts = null;
+ let suggestedHost = null;
+ let i = 0;
+ let lastMaster = null;
+ if (!currentMasters.length) {
+ return false;
+ }
+ const servicesMastersCopy = cloneDeep(servicesMastersRef.current);
+ const selectedServicesMastersCopy = cloneDeep(
+ selectedServicesMastersRef.current
+ );
+ if (currentMasters.length < maxNumMasters) {
+ set(currentMasters.at(-1), "showAddControl", false);
+ // set(currentMasters.at(-1), "showRemoveControl", true);
+ newMaster = {};
+ lastMaster = currentMasters.at(-1);
+ set(newMaster, "display_name", get(lastMaster, "display_name", ""));
+ set(newMaster, "component_name", get(lastMaster, "component_name", ""));
+ set(newMaster, "component", get(lastMaster, "component_name", ""));
+ set(newMaster, "selectedHost", get(lastMaster, "selectedHost", ""));
+ set(newMaster, "hostName", get(lastMaster, "selectedHost", ""));
+ set(newMaster, "serviceId", get(lastMaster, "serviceId", ""));
+ set(newMaster, "isInstalled", false);
+ set(
+ newMaster,
+ "showAdditionalPrefix",
+ showAdditionalPrefix.includes(componentName)
+ );
+ if (currentMasters.length === maxNumMasters - 1) {
+ set(newMaster, "showAddControl", false);
+ } else {
+ set(newMaster, "showAddControl", true);
+ }
+ set(newMaster, "showRemoveControl", true);
+ masterHosts = uniq(map(currentMasters, "selectedHost"));
+ for (i = 0; i < hosts.length; i++) {
+ if (!masterHosts.includes(get(hosts[i], "Hosts.host_name"))) {
+ suggestedHost = get(hosts[i], "Hosts.host_name");
+ break;
+ }
+ }
+ set(newMaster, "selectedHost", suggestedHost);
+ set(newMaster, "hostName", suggestedHost);
+ if (!get(currentMasters?.at(-1), "serviceComponentId", undefined)) {
+ set(currentMasters?.at(-1), "serviceComponentId", 0);
+ }
+ set(
+ newMaster,
+ "serviceComponentId",
+ get(currentMasters?.at(-1), "serviceComponentId") + 1
+ );
+ // Add to all the necessary arrays
+ servicesMastersCopy.push(newMaster);
+ selectedServicesMastersCopy.push(newMaster);
+ currentMasters.push(newMaster);
+
+ // Update all state arrays with the new component
+ const addableMastersCopy = cloneDeep(addableMasters);
+ const otherMasters = addableMastersCopy.filter(
+ (m: any) => m.component_name !== componentName
+ );
+ const updatedAddableMasters = [...otherMasters, ...currentMasters];
+
+ // Update all refs and state
+ servicesMastersRef.current = servicesMastersCopy;
+ selectedServicesMastersRef.current = selectedServicesMastersCopy;
+
+ setSelectedServicesMasters(selectedServicesMastersCopy);
+ setServicesMasters(servicesMastersCopy);
+ setAddableMasters(updatedAddableMasters);
+ setInferredMasterComponents([...inferredMasterComponents, newMaster]);
... 21577 lines suppressed ...
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@ambari.apache.org
For additional commands, e-mail: commits-help@ambari.apache.org