View Bucket Info (#11)

* View Bucket Info

* Fix makefile for portal-ui

* Static UI

* Remove Warning on View Bucket

* Address Comments
This commit is contained in:
Daniel Valdivia
2020-04-02 19:40:26 -07:00
committed by GitHub
parent 0da379dbb2
commit 3a8f1556af
27 changed files with 1519 additions and 1252 deletions

View File

@@ -10,7 +10,7 @@ swagger-gen:
@swagger generate server -A mcs --main-package=mcs --exclude-main -P models.Principal -f ./swagger.yml -r NOTICE
build:
@(cd portal-ui; yarn install; make build; cd ..)
@(cd portal-ui; yarn install; make build-static; cd ..)
@(CGO_ENABLED=0 go build --tags kqueue --ldflags "-s -w" -o mcs ./cmd/mcs)
test:

View File

@@ -1,6 +1,6 @@
default: build
default: build-static
build:
build-static:
@echo "Building frontend static assets to 'build'"
yarn build
go-bindata-assetfs -pkg portal build/...

File diff suppressed because one or more lines are too long

View File

@@ -28,7 +28,7 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>Acme Storage</title>
<title>MinIO Console Server</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,5 +1,5 @@
{
"name": "Acme Storage",
"name": "MinIO Console Server",
"icons": [
{
"src": "android-icon-36x36.png",

View File

@@ -15,17 +15,15 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import {Route, Router, Switch} from "react-router-dom";
import { Route, Router, Switch } from "react-router-dom";
import history from "./history";
import Login from "./screens/LoginPage";
import Signup from "./screens/SignupPage";
import Console from "./screens/Console/Console";
import NotFoundPage from "./screens/NotFoundPage";
import storage from "local-storage-fallback";
import CreatePassword from "./screens/CreatePassword";
import {connect} from "react-redux";
import {AppState} from "./store";
import {userLoggedIn} from "./actions";
import { connect } from "react-redux";
import { AppState } from "./store";
import { userLoggedIn } from "./actions";
const isLoggedIn = () => {
return (
@@ -57,9 +55,7 @@ class Routes extends React.Component<RoutesProps> {
return (
<Router history={history}>
<Switch>
<Route exact path="/create-password" component={CreatePassword} />
<Route exact path="/login" component={Login} />
<Route exact path="/signup" component={Signup} />
{this.props.loggedIn ? (
<Switch>
<Route path="/*" component={Console} />

View File

@@ -29,12 +29,16 @@ const GlobalCss = withStyles({
// @global is handled by jss-plugin-global.
"@global": {
// You should target [class*="MuiButton-root"] instead if you nest themes.
".MuiButton-root": {
".MuiButton-contained": {
fontSize: "14px",
textTransform: "capitalize",
padding: "16px 25px 16px 25px",
borderRadius: "3px"
},
".MuiButton-sizeSmall": {
padding: "4px 10px",
fontSize: "0.8125rem"
},
".MuiTableCell-head": {
borderRadius: "3px 3px 0px 0px",
fontSize: "13px"

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// This file is part of MinIO Buckets 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
@@ -15,326 +15,114 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import { Bucket, BucketList } from "./types";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
createStyles,
StyledProps,
Theme,
withStyles
} from "@material-ui/core/styles";
import history from "../../../history";
import {
Route,
RouteComponentProps,
Router,
Switch,
withRouter
} from "react-router-dom";
import { connect } from "react-redux";
import { AppState } from "../../../store";
import { setMenuOpen } from "../../../actions";
import { ThemedComponentProps } from "@material-ui/core/styles/withTheme";
import NotFoundPage from "../../NotFoundPage";
import BucketList from "./ListBuckets/ListBuckets";
import ViewBucket from "./ViewBucket/ViewBucket";
import ListBuckets from "./ListBuckets/ListBuckets";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
root: {
display: "flex"
},
toolbar: {
background: theme.palette.background.default,
color: "black",
paddingRight: 24 // keep right padding when drawer closed
},
toolbarIcon: {
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
padding: "0 8px",
...theme.mixins.toolbar
},
appBar: {
zIndex: theme.zIndex.drawer + 1,
transition: theme.transitions.create(["width", "margin"], {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
})
},
menuButton: {
marginRight: 36
},
menuButtonHidden: {
display: "none"
},
title: {
flexGrow: 1
},
appBarSpacer: {
height: "5px"
},
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
},
container: {
paddingTop: theme.spacing(4),
paddingBottom: theme.spacing(4)
},
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)
},
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"
fixedHeight: {
minHeight: 240
}
});
interface IBucketsProps {
const mapState = (state: AppState) => ({
open: state.system.sidebarOpen
});
const connector = connect(mapState, { setMenuOpen });
interface BucketsProps {
open: boolean;
title: string;
classes: any;
setMenuOpen: typeof setMenuOpen;
}
interface IBucketsState {
records: Bucket[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedBucket: string;
filterBuckets: string;
}
class Buckets extends React.Component<IBucketsProps, IBucketsState> {
state: IBucketsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: ""
};
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/buckets?offset=${offset}&limit=${rowsPerPage}`)
.then((res: BucketList) => {
this.setState({
loading: false,
records: res.buckets,
totalRecords: res.total,
error: ""
});
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
}
})
.catch(err => {
this.setState({ loading: false, error: err });
});
});
}
closeAddModalAndRefresh() {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
}
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
componentDidMount(): void {
this.fetchRecords();
}
bucketFilter(): void {}
class Buckets extends React.Component<
BucketsProps & RouteComponentProps & StyledProps & ThemedComponentProps
> {
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets
} = this.state;
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP });
};
const confirmDeleteBucket = (bucket: string) => {
this.setState({ deleteOpen: true, selectedBucket: bucket });
};
return (
<React.Fragment>
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Buckets</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={val => {
this.setState({
filterBuckets: val.target.value
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Create Bucket
</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>Creation Date</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records
.slice(offset, offset + rowsPerPage)
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
})
.map(row => (
<TableRow key={row.name}>
<TableCell>{row.name}</TableCell>
<TableCell>
<Moment>{row.creation_date}</Moment>
</TableCell>
<TableCell align="right">
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteBucket(row.name);
}}
>
<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 Buckets</div>
)}
</Paper>
</Grid>
</Grid>
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
</React.Fragment>
<Router history={history}>
<Switch>
<Route path="/buckets/:bucketName" component={ViewBucket} />
<Route path="/" component={ListBuckets} />
<Route component={NotFoundPage} />
</Switch>
</Router>
);
}
}
export default withStyles(styles)(Buckets);
export default withRouter(connector(withStyles(styles)(Buckets)));

View File

@@ -16,7 +16,7 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import Title from "../../../common/Title";
import Title from "../../../../common/Title";
import Typography from "@material-ui/core/Typography";
import {
Button,
@@ -31,7 +31,7 @@ import {
TextField
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../common/api";
import api from "../../../../common/api";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -25,8 +25,8 @@ import {
DialogTitle,
LinearProgress
} from "@material-ui/core";
import api from "../../../common/api";
import { BucketList } from "./types";
import api from "../../../../common/api";
import { BucketList } from "../types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
@@ -96,7 +96,7 @@ class DeleteBucket extends React.Component<
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({deleteError:""},()=>{
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
@@ -126,7 +126,7 @@ class DeleteBucket extends React.Component<
<DialogActions>
<Button
onClick={() => {
this.setState({deleteError:""},()=>{
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}

View File

@@ -0,0 +1,350 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../../common/api";
import { Bucket, BucketList } from "../types";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import AddBucket from "./AddBucket";
import DeleteBucket from "./DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import { CreateIcon } from "../../../../icons";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
import { Link } from "react-router-dom";
import ViewIcon from "@material-ui/icons/Visibility";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
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"
}
});
interface IListBucketsProps {
classes: any;
}
interface IListBucketsState {
records: Bucket[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
addScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedBucket: string;
filterBuckets: string;
}
class ListBuckets extends React.Component<
IListBucketsProps,
IListBucketsState
> {
state: IListBucketsState = {
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
addScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: "",
filterBuckets: ""
};
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const offset = page * rowsPerPage;
api
.invoke("GET", `/api/v1/buckets?offset=${offset}&limit=${rowsPerPage}`)
.then((res: BucketList) => {
this.setState({
loading: false,
records: res.buckets,
totalRecords: res.total,
error: ""
});
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.buckets === undefined ||
res.buckets == null ||
res.buckets.length === 0) &&
page > 0
) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
}
closeAddModalAndRefresh() {
this.setState({ addScreenOpen: false }, () => {
this.fetchRecords();
});
}
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
componentDidMount(): void {
this.fetchRecords();
}
bucketFilter(): void {}
render() {
const { classes } = this.props;
const {
records,
totalRecords,
addScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedBucket,
filterBuckets
} = this.state;
const offset = page * rowsPerPage;
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP });
};
const confirmDeleteBucket = (bucket: string) => {
this.setState({ deleteOpen: true, selectedBucket: bucket });
};
return (
<React.Fragment>
<AddBucket
open={addScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Buckets</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Buckets"
className={classes.searchField}
id="search-resource"
label=""
onChange={val => {
this.setState({
filterBuckets: val.target.value
});
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
this.setState({
addScreenOpen: true
});
}}
>
Create Bucket
</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>Creation Date</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records
.slice(offset, offset + rowsPerPage)
.filter((b: Bucket) => {
if (filterBuckets === "") {
return true;
} else {
if (b.name.indexOf(filterBuckets) >= 0) {
return true;
} else {
return false;
}
}
})
.map(row => (
<TableRow key={row.name}>
<TableCell>{row.name}</TableCell>
<TableCell>
<Moment>{row.creation_date}</Moment>
</TableCell>
<TableCell align="right">
<Link to={`/buckets/${row.name}`}>
<IconButton aria-label="delete">
<ViewIcon />
</IconButton>
</Link>
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteBucket(row.name);
}}
>
<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 Buckets</div>
)}
</Paper>
</Grid>
</Grid>
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
</React.Fragment>
);
}
}
export default withStyles(styles)(ListBuckets);

View File

@@ -0,0 +1,180 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import Grid from "@material-ui/core/Grid";
import 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";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
});
interface ISetAccessPolicyProps {
classes: any;
open: boolean;
bucketName: string;
closeModalAndRefresh: () => void;
}
interface ISetAccessPolicyState {
addLoading: boolean;
addError: string;
accessPolicy: string;
}
class SetAccessPolicy extends React.Component<
ISetAccessPolicyProps,
ISetAccessPolicyState
> {
state: ISetAccessPolicyState = {
addLoading: false,
addError: "",
accessPolicy: ""
};
addRecord(event: React.FormEvent) {
event.preventDefault();
const { addLoading, accessPolicy } = this.state;
const { bucketName } = this.props;
if (addLoading) {
return;
}
this.setState({ addLoading: true }, () => {
api
.invoke("PUT", `/api/v1/buckets/${bucketName}/set-policy`, {
access: accessPolicy
})
.then(res => {
this.setState(
{
addLoading: false,
addError: ""
},
() => {
this.props.closeModalAndRefresh();
}
);
})
.catch(err => {
this.setState({
addLoading: false,
addError: err
});
});
});
}
render() {
const { classes, open } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
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>Change Access Policy</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">
Access Policy
</InputLabel>
<Select
labelId="select-access-policy"
id="select-access-policy"
value={accessPolicy}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
this.setState({ accessPolicy: e.target.value as string });
}}
>
<MenuItem value="PRIVATE">Private</MenuItem>
<MenuItem value="PUBLIC">Public</MenuItem>
<MenuItem value="CUSTOM">Custom</MenuItem>
</Select>
</FormControl>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={addLoading}
>
Set
</Button>
</Grid>
{addLoading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</DialogContent>
</Dialog>
);
}
}
export default withStyles(styles)(SetAccessPolicy);

View File

@@ -0,0 +1,346 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableHead from "@material-ui/core/TableHead";
import TableRow from "@material-ui/core/TableRow";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../../common/api";
import { BucketEvent, BucketEventList, BucketInfo } from "../types";
import {
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import DeleteIcon from "@material-ui/icons/Delete";
import SetAccessPolicy from "./SetAccessPolicy";
import DeleteBucket from "../ListBuckets/DeleteBucket";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
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"
},
noRecords: {
lineHeight: "24px",
textAlign: "center",
padding: "20px"
}
});
interface IViewBucketProps {
classes: any;
match: any;
}
interface IViewBucketState {
info: BucketInfo | null;
records: BucketEvent[];
totalRecords: number;
loading: boolean;
error: string;
deleteError: string;
setAccessPolicyScreenOpen: boolean;
page: number;
rowsPerPage: number;
deleteOpen: boolean;
selectedBucket: string;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
state: IViewBucketState = {
info: null,
records: [],
totalRecords: 0,
loading: false,
error: "",
deleteError: "",
setAccessPolicyScreenOpen: false,
page: 0,
rowsPerPage: 10,
deleteOpen: false,
selectedBucket: ""
};
fetchRecords() {
this.setState({ loading: true }, () => {
const { page, rowsPerPage } = this.state;
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}/events`)
.then((res: BucketEventList) => {
this.setState({
loading: false,
records: res.events,
totalRecords: res.total,
error: ""
});
// if we get 0 results, and page > 0 , go down 1 page
if (
(res.events === undefined ||
res.events == null ||
res.events.length === 0) &&
page > 0
) {
const newPage = page - 1;
this.setState({ page: newPage }, () => {
this.fetchRecords();
});
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
});
});
}
closeAddModalAndRefresh() {
this.setState({ setAccessPolicyScreenOpen: false }, () => {
this.loadInfo();
});
}
closeDeleteModalAndRefresh(refresh: boolean) {
this.setState({ deleteOpen: false }, () => {
if (refresh) {
this.fetchRecords();
}
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ info: res });
})
.catch(err => {});
}
componentDidMount(): void {
this.loadInfo();
this.fetchRecords();
}
bucketFilter(): void {}
render() {
const { classes, match } = this.props;
const {
info,
records,
totalRecords,
setAccessPolicyScreenOpen,
loading,
page,
rowsPerPage,
deleteOpen,
selectedBucket
} = this.state;
const offset = page * rowsPerPage;
const bucketName = match.params["bucketName"];
const handleChangePage = (event: unknown, newPage: number) => {
this.setState({ page: newPage });
};
const handleChangeRowsPerPage = (
event: React.ChangeEvent<HTMLInputElement>
) => {
const rPP = parseInt(event.target.value, 10);
this.setState({ page: 0, rowsPerPage: rPP });
};
const confirmDeleteEvent = (bucket: string) => {
this.setState({ deleteOpen: true, selectedBucket: bucket });
};
let accessPolicy = "n/a";
if (info !== null) {
accessPolicy = info.access;
}
return (
<React.Fragment>
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
/>
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Bucket > {match.params["bucketName"]}
</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
Access Policy: {accessPolicy}
{" "}
<Button
variant="contained"
size="small"
color="primary"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true
});
}}
>
Change Access Policy
</Button>
<br />
Reported Usage: 0 bytes
<br />
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Typography variant="h6">Events</Typography>
</Grid>
<Grid item xs={6} className={classes.actionsTray} />
<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>SQS</TableCell>
<TableCell>Events</TableCell>
<TableCell>Prefix</TableCell>
<TableCell>Suffix</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.slice(offset, offset + rowsPerPage).map(row => (
<TableRow key={row.id}>
<TableCell>{row.arn}</TableCell>
<TableCell>{row.events.join(", ")}</TableCell>
<TableCell>{row.prefix}</TableCell>
<TableCell>{row.suffix}</TableCell>
<TableCell align="right">
<IconButton
aria-label="delete"
onClick={() => {
confirmDeleteEvent(row.id);
}}
>
<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 className={classes.noRecords}>No Events</div>
)}
</Paper>
</Grid>
</Grid>
<DeleteBucket
deleteOpen={deleteOpen}
selectedBucket={selectedBucket}
closeDeleteModalAndRefresh={(refresh: boolean) => {
this.closeDeleteModalAndRefresh(refresh);
}}
/>
</React.Fragment>
);
}
}
export default withStyles(styles)(ViewBucket);

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -19,7 +19,25 @@ export interface Bucket {
creation_date: Date;
}
export interface BucketInfo {
name: string;
access: string;
}
export interface BucketList {
buckets: Bucket[];
total: number;
}
export interface BucketEvent {
id: string;
arn: string;
events: string[];
prefix: string;
suffix: string;
}
export interface BucketEventList {
events: BucketEvent[];
total: number;
}

View File

@@ -1,5 +1,5 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
// 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
@@ -212,7 +212,7 @@ class Console extends React.Component<
<Container maxWidth="lg" className={classes.container}>
<Router history={history}>
<Switch>
<Route exact path="/buckets" component={Buckets} />
<Route path="/buckets" component={Buckets} />
<Route exact path="/permissions" component={Permissions} />
<Route exact path="/policies" component={Policies} />
<Route

View File

@@ -116,24 +116,12 @@ class Menu extends React.Component<MenuProps> {
</ListItemIcon>
<ListItemText primary="Buckets" />
</ListItem>
<ListItem button component={NavLink} to="/permissions">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="Permissions" />
</ListItem>
<ListItem button component={NavLink} to="/policies">
<ListItemIcon>
<PermissionIcon />
</ListItemIcon>
<ListItemText primary="policies" />
</ListItem>
<ListItem button component={NavLink} to="/service_accounts">
<ListItemIcon>
<ServiceAccountIcon />
</ListItemIcon>
<ListItemText primary="Service Accounts" />
</ListItem>
<ListItem button component={NavLink} to="/users">
<ListItemIcon>
<UsersIcon />

View File

@@ -16,7 +16,7 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import {UnControlled as CodeMirror} from 'react-codemirror2'
import { UnControlled as CodeMirror } from "react-codemirror2";
import Typography from "@material-ui/core/Typography";
import {
Button,
@@ -29,9 +29,9 @@ import {
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Title from "../../../common/Title";
import api from "../../../common/api";
import 'codemirror/lib/codemirror.css';
import 'codemirror/theme/material.css';
require('codemirror/mode/javascript/javascript');
import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css";
require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) =>
createStyles({
@@ -40,11 +40,11 @@ const styles = (theme: Theme) =>
},
jsonPolicyEditor: {
minHeight: 400,
width: "100%",
width: "100%"
},
codeMirror: {
fontSize: 14,
},
fontSize: 14
}
});
interface IAddPolicyProps {
@@ -65,7 +65,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
addLoading: false,
addError: "",
policyName: "",
policyDefinition: "",
policyDefinition: ""
};
addRecord(event: React.FormEvent) {
event.preventDefault();
@@ -77,7 +77,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
api
.invoke("POST", "/api/v1/policies", {
name: policyName,
definition: policyDefinition,
definition: policyDefinition
})
.then(res => {
this.setState(
@@ -100,7 +100,7 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
}
render() {
const { classes, open } = this.props;
const { addLoading, addError, policyName, policyDefinition} = this.state;
const { addLoading, addError, policyName, policyDefinition } = this.state;
return (
<Dialog
fullWidth
@@ -154,8 +154,8 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
className={classes.codeMirror}
value=""
options={{
mode: 'javascript',
theme: 'material',
mode: "javascript",
theme: "material",
lineNumbers: true
}}
onChange={(editor, data, value) => {

View File

@@ -40,7 +40,7 @@ interface IDeletePolicyProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedPolicy: string;
selectedPolicy: string;
}
interface IDeletePolicyState {
@@ -48,7 +48,10 @@ interface IDeletePolicyState {
deleteError: string;
}
class DeletePolicy extends React.Component<IDeletePolicyProps, IDeletePolicyState> {
class DeletePolicy extends React.Component<
IDeletePolicyProps,
IDeletePolicyState
> {
state: IDeletePolicyState = {
deleteLoading: false,
deleteError: ""
@@ -88,7 +91,7 @@ class DeletePolicy extends React.Component<IDeletePolicyProps, IDeletePolicyStat
<Dialog
open={deleteOpen}
onClose={() => {
this.setState({deleteError:""},()=>{
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}
@@ -117,7 +120,7 @@ class DeletePolicy extends React.Component<IDeletePolicyProps, IDeletePolicyStat
<DialogActions>
<Button
onClick={() => {
this.setState({deleteError:""},()=>{
this.setState({ deleteError: "" }, () => {
this.props.closeDeleteModalAndRefresh(false);
});
}}

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import {createStyles, Theme, withStyles} from "@material-ui/core/styles";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
@@ -36,13 +36,13 @@ import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import SearchIcon from "@material-ui/icons/Search";
import Moment from "react-moment";
import {PolicyList, Policy} from "./types";
import { PolicyList, Policy } from "./types";
import AddPolicy from "./AddPolicy";
import DeletePolicy from "./DeletePolicy";
import api from "../../../common/api";
import {CreateIcon} from "../../../icons";
import {MinTablePaginationActions} from "../../../common/MinTablePaginationActions";
import VisibilityIcon from '@material-ui/icons/Visibility';
import { CreateIcon } from "../../../icons";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import VisibilityIcon from "@material-ui/icons/Visibility";
const styles = (theme: Theme) =>
createStyles({

View File

@@ -23,7 +23,7 @@ export interface Statement {
export interface Policy {
name: string;
version: string;
statements: Statement[]
statements: Statement[];
}
export interface PolicyList {

View File

@@ -37,7 +37,7 @@ const styles = (theme: Theme) =>
color: "red"
},
strongText: {
fontWeight: 700,
fontWeight: 700
},
keyName: {
marginLeft: 5
@@ -71,13 +71,13 @@ class AddUserContent extends React.Component<
secretKey: "",
selectedGroups: [],
loadingGroups: false,
groupsList: [],
groupsList: []
};
componentDidMount(): void {
const { selectedUser } = this.props;
if (selectedUser !== null) {
console.log('selUsr', selectedUser);
console.log("selUsr", selectedUser);
this.setState({
accessKey: selectedUser.accessKey,
secretKey: ""
@@ -97,7 +97,7 @@ class AddUserContent extends React.Component<
api
.invoke("PUT", `/api/v1/users/${selectedUser.accessKey}`, {
accessKey,
secretKey: (secretKey != "" ? null : secretKey),
secretKey: secretKey != "" ? null : secretKey
})
.then(res => {
this.setState(
@@ -120,7 +120,7 @@ class AddUserContent extends React.Component<
api
.invoke("POST", "/api/v1/users", {
accessKey,
secretKey,
secretKey
})
.then(res => {
this.setState(
@@ -146,12 +146,20 @@ class AddUserContent extends React.Component<
render() {
const { classes, selectedUser } = this.props;
const { addLoading, addError, accessKey, secretKey, selectedGroups, loadingGroups, groupsList } = this.state;
const {
addLoading,
addError,
accessKey,
secretKey,
selectedGroups,
loadingGroups,
groupsList
} = this.state;
return (
<React.Fragment>
<DialogTitle id="alert-dialog-title">
{selectedUser !== null ? 'Edit User' : 'Add User'}
{selectedUser !== null ? "Edit User" : "Add User"}
</DialogTitle>
<DialogContent>
<form
@@ -175,37 +183,39 @@ class AddUserContent extends React.Component<
)}
{selectedUser !== null ? (
<React.Fragment>
<span className={classes.strongText}>Access Key:</span>
<span className={classes.keyName}>{` ${accessKey}`}</span>
</React.Fragment>
<React.Fragment>
<span className={classes.strongText}>Access Key:</span>
<span className={classes.keyName}>{` ${accessKey}`}</span>
</React.Fragment>
) : (
<React.Fragment>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Access Key"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ accessKey: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-multiline-static"
label={selectedUser !== null ? 'New Secret Key': 'Secret Key'}
type="password"
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ secretKey: e.target.value });
}}
autoComplete="current-password"
/>
</Grid>
</React.Fragment>
<React.Fragment>
<Grid item xs={12}>
<TextField
id="standard-basic"
fullWidth
label="Access Key"
value={accessKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ accessKey: e.target.value });
}}
/>
</Grid>
<Grid item xs={12}>
<TextField
id="standard-multiline-static"
label={
selectedUser !== null ? "New Secret Key" : "Secret Key"
}
type="password"
fullWidth
value={secretKey}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ secretKey: e.target.value });
}}
autoComplete="current-password"
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12}>
@@ -213,16 +223,14 @@ class AddUserContent extends React.Component<
</Grid>
<Grid item xs={12}>
<GroupsSelectors
selectedGroups={selectedGroups}
setSelectedGroups={
(elements: string[]) => {
this.setState({
selectedGroups: elements
})
}
}
loading={loadingGroups}
records={groupsList}
selectedGroups={selectedGroups}
setSelectedGroups={(elements: string[]) => {
this.setState({
selectedGroups: elements
});
}}
loading={loadingGroups}
records={groupsList}
/>
</Grid>
<Grid item xs={12}>

View File

@@ -17,143 +17,141 @@
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import React from "react";
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 { User, UsersList } from "./types";
import Typography from "@material-ui/core/Typography";
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red"
}
});
createStyles({
errorBlock: {
color: "red"
}
});
interface IDeleteUserProps {
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedUser: User | null;
classes: any;
closeDeleteModalAndRefresh: (refresh: boolean) => void;
deleteOpen: boolean;
selectedUser: User | null;
}
interface IDeleteUserState {
deleteLoading: boolean;
deleteError: string;
deleteLoading: boolean;
deleteError: string;
}
class DeleteUser extends React.Component<
IDeleteUserProps,
IDeleteUserState
> {
state: IDeleteUserState = {
deleteLoading: false,
deleteError: ""
};
class DeleteUser extends React.Component<IDeleteUserProps, IDeleteUserState> {
state: IDeleteUserState = {
deleteLoading: false,
deleteError: ""
};
removeRecord() {
const { deleteLoading } = this.state;
const { selectedUser } = this.props;
if (deleteLoading) {
return;
}
if (selectedUser == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/users/${selectedUser.accessKey}`, {
id: selectedUser.id
})
.then((res: UsersList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
removeRecord() {
const { deleteLoading } = this.state;
const { selectedUser } = this.props;
if (deleteLoading) {
return;
}
if (selectedUser == null) {
return;
}
this.setState({ deleteLoading: true }, () => {
api
.invoke("DELETE", `/api/v1/users/${selectedUser.accessKey}`, {
id: selectedUser.id
})
.then((res: UsersList) => {
this.setState(
{
deleteLoading: false,
deleteError: ""
},
() => {
this.props.closeDeleteModalAndRefresh(true);
}
);
})
.catch(err => {
this.setState({
deleteLoading: false,
deleteError: err
});
});
});
}
render() {
const { classes, deleteOpen, selectedUser } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedUser === null) {
return <div />;
}
render() {
const { classes, deleteOpen, selectedUser } = this.props;
const { deleteLoading, deleteError } = this.state;
if (selectedUser === null) {
return <div />;
}
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 User</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete user{" "}<b>{selectedUser.accessKey}</b>?
{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>
);
}
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 User</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete user <b>{selectedUser.accessKey}</b>
?
{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)(DeleteUser);

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 from 'react';
import React from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { LinearProgress } from "@material-ui/core";
import Table from "@material-ui/core/Table";
@@ -31,158 +31,157 @@ import TextField from "@material-ui/core/TextField";
import Checkbox from "@material-ui/core/Checkbox";
interface IGroupsProps {
classes: any;
selectedGroups: string[];
setSelectedGroups: any;
records: any[];
loading: boolean;
classes: any;
selectedGroups: string[];
setSelectedGroups: any;
records: any[];
loading: boolean;
}
const styles = (theme: Theme) =>
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight:'bold'
}
}
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10,
}
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: '100%'
},
noFound: {
textAlign: "center",
padding: "10px 0",
createStyles({
seeMore: {
marginTop: theme.spacing(3)
},
paper: {
// padding: theme.spacing(2),
display: "flex",
overflow: "auto",
flexDirection: "column"
},
addSideBar: {
width: "320px",
padding: "20px"
},
errorBlock: {
color: "red"
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0)
},
wrapCell: {
maxWidth: "200px",
whiteSpace: "normal",
wordWrap: "break-word"
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold"
}
});
}
},
actionsTray: {
textAlign: "left",
"& button": {
marginLeft: 10
}
},
filterField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
width: "100%"
},
noFound: {
textAlign: "center",
padding: "10px 0"
}
});
const GroupsSelectors = ({
classes,
selectedGroups,
setSelectedGroups,
records,
loading
}: IGroupsProps) => {
classes,
selectedGroups,
setSelectedGroups,
records,
loading
}: IGroupsProps) => {
if (!records) {
return null;
}
if(!records) {
return null;
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
let elements: string[] = [...selectedGroups]; // We clone the selectedGroups array
if (checked) {
// If the user has checked this field we need to push this to selectedGroupsList
elements.push(value);
} else {
// User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
}
setSelectedGroups(elements);
const selectionChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
const targetD = e.target;
const value = targetD.value;
const checked = targetD.checked;
return elements;
};
let elements : string[] = [...selectedGroups]; // We clone the selectedGroups array
if(checked) { // If the user has checked this field we need to push this to selectedGroupsList
elements.push(value);
} else { // User has unchecked this field, we need to remove it from the list
elements = elements.filter(element => element !== value);
}
setSelectedGroups(elements);
return elements;
};
return (
<React.Fragment>
<Title>Groups</Title>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter Groups"
className={classes.filterField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
</Grid>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Group</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={`group-${row.groupName}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.groupName}
color="primary"
inputProps={{
'aria-label': 'secondary checkbox'
}}
onChange={ selectionChanged }
checked={selectedGroups.includes(row.groupName)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.groupName}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</React.Fragment>
) : (
<div className={classes.noFound}>No Groups Available</div>
)}
</Paper>
</Grid>
</React.Fragment>
);
return (
<React.Fragment>
<Title>Groups</Title>
<Grid item xs={12}>
<Paper className={classes.paper}>
{loading && <LinearProgress />}
{records != null && records.length > 0 ? (
<React.Fragment>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Filter Groups"
className={classes.filterField}
id="search-resource"
label=""
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
)
}}
/>
</Grid>
<Table size="medium">
<TableHead className={classes.minTableHeader}>
<TableRow>
<TableCell>Select</TableCell>
<TableCell>Group</TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map(row => (
<TableRow key={`group-${row.groupName}`}>
<TableCell padding="checkbox">
<Checkbox
value={row.groupName}
color="primary"
inputProps={{
"aria-label": "secondary checkbox"
}}
onChange={selectionChanged}
checked={selectedGroups.includes(row.groupName)}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.groupName}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</React.Fragment>
) : (
<div className={classes.noFound}>No Groups Available</div>
)}
</Paper>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(GroupsSelectors);

View File

@@ -25,14 +25,15 @@ import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import api from "../../../common/api";
import {
Button, IconButton,
LinearProgress,
TableFooter,
TablePagination
Button,
IconButton,
LinearProgress,
TableFooter,
TablePagination
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { User, UsersList } from "./types";
import { usersSort } from '../../../utils/sortFunctions';
import { usersSort } from "../../../utils/sortFunctions";
import { MinTablePaginationActions } from "../../../common/MinTablePaginationActions";
import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
@@ -76,21 +77,21 @@ const styles = (theme: Theme) =>
color: "#393939",
"& tr": {
"& th": {
fontWeight:'bold'
fontWeight: "bold"
}
}
},
actionsTray: {
textAlign: "right",
"& button": {
marginLeft: 10,
marginLeft: 10
}
},
searchField: {
background: "#FFFFFF",
padding: 12,
borderRadius: 5,
boxShadow: "0px 3px 6px #00000012",
boxShadow: "0px 3px 6px #00000012"
}
});
@@ -124,7 +125,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
rowsPerPage: 10,
deleteOpen: false,
selectedUser: null,
addGroupOpen: false,
addGroupOpen: false
};
fetchRecords() {
@@ -234,7 +235,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
)
}}
/>
<Button
@@ -256,7 +257,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: null,
selectedUser: null
});
}}
>
@@ -286,35 +287,35 @@ class Users extends React.Component<IUsersProps, IUsersState> {
<Checkbox
value="secondary"
color="primary"
inputProps={{ 'aria-label': 'secondary checkbox' }}
inputProps={{ "aria-label": "secondary checkbox" }}
/>
</TableCell>
<TableCell className={classes.wrapCell}>
{row.accessKey}
</TableCell>
<TableCell align="right">
<IconButton
aria-label="view"
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: row,
});
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
this.setState({
deleteOpen: true,
selectedUser: row,
});
}}
>
<DeleteIcon />
</IconButton>
<IconButton
aria-label="view"
onClick={() => {
this.setState({
addScreenOpen: true,
selectedUser: row
});
}}
>
<ViewIcon />
</IconButton>
<IconButton
aria-label="delete"
onClick={() => {
this.setState({
deleteOpen: true,
selectedUser: row
});
}}
>
<DeleteIcon />
</IconButton>
</TableCell>
</TableRow>
))}

