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 e14e9317ab AMBARI-26166: Ambari Admin React Implementation: List 
Users/Groups Pages (#3877)
e14e9317ab is described below

commit e14e9317abfac1de5a16702424c28bceb9061e2d
Author: Himanshu Maurya <[email protected]>
AuthorDate: Tue Nov 12 14:39:38 2024 +0530

    AMBARI-26166: Ambari Admin React Implementation: List Users/Groups Pages 
(#3877)
---
 .../main/resources/ui/ambari-admin/package.json    |   2 +
 .../resources/ui/ambari-admin/src/api/Utility.ts   |  74 +++
 .../src/{App.css => api/configs/axiosConfig.ts}    |  61 +--
 .../ui/ambari-admin/src/api/privilegeApi.ts        |  52 ++
 .../ui/ambari-admin/src/{main.tsx => api/types.ts} |  33 +-
 .../ui/ambari-admin/src/api/userGroupApi.ts        | 163 ++++++
 .../main/resources/ui/ambari-admin/src/index.css   |  85 ---
 .../main/resources/ui/ambari-admin/src/main.tsx    |   1 -
 .../ui/ambari-admin/src/router/RoutesList.tsx      |   3 +-
 .../src/{App.css => screens/Users/enums.ts}        |  47 +-
 .../ui/ambari-admin/src/screens/Users/index.tsx    | 588 +++++++++++++++++++++
 .../ui/ambari-admin/src/screens/Users/types.ts     | 116 ++++
 12 files changed, 1058 insertions(+), 167 deletions(-)

diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/package.json 
b/ambari-admin/src/main/resources/ui/ambari-admin/package.json
index 3b406b6ba1..8582b819de 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/package.json
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/package.json
@@ -14,6 +14,7 @@
     "@fortawesome/react-fontawesome": "^0.2.2",
     "@tanstack/react-table": "^8.20.5",
     "@types/lodash": "^4.17.12",
+    "axios": "^1.7.7",
     "bootstrap": "^5.3.3",
     "lodash": "^4.17.21",
     "path": "^0.12.7",
@@ -27,6 +28,7 @@
   },
   "devDependencies": {
     "@eslint/js": "^9.11.1",
+    "@types/axios": "^0.14.4",
     "@types/react": "^18.3.10",
     "@types/react-dom": "^18.3.0",
     "@types/react-router-dom": "^5.3.3",
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/api/Utility.ts 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/Utility.ts
new file mode 100644
index 0000000000..0af78cb149
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/Utility.ts
@@ -0,0 +1,74 @@
+/**
+ * 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 encryptData = (data: string): string => {
+    let encryptedValue = data;
+    if(!data || typeof data !== "string") {
+        console.error("Encryption needs string to be encrypted");
+    }
+    else{
+        encryptedValue = data.split('').map((char) => {
+            const asciiCode = char.charCodeAt(0);
+            if (asciiCode >= 32 && asciiCode <= 126) {
+                return String.fromCharCode(((asciiCode - 32 + 13) % 95) + 32);
+            } else {
+                return char;
+            }
+        }).join('');
+    }
+    return encryptedValue
+}
+export const getFromLocalStorage = (key: string): string => {
+    try {
+        const value = localStorage.getItem(key);
+        return value ? value : '';
+    } catch (error) {
+        throw new Error(`Error getting data from localStorage: ${error}`);
+    }
+}
+export const setInLocalStorage = (key: string, value: string): void => {
+    try {
+        localStorage.setItem(key, value);
+    } catch (error) {
+        throw new Error(`Error setting data in localStorage: ${error}`);
+    }
+}
+export const parseJSONData = (data: string): any => {
+    try {
+        return JSON.parse(data);
+    } catch (error) {
+        throw new Error(`Error parsing JSON data: ${error}`);
+    }
+}
+export const decryptData = (data: string): string => {
+    let encryptedValue = data;
+    let decryptedValue = data;
+    if(!data || typeof data !== "string"){
+        console.error("Decryption needs string to be encrypted");
+    }
+    else{
+        decryptedValue = encryptedValue.split('').map((char) => {
+            const asciiCode = char.charCodeAt(0);
+            if (asciiCode >= 32 && asciiCode <= 126) {
+                return String.fromCharCode(((asciiCode - 32 - 13 + 95) % 95) + 
32);
+            } else {
+                return char;
+            }
+        }).join('');
+    }
+    return decryptedValue
+}
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/App.css 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/configs/axiosConfig.ts
similarity index 53%
copy from ambari-admin/src/main/resources/ui/ambari-admin/src/App.css
copy to 
ambari-admin/src/main/resources/ui/ambari-admin/src/api/configs/axiosConfig.ts
index a5f7a42eaa..a9b46902e3 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/App.css
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/configs/axiosConfig.ts
@@ -15,45 +15,32 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#root {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
-}
 
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
-  filter: drop-shadow(0 0 2em #61dafbaa);
-}
+import axios from "axios";
+import { toast } from "react-hot-toast";
+import { get } from "lodash";
 
-@keyframes logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
-}
+const createAxiosInstance = (baseURL: string, headers = {}) => {
+  const instance = axios.create({
+    baseURL,
+    withCredentials: true,
+    headers: {
+      "Content-Type": "application/json",
+      ...headers,
+    },
+  });
 
-@media (prefers-reduced-motion: no-preference) {
-  a:nth-of-type(2) .logo {
-    animation: logo-spin infinite 20s linear;
-  }
-}
+  instance.interceptors.response.use(undefined, (error) => {
+    const responseMessage = get(error, "response.data.message", undefined);
+    if (responseMessage) {
+      toast.error(responseMessage);
+    } else {
+      toast.error("Something went wrong");
+    }
+    return Promise.reject(error);
+  });
 
-.card {
-  padding: 2em;
-}
+  return instance;
+};
 
-.read-the-docs {
-  color: #888;
-}
+export const adminApi = createAxiosInstance("/api/v1");
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/api/privilegeApi.ts 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/privilegeApi.ts
new file mode 100644
index 0000000000..de536ab99e
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/privilegeApi.ts
@@ -0,0 +1,52 @@
+/**
+ * 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";
+import { PrivilegesDataType } from "./types";
+
+const PrivilegeApi = {
+
+  // Privilege APIs
+
+  addClusterPrivileges: async function (clusterName: string, privilegesData: 
PrivilegesDataType[]) {
+    const url = `/clusters/${clusterName}/privileges`;
+    const response = await adminApi.request({
+      url: url,
+      method: "PUT",
+      data: privilegesData
+    });
+    return response.data;
+  },
+  removeClusterPrivileges: async function (clusterName: string, privilege_id: 
string) {
+    const url = 
`/clusters/${clusterName}/privileges?PrivilegeInfo/privilege_id.in(${privilege_id})`;
+    const response = await adminApi.request({
+      url: url,
+      method: "DELETE",
+    });
+    return response.data;
+  },
+  removeViewPrivileges: async function (view_name: string, version: string, 
instance_name: string, privilege_id: string) {
+    const url = 
`/views/${view_name}/versions/${version}/instances/${instance_name}/privileges/${privilege_id}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "DELETE",
+    });
+    return response.data;
+  },
+};
+
+export default PrivilegeApi;
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/types.ts
similarity index 59%
copy from ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx
copy to ambari-admin/src/main/resources/ui/ambari-admin/src/api/types.ts
index 82e10be7e9..27ec273653 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/types.ts
@@ -15,13 +15,28 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-import { StrictMode } from 'react'
-import { createRoot } from 'react-dom/client'
-import App from './App.tsx'
-import './index.css'
+interface GroupDataType {
+    "Groups/group_name": string;
+};
 
-createRoot(document.getElementById('root')!).render(
-  <StrictMode>
-    <App />
-  </StrictMode>,
-)
+interface UserDataType {
+    "Users/active"?: boolean;
+    "Users/admin"?: boolean;
+    "Users/password"?: string;
+    "Users/user_name"?: string;
+};
+
+interface MembersDataType {
+    "MemberInfo/group_name": string,
+    "MemberInfo/user_name": string,
+};
+
+interface PrivilegesDataType {
+    PrivilegeInfo: {
+        permission_name: string,
+        principal_name: string,
+        principal_type: string,
+    }
+};
+
+export type { GroupDataType, MembersDataType, PrivilegesDataType, UserDataType 
};
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/api/userGroupApi.ts 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/userGroupApi.ts
new file mode 100644
index 0000000000..83df650185
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/api/userGroupApi.ts
@@ -0,0 +1,163 @@
+/**
+ * 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";
+import { GroupDataType, MembersDataType, UserDataType } from "./types";
+
+const UserGroupApi = {
+
+  // Permission APIs
+
+  getPermissions: async function (fields: string) {
+    const url = 
`permissions?PermissionInfo/resource_name.in(CLUSTER,AMBARI)&fields=${fields}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+
+  // User APIs
+
+  userNames: async function () {
+    const url = `/users?Users/user_name.matches(.*.*)`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  usersList: async function (fields: string) {
+    const url = `/users?fields=${fields}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  userData: async function (
+    userName: string,
+    fields: string,
+  ) {
+    const url = `/users/${userName}?fields=${fields}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  addUser: async function (userData: UserDataType) {
+    const url = '/users';
+    const response = await adminApi.request({
+      url: url,
+      method: "POST",
+      data: userData
+    });
+    return response.data;
+  },
+  updateUser: async function (userName: string, userData: UserDataType) {
+    const url = `/users/${userName}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "PUT",
+      data: userData
+    });
+    return response.data;
+  },
+  removeUser: async function (userName: string) {
+    const url = `/users/${userName}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "DELETE",
+    });
+    return response.data;
+  },
+
+  // Group APIs
+
+  groupNames: async function () {
+    const url = `/groups?Groups/group_name.matches(.*.*)`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  groupsList: async function (fields: string) {
+    const url = `/groups?fields=${fields}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  groupData: async function (
+    groupName: string,
+    fields: string,
+  ) {
+    const url = `/groups/${groupName}?fields=${fields}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "GET",
+    });
+    return response.data;
+  },
+  addGroup: async function (groupData: GroupDataType[]) {
+    const url = '/groups';
+    const response = await adminApi.request({
+      url: url,
+      method: "POST",
+      data: groupData
+    });
+    return response.data;
+  },
+  addMember: async function (groupName: string, userName: string) {
+    const url = `/groups/${groupName}/members/${userName}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "POST",
+    });
+    return response.data;
+  },
+  removeMember: async function (groupName: string, userName: string) {
+    const url = `/groups/${groupName}/members/${userName}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "DELETE",
+    });
+    return response.data;
+  },
+  updateMembers: async function (groupName: string, membersData: 
MembersDataType[]) {
+    const url = `/groups/${groupName}/members`;
+    const response = await adminApi.request({
+      url: url,
+      method: "PUT",
+      data: membersData
+    });
+    return response.data;
+  },
+  removeGroup: async function (groupName: string) {
+    const url = `/groups/${groupName}`;
+    const response = await adminApi.request({
+      url: url,
+      method: "DELETE",
+    });
+    return response.data;
+  }
+};
+
+export default UserGroupApi;
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/index.css 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/index.css
deleted file mode 100644
index 68baa43f9b..0000000000
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/index.css
+++ /dev/null
@@ -1,85 +0,0 @@
-/**
- * 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.
- */
-:root {
-  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
-  line-height: 1.5;
-  font-weight: 400;
-
-  color-scheme: light dark;
-  color: rgba(255, 255, 255, 0.87);
-  background-color: #242424;
-
-  font-synthesis: none;
-  text-rendering: optimizeLegibility;
-  -webkit-font-smoothing: antialiased;
-  -moz-osx-font-smoothing: grayscale;
-}
-
-a {
-  font-weight: 500;
-  color: #646cff;
-  text-decoration: inherit;
-}
-a:hover {
-  color: #535bf2;
-}
-
-body {
-  margin: 0;
-  display: flex;
-  place-items: center;
-  min-width: 320px;
-  min-height: 100vh;
-}
-
-h1 {
-  font-size: 3.2em;
-  line-height: 1.1;
-}
-
-button {
-  border-radius: 8px;
-  border: 1px solid transparent;
-  padding: 0.6em 1.2em;
-  font-size: 1em;
-  font-weight: 500;
-  font-family: inherit;
-  background-color: #1a1a1a;
-  cursor: pointer;
-  transition: border-color 0.25s;
-}
-button:hover {
-  border-color: #646cff;
-}
-button:focus,
-button:focus-visible {
-  outline: 4px auto -webkit-focus-ring-color;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    color: #213547;
-    background-color: #ffffff;
-  }
-  a:hover {
-    color: #747bff;
-  }
-  button {
-    background-color: #f9f9f9;
-  }
-}
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx
index 82e10be7e9..7fd60714d2 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/main.tsx
@@ -18,7 +18,6 @@
 import { StrictMode } from 'react'
 import { createRoot } from 'react-dom/client'
 import App from './App.tsx'
-import './index.css'
 
 createRoot(document.getElementById('root')!).render(
   <StrictMode>
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/router/RoutesList.tsx 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/router/RoutesList.tsx
index 4f84a68f61..6c68f3a70c 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/router/RoutesList.tsx
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/router/RoutesList.tsx
@@ -16,6 +16,7 @@
  * limitations under the License.
  */
 import { Redirect } from "react-router-dom";
+import Users from "../screens/Users";
 import WIP from "../components/WIP";
 
 
@@ -78,7 +79,7 @@ export default [
   {
     path: "/userManagement",
     exact: true,
-    Element: () => <WIP />,
+    Element: () => <Users />,
     name: "UserManagement",
   },
   {
diff --git a/ambari-admin/src/main/resources/ui/ambari-admin/src/App.css 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/enums.ts
similarity index 57%
rename from ambari-admin/src/main/resources/ui/ambari-admin/src/App.css
rename to 
ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/enums.ts
index a5f7a42eaa..af198ccd15 100644
--- a/ambari-admin/src/main/resources/ui/ambari-admin/src/App.css
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/enums.ts
@@ -15,45 +15,24 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-#root {
-  max-width: 1280px;
-  margin: 0 auto;
-  padding: 2rem;
-  text-align: center;
+enum PrivilegeType {
+    CLUSTER = "CLUSTER",
+    VIEW = "VIEW",
+    AMBARI = "AMBARI",
 }
 
-.logo {
-  height: 6em;
-  padding: 1.5em;
-  will-change: filter;
-  transition: filter 300ms;
-}
-.logo:hover {
-  filter: drop-shadow(0 0 2em #646cffaa);
-}
-.logo.react:hover {
-  filter: drop-shadow(0 0 2em #61dafbaa);
+enum PrincipalType {
+    USER = "USER",
+    GROUP = "GROUP",
+    ROLE = "ROLE",
 }
 
-@keyframes logo-spin {
-  from {
-    transform: rotate(0deg);
-  }
-  to {
-    transform: rotate(360deg);
-  }
+enum PermissionNameType {
+    VIEW_USER = "VIEW.USER",
 }
 
-@media (prefers-reduced-motion: no-preference) {
-  a:nth-of-type(2) .logo {
-    animation: logo-spin infinite 20s linear;
-  }
+enum DefaultAccess {
+    NONE = "None",
 }
 
-.card {
-  padding: 2em;
-}
-
-.read-the-docs {
-  color: #888;
-}
+export { PrivilegeType, PrincipalType, DefaultAccess, PermissionNameType };
\ No newline at end of file
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/index.tsx 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/index.tsx
new file mode 100644
index 0000000000..e31316d9d0
--- /dev/null
+++ 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/index.tsx
@@ -0,0 +1,588 @@
+/**
+ * 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/no-explicit-any */
+
+import { Button, Col, Nav, Row, Tab } from "react-bootstrap";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import {
+  faFilter,
+  faPencil,
+  faTrashCan,
+} from "@fortawesome/free-solid-svg-icons";
+import DefaultButton from "../../components/DefaultButton";
+import { useContext, useEffect, useState } from "react";
+import { Link, useHistory, useLocation } from "react-router-dom";
+import {
+  GroupInfoType,
+  GroupsListType,
+  UserInfoType,
+  UsersListType,
+} from "./types";
+import AppContent from "../../context/AppContext";
+import { get, set, startCase } from "lodash";
+import { PrivilegeType } from "./enums";
+import toast from "react-hot-toast";
+import UserGroupApi from "../../api/userGroupApi";
+import PrivilegeApi from "../../api/privilegeApi";
+import ConfirmationModal from "../../components/ConfirmationModal";
+import Spinner from "../../components/Spinner";
+import usePagination from "../../hooks/usePagination";
+import Paginator from "../../components/Paginator";
+import ComboSearch from "../../components/ComboSearch";
+import Table from "../../components/Table";
+import {
+  decryptData,
+  getFromLocalStorage,
+  parseJSONData,
+} from "../../api/Utility";
+
+export default function Users() {
+  const [currentLoggedInUser, setCurrentLoggedInUser] = useState("");
+  const [showAddUserModal, setShowAddUserModal] = useState(false);
+  const [showAddGroupModal, setShowAddGroupModal] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [usersList, setUsersList] = useState<UsersListType | null>(null);
+  const [groupsList, setGroupsList] = useState<GroupsListType | null>(null);
+  const {
+    cluster: { cluster_name: clusterName },
+    setSelectedOption,
+  } = useContext(AppContent);
+  const history = useHistory();
+  const location = useLocation();
+  const searchParams = new URLSearchParams(location.search);
+  const [activeTab, setActiveTab] = useState(
+    searchParams.get("tab") || "users"
+  );
+  const [showDeleteGroupModal, setShowDeleteGroupModal] = useState(false);
+  const [groupToDelete, setGroupToDelete] = useState("");
+  const [showDeleteUserModal, setShowDeleteUserModal] = useState(false);
+  const [userToDelete, setUserToDelete] = useState("");
+  const [showUserFilters, setShowUserFilters] = useState(false);
+  const [filteredUsers, setFilteredUsers] = useState<UserInfoType[]>([]);
+  const [showGroupFilters, setShowGroupFilters] = useState(false);
+  const [filteredGroups, setFilteredGroups] = useState<GroupInfoType[]>([]);
+
+  const {
+    currentItems: currentItemsUsers,
+    changePage: changePageUsers,
+    currentPage: currentPageUsers,
+    maxPage: maxPageUsers,
+    itemsPerPage: usersPerPage,
+    setItemsPerPage: setUsersPerPage,
+  } = usePagination(filteredUsers);
+  const {
+    currentItems: currentItemsGroups,
+    changePage: changePageGroups,
+    currentPage: currentPageGroups,
+    maxPage: maxPageGroups,
+    itemsPerPage: groupsPerPage,
+    setItemsPerPage: setGroupsPerPage,
+  } = usePagination(filteredGroups);
+
+  useEffect(() => {
+    let ambariKey = getFromLocalStorage("ambari");
+    if (ambariKey) {
+      let parsedAmbariKey = parseJSONData(decryptData(ambariKey));
+      setCurrentLoggedInUser(get(parsedAmbariKey, "app.loginName", ""));
+    }
+  }, []);
+
+  useEffect(() => {
+    if (activeTab === "users") {
+      setSelectedOption("Users");
+      getUsersList();
+    } else if (activeTab === "groups") {
+      setSelectedOption("Users");
+      getGroupsList();
+    }
+    searchParams.set("tab", activeTab);
+    history.push({
+      pathname: location.pathname,
+      search: searchParams.toString(),
+    });
+  }, [activeTab]);
+
+  useEffect(() => {
+    const searchParams = new URLSearchParams(location.search);
+    const tab = searchParams.get("tab");
+    if (tab) {
+      setActiveTab(tab);
+    }
+  }, [location]);
+
+  const columnsInUserList = [
+    {
+      header: "Username",
+      accessorKey: "Users.user_name",
+      id: "user_name",
+      width: "30%",
+    },
+    {
+      header: "Role",
+      accessorKey: "Users.user_role",
+      id: "user_role",
+      width: "15%",
+    },
+    {
+      header: "Status",
+      accessorKey: "active",
+      id: "active",
+      width: "10%",
+      cell: (info: any) => {
+        return info.row.original.Users.active === true ? "Active" : "Inactive";
+      },
+    },
+    {
+      header: "Type",
+      accessorKey: "Users.user_type",
+      id: "user_type",
+      width: "10%",
+    },
+    {
+      header: "Group",
+      accessorKey: "groups",
+      id: "groups",
+      width: "30%",
+      cell: (info: any) => {
+        return get(info, "row.original.Users.groups")?.length
+          ? get(info, "row.original.Users.groups", [])
+              ?.map((group: any) => group)
+              .join(" ")
+          : "-";
+      },
+    },
+    {
+      header: "Actions",
+      width: "5%",
+      cell: (info: any) => {
+        return (
+          <div className="d-flex">
+            <Link
+              to={`/users/${get(info, "row.original.Users.user_name")}/edit`}
+            >
+              <FontAwesomeIcon
+                icon={faPencil}
+                className="me-3"
+                style={{ cursor: "pointer" }}
+              />
+            </Link>
+            <Button
+              className={
+                get(info, "row.original.Users.user_name") ===
+                  currentLoggedInUser ||
+                get(info, "row.original.Users.user_type").toLowerCase() !==
+                  "local"
+                  ? "btn-wrapping-icon opacity-25 cursor-not-allowed"
+                  : "btn-wrapping-icon"
+              }
+              onClick={() => {
+                if (
+                  get(info, "row.original.Users.user_name") !==
+                    currentLoggedInUser &&
+                  get(info, "row.original.Users.user_type").toLowerCase() ===
+                    "local"
+                ) {
+                  setUserToDelete(get(info, "row.original.Users.user_name"));
+                  setShowDeleteUserModal(true);
+                }
+              }}
+              data-testid={`delete-user-${get(
+                info,
+                "row.original.Users.user_name"
+              )}-btn`}
+            >
+              <FontAwesomeIcon icon={faTrashCan} />
+            </Button>
+          </div>
+        );
+      },
+    },
+  ];
+
+  const columnsInGroupList = [
+    {
+      header: "Group name",
+      accessorKey: "Groups.group_name",
+      id: "group_name",
+      width: "65%",
+    },
+    {
+      header: "Type",
+      accessorKey: "Groups.group_type",
+      id: "group_type",
+      width: "15%",
+    },
+    {
+      header: "Members",
+      width: "15%",
+      cell: (info: any) => {
+        return get(info, "row.original.members")?.length
+          ? get(info, "row.original.members")?.length.toString() + " members"
+          : "0 members";
+      },
+    },
+    {
+      header: "Actions",
+      width: "5%",
+      cell: (info: any) => {
+        return (
+          <div className="d-flex">
+            <Link
+              to={`/groups/${get(info, 
"row.original.Groups.group_name")}/edit`}
+            >
+              <FontAwesomeIcon
+                icon={faPencil}
+                className="me-3"
+                style={{ cursor: "pointer" }}
+              />
+            </Link>
+            <Button
+              className={
+                get(info, "row.original.Groups.group_type").toLowerCase() !==
+                "local"
+                  ? "btn-wrapping-icon opacity-25 cursor-not-allowed"
+                  : "btn-wrapping-icon"
+              }
+              onClick={() => {
+                if (
+                  get(info, "row.original.Groups.group_type").toLowerCase() ===
+                  "local"
+                ) {
+                  setGroupToDelete(get(info, 
"row.original.Groups.group_name"));
+                  setShowDeleteGroupModal(true);
+                }
+              }}
+              data-testid={`delete-group-${get(
+                info,
+                "row.original.Groups.group_name"
+              )}-btn`}
+            >
+              <FontAwesomeIcon icon={faTrashCan} />
+            </Button>
+          </div>
+        );
+      },
+    },
+  ];
+
+  async function getUsersList() {
+    setLoading(true);
+    const data: any = await UserGroupApi.usersList("Users/*,privileges/*");
+    get(data, "items", []).forEach((item: any) => {
+      set(
+        item,
+        "Users.user_type",
+        startCase(get(item, "Users.user_type", "").toLowerCase())
+      );
+      const ambariAdminPrivileges = get(
+        get(item, "privileges", []).filter(
+          (privilege: any) =>
+            get(privilege, "PrivilegeInfo.type") === PrivilegeType.AMBARI
+        ),
+        "[0].PrivilegeInfo.permission_label",
+        ""
+      );
+      const clusterUserPrivileges = get(
+        get(item, "privileges", []).filter(
+          (privilege: any) =>
+            get(privilege, "PrivilegeInfo.type") === PrivilegeType.CLUSTER
+        ),
+        "[0].PrivilegeInfo.permission_label",
+        ""
+      );
+      if (ambariAdminPrivileges !== "") {
+        set(item, "Users.user_role", ambariAdminPrivileges);
+      } else {
+        set(item, "Users.user_role", clusterUserPrivileges);
+      }
+      if (get(item, "Users.active")) {
+        set(item, "Users.user_status", "Active");
+      } else {
+        set(item, "Users.user_status", "Inactive");
+      }
+    });
+    setUsersList(data);
+    setFilteredUsers(get(data, "items", []));
+    setLoading(false);
+  }
+
+  async function getGroupsList() {
+    setLoading(true);
+    const data: any = await UserGroupApi.groupsList("*");
+    get(data, "items", []).forEach((item: any) => {
+      set(
+        item,
+        "Groups.group_type",
+        startCase(get(item, "Groups.group_type", "").toLowerCase())
+      );
+    });
+    setGroupsList(data);
+    setFilteredGroups(get(data, "items", []));
+    setLoading(false);
+  }
+
+  const removePrivileges = async (privileges: any) => {
+    get(privileges, "privileges", []).forEach(async (privilege: any) => {
+      if (get(privilege, "PrivilegeInfo.type") === PrivilegeType.CLUSTER) {
+        await PrivilegeApi.removeClusterPrivileges(
+          clusterName,
+          get(privilege, "PrivilegeInfo.privilege_id")
+        );
+      } else if (get(privilege, "PrivilegeInfo.type") === PrivilegeType.VIEW) {
+        await PrivilegeApi.removeViewPrivileges(
+          get(privilege, "PrivilegeInfo.view_name"),
+          get(privilege, "PrivilegeInfo.version"),
+          get(privilege, "PrivilegeInfo.instance_name"),
+          get(privilege, "PrivilegeInfo.privilege_id")
+        );
+      }
+    });
+  };
+
+  const deleteUser = async (userName: string) => {
+    setShowDeleteUserModal(false);
+    const privileges = await UserGroupApi.userData(
+      userName,
+      "privileges/PrivilegeInfo/*"
+    );
+    await UserGroupApi.removeUser(userName);
+    toast.success(
+      <div className="toast-message">{userName} deleted successfully.</div>
+    );
+    await removePrivileges(privileges);
+    getUsersList();
+  };
+
+  const deleteGroup = async (groupName: string) => {
+    setShowDeleteGroupModal(false);
+    const privileges = await UserGroupApi.groupData(
+      groupName,
+      "privileges/PrivilegeInfo/*"
+    );
+    await UserGroupApi.removeGroup(groupName);
+    toast.success(
+      <div className="toast-message">{groupName} deleted successfully.</div>
+    );
+    await removePrivileges(privileges);
+    getGroupsList();
+  };
+
+  return (
+    <div className="make-all-grey">
+      {showDeleteUserModal ? (
+        <ConfirmationModal
+          isOpen={showDeleteUserModal}
+          onClose={() => setShowDeleteUserModal(false)}
+          modalTitle={"Delete User"}
+          modalBody={`Are you sure you want to delete user "${userToDelete}"?`}
+          successCallback={() => deleteUser(userToDelete)}
+          buttonVariant="danger"
+        />
+      ) : null}
+      {showDeleteGroupModal ? (
+        <ConfirmationModal
+          isOpen={showDeleteGroupModal}
+          onClose={() => setShowDeleteGroupModal(false)}
+          modalTitle={"Delete Group"}
+          modalBody={`Are you sure you want to delete group 
"${groupToDelete}"?`}
+          successCallback={() => deleteGroup(groupToDelete)}
+          buttonVariant="danger"
+        />
+      ) : null}
+      <Tab.Container activeKey={activeTab}>
+        <Row>
+          <Col>
+            <Nav
+              variant="underline"
+              onSelect={(selectedKey) => {
+                if (selectedKey && activeTab !== selectedKey) {
+                  setActiveTab(selectedKey);
+                }
+              }}
+            >
+              <Nav.Item>
+                <Nav.Link
+                  eventKey="users"
+                  className={
+                    activeTab === "users"
+                      ? "tab-border active p-2"
+                      : "tab-border p-2"
+                  }
+                  data-testid="users-tab"
+                >
+                  USERS
+                </Nav.Link>
+              </Nav.Item>
+              <Nav.Item>
+                <Nav.Link
+                  eventKey="groups"
+                  className={
+                    activeTab === "groups"
+                      ? "tab-border active p-2"
+                      : "tab-border p-2"
+                  }
+                  data-testid="groups-tab"
+                >
+                  GROUPS
+                </Nav.Link>
+              </Nav.Item>
+            </Nav>
+          </Col>
+          <Col className="d-flex justify-content-end">
+            <Tab.Content>
+              <Tab.Pane eventKey="users">
+                <DefaultButton
+                  className="me-2"
+                  onClick={() => setShowUserFilters(!showUserFilters)}
+                  data-testid="filter-users-btn"
+                >
+                  <FontAwesomeIcon icon={faFilter} />
+                </DefaultButton>
+                <DefaultButton
+                  onClick={() => setShowAddUserModal(true)}
+                  data-testid="add-users-btn"
+                >
+                  ADD USERS
+                </DefaultButton>
+              </Tab.Pane>
+              <Tab.Pane eventKey="groups">
+                <DefaultButton
+                  className="me-2"
+                  onClick={() => setShowGroupFilters(!showGroupFilters)}
+                  data-testid="filter-groups-btn"
+                >
+                  <FontAwesomeIcon icon={faFilter} />
+                </DefaultButton>
+                <DefaultButton
+                  onClick={() => setShowAddGroupModal(true)}
+                  data-testid="add-groups-btn"
+                >
+                  ADD GROUPS
+                </DefaultButton>
+              </Tab.Pane>
+            </Tab.Content>
+          </Col>
+        </Row>
+        {loading ? (
+          <Spinner />
+        ) : (
+          <Row className="scrollable">
+            <Tab.Content className="mt-3">
+              <Tab.Pane eventKey="users" data-testid="users-list-container">
+                {showUserFilters ? (
+                  <div className="d-flex">
+                    <ComboSearch
+                      fields={[
+                        { label: "Username", value: "Users.user_name" },
+                        { label: "Role", value: "Users.user_role" },
+                        { label: "Status", value: "Users.user_status" },
+                        { label: "Type", value: "Users.user_type" },
+                        { label: "Group", value: "Users.groups" },
+                      ]}
+                      valueMappings={{
+                        username: "Users.user_name",
+                        role: "permission_label",
+                        status: "Users.active",
+                        type: "Users.user_type",
+                        group: "Users.groups",
+                      }}
+                      searchCallback={(filteredData: UserInfoType[]) => {
+                        setFilteredUsers(filteredData);
+                      }}
+                      data={usersList?.items || []}
+                    />
+                  </div>
+                ) : null}
+                <Table
+                  striped={true}
+                  hover={true}
+                  columns={columnsInUserList}
+                  data={currentItemsUsers}
+                />
+                {usersList && usersList.items.length === 0 ? (
+                  <div
+                    className="d-flex justify-content-center mt-4"
+                    data-testid="empty-user-list"
+                  >
+                    NO USERS TO DISPLAY.
+                  </div>
+                ) : null}
+              </Tab.Pane>
+              <Tab.Pane eventKey="groups" data-testid="groups-list-container">
+                {showGroupFilters ? (
+                  <div className="d-flex">
+                    <ComboSearch
+                      fields={[
+                        { label: "Group name", value: "Groups.group_name" },
+                        { label: "Type", value: "Groups.group_type" },
+                      ]}
+                      valueMappings={{
+                        groupname: "Groups.group_name",
+                        type: "Groups.group_type",
+                      }}
+                      searchCallback={(filteredData: GroupInfoType[]) => {
+                        setFilteredGroups(filteredData);
+                      }}
+                      data={groupsList?.items || []}
+                    />
+                  </div>
+                ) : null}
+                <Table
+                  striped={true}
+                  hover={true}
+                  columns={columnsInGroupList}
+                  data={currentItemsGroups}
+                />
+                {groupsList && groupsList.items.length === 0 ? (
+                  <div
+                    className="d-flex justify-content-center mt-4"
+                    data-testid="empty-group-list"
+                  >
+                    NO GROUPS TO DISPLAY.
+                  </div>
+                ) : null}
+              </Tab.Pane>
+            </Tab.Content>
+          </Row>
+        )}
+      </Tab.Container>
+      <div>
+        {activeTab === "users" ? (
+          <Paginator
+            currentPage={currentPageUsers}
+            maxPage={maxPageUsers}
+            changePage={changePageUsers}
+            itemsPerPage={usersPerPage}
+            setItemsPerPage={setUsersPerPage}
+            totalItems={filteredUsers.length}
+          />
+        ) : (
+          <Paginator
+            currentPage={currentPageGroups}
+            maxPage={maxPageGroups}
+            changePage={changePageGroups}
+            itemsPerPage={groupsPerPage}
+            setItemsPerPage={setGroupsPerPage}
+            totalItems={filteredGroups.length}
+          />
+        )}
+      </div>
+    </div>
+  );
+}
diff --git 
a/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/types.ts 
b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/types.ts
new file mode 100644
index 0000000000..6df358e6f0
--- /dev/null
+++ b/ambari-admin/src/main/resources/ui/ambari-admin/src/screens/Users/types.ts
@@ -0,0 +1,116 @@
+/**
+ * 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.
+ */
+interface PrivilegeInfoType {
+    permission_label: string;
+    permission_name: string;
+    principal_name: string;
+    principal_type: string;
+    privilege_id: number;
+    type: string;
+    user_name: string;
+    [key: string]: string | number;
+};
+
+interface PrivilegesType {
+    href: string;
+    PrivilegeInfo: PrivilegeInfoType;
+};
+
+interface UserInfoType {
+    href: string;
+    Users: {
+        active: boolean;
+        admin: boolean;
+        consecutive_failures: number;
+        created: number;
+        display_name: string;
+        groups: string[];
+        ldap_user: boolean;
+        local_user_name: string;
+        user_name: string;
+        user_type: string;
+    };
+    privileges: PrivilegesType[];
+};
+
+interface GroupInfoType {
+    href: string;
+    Groups: {
+        group_name: string;
+        group_type: string;
+        ldap_group: boolean;
+    };
+    privileges: {
+        href: string;
+        PrivilegeInfo: {
+            group_name: string;
+            privilege_id: number;
+        };
+    }[];
+    members: {
+        href: string;
+        MemberInfo: {
+            group_name: string;
+            user_name: string;
+        };
+    }[];
+};
+
+interface GroupNamesType {
+    href: string;
+    items: {
+        href: string;
+        Groups: {
+            group_name: string;
+        };
+    }[];
+};
+
+interface UserNamesType {
+    href: string;
+    itemTotal: string;
+    items: {
+        href: string;
+        Users: {
+            user_name: string;
+        };
+    }[];
+}
+
+interface UsersListType {
+    href: string;
+    items: UserInfoType[];
+}
+
+interface GroupsListType {
+    href: string;
+    items: GroupInfoType[];
+}
+
+interface SelectOptionType {
+    value: string;
+    label: string;
+}
+
+interface ProcessedRbacDataType {
+    [key: string]: { [key: string]: string[] }[];
+}
+
+type PermissionLabel = "None" | "Cluster User" | "Cluster Administrator" | 
"Cluster Operator" | "Service Administrator" | "Service Operator";
+
+export type { UserInfoType, GroupInfoType, GroupNamesType, UserNamesType, 
UsersListType, GroupsListType, SelectOptionType, PermissionLabel, 
ProcessedRbacDataType };
\ No newline at end of file


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


Reply via email to