Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a805a49662 | ||
|
|
296e4ff5ce | ||
|
|
20749d2eae | ||
|
|
ff4e959d11 | ||
|
|
37195fefa8 | ||
|
|
13ef83cee4 | ||
|
|
b89b2d0c6a | ||
|
|
edf687fd8a | ||
|
|
cb60eba373 |
@@ -2,6 +2,11 @@
|
||||
|
||||
A graphical user interface for [MinIO](https://github.com/minio/minio)
|
||||
|
||||
|
||||
| Dashboard | Adding A User |
|
||||
| ------------- | ------------- |
|
||||
|  |  |
|
||||
|
||||
## Setup
|
||||
|
||||
All `mcs` needs is a MinIO user with admin privileges and URL pointing to your MinIO deployment.
|
||||
|
||||
BIN
images/pic1.png
Normal file
BIN
images/pic1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 635 KiB |
BIN
images/pic2.png
Normal file
BIN
images/pic2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 688 KiB |
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
@@ -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"}
|
||||
<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"}
|
||||
<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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
25
portal-ui/src/screens/Console/Clusters/ListClusters/types.ts
Normal file
25
portal-ui/src/screens/Console/Clusters/ListClusters/types.ts
Normal 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;
|
||||
}
|
||||
20
portal-ui/src/screens/Console/Clusters/utils.ts
Normal file
20
portal-ui/src/screens/Console/Clusters/utils.ts
Normal 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" },
|
||||
];
|
||||
@@ -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);
|
||||
@@ -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}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
|
||||
? () => {
|
||||
|
||||
@@ -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;
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
190
portal-ui/src/screens/Console/Policies/SetPolicy.tsx
Normal file
190
portal-ui/src/screens/Console/Policies/SetPolicy.tsx
Normal 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);
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user