Compare commits

...

9 Commits

Author SHA1 Message Date
Alex
a805a49662 Added loaders to bucket information block (#141)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 22:46:42 -07:00
Daniel Valdivia
296e4ff5ce Set Policy For Groups (#140) 2020-05-22 16:09:24 -07:00
Alex
20749d2eae Implemented calculation for zone size in zone modal (#137) 2020-05-22 14:49:42 -05:00
Alex
ff4e959d11 Fixed styles in users policy modal (#139)
Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-22 12:36:41 -07:00
Daniel Valdivia
37195fefa8 Set Policy UI (#138) 2020-05-22 08:48:55 -07:00
Alex
13ef83cee4 Added Clusters mockups (#133) 2020-05-21 20:03:36 -05:00
Alex
b89b2d0c6a Changed bucket detail styles & minor fixes (#136)
Changed bucket detail styles & fixed minor issues for edit access policy & usage report not shown in page

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
2020-05-21 17:19:50 -07:00
Daniel Valdivia
edf687fd8a Loading text on TableWrapper (#135) 2020-05-21 16:49:31 -07:00
Daniel Valdivia
cb60eba373 Landing images (#134) 2020-05-21 12:55:30 -07:00
30 changed files with 2589 additions and 236 deletions

View File

@@ -2,6 +2,11 @@
A graphical user interface for [MinIO](https://github.com/minio/minio)
| Dashboard | Adding A User |
| ------------- | ------------- |
| ![Dashboard](images/pic1.png) | ![Dashboard](images/pic2.png) |
## Setup
All `mcs` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.

BIN
images/pic1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 KiB

BIN
images/pic2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 688 KiB

View File

@@ -31,8 +31,10 @@ var (
watch = "/watch"
notifications = "/notification-endpoints"
buckets = "/buckets"
bucketDetails = "/buckets/:bucketName"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
clusters = "/clusters"
clustersDetail = "/clusters/:clusterName"
)
type ConfigurationActionSet struct {
@@ -181,12 +183,18 @@ var bucketsActionSet = ConfigurationActionSet{
),
}
// serviceAccountsActionSet contains the list of admin actions required for this endpoint to work
// serviceAccountsActionSet no actions needed for this module to work
var serviceAccountsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// clustersActionSet temporally no actions needed for clusters sections to work
var clustersActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
// endpointRules contains the mapping between endpoints and ActionSets, additional rules can be added here
var endpointRules = map[string]ConfigurationActionSet{
configuration: configurationActionSet,
@@ -200,8 +208,10 @@ var endpointRules = map[string]ConfigurationActionSet{
watch: watchActionSet,
notifications: notificationsActionSet,
buckets: bucketsActionSet,
bucketDetails: bucketsActionSet,
bucketsDetail: bucketsActionSet,
serviceAccounts: serviceAccountsActionSet,
clusters: clustersActionSet,
clustersDetail: clustersActionSet,
}
// GetActionsStringFromPolicy extract the admin/s3 actions from a given policy and return them in []string format

View File

@@ -37,7 +37,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
args: args{
[]string{"admin:ServerInfo"},
},
want: 2,
want: 4,
},
{
name: "policies endpoint",
@@ -50,7 +50,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:ListUserPolicies",
},
},
want: 2,
want: 4,
},
{
name: "all admin endpoints",
@@ -59,7 +59,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"admin:*",
},
},
want: 10,
want: 12,
},
{
name: "all s3 endpoints",
@@ -68,7 +68,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 4,
want: 6,
},
{
name: "all admin and s3 endpoints",
@@ -78,7 +78,7 @@ func TestGetAuthorizedEndpoints(t *testing.T) {
"s3:*",
},
},
want: 13,
want: 15,
},
{
name: "no endpoints",

File diff suppressed because one or more lines are too long

View File

@@ -57,3 +57,30 @@ export const timeFromDate = (d: Date) => {
return `${h}:${m}:${s}:${d.getMilliseconds()}`;
};
// units to be used in a dropdown
export const factorForDropdown = () => {
return units.map((unit) => {
return { label: unit, value: unit };
});
};
//getBytes, converts from a value and a unit from units array to bytes
export const getBytes = (value: string, unit: string) => {
const vl: number = parseFloat(value);
const powFactor = units.findIndex((element) => element === unit);
if (powFactor == -1) {
return 0;
}
const factor = Math.pow(1024, powFactor);
const total = vl * factor;
return total.toString(10);
};
//getTotalSize gets the total size of a value & unit
export const getTotalSize = (value: string, unit: string) => {
const bytes = getBytes(value, unit).toString(10);
return niceBytes(bytes);
};

View File

@@ -35,6 +35,7 @@ interface ISetAccessPolicyProps {
classes: any;
open: boolean;
bucketName: string;
actualPolicy: string;
closeModalAndRefresh: () => void;
}
@@ -86,8 +87,14 @@ class SetAccessPolicy extends React.Component<
});
}
componentDidMount() {
const { actualPolicy } = this.props;
this.setState({ accessPolicy: actualPolicy });
}
render() {
const { classes, open } = this.props;
const { classes, open, actualPolicy } = this.props;
const { addLoading, addError, accessPolicy } = this.state;
return (
<ModalWrapper

View File

@@ -17,9 +17,13 @@
import React from "react";
import get from "lodash/get";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Paper from "@material-ui/core/Paper";
import Grid from "@material-ui/core/Grid";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import CircularProgress from "@material-ui/core/CircularProgress";
import api from "../../../../common/api";
import { BucketEvent, BucketEventList, BucketInfo } from "../types";
import { BucketEvent, BucketEventList, BucketInfo, BucketList } from "../types";
import { Button } from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import SetAccessPolicy from "./SetAccessPolicy";
@@ -28,6 +32,7 @@ import { CreateIcon } from "../../../../icons";
import AddEvent from "./AddEvent";
import DeleteEvent from "./DeleteEvent";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { niceBytes } from "../../../../common/utils";
const styles = (theme: Theme) =>
createStyles({
@@ -76,6 +81,42 @@ const styles = (theme: Theme) =>
textAlign: "center",
padding: "20px",
},
gridContainer: {
display: "grid",
gridTemplateColumns: "auto auto",
gridGap: 8,
justifyContent: "flex-start",
alignItems: "center",
"& div:not(.MuiCircularProgress-root)": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
minWidth: 150,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
paperContainer: {
padding: 15,
paddingLeft: 23,
},
headerContainer: {
display: "flex",
justifyContent: "space-between",
},
capitalizeFirst: {
textTransform: "capitalize",
},
});
interface IViewBucketProps {
@@ -87,9 +128,12 @@ interface IViewBucketState {
info: BucketInfo | null;
records: BucketEvent[];
totalRecords: number;
loading: boolean;
loadingBucket: boolean;
loadingEvents: boolean;
loadingSize: boolean;
error: string;
deleteError: string;
errBucket: string;
setAccessPolicyScreenOpen: boolean;
page: number;
rowsPerPage: number;
@@ -97,6 +141,8 @@ interface IViewBucketState {
deleteOpen: boolean;
selectedBucket: string;
selectedEvent: BucketEvent | null;
bucketSize: string;
errorSize: string;
}
class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
@@ -104,9 +150,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
info: null,
records: [],
totalRecords: 0,
loading: false,
loadingBucket: true,
loadingEvents: true,
loadingSize: true,
error: "",
deleteError: "",
errBucket: "",
setAccessPolicyScreenOpen: false,
page: 0,
rowsPerPage: 10,
@@ -114,10 +163,12 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
deleteOpen: false,
selectedBucket: "",
selectedEvent: null,
bucketSize: "0",
errorSize: "",
};
fetchEvents() {
this.setState({ loading: true }, () => {
this.setState({ loadingBucket: true }, () => {
const { page } = this.state;
const { match } = this.props;
const bucketName = match.params["bucketName"];
@@ -128,7 +179,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
const total = get(res, "total", 0);
this.setState({
loading: false,
loadingEvents: false,
records: events || [],
totalRecords: total,
error: "",
@@ -142,7 +193,50 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
}
})
.catch((err: any) => {
this.setState({ loading: false, error: err });
this.setState({ loadingEvents: false, error: err });
});
});
}
fetchBucketsSize() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingSize: true }, () => {
api
.invoke("GET", `/api/v1/buckets`)
.then((res: BucketList) => {
const resBuckets = get(res, "buckets", []);
const bucketInfo = resBuckets.find(
(bucket) => bucket.name === bucketName
);
const size = get(bucketInfo, "size", "0");
this.setState({
loadingSize: false,
errorSize: "",
bucketSize: size,
});
})
.catch((err: any) => {
this.setState({ loadingSize: false, errorSize: err });
});
});
}
loadInfo() {
const { match } = this.props;
const bucketName = match.params["bucketName"];
this.setState({ loadingBucket: true }, () => {
api
.invoke("GET", `/api/v1/buckets/${bucketName}`)
.then((res: BucketInfo) => {
this.setState({ loadingBucket: false, info: res });
})
.catch((err) => {
this.setState({ loadingBucket: false, errBucket: err });
});
});
}
@@ -161,20 +255,10 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
});
}
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.fetchEvents();
this.fetchBucketsSize();
}
bucketFilter(): void {}
@@ -186,12 +270,15 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
records,
totalRecords,
setAccessPolicyScreenOpen,
loading,
loadingEvents,
loadingBucket,
page,
rowsPerPage,
deleteOpen,
addScreenOpen,
selectedEvent,
bucketSize,
loadingSize,
} = this.state;
const offset = page * rowsPerPage;
@@ -242,6 +329,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<SetAccessPolicy
bucketName={bucketName}
open={setAccessPolicyScreenOpen}
actualPolicy={accessPolicy}
closeModalAndRefresh={() => {
this.closeAddModalAndRefresh();
}}
@@ -258,36 +346,75 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
<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 />
<div className={classes.headerContainer}>
<div>
<Paper className={classes.paperContainer}>
<div className={classes.gridContainer}>
<div>Access Policy:</div>
<div className={classes.capitalizeFirst}>
{loadingBucket ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
accessPolicy.toLowerCase()
)}
</div>
<div>Reported Usage:</div>
<div>
{loadingSize ? (
<CircularProgress
color="primary"
size={16}
variant="indeterminate"
/>
) : (
niceBytes(bucketSize)
)}
</div>
</div>
</Paper>
</div>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
size="medium"
onClick={() => {
this.setState({
setAccessPolicyScreenOpen: true,
});
}}
>
Change Access Policy
</Button>
</div>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Typography variant="h6">Events</Typography>
<Tabs
value={0}
indicatorColor="primary"
textColor="primary"
aria-label="cluster-tabs"
>
<Tab label="Events" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
<Button
variant="contained"
color="primary"
size="small"
startIcon={<CreateIcon />}
size="medium"
onClick={() => {
this.setState({
addScreenOpen: true,
@@ -313,7 +440,7 @@ class ViewBucket extends React.Component<IViewBucketProps, IViewBucketState> {
{ label: "Prefix", elementKey: "prefix" },
{ label: "Suffix", elementKey: "suffix" },
]}
isLoading={loading}
isLoading={loadingEvents}
records={filteredRecords}
entityName="Events"
idField="id"

View File

@@ -0,0 +1,201 @@
import React, { useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import Grid from "@material-ui/core/Grid";
import { factorForDropdown, getTotalSize } from "../../../../common/utils";
import { Button, LinearProgress } from "@material-ui/core";
interface IAddZoneProps {
classes: any;
open: boolean;
onCloseZoneAndReload: (shouldReload: boolean) => void;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
"& div": {
flexGrow: 1,
width: "100%",
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...modalBasic,
});
const AddZoneModal = ({
classes,
open,
onCloseZoneAndReload,
}: IAddZoneProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [zoneName, setZoneName] = useState<string>("");
const [numberOfInstances, setNumberOfInstances] = useState<number>(0);
const [volumesPerInstance, setVolumesPerInstance] = useState<number>(0);
const [sizeFactor, setSizeFactor] = useState<string>("GiB");
const [volumeConfiguration, setVolumeConfig] = useState<string>("");
const [storageClass, setStorageClass] = useState<string>("");
const instanceCapacity: number =
parseFloat(volumeConfiguration) * volumesPerInstance;
const totalCapacity: number = instanceCapacity * numberOfInstances;
return (
<ModalWrapper
onClose={() => onCloseZoneAndReload(false)}
modalOpen={open}
title="Add Zone"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<InputBoxWrapper
id="zone_name"
name="zone_name"
type="string"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setZoneName(e.target.value);
}}
label="Name"
value={zoneName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="number_instances"
name="number_instances"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfInstances(parseInt(e.target.value));
}}
label="Volumes per Server"
value={numberOfInstances.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_instance"
name="volumes_per_instance"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerInstance(parseInt(e.target.value));
}}
label="Volumes per Instance"
value={volumesPerInstance.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeConfig(e.target.value);
}}
label="Size"
value={volumeConfiguration}
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={factorForDropdown()}
/>
</div>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="storage_class"
name="storage_class"
type="string"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setStorageClass(e.target.value);
}}
label="Volumes per Server"
value={storageClass}
/>
</Grid>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{getTotalSize(instanceCapacity.toString(10), sizeFactor)}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{getTotalSize(totalCapacity.toString(10), sizeFactor)}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
<div className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</div>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddZoneModal);

