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 11c7abfa92 AMBARI-26178: Ambari Admin React Implementation: List 
Remote Cluster (#3887)
11c7abfa92 is described below

commit 11c7abfa92df8ba94680f60653f38295b4c39c6a
Author: Sandeep  Kumar <[email protected]>
AuthorDate: Thu Nov 21 15:06:49 2024 +0530

    AMBARI-26178: Ambari Admin React Implementation: List Remote Cluster (#3887)
---
 .../src/__mocks__/mockRemoteCluster.ts             | 140 +++++++++++++++++++
 .../ui/ambari-admin/src/api/remoteCluster.ts       |  86 ++++++++++++
 .../ClusterManagement/RemoteClusters/Index.tsx     | 155 +++++++++++++++++++++
 .../ambari-admin/src/tests/RemoteClusters.test.tsx | 119 ++++++++++++++++
 4 files changed, 500 insertions(+)

diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockRemoteCluster.ts
 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockRemoteCluster.ts
new file mode 100644
index 0000000000..92047d340f
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockRemoteCluster.ts
@@ -0,0 +1,140 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export const mockData = [
+    {
+      ClusterInfo: {
+        cluster_id: 1,
+        name: "TestCluster1",
+        services: ["Service1", "Service2"],
+        url: "testClusterUrl1",
+        username: "testUser1",
+      },
+    },
+  ];
+  
+  export const mockClusterDataForEdit = {
+    ClusterInfo: {
+      name: "TestCluster1",
+      url: "http://cluster.example.com:8080/api/v1/clusters/name";,
+      services: ["service1, service2"],
+      username: "testUser",
+    },
+  };
+  
+  export const paginatedMockData = [
+    {
+      ClusterInfo: {
+        cluster_id: 1,
+        name: "TestCluster1",
+        services: ["Service1", "Service2"],
+        url: "testClusterUrl1",
+        username: "testUser1",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 2,
+        name: "TestCluster2",
+        services: ["Service3", "Service4"],
+        url: "testClusterUrl2",
+        username: "testUser2",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 3,
+        name: "TestCluster3",
+        services: ["Service5", "Service6"],
+        url: "testClusterUrl3",
+        username: "testUser3",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 4,
+        name: "TestCluster4",
+        services: ["Service7", "Service8"],
+        url: "testClusterUrl4",
+        username: "testUser4",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 5,
+        name: "TestCluster5",
+        services: ["Service9", "Service10"],
+        url: "testClusterUrl5",
+        username: "testUser5",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 6,
+        name: "TestCluster6",
+        services: ["Service11", "Service12"],
+        url: "testClusterUrl6",
+        username: "testUser6",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 7,
+        name: "TestCluster7",
+        services: ["Service13", "Service14"],
+        url: "testClusterUrl7",
+        username: "testUser7",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 8,
+        name: "TestCluster8",
+        services: ["Service15", "Service16"],
+        url: "testClusterUrl8",
+        username: "testUser8",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 9,
+        name: "TestCluster9",
+        services: ["Service17", "Service18"],
+        url: "testClusterUrl9",
+        username: "testUser9",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 10,
+        name: "TestCluster10",
+        services: ["Service19", "Service20"],
+        url: "testClusterUrl10",
+        username: "testUser10",
+      },
+    },
+    {
+      ClusterInfo: {
+        cluster_id: 11,
+        name: "TestCluster11",
+        services: ["Service21", "Service22"],
+        url: "testClusterUrl11",
+        username: "testUser11",
+      },
+    },
+  ];
+  
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/api/remoteCluster.ts 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/remoteCluster.ts
new file mode 100644
index 0000000000..63037d6266
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/remoteCluster.ts
@@ -0,0 +1,86 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { adminApi } from "./configs/axiosConfig";
+
+interface Cluster {
+    cluster_id?: number;
+    name?: string;
+    url?: string;
+    username?: string;
+    password?: string;
+}
+
+const RemoteClusterApi = {
+    getRemoteClusters: async function () {
+        const url = '/remoteclusters';
+        const response = await adminApi.request({
+          url: url,
+          method: "GET"
+        });
+        // Fetching details for each cluster
+        const clustersData = await Promise.all(
+          response.data.items.map(async (cluster: any) => {
+            const details = await 
this.getRemoteClusterByName(cluster.ClusterInfo.name);
+            return details;
+          })
+        );
+        return clustersData;
+    },
+    getRemoteClusterByName: async function(clusterName: string) {
+        const url = `/remoteclusters/${clusterName}`;
+        const response = await adminApi.request({
+          url: url,
+          method: "GET"
+        });
+        return response.data;
+    },
+    addRemoteCluster: async function (cluster: Cluster) {
+        const url = `/remoteclusters/${cluster.name}`;
+        const response = await adminApi.request({
+          url: url,
+          method: "POST",
+          headers: {
+            'Content-Type': 'application/json'
+          },
+          data: {ClusterInfo: cluster}
+        });
+        return response.data;
+    },
+    updateRemoteCluster: async function (clusterName: string, cluster: 
Cluster) {
+        const url = `/remoteclusters/${clusterName}`;
+        const response = await adminApi.request({
+          url: url,
+          method: "PUT",
+          headers: {
+            'Content-Type': 'application/json'
+          },
+          data: {ClusterInfo: cluster}
+        });
+        return response.data;
+    },
+    deregisterRemoteCluster: async function (clusterName: string) {
+        const url = `/remoteclusters/${clusterName}`;
+        const response = await adminApi.request({
+          url: url,
+          method: "DELETE"
+        });
+        return response.data;
+    }
+};
+
+export default RemoteClusterApi;
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/Index.tsx
 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/Index.tsx
new file mode 100644
index 0000000000..6c25f19c0c
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/ClusterManagement/RemoteClusters/Index.tsx
@@ -0,0 +1,155 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useContext, useEffect, useState } from "react";
+import { Link } from "react-router-dom";
+import { get } from "lodash";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faFilter } from "@fortawesome/free-solid-svg-icons";
+import Spinner from "../../../components/Spinner";
+import Table from "../../../components/Table";
+import DefaultButton from "../../../components/DefaultButton";
+import Paginator from "../../../components/Paginator";
+import usePagination from "../../../hooks/usePagination";
+import RemoteClusterApi from "../../../api/remoteCluster.ts";
+import ComboSearch from "../../../components/ComboSearch";
+import AppContent from "../../../context/AppContext";
+
+export default function RemoteClusters() {
+  const [remoteClusters, setRemoteClusters] = useState([]);
+  const [loading, setLoading] = useState(false);
+  const [showFilters, setShowFilters] = useState(false);
+  const [filteredRemoteClusters, setFilteredRemoteClusters] = useState<
+    unknown[] | ((prevState: never[]) => never[])
+  >([]);
+  const {
+    currentItems,
+    changePage,
+    currentPage,
+    maxPage,
+    itemsPerPage,
+    setItemsPerPage,
+  } = usePagination(filteredRemoteClusters);
+
+  const {
+    setSelectedOption
+  } = useContext(AppContent);
+
+  useEffect(() => {
+    setSelectedOption("Remote Clusters");
+    async function getRemoteClusters() {
+      setLoading(true);
+      const data: any = await RemoteClusterApi.getRemoteClusters();
+      setRemoteClusters(data);
+      setFilteredRemoteClusters(data);
+      setLoading(false);
+    }
+    getRemoteClusters();
+  }, []);
+
+  const columnViewList = [
+    {
+      header: "Cluster Name",
+      accessorKey: "ClusterInfo.name",
+      cell: (info: any) => {
+        return (
+          <div>
+            <Link
+              className="custom-link"
+              to={`/remoteClusters/${get(
+                info,
+                "row.original.ClusterInfo.name"
+              )}/edit`}
+            >
+              {get(info, "row.original.ClusterInfo.name")}
+            </Link>
+          </div>
+        );
+      },
+    },
+    {
+      header: "Services",
+      accessorKey: "ClusterInfo.services",
+      cell: (info: any) => {
+        const services = get(info, "row.original.ClusterInfo.services");
+        const uniqueServices = [...new Set(services)];
+        return uniqueServices.join(", ");
+      },
+    },
+  ];
+
+  if (loading) {
+    return <Spinner />;
+  }
+  return (
+    <>
+      <div className="d-flex justify-content-end pb-3">
+        <DefaultButton
+          onClick={() => {
+            setShowFilters(!showFilters);
+          }}
+          className="me-2"
+        >
+          <FontAwesomeIcon icon={faFilter} />
+        </DefaultButton>
+        <Link to={`/remoteClusters/create`}>
+          <DefaultButton>Register Remote cluster</DefaultButton>
+        </Link>
+      </div>
+      <div>
+        {showFilters ? (
+          <div className="d-flex">
+            <ComboSearch
+              fields={[
+                { label: "Cluster Name", value: "ClusterInfo.name" },
+                { label: "Services", value: "ClusterInfo.services" },
+              ]}
+              valueMappings={{
+                clusterName: "ClusterInfo.name",
+                services: "ClusterInfo.services",
+              }}
+              searchCallback={(
+                filteredData: React.SetStateAction<
+                  any[] | ((prevState: never[]) => never[])
+                >
+              ) => {
+                setFilteredRemoteClusters(filteredData);
+              }}
+              data={remoteClusters}
+            />
+          </div>
+        ) : null}
+
+        <div className="scrollable">
+          <Table
+            columns={columnViewList}
+            data={currentItems}
+            entityName="Remote Clusters"
+          />
+        </div>
+         <Paginator
+          currentPage={currentPage}
+          maxPage={maxPage}
+          changePage={changePage}
+          itemsPerPage={itemsPerPage}
+          setItemsPerPage={setItemsPerPage}
+          totalItems={filteredRemoteClusters.length}
+        />
+      </div>
+    </>
+  );
+}
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RemoteClusters.test.tsx
 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RemoteClusters.test.tsx
