UI: Dashboard (#24)

* UI: Dashboard

* h1 MinIO Console

* UI: Dashboard

* h1 MinIO Console

* Formatting

* paramter fix

Co-authored-by: Lenin Alevski <alevsk.8772@gmail.com>
This commit is contained in:
Daniel Valdivia
2020-04-03 15:46:42 -07:00
committed by GitHub
parent 1e4ed121e9
commit 36602311fa
6 changed files with 311 additions and 255 deletions

View File

@@ -126,9 +126,13 @@ class AddEvent extends React.Component<IAddEventProps, IAddEventState> {
api api
.invoke("GET", `/api/v1/admin/arns`) .invoke("GET", `/api/v1/admin/arns`)
.then((res: ArnList) => { .then((res: ArnList) => {
let arns: string[] = [];
if (res.arns !== null) {
arns = res.arns;
}
this.setState({ this.setState({
addLoading: false, addLoading: false,
arnList: res.arns, arnList: arns,
addError: "" addError: ""
}); });
}) })

View File

@@ -14,232 +14,210 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React, { useEffect, useState } from "react";
import { makeStyles, useTheme } from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import clsx from "clsx"; import clsx from "clsx";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import Paper from "@material-ui/core/Paper"; import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography"; import Typography from "@material-ui/core/Typography";
import { import NetworkCheckIcon from "@material-ui/icons/NetworkCheck";
CartesianGrid, import PieChartIcon from "@material-ui/icons/PieChart";
Line, import ViewHeadlineIcon from "@material-ui/icons/ViewHeadline";
LineChart, import { Usage } from "./types";
ResponsiveContainer, import api from "../../../common/api";
Legend,
Tooltip,
XAxis,
YAxis
} from "recharts";
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
import PieChartIcon from '@material-ui/icons/PieChart';
import ViewHeadlineIcon from '@material-ui/icons/ViewHeadline';
const useStyles = makeStyles(theme => ({ const styles = (theme: Theme) =>
root: { createStyles({
display: "flex" root: {
}, display: "flex"
toolbar: {
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
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
},
container: {
paddingBottom: theme.spacing(4),
"& h6": {
color: "#777777",
fontSize: 14,
}, },
"& p": { toolbar: {
"& span": { paddingRight: 24 // keep right padding when drawer closed
fontSize: 16, },
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
},
drawerPaperClose: {
overflowX: "hidden",
transition: theme.transitions.create("width", {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen
}),
width: theme.spacing(7),
[theme.breakpoints.up("sm")]: {
width: theme.spacing(9)
}
},
appBarSpacer: theme.mixins.toolbar,
content: {
flexGrow: 1,
height: "100vh",
overflow: "auto"
},
container: {
paddingBottom: theme.spacing(4),
"& h6": {
color: "#777777",
fontSize: 14
}, },
"& p": {
"& span": {
fontSize: 16
}
}
}, },
}, paper: {
paper: { padding: theme.spacing(2),
padding: theme.spacing(2), display: "flex",
display: "flex", overflow: "auto",
overflow: "auto", flexDirection: "column"
flexDirection: "column" },
}, fixedHeight: {
fixedHeight: { minHeight: 240
minHeight: 240, },
}, consumptionValue: {
consumptionValue: { color: "#000000",
color: "#000000", fontSize: "60px",
fontSize: "60px", fontWeight: "bold"
fontWeight: "bold", },
}, icon: {
icon: { marginRight: 10,
marginRight: 10, color: "#777777"
color: "#777777", }
}, });
}));
export default function Dashboard() { interface IDashboardProps {
const theme = useTheme(); classes: any;
const classes = useStyles(theme); }
const Dashboard = ({ classes }: IDashboardProps) => {
const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight); const fixedHeightPaper = clsx(classes.paper, classes.fixedHeight);
const [usage, setUsage] = useState<Usage | null>(null);
const [loading, isLoading] = useState<boolean>(true);
const [error, setError] = useState<string>("");
const data = [39,31,37,29,28,31,31,34,39,40,35,40,24,25,30,20,28,38,23,28,22,39,37,37,40,28,28,28,24,31].map((usage, day ) => ({ usage, day })); useEffect(() => {
const data2 = [25,32,21,40,31,30,23,40,26,32].map((usage, day ) => ({ usage, day })); if (loading) {
fetchUsage();
}
}, [loading]);
const fetchUsage = () => {
api
.invoke("GET", `/api/v1/admin/info`)
.then((res: Usage) => {
setUsage(res);
setError("");
isLoading(false);
})
.catch(err => {
setError(err);
isLoading(false);
});
};
const prettyUsage = (usage: string | undefined) => {
if (usage == undefined) {
return "0";
}
return niceBytes(usage);
};
const units = ["bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
while (n >= 1024 && ++l) {
n = n / 1024;
}
//include a decimal point and a tenths-place digit if presenting
//less than ten of KB or greater units
return n.toFixed(n < 10 && l > 0 ? 1 : 0) + " " + units[l];
};
const prettyNumber = (usage: number | undefined) => {
if (usage == undefined) {
return 0;
}
return usage;
};
return ( return (
<Grid container xs={12}> <React.Fragment>
<Grid container xs={12} spacing={3} className={classes.container}> <Grid container xs={12}>
<Grid item xs={12} md={4} lg={4}> <Grid container xs={12} spacing={3} className={classes.container}>
<Paper className={fixedHeightPaper}> <Grid container xs={12}>
<Grid container direction="row" alignItems="center"> <Typography variant="h2">MinIO Console</Typography>
<Grid item className={classes.icon}> </Grid>
<ViewHeadlineIcon/> <Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<ViewHeadlineIcon />
</Grid>
<Grid item>
<Typography variant="h6">Total Buckets</Typography>
</Grid>
</Grid> </Grid>
<Grid item> <Typography className={classes.consumptionValue}>
<Typography variant="h6">All Buckets</Typography> {usage ? prettyNumber(usage.buckets) : 0}
</Typography>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon />
</Grid>
<Grid item>
<Typography variant="h6"> Total Objects</Typography>
</Grid>
</Grid> </Grid>
</Grid> <Typography className={classes.consumptionValue}>
<Typography className={classes.consumptionValue}>238</Typography> {usage ? prettyNumber(usage.objects) : 0}
</Paper> </Typography>
</Grid> </Paper>
<Grid item xs={12} md={4} lg={4}> </Grid>
<Paper className={fixedHeightPaper}> <Grid item xs={12} md={4} lg={4}>
<Grid container direction="row" alignItems="center"> <Paper className={fixedHeightPaper}>
<Grid item className={classes.icon}> <Grid container direction="row" alignItems="center">
<PieChartIcon/> <Grid item className={classes.icon}>
<PieChartIcon />
</Grid>
<Grid item>
<Typography variant="h6">Usage</Typography>
</Grid>
</Grid> </Grid>
<Grid item> <Typography className={classes.consumptionValue}>
<Typography variant="h6">Usage</Typography> {usage ? prettyUsage(usage.usage + "") : 0}
</Grid> </Typography>
</Grid> </Paper>
<Typography className={classes.consumptionValue}>375<span>TB</span></Typography> </Grid>
</Paper>
</Grid>
<Grid item xs={12} md={4} lg={4}>
<Paper className={fixedHeightPaper}>
<Grid container direction="row" alignItems="center">
<Grid item className={classes.icon}>
<NetworkCheckIcon/>
</Grid>
<Grid item>
<Typography variant="h6"> Egress this Month</Typography>
</Grid>
</Grid>
<Typography className={classes.consumptionValue}>1.5<span>TB</span></Typography>
</Paper>
</Grid> </Grid>
</Grid> </Grid>
<Grid container xs={12} spacing={3} className={classes.container}> </React.Fragment>
<Grid item xs={8}>
<Paper className={fixedHeightPaper}>
<Typography variant="h6">Daily Average Usage</Typography>
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={data}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
type="number"
domain={[1, 31]}
interval={0}
dataKey="day"
axisLine={false}
tickLine={false}
/>
<YAxis
dataKey="usage"
width={80}
tick={{ fill: "#737373" }}
dx={-5}
axisLine={false}
tickLine={false}
/>
<Tooltip />
<Legend />
<Line
strokeWidth={2}
yAxisId={0}
type="monotone"
dataKey="usage"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
</LineChart>
</ResponsiveContainer>
</Paper>
</Grid>
<Grid item xs={4}>
<Paper className={fixedHeightPaper}>
<Typography variant="h6">Daily Network Egress</Typography>
<ResponsiveContainer width="100%" height={400}>
<LineChart
data={data2}
>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
type="number"
dataKey="day"
axisLine={false}
tickLine={false}
/>
<YAxis
dataKey="usage"
tick={{ fill: "#737373" }}
dx={-5}
axisLine={false}
tickLine={false}
/>
<Tooltip />
<Legend />
<Line
strokeWidth={2}
yAxisId={0}
type="monotone"
dataKey="usage"
stroke="#8884d8"
activeDot={{ r: 8 }}
/>
</LineChart>
</ResponsiveContainer>
</Paper>
</Grid>
</Grid>
</Grid>
); );
} };
export default withStyles(styles)(Dashboard);