View File

@@ -0,0 +1,428 @@
// 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, { useState, useEffect } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import { Button } from "@material-ui/core";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import Paper from "@material-ui/core/Paper";
import { niceBytes } from "../../../../common/utils";
import AddZoneModal from "./AddZoneModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
interface IClusterDetailsProps {
classes: any;
match: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
containerHeader: {
display: "flex",
justifyContent: "space-between",
},
paperContainer: {
padding: "15px 15px 15px 50px",
},
infoGrid: {
display: "grid",
gridTemplateColumns: "auto auto auto auto",
gridGap: 8,
"& div": {
display: "flex",
alignItems: "center",
},
"& div:nth-child(odd)": {
justifyContent: "flex-end",
fontWeight: 700,
},
"& div:nth-child(2n)": {
paddingRight: 35,
},
},
masterActions: {
width: "25%",
minWidth: "120px",
"& div": {
margin: "5px 0px",
},
},
actionsTray: {
textAlign: "right",
},
...modalBasic,
});
const mainPagination = {
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
ActionsComponent: MinTablePaginationActions,
};
const ClusterDetails = ({ classes, match }: IClusterDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
const [externalIDP, setExternalIDP] = useState<boolean>(false);
const [externalKMS, setExternalKMS] = useState<boolean>(false);
const [zones, setZones] = useState<number>(0);
const [instances, setInstances] = useState<number>(0);
const [drives, setDrives] = useState<number>(0);
const [addZoneOpen, setAddZone] = useState<boolean>(false);
const [addBucketOpen, setAddBucketOpen] = useState<boolean>(false);
const [addReplicationOpen, setAddReplicationOpen] = useState<boolean>(false);
const onCloseZoneAndRefresh = (reload: boolean) => {
setAddZone(false);
if (reload) {
console.log("reload");
}
};
const closeBucketsAndRefresh = () => {
setAddBucketOpen(false);
};
const closeReplicationAndRefresh = (reload: boolean) => {
setAddReplicationOpen(false);
if (reload) {
console.log("reload");
}
};
return (
<React.Fragment>
{addZoneOpen && (
<AddZoneModal
open={addZoneOpen}
onCloseZoneAndReload={onCloseZoneAndRefresh}
/>
)}
{addBucketOpen && (
<AddBucket
open={addBucketOpen}
closeModalAndRefresh={closeBucketsAndRefresh}
/>
)}
{addReplicationOpen && (
<ReplicationSetup
open={addReplicationOpen}
closeModalAndRefresh={closeReplicationAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Cluster > {match.params["clusterName"]}
</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.containerHeader}>
<Paper className={classes.paperContainer}>
<div className={classes.infoGrid}>
<div>Capacity:</div>
<div>{niceBytes(capacity.toString(10))}</div>
<div>Zones:</div>
<div>{zones}</div>
<div>External IDP:</div>
<div>
{externalIDP ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Instances:</div>
<div>{instances}</div>
<div>External KMS:</div>
<div>
{externalKMS ? "Yes" : "No"}&nbsp;&nbsp;
<Button
variant="contained"
color="primary"
size="small"
onClick={() => {}}
>
Edit
</Button>
</div>
<div>Drives:</div>
<div>{drives}</div>
</div>
</Paper>
<div className={classes.masterActions}>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
Warp
</Button>
</div>
<div>
<Button
variant="contained"
color="primary"
fullWidth
onClick={() => {}}
>
See as deployment
</Button>
</div>
</div>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={6}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="cluster-tabs"
>
<Tab label="Zones" />
<Tab label="Buckets" />
<Tab label="Replication" />
</Tabs>
</Grid>
<Grid item xs={6} className={classes.actionsTray}>
{selectedTab === 0 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddZone(true);
}}
>
Add Zone
</Button>
)}
{selectedTab === 1 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddBucketOpen(true);
}}
>
Create Bucket
</Button>
)}
{selectedTab === 2 && (
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setAddReplicationOpen(true);
}}
>
Add Replication
</Button>
)}
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
{selectedTab === 0 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Instances", elementKey: "instances" },
{ label: "# of Drives", elementKey: "drives" },
]}
isLoading={false}
records={[]}
entityName="Zones"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 1 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "replicate",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "mirror",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
{
type: "delete",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Status",
elementKey: "status",
},
{ label: "Name", elementKey: "name" },
{ label: "AccessPolicy", elementKey: "access_policy" },
]}
isLoading={false}
records={[]}
entityName="Buckets"
idField="name"
paginatorConfig={{
...mainPagination,
onChangePage: () => {},
onChangeRowsPerPage: () => {},
}}
/>
)}
{selectedTab === 2 && (
<TableWrapper
itemActions={[
{
type: "view",
onClick: (element) => {
console.log(element);
},
sendOnlyId: true,
},
]}
columns={[
{
label: "Source",
elementKey: "source",
},
{ label: "Source Bucket", elementKey: "source_bucket" },
{ label: "Destination", elementKey: "destination" },
{
label: "Destination Bucket",
elementKey: "destination_bucket",
},
]}
isLoading={false}
records={[]}
entityName="Replication"
idField="id"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: 0,
rowsPerPage: 0,
page: 0,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: () => {},
onChangeRowsPerPage: () => {},
ActionsComponent: MinTablePaginationActions,
}}
/>
)}
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ClusterDetails);