new file mode 100644
index 0000000000..7f83961137
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/RemoteClusters.test.tsx
@@ -0,0 +1,119 @@
+/**
+ * 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, waitFor, screen, fireEvent } from "@testing-library/react";
+import { createMemoryHistory } from "history";
+import { Router } from "react-router-dom";
+import "@testing-library/jest-dom/vitest";
+import RemoteClusters from "../screens/ClusterManagement/RemoteClusters/Index"
+import RemoteClusterApi from "../api/remoteCluster";
+import {
+  mockData,
+  paginatedMockData,
+} from "../__mocks__/mockRemoteCluster";
+import AppContent from "../context/AppContext";
+
+const mockClusterName = "testCluster";
+const mockContext = {
+  cluster: { cluster_name: mockClusterName },
+  setSelectedOption: () => "RemoteCluster",
+};
+
+const renderRemoteCluster = () => {
+  render(
+    <AppContent.Provider value={mockContext}>
+      <Router history={createMemoryHistory()}>
+        <RemoteClusters />
+      </Router>
+    </AppContent.Provider>
+  );
+};
+
+describe("RemoteClusters component", () => {
+  it("renders without crashing", () => {
+    RemoteClusterApi.getRemoteClusters = async () => mockData;
+    renderRemoteCluster();
+  });
+
+  it("shows loading spinner when data is being fetched.", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => [];
+    renderRemoteCluster();
+
+    const spinner = screen.getByTestId("admin-spinner");
+    expect(spinner).toBeInTheDocument();
+  });
+
+  it("display appropriate message when no cluster is rendered.", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => [];
+    renderRemoteCluster();
+    expect(screen.findByText(/No Remote Cluster to show/i));
+  });
+
+  it("renders correct number of items ", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => mockData;
+    renderRemoteCluster();
+
+    await waitFor(() => screen.getByText(/TestCluster/i));
+
+    const items = screen.getAllByRole("listitem");
+    expect(items).toHaveLength(mockData.length);
+  });
+
+  it("renders data for a specific item correctly", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => mockData;
+    renderRemoteCluster();
+
+    await waitFor(() => screen.getByText(/TestCluster/i));
+    expect(screen.getByText(/TestCluster1/i)).toBeInTheDocument();
+    expect(screen.getByText(/Service1/i)).toBeInTheDocument();
+    expect(screen.getByText(/Service2/i)).toBeInTheDocument();
+  });
+
+  it("renders the Register Remote cluster button and navigates to the create 
route on click", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => mockData;
+    const history = createMemoryHistory();
+    render(
+      <AppContent.Provider value={mockContext}>
+        <Router history={history}>
+          <RemoteClusters />
+        </Router>
+      </AppContent.Provider>
+    );
+    await waitFor(() => screen.getByText(/TestCluster1/i));
+    expect(screen.getByText(/TestCluster1/i)).toBeInTheDocument();
+
+    const registerButton = screen.getByRole("button", {
+      name: /Register Remote cluster/i,
+    });
+    expect(registerButton).toBeInTheDocument();
+
+    fireEvent.click(registerButton);
+    await waitFor(() =>
+      expect(history.location.pathname).toBe("/remoteClusters/create")
+    );
+  });
+
+  it("renders pagination when items are more than 10", async () => {
+    RemoteClusterApi.getRemoteClusters = async () => paginatedMockData;
+    renderRemoteCluster();
+    await waitFor(() => screen.getByText(/TestCluster2/i));
+
+    const pagination = screen.getByTestId("pagination");
+    expect(pagination).toBeInTheDocument();
+  });
+});


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to