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 19187af759 AMBARI-26170: Ambari Admin React Implementation: Edit 
Groups Page (#3882)
19187af759 is described below

commit 19187af759fce55550825689102edfaab9ba7195
Author: Himanshu Maurya <[email protected]>
AuthorDate: Tue Nov 19 11:47:10 2024 +0530

    AMBARI-26170: Ambari Admin React Implementation: Edit Groups Page (#3882)
---
 .../ui/ambari-admin/src/__mocks__/mockGroupData.ts |  64 +++
 .../ambari-admin/src/screens/Users/EditGroup.tsx   | 493 +++++++++++++++++++++
 .../ui/ambari-admin/src/tests/EditGroup.test.tsx   | 243 ++++++++++
 3 files changed, 800 insertions(+)

diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockGroupData.ts
 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockGroupData.ts
new file mode 100644
index 0000000000..ed76e1ad19
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/__mocks__/mockGroupData.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.
+ */
+export const groupData = {
+    "href": 
"http://host.example.com:8080/api/v1/groups/group5?fields=Groups,privileges/PrivilegeInfo/*,members/MemberInfo&_=1726662954266";,
+    "Groups": {
+        "group_name": "group5",
+        "group_type": "LOCAL",
+        "ldap_group": false
+    },
+    "privileges": [
+        {
+            "href": 
"http://host.example.com:8080/api/v1/groups/group5/privileges/1007";,
+            "PrivilegeInfo": {
+                "group_name": "group5",
+                "instance_name": "jukende",
+                "permission_label": "View User",
+                "permission_name": "VIEW.USER",
+                "principal_name": "group5",
+                "principal_type": "GROUP",
+                "privilege_id": 1007,
+                "type": "VIEW",
+                "version": "1.0.0",
+                "view_name": "FILES"
+            }
+        },
+        {
+            "href": 
"http://host.example.com:8080/api/v1/groups/group5/privileges/1068";,
+            "PrivilegeInfo": {
+                "cluster_name": "testCluster",
+                "group_name": "group5",
+                "permission_label": "Cluster User",
+                "permission_name": "CLUSTER.USER",
+                "principal_name": "group5",
+                "principal_type": "GROUP",
+                "privilege_id": 1068,
+                "type": "CLUSTER"
+            }
+        }
+    ],
+    "members": [
+        {
+            "href": 
"http://host.example.com:8080/api/v1/groups/group5/members/dsasd";,
+            "MemberInfo": {
+                "group_name": "group5",
+                "user_name": "dsasd"
+            }
+        }
+    ]
+}
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/EditGroup.tsx
 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/EditGroup.tsx
new file mode 100644
index 0000000000..ab1f0d1bc4
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/EditGroup.tsx
@@ -0,0 +1,493 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { useContext, useEffect, useRef, useState } from "react";
+import { Alert, Button, Form } from "react-bootstrap";
+import { useParams } from "react-router";
+import { Link, useHistory, Prompt } from "react-router-dom";
+import RoleBasedAccessControl from "./RoleBasedAccessControl";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+  faCheck,
+  faCloud,
+  faQuestionCircle,
+  faTh,
+  faTrashCan,
+} from "@fortawesome/free-solid-svg-icons";
+import Select from "react-select";
+import { startCase } from "lodash";
+import { permissionLabelToName, userAccessOptions } from "./constants";
+import { get } from "lodash";
+import {
+  GroupInfoType,
+  PermissionLabel,
+  SelectOptionType,
+  UserNamesType,
+} from "./types";
+import { DefaultAccess, PrincipalType, PrivilegeType } from "./enums";
+import { MembersDataType, PrivilegesDataType } from "../../api/types";
+import toast from "react-hot-toast";
+import DefaultButton from "../../components/DefaultButton";
+import UserGroupApi from "../../api/userGroupApi";
+import PrivilegeApi from "../../api/privilegeApi";
+import AppContent from "../../context/AppContext";
+import Spinner from "../../components/Spinner";
+import Table from "../../components/Table";
+import WarningModal from "./WarningModal";
+import { constructLinkToEditInstance } from "../../screens/Users/EditUser"
+
+type ParamsType = {
+  groupName: string;
+};
+
+export default function EditGroup() {
+  const params: ParamsType = useParams();
+
+  const [showGroupAccessModal, setShowGroupAccessModal] = useState(false);
+
+  const [loading, setLoading] = useState(false);
+  const [groupInfo, setGroupInfo] = useState<GroupInfoType>(
+    {} as GroupInfoType
+  );
+  const [userNames, setUserNames] = useState<UserNamesType>(
+    {} as UserNamesType
+  );
+  const [groupMembers, setGroupMembers] = useState<SelectOptionType[]>([]);
+  const {
+    cluster: { cluster_name: clusterName },
+    setSelectedOption,
+  } = useContext(AppContent);
+  const [showUnsavedChangesWarning, setShowUnsavedChangesWarning] =
+    useState(false);
+  const [hasUnsavedChanges, setHasUnsavedChanges] = useState(true);
+  const [isNavigating, setIsNavigating] = useState(false);
+  const [nextLocation, setNextLocation] = useState();
+  const history = useHistory();
+
+  const previousMembers = useRef<string[]>([]);
+  const newMembers = useRef<string[]>([]);
+
+  useEffect(() => {
+    setSelectedOption("Groups");
+    async function getUserNames() {
+      setLoading(true);
+      const data: UserNamesType = await UserGroupApi.userNames();
+      setUserNames(data);
+    }
+    getUserNames();
+    getGroupData();
+  }, []);
+
+  useEffect(() => {
+    previousMembers.current = get(groupInfo, "members", []).map((member) =>
+      get(member, "MemberInfo.user_name")
+    );
+  }, [groupInfo]);
+
+  useEffect(() => {
+    newMembers.current = groupMembers.map((member) => member.value);
+    setHasUnsavedChanges(
+      JSON.stringify(previousMembers) !== JSON.stringify(newMembers)
+    );
+  }, [groupMembers]);
+
+  const userOptions = get(userNames, "items", []).map((user: any) => ({
+    value: get(user, "Users.user_name"),
+    label: get(user, "Users.user_name"),
+  }));
+
+  const columnsInGroupPrivilegesCluster = [
+    {
+      header: "Cluster",
+      cell: (info: any) => {
+        return (
+          <div>
+            <FontAwesomeIcon
+              icon={faCloud}
+              height={13}
+              width={13}
+              className="me-1"
+            />
+            <Link to={"/clusterInformation"} className="custom-link">
+              {get(info, "row.original.PrivilegeInfo.cluster_name")}
+            </Link>
+          </div>
+        );
+      },
+    },
+    {
+      header: "Cluster Role",
+      cell: (info: any) => {
+        return (
+          <div>{get(info, "row.original.PrivilegeInfo.permission_label")}</div>
+        );
+      },
+    },
+  ];
+
+  const columnsInGroupPrivilegesView = [
+    {
+      header: "View",
+      cell: (info: any) => {
+        return (
+          <div className="w-50">
+            <FontAwesomeIcon
+              icon={faTh}
+              height={13}
+              width={13}
+              className="me-1"
+            />
+            <Link
+              to={constructLinkToEditInstance(
+                get(info, "row.original.PrivilegeInfo.view_name"),
+                get(info, "row.original.PrivilegeInfo.version"),
+                get(info, "row.original.PrivilegeInfo.instance_name")
+              )}
+              className="custom-link"
+            >
+              {get(info, "row.original.PrivilegeInfo.instance_name")}
+            </Link>
+          </div>
+        );
+      },
+    },
+    {
+      header: "View Permissions",
+      cell: (info: any) => {
+        return (
+          <div className="d-flex justify-content-between">
+            <div className="d-flex align-items-center">
+              {startCase(
+                get(
+                  info,
+                  "row.original.PrivilegeInfo.permission_label",
+                  ""
+                ).toLowerCase()
+              )}
+            </div>
+            <Button
+              className="btn-wrapping-icon"
+              onClick={() =>
+                deleteViewPrivilege(
+                  get(info, "row.original.PrivilegeInfo.view_name"),
+                  get(info, "row.original.PrivilegeInfo.version"),
+                  get(info, "row.original.PrivilegeInfo.instance_name"),
+                  get(info, "row.original.PrivilegeInfo.privilege_id")
+                )
+              }
+            >
+              <FontAwesomeIcon
+                icon={faTrashCan}
+                data-testid={`remove-privilege-icon-${get(
+                  info,
+                  "row.original.PrivilegeInfo.instance_name"
+                )}`}
+              />
+            </Button>
+          </div>
+        );
+      },
+    },
+  ];
+
+  const getGroupData = async () => {
+    setLoading(true);
+    const data: GroupInfoType = await UserGroupApi.groupData(
+      params.groupName,
+      "Groups,privileges/PrivilegeInfo/*,members/MemberInfo"
+    );
+    setGroupInfo(data);
+    setGroupMembers(
+      get(data, "members", []).map((member) => ({
+        value: get(member, "MemberInfo.user_name"),
+        label: get(member, "MemberInfo.user_name"),
+      }))
+    );
+    setLoading(false);
+  };
+
+  const updateGroupDataMembers = async () => {
+    const membersData: MembersDataType[] = groupMembers.map((member) => ({
+      "MemberInfo/group_name": get(groupInfo, "Groups.group_name", ""),
+      "MemberInfo/user_name": member.value,
+    }));
+    await UserGroupApi.updateMembers(
+      get(groupInfo, "Groups.group_name", ""),
+      membersData
+    );
+    toast.success(
+      <div className="toast-message">
+        Local Members updated for {get(groupInfo, "Groups.group_name", "")}
+      </div>
+    );
+    getGroupData();
+  };
+
+  const updateGroupDataPrivileges = async (value: PermissionLabel) => {
+    if (value === DefaultAccess.NONE) {
+      const currentClusterPrivileges = get(groupInfo, "privileges", []).filter(
+        (privilige) =>
+          get(privilige, "PrivilegeInfo.type") === PrivilegeType.CLUSTER
+      );
+      const removePrivilegesPromises = currentClusterPrivileges.map(
+        (privilege: any) =>
+          PrivilegeApi.removeClusterPrivileges(
+            clusterName,
+            get(privilege, "PrivilegeInfo.privilege_id")
+          )
+      );
+      await Promise.all(removePrivilegesPromises);
+      toast.success(
+        <div className="toast-message">
+          {get(groupInfo, "Groups.group_name", "")}'s explicit privilege has
+          been changed to 'NONE'. Any privilege now seen for this user comes
+          through its Group(s).
+        </div>
+      );
+    } else {
+      const privilegesData: PrivilegesDataType[] = [
+        {
+          PrivilegeInfo: {
+            permission_name: permissionLabelToName[value],
+            principal_name: get(groupInfo, "Groups.group_name", ""),
+            principal_type: PrincipalType.GROUP,
+          },
+        },
+      ];
+      await PrivilegeApi.addClusterPrivileges(clusterName, privilegesData);
+      toast.success(
+        <div className="toast-message">
+          {get(groupInfo, "Groups.group_name", "")} changed to {value}
+        </div>
+      );
+    }
+    getGroupData();
+  };
+
+  const deleteViewPrivilege = async (
+    view_name: string,
+    version: string,
+    instance_name: string,
+    privilege_id: string
+  ) => {
+    await PrivilegeApi.removeViewPrivileges(
+      view_name,
+      version,
+      instance_name,
+      privilege_id
+    );
+    toast.success(
+      <div className="toast-message">
+        {instance_name} view privilege is removed for{" "}
+        {get(groupInfo, "Groups.group_name", "")}
+      </div>
+    );
+    getGroupData();
+  };
+
+  const handleBlockedNavigation = (nextLocation: any) => {
+    if (hasUnsavedChanges) {
+      setShowUnsavedChangesWarning(true);
+      setIsNavigating(true);
+      setNextLocation(nextLocation);
+      return false;
+    }
+    return true;
+  };
+
+  const handleWarningDiscard = () => {
+    if (nextLocation) {
+      setShowUnsavedChangesWarning(false);
+      setIsNavigating(false);
+      setHasUnsavedChanges(false);
+      setTimeout(() => {
+        history.push(get(nextLocation, "pathname"));
+      }, 0);
+    }
+  };
+
+  const handleWarningSave = () => {
+    updateGroupDataMembers();
+    setShowUnsavedChangesWarning(false);
+    setIsNavigating(false);
+  };
+
+  return (
+    <div>
+      <Prompt when={hasUnsavedChanges} message={handleBlockedNavigation} />
+      {isNavigating && showUnsavedChangesWarning ? (
+        <WarningModal
+          isOpen={showUnsavedChangesWarning}
+          onClose={() => setShowUnsavedChangesWarning(false)}
+          handleWarningDiscard={handleWarningDiscard}
+          handleWarningSave={handleWarningSave}
+        />
+      ) : null}
+      <div className="d-flex flex-wrap">
+        <Link to={"/userManagement?tab=groups"} className="custom-link" 
data-testid="groups-list-link">
+          <h4>Groups</h4>
+        </Link>
+        <h4 className="ms-2 make-all-grey">{`/ ${get(groupInfo, 
"Groups.group_name", "")}`}</h4>
+      </div>
+      <hr className="mb-4" />
+      {loading ? (
+        <Spinner />
+      ) : (
+        <Form className="d-flex flex-column">
+          <Form.Group className="d-flex mb-4">
+            <Form.Label className="width-15 mt-2">Type</Form.Label>
+            <Form.Control
+              value={startCase(
+                get(groupInfo, "Groups.group_type", "").toLowerCase()
+              )}
+              readOnly
+              plaintext
+              className="ps-4"
+            />
+          </Form.Group>
+          <Form.Group className="d-flex mb-4">
+            <Form.Label className="mt-2 width-15">Local Members</Form.Label>
+            <Select
+              isMulti
+              name="groups"
+              options={userOptions}
+              className="basic-multi-select w-75"
+              placeholder="Add Group"
+              value={groupMembers}
+              onChange={(e) => setGroupMembers(e as SelectOptionType[])}
+              isDisabled={
+                get(groupInfo, "Groups.group_type", "").toLowerCase() !==
+                "local"
+              }
+              aria-label="select-group"
+            ></Select>
+            <DefaultButton
+              onClick={() => {
+                if (
+                  get(groupInfo, "Groups.group_type", "").toLowerCase() ===
+                  "local" && JSON.stringify(previousMembers) !== 
JSON.stringify(newMembers)
+                ) {
+                  updateGroupDataMembers();
+                }
+              }}
+              className={
+                get(groupInfo, "Groups.group_type", "").toLowerCase() !==
+                "local" || JSON.stringify(previousMembers) === 
JSON.stringify(newMembers)
+                  ? "cursor-not-allowed opacity-50 ms-2"
+                  : "ms-2"
+              }
+              data-testid="save-user-btn"
+            >
+              <FontAwesomeIcon icon={faCheck} />
+            </DefaultButton>
+          </Form.Group>
+          <Form.Group className="d-flex mb-4">
+            <Form.Label className="mt-2 width-15">
+              Group Access{" "}
+              <FontAwesomeIcon
+                icon={faQuestionCircle}
+                onClick={() => setShowGroupAccessModal(true)}
+                data-testid="group-access-info-icon"
+              />
+              <RoleBasedAccessControl
+                isOpen={showGroupAccessModal}
+                onClose={() => setShowGroupAccessModal(false)}
+              />
+            </Form.Label>
+            <Form.Select
+              aria-label="Select"
+              className="w-25 custom-form-control"
+              onChange={(e) =>
+                updateGroupDataPrivileges(e.target.value as PermissionLabel)
+              }
+              value={get(
+                get(groupInfo, "privileges", []).filter(
+                  (privilige) =>
+                    get(privilige, "PrivilegeInfo.type") ===
+                    PrivilegeType.CLUSTER
+                ),
+                "[0].PrivilegeInfo.permission_label"
+              )}
+              data-testid="group-access-dropdown"
+            >
+              {userAccessOptions.map((accessOption, idx) => (
+                <option value={accessOption} key={idx}>
+                  {accessOption}
+                </option>
+              ))}
+            </Form.Select>
+          </Form.Group>
+          <Form.Group className="d-flex mb-4">
+            <Form.Label className="mt-2 width-15">Privileges</Form.Label>
+            {!get(groupInfo, "privileges", []).filter(
+              (privilige) =>
+                get(privilige, "PrivilegeInfo.type") === PrivilegeType.CLUSTER
+            )?.length &&
+            !get(groupInfo, "privileges", []).filter(
+              (privilige) =>
+                get(privilige, "PrivilegeInfo.type") === PrivilegeType.VIEW &&
+                get(privilige, "PrivilegeInfo.principal_type") ===
+                  PrincipalType.GROUP
+            )?.length ? (
+              <Alert className="w-75" variant="info">
+                This group does not have any privileges.
+              </Alert>
+            ) : (
+              <div className="w-75 scrollable">
+                <Table
+                  data={get(groupInfo, "privileges", []).filter(
+                    (privilige) =>
+                      get(privilige, "PrivilegeInfo.type") ===
+                      PrivilegeType.CLUSTER
+                  )}
+                  columns={columnsInGroupPrivilegesCluster}
+                />
+                {get(groupInfo, "privileges", []).filter(
+                  (privilige) =>
+                    get(privilige, "PrivilegeInfo.type") ===
+                    PrivilegeType.CLUSTER
+                )?.length ? null : (
+                  <div className="ps-2">No cluster privileges</div>
+                )}
+                <Table
+                  data={get(groupInfo, "privileges", []).filter(
+                    (privilige) =>
+                      get(privilige, "PrivilegeInfo.type") ===
+                        PrivilegeType.VIEW &&
+                      get(privilige, "PrivilegeInfo.principal_type") ===
+                        PrincipalType.GROUP
+                  )}
+                  columns={columnsInGroupPrivilegesView}
+                  className="mt-5"
+                />
+                {get(groupInfo, "privileges", []).filter(
+                  (privilige) =>
+                    get(privilige, "PrivilegeInfo.type") ===
+                      PrivilegeType.VIEW &&
+                    get(privilige, "PrivilegeInfo.principal_type") ===
+                      PrincipalType.GROUP
+                )?.length ? null : (
+                  <div className="ps-2">No view privileges</div>
+                )}
+              </div>
+            )}
+          </Form.Group>
+        </Form>
+      )}
+    </div>
+  );
+}
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/EditGroup.test.tsx 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/EditGroup.test.tsx
new file mode 100644
index 0000000000..164819c752
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/tests/EditGroup.test.tsx
@@ -0,0 +1,243 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import { render, screen, fireEvent, waitFor } from "@testing-library/react";
+import EditGroup from "../screens/Users/EditGroup";
+import { describe, it, beforeEach, expect, vi } from "vitest";
+import "@testing-library/jest-dom/vitest";
+import UserGroupApi from "../api/userGroupApi";
+import PrivilegeApi from "../api/privilegeApi";
+import { Router } from "react-router";
+import AppContent from "../context/AppContext";
+import { createMemoryHistory } from "history";
+import toast from "react-hot-toast";
+import { rbacData } from "../__mocks__/mockRbacData";
+import { userNames } from "../__mocks__/mockUserNames";
+import { groupData } from "../__mocks__/mockGroupData";
+
+describe("EditGroup component", () => {
+  const mockClusterName = "testCluster";
+  const mockContext = {
+    cluster: { cluster_name: mockClusterName },
+    setSelectedOption: vi.fn(),
+    selectedOption: "Groups",
+    rbacData: {},
+    setRbacData: () => vi.fn(),
+    permissionLabelList: [],
+    setPermissionLabelList: vi.fn(),
+  };
+
+  let mockToastSuccessMessage = "";
+  let mockToastErrorMessage = "";
+
+  toast.success = (message) => {
+    mockToastSuccessMessage = message as string;
+    return "";
+  };
+
+  toast.error = (message) => {
+    mockToastErrorMessage = message as string;
+    return "";
+  };
+
+  beforeEach(() => {
+    vi.clearAllMocks();
+    mockToastSuccessMessage = "";
+    mockToastErrorMessage = "";
+    UserGroupApi.getPermissions = async () => rbacData;
+    UserGroupApi.userNames = async () => userNames;
+    UserGroupApi.groupData = async () => groupData;
+  });
+
+  const renderComponent = () => {
+    render(
+      <AppContent.Provider value={mockContext}>
+        <Router history={createMemoryHistory()}>
+          <EditGroup />
+        </Router>
+      </AppContent.Provider>
+    );
+  };
+
+  it("renders EditGroup component without crashing", async () => {
+    renderComponent();
+    expect(screen.getByText(/Groups/i)).toBeInTheDocument();
+  });
+
+  it("handles loading state", async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByTestId("admin-spinner")).toBeInTheDocument();
+    });
+  });
+
+  it("fetches and displays group data", async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    expect(screen.getByText(/Local/i)).toBeInTheDocument();
+    expect(screen.getByText(/dsasd/i)).toBeInTheDocument();
+    expect(screen.getAllByText(/Cluster User/i)).toHaveLength(2);
+    expect(screen.getAllByText(/View User/i)).toHaveLength(1);
+    expect(screen.getByText(mockClusterName)).toBeInTheDocument();
+  });
+
+  it("updates group members", async () => {
+    UserGroupApi.updateMembers = async () => {
+      toast.success("Local Members updated for testGroup");
+      return { status: 200 };
+    };
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const groupSelect = screen.getByLabelText("select-group");
+
+    fireEvent.mouseDown(groupSelect);
+    fireEvent.click(screen.getByText("cdscs"));
+
+    const saveButton = screen.getByTestId("save-user-btn");
+    fireEvent.click(saveButton);
+
+    await waitFor(() => {
+      expect(mockToastSuccessMessage).toBe(
+        "Local Members updated for testGroup"
+      );
+    });
+  });
+
+  it("updates group privileges", async () => {
+    PrivilegeApi.addClusterPrivileges = async () => {
+      toast.success("group5 changed to Service Operator");
+      return { status: 200 };
+    };
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const privilegeSelect = screen.getByTestId("group-access-dropdown");
+    fireEvent.change(privilegeSelect, {
+      target: { value: "Service Operator" },
+    });
+
+    await waitFor(() => {
+      expect(mockToastSuccessMessage).toBe(
+        "group5 changed to Service Operator"
+      );
+    });
+  });
+
+  it("shows unsaved changes warning modal", async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const groupSelect = screen.getByLabelText("select-group");
+
+    fireEvent.mouseDown(groupSelect);
+    fireEvent.click(screen.getByText("cdscs"));
+
+    const groupsListButton = screen.getByTestId("groups-list-link");
+    fireEvent.click(groupsListButton);
+
+    await waitFor(() => {
+      expect(screen.getByTestId("warning-modal")).toBeInTheDocument();
+    });
+
+    const cancelButton = screen.getByText("Cancel");
+    fireEvent.click(cancelButton);
+
+    await waitFor(() => {
+      expect(screen.queryByTestId("warning-modal")).toBeNull();
+    });
+  });
+
+  it("renders RBAC modal for group access tooltip", async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const groupAccessTooltipIcon = 
screen.getByTestId("group-access-info-icon");
+    fireEvent.click(groupAccessTooltipIcon);
+
+    await waitFor(() => {
+      expect(
+        screen.getByTestId("role-based-access-control-modal")
+      ).toBeInTheDocument();
+    });
+  });
+
+  it("removes view privileges", async () => {
+    PrivilegeApi.removeViewPrivileges = async () => {
+      toast.success("View privileges removed successfully");
+      return { status: 200 };
+    };
+
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const removeViewButton = screen.getByTestId(
+      "remove-privilege-icon-jukende"
+    );
+    fireEvent.click(removeViewButton);
+
+    await waitFor(() => {
+      expect(mockToastSuccessMessage).toBe(
+        "View privileges removed successfully"
+      );
+    });
+  });
+
+  it("handles API errors gracefully", async () => {
+    UserGroupApi.groupData = async () => {
+      toast.error("Failed to fetch group data");
+      return { status: 400 };
+    };
+
+    renderComponent();
+
+    await waitFor(() => {
+      expect(mockToastErrorMessage).toBe("Failed to fetch group data");
+    });
+  });
+
+  it("disables Local Members by default unless changes are made and 
submitted", async () => {
+    renderComponent();
+
+    await waitFor(() => {
+      expect(screen.getByText(/group5/i)).toBeInTheDocument();
+    });
+
+    const saveButton = screen.getByTestId("save-user-btn");
+    expect(saveButton).toHaveClass("cursor-not-allowed opacity-50");
+  });
+});


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

Reply via email to