View File

@@ -0,0 +1,218 @@
// 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, { useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Tab from "@material-ui/core/Tab";
import Tabs from "@material-ui/core/Tabs";
interface IReplicationProps {
classes: any;
open: boolean;
closeModalAndRefresh: (refreshList: boolean) => void;
}
interface IDropDownElements {
label: string;
value: string;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
...modalBasic,
});
const ReplicationSetup = ({
classes,
open,
closeModalAndRefresh,
}: IReplicationProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [selectedTab, setSelectedTab] = useState<number>(0);
const [sourceBucket, setSourceBucket] = useState<string>("");
const [clusterSelected, setClusterSelected] = useState<string>("");
const [destinationBucket, setDestinationBucket] = useState<string>("");
const [address, setAddress] = useState<string>("");
const [bucket, setBucket] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const clustersList: IDropDownElements[] = [];
const sourceBuckets: IDropDownElements[] = [];
const destinationBuckets: IDropDownElements[] = [];
return (
<ModalWrapper
modalOpen={open}
title="Add Zone"
onClose={() => {
closeModalAndRefresh(false);
}}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid item xs={12}>
<SelectWrapper
label="Source Bucket"
options={sourceBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSourceBucket(e.target.value as string);
}}
value={sourceBucket}
name="source_bucket"
id="source_bucket"
/>
</Grid>
<Grid item xs={12}>
<Tabs
value={selectedTab}
indicatorColor="primary"
textColor="primary"
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="cluster-tabs"
>
<Tab label="Local Cluster" />
<Tab label="Remote Cluster" />
</Tabs>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
{selectedTab === 0 && (
<React.Fragment>
<Grid item xs={12}>
<SelectWrapper
label="Cluster"
options={clustersList}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setClusterSelected(e.target.value as string);
}}
value={clusterSelected}
name="cluster"
id="cluster"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
label="Destination Bucket"
options={destinationBuckets}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setDestinationBucket(e.target.value as string);
}}
value={destinationBucket}
name="destination_bucket"
id="destination_bucket"
/>
</Grid>
</React.Fragment>
)}
{selectedTab === 1 && (
<React.Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="address"
name="address"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAddress(e.target.value);
}}
label="Address"
value={address}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="bucket"
name="bucket"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setBucket(e.target.value);
}}
label="Bucket"
value={bucket}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="accessKey"
name="accessKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secretKey"
name="secretKey"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
</React.Fragment>
)}
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(ReplicationSetup);

