UI: Add events to buckets (#22)

* UI: Add events to buckets

* Menu user icon

Co-authored-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Daniel Valdivia
2020-04-03 15:00:34 -07:00
committed by GitHub
parent c6f7a5b995
commit c3c22fc77f
14 changed files with 1383 additions and 839 deletions

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,330 @@
// 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, { ChangeEvent } from "react";
import Grid from "@material-ui/core/Grid";
import Title from "../../../../common/Title";
import Typography from "@material-ui/core/Typography";
import {
Button,
Dialog,
DialogContent,
DialogTitle,
FormControl,
InputLabel,
LinearProgress,
MenuItem,
Select,
TextField
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import TableCell from "@material-ui/core/TableCell";
import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox";
import Table from "@material-ui/core/Table";
import { ArnList, BucketEventList } from "../types";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
}
}
});
interface IAddEventProps {
classes: any;
open: boolean;
selectedBucket: string;
closeModalAndRefresh: () => void;
}
interface IAddEventState {
addLoading: boolean;
addError: string;
prefix: string;
suffix: string;
arn: string;
selectedEvents: string[];
arnList: string[];
}
class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
state: IAddEventState = {
addLoading: false,
addError: "",
prefix: "",
suffix: "",
arn: "",
selectedEvents: [],
arnList: []
};
addRecord(event: React.FormEvent) {
event.preventDefault();
const { prefix, suffix, addLoading, arn, selectedEvents } = this.state;
const { selectedBucket } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
api
.invoke("POST", `/api/v1/buckets/${selectedBucket}/events`, {
configuration: {
arn: arn,
events: selectedEvents,
prefix: prefix,
sufix: suffix
},
ignoreExisting: true
})
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
});
}
fetchArnList() {
this.setState({ addLoading: true }, () => {
api
.invoke("GET", `/api/v1/admin/arns`)
.then((res: ArnList) => {
this.setState({
addLoading: false,
arnList: res.arns,
addError: ""
});
})
.catch((err: any) => {
this.setState({ addLoading: false, addError: err });
});
});
}
componentDidMount(): void {
this.fetchArnList();
}
render() {
const { classes, open } = this.props;
const { addLoading, addError, arn, selectedEvents, arnList } = this.state;
const events = [
{ label: "PUT - Object Uploaded", value: "put" },
{ label: "GET - Object accessed", value: "get" },
{ label: "DELETE - Object Deleted", value: "delete" }
];
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedEvents]; // 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);
}
this.setState({ selectedEvents: selectedEvents });
return elements;
};
const handleClick = (
event: React.MouseEvent<unknown> | ChangeEvent<unknown>,
name: string
) => {
const selectedIndex = selectedEvents.indexOf(name);
let newSelected: string[] = [];
if (selectedIndex === -1) {
newSelected = newSelected.concat(selectedEvents, name);
} else if (selectedIndex === 0) {
newSelected = newSelected.concat(selectedEvents.slice(1));
} else if (selectedIndex === selectedEvents.length - 1) {
newSelected = newSelected.concat(selectedEvents.slice(0, -1));
} else if (selectedIndex > 0) {
newSelected = newSelected.concat(
selectedEvents.slice(0, selectedIndex),
selectedEvents.slice(selectedIndex + 1)
);
}
this.setState({ selectedEvents: newSelected });
};
return (
<Dialog
open={open}
onClose={() => {
this.setState({ addError: "" }, () => {
this.props.closeModalAndRefresh();
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">
<Title>Subscribe To Event</Title>
</DialogTitle>
<DialogContent>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
this.addRecord(e);
}}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<FormControl className={classes.formControl} fullWidth>
<InputLabel id="select-access-policy">ARN</InputLabel>
<Select
labelId="select-access-policy"
id="select-access-policy"
value={arn}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ arn: e.target.value as string });
}}
>
{arnList.map(arn => (
<MenuItem value={arn}>{arn}</MenuItem>
))}
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Event</TableCell>
</TableRow>
</TableHead>
<TableBody>
{events.map(row => (
<TableRow
key={`group-${row.value}`}
onClick={event => handleClick(event, row.value)}
>
<TableCell padding="checkbox">
<Checkbox
value={row.value}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={event => handleClick(event, row.value)}
checked={selectedEvents.includes(row.value)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.label}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Prefix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ prefix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Suffix"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ suffix: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
>
Save
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</DialogContent>
</Dialog>
);
}
}
export default withStyles(styles)(AddEvent);

View File

