This is an automated email from the ASF dual-hosted git repository.
arshad 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 fd50007336 AMBARI-26547 : Ambari Web React: Implement Rolling Restart
Modal (#4063)
fd50007336 is described below
commit fd5000733647a7576b3446a430f8d96beb09888a
Author: Himanshu Maurya <[email protected]>
AuthorDate: Sat Sep 13 22:41:53 2025 +0530
AMBARI-26547 : Ambari Web React: Implement Rolling Restart Modal (#4063)
---
.../latest/src/components/RollingRestartModal.tsx | 396 +++++++++++++++++++++
ambari-web/latest/src/screens/Hosts/utils.tsx | 37 ++
2 files changed, 433 insertions(+)
diff --git a/ambari-web/latest/src/components/RollingRestartModal.tsx
b/ambari-web/latest/src/components/RollingRestartModal.tsx
new file mode 100644
index 0000000000..314365c222
--- /dev/null
+++ b/ambari-web/latest/src/components/RollingRestartModal.tsx
@@ -0,0 +1,396 @@
+/**
+ * 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 { get } from "lodash";
+import Modal from "./Modal";
+import { useEffect, useState } from "react";
+import { pluralize, validateInteger } from "../screens/Hosts/utils";
+import { Alert, Form } from "react-bootstrap";
+import modalManager from "../store/ModalManager";
+import { translate, translateWithVariables } from "../Utils/Utility";
+
+interface RollingRestartModalProps {
+ hostComponentName: string;
+ serviceName: string;
+ isServiceInMM: boolean;
+ staleConfigsOnly: boolean;
+ skipMaintenance: boolean;
+ allHostComponents: any[];
+ turnOnMm: boolean;
+ isOpen: boolean;
+ onClose: () => void;
+ successCallback: (
+ restartComponents: any,
+ batchSize: number,
+ waitTime: number,
+ tolerateSize: number,
+ turnOnMm: boolean
+ ) => void;
+}
+
+export default function RollingRestartModal({
+ hostComponentName,
+ serviceName,
+ isServiceInMM,
+ staleConfigsOnly,
+ skipMaintenance,
+ allHostComponents,
+ turnOnMm,
+ isOpen,
+ onClose,
+ successCallback,
+}: RollingRestartModalProps) {
+ const [formData, setFormData] = useState({
+ batchSize: "-1",
+ interBatchWaitTimeSeconds: "-1",
+ tolerateSize: "-1",
+ staleConfigsOnly: false,
+ turnOnMm: false,
+ });
+
+ const [errorsList, setErrorsList] = useState<string[]>([]);
+ const [warningsList, setWarningsList] = useState<string[]>([]);
+
+ useEffect(() => {
+ initialize();
+ }, []);
+
+ useEffect(() => {
+ validate();
+ }, [JSON.stringify(formData)]);
+
+ const initialize = () => {
+ if (
+ get(formData, "batchSize") === "-1" &&
+ get(formData, "interBatchWaitTimeSeconds") === "-1" &&
+ get(formData, "tolerateSize") === "-1"
+ ) {
+ const restartCount = restartHostComponents().length;
+ let calculatedBatchSize = "1";
+
+ if (restartCount > 10 && hostComponentName !== "DATANODE") {
+ calculatedBatchSize = Math.ceil(restartCount / 10).toString();
+ }
+
+ setFormData({
+ batchSize: calculatedBatchSize,
+ tolerateSize: calculatedBatchSize,
+ interBatchWaitTimeSeconds: "120",
+ staleConfigsOnly: staleConfigsOnly,
+ turnOnMm: turnOnMm,
+ });
+ }
+ };
+
+ const validate = () => {
+ const displayName = pluralize(hostComponentName);
+ const componentName = get(allHostComponents, "[0].componentName", "");
+ const totalCount = restartHostComponents().length;
+ const bs = get(formData, "batchSize");
+ const ts = get(formData, "tolerateSize");
+ const wait = get(formData, "interBatchWaitTimeSeconds");
+ let errors = [];
+ let warnings = [];
+ let bsError = "";
+ let tsError = "";
+ let waitError = "";
+ if (totalCount < 1) {
+ errors.push(
+ translateWithVariables("rollingrestart.dialog.msg.noRestartHosts", {
+ "0": displayName,
+ }) as string
+ );
+ } else {
+ if (componentName == "DATANODE") {
+ if (parseInt(bs) > 1) {
+ warnings.push(
+ translate(
+ "rollingrestart.dialog.warn.datanode.batch.size"
+ ) as string
+ );
+ }
+ bsError = validateInteger(bs, 1, undefined);
+ } else {
+ bsError = validateInteger(bs, 1, totalCount);
+ }
+ tsError = validateInteger(ts, 0, totalCount);
+ if (bsError) {
+ errors.push(
+ translateWithVariables(
+ "rollingrestart.dialog.err.invalid.batchsize",
+ {
+ "0": bsError,
+ }
+ ) as string
+ );
+ }
+ if (tsError) {
+ errors.push(
+ translateWithVariables(
+ "rollingrestart.dialog.err.invalid.toleratesize",
+ {
+ "0": tsError,
+ }
+ ) as string
+ );
+ }
+ }
+ waitError = validateInteger(wait, 0, undefined);
+ if (waitError) {
+ errors.push(
+ translateWithVariables("rollingrestart.dialog.err.invalid.waitTime", {
+ "0": waitError,
+ }) as string
+ );
+ }
+ setErrorsList(errors);
+ setWarningsList(warnings);
+ };
+
+ const restartHostComponents = () => {
+ let hostComponents = skipMaintenance
+ ? allHostComponents
+ : nonMaintainanceHostComponents();
+ if (get(formData, "staleConfigsOnly")) {
+ hostComponents = hostComponents.filter(
+ (hostComponent) => get(hostComponent, "staleConfigs") === true
+ );
+ }
+ return hostComponents;
+ };
+
+ const nonMaintainanceHostComponents = () => {
+ return allHostComponents.filter(
+ (hostComponent) => get(hostComponent, "passiveState") !== "ON"
+ );
+ };
+
+ const restartMessage = () => {
+ return translateWithVariables("rollingrestart.dialog.msg.restart", {
+ "0": pluralize(hostComponentName),
+ });
+ };
+
+ const componentsWithMaintenanceHost = () => {
+ return allHostComponents.filter(
+ (hostComponent) => get(hostComponent, "passiveState") === "ON"
+ );
+ };
+
+ const maintainanceMessage = () => {
+ const count = componentsWithMaintenanceHost().length;
+ if (count > 0) {
+ return translateWithVariables("rollingrestart.dialog.msg.maintainance", {
+ "0": count.toString(),
+ });
+ }
+ return null;
+ };
+
+ const suggestTurnOnMaintenanceMsg = () => {
+ if (!isServiceInMM) {
+ return translateWithVariables(
+ "rollingrestart.dialog.msg.serviceNotInMM",
+ {
+ "0": serviceName,
+ }
+ );
+ }
+ return null;
+ };
+
+ const batchSizeMessage = () => {
+ return translateWithVariables(
+ "rollingrestart.dialog.msg.componentsAtATime",
+ {
+ "0": pluralize(hostComponentName),
+ }
+ );
+ };
+
+ const staleConfigsOnlyMessage = () => {
+ return translateWithVariables(
+ "rollingrestart.dialog.msg.staleConfigsOnly",
+ {
+ "0": pluralize(hostComponentName),
+ }
+ );
+ };
+
+ const turnOnMmMsg = () => {
+ return translateWithVariables("passiveState.turnOnFor", {
+ "0": serviceName,
+ });
+ };
+
+ const getModalBody = () => {
+ return (
+ <div>
+ <Alert variant="info">
+ <div>{restartMessage()}</div>
+ {maintainanceMessage() && (
+ <div className="mt-1">{maintainanceMessage()}</div>
+ )}
+ {suggestTurnOnMaintenanceMsg() && (
+ <div className="mt-1">{suggestTurnOnMaintenanceMsg()}</div>
+ )}
+ </Alert>
+ <Form>
+ <Form.Group className="mb-1 d-flex">
+ <Form.Label className="mt-2 me-3 w-25 d-flex justify-content-end">
+ {translate("common.restart")}
+ </Form.Label>
+ <Form.Control
+ type="text"
+ value={formData.batchSize}
+ onChange={(e) =>
+ setFormData({
+ ...formData,
+ batchSize: e.target.value,
+ })
+ }
+ className="custom-form-control w-20"
+ />
+ <Form.Label className="mt-2 ms-3">{batchSizeMessage()}</Form.Label>
+ </Form.Group>
+ <Form.Group className="mb-1 d-flex">
+ <Form.Label className="mt-2 me-3 w-25 d-flex justify-content-end">
+ {translate("rollingrestart.dialog.msg.timegap.prefix")}
+ </Form.Label>
+ <Form.Control
+ type="text"
+ value={formData.interBatchWaitTimeSeconds}
+ onChange={(e) =>
+ setFormData({
+ ...formData,
+ interBatchWaitTimeSeconds: e.target.value,
+ })
+ }
+ className="custom-form-control w-20"
+ />
+ <Form.Label className="mt-2 ms-3">
+ {translate("rollingrestart.dialog.msg.timegap.suffix")}
+ </Form.Label>
+ </Form.Group>
+ <Form.Group className="mb-2 d-flex">
+ <Form.Label className="mt-2 me-3 w-25 d-flex justify-content-end">
+ {translate("rollingrestart.dialog.msg.toleration.prefix")}
+ </Form.Label>
+ <Form.Control
+ type="text"
+ value={formData.tolerateSize}
+ onChange={(e) =>
+ setFormData({
+ ...formData,
+ tolerateSize: e.target.value,
+ })
+ }
+ className="custom-form-control w-20"
+ />
+ <Form.Label className="mt-2 ms-3">
+ {translate("rollingrestart.dialog.msg.toleration.suffix")}
+ </Form.Label>
+ </Form.Group>
+ <Form.Group className="mb-1 d-flex">
+ <Form.Check
+ checked={formData.staleConfigsOnly}
+ onChange={() =>
+ setFormData({
+ ...formData,
+ staleConfigsOnly: !get(formData, "staleConfigsOnly"),
+ })
+ }
+ className="custom-checkbox w-25 d-flex justify-content-end"
+ />
+ <Form.Label className="mt-1 ms-2">
+ {staleConfigsOnlyMessage()}
+ </Form.Label>
+ </Form.Group>
+ {suggestTurnOnMaintenanceMsg() && (
+ <Form.Group className="mb-3 d-flex">
+ <Form.Check
+ checked={formData.turnOnMm}
+ onChange={() =>
+ setFormData({
+ ...formData,
+ turnOnMm: !get(formData, "turnOnMm"),
+ })
+ }
+ className="custom-checkbox w-25 d-flex justify-content-end"
+ />
+ <Form.Label className="mt-1 ms-2">{turnOnMmMsg()}</Form.Label>
+ </Form.Group>
+ )}
+ </Form>
+ {errorsList.length > 0 && (
+ <Alert variant="danger">
+ <ul className="m-0">
+ {errorsList.map((error: string) => {
+ return (
+ <li key={error} className="text-danger">
+ {error}
+ </li>
+ );
+ })}
+ </ul>
+ </Alert>
+ )}
+ {warningsList.length > 0 && (
+ <Alert variant="warning" className="mt-2">
+ <ul className="m-0">
+ {warningsList.map((warning: string) => {
+ return <li key={warning}>{warning}</li>;
+ })}
+ </ul>
+ </Alert>
+ )}
+ </div>
+ );
+ };
+
+ return (
+ <Modal
+ isOpen={isOpen}
+ onClose={onClose}
+ modalTitle={
+ translateWithVariables("rollingrestart.dialog.title", {
+ "0": hostComponentName,
+ }) as string
+ }
+ modalBody={getModalBody()}
+ successCallback={() => {
+ successCallback(
+ restartHostComponents(),
+ parseInt(formData.batchSize),
+ parseInt(formData.interBatchWaitTimeSeconds),
+ parseInt(formData.tolerateSize),
+ formData.turnOnMm
+ );
+ modalManager.hide();
+ }}
+ options={{
+ buttonSize: "sm" as "sm" | "lg" | undefined,
+ cancelableViaIcon: true,
+ cancelableViaBtn: true,
+ okButtonVariant: "primary",
+ okButtonDisabled: errorsList.length > 0,
+ okButtonText: translate("rollingrestart.dialog.primary") as string,
+ }}
+ />
+ );
+}
diff --git a/ambari-web/latest/src/screens/Hosts/utils.tsx
b/ambari-web/latest/src/screens/Hosts/utils.tsx
index 76c75f7b75..3dfca4f1bc 100644
--- a/ambari-web/latest/src/screens/Hosts/utils.tsx
+++ b/ambari-web/latest/src/screens/Hosts/utils.tsx
@@ -18,6 +18,7 @@
import { get } from "lodash";
import { ComponentType } from "./enums";
+import { translate, translateWithVariables } from "../../Utils/Utility";
export const sortBasedOnMasterSlave = (data: any, key: string) => {
const masterComponents = data.filter(
@@ -31,3 +32,39 @@ export const sortBasedOnMasterSlave = (data: any, key:
string) => {
);
return masterComponents.concat(slaveComponents).concat(clientComponents);
};
+
+export const pluralize = (name: string) => {
+ return name + "s";
+};
+
+export const validateInteger = (
+ str: string | number,
+ min?: number,
+ max?: number
+): string => {
+ if (typeof str === "number") {
+ str = str.toString();
+ }
+ if (str === "" || str.trim().length < 1) {
+ return translate("number.validate.empty") as string;
+ }
+ str = str.trim();
+ const number = parseInt(str);
+ if (isNaN(number)) {
+ return translate("number.validate.notValidNumber") as string;
+ }
+ if (str.length !== number.toString().length) {
+ return translate("number.validate.notValidNumber") as string;
+ }
+ if (min && number < min) {
+ return translateWithVariables("number.validate.lessThanMinimum", {
+ "0": min.toString(),
+ }) as string;
+ }
+ if (max && number > max) {
+ return translateWithVariables("number.validate.moreThanMaximum", {
+ "0": max.toString(),
+ }) as string;
+ }
+ return "";
+};
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]