Creation of reusable components for mcs & implementation in users page (#63)
Creation of reusable componentes for mcs: - ModalWrapper => Modal box component with MinIO styles - InputBoxWrapper => Input box component with MinIO styles - RadioGroupSelector => Component that generates a Radio Group Selector combo with the requested options and MinIO styles Implementation of these new components in users creation / edit components Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
1
go.sum
1
go.sum
@@ -104,6 +104,7 @@ github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ER
|
||||
github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/globalsign/mgo v0.0.0-20180905125535-1ca0a4f7cbcb/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q=
|
||||
github.com/go-bindata/go-bindata v3.1.2+incompatible h1:5vjJMVhowQdPzjE1LdxyFF7YFTXg5IgGVW4gBr5IbvE=
|
||||
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-ldap/ldap v3.0.2+incompatible/go.mod h1:qfd9rJvER9Q0/D/Sqn1DfHRoBp40uXYvFoEVrNEPqRc=
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Lato:wght@400;500;700;900&display=swap" rel="stylesheet">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="%PUBLIC_URL%/apple-icon-180x180.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="%PUBLIC_URL%/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="%PUBLIC_URL%/favicon-96x96.png">
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// 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 { TextField, Grid, InputLabel, TextFieldProps } from "@material-ui/core";
|
||||
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
|
||||
import {
|
||||
createStyles,
|
||||
makeStyles,
|
||||
Theme,
|
||||
withStyles
|
||||
} from "@material-ui/core/styles";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
|
||||
interface InputBoxProps {
|
||||
label: string;
|
||||
classes: any;
|
||||
onChangeFunc: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value: string;
|
||||
id: string;
|
||||
name: string;
|
||||
disabled?: boolean;
|
||||
type?: string;
|
||||
autoComplete?: string;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
textBoxContainer: {
|
||||
flexGrow: 1
|
||||
}
|
||||
});
|
||||
|
||||
const inputStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
borderColor: "#393939",
|
||||
borderRadius: 0
|
||||
},
|
||||
input: {
|
||||
padding: "11px 20px",
|
||||
color: "#393939",
|
||||
fontSize: 14
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
function InputField(props: TextFieldProps) {
|
||||
const classes = inputStyles();
|
||||
|
||||
return (
|
||||
<TextField
|
||||
InputProps={{ classes } as Partial<OutlinedInputProps>}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const InputBoxWrapper = ({
|
||||
label,
|
||||
onChangeFunc,
|
||||
value,
|
||||
id,
|
||||
name,
|
||||
type = "text",
|
||||
autoComplete = "off",
|
||||
disabled = false,
|
||||
classes
|
||||
}: InputBoxProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
<InputLabel htmlFor={id} className={classes.inputLabel}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<div className={classes.textBoxContainer}>
|
||||
<InputField
|
||||
className={classes.boxDesign}
|
||||
id={id}
|
||||
name={name}
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={onChangeFunc}
|
||||
type={type}
|
||||
autoComplete={autoComplete}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(InputBoxWrapper);
|
||||
@@ -0,0 +1,143 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// 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 Grid from "@material-ui/core/Grid";
|
||||
import RadioGroup from "@material-ui/core/RadioGroup";
|
||||
import FormControlLabel from "@material-ui/core/FormControlLabel";
|
||||
import Radio, { RadioProps } from "@material-ui/core/Radio";
|
||||
import { InputLabel } from "@material-ui/core";
|
||||
import {
|
||||
createStyles,
|
||||
Theme,
|
||||
withStyles,
|
||||
makeStyles
|
||||
} from "@material-ui/core/styles";
|
||||
import { fieldBasic } from "../common/styleLibrary";
|
||||
|
||||
interface selectorTypes {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface RadioGroupProps {
|
||||
selectorOptions: selectorTypes[];
|
||||
currentSelection: string;
|
||||
label: string;
|
||||
id: string;
|
||||
name: string;
|
||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
classes: any;
|
||||
displayInColumn?: boolean;
|
||||
}
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
...fieldBasic,
|
||||
radioBoxContainer: {
|
||||
flexGrow: 1
|
||||
}
|
||||
});
|
||||
|
||||
const radioStyles = makeStyles({
|
||||
root: {
|
||||
"&:hover": {
|
||||
backgroundColor: "transparent"
|
||||
}
|
||||
},
|
||||
icon: {
|
||||
borderRadius: "100%",
|
||||
width: 14,
|
||||
height: 14,
|
||||
border: "1px solid #000"
|
||||
},
|
||||
checkedIcon: {
|
||||
borderRadius: "100%",
|
||||
width: 14,
|
||||
height: 14,
|
||||
border: "1px solid #000",
|
||||
padding: 4,
|
||||
position: "relative",
|
||||
"&::after": {
|
||||
content: '" "',
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: "100%",
|
||||
display: "block",
|
||||
position: "absolute",
|
||||
backgroundColor: "#000",
|
||||
top: 2,
|
||||
left: 2
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const RadioButton = (props: RadioProps) => {
|
||||
const classes = radioStyles();
|
||||
|
||||
return (
|
||||
<Radio
|
||||
className={classes.root}
|
||||
disableRipple
|
||||
color="default"
|
||||
checkedIcon={<span className={classes.checkedIcon} />}
|
||||
icon={<span className={classes.icon} />}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const RadioGroupSelector = ({
|
||||
selectorOptions = [],
|
||||
currentSelection,
|
||||
label,
|
||||
id,
|
||||
name,
|
||||
onChange,
|
||||
classes,
|
||||
displayInColumn = false
|
||||
}: RadioGroupProps) => {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Grid item xs={12} className={classes.fieldContainer}>
|
||||
<InputLabel htmlFor={id} className={classes.inputLabel}>
|
||||
{label}
|
||||
</InputLabel>
|
||||
<div className={classes.radioBoxContainer}>
|
||||
<RadioGroup
|
||||
aria-label={id}
|
||||
id={id}
|
||||
name={name}
|
||||
value={currentSelection}
|
||||
onChange={onChange}
|
||||
row={!displayInColumn}
|
||||
>
|
||||
{selectorOptions.map(selectorOption => {
|
||||
return (
|
||||
<FormControlLabel
|
||||
value={selectorOption.value}
|
||||
control={<RadioButton />}
|
||||
label={selectorOption.label}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</RadioGroup>
|
||||
</div>
|
||||
</Grid>
|
||||
</React.Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(RadioGroupSelector);
|
||||
@@ -0,0 +1,31 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// 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/>.
|
||||
|
||||
// This object contains variables that will be used across form components.
|
||||
export const fieldBasic = {
|
||||
inputLabel: {
|
||||
fontWeight: 500,
|
||||
marginRight: 16,
|
||||
minWidth: 80,
|
||||
fontSize: 14,
|
||||
color: "#393939"
|
||||
},
|
||||
fieldContainer: {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 10
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
// This file is part of MinIO Console Server
|
||||
// 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 Typography from "@material-ui/core/Typography";
|
||||
import { Dialog, DialogContent, DialogTitle } from "@material-ui/core";
|
||||
import IconButton from "@material-ui/core/IconButton";
|
||||
import CloseIcon from "@material-ui/icons/Close";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
|
||||
interface IModalProps {
|
||||
classes: any;
|
||||
closeModalAndRefresh: () => void;
|
||||
modalOpen: boolean;
|
||||
title: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
const baseCloseLine = {
|
||||
content: '" "',
|
||||
borderLeft: "2px solid #707070",
|
||||
height: 33,
|
||||
width: 1,
|
||||
position: "absolute"
|
||||
};
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
dialogContainer: {
|
||||
padding: "12px 26px 22px"
|
||||
},
|
||||
closeContainer: {
|
||||
textAlign: "right"
|
||||
},
|
||||
closeButton: {
|
||||
width: 45,
|
||||
height: 45,
|
||||
padding: 0,
|
||||
backgroundColor: "initial",
|
||||
"&:hover": {
|
||||
backgroundColor: "initial"
|
||||
},
|
||||
"&:active": {
|
||||
backgroundColor: "initial"
|
||||
}
|
||||
},
|
||||
modalCloseIcon: {
|
||||
fontSize: 35,
|
||||
color: "#707070",
|
||||
fontWeight: 300,
|
||||
"&:hover": {
|
||||
color: "#000"
|
||||
}
|
||||
},
|
||||
closeIcon: {
|
||||
"&::before": {
|
||||
...baseCloseLine,
|
||||
transform: "rotate(45deg)"
|
||||
},
|
||||
"&::after": {
|
||||
...baseCloseLine,
|
||||
transform: "rotate(-45deg)"
|
||||
},
|
||||
"&:hover::before, &:hover::after": {
|
||||
borderColor: "#000"
|
||||
},
|
||||
width: 24,
|
||||
height: 24,
|
||||
display: "block",
|
||||
position: "relative"
|
||||
},
|
||||
titleClass: {
|
||||
padding: "0px 24px 12px"
|
||||
}
|
||||
});
|
||||
|
||||
const ModalWrapper = ({
|
||||
closeModalAndRefresh,
|
||||
modalOpen,
|
||||
title,
|
||||
children,
|
||||
classes
|
||||
}: IModalProps) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onClose={closeModalAndRefresh}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
maxWidth={"md"}
|
||||
fullWidth
|
||||
>
|
||||
<div className={classes.dialogContainer}>
|
||||
<div className={classes.closeContainer}>
|
||||
<IconButton
|
||||
aria-label="close"
|
||||
className={classes.closeButton}
|
||||
onClick={closeModalAndRefresh}
|
||||
disableRipple
|
||||
>
|
||||
<span className={classes.closeIcon} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<DialogTitle id="alert-dialog-title" className={classes.titleClass}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>{children}</DialogContent>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default withStyles(styles)(ModalWrapper);
|
||||
@@ -15,6 +15,7 @@
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import React, { useState, useEffect } from "react";
|
||||
import get from "lodash/get";
|
||||
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import { LinearProgress } from "@material-ui/core";
|
||||
import TableContainer from "@material-ui/core/TableContainer";
|
||||
@@ -86,7 +87,8 @@ const styles = (theme: Theme) =>
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
zIndex: 500
|
||||
},
|
||||
noFound: {
|
||||
textAlign: "center",
|
||||
@@ -96,7 +98,7 @@ const styles = (theme: Theme) =>
|
||||
maxHeight: 250
|
||||
},
|
||||
stickyHeader: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "#fff"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -122,13 +124,15 @@ const UsersSelectors = ({
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const selUsers = !selectedUsers ? [] : selectedUsers;
|
||||
|
||||
//Fetch Actions
|
||||
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const targetD = e.target;
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...selectedUsers]; // We clone the selectedGroups array
|
||||
let elements: string[] = [...selUsers]; // We clone the selectedGroups array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to selectedGroupsList
|
||||
@@ -146,7 +150,13 @@ const UsersSelectors = ({
|
||||
api
|
||||
.invoke("GET", `/api/v1/users`)
|
||||
.then((res: UsersList) => {
|
||||
setRecords(res.users.sort(usersSort));
|
||||
let users = get(res, "users", []);
|
||||
|
||||
if (!users) {
|
||||
users = [];
|
||||
}
|
||||
|
||||
setRecords(users.sort(usersSort));
|
||||
setError("");
|
||||
isLoading(false);
|
||||
})
|
||||
@@ -211,7 +221,7 @@ const UsersSelectors = ({
|
||||
"aria-label": "secondary checkbox"
|
||||
}}
|
||||
onChange={selectionChanged}
|
||||
checked={selectedUsers.includes(row.accessKey)}
|
||||
checked={selUsers.includes(row.accessKey)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
|
||||
@@ -29,6 +29,9 @@ import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
|
||||
import api from "../../../common/api";
|
||||
import { User } from "./types";
|
||||
import GroupsSelectors from "./GroupsSelectors";
|
||||
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
|
||||
import InputBoxWrapper from "../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
|
||||
import RadioGroupSelector from "../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
|
||||
|
||||
const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -40,6 +43,9 @@ const styles = (theme: Theme) =>
|
||||
},
|
||||
keyName: {
|
||||
marginLeft: 5
|
||||
},
|
||||
buttonContainer: {
|
||||
textAlign: "right"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,6 +53,7 @@ interface IAddUserContentProps {
|
||||
classes: any;
|
||||
closeModalAndRefresh: () => void;
|
||||
selectedUser: User | null;
|
||||
open: boolean;
|
||||
}
|
||||
|
||||
interface IAddUserContentState {
|
||||
@@ -57,6 +64,7 @@ interface IAddUserContentState {
|
||||
selectedGroups: string[];
|
||||
loadingGroups: boolean;
|
||||
groupsList: any[];
|
||||
enabled: string;
|
||||
}
|
||||
|
||||
class AddUserContent extends React.Component<
|
||||
@@ -70,23 +78,32 @@ class AddUserContent extends React.Component<
|
||||
secretKey: "",
|
||||
selectedGroups: [],
|
||||
loadingGroups: false,
|
||||
groupsList: []
|
||||
groupsList: [],
|
||||
enabled: "enabled"
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
const { selectedUser } = this.props;
|
||||
if (selectedUser !== null) {
|
||||
console.log("selUsr", selectedUser);
|
||||
if (selectedUser == null) {
|
||||
this.setState({
|
||||
accessKey: selectedUser.accessKey,
|
||||
secretKey: ""
|
||||
accessKey: "",
|
||||
secretKey: "",
|
||||
selectedGroups: []
|
||||
});
|
||||
} else {
|
||||
this.getUserInformation();
|
||||
}
|
||||
}
|
||||
|
||||
saveRecord(event: React.FormEvent) {
|
||||
event.preventDefault();
|
||||
const { accessKey, addLoading, secretKey, selectedGroups } = this.state;
|
||||
const {
|
||||
accessKey,
|
||||
addLoading,
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
enabled
|
||||
} = this.state;
|
||||
const { selectedUser } = this.props;
|
||||
if (addLoading) {
|
||||
return;
|
||||
@@ -95,8 +112,7 @@ class AddUserContent extends React.Component<
|
||||
if (selectedUser !== null) {
|
||||
api
|
||||
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
|
||||
accessKey,
|
||||
secretKey: secretKey !== "" ? null : secretKey,
|
||||
status: enabled,
|
||||
groups: selectedGroups
|
||||
})
|
||||
.then(res => {
|
||||
@@ -145,6 +161,33 @@ class AddUserContent extends React.Component<
|
||||
});
|
||||
}
|
||||
|
||||
getUserInformation() {
|
||||
const { selectedUser } = this.props;
|
||||
|
||||
if (!selectedUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
api
|
||||
.invoke("GET", `/api/v1/users/${selectedUser.accessKey}`)
|
||||
.then(res => {
|
||||
console.log(res);
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: "",
|
||||
accessKey: res.accessKey,
|
||||
selectedGroups: res.memberOf,
|
||||
enabled: res.status
|
||||
});
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({
|
||||
addLoading: false,
|
||||
addError: err
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, selectedUser } = this.props;
|
||||
const {
|
||||
@@ -154,15 +197,19 @@ class AddUserContent extends React.Component<
|
||||
secretKey,
|
||||
selectedGroups,
|
||||
loadingGroups,
|
||||
groupsList
|
||||
groupsList,
|
||||
enabled
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{selectedUser !== null ? "Edit User" : "Add User"}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<ModalWrapper
|
||||
closeModalAndRefresh={() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}}
|
||||
modalOpen={this.props.open}
|
||||
title={selectedUser !== null ? "Edit User" : "Add User"}
|
||||
>
|
||||
<React.Fragment>
|
||||
<form
|
||||
noValidate
|
||||
autoComplete="off"
|
||||
@@ -183,45 +230,44 @@ class AddUserContent extends React.Component<
|
||||
</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>
|
||||
)}
|
||||
<InputBoxWrapper
|
||||
id="accesskey-input"
|
||||
name="accesskey-input"
|
||||
label="Access Key"
|
||||
value={accessKey}
|
||||
onChangeFunc={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ accessKey: e.target.value });
|
||||
}}
|
||||
disabled={selectedUser !== null}
|
||||
/>
|
||||
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
{selectedUser !== null ? (
|
||||
<RadioGroupSelector
|
||||
currentSelection={enabled}
|
||||
id="user-status"
|
||||
name="user-status"
|
||||
label="Status"
|
||||
onChange={e => {
|
||||
this.setState({ enabled: e.target.value });
|
||||
}}
|
||||
selectorOptions={[
|
||||
{ label: "Enabled", value: "enabled" },
|
||||
{ label: "Disabled", value: "disabled" }
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<InputBoxWrapper
|
||||
id="standard-multiline-static"
|
||||
name="standard-multiline-static"
|
||||
label="Secret Key"
|
||||
type="password"
|
||||
value={secretKey}
|
||||
onChangeFunc={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({ secretKey: e.target.value });
|
||||
}}
|
||||
autoComplete="current-password"
|
||||
/>
|
||||
)}
|
||||
<Grid item xs={12}>
|
||||
<GroupsSelectors
|
||||
selectedGroups={selectedGroups}
|
||||
@@ -237,12 +283,11 @@ class AddUserContent extends React.Component<
|
||||
<Grid item xs={12}>
|
||||
<br />
|
||||
</Grid>
|
||||
<Grid item xs={12}>
|
||||
<Grid item xs={12} className={classes.buttonContainer}>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={addLoading}
|
||||
>
|
||||
Save
|
||||
@@ -255,8 +300,8 @@ class AddUserContent extends React.Component<
|
||||
)}
|
||||
</Grid>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</React.Fragment>
|
||||
</React.Fragment>
|
||||
</ModalWrapper>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -275,19 +320,7 @@ class AddUser extends React.Component<IAddUserProps, IAddUserState> {
|
||||
state: IAddUserState = {};
|
||||
|
||||
render() {
|
||||
const { open } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={() => {
|
||||
this.props.closeModalAndRefresh();
|
||||
}}
|
||||
aria-labelledby="alert-dialog-title"
|
||||
aria-describedby="alert-dialog-description"
|
||||
>
|
||||
<AddUserWrapper {...this.props} />
|
||||
</Dialog>
|
||||
);
|
||||
return <AddUserWrapper {...this.props} />;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import Checkbox from "@material-ui/core/Checkbox";
|
||||
import api from "../../../common/api";
|
||||
import { groupsSort } from "../../../utils/sortFunctions";
|
||||
import { GroupsList } from "../Groups/types";
|
||||
import get from "lodash/get";
|
||||
|
||||
interface IGroupsProps {
|
||||
classes: any;
|
||||
@@ -88,17 +89,18 @@ const styles = (theme: Theme) =>
|
||||
padding: 12,
|
||||
borderRadius: 5,
|
||||
boxShadow: "0px 3px 6px #00000012",
|
||||
width: "100%"
|
||||
width: "100%",
|
||||
zIndex: 500
|
||||
},
|
||||
noFound: {
|
||||
textAlign: "center",
|
||||
padding: "10px 0"
|
||||
},
|
||||
tableContainer: {
|
||||
maxHeight: 250
|
||||
maxHeight: 200
|
||||
},
|
||||
stickyHeader: {
|
||||
backgroundColor: "transparent"
|
||||
backgroundColor: "#fff"
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,11 +126,18 @@ const GroupsSelectors = ({
|
||||
}
|
||||
}, [loading]);
|
||||
|
||||
const selGroups = !selectedGroups ? [] : selectedGroups;
|
||||
|
||||
const fetchGroups = () => {
|
||||
api
|
||||
.invoke("GET", `/api/v1/groups`)
|
||||
.then((res: GroupsList) => {
|
||||
setRecords(res.groups.sort(groupsSort));
|
||||
let groups = get(res, "groups", []);
|
||||
|
||||
if (!groups) {
|
||||
groups = [];
|
||||
}
|
||||
setRecords(groups.sort(groupsSort));
|
||||
setError("");
|
||||
isLoading(false);
|
||||
})
|
||||
@@ -143,7 +152,7 @@ const GroupsSelectors = ({
|
||||
const value = targetD.value;
|
||||
const checked = targetD.checked;
|
||||
|
||||
let elements: string[] = [...selectedGroups]; // We clone the selectedGroups array
|
||||
let elements: string[] = [...selGroups]; // We clone the selectedGroups array
|
||||
|
||||
if (checked) {
|
||||
// If the user has checked this field we need to push this to selectedGroupsList
|
||||
@@ -212,7 +221,7 @@ const GroupsSelectors = ({
|
||||
"aria-label": "secondary checkbox"
|
||||
}}
|
||||
onChange={selectionChanged}
|
||||
checked={selectedGroups.includes(groupName)}
|
||||
checked={selGroups.includes(groupName)}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className={classes.wrapCell}>
|
||||
|
||||
@@ -215,14 +215,24 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<AddUser
|
||||
open={addScreenOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeModalAndRefresh={() => {
|
||||
this.closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
|
||||
{addScreenOpen && (
|
||||
<AddUser
|
||||
open={addScreenOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeModalAndRefresh={() => {
|
||||
this.closeAddModalAndRefresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{deleteOpen && (
|
||||
<DeleteUser
|
||||
deleteOpen={deleteOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
this.closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Grid container>
|
||||
<Grid item xs={12}>
|
||||
<Typography variant="h6">Users</Typography>
|
||||
@@ -355,13 +365,6 @@ class Users extends React.Component<IUsersProps, IUsersState> {
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<DeleteUser
|
||||
deleteOpen={deleteOpen}
|
||||
selectedUser={selectedUser}
|
||||
closeDeleteModalAndRefresh={(refresh: boolean) => {
|
||||
this.closeDeleteModalAndRefresh(refresh);
|
||||
}}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package restapi
|
||||
|
||||
import (
|
||||
"github.com/go-openapi/errors"
|
||||
"github.com/go-openapi/runtime/middleware"
|
||||
"github.com/go-openapi/swag"
|
||||
"github.com/minio/mcs/restapi/operations"
|
||||
@@ -27,7 +28,6 @@ import (
|
||||
"github.com/minio/minio/pkg/madmin"
|
||||
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
)
|
||||
|
||||
@@ -74,6 +74,15 @@ func registerUsersHandlers(api *operations.McsAPI) {
|
||||
|
||||
return admin_api.NewGetUserOK().WithPayload(userInfoResponse)
|
||||
})
|
||||
// Update User
|
||||
api.AdminAPIUpdateUserInfoHandler = admin_api.UpdateUserInfoHandlerFunc(func(params admin_api.UpdateUserInfoParams, principal *models.Principal) middleware.Responder {
|
||||
userUpdateResponse, err := getUpdateUserResponse(params)
|
||||
if err != nil {
|
||||
return admin_api.NewUpdateUserInfoDefault(500).WithPayload(&models.Error{Code: 500, Message: swag.String(err.Error())})
|
||||
}
|
||||
|
||||
return admin_api.NewUpdateUserInfoOK().WithPayload(userUpdateResponse)
|
||||
})
|
||||
}
|
||||
|
||||
func listUsers(ctx context.Context, client MinioAdmin) ([]*models.User, error) {
|
||||
@@ -304,7 +313,7 @@ func updateUserGroups(ctx context.Context, client MinioAdmin, user string, group
|
||||
}
|
||||
|
||||
if channelHasError {
|
||||
errRt := errors.New("there was an error updating the groups")
|
||||
errRt := errors.New(500, "there was an error updating the groups")
|
||||
return nil, errRt
|
||||
}
|
||||
|
||||
@@ -345,3 +354,51 @@ func getUpdateUserGroupsResponse(params admin_api.UpdateUserGroupsParams) (*mode
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// setUserStatus invokes setUserStatus from madmin to update user status
|
||||
func setUserStatus(ctx context.Context, client MinioAdmin, user string, status string) error {
|
||||
var setStatus madmin.AccountStatus
|
||||
switch status {
|
||||
case "enabled":
|
||||
setStatus = madmin.AccountEnabled
|
||||
case "disabled":
|
||||
setStatus = madmin.AccountDisabled
|
||||
default:
|
||||
return errors.New(500, "status not valid")
|
||||
}
|
||||
|
||||
if err := client.setUserStatus(ctx, user, setStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUpdateUserResponse(params admin_api.UpdateUserInfoParams) (*models.User, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
mAdmin, err := newMAdminClient()
|
||||
if err != nil {
|
||||
log.Println("error creating Madmin Client:", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// create a minioClient interface implementation
|
||||
// defining the client to be used
|
||||
adminClient := adminClient{client: mAdmin}
|
||||
|
||||
name := params.Name
|
||||
status := *params.Body.Status
|
||||
groups := params.Body.Groups
|
||||
|
||||
if err := setUserStatus(ctx, adminClient, name, status); err != nil {
|
||||
log.Println("error updating user status:", status)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userElem, errUG := updateUserGroups(ctx, adminClient, name, groups)
|
||||
|
||||
if errUG != nil {
|
||||
return nil, errUG
|
||||
}
|
||||
return userElem, nil
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ var minioListUsersMock func() (map[string]madmin.UserInfo, error)
|
||||
var minioAddUserMock func(accessKey, secreyKey string) error
|
||||
var minioRemoveUserMock func(accessKey string) error
|
||||
var minioGetUserInfoMock func(accessKey string) (madmin.UserInfo, error)
|
||||
var minioSetUserStatusMock func(accessKey string, status madmin.AccountStatus) error
|
||||
|
||||
// mock function of listUsers()
|
||||
func (ac adminClientMock) listUsers(ctx context.Context) (map[string]madmin.UserInfo, error) {
|
||||
@@ -54,6 +55,11 @@ func (ac adminClientMock) getUserInfo(ctx context.Context, accessKey string) (ma
|
||||
return minioGetUserInfoMock(accessKey)
|
||||
}
|
||||
|
||||
//mock function of setUserStatus()
|
||||
func (ac adminClientMock) setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error {
|
||||
return minioSetUserStatusMock(accessKey, status)
|
||||
}
|
||||
|
||||
func TestListUsers(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
@@ -321,3 +327,44 @@ func TestGetUserInfo(t *testing.T) {
|
||||
assert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetUserStatus(t *testing.T) {
|
||||
assert := asrt.New(t)
|
||||
adminClient := adminClientMock{}
|
||||
function := "setUserStatus()"
|
||||
userName := "userName123"
|
||||
ctx := context.Background()
|
||||
|
||||
// Test-1: setUserStatus() update valid disabled status
|
||||
expectedStatus := "disabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// Test-2: setUserStatus() update valid enabled status
|
||||
expectedStatus = "enabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); err != nil {
|
||||
t.Errorf("Failed on %s:, error occurred: %s", function, err.Error())
|
||||
}
|
||||
// Test-3: setUserStatus() update invalid status, should send error
|
||||
expectedStatus = "invalid"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
return nil
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); assert.Error(err) {
|
||||
assert.Equal("status not valid", err.Error())
|
||||
}
|
||||
// Test-4: setUserStatus() handler error correctly
|
||||
expectedStatus = "enabled"
|
||||
minioSetUserStatusMock = func(accessKey string, status madmin.AccountStatus) error {
|
||||
return errors.New("error")
|
||||
}
|
||||
if err := setUserStatus(ctx, adminClient, userName, expectedStatus); assert.Error(err) {
|
||||
assert.Equal("error", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ type MinioAdmin interface {
|
||||
addUser(ctx context.Context, acessKey, SecretKey string) error
|
||||
removeUser(ctx context.Context, accessKey string) error
|
||||
getUserInfo(ctx context.Context, accessKey string) (madmin.UserInfo, error)
|
||||
setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error
|
||||
listGroups(ctx context.Context) ([]string, error)
|
||||
updateGroupMembers(ctx context.Context, greq madmin.GroupAddRemove) error
|
||||
getGroupDescription(ctx context.Context, group string) (*madmin.GroupDesc, error)
|
||||
@@ -105,6 +106,11 @@ func (ac adminClient) getUserInfo(ctx context.Context, accessKey string) (madmin
|
||||
return ac.client.GetUserInfo(ctx, accessKey)
|
||||
}
|
||||
|
||||
// implements madmin.SetUserStatus()
|
||||
func (ac adminClient) setUserStatus(ctx context.Context, accessKey string, status madmin.AccountStatus) error {
|
||||
return ac.client.SetUserStatus(ctx, accessKey, status)
|
||||
}
|
||||
|
||||
// implements madmin.ListGroups()
|
||||
func (ac adminClient) listGroups(ctx context.Context) ([]string, error) {
|
||||
return ac.client.ListGroups(ctx)
|
||||
|
||||
Reference in New Issue
Block a user