@@ -0,0 +1,160 @@
// 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 { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
} from "@material-ui/core";
import api from "../../../../common/api";
import { BucketEvent, BucketList } from "../types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
});
interface IDeleteEventProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedBucket: string;
bucketEvent: BucketEvent | null;
}
interface IDeleteEventState {
deleteLoading: boolean;
deleteError: string;
}
class DeleteEvent extends React.Component<
IDeleteEventProps,
IDeleteEventState
> {
state: IDeleteEventState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedBucket, bucketEvent } = this.props;
if (deleteLoading) {
return;
}
if (bucketEvent == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke(
"DELETE",
`/api/v1/buckets/${selectedBucket}/events/${bucketEvent.id}`,
{
name: selectedBucket
}
)
.then((res: BucketList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
});
}
render() {
const { classes, deleteOpen, selectedBucket } = this.props;
const { deleteLoading, deleteError } = this.state;
return (
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Bucket</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete this event?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button
onClick={() => {
this.removeRecord();
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(DeleteEvent);

View File

@@ -37,6 +37,9 @@ import DeleteIcon from "@material-ui/icons/Delete";
import SetAccessPolicy from "./SetAccessPolicy"; import SetAccessPolicy from "./SetAccessPolicy";
import DeleteBucket from "../ListBuckets/DeleteBucket"; import DeleteBucket from "../ListBuckets/DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions"; import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -102,8 +105,10 @@ interface IViewBucketState {
setAccessPolicyScreenOpen: boolean; setAccessPolicyScreenOpen: boolean;
page: number; page: number;
rowsPerPage: number; rowsPerPage: number;
addScreenOpen: boolean;
deleteOpen: boolean; deleteOpen: boolean;
selectedBucket: string; selectedBucket: string;
selectedEvent: BucketEvent | null;
} }
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> { class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -117,11 +122,13 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
setAccessPolicyScreenOpen: false, setAccessPolicyScreenOpen: false,
page: 0, page: 0,
rowsPerPage: 10, rowsPerPage: 10,
addScreenOpen: false,
deleteOpen: false, deleteOpen: false,
selectedBucket: "" selectedBucket: "",
selectedEvent: null
}; };
fetchRecords() { fetchEvents() {
this.setState({ loading: true }, () => { this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state; const { page, rowsPerPage } = this.state;
const { match } = this.props; const { match } = this.props;
@@ -144,7 +151,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
) { ) {
const newPage = page - 1; const newPage = page - 1;
this.setState({ page: newPage }, () => { this.setState({ page: newPage }, () => {
this.fetchRecords(); this.fetchEvents();
}); });
} }
}) })
@@ -163,7 +170,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
closeDeleteModalAndRefresh(refresh: boolean) { closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => { this.setState({ deleteOpen: false }, () => {
if (refresh) { if (refresh) {
this.fetchRecords(); this.fetchEvents();
} }
}); });
} }
@@ -181,7 +188,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
componentDidMount(): void { componentDidMount(): void {
this.loadInfo(); this.loadInfo();
this.fetchRecords(); this.fetchEvents();
} }
bucketFilter(): void {} bucketFilter(): void {}
@@ -197,7 +204,9 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
page, page,
rowsPerPage, rowsPerPage,
deleteOpen, deleteOpen,
selectedBucket addScreenOpen,
selectedBucket,
selectedEvent
} = this.state; } = this.state;
const offset = page * rowsPerPage; const offset = page * rowsPerPage;
@@ -215,8 +224,8 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
this.setState({ page: 0, rowsPerPage: rPP }); this.setState({ page: 0, rowsPerPage: rPP });
}; };
const confirmDeleteEvent = (bucket: string) => { const confirmDeleteEvent = (evnt: BucketEvent) => {
this.setState({ deleteOpen: true, selectedBucket: bucket }); this.setState({ deleteOpen: true, selectedEvent: evnt });
}; };
let accessPolicy = "n/a"; let accessPolicy = "n/a";
@@ -226,6 +235,14 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
return ( return (
<React.Fragment> <React.Fragment>
<AddEvent
open={addScreenOpen}
selectedBucket={bucketName}
closeModalAndRefresh={() => {
this.setState({ addScreenOpen: false });
this.fetchEvents();
}}
/>
<SetAccessPolicy <SetAccessPolicy
bucketName={bucketName} bucketName={bucketName}
open={setAccessPolicyScreenOpen} open={setAccessPolicyScreenOpen}
@@ -267,7 +284,21 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<Grid item xs={6}> <Grid item xs={6}>
<Typography variant="h6">Events</Typography> <Typography variant="h6">Events</Typography>
</Grid> </Grid>
<Grid item xs={6} className={classes.actionsTray} /> <Grid item xs={6} className={classes.actionsTray}>
<Button
variant="contained"
color="primary"
size="small"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Subcribe to Event
</Button>
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
@@ -296,7 +327,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<IconButton <IconButton
aria-label="delete" aria-label="delete"
onClick={() => { onClick={() => {
confirmDeleteEvent(row.id); confirmDeleteEvent(row);
}} }}
> >
<DeleteIcon /> <DeleteIcon />
@@ -331,9 +362,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
</Grid> </Grid>
</Grid> </Grid>
<DeleteBucket <DeleteEvent
deleteOpen={deleteOpen} deleteOpen={deleteOpen}
selectedBucket={selectedBucket} selectedBucket={bucketName}
bucketEvent={selectedEvent}
closeDeleteModalAndRefresh={(refresh: boolean) => { closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh); this.closeDeleteModalAndRefresh(refresh);
}} }}

View File

@@ -41,3 +41,7 @@ export interface BucketEventList {
events: BucketEvent[]; events: BucketEvent[];
total: number; total: number;
} }
export interface ArnList {
arns: string[];
}

View File