View File

@@ -0,0 +1,21 @@
// 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/>.
export interface Usage {
usage: number;
buckets: number;
objects: number;
}

View File

@@ -32,7 +32,7 @@ import api from "../../../common/api";
import "codemirror/lib/codemirror.css"; import "codemirror/lib/codemirror.css";
import "codemirror/theme/material.css"; import "codemirror/theme/material.css";
import PolicyBuilder from "./PolicyBuilder"; import PolicyBuilder from "./PolicyBuilder";
import {Policy} from "./types"; import { Policy } from "./types";
require("codemirror/mode/javascript/javascript"); require("codemirror/mode/javascript/javascript");
const styles = (theme: Theme) => const styles = (theme: Theme) =>
@@ -144,7 +144,6 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
id="standard-basic" id="standard-basic"
fullWidth fullWidth
label="Policy Name" label="Policy Name"
value={policyEdit ? policyEdit.name : ""}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => { onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ policyName: e.target.value }); this.setState({ policyName: e.target.value });
}} }}
@@ -170,17 +169,19 @@ class AddPolicy extends React.Component<IAddPolicyProps, IAddPolicyState> {
<Grid item xs={12}> <Grid item xs={12}>
<br /> <br />
</Grid> </Grid>
{!policyEdit && <Grid item xs={12}> {!policyEdit && (
<Button <Grid item xs={12}>
type="submit" <Button
variant="contained" type="submit"
color="primary" variant="contained"
fullWidth color="primary"
disabled={addLoading} fullWidth
> disabled={addLoading}
Save >
</Button> Save
</Grid>} </Button>
</Grid>
)}
{addLoading && ( {addLoading && (
<Grid item xs={12}> <Grid item xs={12}>
<LinearProgress /> <LinearProgress />

View File

@@ -120,7 +120,7 @@ class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
deleteOpen: false, deleteOpen: false,
selectedPolicy: "", selectedPolicy: "",
filterPolicies: "", filterPolicies: "",
policyEdit: null, policyEdit: null
}; };
fetchRecords() { fetchRecords() {
@@ -187,7 +187,7 @@ class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
deleteOpen, deleteOpen,
selectedPolicy, selectedPolicy,
filterPolicies, filterPolicies,
policyEdit, policyEdit
} = this.state; } = this.state;
const offset = page * rowsPerPage; const offset = page * rowsPerPage;
@@ -250,7 +250,7 @@ class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
onClick={() => { onClick={() => {
this.setState({ this.setState({
addScreenOpen: true, addScreenOpen: true,
policyEdit: null, policyEdit: null
}); });
}} }}
> >
@@ -294,7 +294,7 @@ class Policies extends React.Component<IPoliciesProps, IPoliciesState> {
onClick={() => { onClick={() => {
this.setState({ this.setState({
addScreenOpen: true, addScreenOpen: true,
policyEdit: row, policyEdit: row
}); });
}} }}
> >

View File

@@ -15,16 +15,24 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react"; import React from "react";
import {createStyles, Theme, withStyles} from "@material-ui/core/styles"; import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid"; import Grid from "@material-ui/core/Grid";
import remove from "lodash/remove"; import remove from "lodash/remove";
import {Checkbox, FormControlLabel, FormGroup, FormLabel, Paper, Radio, RadioGroup} from "@material-ui/core"; import {
import {Statement} from "./types"; Checkbox,
FormControlLabel,
FormGroup,
FormLabel,
Paper,
Radio,
RadioGroup
} from "@material-ui/core";
import { Statement } from "./types";
const styles = (theme: Theme) => const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
flexGrow: 1, flexGrow: 1
}, },
errorBlock: { errorBlock: {
color: "red" color: "red"
@@ -38,9 +46,9 @@ const styles = (theme: Theme) =>
}, },
paper: { paper: {
padding: theme.spacing(1), padding: theme.spacing(1),
textAlign: 'center', textAlign: "center",
color: theme.palette.text.secondary, color: theme.palette.text.secondary
}, }
}); });
interface IPolicyBuilderProps { interface IPolicyBuilderProps {
@@ -56,22 +64,29 @@ interface IPolicyBuilderState {
currentStatementRead: boolean; currentStatementRead: boolean;
} }
class PolicyBuilder extends React.Component<IPolicyBuilderProps, IPolicyBuilderState> { class PolicyBuilder extends React.Component<
IPolicyBuilderProps,
IPolicyBuilderState
> {
state: IPolicyBuilderState = { state: IPolicyBuilderState = {
policyString: "", policyString: "",
statements: [], statements: [],
currentStatement: { currentStatement: {
effect: "", effect: "",
actions: [], actions: [],
resources: [], resources: []
}, },
currentStatementWrite: false, currentStatementWrite: false,
currentStatementRead: false, currentStatementRead: false
}; };
render() { render() {
const { classes, policyDefinition } = this.props; const { classes, policyDefinition } = this.props;
const { currentStatement,currentStatementWrite, currentStatementRead } = this.state; const {
currentStatement,
currentStatementWrite,
currentStatementRead
} = this.state;
console.log(currentStatement); console.log(currentStatement);
return ( return (
<div className={classes.root}> <div className={classes.root}>
@@ -85,12 +100,25 @@ class PolicyBuilder extends React.Component<IPolicyBuilderProps, IPolicyBuilderS
aria-label="effect" aria-label="effect"
name="effect" name="effect"
value={currentStatement.effect} value={currentStatement.effect}
onChange={(e: React.ChangeEvent<HTMLInputElement>, value: string) => { onChange={(
this.setState( { currentStatement: { ...currentStatement, effect: value }}); e: React.ChangeEvent<HTMLInputElement>,
value: string
) => {
this.setState({
currentStatement: { ...currentStatement, effect: value }
});
}} }}
> >
<FormControlLabel value="Deny" control={<Radio />} label="Deny" /> <FormControlLabel
<FormControlLabel value="Allow" control={<Radio />} label="Allow" /> value="Deny"
control={<Radio />}
label="Deny"
/>
<FormControlLabel
value="Allow"
control={<Radio />}
label="Allow"
/>
</RadioGroup> </RadioGroup>
</Paper> </Paper>
</Grid> </Grid>
@@ -102,16 +130,30 @@ class PolicyBuilder extends React.Component<IPolicyBuilderProps, IPolicyBuilderS
control={ control={
<Checkbox <Checkbox
checked={currentStatementRead} checked={currentStatementRead}
onChange={(e: React.ChangeEvent<HTMLInputElement>, checked: boolean) => { onChange={(
const readActions = ["s3:ListBucket", "s3:GetObject", "s3:GetBucketLocation"]; e: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
const readActions = [
"s3:ListBucket",
"s3:GetObject",
"s3:GetBucketLocation"
];
let actions = currentStatement.actions; let actions = currentStatement.actions;
if (checked) { if (checked) {
actions.push(...readActions) actions.push(...readActions);
} else { } else {
actions = remove(actions, (action) => readActions.includes(action)) actions = remove(actions, action =>
readActions.includes(action)
);
} }
this.setState( { currentStatement: { ...currentStatement, actions: actions }}); this.setState({
this.setState( { currentStatementRead: checked }); currentStatement: {
...currentStatement,
actions: actions
}
});
this.setState({ currentStatementRead: checked });
}} }}
name="read" name="read"
/> />
@@ -122,16 +164,26 @@ class PolicyBuilder extends React.Component<IPolicyBuilderProps, IPolicyBuilderS
control={ control={
<Checkbox <Checkbox
checked={currentStatementWrite} checked={currentStatementWrite}
onChange={(e: React.ChangeEvent<HTMLInputElement>, checked: boolean) => { onChange={(
e: React.ChangeEvent<HTMLInputElement>,
checked: boolean
) => {
const writeActions = ["s3:PutObject"]; const writeActions = ["s3:PutObject"];
let actions = currentStatement.actions; let actions = currentStatement.actions;
if (checked) { if (checked) {
actions.push(...writeActions) actions.push(...writeActions);
} else { } else {
actions = remove(actions, (action) => writeActions.includes(action)) actions = remove(actions, action =>
writeActions.includes(action)
);
} }
this.setState( { currentStatement: { ...currentStatement, actions: actions }}); this.setState({
this.setState( { currentStatementWrite: checked }); currentStatement: {
...currentStatement,
actions: actions
}
});
this.setState({ currentStatementWrite: checked });
}} }}
name="write" name="write"
/> />
@@ -150,7 +202,7 @@ class PolicyBuilder extends React.Component<IPolicyBuilderProps, IPolicyBuilderS
</Grid> </Grid>
</Grid> </Grid>
</div> </div>
) );
} }
} }