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 d5389b61da AMBARI-26180: Ambari Admin React Implementation: Register
Remote Cluster #3889
d5389b61da is described below
commit d5389b61dafda74d9060604cf391d639acc698a4
Author: Sandeep Kumar <[email protected]>
AuthorDate: Fri Nov 22 07:25:04 2024 +0530
AMBARI-26180: Ambari Admin React Implementation: Register Remote Cluster
#3889
---
.../RemoteClusters/RegisterRemoteCluster.tsx | 246 +++++++++++++++++++++
.../src/tests/RegisterRemoteCluster.test.tsx | 206 +++++++++++++++++
2 files changed, 452 insertions(+)
diff --git
a/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/RegisterRemoteCluster.tsx
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/RegisterRemoteCluster.tsx
new file mode 100644
index 0000000000..52243a36cb
--- /dev/null
+++
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/RegisterRemoteCluster.tsx
@@ -0,0 +1,246 @@
+/**
+ * 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 { Form, Button } from "react-bootstrap";
+import { useContext, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { useHistory } from "react-router-dom";
+import toast from "react-hot-toast";
+import DefaultButton from "../../../components/DefaultButton";
+import RemoteClusterApi from "../../../api/remoteCluster";
+import AppContent from "../../../context/AppContext";
+
+export default function RegisterRemoteCluster() {
+ const [cluster, setCluster] = useState({
+ name: "",
+ url: "",
+ username: "",
+ password: "",
+ });
+ const [validated, setValidated] = useState(false);
+ const [errorMessageClusterName, setErrorMessageClusterName] = useState("");
+ const [errorMessageUrl, setErrorMessageUrl] = useState("");
+ const history = useHistory();
+
+ const validateInputs = (name: string, value: string) => {
+ let urlPattern = new RegExp(
+ "^https?://[a-zA-Z0-9]+.example.com(:[0-9]{4})?(/.*)?$"
+ );
+ let clusterNamePattern = new RegExp("^[A-Za-z0-9]{1,80}$");
+
+ if (name === "url") {
+ if (value === "") {
+ setErrorMessageUrl("This field is required.");
+ } else if (!urlPattern.test(value)) {
+ setErrorMessageUrl("Must be a valid URL.");
+ } else {
+ setErrorMessageUrl("");
+ }
+ }
+
+ if (name === "name") {
+ if (value === "") {
+ setErrorMessageClusterName("This field is required.");
+ } else if (!clusterNamePattern.test(value)) {
+ setErrorMessageClusterName(
+ "Must not contain any special characters or spaces."
+ );
+ } else {
+ setErrorMessageClusterName("");
+ }
+ }
+ };
+ const { setSelectedOption } = useContext(AppContent);
+
+ useEffect(() => {setSelectedOption("Remote Clusters")}, []);
+
+ const handleInputChange = (e: any) => {
+ const { name, value } = e.target;
+ setCluster({ ...cluster, [name]: value });
+ validateInputs(name, value);
+ };
+
+ const handleSubmit = async (e: any) => {
+ e.preventDefault();
+ setValidated(true);
+
+ validateInputs("name", cluster.name);
+ validateInputs("url", cluster.url);
+
+ if (
+ !e.currentTarget.checkValidity() ||
+ errorMessageUrl ||
+ errorMessageClusterName
+ ) {
+ return;
+ }
+
+ if (e.currentTarget.checkValidity()) {
+ try {
+ await RemoteClusterApi.addRemoteCluster(cluster);
+ toast.success(
+ <div className="toast-message">
+ Cluster "{cluster.name}" registered successfully!
+ </div>
+ );
+ history.push(`/remoteClusters/${cluster.name}/edit`);
+ } catch (error) {
+ if (error instanceof Error)
+ toast.error(
+ <div className="toast-message">
+ Error while adding remote Cluster: {error.message}
+ </div>
+ );
+ else toast.error("Error while adding remote cluster");
+ }
+ }
+ };
+
+ return (
+ <>
+ <div className="d-flex justify-content-between align-items-center
border-bottom pb-3">
+ <div className="d-flex ">
+ <Link to={`/remoteClusters`} className="fs-lg text-decoration-none">
+ <h4 className="custom-link">Remote Clusters </h4>
+ </Link>
+ <h4 className="mx-2">/</h4>
+ <h4>Register</h4>
+ </div>
+ </div>
+
+ <Form
+ onSubmit={handleSubmit}
+ className="mt-4"
+ noValidate
+ validated={validated}
+ >
+ <Form.Group
+ controlId="formClusterName"
+ className="row align-items-center"
+ >
+ <Form.Label className="col-sm-2 text-center">
+ Cluster Name*
+ </Form.Label>
+ <div className="col-sm-10">
+ <Form.Control
+ className="rounded-1"
+ isInvalid={!!errorMessageClusterName}
+ type="text"
+ placeholder="Ambari Cluster Name"
+ name="name"
+ value={cluster.name}
+ pattern="^[A-Za-z0-9]{1,80}$"
+ maxLength={80}
+ onChange={handleInputChange}
+ required
+ />
+ <Form.Control.Feedback type="invalid">
+ {errorMessageClusterName}
+ </Form.Control.Feedback>
+ </div>
+ </Form.Group>
+
+ <Form.Group
+ controlId="formAmbariClusterUrl"
+ className="mt-3 row align-items-center"
+ >
+ <Form.Label className="col-sm-2 text-center">
+ Ambari Cluster URL*
+ </Form.Label>
+ <div className="col-sm-10">
+ <Form.Control
+ isInvalid={!!errorMessageUrl}
+ type="text"
+
placeholder="http://ambari.server:8080/api/v1/clusters/clusterName"
+ name="url"
+ value={cluster.url}
+
pattern="^https?:\/\/[a-zA-Z0-9]+\.example\.com(:[0-9]{4})?(\/.*)?$"
+ maxLength={80}
+ onChange={handleInputChange}
+ required
+ />
+ <Form.Control.Feedback type="invalid">
+ {errorMessageUrl}
+ </Form.Control.Feedback>
+ </div>
+ </Form.Group>
+
+ <Form.Group
+ controlId="formClusterUser"
+ className="mt-3 row align-items-center"
+ >
+ <Form.Label className="col-sm-2 text-center">
+ Cluster User*
+ </Form.Label>
+ <div className="col-sm-10">
+ <Form.Control
+ type="text"
+ placeholder="Cluster User"
+ name="username"
+ value={cluster.username}
+ onChange={handleInputChange}
+ required
+ />
+ <Form.Control.Feedback type="invalid">
+ This field is required.
+ </Form.Control.Feedback>
+ </div>
+ </Form.Group>
+
+ <Form.Group
+ controlId="formPassword"
+ className="mt-3 row align-items-center"
+ >
+ <Form.Label className="col-sm-2 text-center">Password*</Form.Label>
+ <div className="col-sm-10">
+ <Form.Control
+ type="password"
+ placeholder="Password"
+ name="password"
+ value={cluster.password}
+ onChange={handleInputChange}
+ required
+ />
+ <Form.Control.Feedback type="invalid">
+ This field is required.
+ </Form.Control.Feedback>
+ </div>
+ </Form.Group>
+
+ <div className="d-flex justify-content-end mt-4">
+ <Link className="text-decoration-none" to={`/remoteClusters`}>
+ <DefaultButton
+ variant="primary"
+ className="rounded-1 text-uppercase"
+ size="sm"
+ >
+ Cancel
+ </DefaultButton>
+ </Link>
+ <Button
+ variant="success"
+ className="ms-2 rounded-1 text-uppercase"
+ size="sm"
+ type="submit"
+ >
+ Save
+ </Button>
+ </div>
+ </Form>
+ </>
+ );
+}
diff --git
a/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RegisterRemoteCluster.test.tsx
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RegisterRemoteCluster.test.tsx
new file mode 100644
index 0000000000..2761864039
--- /dev/null
+++
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RegisterRemoteCluster.test.tsx
@@ -0,0 +1,206 @@
+/**
+ * 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 { describe, it, expect } from "vitest";
+import { render, screen, waitFor, fireEvent } from "@testing-library/react";
+import { createMemoryHistory } from "history";
+import { Router } from "react-router-dom";
+import "@testing-library/jest-dom/vitest";
+import RemoteClusterApi from "../api/remoteCluster";
+import RegisterRemoteCluster from
"../screens/ClusterManagement/RemoteClusters/RegisterRemoteCluster";
+import AppContent from "../context/AppContext";
+import toast from "react-hot-toast";
+
+const mockClusterName = "TestCluster1";
+const mockContext = {
+ cluster: { cluster_name: mockClusterName },
+ setSelectedOption: () => "RemoteCluster",
+};
+
+const renderRegisterRemoteCluster = () => {
+ render(
+ <AppContent.Provider value={mockContext}>
+ <Router history={createMemoryHistory()}>
+ <RegisterRemoteCluster />
+ </Router>
+ </AppContent.Provider>
+ );
+};
+
+let mockToastSuccessMessage: string;
+let mockToastErrorMessage: string;
+
+toast.success = (message) => {
+ mockToastSuccessMessage = message as string;
+ return "";
+};
+
+toast.error = (message) => {
+ mockToastErrorMessage = message as string;
+ return "";
+};
+
+describe("RegisterRemoteCluster component", () => {
+ it("renders without creshing", () => {
+ renderRegisterRemoteCluster();
+ });
+
+ it("render form for registering remote Cluster", () => {
+ renderRegisterRemoteCluster();
+ expect(getClusterName()).toBeInTheDocument();
+ expect(getClusterUrl()).toBeInTheDocument();
+ expect(getClusterUserName()).toBeInTheDocument();
+ expect(getPassword()).toBeInTheDocument();
+ });
+
+ it("should redirect to /remoteCluster on clicking cancel button", async ()
=> {
+ const history = createMemoryHistory();
+ render(
+ <AppContent.Provider value={mockContext}>
+ <Router history={history}>
+ <RegisterRemoteCluster />
+ </Router>
+ </AppContent.Provider>
+ );
+ expect(getClusterName()).toBeInTheDocument();
+ expect(getClusterUrl()).toBeInTheDocument();
+ expect(getClusterUserName()).toBeInTheDocument();
+ expect(getPassword()).toBeInTheDocument();
+
+ const cancelButton = screen.getByRole("button", { name: /cancel/i });
+ expect(cancelButton).toBeInTheDocument();
+
+ fireEvent.click(cancelButton);
+ await waitFor(() => {});
+
+ await waitFor(() => {
+ expect(history.location.pathname).toBe("/remoteClusters");
+ });
+ });
+
+ it("should display required errors when input fields are empty", async () =>
{
+ renderRegisterRemoteCluster();
+ fireEvent.click(getSaveButton());
+
+ await waitFor(async () => {
+ const errorMessages = await screen.findAllByText(/is required/i);
+ expect(errorMessages).toHaveLength(4);
+ });
+ });
+
+ it("should display error when cluster name includes special characters or
spaces", async () => {
+ renderRegisterRemoteCluster();
+ const clusterNameInput = getClusterName();
+ fireEvent.change(clusterNameInput, {target: {value: "invalid name@"}});
+ fireEvent.keyDown(clusterNameInput, { key: "Tab", code: "Tab" });
+ fireEvent.keyUp(clusterNameInput, { key: "Tab", code: "Tab" });
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("Must not contain any special characters or spaces.")
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("should display error when url is invalid", async () => {
+ renderRegisterRemoteCluster();
+ const clusterUrlInput = getClusterUrl();
+ fireEvent.change(clusterUrlInput, {target: {value: "invalid_url"}});
+ fireEvent.keyDown(clusterUrlInput, { key: "Tab", code: "Tab" });
+ fireEvent.keyUp(clusterUrlInput, { key: "Tab", code: "Tab" });
+
+ await waitFor(() => {
+ expect(screen.getByText("Must be a valid URL.")).toBeInTheDocument();
+ });
+ });
+
+ it("should submit form when all values are valid", async () => {
+ RemoteClusterApi.addRemoteCluster = async () => {
+ toast.success(`Cluster "${mockClusterName}" registered successfully`);
+ return { status: 200 };
+ };
+
+ renderRegisterRemoteCluster();
+ fireEvent.change(getClusterName(), { target: { value: "TestCluster1" } });
+ fireEvent.change(getClusterUrl(), {
+ target: {
+ value:
"http://clusterHost.example.com:8080/api/v1/clusters/clusterName",
+ },
+ });
+ fireEvent.change(getClusterUserName(), { target: { value: "admin" } });
+ fireEvent.change(getPassword(), { target: { value: "admin" } });
+ fireEvent.click(getSaveButton());
+
+ await waitFor(() => {
+ expect(mockToastSuccessMessage).not.toBeUndefined;
+ expect(mockToastSuccessMessage).toBe(
+ `Cluster "TestCluster1" registered successfully`
+ );
+ });
+ });
+
+ it("should display error when API call fails", async () => {
+ RemoteClusterApi.addRemoteCluster = async () => {
+ toast.error("Error while adding remote cluster");
+ return { status: 400 };
+ };
+
+ renderRegisterRemoteCluster();
+ fireEvent.change(getClusterName(), { target: { value: "TestCluster1" } });
+ fireEvent.change(getClusterUrl(), {
+ target: {
+ value:
"http://clusterHost.example.com:8080/api/v1/clusters/clusterName",
+ },
+ });
+ fireEvent.change(getClusterUserName(), { target: { value: "admin" } });
+ fireEvent.change(getPassword(), { target: { value: "admin" } });
+ fireEvent.click(getSaveButton());
+
+ await waitFor(() => {
+ expect(mockToastErrorMessage).not.toBeUndefined;
+ expect(mockToastErrorMessage).toBe("Error while adding remote cluster");
+ });
+ });
+});
+
+function getClusterName() {
+ return screen.getByRole("textbox", {
+ name: /cluster name/i,
+ });
+}
+
+function getClusterUrl() {
+ return screen.getByRole("textbox", {
+ name: /ambari cluster/i,
+ });
+}
+
+function getClusterUserName() {
+ return screen.getByRole("textbox", {
+ name: /cluster user/i,
+ });
+}
+
+function getPassword() {
+ return screen.getByLabelText(/password/i);
+}
+
+function getSaveButton() {
+ return screen.getByRole("button", {
+ name: /save/i,
+ });
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]