View File

@@ -0,0 +1,325 @@
// 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, { useState, useEffect } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button, LinearProgress } from "@material-ui/core";
import api from "../../../../common/api";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { modalBasic } from "../../Common/FormComponents/common/styleLibrary";
import { IVolumeConfiguration, IZone } from "./types";
import CheckboxWrapper from "../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { factorForDropdown, units } from "../../../../common/utils";
import ZonesMultiSelector from "./ZonesMultiSelector";
import { storageClasses } from "../utils";
interface IAddClusterProps {
open: boolean;
closeModalAndRefresh: (reloadData: boolean) => any;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
buttonContainer: {
textAlign: "right",
},
multiContainer: {
display: "flex",
alignItems: "center" as const,
justifyContent: "flex-start" as const,
},
sizeFactorContainer: {
marginLeft: 8,
},
...modalBasic,
});
const AddCluster = ({
open,
closeModalAndRefresh,
classes,
}: IAddClusterProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [clusterName, setClusterName] = useState<string>("");
const [imageName, setImageName] = useState<string>("");
const [serviceName, setServiceName] = useState<string>("");
const [zones, setZones] = useState<IZone[]>([]);
const [volumesPerServer, setVolumesPerServer] = useState<number>(0);
const [volumeConfiguration, setVolumeConfiguration] = useState<
IVolumeConfiguration
>({ size: "", storage_class: "" });
const [mountPath, setMountPath] = useState<string>("");
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [enableMCS, setEnableMCS] = useState<boolean>(false);
const [enableSSL, setEnableSSL] = useState<boolean>(false);
const [sizeFactor, setSizeFactor] = useState<string>("GiB");
useEffect(() => {
if (addSending) {
api
.invoke("POST", `/api/v1/clusters`, {
name: clusterName,
})
.then(() => {
setAddSending(false);
setAddError("");
closeModalAndRefresh(true);
})
.catch((err) => {
setAddSending(false);
setAddError(err);
});
}
}, [addSending]);
const setVolumeConfig = (item: string, value: string) => {
const volumeCopy: IVolumeConfiguration = {
size: item !== "size" ? volumeConfiguration.size : value,
storage_class:
item !== "storage_class" ? volumeConfiguration.storage_class : value,
};
setVolumeConfiguration(volumeCopy);
};
return (
<ModalWrapper
title="Create Cluster"
modalOpen={open}
onClose={() => {
setAddError("");
closeModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
}}
>
<Grid container>
<Grid item xs={12} className={classes.formScrollable}>
{addError !== "" && (
<Grid item xs={12}>
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{addError}
</Typography>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
id="cluster-name"
name="cluster-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setClusterName(e.target.value);
}}
label="Cluster Name"
value={clusterName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
}}
label="Image"
value={imageName}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="service_name"
name="service_name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setServiceName(e.target.value);
}}
label="Service Name"
value={serviceName}
/>
</Grid>
<Grid item xs={12}>
<div>
<ZonesMultiSelector
label="Zones"
name="zones_selector"
onChange={() => {}}
elements={zones}
/>
</div>
</Grid>
<Grid item xs={12}>
<Typography component="h3">Volume Configuration</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="volumes_per_server"
name="volumes_per_server"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerServer(parseInt(e.target.value));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12}>
<div className={classes.multiContainer}>
<div>
<InputBoxWrapper
id="volume_size"
name="volume_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeConfig("size", e.target.value);
}}
label="Size"
value={volumeConfiguration.size}
/>
</div>
<div className={classes.sizeFactorContainer}>
<SelectWrapper
label=""
id="size_factor"
name="size_factor"
value={sizeFactor}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={factorForDropdown()}
/>
</div>
</div>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setVolumeConfig("storage_class", e.target.value as string);
}}
label="Storage Class"
value={volumeConfiguration.storage_class}
options={storageClasses}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="mount_path"
name="mount_path"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setMountPath(e.target.value);
}}
label="Mount Path"
value={mountPath}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="access_key"
name="access_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(e.target.value);
}}
label="Access Key"
value={accessKey}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="secret_key"
name="secret_key"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(e.target.value);
}}
label="Secret Key"
value={secretKey}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="enabled_mcs"
id="enabled_mcs"
name="enabled_mcs"
checked={enableMCS}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableMCS(checked);
}}
label={"Enable mcs"}
/>
</Grid>
<Grid item xs={12}>
<CheckboxWrapper
value="enable_ssl"
id="enable_ssl"
name="enable_ssl"
checked={enableSSL}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setEnableSSL(checked);
}}
label={"Enable SSL"}
/>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</Grid>
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddCluster);

