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

View File

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

View File

@@ -16,228 +16,241 @@
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {Button, Dialog, 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 {
Button,
Dialog,
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 Typography from "@material-ui/core/Typography";
import api from "../../../common/api";
import UsersSelectors from "./UsersSelectors";
import {GroupsList} from "./types";
import {groupsSort} from "../../../utils/sortFunctions";
import { GroupsList } from "./types";
import { groupsSort } from "../../../utils/sortFunctions";
import Title from "../../../common/Title";
interface IGroupProps {
open: boolean;
selectedGroup: any;
closeModalAndRefresh: any;
classes: any;
open: boolean;
selectedGroup: any;
closeModalAndRefresh: any;
classes: any;
}
interface MainGroupProps {
members: string[];
name: string;
status: string;
members: string[];
name: string;
status: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700,
},
keyName: {
marginLeft: 5
}
});
createStyles({
errorBlock: {
color: "red"
},
strongText: {
fontWeight: 700
},
keyName: {
marginLeft: 5
}
});
const AddGroup = ({
open,
selectedGroup,
closeModalAndRefresh,
classes,
}: IGroupProps) => {
open,
selectedGroup,
closeModalAndRefresh,
classes
}: 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
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);
//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);
//Effects
useEffect(() => {
if (selectedGroup !== null) {
isLoadingGroup(true);
} else {
setGroupName("");
setSelectedUsers([]);
}
}, [selectedGroup]);
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);
});
}
};
useEffect(() => {
if (saving) {
saveRecord();
}
}, [saving]);
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);
});
};
useEffect(() => {
if (selectedGroup && loadingGroup) {
fetchGroupInfo();
}
}, [loadingGroup]);
return (<Dialog
open={open}
onClose={closeModalAndRefresh}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
//Fetch Actions
const setSaving = (event: React.FormEvent) => {
event.preventDefault();
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">
{selectedGroup !== null ? `Group Edit - ${groupName}` : 'Add Group'}
</DialogTitle>
<DialogContent>
<form
noValidate
autoComplete="off"
onSubmit={setSaving}
>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<DialogTitle id="alert-dialog-title">
{selectedGroup !== null ? `Group Edit - ${groupName}` : "Add Group"}
</DialogTitle>
<DialogContent>
<form noValidate autoComplete="off" onSubmit={setSaving}>
<Grid container>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<Title>Status</Title>
<RadioGroup
aria-label="status"
name="status"
value={groupEnabled}
onChange={(e) => {
setGroupEnabled(e.target.value);
}}
>
<FormControlLabel value="enabled" control={<Radio color={'primary'} />} label="Enabled" />
<FormControlLabel value="disabled" control={<Radio color={'primary'} />} label="Disabled" />
</RadioGroup>
</Grid>
</React.Fragment>
) : (
<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>
)}
{selectedGroup !== null ? (
<React.Fragment>
<Grid item xs={12}>
<Title>Status</Title>
<RadioGroup
aria-label="status"
name="status"
value={groupEnabled}
onChange={e => {
setGroupEnabled(e.target.value);
}}
>
<FormControlLabel
value="enabled"
control={<Radio color={"primary"} />}
label="Enabled"
/>
<FormControlLabel
value="disabled"
control={<Radio color={"primary"} />}
label="Disabled"
/>
</RadioGroup>
</Grid>
</form>
</DialogContent>
</Dialog>);
</React.Fragment>
) : (
<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);

View File

