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
This commit is contained in:
jinapurapu
2022-05-16 11:28:57 -07:00
committed by GitHub
parent 6e4b8884e6
commit 448a80af4a
6 changed files with 254 additions and 111 deletions

View File

@@ -699,6 +699,7 @@ const TableWrapper = ({
inputProps={{
"aria-label": "secondary checkbox",
}}
className="TableCheckbox"
checked={isSelected}
onChange={onSelect}
onClick={(e) => {

View File

@@ -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) => (
<div key={user}>
<b>{user}</b>
</div>
));
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 (
<ConfirmDialog
title={`Delete User`}
title={`Delete User${selectedUsers.length > 1 ? "s" : ""}`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
@@ -69,8 +79,9 @@ const DeleteUser = ({
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete user <br />
<b>{selectedUser.accessKey}</b>?
Are you sure you want to delete the following {selectedUsers.length}{" "}
user{selectedUsers.length > 1 ? "s?" : "?"}
<b>{renderUsers}</b>
</DialogContentText>
}
/>

View File

@@ -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);
};

View File

@@ -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<User[]>([]);
const [loading, setLoading] = useState<boolean>(true);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedUser, setSelectedUser] = useState<User | null>(null);
const [addGroupOpen, setAddGroupOpen] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
const [checkedUsers, setCheckedUsers] = useState<string[]>([]);
const [policyOpen, setPolicyOpen] = useState<boolean>(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<HTMLInputElement>) => {
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 (
<Fragment>
{policyOpen && (
<SetPolicy
open={policyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
setLoading(true);
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}
selectedUser={selectedUser}
selectedUsers={checkedUsers}
closeDeleteModalAndRefresh={(refresh: boolean) => {
closeDeleteModalAndRefresh(refresh);
}}
@@ -233,6 +206,25 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
overrideClass={classes.searchField}
value={filter}
/>
<SecureComponent
resource={CONSOLE_UI_RESOURCE}
scopes={[IAM_SCOPES.ADMIN_DELETE_USER]}
matchAll
errorProps={{ disabled: true }}
>
<RBIconButton
tooltip={"Delete Selected"}
onClick={() => {
setDeleteOpen(true);
}}
text={"Delete Selected"}
icon={<DeleteIcon />}
color="secondary"
disabled={checkedUsers.length === 0}
variant={"outlined"}
aria-label="delete-selected-users"
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_ADD_USER_TO_GROUP]}
resource={CONSOLE_UI_RESOURCE}
@@ -280,7 +272,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
<Fragment>
{records.length > 0 && (
<Fragment>
<Grid item xs={12} className={classes.tableBlock}>
<Grid item xs={12} className={classes.tableBlock} marginBottom={"15px"}>
<SecureComponent
scopes={[IAM_SCOPES.ADMIN_LIST_USERS]}
resource={CONSOLE_UI_RESOURCE}
@@ -291,7 +283,11 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
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) => {
/>
</SecureComponent>
</Grid>
<Grid item xs={12} marginTop={"25px"}>
<HelpBox
<HelpBox
title={"Users"}
iconComponent={<UsersIcon />}
help={
<Fragment>
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.
<br />
<br />
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.
<br />
<br />
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.
<br />
Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads.
<br />
<br />
Users inherit access permissions to data and resources through the groups they belong to.
<br />
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.
<br />
<br />
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.
<br />
<br />
You can learn more at our{" "}
<a
href="https://docs.min.io/minio/baremetal/monitoring/bucket-notifications/bucket-notifications.html?ref=con"
href="https://docs.min.io/minio/k8s/tutorials/user-management.html?ref=con"
target="_blank"
rel="noreferrer"
>
@@ -331,7 +329,6 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
</Fragment>
}
/>
</Grid>
</Fragment>
)}
{records.length === 0 && (
@@ -339,7 +336,7 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
container
justifyContent={"center"}
alignContent={"center"}
alignItems={"center"}
alignItems={"start"}
>
<Grid item xs={8}>
<HelpBox
@@ -347,46 +344,51 @@ const ListUsers = ({ classes, setErrorSnackMessage, history }: IUsersProps) => {
iconComponent={<UsersIcon />}
help={
<Fragment>
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.
<br />
<br />
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.
<SecureComponent
scopes={[
IAM_SCOPES.ADMIN_CREATE_USER,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]}
matchAll
resource={CONSOLE_UI_RESOURCE}
>
<Fragment>
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
history.push(`${IAM_PAGES.USER_ADD}`);
}}
>
Create a User
</AButton>
.
</Fragment>
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.
<br />
Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads.
<br />
<br />
Users inherit access permissions to data and resources through the groups they belong to.
<br />
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.
<br />
<br />
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.
<SecureComponent
scopes={[
IAM_SCOPES.ADMIN_CREATE_USER,
IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
IAM_SCOPES.ADMIN_LIST_GROUPS,
]}
matchAll
resource={CONSOLE_UI_RESOURCE}
>
<br />
<br />
To get started,{" "}
<AButton
onClick={() => {
history.push(`${IAM_PAGES.USER_ADD}`);
}}
>
Create a User
</AButton>
.
</SecureComponent>
</Fragment>
}
/>
</Grid>
</Grid>
)}
</Fragment>
)}
</PageLayout>

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<Box
sx={{
display: "flex",
"& .min-icon": {
marginRight: "10px",
height: "23px",
width: "23px",
marginBottom: "10px",
},
}}
>
{icon}{" "}
<div style={{ fontSize: "14px", fontStyle: "italic", color: "#5E5E5E" }}>
{description}
</div>
</Box>
);
};
const UsersHelpBox = () => {
return (
<Box
sx={{
flex: 1,
border: "1px solid #eaeaea",
borderRadius: "2px",
display: "flex",
flexFlow: "column",
padding: "20px",
marginTop: {
xs: "0px",
},
}}
>
<Box
sx={{
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
marginBottom: "16px",
"& .min-icon": {
height: "21px",
width: "21px",
marginRight: "15px",
},
}}
>
<HelpIconFilled />
<div>Learn more about the Users feature</div>
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
}}
>
<FeatureItem icon={<UsersIcon />} description={`Create Users`} />
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
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.
<br />
</Box>
<FeatureItem icon={<GroupsIcon />} description={`Manage Groups`} />
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
Groups provide a simplified method for managing shared permissions among users with common access patterns and workloads.
<br />
<br />
Users inherit access permissions to data and resources through the groups they belong to.
<br />
</Box>
<FeatureItem
icon={<ChangeAccessPolicyIcon />}
description={`Assign Policies`}
/>
<Box sx={{ fontSize: "14px", marginBottom: "15px" }}>
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.
<br />
<br />
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.
<br />
</Box>
</Box>
</Box>
);
};
export default UsersHelpBox;

View File

@@ -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();