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]