This is an automated email from the ASF dual-hosted git repository.
jialiang 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 72c4298a9e AMBARI-26395:Step Wizard and Operation Progress components
(#4049)
72c4298a9e is described below
commit 72c4298a9ee96a69695ec45b57474e4c740ed40e
Author: vanshuhassija <[email protected]>
AuthorDate: Tue Aug 26 15:08:22 2025 +0530
AMBARI-26395:Step Wizard and Operation Progress components (#4049)
---
ambari-web/latest/package-lock.json | 356 +++++++++++++--
ambari-web/latest/package.json | 6 +
ambari-web/latest/src/Utils/Utility.ts | 6 +
ambari-web/latest/src/Utils/db.ts | 178 ++++++++
ambari-web/latest/src/api/chooseServicesApi.ts | 48 ++
ambari-web/latest/src/api/clusterApi.ts | 203 +++++++++
ambari-web/latest/src/api/config/axiosConfig.ts | 117 +++++
ambari-web/latest/src/api/configsApi.ts | 270 ++++++++++++
ambari-web/latest/src/api/loginApi.ts | 129 ++++++
ambari-web/latest/src/api/requestApi.ts | 188 ++++++++
ambari-web/latest/src/api/servicesApi.ts | 45 ++
.../latest/src/components/OperationProgress.tsx | 335 ++++++++++++++
.../latest/src/components/StepWizard/index.tsx | 32 +-
ambari-web/latest/src/constants.ts | 28 ++
ambari-web/latest/src/hooks/useDebounce.ts | 44 ++
ambari-web/latest/src/hooks/usePagination.ts | 63 +++
ambari-web/latest/src/hooks/usePolling.ts | 64 +++
ambari-web/latest/src/hooks/usePrevious.ts | 27 ++
ambari-web/latest/src/hooks/useStepWizard.ts | 141 ++++++
ambari-web/latest/src/store/context.tsx | 489 +++++++++++++++++++++
ambari-web/latest/src/store/reducer.ts | 29 ++
ambari-web/latest/src/store/types.ts | 27 ++
ambari-web/latest/src/types/StepWizard.ts | 28 ++
23 files changed, 2813 insertions(+), 40 deletions(-)
diff --git a/ambari-web/latest/package-lock.json
b/ambari-web/latest/package-lock.json
index 9de0dad382..fb55447ecb 100644
--- a/ambari-web/latest/package-lock.json
+++ b/ambari-web/latest/package-lock.json
@@ -10,16 +10,21 @@
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@stomp/stompjs": "^7.1.1",
"@types/lodash": "^4.17.16",
+ "axios": "^1.11.0",
"bootstrap": "^5.3.6",
+ "classnames": "^2.5.1",
+ "dayjs": "^1.11.13",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
+ "js-cookie": "^3.0.5",
"lodash": "^4.17.21",
- "moment-timezone": "^0.5.48",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.0.0",
+ "react-hot-toast": "^2.5.2",
"react-i18next": "^15.5.1",
"react-router-dom": "^7.6.0",
"sass": "^1.88.0",
@@ -27,6 +32,8 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
+ "@types/js-cookie": "^3.0.6",
+ "@types/node": "^24.2.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
@@ -1782,6 +1789,11 @@
"win32"
]
},
+ "node_modules/@stomp/stompjs": {
+ "version": "7.1.1",
+ "resolved":
"https://registry.npmjs.org/@stomp/stompjs/-/stompjs-7.1.1.tgz",
+ "integrity":
"sha512-chcDs6YkAnKp1FqzwhGvh3i7v0+/ytzqWdKYw6XzINEKAzke/iD00dNgFPWSZEqktHOK+C1gSzXhLkLbARIaZw=="
+ },
"node_modules/@swc/helpers": {
"version": "0.5.17",
"resolved":
"https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz",
@@ -1843,6 +1855,12 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/js-cookie": {
+ "version": "3.0.6",
+ "resolved":
"https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
+ "integrity":
"sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ==",
+ "dev": true
+ },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved":
"https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@@ -1856,6 +1874,15 @@
"integrity":
"sha512-RRVJ+J3J+WmyOTqnz3PiBLA501eKwXl2noseKOrNo/6+XEHjTAxO4xHvxQB6QuNm+s4WRbn6rSiap8+EA+ykFQ==",
"license": "MIT"
},
+ "node_modules/@types/node": {
+ "version": "24.2.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
+ "integrity":
"sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
+ "dev": true,
+ "dependencies": {
+ "undici-types": "~7.10.0"
+ }
+ },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved":
"https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -2208,6 +2235,21 @@
"dev": true,
"license": "Python-2.0"
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity":
"sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
+ "node_modules/axios": {
+ "version": "1.11.0",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz",
+ "integrity":
"sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.4",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved":
"https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -2291,6 +2333,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved":
"https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity":
"sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2357,8 +2411,7 @@
"node_modules/classnames": {
"version": "2.5.1",
"resolved":
"https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz",
- "integrity":
"sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
- "license": "MIT"
+ "integrity":
"sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="
},
"node_modules/color-convert": {
"version": "2.0.1",
@@ -2380,6 +2433,17 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved":
"https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity":
"sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/concat-map": {
"version": "0.0.1",
"resolved":
"https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@@ -2424,6 +2488,11 @@
"integrity":
"sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity":
"sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
+ },
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
@@ -2449,6 +2518,14 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved":
"https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity":
"sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/dequal": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2481,6 +2558,19 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved":
"https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity":
"sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/electron-to-chromium": {
"version": "1.5.155",
"resolved":
"https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
@@ -2488,6 +2578,47 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved":
"https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity":
"sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity":
"sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved":
"https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity":
"sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved":
"https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity":
"sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.25.4",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz",
@@ -2855,6 +2986,40 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved":
"https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity":
"sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
+ "integrity":
"sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -2870,6 +3035,14 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved":
"https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity":
"sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved":
"https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -2880,6 +3053,41 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved":
"https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity":
"sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity":
"sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved":
"https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -2906,6 +3114,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/goober": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz",
+ "integrity":
"sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==",
+ "peerDependencies": {
+ "csstype": "^3.0.10"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity":
"sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/graphemer": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
@@ -2923,6 +3150,42 @@
"node": ">=8"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved":
"https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity":
"sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved":
"https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity":
"sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity":
"sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/html-parse-stringify": {
"version": "3.0.1",
"resolved":
"https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
@@ -3064,6 +3327,14 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/js-cookie": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+ "integrity":
"sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -3173,8 +3444,7 @@
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
- "integrity":
"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
- "license": "MIT"
+ "integrity":
"sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash.merge": {
"version": "4.6.2",
@@ -3205,6 +3475,14 @@
"yallist": "^3.0.2"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved":
"https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity":
"sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/merge2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
@@ -3229,6 +3507,25 @@
"node": ">=8.6"
}
},
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity":
"sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved":
"https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity":
"sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
@@ -3242,27 +3539,6 @@
"node": "*"
}
},
- "node_modules/moment": {
- "version": "2.30.1",
- "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
- "integrity":
"sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
- "license": "MIT",
- "engines": {
- "node": "*"
- }
- },
- "node_modules/moment-timezone": {
- "version": "0.5.48",
- "resolved":
"https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
- "integrity":
"sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
- "license": "MIT",
- "dependencies": {
- "moment": "^2.29.4"
- },
- "engines": {
- "node": "*"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -3485,6 +3761,11 @@
"react": ">=0.14.0"
}
},
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved":
"https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity":
"sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
@@ -3560,7 +3841,6 @@
"version": "1.11.6",
"resolved":
"https://registry.npmjs.org/react-bootstrap-icons/-/react-bootstrap-icons-1.11.6.tgz",
"integrity":
"sha512-ycXiyeSyzbS1C4+MlPTYe0riB+UlZ7LV7YZQYqlERV2cxDiKtntI0huHmP/3VVvzPt4tGxqK0K+Y6g7We3U6tQ==",
- "license": "MIT",
"dependencies": {
"prop-types": "^15.7.2"
},
@@ -3580,6 +3860,22 @@
"react": "^19.1.0"
}
},
+ "node_modules/react-hot-toast": {
+ "version": "2.5.2",
+ "resolved":
"https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz",
+ "integrity":
"sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==",
+ "dependencies": {
+ "csstype": "^3.1.3",
+ "goober": "^2.1.16"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "react": ">=16",
+ "react-dom": ">=16"
+ }
+ },
"node_modules/react-i18next": {
"version": "15.5.1",
"resolved":
"https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz",
@@ -4028,6 +4324,12 @@
"react": ">=15.0.0"
}
},
+ "node_modules/undici-types": {
+ "version": "7.10.0",
+ "resolved":
"https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
+ "integrity":
"sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
+ "dev": true
+ },
"node_modules/update-browserslist-db": {
"version": "1.1.3",
"resolved":
"https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
diff --git a/ambari-web/latest/package.json b/ambari-web/latest/package.json
index 77b6d90cf3..5d8c569c5b 100755
--- a/ambari-web/latest/package.json
+++ b/ambari-web/latest/package.json
@@ -12,19 +12,23 @@
"dependencies": {
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
+ "@stomp/stompjs": "^7.1.1",
"@types/lodash": "^4.17.16",
+ "axios": "^1.11.0",
"bootstrap": "^5.3.6",
"classnames": "^2.5.1",
"dayjs": "^1.11.13",
"html-react-parser": "^5.2.6",
"i18next": "^25.1.2",
"i18next-browser-languagedetector": "^8.1.0",
+ "js-cookie": "^3.0.5",
"isomorphic-dompurify": "^2.26.0",
"lodash": "^4.17.21",
"react": "^19.0.0",
"react-bootstrap": "^2.10.10",
"react-bootstrap-icons": "^1.11.6",
"react-dom": "^19.0.0",
+ "react-hot-toast": "^2.5.2",
"react-i18next": "^15.5.1",
"react-router-dom": "^7.6.0",
"sass": "^1.88.0",
@@ -32,6 +36,8 @@
},
"devDependencies": {
"@eslint/js": "^9.21.0",
+ "@types/js-cookie": "^3.0.6",
+ "@types/node": "^24.2.1",
"@types/react": "^19.0.10",
"@types/react-dom": "^19.0.4",
"@vitejs/plugin-react": "^4.3.4",
diff --git a/ambari-web/latest/src/Utils/Utility.ts
b/ambari-web/latest/src/Utils/Utility.ts
index 588779a422..1fba1250ee 100644
--- a/ambari-web/latest/src/Utils/Utility.ts
+++ b/ambari-web/latest/src/Utils/Utility.ts
@@ -459,6 +459,12 @@ export function
isShownOnAddServiceAssignMasterPage(component:string,isMaster:bo
return isVisible;
}
+
+export const redirectToLogin = () => {
+ window.location.href = "#/login";
+ window.location.reload();
+}
+
export const translate = (messageKey: string) => {
return parse(DOMPurify.sanitize(t(messageKey), { USE_PROFILES: { html: true
} } ));
}
diff --git a/ambari-web/latest/src/Utils/db.ts
b/ambari-web/latest/src/Utils/db.ts
new file mode 100644
index 0000000000..e2352c6189
--- /dev/null
+++ b/ambari-web/latest/src/Utils/db.ts
@@ -0,0 +1,178 @@
+/**
+ * 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 { Utility } from './Utility';
+
+interface DbData {
+ app: {
+ loginName: string;
+ authenticated: boolean;
+ user?: any;
+ auth?: any;
+ configs: any[];
+ tags: any[];
+ tables: {
+ filterConditions: Record<string, any>;
+ displayLength: Record<string, any>;
+ startIndex: Record<string, any>;
+ sortingConditions: Record<string, any>;
+ selectedItems: Record<string, any>;
+ };
+ };
+ Installer: Record<string, any>;
+ AddHost: Record<string, any>;
+ AddService: Record<string, any>;
+ WidgetWizard: Record<string, any>;
+ KerberosWizard: Record<string, any>;
+ ReassignMaster: Record<string, any>;
+ AddSecurity: Record<string, any>;
+ HighAvailabilityWizard: Record<string, any>;
+ RollbackHighAvailabilityWizard: Record<string, any>;
+ tmp: Record<string, any>;
+ [key: string]: any;
+ }
+
+ const InitialData: DbData = {
+ app: {
+ loginName: '',
+ authenticated: false,
+ configs: [],
+ tags: [],
+ tables: {
+ filterConditions: {},
+ displayLength: {},
+ startIndex: {},
+ sortingConditions: {},
+ selectedItems: {}
+ }
+ },
+ Installer: {},
+ AddHost: {},
+ AddService: {},
+ WidgetWizard: {},
+ KerberosWizard: {},
+ ReassignMaster: {},
+ AddSecurity: {},
+ HighAvailabilityWizard: {},
+ RollbackHighAvailabilityWizard: {},
+ tmp: {}
+ };
+
+ class Database {
+ private data: DbData;
+
+ constructor() {
+ this.data = this.getDb() || InitialData;
+ }
+
+ private checkNamespace(namespace: string): boolean {
+ if (!namespace) return false;
+ if (!this.data[namespace]) {
+ this.data[namespace] = {};
+ }
+ return true;
+ }
+
+ getDb(): DbData | null {
+ try {
+ const stored = localStorage.getItem('ambari');
+ return stored ? JSON.parse(stored) : null;
+ } catch (e) {
+ console.error('Error reading from localStorage:', e);
+ return null;
+ }
+ }
+
+ private setDb(data: DbData): void {
+ try {
+ localStorage.setItem('ambari', JSON.stringify(data));
+ } catch (e) {
+ console.error('Error writing to localStorage:', e);
+ }
+ }
+
+ getItem(key: string): string | null {
+ const value = localStorage.getItem(key);
+ if (value === null) return null;
+ return Utility.decryptData(value);
+}
+ setItem(key: string, value: string): void {
+ try {
+ // Always encrypt before storing to ensure consistency
+ const encrypted = Utility.encryptData(value);
+ localStorage.setItem(key, encrypted);
+ } catch (e) {
+ console.error(`Error encrypting data for key ${key}:`, e);
+ // Fallback to storing unencrypted data
+ localStorage.setItem(key, value);
+ console.warn(`Stored unencrypted data for key ${key} due to encryption
failure`);
+ }
+ }
+
+ cleanUp(): void {
+ this.data = InitialData;
+ this.setDb(this.data);
+ }
+
+ createNameSpace(namespace: string): void {
+ if (!this.data[namespace]) {
+ this.data[namespace] = {};
+ this.setDb(this.data);
+ }
+ }
+
+ // Core get/set methods
+ get(namespace: string, key: string): any {
+ const data = this.getDb();
+ if (!data || !this.checkNamespace(namespace)) return null;
+ return key.includes('user-pref') ?
+ data[namespace][key] :
+ this.getNestedValue(data[namespace], key);
+ }
+
+ set(namespace: string, key: string, value: any): void {
+ const data = this.getDb() || InitialData;
+ if (!this.checkNamespace(namespace)) return;
+ if (key.includes('user-pref')) {
+ data[namespace][key] = value;
+ } else {
+ this.setNestedValue(data[namespace], key, value);
+ }
+ this.setDb(data);
+ }
+
+ private getNestedValue(obj: any, path: string): any {
+ return path.split('.').reduce((acc, part) => acc && acc[part], obj);
+ }
+
+ private setNestedValue(obj: any, path: string, value: any): void {
+ const parts = path.split('.');
+ const last = parts.pop()!;
+ const target = parts.reduce((acc, part) => {
+ if (!acc[part]) acc[part] = {};
+ return acc[part];
+ }, obj);
+ target[last] = value;
+ }
+
+ getInitialData(): string {
+ return JSON.parse(JSON.stringify(InitialData));
+ }
+
+ }
+
+ export const db = new Database()
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/chooseServicesApi.ts
b/ambari-web/latest/src/api/chooseServicesApi.ts
new file mode 100644
index 0000000000..ce7b9c9a55
--- /dev/null
+++ b/ambari-web/latest/src/api/chooseServicesApi.ts
@@ -0,0 +1,48 @@
+/**
+ * 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 ChooseServicesApi = {
+ serviceDetails: async function (serviceName: string, clusterName: string) {
+ const url = `/clusters/${clusterName}/services/${serviceName}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ servicesList: async function (clusterName: string) {
+ const url = `/clusters/${clusterName}/services`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+
+ getServices: async (stack: string, version: string, services?: string[]) => {
+ const url =
`stacks/${stack}/versions/${version}/services?fields=StackServices/*,components/*,components/dependencies/Dependencies/scope,components/dependencies/Dependencies/service_name,artifacts/Artifacts/artifact_name${
+ services ? `&StackServices/service_name.in(${services.join(",")})` : ""
+ }`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+};
diff --git a/ambari-web/latest/src/api/clusterApi.ts
b/ambari-web/latest/src/api/clusterApi.ts
new file mode 100644
index 0000000000..2d7c1cb549
--- /dev/null
+++ b/ambari-web/latest/src/api/clusterApi.ts
@@ -0,0 +1,203 @@
+/**
+ * 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, supressErrorAmbariApi } from "./config/axiosConfig";
+
+const ClusterApi = {
+ loadAmbariProperties: async (fields = "") => {
+ const url = `/services/AMBARI/components/AMBARI_SERVER${fields}`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ fetchClusterDetails: async function (updateClusterPayloadData: object,
clusterName: string) {
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: updateClusterPayloadData
+ });
+ return response;
+ },
+ getDesiredClusterConfigs: async function (clusterName:
string,fields=`Clusters/desired_configs`) {
+ const url = `/clusters/${clusterName}?fields=${fields}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ updateCluster:async function(clusterName:string,data:any){
+ const url=`/clusters/${clusterName}`
+ const response=await ambariApi.request({
+ url:url,
+ method:"PUT",
+ data
+ })
+ return response.data
+ },
+ getCluster: async function (clusterName:string) {
+ const url =
`/clusters/${clusterName}?fields=Clusters/desired_configs/cluster-env`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getAllClusters: async function () {
+ const url = `/clusters`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getRequests: async function (clusterName: string,pageSize:number) {
+ const url =
`/clusters/${clusterName}/requests?to=end&page_size=${pageSize}&fields=Requests/end_time,Requests/id,Requests/progress_percent,Requests/request_context,Requests/request_status,Requests/start_time,Requests/cluster_name,Requests/user_name&minimal_response=true`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ deleteCluster: async function (clusterName:string) {
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "DELETE",
+ })
+ return response.data
+ },
+ 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({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ updateRequest:async function
(clusterName:string,requestId:number|string,payload:any){
+ const url=`/clusters/${clusterName}/requests/${requestId}`
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data:payload
+ });
+ return response.data;
+ },
+ getClusterRequestTaskLogs:async
function(clusterName:string,requestId:number|string,taskId:number|string){
+ const url=`/clusters/${clusterName}/requests/${requestId}/tasks/${taskId}`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getClusterName : async function () {
+ const url = `/clusters?fields=Clusters`;
+ const response = await ambariApi.request({
+ url: url,
+ 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 () {
+ const url=
`/clusters?fields=Clusters/provisioning_state,Clusters/security_type,Clusters/version,Clusters/cluster_id`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getPersistData: async function (key:any) {
+ const url = `/persist/${key}`;
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ postPersistData: async function (data:any) {
+ const url = `/persist`;
+ const response = await supressErrorAmbariApi.request({
+ url: url,
+ method: "POST",
+ data
+ });
+ return response.data;
+ },
+ getHosts: async function (clusterName:string) {
+ const url = `/clusters/${clusterName}/hosts?minimal_response=true`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getUpgradeState: async function (clusterName: string) {
+ const url = `/clusters/${clusterName}/upgrades?fields=Upgrade`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET"
+ })
+ return response.data;
+ },
+ noopPolling: async function () {
+ // const timestamp = new Date().getTime();
+ const url = `/clusters`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response;
+ },
+ getUserTimeout: async () => {
+ return ambariApi.request({
+ url: '/services/AMBARI/components/AMBARI_SERVER',
+ method: 'GET',
+ params: {
+ fields:
'RootServiceComponents/properties/user.inactivity.timeout.default',
+ _: Date.now() // Cache buster
+ }
+ });
+ },
+
+ createClusterCustomAction: async function (clusterName: string, payload:
any) {
+ const url = `/clusters/${clusterName}/requests`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload
+ });
+ return response.data;
+ },
+ createCustomAction: async function ( payload: any) {
+ const url = `/requests`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload,
+ });
+ return response.data;
+ }
+ }
+
+export default ClusterApi;
\ 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
new file mode 100644
index 0000000000..af5cbffc43
--- /dev/null
+++ b/ambari-web/latest/src/api/config/axiosConfig.ts
@@ -0,0 +1,117 @@
+/**
+ * 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 axios from "axios";
+import { toast } from "react-hot-toast";
+import { get } from "lodash";
+
+const config = {
+ development: {
+ VITE_API_PROXY_TARGET: "<PROXY URL HERE>",
+ //VITE_TOKEN: "<PROXY TOKEN HERE>",
+ },
+ production: {
+ VITE_API_PROXY_TARGET: "",
+ },
+};
+
+let currentEnv = "development"; // however you determine the current
environment
+
+if (process.env.NODE_ENV) {
+ currentEnv = process.env.NODE_ENV;
+}
+
+const createAxiosInstance = (baseURL: string, headers = {}) => {
+ if (currentEnv != undefined) {
+ if (currentEnv == "development") {
+ headers = {
+ "Content-Type": "application/json",
+ // Authorization: `Basic
${btoa(localStorage.getItem("proxy_token")||"")}`,
+ ...headers,
+ };
+ } else {
+ headers = {
+ "Content-Type": "application/json",
+ // Authorization: `Basic
${btoa(localStorage.getItem("proxy_token")||"")}`,
+ ...headers,
+ };
+ }
+ } else {
+ console.error(`No configuration found for target: ${currentEnv}`);
+ }
+
+ const instance = axios.create({
+ baseURL,
+ withCredentials: true,
+ headers: headers,
+ });
+
+ instance.interceptors.response.use(undefined, (error) => {
+ const responseMessage = get(error, "response.data.message", undefined);
+ // Check for 403 Forbidden status
+ if (error.response && error.response.status === 403) {
+ // Redirect to login page
+ window.location.href = "/#/login";
+ return Promise.reject(error);
+ }
+ if (responseMessage && error.response.status !== 400) {
+ toast.error(responseMessage);
+ }
+ return Promise.reject(error);
+ });
+
+ return instance;
+};
+
+const createSupressErrorAxiosInstance = (baseURL: string, headers = {}) => {
+ if (currentEnv != undefined) {
+ headers = {
+ "Content-Type": "application/json",
+ ...headers,
+ };
+ }
+
+ const instance = axios.create({
+ baseURL,
+ withCredentials: true,
+ headers: headers,
+ });
+
+ instance.interceptors.response.use(undefined, (error) => {
+ // Check for 403 Forbidden status
+ if (error.response && error.response.status === 403) {
+ // Redirect to login page
+ window.location.href = "/#/login";
+ return Promise.reject(error);
+ }
+ return Promise.reject(error);
+ });
+
+ return instance;
+};
+
+let endpoint = "";
+if (config.development.VITE_API_PROXY_TARGET != undefined) {
+ if (config.development.VITE_API_PROXY_TARGET != "") {
+ endpoint = "/api/v1";
+ } else {
+ endpoint = `${config.production.VITE_API_PROXY_TARGET}/api/v1`;
+ }
+}
+
+export const ambariApi = createAxiosInstance(endpoint);
+export const supressErrorAmbariApi = createSupressErrorAxiosInstance(endpoint);
diff --git a/ambari-web/latest/src/api/configsApi.ts
b/ambari-web/latest/src/api/configsApi.ts
new file mode 100644
index 0000000000..a320da67bb
--- /dev/null
+++ b/ambari-web/latest/src/api/configsApi.ts
@@ -0,0 +1,270 @@
+/**
+ * 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 { AxiosResponse } from "axios";
+import { ambariApi } from "./config/axiosConfig";
+
+const ConfigsApi = {
+ getServiceConfigurations: async function (
+ stack: string,
+ verison: string,
+ services: string
+ ) {
+ const url =
`stacks/${stack}/versions/${verison}/services?StackServices/service_name.in(${services})&fields=configurations/*,configurations/dependencies/*,StackServices/config_types/*`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getConfigProperties: async function (
+ stack: string,
+ verison: string,
+ services: string
+ ) {
+ const url =
`stacks/${stack}/versions/${verison}/services?StackServices/service_name.in(${services})&fields=configurations/*,configurations/dependencies/*,StackServices/display_name,StackServices/config_types/*&_=1728974996201`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+
+ validateConfigProperties: async function (
+ stack: string,
+ verison: string,
+ payload: any
+ ) {
+ const url = `stacks/${stack}/versions/${verison}/validations`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload,
+ });
+ return response.data;
+ },
+
+ getConfigValues: async function (clusterName: string, services: string) {
+ const url =
`clusters/${clusterName}/configurations/service_config_versions?service_name.in(${services})&is_current=true&fields=*`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+
+ getVersionConfigValues: async function (
+ clusterName: string,
+ services: string,
+ version: string
+ ) {
+ const url =
`clusters/${clusterName}/configurations/service_config_versions?(service_name=${services}&service_config_version.in(${version}))`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+
+ getTheme: async (
+ stackName: string,
+ stackVersion: string,
+ services: string
+ ) => {
+ const url =
`/stacks/${stackName}/versions/${stackVersion}/services?StackServices/service_name.in(${services})&themes/ThemeInfo/default=true&fields=themes/*`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ loadConfigTags: async (clusterName: string) => {
+ const url = `/clusters/${clusterName}?fields=Clusters/desired_configs`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ reassignLoadConfigs: async (clusterName: string, urlParams: string) => {
+ const url = `/clusters/${clusterName}/configurations?${urlParams}`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ updateConfigTags: async function (clusterName: string) {
+ const url = `/clusters/${clusterName}?fields=Clusters/desired_configs`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ const tags = [];
+ for (let site in data.Clusters.desired_configs) {
+ tags.push({
+ siteName: site,
+ tagName: data.Clusters.desired_configs[site].tag,
+ });
+ }
+ return tags;
+ },
+ getConfigsByTags: async function (clusterName: string, params: string) {
+ const url = `/clusters/${clusterName}/configurations?${params}`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return data;
+ },
+ updateServiceConfigurations: async function (clusterName: string, data: any)
{
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url,
+ method: "PUT",
+ data: {
+ Clusters: {
+ desired_config: data.desired_config,
+ },
+ },
+ });
+ return response.data;
+ },
+ updateServiceMultiConfigurations: async function (
+ clusterName: string,
+ data: any
+ ) {
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url,
+ method: "PUT",
+ data: data.configs,
+ });
+ return response.data;
+ },
+
+ loadConfigsFromStack: async function (
+ stack: string,
+ version: string,
+ serviceNames: string[]
+ ) {
+ const url = serviceNames.length
+ ?
`/stacks/${stack}/versions/${version}/services?StackServices/service_name.in(${serviceNames})&fields=configurations/*,configurations/dependencies/*,StackServices/config_types/*`
+ :
`/stacks/${stack}/versions/${version}/services?fields=configurations/*,StackServices/config_types/*`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return data;
+ },
+ getConfigGroups: async function (clusterName: string, serviceName: string) {
+ const url =
`clusters/${clusterName}/config_groups?ConfigGroup/tag.in(${serviceName})&fields=*`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return data;
+ },
+ createNewConfigGroup: async function (clusterName: string, payload: any) {
+ const url = `clusters/${clusterName}/config_groups`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "POST",
+ data: payload,
+ });
+ return data;
+ },
+ saveConfigs: async function (clusterName: string, payload: any) {
+ const url = `clusters/${clusterName}`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "PUT",
+ data: payload,
+ });
+ return data;
+ },
+ updateConfigGroupProperties: async function (
+ clusterName: string,
+ groupId: string,
+ payload: any
+ ) {
+ const url = `clusters/${clusterName}/config_groups/${groupId}`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "PUT",
+ data: payload,
+ });
+ return data;
+ },
+ getDesiredConfigsInfo: async (
+ clusterName: string
+ ): Promise<AxiosResponse> => {
+ const url =
`/clusters/${clusterName}?fields=Clusters/desired_configs&_=${Date.now()}\``;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response;
+ },
+ getEnabledConfigsForRangerPlugins: async (
+ hdfsTagVersion: string,
+ hbaseTagVersion: string,
+ hiveTagVersion: string,
+ yarnTagVersion: string,
+ clusterName: string
+ ): Promise<AxiosResponse> => {
+ const url =
+
`/clusters/${clusterName}/configurations?(type=ranger-hdfs-plugin-properties&tag=${hdfsTagVersion})|`
+
+ `(type=ranger-yarn-plugin-properties&tag=${yarnTagVersion})|` +
+ `(type=hive-env&tag=${hiveTagVersion})|` +
+
`(type=ranger-hbase-plugin-properties&tag=${hbaseTagVersion})&_=${Date.now()}`;
+
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response;
+ },
+ getRecommendations: async function (
+ stack: string,
+ version: string,
+ payload: any
+ ) {
+ const url = `stacks/${stack}/versions/${version}/recommendations`;
+ const { data } = await ambariApi.request({
+ url,
+ method: "POST",
+ data: payload,
+ });
+ return data;
+ },
+ getServiceConfigVersions: async function (
+ clusterName: string,
+ serviceName: string
+ ): Promise<AxiosResponse> {
+ const serviceConfigFields =
+
"service_config_version,user,hosts,group_id,group_name,is_current,createtime,service_name,service_config_version_note,stack_id,is_cluster_compatible";
+ const url =
`clusters/${clusterName}/configurations/service_config_versions?service_name=${serviceName}&fields=${serviceConfigFields}&sortBy=service_config_version.desc&minimal_response=true&_=${Date.now()}`;
+ const response = await ambariApi.request({
+ url,
+ method: "GET",
+ });
+ return response.data.items;
+ },
+};
+
+export default ConfigsApi;
diff --git a/ambari-web/latest/src/api/loginApi.ts
b/ambari-web/latest/src/api/loginApi.ts
new file mode 100644
index 0000000000..97cbc5e66c
--- /dev/null
+++ b/ambari-web/latest/src/api/loginApi.ts
@@ -0,0 +1,129 @@
+/**
+ * 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.ts";
+import {misc} from "../Utils/misc.ts";
+import { db } from "../Utils/db";
+import Cookies from 'js-cookie';
+
+interface LoginParams {
+ usr: string;
+ loginName: string;
+}
+interface dataLoginType {
+ Users: any;
+ loginData: any;
+}
+interface LoginDataParamsType {
+ loginName: string;
+ loginData: any;
+}
+const LoginApi = {
+
+ authenticate: async function (username: string, password: string) {
+ const hashForUserNamePassword = misc.utf8ToB64(username + ":" + password);
+ const response = await ambariApi.request({
+ url: "/auth",
+ method: "POST",
+ headers: {
+ 'Content-Type': 'text/plain',
+ Authorization: "Basic " + hashForUserNamePassword,
+ },
+ });
+ return response;
+ },
+ handleSuccessfulLogin: async function (params: LoginParams) {
+ const url =
`/users/${encodeURIComponent(params.loginName)}?fields=*,privileges/PrivilegeInfo/cluster_name,privileges/PrivilegeInfo/permission_name`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ data: {
+ usr: params.usr,
+ loginName: params.loginName
+ }
+ });
+ return response;
+ },
+ loadAuthorizationsCallback: async function(params: LoginParams) {
+ const url =
`/users/${encodeURIComponent(params.loginName)}/authorizations?fields=*`
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ data: {userName: params.loginName},
+ });
+ return response;
+ },
+ afterLoginSuccessCallback: async function(data: dataLoginType) {
+ const response = await ambariApi.request({
+ url: "/settings/motd",
+ method: "GET",
+ data: {
+ loginName: data.Users.user_name,
+ loginData: data
+ }
+ });
+ return response.data;
+ },
+ setClusterDataCallback: async function(params: LoginDataParamsType) {
+ const requestData = {
+ loginName: params.loginName,
+ loginData: params.loginData
+ };
+ const response = await ambariApi.request({
+ url:
"/clusters?fields=Clusters/provisioning_state,Clusters/security_type,Clusters/version,Clusters/cluster_id",
+ data: requestData,
+ })
+ return response;
+ },
+ logout: async () => {
+ Cookies.remove('AMBARISESSIONID', { path: '/' ,domain: 'localhost',
secure: true })
+ console.log('After logout cookies:', document.cookie);
+ const timestamp = Date.now();
+ try {
+ const response = await ambariApi.request({
+ url: `/logout?_=${timestamp}`,
+ method: "GET",
+ headers: {
+ 'X-Requested-By': 'X-Requested-By'
+ },
+ withCredentials: true,
+ auth: {
+ username: timestamp.toString(),
+ password: timestamp.toString()
+ }
+ });
+
+ // Only clean up and redirect on successful logout
+ if (response.status === 200) {
+ db.cleanUp();
+ // Remove AMBARI-SESSION-ID cookie
+ // // Clear auth header
+ // delete ambariApi.defaults.headers.common['Authorization'];
+ // // Clear JWT cookie
+ // document.cookie = "jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC;
path=/;";
+ window.location.replace('/#/login');
+ }
+ return response;
+ } catch (error) {
+ console.error('Logout failed:', error);
+ throw error; // Let the UI handle the error
+ }
+ },
+
+}
+export default LoginApi;
+
diff --git a/ambari-web/latest/src/api/requestApi.ts
b/ambari-web/latest/src/api/requestApi.ts
new file mode 100644
index 0000000000..536d4cc948
--- /dev/null
+++ b/ambari-web/latest/src/api/requestApi.ts
@@ -0,0 +1,188 @@
+/**
+ * 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";
+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({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getRunningRequests: async function (clusterName: string) {
+ const url =
`/clusters/${clusterName}/requests/?page_size=20&fields=Requests/request_status&Requests/request_status.in(IN_PROGRESS)`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ stopServices: async function (clusterName:string, stopServicesdata:
object) {
+ const url = `clusters/${clusterName}/services`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: stopServicesdata,
+ headers:{
+ "Content-Type":"text/plain"
+ }
+ });
+ return response.data;
+ },
+ startServices: async function (clusterName: string, payload: any, params:
string, method="PUT"){
+ const url = `/clusters/${clusterName}/services?params/${params}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: method,
+ data: payload,
+ headers:{
+ "Content-Type":"text/plain"
+ }
+ })
+ return response.data;
+ },
+ performRequests: async function (clusterName: string, payload: any,
method="PUT") {
+ const url =
`/clusters/${clusterName}/services?ServiceInfo/state=INSTALLED&ServiceInfo/service_name=KERBEROS`
+ const response = await ambariApi.request({
+ url: url,
+ method: method,
+ data: payload,
+ headers:{
+ "Content-Type":"text/plain"
+ }
+ })
+ return response.data;
+ },
+ postRequest: async function (clusterName: string, payload: any,
method="POST") {
+ const url = `/clusters/${clusterName}/requests`
+ const response = await ambariApi.request({
+ url: url,
+ method: method,
+ data: payload,
+ })
+ return response.data;
+ },
+ getServices: async function (clusterName: string, payload: any, params:
string, method="PUT"){
+ const url = `/clusters/${clusterName}/services?${params}`
+ const response = await ambariApi.request({
+ url: url,
+ method: method,
+ data: payload,
+ headers: {
+ "Content-Type": "text/plain"
+ }
+ })
+ return response.data;
+ },
+ getServicesWithStatus: async function (clusterName: string, payload: any,
params: string, method="PUT"){
+ const url = `/clusters/${clusterName}/services?${params}`
+ const response = await ambariApi.request({
+ url: url,
+ method: method,
+ data: payload,
+ headers: {
+ "Content-Type": "text/plain"
+ }
+ })
+ if (!response.data)
+ response.data = {};
+
+ set(response.data, "status", response.status);
+ return response.data;
+ },
+ preparingOperations: async function (clusterName: string, payload: any,
params="") {
+ let url = `/clusters/${clusterName}`;
+ if(params !== "")
+ url = `${url}?${params}`
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: payload,
+ headers:{
+ "Content-Type":"text/plain"
+ }
+ })
+ return response.data;
+ },
+ regenerateKeytabs: async function (clusterName: string, payload: any,
params: string) {
+ const url = `/clusters/${clusterName}?${params}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ data: payload,
+ headers: {
+ "Content-Type": "text/plain"
+ }
+ })
+ return response.data;
+ },
+ kerberosDescriptor: async function (clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/artifacts/kerberos_descriptor`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload,
+ headers: {
+ "Content-Type": "text/plain"
+ }
+ })
+ return response.data;
+ },
+ getTask: async function (
+ clusterName: string,
+ requestId: string,
+ taskId: string
+ ) {
+ const url =
`/clusters/${clusterName}/requests/${requestId}/tasks/${taskId}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ installPackages: async function (clusterName: string, payload: any) {
+ const url = `/clusters/${clusterName}/stack_versions`
+ const response = await ambariApi.request({
+ url: url,
+ method: "POST",
+ data: payload,
+ headers: {
+ "Content-Type": "text/plain"
+ }
+ })
+ return response.data
+ },
+ getTaskId : async function (requestId: string) {
+ const url = `/requests/${requestId}/tasks/?`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ getTaskStatus: async function (requestID:string,taskId: string) {
+ const url = `requests/${requestID}/tasks/${taskId}?`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ }
+};
\ No newline at end of file
diff --git a/ambari-web/latest/src/api/servicesApi.ts
b/ambari-web/latest/src/api/servicesApi.ts
new file mode 100644
index 0000000000..1d176867bb
--- /dev/null
+++ b/ambari-web/latest/src/api/servicesApi.ts
@@ -0,0 +1,45 @@
+/**
+ * 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 ServicesApi = {
+ getServices: async (stack: string, version: string) => {
+ const url =
`stacks/${stack}/versions/${version}/services?fields=StackServices/*,components/*,components/dependencies/Dependencies/scope,components/dependencies/Dependencies/service_name,artifacts/Artifacts/artifact_name`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "GET",
+ });
+ return response.data;
+ },
+ deleteServiceWithUpdatedConfigs: async function (
+ clusterName: string,
+ data: any
+ ) {
+ const url = `/clusters/${clusterName}`;
+ const response = await ambariApi.request({
+ url: url,
+ method: "PUT",
+ headers: {
+ "Content-Type": "text/plain",
+ "X-Requested-By": "ambari-web",
+ },
+ data: JSON.stringify(data),
+ });
+ return response.data;
+ },
+};
diff --git a/ambari-web/latest/src/components/OperationProgress.tsx
b/ambari-web/latest/src/components/OperationProgress.tsx
new file mode 100644
index 0000000000..9467a390e0
--- /dev/null
+++ b/ambari-web/latest/src/components/OperationProgress.tsx
@@ -0,0 +1,335 @@
+/**
+ * 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, findIndex, get, has, set } from "lodash";
+import { useContext, useEffect, useRef, useState } from "react";
+import usePolling from "../hooks/usePolling";
+import { RequestApi } from "../api/requestApi";
+import { AppContext } from "../store/context";
+import { isFailed, isFinished } from "../Utils/Utility";
+import { ProgressStatus} from "../constants";
+import { Button, ProgressBar, Stack } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+ faCircleCheck,
+ faTimes,
+ faUndo,
+} from "@fortawesome/free-solid-svg-icons";
+// import modalManager from "../store/ModalManager";
+// import BackgroundOperations from "../screens/BackgroundOperations";
+
+type PropTypes = {
+ title: string;
+ description: string;
+ setCompletionStatus: (completed: boolean) => void;
+ operations: [
+ {
+ id: string | number;
+ label: "string";
+ callback: any;
+ skippable: boolean;
+ }
+ ];
+ dispatch?: (operationsState: any) => void;
+};
+
+type OperationRequestResponse = {
+ Requests: {
+ id: number | string;
+ status: string;
+ };
+ href: string;
+ status?:number;
+};
+
+function OperationsProgress({
+ // title,
+ // description,
+ setCompletionStatus,
+ operations,
+ dispatch,
+}: PropTypes) {
+ const [operationsState, setOperationsState] = useState(operations);
+ const operationsRef = useRef(operations);
+ const activeOperationId = useRef<any>(null);
+ const { clusterName } = useContext(AppContext);
+ const { stopPolling, pausePolling, resumePolling } = usePolling(
+ trackCurrentRequestStatus
+ );
+ const activeRequestId = useRef<string|number>(0);
+ const startedTasks: any = useRef([]);
+ async function trackCurrentRequestStatus() {
+ const operationsStateCopy = cloneDeep(operationsRef.current);
+ const trackingStatusForOperation: any = operationsStateCopy.find(
+ (operation) => operation.id == activeOperationId.current
+ );
+ if (activeRequestId.current) {
+ const activeRequestStatus = await RequestApi.getRequestStatus(
+ clusterName,
+ activeRequestId.current as any
+ );
+ const { Requests } = activeRequestStatus;
+ if (activeRequestStatus?.Requests?.request_status) {
+ set(
+ trackingStatusForOperation,
+ "requestId",
+ activeRequestStatus?.Requests?.id
+ );
+ set(
+ trackingStatusForOperation,
+ "status",
+ activeRequestStatus?.Requests?.request_status||"FAILED"
+ );
+ set(
+ trackingStatusForOperation,
+ "progress",
+ activeRequestStatus?.Requests?.progress_percent
+ );
+ set(
+ trackingStatusForOperation,
+ "requestInfo",
+ activeRequestStatus?.Requests
+ );
+ const requestStages = filter(
+ activeRequestStatus.stages,
+ function (stage) {
+ return has(stage, "Stage.context");
+ }
+ );
+ set(trackCurrentRequestStatus, "stages", requestStages);
+ setOperationsState(operationsStateCopy);
+ operationsRef.current = operationsStateCopy;
+ console.log("Current request status is", Requests.request_status);
+ if (isFinished(Requests.request_status)) {
+ console.log("Operation Progress operation finished");
+ if (Requests.request_status === ProgressStatus.FAILED) {
+ pausePolling();
+ } else {
+ if (activeOperationId.current == (operations as any)?.at(-1)?.id) {
+ stopPolling();
+ setCompletionStatus(true);
+ } else {
+ const currentActiveIndex = findIndex(operations, [
+ "id",
+ Number(activeOperationId?.current),
+ ]);
+ executeTask(operationsRef.current[Number(currentActiveIndex) +
1]?.id);
+ }
+ }
+ }
+ }
+ }
+ }
+ async function executeTask(id: string | number) {
+ id = Number(id);
+ if (!startedTasks.current.includes(id)) {
+ activeOperationId.current = id;
+ startedTasks.current.push(id);
+ const operationsStateCopy = cloneDeep(operationsRef.current);
+ const matchingOperation: any = operationsStateCopy.find(
+ (operation) => operation.id == id
+ );
+ if (matchingOperation) {
+ try {
+ const operationCallbackResponse:OperationRequestResponse = await
matchingOperation?.callback();
+ if (operationCallbackResponse?.Requests) {
+ matchingOperation.requestId =
operationCallbackResponse?.Requests?.id;
+ activeRequestId.current = operationCallbackResponse?.Requests?.id;
+ }
+ //TODO: @vhassija Please verify for all statusCode
+ else if(operationCallbackResponse?.status === 200||
!operationCallbackResponse){
+ if (activeOperationId.current == (operationsRef.current as
any)?.at(-1)?.id) {
+ stopPolling();
+ setCompletionStatus(true);
+ } else {
+ const currentActiveIndex =
operationsRef.current.findIndex((operation) => operation.id ==
activeOperationId.current);
+ executeTask(operationsRef.current[Number(currentActiveIndex) +
1]?.id);
+ }
+ }
+ else{
+ console.error("Operation failed with response",
operationCallbackResponse);
+ matchingOperation.status = "FAILED";
+ }
+ }
+ catch(err){
+ console.error("Got request", err)
+ matchingOperation.status = "FAILED";
+
+ }
+ }
+ setOperationsState(operationsStateCopy);
+ operationsRef.current = operationsStateCopy;
+ }
+ }
+ const retryOperation = () => {
+ startedTasks.current = startedTasks.current.filter((task:any) => {
+ task != activeOperationId.current;
+ });
+ executeTask(activeOperationId.current as any);
+ resumePolling();
+ };
+ const renderStagesForOperation = (operation: any) => {
+ return (
+ <Stack direction="vertical">
+ {operation.stages.map((stage: any) => {
+ return (
+ <Stack
+ direction="horizontal"
+ className="justify-content-between mt-3"
+ key={stage.context}
+ >
+ <div className="d-flex align-items-center">
+ {isFinished(stage.status) && (
+ <FontAwesomeIcon icon={faCircleCheck} color="success" />
+ )}
+ {isFailed(stage.status) && (
+ <FontAwesomeIcon icon={faTimes} color="danger" />
+ )}
+ <div>{stage.context} </div>
+ {isFailed(stage.status) ? (
+ <Button
+ size="sm"
+ onClick={retryOperation}
+ variant="success"
+ className="ms-2"
+ >
+ <FontAwesomeIcon className="me-2" icon={faUndo} />
+ Retry Operation
+ </Button>
+ ) : null}
+ </div>
+ {get(stage, "progress_percent", 0) &&
+ !isFinished(stage.status) ? (
+ <ProgressBar
+ striped
+ className={`w-25`}
+ variant="info"
+ now={stage.progress_percent}
+ label={`${Math.floor(stage.progress)}%`}
+ />
+ ) : null}
+ </Stack>
+ );
+ })}
+ </Stack>
+ );
+ };
+
+ useEffect(() => {
+ if(dispatch){
+ dispatch(operationsState);
+ }
+ }, [JSON.stringify(operationsState)]);
+
+ useEffect(() => {
+ let idx = -1;
+ for (let i = operationsRef.current.length - 1; i >= 0; i--) {
+ if (
+ get(operationsRef.current[i], "requestId", "") ||
+ get(operationsRef.current[i], "status", "")
+ ) {
+ idx = i;
+ activeOperationId.current = operationsRef.current?.[i]?.id;
+ break;
+ }
+ }
+ if (idx === -1) {
+ executeTask(operationsRef.current?.[0]?.id);
+ } else {
+ for (let i = 0; i <= idx; i++) {
+ if (
+ get(operationsRef.current[i], "requestId", "") ||
+ get(operationsRef.current[i], "status", "")
+ ) {
+ startedTasks.current.push(operationsRef.current[i].id);
+ }
+ }
+ }
+ }, []);
+
+
+ return (
+ <div className="p-3">
+ <Stack direction="vertical">
+ {operationsState.map((operation: any) => {
+ const operationStages = operation.stages || [];
+ if (operationStages.length) {
+ return renderStagesForOperation(operation);
+ } else {
+ return (
+ <Stack
+ direction="horizontal"
+ className="justify-content-between mt-3"
+ key={operation.label}
+ >
+ <div className="d-flex align-items-center">
+ <div
+ onClick={() => {
+ // modalManager.show(
+ // <BackgroundOperations
+ // isOpen
+ // onClose={() => {
+ // modalManager.hide();
+ // }}
+ // rootLevel={ViewLevel.HOSTS}
+ // requestId={
+ // operation.requestId ||
operation?.requestInfo?.id
+ // }
+ // />
+ // );
+ }}
+ className={`${
+ isFinished(operation.status) ||
+ has(operation, "progress") ||
+ has(operation, "requestId")
+ ? "custom-link"
+ : ""
+ }`}
+ >
+ {operation.label}{" "}
+ </div>
+ {isFailed(operation.status) ? (
+ <Button
+ size="sm"
+ onClick={retryOperation}
+ variant="success"
+ className="ms-2"
+ >
+ <FontAwesomeIcon className="me-2" icon={faUndo} />
+ Retry Operation
+ </Button>
+ ) : null}
+ </div>
+ {has(operation, "progress") && !isFinished(operation.status) ?
(
+ <ProgressBar
+ striped
+ className={`w-25`}
+ variant="info"
+ now={operation.progress}
+ label={`${Math.floor(operation.progress)}%`}
+ />
+ ) : null}
+ </Stack>
+ );
+ }
+ })}
+ </Stack>
+ </div>
+ );
+}
+
+export default OperationsProgress;
diff --git a/ambari-web/latest/src/components/StepWizard/index.tsx
b/ambari-web/latest/src/components/StepWizard/index.tsx
index 88d15f2ea3..6cafd67fb8 100644
--- a/ambari-web/latest/src/components/StepWizard/index.tsx
+++ b/ambari-web/latest/src/components/StepWizard/index.tsx
@@ -15,29 +15,30 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { FunctionComponent, memo, useState } from "react";
+import { FunctionComponent, memo, useContext, useState } from "react";
import "./styles.scss";
import classNames from "classnames";
-import { CheckLg} from "react-bootstrap-icons";
+import { CheckLg } from "react-bootstrap-icons";
import ConfirmationModal from "../ConfirmationModal";
+import { get } from "lodash";
+
interface StepWizardProps {
wizardUtilities: any;
+ Context?: React.Context<any>;
}
const StepWizard: FunctionComponent<StepWizardProps> = ({
wizardUtilities,
+ Context,
}: any) => {
- const {
- activeStep,
- wizardSteps,
- jumpToStep,
- canJumpFromCurrentStep,
- } = wizardUtilities;
+ const { activeStep, wizardSteps, jumpToStep, canJumpFromCurrentStep } =
+ wizardUtilities;
+ const contextValue = useContext<any>(Context || {});
+ const flushStateToDb = get(contextValue, "flushStateToDb", "");
const [jumpStep, setjumpStep] = useState(0);
const [showNavigationModal, setShowNavigationModal] = useState(false);
- console.log("active step in stepwizard ", activeStep)
return (
- <div className="step-wizard h-100" style={{ position: "relative" }}>
+ <div className="step-wizard h-95" style={{ position: "relative" }}>
<ConfirmationModal
isOpen={showNavigationModal}
onClose={() => {
@@ -47,6 +48,9 @@ const StepWizard: FunctionComponent<StepWizardProps> = ({
modalBody={`If you proceed to go back to Step ${jumpStep}, you will
lose any changes you made.`}
successCallback={() => {
jumpToStep(jumpStep);
+ if (flushStateToDb) {
+ flushStateToDb("jump", jumpStep);
+ }
setShowNavigationModal(false);
}}
/>
@@ -58,8 +62,10 @@ const StepWizard: FunctionComponent<StepWizardProps> = ({
<div
key={currentStep}
onClick={() => {
- setShowNavigationModal(true);
- setjumpStep(Number(currentStep));
+ if (wizardSteps[activeStep].canGoBack) {
+ setShowNavigationModal(true);
+ setjumpStep(Number(currentStep));
+ }
}}
className={classNames(
"d-flex align-items-center step-wizard-step cursor-pointer",
@@ -95,7 +101,7 @@ const StepWizard: FunctionComponent<StepWizardProps> = ({
})}
</div>
<div className="px-5 py-4 w-100 mh-100 y-scroll wizard-content">
- {wizardSteps[activeStep].Component}
+ {wizardSteps[activeStep]?.Component}
</div>
</div>
{/* <div className="step-wizard-footer d-flex justify-content-between
bg-white p-2">
diff --git a/ambari-web/latest/src/constants.ts
b/ambari-web/latest/src/constants.ts
new file mode 100644
index 0000000000..09d775ee8c
--- /dev/null
+++ b/ambari-web/latest/src/constants.ts
@@ -0,0 +1,28 @@
+/**
+ * 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 enum ClusterProgressStatus {
+ PROVISIONING = "PROVISIONING",
+ ENABLING_NAMENODE_HA = "ENABLING_NAMENODE_HA",
+ ADDING_HOST = "ADDING_HOST",
+ ADDING_SERVICE = "ADDING_SERVICE",
+}
+export enum ProgressStatus {
+ IN_PROGRESS = "IN_PROGRESS",
+ COMPLETED = "COMPLETED",
+ FAILED = "FAILED",
+}
\ No newline at end of file
diff --git a/ambari-web/latest/src/hooks/useDebounce.ts
b/ambari-web/latest/src/hooks/useDebounce.ts
new file mode 100644
index 0000000000..67f047840f
--- /dev/null
+++ b/ambari-web/latest/src/hooks/useDebounce.ts
@@ -0,0 +1,44 @@
+/**
+ * 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 React from "react";
+
+export const useDebounce = ( callback: (...args: any[]) => void,
+ delay: number,
+) => {
+ const callbackRef = React.useRef(callback);
+ React.useLayoutEffect(() => {
+ callbackRef.current = callback;
+ });
+ let timer: ReturnType<typeof setTimeout>;
+ const naiveDebounce = ( func: (...args: any[]) => void,
+ delayMs: number,
+ ...args: any[]
+ ) => {
+ clearTimeout(timer);
+ timer = setTimeout(() => {
+ func(...args);
+ }, delayMs); };
+
+ return React.useMemo(() => (...args: any) => naiveDebounce(
+ callbackRef.current,
+ delay,
+ ...args,
+ ), [delay]);
+};
+
+
\ No newline at end of file
diff --git a/ambari-web/latest/src/hooks/usePagination.ts
b/ambari-web/latest/src/hooks/usePagination.ts
new file mode 100644
index 0000000000..ffd79ffdb3
--- /dev/null
+++ b/ambari-web/latest/src/hooks/usePagination.ts
@@ -0,0 +1,63 @@
+/**
+ * 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 { useState, useEffect, useMemo } from 'react';
+
+const usePagination = (items: any[], initialItemsPerPage=10) => {
+ const [currentPage, setCurrentPage] = useState(1);
+ const [itemsPerPage, setItemsPerPage] = useState(initialItemsPerPage);
+
+// Calculate max page based on current items and itemsPerPage
+ const maxPage = Math.max(1, Math.ceil(items.length / itemsPerPage));
+
+ // Reset to page 1 if the data changes significantly or if current page is
out of bounds
+ useEffect(() => {
+ if (currentPage > maxPage) {
+ setCurrentPage(1);
+ }
+ }, [items.length, maxPage, currentPage]);
+
+ // Use useMemo to ensure we don't unnecessarily recalculate the current items
+ // This helps ensure that sorting is preserved when paginating
+ const currentItems = useMemo(() => {
+ // Get the current slice of items based on pagination settings
+ return items.slice(
+ (currentPage - 1) * itemsPerPage,
+ currentPage * itemsPerPage
+ );
+ }, [items, currentPage, itemsPerPage]);
+
+ const changePage = (newPage: number) => {
+ const safePage = Math.max(1, Math.min(newPage, maxPage));
+ setCurrentPage(safePage);
+ };
+
+ const updateItemsPerPage = (newItemsPerPage: number) => {
+ setItemsPerPage(newItemsPerPage);
+ setCurrentPage(1); // Reset to the first page when changing items per page
+ };
+
+ return {
+ currentItems,
+ changePage,
+ currentPage,
+ maxPage,
+ itemsPerPage,
+ setItemsPerPage: updateItemsPerPage
+ };
+};
+export default usePagination;
\ No newline at end of file
diff --git a/ambari-web/latest/src/hooks/usePolling.ts
b/ambari-web/latest/src/hooks/usePolling.ts
new file mode 100644
index 0000000000..5edc08406a
--- /dev/null
+++ b/ambari-web/latest/src/hooks/usePolling.ts
@@ -0,0 +1,64 @@
+/**
+ * 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 @typescript-eslint/ban-types */
+import { useEffect, useRef, useCallback, useState } from 'react';
+
+function usePolling(apiFunction: Function, interval = 2000) {
+ const savedCallback = useRef<Function>(null);
+ const intervalId = useRef<NodeJS.Timeout | null>(null);
+ const [isPaused, setIsPaused] = useState(false);
+
+ const stopPolling = useCallback(() => {
+ if (intervalId.current) {
+ clearInterval(intervalId.current);
+ intervalId.current = null;
+ }
+ }, []);
+
+ const pausePolling = useCallback(() => {
+ setIsPaused(true);
+ stopPolling();
+ }, [stopPolling]);
+
+ const resumePolling = useCallback(() => {
+ setIsPaused(false);
+ }, []);
+
+ // Remember the latest callback.
+ useEffect(() => {
+ savedCallback.current = apiFunction;
+ }, [apiFunction]);
+
+ // Set up the interval.
+ useEffect(() => {
+ function tick() {
+ if (savedCallback.current) {
+ savedCallback.current();
+ }
+ }
+
+ if (!isPaused && interval !== null) {
+ intervalId.current = setInterval(tick, interval);
+ return () => stopPolling();
+ }
+ }, [interval, isPaused, stopPolling]);
+
+ return { stopPolling, pausePolling, resumePolling };
+}
+
+export default usePolling;
\ No newline at end of file
diff --git a/ambari-web/latest/src/hooks/usePrevious.ts
b/ambari-web/latest/src/hooks/usePrevious.ts
new file mode 100644
index 0000000000..45cfc239d6
--- /dev/null
+++ b/ambari-web/latest/src/hooks/usePrevious.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 { useEffect, useRef } from "react";
+
+const usePrevious = (value:any) => {
+ const ref = useRef<any>(null);
+ useEffect(() => {
+ ref.current = value;
+ });
+ return ref.current;
+ };
+export default usePrevious;
diff --git a/ambari-web/latest/src/hooks/useStepWizard.ts
b/ambari-web/latest/src/hooks/useStepWizard.ts
new file mode 100644
index 0000000000..425c1bcefb
--- /dev/null
+++ b/ambari-web/latest/src/hooks/useStepWizard.ts
@@ -0,0 +1,141 @@
+/**
+ * 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 { useState, useEffect } from "react";
+// import { useNavigate } from "react-router-dom";
+import { cloneDeep } from "lodash";
+import { Step } from "../types/StepWizard";
+import { useLocation, useNavigate, useParams } from "react-router-dom";
+
+const useStepWizard = (steps: any, initialActiveStep = 0, onCancel?: any) => {
+ const [activeStep, setActiveStep] = useState(initialActiveStep || 0);
+ const navigate = useNavigate();
+ const [wizardSteps, setWizardSteps] = useState<{ [key: number]: Step }>(
+ steps
+ );
+ const { stepNumber } = useParams();
+ const location = useLocation();
+ useEffect(() => {
+ if (stepNumber) {
+ navigate(location.pathname.replace(/step\d+/g, `step${activeStep}`));
+ }
+ }, [activeStep]);
+
+ const initialiseNextCallback = async (next: any) => {
+ const wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].onNext = next;
+ setWizardSteps(wizardStepsCopy);
+ };
+
+ const handleNext = async () => {
+ const wizardStepsCopy = { ...wizardSteps };
+
+ if (wizardSteps[activeStep].onNext) {
+ try {
+ const canProceed = await wizardSteps[activeStep].onNext();
+ if (canProceed) {
+ wizardStepsCopy[activeStep].completed = true;
+ setWizardSteps(wizardStepsCopy);
+ if (activeStep !== Object.keys(steps).length - 1) {
+ setActiveStep(activeStep + 1);
+ }
+ }
+ } catch (error) {
+ console.log("Cannot move to next step:", error);
+ }
+ }
+ };
+ const handleNextImperitive = async () => {
+ const wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].completed = true;
+ setActiveStep(Number(activeStep) + 1);
+ setWizardSteps(wizardStepsCopy);
+ };
+ const handleBackImperitive = async () => {
+ const wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].completed = false;
+ wizardStepsCopy[activeStep - 1].completed = false;
+ setActiveStep(Number(activeStep) - 1);
+ setWizardSteps(wizardStepsCopy);
+ };
+
+ const enableNext = async (nextCallback: any) => {
+ const wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].isNextEnabled = true;
+ wizardStepsCopy[activeStep].onNext = nextCallback;
+ setWizardSteps(wizardStepsCopy);
+ };
+ const disableNext = () => {
+ const wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].isNextEnabled = false;
+ setWizardSteps(wizardStepsCopy);
+ };
+
+ const handleBack = () => {
+ let wizardStepsCopy = { ...wizardSteps };
+ wizardStepsCopy[activeStep].completed = false;
+ wizardStepsCopy[activeStep - 1].completed = false;
+ setWizardSteps(wizardStepsCopy);
+ if (activeStep !== 0) setActiveStep(activeStep - 1);
+ };
+
+ const canJumpFromCurrentStep = (currentStep: number) => {
+ return !(
+ currentStep > activeStep ||
+ (currentStep !== activeStep && !wizardSteps[activeStep]?.canGoBack)
+ );
+ };
+
+ useEffect(() => {
+ // navigate(`/installer/step${activeStep}`);
+ }, [activeStep]);
+
+ const jumpToStep = (stepNumber: number, isImperitiveJump = false) => {
+ if (canJumpFromCurrentStep(stepNumber) || isImperitiveJump) {
+ const wizardStepsCopy = cloneDeep(wizardSteps);
+ for (const step in wizardStepsCopy) {
+ if (Number(step) >= stepNumber) {
+ wizardStepsCopy[step].completed = false;
+ } else {
+ wizardStepsCopy[step].completed = true;
+ }
+ }
+ setWizardSteps(wizardStepsCopy);
+ setActiveStep(stepNumber);
+ }
+ };
+ console.log("Wizard Steps in useStepWizard", wizardSteps);
+
+ return {
+ activeStep,
+ wizardSteps,
+ handleNext,
+ handleBack,
+ jumpToStep,
+ canJumpFromCurrentStep,
+ enableNext,
+ disableNext,
+ initialiseNextCallback,
+ handleNextImperitive,
+ handleBackImperitive,
+ currentStep: wizardSteps[activeStep],
+ prevStepNumber: activeStep - 1,
+ onCancel
+ };
+};
+
+export default useStepWizard;
diff --git a/ambari-web/latest/src/store/context.tsx
b/ambari-web/latest/src/store/context.tsx
new file mode 100644
index 0000000000..f3886871a5
--- /dev/null
+++ b/ambari-web/latest/src/store/context.tsx
@@ -0,0 +1,489 @@
+/**
+ * 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 React, {
+ createContext,
+ Dispatch,
+ useEffect,
+ useReducer,
+ useState,
+} from "react";
+import { State, Action } from "./types";
+import { reducer, initialState } from "./reducer";
+import { Client } from "@stomp/stompjs";
+import ClusterApi from "../api/clusterApi";
+import { ChooseServicesApi } from "../api/chooseServicesApi";
+import { ServicesApi } from "../api/servicesApi";
+import { get, isEmpty, isString, map, set } from "lodash";
+import ConfigsApi from "../api/configsApi";
+import {
+// mapStackConfigProperties,
+ redirectToLogin,
+} from "../Utils/Utility";
+import LoginApi from "../api/loginApi";
+import { db } from "../Utils/db";
+// import {LocalStorageOps} from "../Utils/LocalStorageOps";
+
+interface AppContextProps {
+ state: State;
+ dispatch: Dispatch<Action>;
+ client: any;
+ isSocketConnected: boolean;
+ parsedSocketMessages: any[];
+ clusterName: string;
+ services: any[];
+ cluster: any;
+ isAppLoaded: boolean;
+ serviceComponentInfo: any;
+ isKerberosEnabled: boolean;
+ stackConfigurations: any;
+ allHostNames: string[];
+ ambariProperties: any;
+ upgradeState: string;
+ setUpgradeState: (state: string) => void;
+ upgradeDirection: string;
+ setUpgradeDirection: (direction: string) => void;
+ upgradeSuspend: boolean;
+ currentStackVersion: string;
+ setCurrentStackVersion: (version: string) => void;
+ upgradeId: number;
+ setUpgradeId: (id: number) => void;
+ userUrl?: string;
+ sessionsValidated: boolean;
+ sessionExists: boolean;
+ clusterState: any;
+}
+
+export const AppContext = createContext<AppContextProps>({
+ state: initialState,
+ dispatch: () => undefined,
+ client: null,
+ isSocketConnected: false,
+ parsedSocketMessages: [],
+ clusterName: "",
+ services: [],
+ cluster: {},
+ isAppLoaded: false,
+ serviceComponentInfo: {},
+ isKerberosEnabled: false,
+ stackConfigurations: [],
+ allHostNames: [],
+ ambariProperties: {},
+ upgradeState: "",
+ setUpgradeState: () => {},
+ upgradeDirection: "",
+ setUpgradeDirection: () => {},
+ upgradeSuspend: false,
+ currentStackVersion: "",
+ setCurrentStackVersion: () => {},
+ upgradeId: 0,
+ setUpgradeId: () => {},
+ sessionExists: false,
+ sessionsValidated: false,
+ clusterState: {}
+});
+
+export const AppProvider: React.FC<{ children: React.ReactNode }> = ({
+ children,
+}) => {
+ const [state, dispatch] = useReducer(reducer, initialState);
+ const [isSocketConnected, setIsSocketConnected] = useState(false);
+ const [socketClient, setSocketClient] = useState(null);
+ const [isAppLoaded, setAppLoaded] = useState(false);
+ const [parsedSocketMessages, setParsedSocketMessages] = useState<any[]>([]);
+ const [clusterName, setClusterName] = useState<string>("");
+ const [isKerberosEnabled, setIsKerberosEnabled] = useState(false);
+ const [cluster, setCluster] = useState<any>({});
+ const [serviceComponentInfo, setServiceComponentInfo] = useState<any>({});
+ const [upgradeState, setUpgradeState] = useState<string>("");
+ const [upgradeDirection, setUpgradeDirection] = useState<string>("");
+ const [upgradeSuspend, setUpgradeSuspend] = useState<boolean>(false);
+ const [upgradeId, setUpgradeId] = useState<number>(0);
+ const [currentStackVersion, setCurrentStackVersion] = useState<string>("");
+ const [ambariProperties, setAmbariProperties] = useState({});
+ const [sessionsValidated, setSessionsValidated] = useState(false);
+ const [sessionExists, setSessionExists] = useState(false);
+ const [clusterState, setClusterState] = useState({});
+ const [userUrl, setUserUrl] = useState("");
+ const client = new Client({
+ brokerURL: "/api/stomp/v1/websocket", // 'ws://localhost:15674/ws'
+ debug: function (str) {
+ console.log(str);
+ },
+ reconnectDelay: 1000,
+ heartbeatIncoming: 1000,
+ heartbeatOutgoing: 1000,
+ });
+ const [services, setServices] = useState([]);
+ const [stackConfigurations, setStackConfigurations] = useState([]);
+
+ const [allHostNames, setAllHostNames] = useState([]);
+ const fetchClusterServices = async () => {
+ try {
+ const clusterServices = await
ChooseServicesApi.servicesList(clusterName);
+ setServices(clusterServices.items);
+ } catch (err) {
+ setServices([]);
+ }
+ };
+
+ const fetchClusterState = async () => {
+ try {
+ const state = await ClusterApi.getPersistData("CLUSTER_STATE");
+ setClusterState(state);
+ } catch (error) {
+ console.error("Failed to fetch cluster state:", error);
+ }
+ };
+
+ useEffect(() => {
+ fetchClusterState();
+ }, []);
+
+ useEffect(() => {
+ if (clusterName) {
+ fetchClusterServices();
+ fetchAllHostNames();
+ fetchUpgradeStates();
+ }
+ }, [clusterName]);
+
+ useEffect(() => {
+ async function fetchStackConfigs() {
+ const stack = get(cluster, "version", "").split("-")[0];
+ const version = get(cluster, "version", "").split("-")[1];
+ const serviceNames = map(services, "ServiceInfo.service_name").join(",");
+ if (stack && version && serviceNames) {
+ //@ts-ignore
+ const response = await ConfigsApi.getServiceConfigurations(
+ stack,
+ version,
+ serviceNames
+ );
+ //TODO: Uncomment this once mapStackConfigProperties is defined
+ // const stackConfigs = mapStackConfigProperties(response);
+ const stackConfigs:never[]=[];
+
+ setStackConfigurations(stackConfigs);
+ }
+ }
+ fetchStackConfigs();
+ }, [services, cluster]);
+
+ const fetchClusterName = async () => {
+ try {
+ const name = await ClusterApi.getClusterName();
+ setClusterName(name);
+ } catch (error) {
+ console.error("Failed to fetch cluster name:", error);
+ }
+ };
+ const fetchClusterData = async () => {
+ try {
+ const clusterData = await ClusterApi.getClusterData();
+ set(
+ clusterData,
+ "items.[0].Clusters.stack",
+ get(clusterData, "items.[0].Clusters.version", "")?.split("-")[0]
+ );
+ set(
+ clusterData,
+ "items[0].Clusters.versionNum",
+ get(clusterData, "items.[0].Clusters.version", "")?.split("-")[1]
+ );
+ setCluster(clusterData?.items[0]?.Clusters);
+ setIsKerberosEnabled(
+ clusterData?.items?.[0]?.Clusters?.security_type === "KERBEROS"
+ );
+ } catch (error) {
+ console.error("Failed to fetch cluster data:", error);
+ }
+ };
+
+ const fetchServiceComponentInfo = async () => {
+ const stack = get(cluster, "version", "").split("-")[0];
+ const version = get(cluster, "version", "").split("-")[1];
+ try {
+ const data = await ServicesApi.getServices(stack, version);
+ setServiceComponentInfo(data);
+ } catch (error) {
+ console.error("Failed to fetch service component info:", error);
+ }
+ };
+
+ const fetchAllHostNames = async () => {
+ try {
+ const data = await ClusterApi.getHosts(clusterName);
+ const hostNames = data.items.map((item: any) => item.Hosts.host_name);
+ setAllHostNames(hostNames);
+ } catch (error) {
+ console.log("Error getting hosts");
+ }
+ };
+ const getAmbariProperties = async () => {
+ const response = await ClusterApi.loadAmbariProperties();
+ setAmbariProperties(response);
+ };
+ const fetchUserInfo = async () => {
+ try {
+ try {
+ let username = "";
+ const ambariLocalData = db.getItem("ambari");
+ if (ambariLocalData) {
+ let parsedData = {};
+ try {
+ parsedData = JSON.parse(db.getItem("ambari") || "{}");
+ if (isString(parsedData)) {
+ parsedData = JSON.parse(parsedData);
+ }
+ } catch (err) {
+ console.log("Error parsing ambari data", err);
+ parsedData = {};
+ }
+
+ let ambari: any = parsedData;
+ if (ambari?.app?.loginName) {
+ username = ambari.app.loginName;
+ // If we already have a username, we can consider the user
authenticated
+ if (!username) {
+ window.location.href = "/#/login";
+ }
+ setSessionsValidated(true);
+ setSessionExists(true);
+ }
+ } else {
+ redirectToLogin();
+ }
+ // If we don't have user info in localStorage
+
+ //fetch initial LS value for key ambari with empty fields
+ let initialAmbariLsData: any = { app: {} };
+
+ //set login name in the app object within the ambari object
+ initialAmbariLsData.app.loginName = encodeURIComponent(username);
+ initialAmbariLsData.app.authenticated = true;
+ const params = { usr: "", loginName: encodeURIComponent(username) };
+ const response = await LoginApi.handleSuccessfulLogin(params);
+ initialAmbariLsData.app.user = response.data.Users;
+ //convert JS object to JSON String and then encrypt the JSON String
+ initialAmbariLsData = JSON.stringify(initialAmbariLsData);
+ //encrypt the data and store it in Local Storage
+ db.setItem("ambari", initialAmbariLsData);
+ return true;
+ } catch (error) {
+ setSessionsValidated(true);
+ setSessionExists(false);
+ return false;
+ }
+ } catch (err) {
+ console.log("Error in fetching user info", err);
+ setSessionsValidated(true);
+ setSessionExists(false);
+ return false;
+ }
+ };
+
+ const fetchUpgradeStates = async () => {
+ const response = await ClusterApi.getUpgradeState(clusterName);
+
+ // response would have items get the last item.
+ const lastItemIndex = response?.items?.length - 1;
+ const upgradeState = get(
+ response,
+ `items[${lastItemIndex}].Upgrade.request_status`,
+ "NOT_REQUIRED"
+ );
+ const upgradeSuspend = get(
+ response,
+ `items[${lastItemIndex}].Upgrade.suspended`,
+ false
+ );
+ const upgradeId = get(
+ response,
+ `items[${lastItemIndex}].Upgrade.request_id`,
+ 0
+ );
+ const upgradeDirection = get(response,
`items[${lastItemIndex}].Upgrade.direction`, "UPGRADE");
+ setUpgradeDirection(upgradeDirection);
+ setUpgradeId(upgradeId);
+ setUpgradeState(upgradeState);
+ setUpgradeSuspend(upgradeSuspend);
+ };
+
+ async function getUserUrl() {
+ const persistedData = await ClusterApi.getPersistData(
+ "USER_REDIRECTION_URL"
+ );
+ setUserUrl(persistedData);
+ }
+ useEffect(() => {
+ async function moveAppToReadyState() {
+ const isAuthenticated = await fetchUserInfo();
+ setSessionsValidated(true);
+ setSessionExists(true);
+ if (isAuthenticated) {
+ await fetchClusterName();
+ await fetchClusterData();
+ await getAmbariProperties();
+ try {
+ await getUserUrl();
+ } catch (err) {
+ } finally {
+ setAppLoaded(true);
+ }
+ } else {
+ }
+ }
+ moveAppToReadyState();
+ }, []);
+
+ useEffect(() => {
+ if (!isEmpty(cluster)&&cluster?.versionNum&&cluster?.stack) {
+ fetchServiceComponentInfo();
+ }
+ }, [cluster]);
+
+ client.onConnect = function () {
+ setSocketClient(client as any);
+ setIsSocketConnected(true);
+ client.subscribe("/events/requests", (message: any) => {
+ // called when the client receives a STOMP message from the server
+ if (message.body) {
+ try {
+ const parsedMessage = JSON.parse(message.body);
+ //TODO: parsedSocketMessages can exceed to very long list, need to
limit it to some constant
+ setParsedSocketMessages((prevMessages) => [
+ parsedMessage,
+ ...prevMessages,
+ ]);
+ } catch {
+ console.log("Error in parsing socket message");
+ }
+ } else {
+ }
+ });
+ client.subscribe("/events/services", (message: any) => {
+ // called when the client receives a STOMP message from the server
+ if (message.body) {
+ try {
+ const parsedMessage = JSON.parse(message.body);
+ //TODO: parsedSocketMessages can exceed to very long list, need to
limit it to some constant
+ setParsedSocketMessages((prevMessages) => [
+ parsedMessage,
+ ...prevMessages,
+ ]);
+ } catch {
+ console.log("Error in parsing socket message");
+ }
+ } else {
+ }
+ });
+ client.subscribe("/events/hosts", (message: any) => {
+ if (message.body) {
+ try {
+ const parsedMessage = JSON.parse(message.body);
+ set(parsedMessage, "destination", message.headers.destination);
+ setParsedSocketMessages((prevMessages) => [
+ parsedMessage,
+ ...prevMessages,
+ ]);
+ } catch {
+ console.log("Error in parsing socket message");
+ }
+ } else {
+ }
+ });
+ client.subscribe("/events/requests", (message: any) => {
+ if (message.body) {
+ try {
+ const parsedMessage = JSON.parse(message.body);
+ set(parsedMessage, "destination", message.headers.destination);
+ setParsedSocketMessages((prevMessages) => [
+ parsedMessage,
+ ...prevMessages,
+ ]);
+ } catch {
+ console.log("Error in parsing socket message");
+ }
+ } else {
+ }
+ });
+ client.subscribe("/events/hostcomponents", (message: any) => {
+ if (message.body) {
+ try {
+ const parsedMessage = JSON.parse(message.body);
+ set(parsedMessage, "destination", message.headers.destination);
+ setParsedSocketMessages((prevMessages) => [
+ parsedMessage,
+ ...prevMessages,
+ ]);
+ } catch {
+ console.log("Error in parsing socket message");
+ }
+ } else {
+ }
+ });
+ };
+
+ client.onStompError = function (frame: any) {
+ // Will be invoked in case of error encountered at Broker
+ console.error("Broker reported error: " + frame.headers["message"]);
+ console.error("Additional details: " + frame.body);
+ };
+
+ useEffect(() => {
+ client.activate();
+ }, []);
+
+ // add a method to check if clusterExists if yes return clusterName from
API otherwise return empty string
+
+ return (
+ <AppContext.Provider
+ value={{
+ state,
+ dispatch,
+ client: socketClient,
+ isSocketConnected,
+ parsedSocketMessages,
+ clusterName,
+ cluster,
+ isAppLoaded,
+ services,
+ serviceComponentInfo,
+ ambariProperties,
+ isKerberosEnabled,
+ stackConfigurations,
+ allHostNames,
+ upgradeState,
+ setUpgradeState,
+ upgradeDirection,
+ setUpgradeDirection,
+ upgradeSuspend,
+ currentStackVersion,
+ setCurrentStackVersion,
+ upgradeId,
+ setUpgradeId,
+ userUrl,
+ sessionExists,
+ sessionsValidated,
+ clusterState,
+ }}
+ >
+ {children}
+ </AppContext.Provider>
+ );
+};
diff --git a/ambari-web/latest/src/store/reducer.ts
b/ambari-web/latest/src/store/reducer.ts
new file mode 100644
index 0000000000..138456e5b9
--- /dev/null
+++ b/ambari-web/latest/src/store/reducer.ts
@@ -0,0 +1,29 @@
+/**
+ * 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 { State, Action, ActionTypes } from "./types";
+
+export const initialState: State = { selectedOption: "dashboard" };
+
+export const reducer = (state: State, action: Action): State => {
+ switch (action.type) {
+ case ActionTypes.SELECTOPTION:
+ return { ...state, selectedOption: action.payload };
+ default:
+ return state;
+ }
+};
diff --git a/ambari-web/latest/src/store/types.ts
b/ambari-web/latest/src/store/types.ts
new file mode 100644
index 0000000000..eb6b7b7470
--- /dev/null
+++ b/ambari-web/latest/src/store/types.ts
@@ -0,0 +1,27 @@
+/**
+ * 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 interface State {
+ selectedOption: string;
+}
+
+export enum ActionTypes {
+ SELECTOPTION = "SELECTOPTION",
+}
+
+export type Action =
+ | { type: ActionTypes.SELECTOPTION; payload: string; }
diff --git a/ambari-web/latest/src/types/StepWizard.ts
b/ambari-web/latest/src/types/StepWizard.ts
new file mode 100644
index 0000000000..7bdcf74f21
--- /dev/null
+++ b/ambari-web/latest/src/types/StepWizard.ts
@@ -0,0 +1,28 @@
+/**
+ * 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 { ReactNode } from "react";
+
+export interface Step {
+ label: String;
+ completed: Boolean;
+ Component: ReactNode;
+ canGoBack: Boolean;
+ isNextEnabled: Boolean;
+ onNext?:any;
+ nextLabel?:String;
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]