Groups list component creation (#14)
* Reconnected Users delete modal * Fix API path * Reconnected access name modal * Fixed title issue * Fixed reset state * Added groups selectors list control component * Change of edit form to not edit access key & access secret values Co-authored-by: Benjamin Perez <benjamin@bexsoft.net> Co-authored-by: Daniel Valdivia <hola@danielvaldivia.com>
This commit is contained in:
@@ -29,11 +29,18 @@ import {
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import { User } from "./types";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
},
|
||||
strongText: {
|
||||
fontWeight: 700,
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,6 +55,9 @@ interface IAddUserContentState {
|
||||
addError: string;
|
||||
accessKey: string;
|
||||
secretKey: string;
|
||||
selectedGroups: string[];
|
||||
loadingGroups: boolean;
|
||||
groupsList: any[];
|
||||
}
|
||||
|
||||
class AddUserContent extends React.Component<
|
||||
@@ -58,7 +68,10 @@ class AddUserContent extends React.Component<
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessKey: "",
|
||||
secretKey: ""
|
||||
secretKey: "",
|
||||
selectedGroups: [],
|
||||
loadingGroups: false,
|
||||
groupsList: [],
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
@@ -74,7 +87,7 @@ class AddUserContent extends React.Component<
|
||||
|
||||
saveRecord(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
const { accessKey, addLoading, secretKey } = this.state;
|
||||
const { accessKey, addLoading, secretKey, selectedGroups } = this.state;
|
||||
const { selectedUser } = this.props;
|
||||
if (addLoading) {
|
||||
return;
|
||||
@@ -133,12 +146,12 @@ class AddUserContent extends React.Component<
|
||||
|
||||
render() {
|
||||
const { classes, selectedUser } = this.props;
|
||||
const { addLoading, addError, accessKey, secretKey } = this.state;
|
||||
const { addLoading, addError, accessKey, secretKey, selectedGroups, loadingGroups, groupsList } = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
Create User
|
||||
{selectedUser !== null ? 'Edit User' : 'Add User'}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<form
|
||||
@@ -149,13 +162,6 @@ class AddUserContent extends React.Component<
|
||||
}}
|
||||
>
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
{selectedUser !== null ? (
|
||||
<Title>Edit User</Title>
|
||||
) : (
|
||||
<Title>Add User</Title>
|
||||
)}
|
||||
</Grid>
|
||||
{addError !== "" && (
|
||||
<Grid item xs={12}>
|
||||
<Typography
|
||||
@@ -167,33 +173,58 @@ class AddUserContent extends React.Component<
|
||||
</Typography>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
fullWidth
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="standard-multiline-static"
|
||||
label={selectedUser !== null ? 'New Secret Key': 'Secret Key'}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={secretKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
{selectedUser !== null ? (
|
||||
<React.Fragment>
|
||||
<span className={classes.strongText}>Access Key:</span>
|
||||
<span className={classes.keyName}>{` ${accessKey}`}</span>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="standard-basic"
|
||||
fullWidth
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<TextField
|
||||
id="standard-multiline-static"
|
||||
label={selectedUser !== null ? 'New Secret Key': 'Secret Key'}
|
||||
type="password"
|
||||
fullWidth
|
||||
value={secretKey}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
)}
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
setSelectedGroups={
|
||||
(elements: string[]) => {
|
||||
this.setState({
|
||||
selectedGroups: elements
|
||||
})
|
||||
}
|
||||
}
|
||||
loading={loadingGroups}
|
||||
records={groupsList}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Button
|
||||
type="submit"
|
||||
|
||||
@@ -68,7 +68,7 @@ class DeleteUser extends React.Component<
|
||||
}
|
||||
this.setState({ deleteLoading: true }, () => {
|
||||
api
|
||||
.invoke("DELETE", `/api/v1/users/${selectedUser.id}`, {
|
||||
.invoke("DELETE", `/api/v1/users/${selectedUser.accessKey}`, {
|
||||
id: selectedUser.id
|
||||
})
|
||||
.then((res: UsersList) => {
|
||||
@@ -114,7 +114,7 @@ class DeleteUser extends React.Component<
|
||||
<DialogContent>
|
||||
{deleteLoading && <LinearProgress />}
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete user{" "}<b>{selectedUser.name}</b>?
|
||||
Are you sure you want to delete user{" "}<b>{selectedUser.accessKey}</b>?
|
||||
{deleteError !== "" && (
|
||||
<React.Fragment>
|
||||
<br />
|
||||
|
||||
188
portal-ui/src/screens/Console/Users/GroupsSelectors.tsx
Normal file
188
portal-ui/src/screens/Console/Users/GroupsSelectors.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
// This file is part of MinIO Kubernetes Cloud
|
||||
// Copyright (c) 2020 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
import Grid from "@material-ui/core/Grid";
|
||||
import Title from "../../../common/Title";
|
||||
import InputAdornment from "@material-ui/core/InputAdornment";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import TextField from "@material-ui/core/TextField";
|
||||
import Checkbox from "@material-ui/core/Checkbox";
|
||||
|
||||
interface IGroupsProps {
|
||||
classes: any;
|
||||
selectedGroups: string[];
|
||||
setSelectedGroups: any;
|
||||
records: any[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
seeMore: {
|
||||
marginTop: theme.spacing(3)
|
||||
},
|
||||
paper: {
|
||||
// padding: theme.spacing(2),
|
||||
display: "flex",
|
||||
overflow: "auto",
|
||||
flexDirection: "column"
|
||||
},
|
||||
addSideBar: {
|
||||
width: "320px",
|
||||
padding: "20px"
|
||||
},
|
||||
errorBlock: {
|
||||
color: "red"
|
||||
},
|
||||
tableToolbar: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(0)
|
||||
},
|
||||
wrapCell: {
|
||||
maxWidth: "200px",
|
||||
whiteSpace: "normal",
|
||||
wordWrap: "break-word"
|
||||
},
|
||||
minTableHeader: {
|
||||
color: "#393939",
|
||||
"& tr": {
|
||||
"& th": {
|
||||
fontWeight:'bold'
|
||||
}
|
||||
}
|
||||
},
|
||||
actionsTray: {
|
||||
textAlign: "left",
|
||||
"& button": {
|
||||
marginLeft: 10,
|
||||
}
|
||||
},
|
||||
filterField: {
|
||||
background: "#FFFFFF",
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
width: '100%'
|
||||
},
|
||||
noFound: {
|
||||
textAlign: "center",
|
||||
padding: "10px 0",
|
||||
}
|
||||
});
|
||||
|
||||
const GroupsSelectors = ({
|
||||
classes,
|
||||
selectedGroups,
|
||||
setSelectedGroups,
|
||||
records,
|
||||
loading
|
||||
}: IGroupsProps) => {
|
||||
|
||||
if(!records) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements : string[] = [...selectedGroups]; // We clone the selectedGroups array
|
||||
|
||||
if(checked) { // If the user has checked this field we need to push this to selectedGroupsList
|
||||
elements.push(value);
|
||||
} else { // User has unchecked this field, we need to remove it from the list
|
||||
elements = elements.filter(element => element !== value);
|
||||
}
|
||||
setSelectedGroups(elements);
|
||||
|
||||
return elements;
|
||||
};
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Title>Groups</Title>
|
||||
<Grid item xs={12}>
|
||||
<Paper className={classes.paper}>
|
||||
{loading && <LinearProgress />}
|
||||
{records != null && records.length > 0 ? (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.actionsTray}>
|
||||
<TextField
|
||||
placeholder="Filter Groups"
|
||||
className={classes.filterField}
|
||||
id="search-resource"
|
||||
label=""
|
||||
InputProps={{
|
||||
disableUnderline: true,
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon />
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Table size="medium">
|
||||
<TableHead className={classes.minTableHeader}>
|
||||
<TableRow>
|
||||
<TableCell>Select</TableCell>
|
||||
<TableCell>Group</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{records.map(row => (
|
||||
<TableRow key={`group-${row.groupName}`}>
|
||||
<TableCell padding="checkbox">
|
||||
<Checkbox
|
||||
value={row.groupName}
|
||||
color="primary"
|
||||
inputProps={{
|
||||
'aria-label': 'secondary checkbox'
|
||||
}}
|
||||
onChange={ selectionChanged }
|
||||
checked={selectedGroups.includes(row.groupName)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
{row.groupName}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</React.Fragment>
|
||||
) : (
|
||||
<div className={classes.noFound}>No Groups Available</div>
|
||||
)}
|
||||
</Paper>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(GroupsSelectors);
|
||||
|
||||
|
||||
@@ -255,7 +255,8 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
startIcon={<CreateIcon />}
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true
|
||||
addScreenOpen: true,
|
||||
selectedUser: null,
|
||||
});
|
||||
}}
|
||||
>
|
||||
@@ -295,6 +296,10 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
<IconButton
|
||||
aria-label="view"
|
||||
onClick={() => {
|
||||
this.setState({
|
||||
addScreenOpen: true,
|
||||
selectedUser: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<ViewIcon />
|
||||
@@ -302,7 +307,10 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
<IconButton
|
||||
aria-label="delete"
|
||||
onClick={() => {
|
||||
|
||||
this.setState({
|
||||
deleteOpen: true,
|
||||
selectedUser: row,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<DeleteIcon />
|
||||
|
||||
Reference in New Issue
Block a user