Added Clusters mockups (#133)

This commit is contained in:
Alex
2020-05-21 20:03:36 -05:00
committed by GitHub
parent b89b2d0c6a
commit 13ef83cee4
19 changed files with 1992 additions and 128 deletions

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,11 @@ 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 };
});
};

View File

@@ -0,0 +1,157 @@
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 } 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,
},
...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>("");
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.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)(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

@@ -32,6 +32,7 @@ import {
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
@@ -88,12 +89,6 @@ const rowText = {
borderColor: borderColor,
};
const checkBoxBasic = {
width: 16,
height: 16,
borderRadius: 3,
};
const styles = (theme: Theme) =>
createStyles({
dialogContainer: {
@@ -141,12 +136,6 @@ const styles = (theme: Theme) =>
paginatorComponent: {
borderBottom: 0,
},
unCheckedIcon: { ...checkBoxBasic, border: "1px solid #d0d0d0" },
checkedIcon: {
...checkBoxBasic,
border: "1px solid #201763",
backgroundColor: "#201763",
},
checkBoxRow: {
borderColor: borderColor,
},
@@ -154,6 +143,7 @@ const styles = (theme: Theme) =>
paddingTop: "100px",
paddingBottom: "100px",
},
...checkboxIcons,
});
// Function that renders Title Columns

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

@@ -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",