From 448a80af4a07b2467910a2517d3bb960579dacb7 Mon Sep 17 00:00:00 2001 From: jinapurapu <65002498+jinapurapu@users.noreply.github.com> Date: Mon, 16 May 2022 11:28:57 -0700 Subject: [PATCH] Users screen UI revision (#1961) * Changed Users screen UI to selector based delete, table layout changes, updated testcafe permissions test to reflect new delete User sequence, fixed ListUsers checkbox permission issue --- .../Common/TableWrapper/TableWrapper.tsx | 1 + .../src/screens/Console/Users/DeleteUser.tsx | 37 ++-- .../screens/Console/Users/DeleteUserModal.tsx | 4 +- .../src/screens/Console/Users/ListUsers.tsx | 186 +++++++++--------- .../screens/Console/Users/UsersHelpBox.tsx | 125 ++++++++++++ portal-ui/tests/permissions-1/users.ts | 12 +- 6 files changed, 254 insertions(+), 111 deletions(-) create mode 100644 portal-ui/src/screens/Console/Users/UsersHelpBox.tsx diff --git a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx index 05913d595..d21820506 100644 --- a/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/TableWrapper/TableWrapper.tsx @@ -699,6 +699,7 @@ const TableWrapper = ({ inputProps={{ "aria-label": "secondary checkbox", }} + className="TableCheckbox" checked={isSelected} onChange={onSelect} onClick={(e) => { diff --git a/portal-ui/src/screens/Console/Users/DeleteUser.tsx b/portal-ui/src/screens/Console/Users/DeleteUser.tsx index 3bb88a964..72712a718 100644 --- a/portal-ui/src/screens/Console/Users/DeleteUser.tsx +++ b/portal-ui/src/screens/Console/Users/DeleteUser.tsx @@ -17,7 +17,6 @@ import React from "react"; import { connect } from "react-redux"; import { DialogContentText } from "@mui/material"; -import { User } from "./types"; import { setErrorSnackMessage } from "../../../actions"; import useApi from "../Common/Hooks/useApi"; import ConfirmDialog from "../Common/ModalWrapper/ConfirmDialog"; @@ -28,14 +27,14 @@ import { encodeURLString } from "../../../common/utils"; interface IDeleteUserProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; deleteOpen: boolean; - selectedUser: User | null; + selectedUsers: string[] | null; setErrorSnackMessage: typeof setErrorSnackMessage; } const DeleteUser = ({ closeDeleteModalAndRefresh, deleteOpen, - selectedUser, + selectedUsers, setErrorSnackMessage, }: IDeleteUserProps) => { const onDelSuccess = () => closeDeleteModalAndRefresh(true); @@ -44,23 +43,34 @@ const DeleteUser = ({ const [deleteLoading, invokeDeleteApi] = useApi(onDelSuccess, onDelError); - if (!selectedUser) { + const userLoggedIn = localStorage.getItem("userLoggedIn") || ""; + + if (!selectedUsers) { return null; } + const renderUsers = selectedUsers.map((user) => ( +
+ {user} +
+ )); const onConfirmDelete = () => { - invokeDeleteApi( - "DELETE", - `/api/v1/user/${encodeURLString(selectedUser.accessKey)}`, - { - id: selectedUser.id, + for (let user of selectedUsers) { + if (user === userLoggedIn) { + setErrorSnackMessage({ + errorMessage: "Cannot delete currently logged in user", + detailedError: `Cannot delete currently logged in user ${userLoggedIn}`, + }); + closeDeleteModalAndRefresh(true); + } else { + invokeDeleteApi("DELETE", `/api/v1/user/${encodeURLString(user)}`); } - ); + } }; return ( 1 ? "s" : ""}`} confirmText={"Delete"} isOpen={deleteOpen} titleIcon={} @@ -69,8 +79,9 @@ const DeleteUser = ({ onClose={onClose} confirmationContent={ - Are you sure you want to delete user
- {selectedUser.accessKey}? + Are you sure you want to delete the following {selectedUsers.length}{" "} + user{selectedUsers.length > 1 ? "s?" : "?"} + {renderUsers}
} /> diff --git a/portal-ui/src/screens/Console/Users/DeleteUserModal.tsx b/portal-ui/src/screens/Console/Users/DeleteUserModal.tsx index 767edfb27..a64654bd2 100644 --- a/portal-ui/src/screens/Console/Users/DeleteUserModal.tsx +++ b/portal-ui/src/screens/Console/Users/DeleteUserModal.tsx @@ -26,7 +26,7 @@ import { ConfirmDeleteIcon } from "../../../icons"; import { IAM_PAGES } from "../../../common/SecureComponent/permissions"; import { encodeURLString } from "../../../common/utils"; -interface IDeleteUserProps { +interface IDeleteUserStringProps { closeDeleteModalAndRefresh: (refresh: boolean) => void; deleteOpen: boolean; userName: string; @@ -38,7 +38,7 @@ const DeleteUserModal = ({ deleteOpen, userName, setErrorSnackMessage, -}: IDeleteUserProps) => { +}: IDeleteUserStringProps) => { const onDelSuccess = () => { history.push(IAM_PAGES.USERS); }; diff --git a/portal-ui/src/screens/Console/Users/ListUsers.tsx b/portal-ui/src/screens/Console/Users/ListUsers.tsx index ab241bd88..cb8939807 100644 --- a/portal-ui/src/screens/Console/Users/ListUsers.tsx +++ b/portal-ui/src/screens/Console/Users/ListUsers.tsx @@ -23,7 +23,7 @@ import api from "../../../common/api"; import { Grid, LinearProgress } from "@mui/material"; import { User, UsersList } from "./types"; import { usersSort } from "../../../utils/sortFunctions"; -import { GroupsIcon, AddIcon, UsersIcon } from "../../../icons"; +import { GroupsIcon, AddIcon, DeleteIcon, UsersIcon } from "../../../icons"; import { actionsTray, containerForHeader, @@ -35,7 +35,7 @@ import { ErrorResponseHandler } from "../../../common/types"; import TableWrapper from "../Common/TableWrapper/TableWrapper"; import PageHeader from "../Common/PageHeader/PageHeader"; -import { decodeURLString, encodeURLString } from "../../../common/utils"; +import { encodeURLString } from "../../../common/utils"; import HelpBox from "../../../common/HelpBox"; import AButton from "../Common/AButton/AButton"; import PageLayout from "../Common/Layout/PageLayout"; @@ -54,9 +54,6 @@ import { SecureComponent, } from "../../../common/SecureComponent"; -const SetPolicy = withSuspense( - React.lazy(() => import("../Policies/SetPolicy")) -); const DeleteUser = withSuspense(React.lazy(() => import("./DeleteUser"))); const AddToGroup = withSuspense(React.lazy(() => import("./BulkAddToGroup"))); @@ -83,20 +80,14 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { const [records, setRecords] = useState([]); const [loading, setLoading] = useState(true); const [deleteOpen, setDeleteOpen] = useState(false); - const [selectedUser, setSelectedUser] = useState(null); const [addGroupOpen, setAddGroupOpen] = useState(false); const [filter, setFilter] = useState(""); const [checkedUsers, setCheckedUsers] = useState([]); - const [policyOpen, setPolicyOpen] = useState(false); const displayListUsers = hasPermission(CONSOLE_UI_RESOURCE, [ IAM_SCOPES.ADMIN_LIST_USERS, ]); - const deleteUser = hasPermission(CONSOLE_UI_RESOURCE, [ - IAM_SCOPES.ADMIN_DELETE_USER, - ]); - const viewUser = hasPermission(CONSOLE_UI_RESOURCE, [ IAM_SCOPES.ADMIN_GET_USER, ]); @@ -105,11 +96,16 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP, ]); + const deleteUser = hasPermission(CONSOLE_UI_RESOURCE, [ + IAM_SCOPES.ADMIN_DELETE_USER, + ]); + const closeDeleteModalAndRefresh = (refresh: boolean) => { setDeleteOpen(false); if (refresh) { setLoading(true); } + setCheckedUsers([]); }; const closeAddGroupBulk = (unCheckAll: boolean = false) => { @@ -145,9 +141,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { ); const selectionChanged = (e: React.ChangeEvent) => { - const targetD = e.target; - const value = targetD.value; - const checked = targetD.checked; + const { target: { value = "", checked = false } = {} } = e; let elements: string[] = [...checkedUsers]; // We clone the checkedUsers array @@ -170,15 +164,6 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { ); }; - const deleteAction = (selectionElement: any): void => { - setDeleteOpen(true); - setSelectedUser(selectionElement); - }; - - const userLoggedIn = decodeURLString( - localStorage.getItem("userLoggedIn") || "" - ); - const tableActions = [ { type: "view", @@ -186,30 +171,18 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { disableButtonFunction: () => !viewUser, }, { - type: "delete", - onClick: deleteAction, - disableButtonFunction: (topValue: any) => - topValue === userLoggedIn || !deleteUser, + type: "edit", + onClick: viewAction, + disableButtonFunction: () => !viewUser, }, ]; return ( - {policyOpen && ( - { - setPolicyOpen(false); - setLoading(true); - }} - /> - )} {deleteOpen && ( { closeDeleteModalAndRefresh(refresh); }} @@ -233,6 +206,25 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { overrideClass={classes.searchField} value={filter} /> + + { + setDeleteOpen(true); + }} + text={"Delete Selected"} + icon={} + color="secondary" + disabled={checkedUsers.length === 0} + variant={"outlined"} + aria-label="delete-selected-users" + /> + { {records.length > 0 && ( - + { columns={[ { label: "Access Key", elementKey: "accessKey" }, ]} - onSelect={addUserToGroup ? selectionChanged : undefined} + onSelect={ + addUserToGroup || deleteUser + ? selectionChanged + : undefined + } selectedItems={checkedUsers} isLoading={loading} records={filteredRecords} @@ -300,28 +296,30 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { /> - - } help={ - A MinIO user consists of a unique access key (username) - and corresponding secret key (password). Clients must - authenticate their identity by specifying both a valid - access key (username) and the corresponding secret key - (password) of an existing MinIO user. -
-
- Each user can have one or more assigned policies that - explicitly list the actions and resources to which that - user has access. Users can also inherit policies from - the groups in which they have membership. -
-
+ A MinIO user consists of a unique access key (username) and + corresponding secret key (password). Clients must authenticate their + identity by specifying both a valid access key (username) and the + corresponding secret key (password) of an existing MinIO user. +
+ Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads. +
+
+ Users inherit access permissions to data and resources through the groups they belong to. +
+ MinIO uses Policy-Based Access Control (PBAC) to define the authorized actions and resources to which an authenticated user has access. Each policy describes one or more actions and conditions that outline the permissions of a user or group of users. +
+
+ Each user can access only those resources and operations which are explicitly granted by the built-in role. MinIO denies access to any other resource or action by default. +
+
You can learn more at our{" "} @@ -331,7 +329,6 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
} /> -
)} {records.length === 0 && ( @@ -339,7 +336,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => { container justifyContent={"center"} alignContent={"center"} - alignItems={"center"} + alignItems={"start"} > { iconComponent={} help={ - A MinIO user consists of a unique access key (username) - and corresponding secret key (password). Clients must - authenticate their identity by specifying both a valid - access key (username) and the corresponding secret key - (password) of an existing MinIO user. -
-
- Each user can have one or more assigned policies that - explicitly list the actions and resources to which that - user has access. Users can also inherit policies from - the groups in which they have membership. - - -
-
- To get started,{" "} - { - history.push(`${IAM_PAGES.USER_ADD}`); - }} - > - Create a User - - . -
+ A MinIO user consists of a unique access key (username) and + corresponding secret key (password). Clients must authenticate their + identity by specifying both a valid access key (username) and the + corresponding secret key (password) of an existing MinIO user. +
+ Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads. +
+
+ Users inherit access permissions to data and resources through the groups they belong to. +
+ MinIO uses Policy-Based Access Control (PBAC) to define the authorized actions and resources to which an authenticated user has access. Each policy describes one or more actions and conditions that outline the permissions of a user or group of users. +
+
+ Each user can access only those resources and operations which are explicitly granted by the built-in role. MinIO denies access to any other resource or action by default. + + +
+
+ To get started,{" "} + { + history.push(`${IAM_PAGES.USER_ADD}`); + }} + > + Create a User + + .
} /> +
)} + +
)} diff --git a/portal-ui/src/screens/Console/Users/UsersHelpBox.tsx b/portal-ui/src/screens/Console/Users/UsersHelpBox.tsx new file mode 100644 index 000000000..4d2fe03bb --- /dev/null +++ b/portal-ui/src/screens/Console/Users/UsersHelpBox.tsx @@ -0,0 +1,125 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 MinIO, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . +import React from "react"; +import { Box } from "@mui/material"; +import { + HelpIconFilled, + UsersIcon, + ChangeAccessPolicyIcon, + GroupsIcon, +} from "../../../icons"; + +const FeatureItem = ({ + icon, + description, +}: { + icon: any; + description: string; +}) => { + return ( + + {icon}{" "} +
+ {description} +
+
+ ); +}; +const UsersHelpBox = () => { + return ( + + + +
Learn more about the Users feature
+
+ + + + } description={`Create Users`} /> + + A MinIO user consists of a unique access key (username) and + corresponding secret key (password). Clients must authenticate their + identity by specifying both a valid access key (username) and the + corresponding secret key (password) of an existing MinIO user. +
+
+ } description={`Manage Groups`} /> + + Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads. +
+
+ Users inherit access permissions to data and resources through the groups they belong to. +
+
+ } + description={`Assign Policies`} + /> + + MinIO uses Policy-Based Access Control (PBAC) to define the authorized actions and resources to which an authenticated user has access. Each policy describes one or more actions and conditions that outline the permissions of a user or group of users. +
+
+ Each user can access only those resources and operations which are explicitly granted by the built-in role. MinIO denies access to any other resource or action by default. +
+ +
+
+
+ ); +}; + +export default UsersHelpBox; diff --git a/portal-ui/tests/permissions-1/users.ts b/portal-ui/tests/permissions-1/users.ts index 62934cb19..421162bac 100644 --- a/portal-ui/tests/permissions-1/users.ts +++ b/portal-ui/tests/permissions-1/users.ts @@ -26,9 +26,10 @@ const userListItem = Selector(".ReactVirtualized__Table__rowColumn").withText( ); const userDeleteIconButton = userListItem - .nextSibling() - .child("button") - .withAttribute("aria-label", "delete"); + .child("checkbox") + .withAttribute("aria-label", "secondary checkbox"); + +const userCheckbox = Selector(".TableCheckbox"); fixture("For user with Users permissions") .page("http://localhost:9090") @@ -78,12 +79,15 @@ test("Users table exists", async (t) => { test("Created User can be viewed and deleted", async (t) => { const userListItemExists = userListItem.exists; + const deleteSelectedButton = + Selector("button:enabled").withExactText("Delete Selected"); await t .navigateTo(usersPageUrl) .typeText(elements.searchResourceInput, constants.TEST_USER_NAME) .expect(userListItemExists) .ok() - .click(userDeleteIconButton) + .click(userCheckbox) + .click(deleteSelectedButton) .click(elements.deleteButton) .expect(userListItemExists) .notOk();