@@ -16,228 +16,241 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {Button, Dialog, DialogContent, DialogTitle, LinearProgress, TextField} from "@material-ui/core"; import {
import Radio from '@material-ui/core/Radio'; Button,
import RadioGroup from '@material-ui/core/RadioGroup'; Dialog,
import FormControlLabel from '@material-ui/core/FormControlLabel'; DialogContent,
DialogTitle,
LinearProgress,
TextField
} from "@material-ui/core";
import Radio from "@material-ui/core/Radio";
import RadioGroup from "@material-ui/core/RadioGroup";
import FormControlLabel from "@material-ui/core/FormControlLabel";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import api from "../../../common/api"; import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors"; import UsersSelectors from "./UsersSelectors";
import {GroupsList} from "./types"; import { GroupsList } from "./types";
import {groupsSort} from "../../../utils/sortFunctions"; import { groupsSort } from "../../../utils/sortFunctions";
import Title from "../../../common/Title"; import Title from "../../../common/Title";
interface IGroupProps { interface IGroupProps {
open: boolean; open: boolean;
selectedGroup: any; selectedGroup: any;
closeModalAndRefresh: any; closeModalAndRefresh: any;
classes: any; classes: any;
} }
interface MainGroupProps { interface MainGroupProps {
members: string[]; members: string[];
name: string; name: string;
status: string; status: string;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
errorBlock: { errorBlock: {
color: "red" color: "red"
}, },
strongText: { strongText: {
fontWeight: 700, fontWeight: 700
}, },
keyName: { keyName: {
marginLeft: 5 marginLeft: 5
} }
}); });
const AddGroup = ({ const AddGroup = ({
open, open,
selectedGroup, selectedGroup,
closeModalAndRefresh, closeModalAndRefresh,
classes, classes
}: IGroupProps) => { }: IGroupProps) => {
//Local States
const [groupName, setGroupName] = useState<string>("");
const [groupEnabled, setGroupEnabled] = useState<string>("");
const [saving, isSaving] = useState<boolean>(false);
const [addError, setError] = useState<string>("");
const [selectedUsers, setSelectedUsers] = useState<string[]>([]);
const [loadingGroup, isLoadingGroup] = useState<boolean>(false);
//Local States //Effects
const [groupName, setGroupName] = useState<string>(""); useEffect(() => {
const [groupEnabled, setGroupEnabled] = useState<string>(""); if (selectedGroup !== null) {
const [saving, isSaving] = useState<boolean>(false); isLoadingGroup(true);
const [addError, setError] = useState<string>(""); } else {
const [selectedUsers, setSelectedUsers] = useState<string[]>([]); setGroupName("");
const [loadingGroup, isLoadingGroup] = useState<boolean>(false); setSelectedUsers([]);
//Effects
useEffect(() => {
if(selectedGroup !== null) {
isLoadingGroup(true);
} else {
setGroupName("");
setSelectedUsers([]);
}
}, [selectedGroup]);
useEffect(() => {
if(saving) {
saveRecord();
}
}, [saving]);
useEffect(() => {
if(selectedGroup && loadingGroup) {
fetchGroupInfo();
}
}, [loadingGroup]);
//Fetch Actions
const setSaving = (event: React.FormEvent) => {
event.preventDefault();
isSaving(true);
} }
}, [selectedGroup]);
const saveRecord = () => { useEffect(() => {
if (selectedGroup !== null) { if (saving) {
api saveRecord();
.invoke("PUT", `/api/v1/groups/${groupName}`, { }
group: groupName, }, [saving]);
members: selectedUsers,
status: groupEnabled,
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
} else {
api.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers,
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
}
};
const fetchGroupInfo = () => { useEffect(() => {
api if (selectedGroup && loadingGroup) {
.invoke("GET", `/api/v1/groups/${selectedGroup}`) fetchGroupInfo();
.then((res: MainGroupProps) => { }
setGroupEnabled(res.status); }, [loadingGroup]);
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
setError(err);
isLoadingGroup(false);
});
};
return (<Dialog //Fetch Actions
open={open} const setSaving = (event: React.FormEvent) => {
onClose={closeModalAndRefresh} event.preventDefault();
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description" isSaving(true);
};
const saveRecord = () => {
if (selectedGroup !== null) {
api
.invoke("PUT", `/api/v1/groups/${groupName}`, {
group: groupName,
members: selectedUsers,
status: groupEnabled
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
} else {
api
.invoke("POST", "/api/v1/groups", {
group: groupName,
members: selectedUsers
})
.then(res => {
isSaving(false);
setError("");
closeModalAndRefresh();
})
.catch(err => {
isSaving(false);
setError(err);
});
}
};
const fetchGroupInfo = () => {
api
.invoke("GET", `/api/v1/groups/${selectedGroup}`)
.then((res: MainGroupProps) => {
setGroupEnabled(res.status);
setGroupName(res.name);
setSelectedUsers(res.members);
})
.catch(err => {
setError(err);
isLoadingGroup(false);
});
};
return (
<Dialog
open={open}
onClose={closeModalAndRefresh}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
> >
<DialogTitle id="alert-dialog-title"> <DialogTitle id="alert-dialog-title">
{selectedGroup !== null ? `Group Edit - ${groupName}` : 'Add Group'} {selectedGroup !== null ? `Group Edit - ${groupName}` : "Add Group"}
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<form <form noValidate autoComplete="off" onSubmit={setSaving}>
noValidate <Grid container>
autoComplete="off" {addError !== "" && (
onSubmit={setSaving} <Grid item xs={12}>
> <Typography
<Grid container> component="p"
{addError !== "" && ( variant="body1"
<Grid item xs={12}> className={classes.errorBlock}
<Typography >
component="p" {addError}
variant="body1" </Typography>
className={classes.errorBlock} </Grid>
> )}
{addError}
</Typography>
</Grid>
)}
{selectedGroup !== null ? ( {selectedGroup !== null ? (
<React.Fragment> <React.Fragment>
<Grid item xs={12}> <Grid item xs={12}>
<Title>Status</Title> <Title>Status</Title>
<RadioGroup <RadioGroup
aria-label="status" aria-label="status"
name="status" name="status"
value={groupEnabled} value={groupEnabled}
onChange={(e) => { onChange={e => {
setGroupEnabled(e.target.value); setGroupEnabled(e.target.value);
}} }}
> >
<FormControlLabel value="enabled" control={<Radio color={'primary'} />} label="Enabled" /> <FormControlLabel
<FormControlLabel value="disabled" control={<Radio color={'primary'} />} label="Disabled" /> value="enabled"
</RadioGroup> control={<Radio color={"primary"} />}
</Grid> label="Enabled"
</React.Fragment> />
) : ( <FormControlLabel
<React.Fragment> value="disabled"
<Grid item xs={12}> control={<Radio color={"primary"} />}
<TextField label="Disabled"
id="standard-basic" />
fullWidth </RadioGroup>
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
{saving && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid> </Grid>
</form> </React.Fragment>
</DialogContent> ) : (
</Dialog>); <React.Fragment>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Name"
value={groupName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setGroupName(e.target.value);
}}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<UsersSelectors
selectedUsers={selectedUsers}
setSelectedUsers={setSelectedUsers}
/>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={saving}
>
Save
</Button>
</Grid>
{saving && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</DialogContent>
</Dialog>
);
}; };
export default withStyles(styles)(AddGroup); export default withStyles(styles)(AddGroup);

View File

@@ -17,118 +17,115 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { import {
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogTitle, DialogTitle,
LinearProgress LinearProgress
} from "@material-ui/core"; } from "@material-ui/core";
import api from "../../../common/api"; import api from "../../../common/api";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import {UsersList} from "../Users/types"; import { UsersList } from "../Users/types";
interface IDeleteGroup { interface IDeleteGroup {
selectedGroup: string; selectedGroup: string;
deleteOpen: boolean; deleteOpen: boolean;
closeDeleteModalAndRefresh: any; closeDeleteModalAndRefresh: any;
classes: any; classes: any;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
errorBlock: { errorBlock: {
color: "red" color: "red"
} }
}); });
const DeleteGroup = ({ const DeleteGroup = ({
selectedGroup, selectedGroup,
deleteOpen, deleteOpen,
closeDeleteModalAndRefresh, closeDeleteModalAndRefresh,
classes, classes
}:IDeleteGroup) => { }: IDeleteGroup) => {
const [isDeleting, setDeleteLoading] = useState<boolean>(false); const [isDeleting, setDeleteLoading] = useState<boolean>(false);
const [deleteError, setError] = useState<string>(""); const [deleteError, setError] = useState<string>("");
useEffect(() => { useEffect(() => {
if(isDeleting) { if (isDeleting) {
removeRecord(); removeRecord();
} }
}, [isDeleting]); }, [isDeleting]);
const removeRecord = () => { const removeRecord = () => {
if (!selectedGroup) { if (!selectedGroup) {
return; return;
} }
api api
.invoke("DELETE", `/api/v1/groups/${selectedGroup}`) .invoke("DELETE", `/api/v1/groups/${selectedGroup}`)
.then((res: UsersList) => { .then((res: UsersList) => {
setDeleteLoading(false); setDeleteLoading(false);
setError("");
closeDeleteModalAndRefresh(true);
})
.catch(err => {
setDeleteLoading(false);
setError(err);
});
};
const closeNoAction = () => {
setError(""); setError("");
closeDeleteModalAndRefresh(false);
};
return (<React.Fragment> closeDeleteModalAndRefresh(true);
<Dialog })
open={deleteOpen} .catch(err => {
onClose={closeNoAction} setDeleteLoading(false);
aria-labelledby="alert-dialog-title" setError(err);
aria-describedby="alert-dialog-description" });
> };
<DialogTitle id="alert-dialog-title">Delete User</DialogTitle>
<DialogContent> const closeNoAction = () => {
{isDeleting && <LinearProgress />} setError("");
<DialogContentText id="alert-dialog-description"> closeDeleteModalAndRefresh(false);
Are you sure you want to delete group <b>{selectedGroup}</b> };
?
{deleteError !== "" && ( return (
<React.Fragment> <React.Fragment>
<br /> <Dialog
<Typography open={deleteOpen}
component="p" onClose={closeNoAction}
variant="body1" aria-labelledby="alert-dialog-title"
className={classes.errorBlock} aria-describedby="alert-dialog-description"
> >
{deleteError} <DialogTitle id="alert-dialog-title">Delete User</DialogTitle>
</Typography> <DialogContent>
</React.Fragment> {isDeleting && <LinearProgress />}
)} <DialogContentText id="alert-dialog-description">
</DialogContentText> Are you sure you want to delete group <b>{selectedGroup}</b>?
</DialogContent> {deleteError !== "" && (
<DialogActions> <React.Fragment>
<Button <br />
onClick={closeNoAction} <Typography
color="primary" component="p"
disabled={isDeleting} variant="body1"
className={classes.errorBlock}
> >
Cancel {deleteError}
</Button> </Typography>
<Button </React.Fragment>
onClick={() => { )}
setDeleteLoading(true); </DialogContentText>
}} </DialogContent>
color="secondary" <DialogActions>
autoFocus <Button onClick={closeNoAction} color="primary" disabled={isDeleting}>
> Cancel
Delete </Button>
</Button> <Button
</DialogActions> onClick={() => {
</Dialog> setDeleteLoading(true);
</React.Fragment>); }}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
</React.Fragment>
);
}; };
export default withStyles(styles)(DeleteGroup) export default withStyles(styles)(DeleteGroup);

View File

@@ -21,7 +21,13 @@ import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment"; import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search"; import SearchIcon from "@material-ui/icons/Search";
import {Button, IconButton, LinearProgress, TableFooter, TablePagination} from "@material-ui/core"; import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
import TableHead from "@material-ui/core/TableHead"; import TableHead from "@material-ui/core/TableHead";
@@ -31,271 +37,270 @@ import TableBody from "@material-ui/core/TableBody";
import Checkbox from "@material-ui/core/Checkbox"; import Checkbox from "@material-ui/core/Checkbox";
import ViewIcon from "@material-ui/icons/Visibility"; import ViewIcon from "@material-ui/icons/Visibility";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
import {CreateIcon} from "../../../icons"; import { CreateIcon } from "../../../icons";
import api from "../../../common/api"; import api from "../../../common/api";
import {MinTablePaginationActions} from "../../../common/MinTablePaginationActions"; import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import {GroupsList} from "./types"; import { GroupsList } from "./types";
import {groupsSort, usersSort} from "../../../utils/sortFunctions"; import { groupsSort, usersSort } from "../../../utils/sortFunctions";
import {UsersList} from "../Users/types"; import { UsersList } from "../Users/types";
import AddGroup from "../Groups/AddGroup"; import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup"; import DeleteGroup from "./DeleteGroup";
interface IGroupsProps { interface IGroupsProps {
classes: any; classes: any;
openGroupModal: any; openGroupModal: any;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
seeMore: { seeMore: {
marginTop: theme.spacing(3) marginTop: theme.spacing(3)
}, },
paper: { paper: {
// padding: theme.spacing(2), // padding: theme.spacing(2),
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column" flexDirection: "column"
}, },
addSideBar: { addSideBar: {
width: "320px", width: "320px",
padding: "20px" padding: "20px"
}, },
errorBlock: { errorBlock: {
color: "red" color: "red"
}, },
tableToolbar: { tableToolbar: {
paddingLeft: theme.spacing(2), paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0) paddingRight: theme.spacing(0)
}, },
wrapCell: { wrapCell: {
maxWidth: "200px", maxWidth: "200px",
whiteSpace: "normal", whiteSpace: "normal",
wordWrap: "break-word" wordWrap: "break-word"
}, },
minTableHeader: { minTableHeader: {
color: "#393939", color: "#393939",
"& tr": { "& tr": {
"& th": { "& th": {
fontWeight:'bold' fontWeight: "bold"
}
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
} }
}); }
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
});
const Groups = ({ const Groups = ({ classes }: IGroupsProps) => {
classes, const [addGroupOpen, setGroupOpen] = useState<boolean>(false);
}: IGroupsProps) => { const [selectedGroup, setSelectedGroup] = useState<any>(null);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [loading, isLoading] = useState<boolean>(false);
const [records, setRecords] = useState<any[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [addGroupOpen, setGroupOpen] = useState<boolean>(false); const handleChangePage = (event: unknown, newPage: number) => {
const [selectedGroup, setSelectedGroup] = useState<any>(null); setPage(newPage);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false); };
const [loading, isLoading] = useState<boolean>(false);
const [records, setRecords] = useState<any[]>([]);
const [totalRecords, setTotalRecords] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const handleChangePage = (event: unknown, newPage: number) => { const handleChangeRowsPerPage = (
setPage(newPage); event: React.ChangeEvent<HTMLInputElement>
}; ) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const handleChangeRowsPerPage = ( useEffect(() => {
event: React.ChangeEvent<HTMLInputElement> isLoading(true);
) => { }, []);
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
useEffect(() => { useEffect(() => {
isLoading(true); isLoading(true);
}, []); }, [page, rowsPerPage]);
useEffect(() => { useEffect(() => {
isLoading(true); if (loading) {
}, [page, rowsPerPage]); fetchRecords();
}
}, [loading]);
useEffect(() => { const fetchRecords = () => {
if(loading) { const offset = page * rowsPerPage;
fetchRecords(); api
.invoke("GET", `/api/v1/groups?offset=${offset}&limit=${rowsPerPage}`)
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setTotalRecords(res.total);
setError("");
isLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.groups || res.groups.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
} }
}, [loading]); })
.catch(err => {
setError(err);
isLoading(false);
});
};
const fetchRecords = () => { const closeAddModalAndRefresh = () => {
const offset = page * rowsPerPage; setGroupOpen(false);
api isLoading(true);
.invoke("GET", `/api/v1/groups?offset=${offset}&limit=${rowsPerPage}`) };
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setTotalRecords(res.total);
setError("");
isLoading(false);
// if we get 0 results, and page > 0 , go down 1 page const closeDeleteModalAndRefresh = (refresh: boolean) => {
if ((!res.groups || res.groups.length === 0) && page > 0) { setDeleteOpen(false);
const newPage = page - 1;
setPage(newPage);
}
})
.catch(err => {
setError(err);
isLoading(false);
});
};
if (refresh) {
isLoading(true);
}
};
const filteredRecords = records.filter(elementItem =>
elementItem.includes(filter)
);
const closeAddModalAndRefresh = () => { return (
setGroupOpen(false); <React.Fragment>
isLoading(true); {addGroupOpen && (
}; <AddGroup
open={addGroupOpen}
const closeDeleteModalAndRefresh = (refresh: boolean) => { selectedGroup={selectedGroup}
setDeleteOpen(false); closeModalAndRefresh={closeAddModalAndRefresh}
/>
if (refresh) { )}
isLoading(true); {deleteOpen && (
} <DeleteGroup
}; deleteOpen={deleteOpen}
selectedGroup={selectedGroup}
const filteredRecords = records.filter((elementItem) => elementItem.includes(filter)); closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
return (<React.Fragment> )}
{ addGroupOpen && <Grid container>
<AddGroup <Grid item xs={12}>
open={addGroupOpen} <Typography variant="h6">Groups</Typography>
selectedGroup={selectedGroup}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
}
{ deleteOpen &&
<DeleteGroup
deleteOpen={deleteOpen}
selectedGroup={selectedGroup}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
onChange={(e) => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={ <CreateIcon /> }
onClick={() => {
setSelectedGroup(null);
setGroupOpen(true);
}}
>
Create Group
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map(group => (
<TableRow key={`user-${group}`}>
<TableCell className={classes.wrapCell}>
{group}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="view"
onClick={() => {
setGroupOpen(true);
setSelectedGroup(group);
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
setSelectedGroup(group);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Groups Available</div>
)}
</Paper>
</Grid>
</Grid> </Grid>
</React.Fragment>) <Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Groups"
className={classes.searchField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
onChange={e => {
setFilter(e.target.value);
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setSelectedGroup(null);
setGroupOpen(true);
}}
>
Create Group
</Button>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Name</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{filteredRecords.map(group => (
<TableRow key={`user-${group}`}>
<TableCell className={classes.wrapCell}>
{group}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="view"
onClick={() => {
setGroupOpen(true);
setSelectedGroup(group);
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
setDeleteOpen(true);
setSelectedGroup(group);
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
<TableFooter>
<TableRow>
<TablePagination
rowsPerPageOptions={[5, 10, 25]}
colSpan={3}
count={totalRecords}
rowsPerPage={rowsPerPage}
page={page}
SelectProps={{
inputProps: { "aria-label": "rows per page" },
native: true
}}
onChangePage={handleChangePage}
onChangeRowsPerPage={handleChangeRowsPerPage}
ActionsComponent={MinTablePaginationActions}
/>
</TableRow>
</TableFooter>
</Table>
) : (
<div>No Groups Available</div>
)}
</Paper>
</Grid>
</Grid>
</React.Fragment>
);
}; };
export default withStyles(styles)(Groups); export default withStyles(styles)(Groups);

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core"; import { LinearProgress } from "@material-ui/core";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@@ -31,163 +31,164 @@ import { usersSort } from "../../../utils/sortFunctions";
import api from "../../../common/api"; import api from "../../../common/api";
interface IGroupsProps { interface IGroupsProps {
classes: any; classes: any;
selectedUsers: string[]; selectedUsers: string[];
setSelectedUsers: any; setSelectedUsers: any;
} }
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
seeMore: { seeMore: {
marginTop: theme.spacing(3) marginTop: theme.spacing(3)
}, },
paper: { paper: {
// padding: theme.spacing(2), // padding: theme.spacing(2),
display: "flex", display: "flex",
overflow: "auto", overflow: "auto",
flexDirection: "column" flexDirection: "column"
}, },
addSideBar: { addSideBar: {
width: "320px", width: "320px",
padding: "20px" padding: "20px"
}, },
errorBlock: { errorBlock: {
color: "red" color: "red"
}, },
tableToolbar: { tableToolbar: {
paddingLeft: theme.spacing(2), paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0) paddingRight: theme.spacing(0)
}, },
wrapCell: { wrapCell: {
maxWidth: "200px", maxWidth: "200px",
whiteSpace: "normal", whiteSpace: "normal",
wordWrap: "break-word" wordWrap: "break-word"
}, },
minTableHeader: { minTableHeader: {
color: "#393939", color: "#393939",
"& tr": { "& tr": {
"& th": { "& th": {
fontWeight:'bold' 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",
} }
}); }
},
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 UsersSelectors = ({ const UsersSelectors = ({
classes, classes,
selectedUsers, selectedUsers,
setSelectedUsers, setSelectedUsers
}: IGroupsProps) => { }: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
//Local States //Effects
const [records, setRecords] = useState<any[]>([]); useEffect(() => {
const [loading, isLoading] = useState<boolean>(false); isLoading(true);
const [error, setError] = useState<string>(""); }, []);
//Effects useEffect(() => {
useEffect(() => { if (loading) {
isLoading(true); fetchUsers();
}, []); }
}, [loading]);
useEffect(() => { //Fetch Actions
if(loading) { const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
fetchUsers(); const targetD = e.target;
} const value = targetD.value;
},[loading]); const checked = targetD.checked;
//Fetch Actions let elements: string[] = [...selectedUsers]; // We clone the selectedGroups array
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 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);
}
setSelectedUsers(elements);
if(checked) { // If the user has checked this field we need to push this to selectedGroupsList return elements;
elements.push(value); };
} else { // User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
}
setSelectedUsers(elements);
return elements; const fetchUsers = () => {
}; api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
setRecords(res.users.sort(usersSort));
setError("");
isLoading(false);
})
.catch(err => {
setError(err);
isLoading(false);
});
};
const fetchUsers = () => { return (
api <React.Fragment>
.invoke("GET", `/api/v1/users`) <Title>Members</Title>
.then((res: UsersList) => { <Grid item xs={12}>
setRecords(res.users.sort(usersSort)); <Paper className={classes.paper}>
setError(""); {loading && <LinearProgress />}
isLoading(false); {records != null && records.length > 0 ? (
}) <React.Fragment>
.catch(err => { <Table size="medium">
setError(err); <TableHead className={classes.minTableHeader}>
isLoading(false); <TableRow>
}); <TableCell>Select</TableCell>
}; <TableCell>Access Key</TableCell>
</TableRow>
return ( </TableHead>
<React.Fragment> <TableBody>
<Title>Members</Title> {records.map(row => (
<Grid item xs={12}> <TableRow key={`group-${row.accessKey}`}>
<Paper className={classes.paper}> <TableCell padding="checkbox">
{loading && <LinearProgress />} <Checkbox
{records != null && records.length > 0 ? ( value={row.accessKey}
<React.Fragment> color="primary"
<Table size="medium"> inputProps={{
<TableHead className={classes.minTableHeader}> "aria-label": "secondary checkbox"
<TableRow> }}
<TableCell>Select</TableCell> onChange={selectionChanged}
<TableCell>Access Key</TableCell> checked={selectedUsers.includes(row.accessKey)}
</TableRow> />
</TableHead> </TableCell>
<TableBody> <TableCell className={classes.wrapCell}>
{records.map(row => ( {row.accessKey}
<TableRow key={`group-${row.accessKey}`}> </TableCell>
<TableCell padding="checkbox"> </TableRow>
<Checkbox ))}
value={row.accessKey} </TableBody>
color="primary" </Table>
inputProps={{ </React.Fragment>
'aria-label': 'secondary checkbox' ) : (
}} <div className={classes.noFound}>No Users Available</div>
onChange={ selectionChanged } )}
checked={selectedUsers.includes(row.accessKey)} </Paper>
/> </Grid>
</TableCell> </React.Fragment>
<TableCell className={classes.wrapCell}> );
{row.accessKey}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</React.Fragment>
) : (
<div className={classes.noFound}>No Users Available</div>
)}
</Paper>
</Grid>
</React.Fragment>
);
}; };
export default withStyles(styles)(UsersSelectors); export default withStyles(styles)(UsersSelectors);

View File

@@ -15,16 +15,16 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
export interface Group { export interface Group {
name: string; name: string;
id: string; id: string;
email: string; email: string;
is_me: boolean; is_me: boolean;
enabled: boolean; enabled: boolean;
accessKey: string; accessKey: string;
secretKey: string; secretKey: string;
} }
export interface GroupsList { export interface GroupsList {
groups: string[]; groups: string[];
total:number; total: number;
} }

View File

@@ -32,10 +32,10 @@ import {
BucketsIcon, BucketsIcon,
DashboardIcon, DashboardIcon,
PermissionIcon, PermissionIcon,
ServiceAccountIcon,
UsersIcon UsersIcon
} from "../../icons"; } from "../../icons";
import { createStyles, Theme } from "@material-ui/core/styles"; import { createStyles, Theme } from "@material-ui/core/styles";
import PersonIcon from "@material-ui/icons/Person";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
@@ -124,7 +124,7 @@ class Menu extends React.Component<MenuProps> {
</ListItem> </ListItem>
<ListItem button component={NavLink} to="/users"> <ListItem button component={NavLink} to="/users">
<ListItemIcon> <ListItemIcon>
<UsersIcon /> <PersonIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary="Users" /> <ListItemText primary="Users" />
</ListItem> </ListItem>

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, {useEffect, useState} from "react"; import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core"; import { LinearProgress } from "@material-ui/core";
import Table from "@material-ui/core/Table"; import Table from "@material-ui/core/Table";
@@ -30,9 +30,9 @@ import SearchIcon from "@material-ui/icons/Search";
import TextField from "@material-ui/core/TextField"; import TextField from "@material-ui/core/TextField";
import Checkbox from "@material-ui/core/Checkbox"; import Checkbox from "@material-ui/core/Checkbox";
import api from "../../../common/api"; import api from "../../../common/api";
import {UsersList} from "./types"; import { UsersList } from "./types";
import {groupsSort, usersSort} from "../../../utils/sortFunctions"; import { groupsSort, usersSort } from "../../../utils/sortFunctions";
import {GroupsList} from "../Groups/types"; import { GroupsList } from "../Groups/types";
interface IGroupsProps { interface IGroupsProps {
classes: any; classes: any;
@@ -99,38 +99,38 @@ const styles = (theme: Theme) =>
const GroupsSelectors = ({ const GroupsSelectors = ({
classes, classes,
selectedGroups, selectedGroups,
setSelectedGroups, setSelectedGroups
}: IGroupsProps) => { }: IGroupsProps) => {
// Local State // Local State
const [records, setRecords] = useState<any[]>([]); const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false); const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>(""); const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>(""); const [filter, setFilter] = useState<string>("");
//Effects //Effects
useEffect(() => { useEffect(() => {
isLoading(true); isLoading(true);
}, []); }, []);
useEffect(() => { useEffect(() => {
if(loading) { if (loading) {
fetchGroups(); fetchGroups();
} }
},[loading]); }, [loading]);
const fetchGroups = () => { const fetchGroups = () => {
api api
.invoke("GET", `/api/v1/groups`) .invoke("GET", `/api/v1/groups`)
.then((res: GroupsList) => { .then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort)); setRecords(res.groups.sort(groupsSort));
setError(""); setError("");
isLoading(false); isLoading(false);
}) })
.catch(err => { .catch(err => {
setError(err); setError(err);
isLoading(false); isLoading(false);
}); });
}; };
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target; const targetD = e.target;
@@ -151,7 +151,9 @@ const GroupsSelectors = ({
return elements; return elements;
}; };
const filteredRecords = records.filter((elementItem) => elementItem.includes(filter)); const filteredRecords = records.filter(elementItem =>
elementItem.includes(filter)
);
return ( return (
<React.Fragment> <React.Fragment>
@@ -175,8 +177,8 @@ const GroupsSelectors = ({
</InputAdornment> </InputAdornment>
) )
}} }}
onChange={(e) => { onChange={e => {
setFilter(e.target.value); setFilter(e.target.value);
}} }}
/> />
</Grid> </Grid>