View File

@@ -1,252 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2020 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import Avatar from "@material-ui/core/Avatar";
import Button from "@material-ui/core/Button";
import CssBaseline from "@material-ui/core/CssBaseline";
import TextField from "@material-ui/core/TextField";
import Grid from "@material-ui/core/Grid";
import Box from "@material-ui/core/Box";
import LockOutlinedIcon from "@material-ui/icons/LockOutlined";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import request from "superagent";
import { useHistory } from "react-router";
import { CircularProgress } from "@material-ui/core";
import storage from "local-storage-fallback";
import Copyright from "../common/Copyright";
const useStyles = makeStyles(theme => ({
"@global": {
body: {
backgroundColor: theme.palette.common.white
}
},
paper: {
marginTop: theme.spacing(8),
display: "flex",
flexDirection: "column",
alignItems: "center"
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main
},
form: {
width: "100%", // Fix IE 11 issue.
marginTop: theme.spacing(3)
},
submit: {
margin: theme.spacing(3, 0, 2)
},
errorBlock: {
color: "red"
},
spinner: {
margin: "auto"
}
}));
const CreatePassword: React.FC = () => {
const classes = useStyles();
const { push } = useHistory();
const [password, setPassword] = React.useState<string>("");
const [token, setToken] = React.useState<string>("");
const [tokenValidated, setTokenValidated] = React.useState<boolean>(false);
const [tokenValid, setTokenValid] = React.useState<boolean>(true);
const [repeatPassword, setRepeatPassword] = React.useState<string>("");
const [error, setError] = React.useState<string>("");
const formSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (password !== repeatPassword) {
setError("Passwords don't match");
return;
}
if (password.length < 8) {
setError("Password should longer than 8 characters");
return;
}
const url = "/api/v1/users/set_password";
request
.post(url)
.send({
url_token: token,
password: password
})
.then((res: any) => {
if (res.body.jwt_token) {
// store the jwt token
storage.setItem("token", res.body.jwt_token);
return res.body.jwt_token;
} else if (res.body.error) {
// throw will be moved to catch block once bad CreatePassword returns 403
throw res.body.error;
}
})
.then(() => {
push("/");
})
.catch(err => {
setError(err);
});
};
// validate the token passed via url
React.useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const urlToken = urlParams.get("t") as string;
if (urlToken === null || urlToken === "") {
return;
}
setToken(urlToken);
const url = "/api/v1/validate_invite";
request
.post(url)
.send({ url_token: urlToken })
.then((res: any) => {
console.log(res);
// store the email to display
setTokenValidated(true);
})
.catch(err => {
setError(err);
setTokenValidated(true);
setTokenValid(false);
});
}, []); // empty array ensures the effect is ran only once on component mount
return (
<Container component="main" maxWidth="xs">
<CssBaseline />
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon />
</Avatar>
<Typography component="h1" variant="h5">
Create Password
</Typography>
{!tokenValidated && (
<Grid container spacing={2}>
<Grid item xs={12} alignItems="center" justify="center">
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
>
<Grid item xs={12} alignItems="center" justify="center">
<CircularProgress />
</Grid>
<Grid item xs={12} alignItems="center" justify="center">
<Typography variant="body1" gutterBottom>
Validating token.
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
)}
{tokenValidated && !tokenValid && (
<Grid container spacing={2}>
<Grid item xs={12} alignItems="center" justify="center">
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justify="center"
>
<Grid item xs={12} alignItems="center" justify="center">
<Typography variant="body1" gutterBottom>
This token is invalid.
</Typography>
</Grid>
</Grid>
</Grid>
</Grid>
)}
{tokenValidated && tokenValid && (
<form className={classes.form} noValidate onSubmit={formSubmit}>
<Grid container spacing={2}>
{error !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{`${error}`}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
value={password}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setPassword(e.target.value)
}
name="password"
label="Password"
type="password"
id="password"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
value={repeatPassword}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setRepeatPassword(e.target.value)
}
name="repeat_password"
label="Repeat Password"
type="password"
id="repeat_password"
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Create Password
</Button>
</form>
)}
</div>
<Box mt={5}>
<Copyright />
</Box>
</Container>
);
};
export default CreatePassword;

