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:
Alex
2020-04-11 00:13:31 -05:00
committed by GitHub
parent 5c5e84b289
commit 1b1ed55252
14 changed files with 797 additions and 224 deletions

1
go.sum
View File

@@ -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

View File

@@ -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">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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