View File

@@ -127,7 +127,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
deleteOpen: false, deleteOpen: false,
selectedUser: null, selectedUser: null,
addGroupOpen: false, addGroupOpen: false,
filter: "", filter: ""
}; };
fetchRecords() { fetchRecords() {
@@ -191,7 +191,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
rowsPerPage, rowsPerPage,
deleteOpen, deleteOpen,
selectedUser, selectedUser,
filter, filter
} = this.state; } = this.state;
const handleChangePage = (event: unknown, newPage: number) => { const handleChangePage = (event: unknown, newPage: number) => {
@@ -209,7 +209,9 @@ class Users extends React.Component<IUsersProps, IUsersState> {
}); });
}; };
const filteredRecords = records.filter((elementItem) => elementItem.accessKey.includes(filter)); const filteredRecords = records.filter(elementItem =>
elementItem.accessKey.includes(filter)
);
return ( return (
<React.Fragment> <React.Fragment>
@@ -242,8 +244,8 @@ class Users extends React.Component<IUsersProps, IUsersState> {
</InputAdornment> </InputAdornment>
) )
}} }}
onChange={(e) => { onChange={e => {
this.setState({filter: e.target.value}); this.setState({ filter: e.target.value });
}} }}
/> />
<Button <Button

View File

@@ -15,29 +15,27 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
interface userInterface { interface userInterface {
accessKey: string; accessKey: string;
} }
export const usersSort = (a: userInterface, b: userInterface) => { export const usersSort = (a: userInterface, b: userInterface) => {
if (a.accessKey > b.accessKey) { if (a.accessKey > b.accessKey) {
return 1; return 1;
} }
if (a.accessKey < b.accessKey) { if (a.accessKey < b.accessKey) {
return -1; return -1;
} }
// a must be equal to b // a must be equal to b
return 0; return 0;
}; };
export const groupsSort = (a: string, b: string) => { export const groupsSort = (a: string, b: string) => {
if (a > b) { if (a > b) {
return 1; return 1;
} }
if (a < b) { if (a < b) {
return -1; return -1;
} }
// a must be equal to b // a must be equal to b
return 0; return 0;
}; };