View File

@@ -14,40 +14,41 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import CssBaseline from '@material-ui/core/CssBaseline';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import {makeStyles} from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
import React from "react";
import CssBaseline from "@material-ui/core/CssBaseline";
import Box from "@material-ui/core/Box";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
import Container from "@material-ui/core/Container";
import Copyright from "../common/Copyright";
import history from "../history";
const useStyles = makeStyles(theme => ({
'@global': {
"@global": {
body: {
backgroundColor: theme.palette.common.white,
},
backgroundColor: theme.palette.common.white
}
},
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
display: "flex",
flexDirection: "column",
alignItems: "center"
}
}));
const NotFound: React.FC = () => {
const classes = useStyles();
console.log(history);
return (
<Container component="main">
<CssBaseline/>
<CssBaseline />
<div className={classes.paper}>
<Typography variant="h1" component="h1">
404 Not Found
</Typography>
</div>
<Box mt={5}>
<Copyright/>
<Copyright />
</Box>
</Container>
);

View File

@@ -1,207 +0,0 @@
// This file is part of MinIO Console Server
// Copyright (c) 2019 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from 'react';
import Avatar from '@material-ui/core/Avatar';
import Button from '@material-ui/core/Button';
import CssBaseline from '@material-ui/core/CssBaseline';
import TextField from '@material-ui/core/TextField';
import FormControlLabel from '@material-ui/core/FormControlLabel';
import Checkbox from '@material-ui/core/Checkbox';
import Link from '@material-ui/core/Link';
import Grid from '@material-ui/core/Grid';
import Box from '@material-ui/core/Box';
import LockOutlinedIcon from '@material-ui/icons/LockOutlined';
import Typography from '@material-ui/core/Typography';
import {makeStyles} from '@material-ui/core/styles';
import Container from '@material-ui/core/Container';
import Copyright from "../common/Copyright";
const useStyles = makeStyles(theme => ({
'@global': {
body: {
backgroundColor: theme.palette.common.white,
},
},
paper: {
marginTop: theme.spacing(8),
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
},
avatar: {
margin: theme.spacing(1),
backgroundColor: theme.palette.secondary.main,
},
form: {
width: '100%', // Fix IE 11 issue.
marginTop: theme.spacing(3),
},
submit: {
margin: theme.spacing(3, 0, 2),
},
}));
const Signup: React.FC = () => {
const classes = useStyles();
return (
<Container component="main" maxWidth="xs">
<CssBaseline/>
<div className={classes.paper}>
<Avatar className={classes.avatar}>
<LockOutlinedIcon/>
</Avatar>
<Typography component="h1" variant="h5">
Acme Storage Sign up
</Typography>
<form className={classes.form} noValidate>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
autoComplete="fullname"
name="fullname"
variant="outlined"
required
fullWidth
id="fullname"
label="Full Name"
autoFocus
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="email"
label="Email Address"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
name="password"
label="Password"
type="password"
id="password"
autoComplete="current-password"
/>
</Grid>
<hr/>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="company"
label="Company Name"
name="email"
autoComplete="email"
/>
</Grid>
<Grid item xs={12}>
<TextField
variant="outlined"
required
fullWidth
id="company_short_name"
label="Short Name"
name="company_short_name"
autoComplete="company_short_name"
/>
</Grid>
<Grid item xs={12} sm={8}>
<TextField
variant="outlined"
required
fullWidth
id="credit_card_number"
label="Credit Card Number"
name="credit_card_number"
/>
</Grid>
<Grid item xs={12} sm={4}>
<TextField
variant="outlined"
required
fullWidth
id="ccv"
label="CCV"
name="ccv"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="expiration_mm"
label="Expiration MM"
name="expiration_mm"
autoComplete="expiration_mm"
/>
</Grid>
<Grid item xs={12} sm={6}>
<TextField
variant="outlined"
required
fullWidth
id="expiration_yy"
label="Expiration YY"
name="expiration_YY"
autoComplete="expiration_yy"
/>
</Grid>
<Grid item xs={12}>
<FormControlLabel
control={<Checkbox value="allowExtraEmails" color="primary"/>}
label="Agree to Terms & Conditions."
/>
</Grid>
</Grid>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
className={classes.submit}
>
Sign Up
</Button>
<Grid container justify="flex-end">
<Grid item>
<Link href="#" variant="body2">
Already have an account? Sign in
</Link>
</Grid>
</Grid>
</form>
</div>
<Box mt={5}>
<Copyright/>
</Box>
</Container>
);
};
export default Signup;