Connect List,Add Tenants (#148)

This commit is contained in:
Daniel Valdivia
2020-06-02 09:52:37 -07:00
committed by GitHub
parent 0fa1d4bf7c
commit 8af3665ae2
15 changed files with 305 additions and 185 deletions

View File

@@ -35,8 +35,8 @@ var (
buckets = "/buckets"
bucketsDetail = "/buckets/:bucketName"
serviceAccounts = "/service-accounts"
clusters = "/clusters"
clustersDetail = "/clusters/:clusterName"
tenants = "/tenants"
tenantsDetail = "/tenants/:tenantName"
heal = "/heal"
)
@@ -192,8 +192,8 @@ var serviceAccountsActionSet = ConfigurationActionSet{
actions: iampolicy.NewActionSet(),
}
// clustersActionSet temporally no actions needed for clusters sections to work
var clustersActionSet = ConfigurationActionSet{
// tenantsActionSet temporally no actions needed for tenants sections to work
var tenantsActionSet = ConfigurationActionSet{
actionTypes: iampolicy.NewActionSet(),
actions: iampolicy.NewActionSet(),
}
@@ -228,8 +228,8 @@ var endpointRules = map[string]ConfigurationActionSet{
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode
var operatorRules = map[string]ConfigurationActionSet{
clusters: clustersActionSet,
clustersDetail: clustersActionSet,
tenants: tenantsActionSet,
tenantsDetail: tenantsActionSet,
}
// operatorOnly ENV variable

File diff suppressed because one or more lines are too long

View File

@@ -25,6 +25,7 @@ export const units = [
"ZiB",
"YiB",
];
export const k8sUnits = ["Ki", "Mi", "Gi", "Ti", "Pi", "Ei"];
export const niceBytes = (x: string) => {
let l = 0,
n = parseInt(x, 10) || 0;
@@ -65,6 +66,13 @@ export const factorForDropdown = () => {
});
};
// units to be used in a dropdown
export const k8sfactorForDropdown = () => {
return k8sUnits.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);

View File

@@ -64,10 +64,10 @@ import Trace from "./Trace/Trace";
import Logs from "./Logs/Logs";
import Heal from "./Heal/Heal";
import Watch from "./Watch/Watch";
import ListClusters from "./Clusters/ListClusters/ListClusters";
import ListTenants from "./Tenants/ListTenants/ListTenants";
import { ISessionResponse } from "./types";
import { saveSessionResponse } from "./actions";
import ClusterDetails from "./Clusters/ClusterDetails/ClusterDetails";
import TenantDetails from "./Tenants/TenantDetails/TenantDetails";
function Copyright() {
return (
@@ -301,11 +301,11 @@ const Console = ({
path: "/webhook/audit",
},
{
component: ListClusters,
path: "/clusters",
component: ListTenants,
path: "/tenants",
},
{
component: ClusterDetails,
component: TenantDetails,
path: "/clusters/:clusterName",
},
];

View File

@@ -246,8 +246,8 @@ class Menu extends React.Component<MenuProps> {
group: "Operator",
type: "item",
component: NavLink,
to: "/clusters",
name: "Clusters",
to: "/tenants",
name: "Tenants",
icon: <StorageIcon />,
},
{

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState, useEffect } from "react";
import React, { useEffect, useState } from "react";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
@@ -26,11 +26,11 @@ 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 { k8sfactorForDropdown } from "../../../../common/utils";
import ZonesMultiSelector from "./ZonesMultiSelector";
import { storageClasses } from "../utils";
interface IAddClusterProps {
interface IAddTenantProps {
open: boolean;
closeModalAndRefresh: (reloadData: boolean) => any;
classes: any;
@@ -55,14 +55,14 @@ const styles = (theme: Theme) =>
...modalBasic,
});
const AddCluster = ({
const AddTenant = ({
open,
closeModalAndRefresh,
classes,
}: IAddClusterProps) => {
}: IAddTenantProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [addError, setAddError] = useState<string>("");
const [clusterName, setClusterName] = useState<string>("");
const [tenantName, setTenantName] = useState<string>("");
const [imageName, setImageName] = useState<string>("");
const [serviceName, setServiceName] = useState<string>("");
const [zones, setZones] = useState<IZone[]>([]);
@@ -75,13 +75,30 @@ const AddCluster = ({
const [secretKey, setSecretKey] = useState<string>("");
const [enableMCS, setEnableMCS] = useState<boolean>(false);
const [enableSSL, setEnableSSL] = useState<boolean>(false);
const [sizeFactor, setSizeFactor] = useState<string>("GiB");
const [sizeFactor, setSizeFactor] = useState<string>("Gi");
useEffect(() => {
if (addSending) {
let cleanZones: IZone[] = [];
for (let zone of zones) {
if (zone.name !== "") {
cleanZones.push(zone);
}
}
api
.invoke("POST", `/api/v1/clusters`, {
name: clusterName,
.invoke("POST", `/api/v1/tenants`, {
name: tenantName,
service_name: tenantName,
enable_ssl: enableSSL,
enable_mcs: enableMCS,
access_key: accessKey,
secret_key: secretKey,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: `${volumeConfiguration.size}${sizeFactor}`,
},
zones: cleanZones,
})
.then(() => {
setAddSending(false);
@@ -107,7 +124,7 @@ const AddCluster = ({
return (
<ModalWrapper
title="Create Cluster"
title="Create Tenant"
modalOpen={open}
onClose={() => {
setAddError("");
@@ -139,13 +156,13 @@ const AddCluster = ({
)}
<Grid item xs={12}>
<InputBoxWrapper
id="cluster-name"
name="cluster-name"
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setClusterName(e.target.value);
setTenantName(e.target.value);
}}
label="Cluster Name"
value={clusterName}
label="Tenant Name"
value={tenantName}
/>
</Grid>
<Grid item xs={12}>
@@ -155,7 +172,7 @@ const AddCluster = ({
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setImageName(e.target.value);
}}
label="Image"
label="MinIO Image"
value={imageName}
/>
</Grid>
@@ -175,7 +192,9 @@ const AddCluster = ({
<ZonesMultiSelector
label="Zones"
name="zones_selector"
onChange={() => {}}
onChange={(elements: IZone[]) => {
setZones(elements);
}}
elements={zones}
/>
</div>
@@ -220,7 +239,7 @@ const AddCluster = ({
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
setSizeFactor(e.target.value as string);
}}
options={factorForDropdown()}
options={k8sfactorForDropdown()}
/>
</div>
</div>
@@ -322,4 +341,4 @@ const AddCluster = ({
);
};
export default withStyles(styles)(AddCluster);
export default withStyles(styles)(AddTenant);

View File

@@ -28,10 +28,10 @@ import Typography from "@material-ui/core/Typography";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import api from "../../../../common/api";
interface IDeleteCluster {
interface IDeleteTenant {
classes: any;
deleteOpen: boolean;
selectedCluster: string;
selectedTenant: string;
closeDeleteModalAndRefresh: (refreshList: boolean) => any;
}
@@ -42,27 +42,29 @@ const styles = (theme: Theme) =>
},
});
const DeleteCluster = ({
const DeleteTenant = ({
classes,
deleteOpen,
selectedCluster,
selectedTenant,
closeDeleteModalAndRefresh,
}: IDeleteCluster) => {
}: IDeleteTenant) => {
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);
});
if (deleteLoading) {
api
.invoke("DELETE", `/api/v1/clusters/${selectedTenant}`)
.then(() => {
setDeleteLoading(false);
setDeleteError("");
closeDeleteModalAndRefresh(true);
})
.catch((err) => {
setDeleteLoading(false);
setDeleteError(err);
});
}
}, [deleteLoading]);
const removeRecord = () => {
@@ -79,11 +81,11 @@ const DeleteCluster = ({
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
>
<DialogTitle id="alert-dialog-title">Delete Cluster</DialogTitle>
<DialogTitle id="alert-dialog-title">Delete Tenant</DialogTitle>
<DialogContent>
{deleteLoading && <LinearProgress />}
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete cluster <b>{selectedCluster}</b>?
Are you sure you want to delete tenant <b>{selectedTenant}</b>?
{deleteError !== "" && (
<React.Fragment>
<br />
@@ -117,4 +119,4 @@ const DeleteCluster = ({
);
};
export default withStyles(styles)(DeleteCluster);
export default withStyles(styles)(DeleteTenant);

View File

@@ -14,7 +14,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import Grid from "@material-ui/core/Grid";
import Typography from "@material-ui/core/Typography";
import TextField from "@material-ui/core/TextField";
@@ -24,12 +24,14 @@ 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";
import api from "../../../../common/api";
import { ITenant, ITenantsResponse } from "./types";
import { niceBytes } from "../../../../common/utils";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "./AddTenant";
interface IClustersList {
interface ITenantsList {
classes: any;
}
@@ -77,19 +79,20 @@ const styles = (theme: Theme) =>
},
});
const ListClusters = ({ classes }: IClustersList) => {
const [createClusterOpen, setCreateClusterOpen] = useState<boolean>(false);
const ListTenants = ({ classes }: ITenantsList) => {
const [createTenantOpen, setCreateTenantOpen] = useState<boolean>(false);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedCluster, setSelectedCluster] = useState<any>(null);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const [filterClusters, setFilterClusters] = useState<string>("");
const [filterTenants, setFilterTenants] = useState<string>("");
const [records, setRecords] = useState<any[]>([]);
const [offset, setOffset] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(0);
const [rowsPerPage, setRowsPerPage] = useState<number>(10);
const [page, setPage] = useState<number>(0);
const [error, setError] = useState<string>("");
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCreateClusterOpen(false);
setCreateTenantOpen(false);
if (reloadData) {
setIsLoading(true);
@@ -104,8 +107,8 @@ const ListClusters = ({ classes }: IClustersList) => {
}
};
const confirmDeleteCluster = (cluster: string) => {
setSelectedCluster(cluster);
const confirmDeleteTenant = (tenant: string) => {
setSelectedTenant(tenant);
setDeleteOpen(true);
};
@@ -122,17 +125,17 @@ const ListClusters = ({ classes }: IClustersList) => {
};
const tableActions = [
{ type: "view", to: `/clusters`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteCluster, sendOnlyId: true },
{ type: "view", to: `/tenants`, sendOnlyId: true },
{ type: "delete", onClick: confirmDeleteTenant, sendOnlyId: true },
];
const filteredRecords = records
.slice(offset, offset + rowsPerPage)
.filter((b: any) => {
if (filterClusters === "") {
if (filterTenants === "") {
return true;
} else {
if (b.name.indexOf(filterClusters) >= 0) {
if (b.name.indexOf(filterTenants) >= 0) {
return true;
} else {
return false;
@@ -140,36 +143,84 @@ const ListClusters = ({ classes }: IClustersList) => {
}
});
useEffect(() => {
if (isLoading) {
const fetchRecords = () => {
const offset = page * rowsPerPage;
api
.invoke(
"GET",
`/api/v1/tenants?offset=${offset}&limit=${rowsPerPage}`
)
.then((res: ITenantsResponse) => {
if (res === null) {
setIsLoading(false);
return;
}
let resTenants: ITenant[] = [];
if (res.tenants !== null) {
resTenants = res.tenants;
}
for (let i = 0; i < resTenants.length; i++) {
const total =
resTenants[i].volume_count * resTenants[i].volume_size;
resTenants[i].capacity = niceBytes(total + "");
}
setRecords(resTenants);
setError("");
setIsLoading(false);
// if we get 0 results, and page > 0 , go down 1 page
if ((!res.tenants || res.tenants.length === 0) && page > 0) {
const newPage = page - 1;
setPage(newPage);
}
})
.catch((err) => {
setError(err);
setIsLoading(false);
});
};
fetchRecords();
}
}, [isLoading, page, rowsPerPage]);
useEffect(() => {
setIsLoading(true);
}, []);
return (
<React.Fragment>
{createClusterOpen && (
<AddCluster
open={createClusterOpen}
{createTenantOpen && (
<AddTenant
open={createTenantOpen}
closeModalAndRefresh={closeAddModalAndRefresh}
/>
)}
{deleteOpen && (
<DeleteCluster
<DeleteTenant
deleteOpen={deleteOpen}
selectedCluster={selectedCluster}
selectedTenant={selectedTenant}
closeDeleteModalAndRefresh={closeDeleteModalAndRefresh}
/>
)}
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">Clusters</Typography>
<Typography variant="h6">Tenants</Typography>
</Grid>
<Grid item xs={12}>
<br />
</Grid>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Clusters"
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterClusters(val.target.value);
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
@@ -185,10 +236,10 @@ const ListClusters = ({ classes }: IClustersList) => {
color="primary"
startIcon={<CreateIcon />}
onClick={() => {
setCreateClusterOpen(true);
setCreateTenantOpen(true);
}}
>
Create Cluster
Create Tenant
</Button>
</Grid>
<Grid item xs={12}>
@@ -200,11 +251,12 @@ const ListClusters = ({ classes }: IClustersList) => {
columns={[
{ label: "Name", elementKey: "name" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Zones", elementKey: "zones_counter" },
{ label: "# of Zones", elementKey: "zone_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Clusters"
entityName="Tenants"
idField="name"
paginatorConfig={{
rowsPerPageOptions: [5, 10, 25],
@@ -227,4 +279,4 @@ const ListClusters = ({ classes }: IClustersList) => {
);
};
export default withStyles(styles)(ListClusters);
export default withStyles(styles)(ListTenants);

View File

@@ -23,3 +23,19 @@ export interface IVolumeConfiguration {
size: string;
storage_class: string;
}
export interface ITenant {
name: string;
zone_count: number;
currentState: string;
instance_count: 4;
creation_date: Date;
volume_size: number;
volume_count: number;
// computed
capacity: string;
}
export interface ITenantsResponse {
tenants: ITenant[];
}

View File

@@ -31,7 +31,7 @@ import AddZoneModal from "./AddZoneModal";
import AddBucket from "../../Buckets/ListBuckets/AddBucket";
import ReplicationSetup from "./ReplicationSetup";
interface IClusterDetailsProps {
interface ITenantDetailsProps {
classes: any;
match: any;
}
@@ -101,7 +101,7 @@ const mainPagination = {
ActionsComponent: MinTablePaginationActions,
};
const ClusterDetails = ({ classes, match }: IClusterDetailsProps) => {
const TenantDetails = ({ classes, match }: ITenantDetailsProps) => {
const [selectedTab, setSelectedTab] = useState<number>(0);
const [capacity, setCapacity] = useState<number>(0);
const [externalIDP, setExternalIDP] = useState<boolean>(false);
@@ -156,7 +156,7 @@ const ClusterDetails = ({ classes, match }: IClusterDetailsProps) => {
<Grid container>
<Grid item xs={12}>
<Typography variant="h6">
Cluster > {match.params["clusterName"]}
Tenant > {match.params["clusterName"]}
</Typography>
</Grid>
<Grid item xs={12}>
@@ -233,7 +233,7 @@ const ClusterDetails = ({ classes, match }: IClusterDetailsProps) => {
onChange={(_, newValue: number) => {
setSelectedTab(newValue);
}}
aria-label="cluster-tabs"
aria-label="tenant-tabs"
>
<Tab label="Zones" />
<Tab label="Buckets" />
@@ -425,4 +425,4 @@ const ClusterDetails = ({ classes, match }: IClusterDetailsProps) => {
);
};
export default withStyles(styles)(ClusterDetails);
export default withStyles(styles)(TenantDetails);

View File

@@ -164,7 +164,7 @@ func FileServerMiddleware(next http.Handler) http.Handler {
switch {
case strings.HasPrefix(r.URL.Path, "/ws"):
serveWS(w, r)
case strings.HasPrefix(r.URL.Path, "/api/v1/clusters"):
case strings.HasPrefix(r.URL.Path, "/api/v1/tenants"):
client := &http.Client{}
serverMkube(client, w, r)
case strings.HasPrefix(r.URL.Path, "/api"):