View File

@@ -0,0 +1,120 @@
// 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, { useState, useEffect } from "react";
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
LinearProgress,
} from "@material-ui/core";
import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
interface IDeleteCluster {
classes: any;
deleteOpen: boolean;
selectedCluster: string;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
const styles = (theme: Theme) =>
createStyles({
errorBlock: {
color: "red",
},
});
const DeleteCluster = ({
classes,
deleteOpen,
selectedCluster,
closeDeleteModalAndRefresh,
}: IDeleteCluster) => {
const [deleteLoading, setDeleteLoading] = useState(false);
const [deleteError, setDeleteError] = useState("");
useEffect(() => {
api
.invoke("DELETE", `/api/v1/clusters/${selectedCluster}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}, [deleteLoading]);
const removeRecord = () => {
setDeleteLoading(true);
};
return (
<Dialog
open={deleteOpen}
onClose={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Cluster</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete cluster <b>{selectedCluster}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
<Typography
component="p"
variant="body1"
className={classes.errorBlock}
>
{deleteError}
</Typography>
</React.Fragment>
)}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => {
setDeleteError("");
closeDeleteModalAndRefresh(false);
}}
color="primary"
disabled={deleteLoading}
>
Cancel
</Button>
<Button onClick={removeRecord} color="secondary" autoFocus>
Delete
</Button>
</DialogActions>
</Dialog>
);
};
export default withStyles(styles)(DeleteCluster);

View File

@@ -0,0 +1,231 @@
// 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, { useState } from "react";
import Grid from "@material-ui/core/Grid";
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 } from "@material-ui/core";
import { CreateIcon } from "../../../../icons";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import { MinTablePaginationActions } from "../../../../common/MinTablePaginationActions";
import AddCluster from "./AddCluster";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import DeleteCluster from "./DeleteCluster";
import { Link } from "react-router-dom";
interface IClustersList {
classes: any;
}
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",
},
});
const ListClusters = ({ classes }: IClustersList) => {
const [createClusterOpen, setCreateClusterOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedCluster, setSelectedCluster] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterClusters, setFilterClusters] = useState<string>("");
const [records, setRecords] = useState<any[]>([]);
const [offset, setOffset] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(0);
const [page, setPage] = useState<number>(0);
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCreateClusterOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
if (reloadData) {
setIsLoading(true);
}
};
const confirmDeleteCluster = (cluster: string) => {
setSelectedCluster(cluster);
setDeleteOpen(true);
};
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 tableActions = [
{ type: "view", to: `/clusters`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteCluster, sendOnlyId: true },
];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: any) => {
if (filterClusters === "") {
return true;
} else {
if (b.name.indexOf(filterClusters) >= 0) {
return true;
} else {
return false;
}
}
});
return (
<React.Fragment>
{createClusterOpen && (
<AddCluster
open={createClusterOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
<DeleteCluster
deleteOpen={deleteOpen}
selectedCluster={selectedCluster}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Clusters</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Clusters"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterClusters(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<Button
variant="contained"
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateClusterOpen(true);
}}
>
Create Cluster
</Button>
</Grid>
<Grid item xs={12}>
REMOVE THIS:: <Link to={"/clusters/demoCluster"}>Test</Link>
<br />
</Grid>
<Grid item xs={12}>
<TableWrapper
itemActions={tableActions}
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zones_counter" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Clusters"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
colSpan: 3,
count: filteredRecords.length,
rowsPerPage: rowsPerPage,
page: page,
SelectProps: {
inputProps: { "aria-label": "rows per page" },
native: true,
},
onChangePage: handleChangePage,
onChangeRowsPerPage: handleChangeRowsPerPage,
ActionsComponent: MinTablePaginationActions,
}}
/>
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ListClusters);

View File

@@ -0,0 +1,183 @@
// 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, { useState, useEffect, createRef, ChangeEvent } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Grid from "@material-ui/core/Grid";
import get from "lodash/get";
import { InputLabel, Tooltip } from "@material-ui/core";
import HelpIcon from "@material-ui/icons/Help";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import {
fieldBasic,
tooltipHelper,
} from "../../Common/FormComponents/common/styleLibrary";
import { IZone } from "./types";
interface IZonesMultiSelector {
elements: IZone[];
name: string;
label: string;
tooltip?: string;
classes: any;
onChange: (elements: IZone[]) => void;
}
const gridBasic = {
display: "grid",
gridTemplateColumns: "calc(50% - 4px) calc(50% - 4px)",
gridGap: 8,
};
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
inputLabel: {
...fieldBasic.inputLabel,
width: 116,
},
inputContainer: {
height: 150,
overflowY: "auto",
padding: 15,
position: "relative",
border: "1px solid #c4c4c4",
},
labelContainer: {
display: "flex",
},
inputGrid: {
...gridBasic,
},
inputTitles: {
...gridBasic,
marginBottom: 5,
},
});
const ZonesMultiSelector = ({
elements,
name,
label,
tooltip = "",
onChange,
classes,
}: IZonesMultiSelector) => {
const defaultZone: IZone = { name: "", servers: 0 };
const [currentElements, setCurrentElements] = useState<IZone[]>([
{ ...defaultZone },
]);
const bottomList = createRef<HTMLDivElement>();
// Use effect to send new values to onChange
useEffect(() => {
onChange(currentElements);
}, [currentElements]);
// If the last input is not empty, we add a new one
const addEmptyRow = (elementsUp: IZone[]) => {
const lastElement = elementsUp[elementsUp.length - 1];
if (lastElement.servers !== 0 && lastElement.name !== "") {
elementsUp.push({ ...defaultZone });
const refScroll = bottomList.current;
if (refScroll) {
refScroll.scrollIntoView(false);
}
}
return elementsUp;
};
// Onchange function for input box, we get the dataset-index & only update that value in the array
const onChangeElement = (e: ChangeEvent<HTMLInputElement>, field: string) => {
e.persist();
let updatedElement = [...currentElements];
const index = get(e.target, "dataset.index", 0);
const rowPosition: IZone = updatedElement[index];
rowPosition.servers =
field === "servers" ? parseInt(e.target.value) : rowPosition.servers;
rowPosition.name = field === "name" ? e.target.value : rowPosition.name;
updatedElement[index] = rowPosition;
updatedElement = addEmptyRow(updatedElement);
setCurrentElements(updatedElement);
};
const inputs = currentElements.map((element, index) => {
return (
<React.Fragment key={`zone-${name}-${index.toString()}`}>
<div>
<InputBoxWrapper
id={`${name}-${index.toString()}-name`}
label={""}
name={`${name}-${index.toString()}-name`}
value={currentElements[index].name}
onChange={(e) => onChangeElement(e, "name")}
index={index}
key={`Zones-${name}-${index.toString()}-name`}
/>
</div>
<div>
<InputBoxWrapper
type="number"
id={`${name}-${index.toString()}-servers`}
label={""}
name={`${name}-${index.toString()}-servers`}
value={currentElements[index].servers.toString(10)}
onChange={(e) => onChangeElement(e, "servers")}
index={index}
key={`Zones-${name}-${index.toString()}-servers`}
/>
</div>
</React.Fragment>
);
});
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
<Grid item xs={12}>
<div className={classes.inputTitles}>
<div>Name</div>
<div>Servers</div>
</div>
<div className={classes.inputContainer}>
<div className={classes.inputGrid}>{inputs}</div>
</div>
<div ref={bottomList} />
</Grid>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(ZonesMultiSelector);

View File

@@ -0,0 +1,25 @@
// 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 IZone {
name: string;
servers: number;
}
export interface IVolumeConfiguration {
size: string;
storage_class: string;
}

View File

@@ -0,0 +1,20 @@
// 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 const storageClasses = [
{ label: "Standard", value: "STANDARD" },
{ label: "Reduced Redundancy", value: "REDUCED_REDUNDANCY" },
];

View File

@@ -0,0 +1,107 @@
// 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 {
Checkbox,
Grid,
InputLabel,
TextField,
TextFieldProps,
Tooltip,
} from "@material-ui/core";
import { OutlinedInputProps } from "@material-ui/core/OutlinedInput";
import {
createStyles,
makeStyles,
Theme,
withStyles,
} from "@material-ui/core/styles";
import {
checkboxIcons,
fieldBasic,
tooltipHelper,
} from "../common/styleLibrary";
import HelpIcon from "@material-ui/icons/Help";
interface CheckBoxProps {
label: string;
classes: any;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
value: string | boolean;
id: string;
name: string;
disabled?: boolean;
tooltip?: string;
index?: number;
checked: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...fieldBasic,
...tooltipHelper,
...checkboxIcons,
labelContainer: {
flexGrow: 1,
},
});
const CheckboxWrapper = ({
label,
onChange,
value,
id,
name,
checked = false,
disabled = false,
tooltip = "",
classes,
}: CheckBoxProps) => {
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<div className={classes.labelContainer}>
<Checkbox
name={name}
id={id}
value={value}
color="primary"
inputProps={{ "aria-label": "secondary checkbox" }}
checked={checked}
onChange={onChange}
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
disabled={disabled}
/>
</div>
</Grid>
</React.Fragment>
);
};
export default withStyles(styles)(CheckboxWrapper);

View File

@@ -92,16 +92,18 @@ const SelectWrapper = ({
return (
<React.Fragment>
<Grid item xs={12} className={classes.fieldContainer}>
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>
{tooltip !== "" && (
<div className={classes.tooltipContainer}>
<Tooltip title={tooltip} placement="top-start">
<HelpIcon className={classes.tooltip} />
</Tooltip>
</div>
)}
</InputLabel>
)}
<FormControl variant="outlined" fullWidth>
<Select
id={id}

View File

@@ -61,3 +61,18 @@ export const tooltipHelper = {
fontSize: 18,
},
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
export const checkboxIcons = {
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
};

View File

@@ -19,6 +19,7 @@ import { IconButton } from "@material-ui/core";
import ViewIcon from "./TableActionIcons/ViewIcon";
import PencilIcon from "./TableActionIcons/PencilIcon";
import DeleteIcon from "./TableActionIcons/DeleteIcon";
import DescriptionIcon from "./TableActionIcons/DescriptionIcon";
import { Link } from "react-router-dom";
interface IActionButton {
@@ -39,6 +40,8 @@ const defineIcon = (type: string, selected: boolean) => {
return <PencilIcon active={selected} />;
case "delete":
return <DeleteIcon active={selected} />;
case "description":
return <DescriptionIcon active={selected} />;
}
return null;
@@ -51,13 +54,14 @@ const TableActionButton = ({
idField,
selected,
to,
sendOnlyId = false
sendOnlyId = false,
}: IActionButton) => {
const valueClick = sendOnlyId ? valueToSend[idField] : valueToSend;
const buttonElement = (
<IconButton
aria-label={type}
size={"small"}
onClick={
onClick
? () => {

View File

@@ -0,0 +1,20 @@
import React from "react";
import { IIcon, selected, unSelected } from "./common";
const PencilIcon = ({ active = false }: IIcon) => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
>
<path
fill={active ? selected : unSelected}
d="M14 2H6c-1.1 0-1.99.9-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"
></path>
</svg>
);
};
export default PencilIcon;

View File

@@ -27,10 +27,12 @@ import {
Paper,
Grid,
Checkbox,
Typography,
} from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { TablePaginationActionsProps } from "@material-ui/core/TablePagination/TablePaginationActions";
import TableActionButton from "./TableActionButton";
import { checkboxIcons } from "../FormComponents/common/styleLibrary";
//Interfaces for table Items
@@ -87,12 +89,6 @@ const rowText = {
borderColor: borderColor,
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
@@ -103,6 +99,7 @@ const styles = (theme: Theme) =>
overflow: "auto",
flexDirection: "column",
padding: "19px 38px",
minHeight: "200px",
},
minTableHeader: {
color: "#393939",
@@ -133,21 +130,20 @@ const styles = (theme: Theme) =>
},
},
actionsContainer: {
width: 120,
width: 150,
borderColor: borderColor,
},
paginatorComponent: {
borderBottom: 0,
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
checkBoxRow: {
borderColor: borderColor,
},
loadingBox: {
paddingTop: "100px",
paddingBottom: "100px",
},
...checkboxIcons,
});
// Function that renders Title Columns
@@ -226,7 +222,16 @@ const TableWrapper = ({
return (
<Grid item xs={12}>
<Paper className={classes.paper}>
{isLoading && <LinearProgress />}
{isLoading && (
<Grid container className={classes.loadingBox}>
<Grid item xs={12} style={{ textAlign: "center" }}>
<Typography component="h3">Loading...</Typography>
</Grid>
<Grid item xs={12}>
<LinearProgress />
</Grid>
</Grid>
)}
{records && records.length > 0 ? (
<Table size="small" stickyHeader={stickyHeader}>
<TableHead className={classes.minTableHeader}>
@@ -298,7 +303,9 @@ const TableWrapper = ({
</TableBody>
</Table>
) : (
<div>{`There are no ${entityName} yet.`}</div>
<React.Fragment>
{!isLoading && <div>{`There are no ${entityName} yet.`}</div>}
</React.Fragment>
)}
</Paper>
{paginatorConfig && (

View File

@@ -63,8 +63,10 @@ import WebhookPanel from "./Configurations/ConfigurationPanels/WebhookPanel";
import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Watch from "./Watch/Watch";
import ListClusters from "./Clusters/ListClusters/ListClusters";
import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import ClusterDetails from "./Clusters/ClusterDetails/ClusterDetails";
function Copyright() {
return (
@@ -293,6 +295,14 @@ const Console = ({
component: WebhookPanel,
path: "/webhook/audit",
},
{
component: ListClusters,
path: "/clusters",
},
{
component: ClusterDetails,
path: "/clusters/:clusterName",
},
];
const allowedRoutes = routes.filter((route: any) => allowedPages[route.path]);

View File

@@ -30,6 +30,7 @@ import { stringSort } from "../../../utils/sortFunctions";
import AddGroup from "../Groups/AddGroup";
import DeleteGroup from "./DeleteGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import SetPolicy from "../Policies/SetPolicy";
interface IGroupsProps {
classes: any;
@@ -96,6 +97,7 @@ const Groups = ({ classes }: IGroupsProps) => {
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const [filter, setFilter] = useState<string>("");
const [policyOpen, setPolicyOpen] = useState<boolean>(false);
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
@@ -176,8 +178,14 @@ const Groups = ({ classes }: IGroupsProps) => {
setSelectedGroup(group);
};
const setPolicyAction = (selectionElement: any): void => {
setPolicyOpen(true);
setSelectedGroup(selectionElement);
};
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
@@ -197,6 +205,16 @@ const Groups = ({ classes }: IGroupsProps) => {
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={policyOpen}
selectedGroup={selectedGroup}
selectedUser={null}
closeModalAndRefresh={() => {
setPolicyOpen(false);
}}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Groups</Typography>

View File

@@ -20,6 +20,7 @@ import ListItemIcon from "@material-ui/core/ListItemIcon";
import RoomServiceIcon from "@material-ui/icons/RoomService";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import CenterFocusWeakIcon from "@material-ui/icons/CenterFocusWeak";
import StorageIcon from "@material-ui/icons/Storage";
import ListItemText from "@material-ui/core/ListItemText";
import { NavLink } from "react-router-dom";
import { Divider, Typography, withStyles } from "@material-ui/core";
@@ -222,6 +223,20 @@ class Menu extends React.Component<MenuProps> {
name: "Configurations List",
icon: <ListAltIcon />,
},
{
type: "title",
name: "Operator",
component: Typography,
},
{
group: "Operator",
type: "item",
component: NavLink,
to: "/clusters",
name: "Clusters",
icon: <StorageIcon />,
forceDisplay: true,
},
{
type: "divider",
key: "divider-2",

View File

@@ -0,0 +1,190 @@
// 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, { useCallback, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import {
Button,
LinearProgress,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import { modalBasic } from "../Common/FormComponents/common/styleLibrary";
import { User } from "../Users/types";
import ModalWrapper from "../Common/ModalWrapper/ModalWrapper";
import { Policy, PolicyList } from "./types";
import api from "../../../common/api";
import { policySort } from "../../../utils/sortFunctions";
import { Group } from "../Groups/types";
interface ISetPolicyProps {
classes: any;
closeModalAndRefresh: () => void;
selectedUser: User | null;
selectedGroup: string | null;
open: boolean;
}
const styles = (theme: Theme) =>
createStyles({
...modalBasic,
buttonContainer: {
textAlign: "right",
},
});
const SetPolicy = ({
classes,
closeModalAndRefresh,
selectedUser,
selectedGroup,
open,
}: ISetPolicyProps) => {
//Local States
const [records, setRecords] = useState<Policy[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>("");
const fetchRecords = () => {
setLoading(true);
api
.invoke("GET", `/api/v1/policies?limit=1000`)
.then((res: PolicyList) => {
const policies = res.policies === null ? [] : res.policies;
setLoading(false);
setRecords(policies.sort(policySort));
setError("");
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
const setPolicyAction = (policyName: string) => {
let entity = "user";
let value = null;
if (selectedGroup !== null) {
entity = "group";
value = selectedGroup;
} else {
if (selectedUser !== null) {
value = selectedUser.accessKey;
}
}
setLoading(true);
api
.invoke("PUT", `/api/v1/set-policy/${policyName}`, {
entityName: value,
entityType: entity,
})
.then((res: any) => {
setLoading(false);
setError("");
closeModalAndRefresh();
})
.catch((err) => {
setLoading(false);
setError(err);
});
};
useEffect(() => {
if (open) {
fetchRecords();
}
}, [open]);
return (
<ModalWrapper
onClose={() => {
closeModalAndRefresh();
}}
modalOpen={open}
title={
selectedUser !== null ? "Set Policy to User" : "Set Policy to Group"
}
>
<Grid container className={classes.formScrollable}>
<Grid item xs={12}>
<TableContainer component={Paper}>
<Table
className={classes.table}
size="small"
aria-label="a dense table"
>
<TableHead>
<TableRow>
<TableCell>Policy</TableCell>
<TableCell align="right"></TableCell>
</TableRow>
</TableHead>
<TableBody>
{records.map((row) => (
<TableRow key={row.name}>
<TableCell component="th" scope="row">
{row.name}
</TableCell>
<TableCell align="right">
<Button
variant="contained"
color="primary"
size={"small"}
onClick={() => {
setPolicyAction(row.name);
}}
>
Set
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Grid>
</Grid>
<Grid item xs={12} className={classes.buttonContainer}>
<Button
type="submit"
variant="contained"
color="primary"
onClick={() => {
closeModalAndRefresh();
}}
>
Cancel
</Button>
</Grid>
{loading && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</ModalWrapper>
);
};
export default withStyles(styles)(SetPolicy);

View File

@@ -34,6 +34,8 @@ import AddUser from "./AddUser";
import DeleteUser from "./DeleteUser";
import AddToGroup from "./AddToGroup";
import TableWrapper from "../Common/TableWrapper/TableWrapper";
import DescriptionIcon from "@material-ui/icons/Description";
import SetPolicy from "../Policies/SetPolicy";
const styles = (theme: Theme) =>
createStyles({
@@ -102,6 +104,7 @@ interface IUsersState {
addGroupOpen: boolean;
filter: string;
checkedUsers: string[];
setPolicyOpen: boolean;
}
class Users extends React.Component<IUsersProps, IUsersState> {
@@ -119,6 +122,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
addGroupOpen: false,
filter: "",
checkedUsers: [],
setPolicyOpen: false,
};
fetchRecords() {
@@ -192,6 +196,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
filter,
checkedUsers,
addGroupOpen,
setPolicyOpen,
} = this.state;
const handleChangePage = (event: unknown, newPage: number) => {
@@ -243,6 +248,13 @@ class Users extends React.Component<IUsersProps, IUsersState> {
});
};
const setPolicyAction = (selectionElement: any): void => {
this.setState({
setPolicyOpen: true,
selectedUser: selectionElement,
});
};
const deleteAction = (selectionElement: any): void => {
this.setState({
deleteOpen: true,
@@ -252,6 +264,7 @@ class Users extends React.Component<IUsersProps, IUsersState> {
const tableActions = [
{ type: "view", onClick: viewAction },
{ type: "description", onClick: setPolicyAction },
{ type: "delete", onClick: deleteAction },
];
@@ -266,6 +279,16 @@ class Users extends React.Component<IUsersProps, IUsersState> {
}}
/>
)}
{setPolicyOpen && (
<SetPolicy
open={setPolicyOpen}
selectedUser={selectedUser}
selectedGroup={null}
closeModalAndRefresh={() => {
this.setState({ setPolicyOpen: false });
}}
/>
)}
{deleteOpen && (
<DeleteUser
deleteOpen={deleteOpen}

View File

@@ -18,6 +18,10 @@ interface userInterface {
accessKey: string;
}
interface policyInterface {
name: string;
}
export const usersSort = (a: userInterface, b: userInterface) => {
if (a.accessKey > b.accessKey) {
return 1;
@@ -29,6 +33,17 @@ export const usersSort = (a: userInterface, b: userInterface) => {
return 0;
};
export const policySort = (a: policyInterface, b: policyInterface) => {
if (a.name > b.name) {
return 1;
}
if (a.name < b.name) {
return -1;
}
// a must be equal to b
return 0;
};
export const stringSort = (a: string, b: string) => {
if (a > b) {
return 1;