@@ -17,118 +17,115 @@
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress
} from "@material-ui/core";
import api from "../../../common/api";
import Typography from "@material-ui/core/Typography";
import {UsersList} from "../Users/types";
import { UsersList } from "../Users/types";
interface IDeleteGroup {
selectedGroup: string;
deleteOpen: boolean;
closeDeleteModalAndRefresh: any;
classes: any;
selectedGroup: string;
deleteOpen: boolean;
closeDeleteModalAndRefresh: any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
});
createStyles({
errorBlock: {
color: "red"
}
});
const DeleteGroup = ({
selectedGroup,
deleteOpen,
closeDeleteModalAndRefresh,
classes,
}:IDeleteGroup) => {
const [isDeleting, setDeleteLoading] = useState<boolean>(false);
const [deleteError, setError] = useState<string>("");
selectedGroup,
deleteOpen,
closeDeleteModalAndRefresh,
classes
}: IDeleteGroup) => {
const [isDeleting, setDeleteLoading] = useState<boolean>(false);
const [deleteError, setError] = useState<string>("");
useEffect(() => {
if(isDeleting) {
removeRecord();
}
}, [isDeleting]);
useEffect(() => {
if (isDeleting) {
removeRecord();
}
}, [isDeleting]);
const removeRecord = () => {
if (!selectedGroup) {
return;
}
const removeRecord = () => {
if (!selectedGroup) {
return;
}
api
.invoke("DELETE", `/api/v1/groups/${selectedGroup}`)
.then((res: UsersList) => {
setDeleteLoading(false);
setError("");
closeDeleteModalAndRefresh(true);
})
.catch(err => {
setDeleteLoading(false);
setError(err);
});
};
const closeNoAction = () => {
api
.invoke("DELETE", `/api/v1/groups/${selectedGroup}`)
.then((res: UsersList) => {
setDeleteLoading(false);
setError("");
closeDeleteModalAndRefresh(false);
};
return (<React.Fragment>
<Dialog
open={deleteOpen}
onClose={closeNoAction}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete User</DialogTitle>
<DialogContent>
{isDeleting && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete group <b>{selectedGroup}</b>
?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={closeNoAction}
color="primary"
disabled={isDeleting}
closeDeleteModalAndRefresh(true);
})
.catch(err => {
setDeleteLoading(false);
setError(err);
});
};
const closeNoAction = () => {
setError("");
closeDeleteModalAndRefresh(false);
};
return (
<React.Fragment>
<Dialog
open={deleteOpen}
onClose={closeNoAction}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete User</DialogTitle>
<DialogContent>
{isDeleting && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete group <b>{selectedGroup}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
Cancel
</Button>
<Button
onClick={() => {
setDeleteLoading(true);
}}
color="secondary"
autoFocus
>
Delete
</Button>
</DialogActions>
</Dialog>
</React.Fragment>);
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={closeNoAction} color="primary" disabled={isDeleting}>
Cancel
</Button>
<Button
onClick={() => {
setDeleteLoading(true);
}}
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 InputAdornment from "@material-ui/core/InputAdornment";
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 Table from "@material-ui/core/Table";
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 ViewIcon from "@material-ui/icons/Visibility";
import DeleteIcon from "@material-ui/icons/Delete";
import {CreateIcon} from "../../../icons";
import { CreateIcon } from "../../../icons";
import api from "../../../common/api";
import {MinTablePaginationActions} from "../../../common/MinTablePaginationActions";
import {GroupsList} from "./types";
import {groupsSort, usersSort} from "../../../utils/sortFunctions";
import {UsersList} from "../Users/types";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { GroupsList } from "./types";
import { groupsSort, usersSort } from "../../../utils/sortFunctions";
import { UsersList } from "../Users/types";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
interface IGroupsProps {
classes: any;
openGroupModal: any;
classes: any;
openGroupModal: any;
}
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight:'bold'
}
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
});
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012"
}
});
const Groups = ({
classes,
}: IGroupsProps) => {
const Groups = ({ classes }: IGroupsProps) => {
const [addGroupOpen, setGroupOpen] = useState<boolean>(false);
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 [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 handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
setPage(0);
setRowsPerPage(rPP);
};
useEffect(() => {
isLoading(true);
}, []);
useEffect(() => {
isLoading(true);
}, []);
useEffect(() => {
isLoading(true);
}, [page, rowsPerPage]);
useEffect(() => {
isLoading(true);
}, [page, rowsPerPage]);
useEffect(() => {
if (loading) {
fetchRecords();
}
}, [loading]);
useEffect(() => {
if(loading) {
fetchRecords();
const fetchRecords = () => {
const offset = page * rowsPerPage;
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 offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/groups?offset=${offset}&limit=${rowsPerPage}`)
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setTotalRecords(res.total);
setError("");
isLoading(false);
const closeAddModalAndRefresh = () => {
setGroupOpen(false);
isLoading(true);
};
// 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);
}
})
.catch(err => {
setError(err);
isLoading(false);
});
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
isLoading(true);
}
};
const filteredRecords = records.filter(elementItem =>
elementItem.includes(filter)
);
const closeAddModalAndRefresh = () => {
setGroupOpen(false);
isLoading(true);
};
const closeDeleteModalAndRefresh = (refresh: boolean) => {
setDeleteOpen(false);
if (refresh) {
isLoading(true);
}
};
const filteredRecords = records.filter((elementItem) => elementItem.includes(filter));
return (<React.Fragment>
{ addGroupOpen &&
<AddGroup
open={addGroupOpen}
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>
return (
<React.Fragment>
{addGroupOpen && (
<AddGroup
open={addGroupOpen}
selectedGroup={selectedGroup}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
<DeleteGroup
deleteOpen={deleteOpen}
selectedGroup={selectedGroup}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>
</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);

View File

@@ -14,7 +14,7 @@
// 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, { useState, useEffect } from 'react';
import React, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core";
import Table from "@material-ui/core/Table";
@@ -31,163 +31,164 @@ import { usersSort } from "../../../utils/sortFunctions";
import api from "../../../common/api";
interface IGroupsProps {
classes: any;
selectedUsers: string[];
setSelectedUsers: any;
classes: any;
selectedUsers: string[];
setSelectedUsers: any;
}
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight:'bold'
}
}
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10,
}
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: '100%'
},
noFound: {
textAlign: "center",
padding: "10px 0",
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
});
}
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%"
},
noFound: {
textAlign: "center",
padding: "10px 0"
}
});
const UsersSelectors = ({
classes,
selectedUsers,
setSelectedUsers,
}: IGroupsProps) => {
classes,
selectedUsers,
setSelectedUsers
}: IGroupsProps) => {
//Local States
const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
//Local States
const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
//Effects
useEffect(() => {
isLoading(true);
}, []);
//Effects
useEffect(() => {
isLoading(true);
}, []);
useEffect(() => {
if (loading) {
fetchUsers();
}
}, [loading]);
useEffect(() => {
if(loading) {
fetchUsers();
}
},[loading]);
//Fetch Actions
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
//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[] = [...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
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;
};
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 = () => {
api
.invoke("GET", `/api/v1/users`)
.then((res: UsersList) => {
setRecords(res.users.sort(usersSort));
setError("");
isLoading(false);
})
.catch(err => {
setError(err);
isLoading(false);
});
};
return (
<React.Fragment>
<Title>Members</Title>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<React.Fragment>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Access Key</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={`group-${row.accessKey}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.accessKey}
color="primary"
inputProps={{
'aria-label': 'secondary checkbox'
}}
onChange={ selectionChanged }
checked={selectedUsers.includes(row.accessKey)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.accessKey}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</React.Fragment>
) : (
<div className={classes.noFound}>No Users Available</div>
)}
</Paper>
</Grid>
</React.Fragment>
);
return (
<React.Fragment>
<Title>Members</Title>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<React.Fragment>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Access Key</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={`group-${row.accessKey}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.accessKey}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={selectionChanged}
checked={selectedUsers.includes(row.accessKey)}
/>
</TableCell>
<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);

View File

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

View File

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

View File

@@ -14,7 +14,7 @@
// 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, {useEffect, useState} from "react";
import React, { useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core";
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 Checkbox from "@material-ui/core/Checkbox";
import api from "../../../common/api";
import {UsersList} from "./types";
import {groupsSort, usersSort} from "../../../utils/sortFunctions";
import {GroupsList} from "../Groups/types";
import { UsersList } from "./types";
import { groupsSort, usersSort } from "../../../utils/sortFunctions";
import { GroupsList } from "../Groups/types";
interface IGroupsProps {
classes: any;
@@ -99,38 +99,38 @@ const styles = (theme: Theme) =>
const GroupsSelectors = ({
classes,
selectedGroups,
setSelectedGroups,
setSelectedGroups
}: IGroupsProps) => {
// Local State
const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
// Local State
const [records, setRecords] = useState<any[]>([]);
const [loading, isLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
//Effects
useEffect(() => {
isLoading(true);
}, []);
//Effects
useEffect(() => {
isLoading(true);
}, []);
useEffect(() => {
if(loading) {
fetchGroups();
}
},[loading]);
useEffect(() => {
if (loading) {
fetchGroups();
}
}, [loading]);
const fetchGroups = () => {
api
.invoke("GET", `/api/v1/groups`)
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setError("");
isLoading(false);
})
.catch(err => {
setError(err);
isLoading(false);
});
};
const fetchGroups = () => {
api
.invoke("GET", `/api/v1/groups`)
.then((res: GroupsList) => {
setRecords(res.groups.sort(groupsSort));
setError("");
isLoading(false);
})
.catch(err => {
setError(err);
isLoading(false);
});
};
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
@@ -151,7 +151,9 @@ const GroupsSelectors = ({
return elements;
};
const filteredRecords = records.filter((elementItem) => elementItem.includes(filter));
const filteredRecords = records.filter(elementItem =>
elementItem.includes(filter)
);
return (
<React.Fragment>
@@ -175,8 +177,8 @@ const GroupsSelectors = ({
</InputAdornment>
)
}}
onChange={(e) => {
setFilter(e.target.value);
onChange={e => {
setFilter(e.target.value);
}}
/>
</Grid>

View File

@@ -127,7 +127,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
deleteOpen: false,
selectedUser: null,
addGroupOpen: false,
filter: "",
filter: ""
};
fetchRecords() {
@@ -191,7 +191,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
rowsPerPage,
deleteOpen,
selectedUser,
filter,
filter
} = this.state;
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 (
<React.Fragment>
@@ -242,8 +244,8 @@ class Users extends React.Component<IUsersProps, IUsersState> {
</InputAdornment>
)
}}
onChange={(e) => {
this.setState({filter: e.target.value});
onChange={e => {
this.setState({ filter: e.target.value });
}}
/>
<Button

View File

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