Add Tenant in non-linear way (#1027)

* Add Tenant in non-linear way

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2021-09-13 16:00:44 -07:00
committed by GitHub
parent 875647577a
commit 0fdf5ee0fc
23 changed files with 1282 additions and 864 deletions

View File

@@ -42,6 +42,8 @@ var (
serviceAccounts = "/account"
changePassword = "/account/change-password"
tenants = "/tenants"
tenantsAdd = "/tenants/add"
tenantsAddSub = "/tenants/add/*"
tenantsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName"
tenantHop = "/namespaces/:tenantNamespace/tenants/:tenantName/hop"
podsDetail = "/namespaces/:tenantNamespace/tenants/:tenantName/pods/:podName"
@@ -317,6 +319,8 @@ var endpointRules = map[string]ConfigurationActionSet{
// operatorRules contains the mapping between endpoints and ActionSets for operator only mode
var operatorRules = map[string]ConfigurationActionSet{
tenants: tenantsActionSet,
tenantsAdd: tenantsActionSet,
tenantsAddSub: tenantsActionSet,
tenantsDetail: tenantsActionSet,
tenantHop: tenantsActionSet,
tenantsDetailSummary: tenantsActionSet,

View File

@@ -19,8 +19,6 @@ package acl
import (
"reflect"
"testing"
iampolicy "github.com/minio/pkg/iam/policy"
)
type args struct {
@@ -111,80 +109,10 @@ func TestOperatorOnlyEndpoints(t *testing.T) {
tests := []endpoint{
{
name: "Operator Only - all admin endpoints",
args: args{
[]string{
"admin:*",
},
},
want: 15,
},
{
name: "Operator Only - all s3 endpoints",
args: args{
[]string{
"s3:*",
},
},
want: 15,
},
{
name: "Operator Only - all admin and s3 endpoints",
args: args{
[]string{
"admin:*",
"s3:*",
},
},
want: 15,
},
{
name: "Operator Only - default endpoints",
args: args{
[]string{},
},
want: 15,
args: args{},
want: 17,
},
}
validateEndpoints(t, tests)
}
func TestGetActionsStringFromPolicy(t *testing.T) {
type args struct {
policy *iampolicy.Policy
}
tests := []struct {
name string
args args
want int
}{
{
name: "parse ReadOnly policy",
args: args{
policy: &iampolicy.ReadOnly,
},
want: 2,
},
{
name: "parse WriteOnly policy",
args: args{
policy: &iampolicy.WriteOnly,
},
want: 1,
},
{
name: "parse AdminDiagnostics policy",
args: args{
policy: &iampolicy.AdminDiagnostics,
},
want: 8,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetActionsStringFromPolicy(tt.args.policy); !reflect.DeepEqual(len(got), tt.want) {
t.Errorf("GetActionsStringFromPolicy() = %v, want %v", len(got), tt.want)
}
})
}
}

View File

@@ -15,11 +15,6 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { SvgIcon } from "@material-ui/core";
interface IConsoleLogo {
width?: number;
}
const ConsoleLogo = () => {
return (

View File

@@ -27,9 +27,6 @@ const OperatorLogo = ({ width = 120 }: IOperatorLogo) => {
viewBox="0 0 606.583 134.691"
width={width}
>
<defs>
<style>{".prefix__cls-1{fill:#fff}"}</style>
</defs>
<g id="prefix__Layer_2" data-name="Layer 2">
<g id="prefix__Layer_1-2" data-name="Layer 1">
<path

View File

@@ -587,6 +587,9 @@ export const wizardCommon = {
},
},
},
paperWrapper: {
padding: 12,
},
};
export const buttonsStyles = {

View File

@@ -18,7 +18,9 @@ import React, { useState, Fragment } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { IWizardMain } from "./types";
import WizardPage from "./WizardPage";
import { Grid } from "@material-ui/core";
import { Grid, List } from "@material-ui/core";
import ListItem from "@material-ui/core/ListItem";
import ListItemText from "@material-ui/core/ListItemText";
const styles = (theme: Theme) =>
createStyles({
@@ -28,32 +30,10 @@ const styles = (theme: Theme) =>
height: "100%",
flexGrow: 1,
},
wizFromContainer: {
height: "calc(100vh - 270px)",
minHeight: 450,
padding: "0 30px",
},
wizFromContainer: {},
wizFromModal: {
position: "relative",
},
wizardSteps: {
minWidth: 180,
marginRight: 10,
borderRight: "#eaeaea 1px solid",
display: "flex",
flexGrow: 1,
flexDirection: "column",
height: "100%",
"& ul": {
padding: "0 15px 0 40px",
marginTop: 0,
"& li": {
listStyle: "lower-roman",
marginBottom: 12,
},
},
},
modalWizardSteps: {
padding: 5,
borderBottom: "#eaeaea 1px solid",
@@ -85,6 +65,7 @@ const styles = (theme: Theme) =>
},
},
paddedContentGrid: {
marginTop: 8,
padding: "0 10px",
},
stepsLabel: {
@@ -158,6 +139,26 @@ const GenericWizard = ({
}
const stepsList = () => {
return (
<Fragment>
<List component="nav" dense={true}>
{wizardSteps.map((step, index) => {
return (
<ListItem
button
onClick={() => pageChange(index)}
key={`wizard-${index.toString()}`}
selected={currentStep === index}
>
<ListItemText primary={step.label} />
</ListItem>
);
})}
</List>
</Fragment>
);
};
const stepsListModal = () => {
return (
<ul>
{wizardSteps.map((step, index) => {
@@ -186,16 +187,13 @@ const GenericWizard = ({
<Fragment>
<div className={classes.stepsMasterContainer}>
<div className={`${classes.stepsLabel} stepsModalTitle`}>Steps</div>
<div className={classes.modalWizardSteps}>{stepsList()}</div>
<div className={classes.modalWizardSteps}>{stepsListModal()}</div>
</div>
</Fragment>
) : (
<Fragment>
<Grid item xs={12} sm={3} md={3} lg={3} xl={2}>
<div className={classes.wizardSteps}>
<span className={classes.stepsLabel}>Steps</span>
{stepsList()}
</div>
<Grid item xs={12} sm={2} md={2} lg={2} xl={2}>
{stepsList()}
</Grid>
</Fragment>
)}
@@ -203,9 +201,9 @@ const GenericWizard = ({
<Grid
item
xs={12}
sm={forModal ? 12 : 9}
md={forModal ? 12 : 9}
lg={forModal ? 12 : 9}
sm={forModal ? 12 : 10}
md={forModal ? 12 : 10}
lg={forModal ? 12 : 10}
xl={forModal ? 12 : 10}
className={forModal ? "" : classes.paddedContentGrid}
>

View File

@@ -28,7 +28,7 @@ const styles = (theme: Theme) =>
wizardComponent: {
overflowY: "auto",
marginBottom: 10,
height: "calc(100vh - 342px)",
height: "calc(100vh - 100px - 80px)",
maxWidth: 840,
width: "100%",
},

View File

@@ -1,10 +1,9 @@
import React, { Fragment } from "react";
import React from "react";
import Grid from "@material-ui/core/Grid";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
import { setMenuOpen, userLoggedIn } from "../../../../actions";
import OperatorLogo from "../../../../icons/OperatorLogo";
import ConsoleLogo from "../../../../icons/ConsoleLogo";

View File

@@ -14,12 +14,10 @@
// 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, { Fragment, useState, useEffect } from "react";
import clsx from "clsx";
import React, { Fragment, useEffect, useState } from "react";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, LinearProgress } from "@material-ui/core";
import CssBaseline from "@material-ui/core/CssBaseline";
import Drawer from "@material-ui/core/Drawer";
import Container from "@material-ui/core/Container";
import Snackbar from "@material-ui/core/Snackbar";
import history from "../../history";
@@ -58,6 +56,7 @@ import Storage from "./Storage/Storage";
import Metrics from "./Dashboard/Metrics";
import Hop from "./Tenants/TenantDetails/hop/Hop";
import MainError from "./Common/MainError/MainError";
import AddTenant from "./Tenants/AddTenant/AddTenant";
const drawerWidth = 245;
@@ -299,6 +298,10 @@ const Console = ({
component: TenantsMain,
path: "/tenants",
},
{
component: AddTenant,
path: "/tenants/add",
},
{
component: Storage,
path: "/storage",

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, { Fragment, useState } from "react";
import React from "react";
import { connect } from "react-redux";
import { NavLink } from "react-router-dom";
import {
@@ -265,18 +265,6 @@ const styles = (theme: Theme) =>
},
});
// Menu State builder for groups
const menuStateBuilder = () => {
let elements: any = [];
menuGroups.forEach((menuItem) => {
if (menuItem.collapsible) {
elements[menuItem.group] = true;
}
});
return elements;
};
interface IMenuProps {
classes: any;
userLoggedIn: typeof userLoggedIn;
@@ -511,14 +499,6 @@ const Menu = ({
item.fsHidden !== false
);
const handleDrawerOpen = () => {
setMenuOpen(true);
};
const handleDrawerClose = () => {
setMenuOpen(false);
};
return (
<React.Fragment>
<Drawer

View File

@@ -40,24 +40,26 @@ import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt"
import NameTenant from "./Steps/NameTenant";
import { AppState } from "../../../../store";
import { ICertificatesItems, IFieldStore } from "../types";
import { updateAddField } from "../actions";
import { resetAddTenantForm, updateAddField } from "../actions";
import Configure from "./Steps/Configure";
import IdentityProvider from "./Steps/IdentityProvider";
import Security from "./Steps/Security";
import Encryption from "./Steps/Encryption";
import TenantSize from "./Steps/TenantSize";
import Preview from "./Steps/Preview";
import Affinity from "./Steps/Affinity";
import PageHeader from "../../Common/PageHeader/PageHeader";
import history from "../../../../history";
import Images from "./Steps/Images";
interface IAddTenantProps {
closeAndRefresh: (reloadData: boolean) => any;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
resetAddTenantForm: typeof resetAddTenantForm;
updateAddField: typeof updateAddField;
fields: IFieldStore;
certificates: ICertificatesItems;
selectedStorageClass: string;
namespace: string;
validPages: string[];
advancedMode: boolean;
classes: any;
}
@@ -73,13 +75,13 @@ const styles = (theme: Theme) =>
const AddTenant = ({
classes,
advancedMode,
fields,
certificates,
selectedStorageClass,
namespace,
validPages,
setModalErrorSnackMessage,
closeAndRefresh,
resetAddTenantForm,
}: IAddTenantProps) => {
// Modals
const [showNewCredentials, setShowNewCredentials] = useState<boolean>(false);
@@ -605,144 +607,80 @@ const AddTenant = ({
type: "other",
enabled: true,
action: () => {
closeAndRefresh(false);
history.push("/tenants");
},
};
const createButton = {
label: "Create",
type: "submit",
enabled:
!addSending &&
selectedStorageClass !== "" &&
validPages.includes("tenantSize"),
action: () => {
setAddSending(true);
},
};
const wizardSteps: IWizardElement[] = [
{
label: "Name Tenant",
label: "Setup",
componentRender: <NameTenant />,
buttons: [
cancelButton,
{
label: "Next",
type: "next",
enabled: validPages.includes("nameTenant"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Configure",
advancedOnly: true,
componentRender: <Configure />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("configure"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Pod Affinity",
label: "Images",
advancedOnly: true,
componentRender: <Images />,
buttons: [cancelButton, createButton],
},
{
label: "Pod Placement",
advancedOnly: true,
componentRender: <Affinity />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("affinity"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Identity Provider",
advancedOnly: true,
componentRender: <IdentityProvider />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("identityProvider"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Security",
advancedOnly: true,
componentRender: <Security />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("security"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Encryption",
advancedOnly: true,
componentRender: <Encryption />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("encryption"),
},
],
buttons: [cancelButton, createButton],
},
{
label: "Tenant Size",
componentRender: <TenantSize />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Next",
type: "next",
enabled: validPages.includes("tenantSize"),
},
],
},
{
label: "Preview Configuration",
label: "Review",
componentRender: <Preview />,
buttons: [
cancelButton,
{ label: "Back", type: "back", enabled: true },
{
label: "Create",
type: "submit",
enabled: !addSending,
action: () => {
setAddSending(true);
},
},
],
buttons: [cancelButton, createButton],
},
];
let filteredWizardSteps = wizardSteps;
if (!advancedMode) {
filteredWizardSteps = wizardSteps.filter((step) => !step.advancedOnly);
}
const closeCredentialsModal = () => {
closeAndRefresh(true);
resetAddTenantForm();
history.push("/tenants");
};
return (
<Fragment>
<Grid item xs={12} className={classes.customTitle}>
Create New Tenant
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
{showNewCredentials && (
<CredentialsPrompt
newServiceAccount={createdAccount}
@@ -753,7 +691,13 @@ const AddTenant = ({
entity="Tenant"
/>
)}
<Grid container>
<PageHeader label={"Create New Tenant"} />
<Grid container className={classes.container}>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={12}>
<GenericWizard wizardSteps={filteredWizardSteps} />
</Grid>
@@ -763,16 +707,18 @@ const AddTenant = ({
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
namespace: state.tenants.createTenant.fields.nameTenant.namespace,
validPages: state.tenants.createTenant.validPages,
fields: state.tenants.createTenant.fields,
certificates: state.tenants.createTenant.certificates,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
});
const connector = connect(mapState, {
setModalErrorSnackMessage,
updateAddField,
resetAddTenantForm,
});
export default withStyles(styles)(connector(AddTenant));

View File

@@ -17,7 +17,7 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, IconButton } from "@material-ui/core";
import { Grid, IconButton, Paper } from "@material-ui/core";
import { AppState } from "../../../../../store";
import { isPageValid, updateAddField } from "../../actions";
import { setModalErrorSnackMessage } from "../../../../../actions";
@@ -190,9 +190,9 @@ const Affinity = ({
}, [isPageValid, podAffinity, nodeSelectorLabels]);
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Pod Affinity</h3>
<h3 className={classes.h3Section}>Pod Placement</h3>
<span className={classes.descriptionText}>
Configure how pods will be assigned to nodes
</span>
@@ -208,11 +208,11 @@ const Affinity = ({
}}
selectorOptions={[
{ label: "None", value: "none" },
{ label: "Default (Pod Anti-afinnity)", value: "default" },
{ label: "Default (Pod Anti-Affinnity)", value: "default" },
{ label: "Node Selector", value: "nodeSelector" },
]}
/>
MinIO supports multiple configurations for Pod Afinnity
MinIO supports multiple configurations for Pod Affinity
</Grid>
{podAffinity === "nodeSelector" && (
<Fragment>
@@ -371,7 +371,7 @@ const Affinity = ({
</Grid>
</Fragment>
)}
</Fragment>
</Paper>
);
};

View File

@@ -17,7 +17,7 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid } from "@material-ui/core";
import { Grid, Paper } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
@@ -304,208 +304,13 @@ const Configure = ({
};
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Configure</h3>
<span className={classes.descriptionText}>
Basic configurations for tenant management
</span>
</div>
<Grid item xs={12}>
<FormSwitchWrapper
value="custom_image"
id="custom_image"
name="custom_image"
checked={customImage}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("customImage", checked);
}}
label={"Use custom image"}
/>
</Grid>
{customImage && (
<Fragment>
Please enter the MinIO docker image to use
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageName", e.target.value);
cleanValidation("image");
}}
label="MinIO's Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="E.g. minio/minio:RELEASE.2021-08-20T18-32-01Z"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchImage"
name="logSearchImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchImage", e.target.value);
cleanValidation("logSearchImage");
}}
label="Log Search API's Image"
value={logSearchImage}
error={validationErrors["logSearchImage"] || ""}
placeholder="E.g. minio/logsearchapi:v4.1.1"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="kesImage"
name="kesImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("kesImage", e.target.value);
cleanValidation("kesImage");
}}
label="KES Image"
value={kesImage}
error={validationErrors["kesImage"] || ""}
placeholder="E.g. minio/kes:v0.14.0"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchPostgresImage"
name="logSearchPostgresImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresImage", e.target.value);
cleanValidation("logSearchPostgresImage");
}}
label="Log Search Postgres's Image"
value={logSearchPostgresImage}
error={validationErrors["logSearchPostgresImage"] || ""}
placeholder="E.g. library/postgres:13"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchPostgresInitImage"
name="logSearchPostgresInitImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresInitImage", e.target.value);
cleanValidation("logSearchPostgresInitImage");
}}
label="Log Search Postgres's Init Image"
value={logSearchPostgresInitImage}
error={validationErrors["logSearchPostgresInitImage"] || ""}
placeholder="E.g. library/busybox:1.33.1"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusImage"
name="prometheusImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusImage", e.target.value);
cleanValidation("prometheusImage");
}}
label="Prometheus Image"
value={prometheusImage}
error={validationErrors["prometheusImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusSidecarImage"
name="prometheusSidecarImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSidecarImage", e.target.value);
cleanValidation("prometheusSidecarImage");
}}
label="Prometheus Sidecar Image"
value={prometheusSidecarImage}
error={validationErrors["prometheusSidecarImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusInitImage"
name="prometheusInitImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusInitImage", e.target.value);
cleanValidation("prometheusInitImage");
}}
label="Prometheus Init Image"
value={prometheusInitImage}
error={validationErrors["prometheusInitImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
</Fragment>
)}
{customImage && (
<Fragment>
<Grid item xs={12}>
<FormSwitchWrapper
value="custom_docker_hub"
id="custom_docker_hub"
name="custom_docker_hub"
checked={customDockerhub}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("customDockerhub", checked);
}}
label={"Set/Update Image Registry"}
/>
</Grid>
</Fragment>
)}
{customDockerhub && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="registry"
name="registry"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistry", e.target.value);
}}
label="Endpoint"
value={imageRegistry}
error={validationErrors["registry"] || ""}
placeholder="E.g. https://index.docker.io/v1/"
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryUsername"
name="registryUsername"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryUsername", e.target.value);
}}
label="Username"
value={imageRegistryUsername}
error={validationErrors["registryUsername"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryPassword"
name="registryPassword"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryPassword", e.target.value);
}}
label="Password"
value={imageRegistryPassword}
error={validationErrors["registryPassword"] || ""}
required
/>
</Grid>
</Fragment>
)}
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Expose Services</h3>
<span className={classes.descriptionText}>
@@ -662,7 +467,7 @@ const Configure = ({
<br />
</Fragment>
)}
</Fragment>
</Paper>
);
};

View File

@@ -17,7 +17,7 @@
import React, { Fragment, useState, useEffect, useCallback } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Typography } from "@material-ui/core";
import { Paper, Typography } from "@material-ui/core";
import Grid from "@material-ui/core/Grid";
import {
updateAddField,
@@ -328,7 +328,7 @@ const Encryption = ({
]);
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Encryption</h3>
<span className={classes.descriptionText}>
@@ -862,7 +862,7 @@ const Encryption = ({
)}
</Fragment>
)}
</Fragment>
</Paper>
);
};

View File

@@ -17,7 +17,13 @@
import React, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, IconButton, Tooltip, Typography } from "@material-ui/core";
import {
Grid,
IconButton,
Paper,
Tooltip,
Typography,
} from "@material-ui/core";
import CasinoIcon from "@material-ui/icons/Casino";
import DeleteIcon from "@material-ui/icons/Delete";
import {
@@ -378,7 +384,7 @@ const IdentityProvider = ({
});
}
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Identity Provider</h3>
<span className={classes.descriptionText}>
@@ -696,7 +702,7 @@ const IdentityProvider = ({
</Grid>
</Fragment>
)}
</Fragment>
</Paper>
);
};

View File

@@ -0,0 +1,543 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Grid, Paper } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { isPageValid, updateAddField } from "../../actions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
interface IImagesProps {
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
storageClasses: any;
classes: any;
customImage: boolean;
imageName: string;
customDockerhub: boolean;
imageRegistry: string;
imageRegistryUsername: string;
imageRegistryPassword: string;
exposeMinIO: boolean;
exposeConsole: boolean;
prometheusCustom: boolean;
logSearchCustom: boolean;
logSearchVolumeSize: string;
logSearchSizeFactor: string;
prometheusVolumeSize: string;
prometheusSizeFactor: string;
logSearchSelectedStorageClass: string;
logSearchImage: string;
kesImage: string;
logSearchPostgresImage: string;
logSearchPostgresInitImage: string;
prometheusSelectedStorageClass: string;
prometheusImage: string;
prometheusSidecarImage: string;
prometheusInitImage: string;
selectedStorageClass: string;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
...modalBasic,
...wizardCommon,
});
const Images = ({
classes,
storageClasses,
customImage,
imageName,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
exposeMinIO,
exposeConsole,
prometheusCustom,
logSearchCustom,
logSearchVolumeSize,
logSearchSizeFactor,
logSearchImage,
kesImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
prometheusVolumeSize,
prometheusSizeFactor,
logSearchSelectedStorageClass,
prometheusSelectedStorageClass,
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
updateAddField,
isPageValid,
selectedStorageClass,
}: IImagesProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("configure", field, value);
},
[updateAddField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (prometheusCustom) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "prometheus_storage_class",
required: true,
value: prometheusSelectedStorageClass,
customValidation: prometheusSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "prometheus_volume_size",
required: true,
value: prometheusVolumeSize,
customValidation:
prometheusVolumeSize === "" || parseInt(prometheusVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greatter than 0`,
},
];
}
if (logSearchCustom) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "log_search_storage_class",
required: true,
value: logSearchSelectedStorageClass,
customValidation: logSearchSelectedStorageClass === "",
customValidationMessage: "Field cannot be empty",
},
{
fieldKey: "log_search_volume_size",
required: true,
value: logSearchVolumeSize,
customValidation:
logSearchVolumeSize === "" || parseInt(logSearchVolumeSize) <= 0,
customValidationMessage: `Volume size must be present and be greatter than 0`,
},
];
}
if (customImage) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "image",
required: false,
value: imageName,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/minio:VERSION'",
},
{
fieldKey: "logSearchImage",
required: false,
value: logSearchImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/logsearchapi:VERSION'",
},
{
fieldKey: "kesImage",
required: false,
value: kesImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage: "Format must be of form: 'minio/kes:VERSION'",
},
{
fieldKey: "logSearchPostgresImage",
required: false,
value: logSearchPostgresImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/postgres:VERSION'",
},
{
fieldKey: "logSearchPostgresInitImage",
required: false,
value: logSearchPostgresInitImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/busybox:VERSION'",
},
{
fieldKey: "prometheusImage",
required: false,
value: prometheusImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'minio/prometheus:VERSION'",
},
{
fieldKey: "prometheusSidecarImage",
required: false,
value: prometheusSidecarImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'project/container:VERSION'",
},
{
fieldKey: "prometheusInitImage",
required: false,
value: prometheusInitImage,
pattern: /^((.*?)\/(.*?):(.+))$/,
customPatternMessage:
"Format must be of form: 'library/busybox:VERSION'",
},
];
if (customDockerhub) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "registry",
required: true,
value: imageRegistry,
},
{
fieldKey: "registryUsername",
required: true,
value: imageRegistryUsername,
},
{
fieldKey: "registryPassword",
required: true,
value: imageRegistryPassword,
},
];
}
}
const commonVal = commonFormValidation(customAccountValidation);
isPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
customImage,
imageName,
logSearchImage,
kesImage,
logSearchPostgresImage,
logSearchPostgresInitImage,
prometheusImage,
prometheusSidecarImage,
prometheusInitImage,
customDockerhub,
imageRegistry,
imageRegistryUsername,
imageRegistryPassword,
isPageValid,
prometheusCustom,
logSearchCustom,
prometheusSelectedStorageClass,
prometheusVolumeSize,
logSearchSelectedStorageClass,
logSearchVolumeSize,
]);
useEffect(() => {
// New default values is current selection is invalid
if (storageClasses.length > 0) {
const filterPrometheus = storageClasses.filter(
(item: any) => item.value === prometheusSelectedStorageClass
);
if (filterPrometheus.length === 0) {
updateField("prometheusSelectedStorageClass", selectedStorageClass);
}
const filterLogSearch = storageClasses.filter(
(item: any) => item.value === logSearchSelectedStorageClass
);
if (filterLogSearch.length === 0) {
updateField("logSearchSelectedStorageClass", selectedStorageClass);
}
}
}, [
logSearchSelectedStorageClass,
prometheusSelectedStorageClass,
selectedStorageClass,
storageClasses,
updateField,
]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Container Images</h3>
<span className={classes.descriptionText}>
Images used by the Tenant Deployment
</span>
</div>
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="image"
name="image"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageName", e.target.value);
cleanValidation("image");
}}
label="MinIO's Image"
value={imageName}
error={validationErrors["image"] || ""}
placeholder="E.g. minio/minio:RELEASE.2021-08-20T18-32-01Z"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchImage"
name="logSearchImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchImage", e.target.value);
cleanValidation("logSearchImage");
}}
label="Log Search API's Image"
value={logSearchImage}
error={validationErrors["logSearchImage"] || ""}
placeholder="E.g. minio/logsearchapi:v4.1.1"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="kesImage"
name="kesImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("kesImage", e.target.value);
cleanValidation("kesImage");
}}
label="KES Image"
value={kesImage}
error={validationErrors["kesImage"] || ""}
placeholder="E.g. minio/kes:v0.14.0"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchPostgresImage"
name="logSearchPostgresImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresImage", e.target.value);
cleanValidation("logSearchPostgresImage");
}}
label="Log Search Postgres's Image"
value={logSearchPostgresImage}
error={validationErrors["logSearchPostgresImage"] || ""}
placeholder="E.g. library/postgres:13"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="logSearchPostgresInitImage"
name="logSearchPostgresInitImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("logSearchPostgresInitImage", e.target.value);
cleanValidation("logSearchPostgresInitImage");
}}
label="Log Search Postgres's Init Image"
value={logSearchPostgresInitImage}
error={validationErrors["logSearchPostgresInitImage"] || ""}
placeholder="E.g. library/busybox:1.33.1"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusImage"
name="prometheusImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusImage", e.target.value);
cleanValidation("prometheusImage");
}}
label="Prometheus Image"
value={prometheusImage}
error={validationErrors["prometheusImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusSidecarImage"
name="prometheusSidecarImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusSidecarImage", e.target.value);
cleanValidation("prometheusSidecarImage");
}}
label="Prometheus Sidecar Image"
value={prometheusSidecarImage}
error={validationErrors["prometheusSidecarImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="prometheusInitImage"
name="prometheusInitImage"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("prometheusInitImage", e.target.value);
cleanValidation("prometheusInitImage");
}}
label="Prometheus Init Image"
value={prometheusInitImage}
error={validationErrors["prometheusInitImage"] || ""}
placeholder="E.g. quay.io/prometheus/prometheus:latest"
/>
</Grid>
</Fragment>
{customImage && (
<Fragment>
<Grid item xs={12}>
<FormSwitchWrapper
value="custom_docker_hub"
id="custom_docker_hub"
name="custom_docker_hub"
checked={customDockerhub}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("customDockerhub", checked);
}}
label={"Set/Update Image Registry"}
/>
</Grid>
</Fragment>
)}
{customDockerhub && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
id="registry"
name="registry"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistry", e.target.value);
}}
label="Endpoint"
value={imageRegistry}
error={validationErrors["registry"] || ""}
placeholder="E.g. https://index.docker.io/v1/"
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryUsername"
name="registryUsername"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryUsername", e.target.value);
}}
label="Username"
value={imageRegistryUsername}
error={validationErrors["registryUsername"] || ""}
required
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="registryPassword"
name="registryPassword"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("imageRegistryPassword", e.target.value);
}}
label="Password"
value={imageRegistryPassword}
error={validationErrors["registryPassword"] || ""}
required
/>
</Grid>
</Fragment>
)}
</Paper>
);
};
const mapState = (state: AppState) => ({
storageClasses: state.tenants.createTenant.storageClasses,
customImage: state.tenants.createTenant.fields.configure.customImage,
imageName: state.tenants.createTenant.fields.configure.imageName,
customDockerhub: state.tenants.createTenant.fields.configure.customDockerhub,
imageRegistry: state.tenants.createTenant.fields.configure.imageRegistry,
imageRegistryUsername:
state.tenants.createTenant.fields.configure.imageRegistryUsername,
imageRegistryPassword:
state.tenants.createTenant.fields.configure.imageRegistryPassword,
exposeMinIO: state.tenants.createTenant.fields.configure.exposeMinIO,
exposeConsole: state.tenants.createTenant.fields.configure.exposeConsole,
prometheusCustom:
state.tenants.createTenant.fields.configure.prometheusCustom,
logSearchCustom: state.tenants.createTenant.fields.configure.logSearchCustom,
logSearchVolumeSize:
state.tenants.createTenant.fields.configure.logSearchVolumeSize,
logSearchSizeFactor:
state.tenants.createTenant.fields.configure.logSearchSizeFactor,
prometheusVolumeSize:
state.tenants.createTenant.fields.configure.prometheusVolumeSize,
prometheusSizeFactor:
state.tenants.createTenant.fields.configure.prometheusSizeFactor,
logSearchSelectedStorageClass:
state.tenants.createTenant.fields.configure.logSearchSelectedStorageClass,
logSearchImage: state.tenants.createTenant.fields.configure.logSearchImage,
kesImage: state.tenants.createTenant.fields.configure.kesImage,
logSearchPostgresImage:
state.tenants.createTenant.fields.configure.logSearchPostgresImage,
logSearchPostgresInitImage:
state.tenants.createTenant.fields.configure.logSearchPostgresInitImage,
prometheusSelectedStorageClass:
state.tenants.createTenant.fields.configure.prometheusSelectedStorageClass,
prometheusImage: state.tenants.createTenant.fields.configure.prometheusImage,
prometheusSidecarImage:
state.tenants.createTenant.fields.configure.prometheusSidecarImage,
prometheusInitImage:
state.tenants.createTenant.fields.configure.prometheusInitImage,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(Images));

View File

@@ -14,7 +14,13 @@
// 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, { useEffect, useState, useMemo, useCallback } from "react";
import React, {
Fragment,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import get from "lodash/get";
@@ -26,17 +32,16 @@ import {
} from "../../../Common/FormComponents/common/styleLibrary";
import { setModalErrorSnackMessage } from "../../../../../actions";
import {
setAdvancedMode,
updateAddField,
isPageValid,
setStorageClassesList,
setLimitSize,
setStorageClassesList,
updateAddField,
} from "../../actions";
import {
getLimitSizes,
IQuotaElement,
IQuotas,
Opts,
getLimitSizes,
} from "../../ListTenants/utils";
import { AppState } from "../../../../../store";
import { commonFormValidation } from "../../../../../utils/validationFunctions";
@@ -45,15 +50,20 @@ import { ErrorResponseHandler } from "../../../../../common/types";
import api from "../../../../../common/api";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import AddIcon from "../../../../../icons/AddIcon";
import AddNamespaceModal from "./helpers/AddNamespaceModal";
import SizePreview from "./SizePreview";
import TenantSize from "./TenantSize";
import { Paper } from "@material-ui/core";
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
sizePreview: {
position: "fixed",
},
...modalBasic,
...wizardCommon,
});
@@ -62,7 +72,6 @@ interface INameTenantScreen {
classes: any;
storageClasses: Opts[];
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
setAdvancedMode: typeof setAdvancedMode;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
setStorageClassesList: typeof setStorageClassesList;
@@ -70,17 +79,14 @@ interface INameTenantScreen {
tenantName: string;
namespace: string;
selectedStorageClass: string;
advancedMode: boolean;
}
const NameTenant = ({
classes,
storageClasses,
advancedMode,
tenantName,
namespace,
selectedStorageClass,
setAdvancedMode,
updateAddField,
setStorageClassesList,
setLimitSize,
@@ -250,7 +256,7 @@ const NameTenant = ({
};
return (
<React.Fragment>
<Fragment>
{openAddNSConfirm && (
<AddNamespaceModal
addNamespaceOpen={openAddNSConfirm}
@@ -258,87 +264,77 @@ const NameTenant = ({
namespace={namespace}
/>
)}
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Name Tenant</h3>
<span className={classes.descriptionText}>
How would you like to name this new tenant?
</span>
</div>
<Grid item xs={12}>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("tenantName", e.target.value);
frmValidationCleanup("tenant-name");
}}
label="Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
<Grid container>
<Grid item xs={8}>
<Paper className={classes.paperWrapper}>
<Grid container>
<Grid item xs={12}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Name Tenant</h3>
<span className={classes.descriptionText}>
How would you like to name this new tenant?
</span>
</div>
<InputBoxWrapper
id="tenant-name"
name="tenant-name"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("tenantName", e.target.value);
frmValidationCleanup("tenant-name");
}}
label="Name"
value={tenantName}
required
error={validationErrors["tenant-name"] || ""}
/>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("namespace", e.target.value);
frmValidationCleanup("namespace");
}}
label="Namespace"
value={namespace}
error={validationErrors["namespace"] || ""}
overlayIcon={showCreateButton ? <AddIcon /> : null}
overlayAction={addNamespace}
required
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField(
"selectedStorageClass",
e.target.value as string
);
}}
label="Storage Class"
value={selectedStorageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<TenantSize />
</Grid>
</Paper>
</Grid>
<Grid item xs={4}>
<div className={classes.sizePreview}>
<SizePreview />
</div>
</Grid>
</Grid>
<Grid item xs={12}>
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("namespace", e.target.value);
frmValidationCleanup("namespace");
}}
label="Namespace"
value={namespace}
error={validationErrors["namespace"] || ""}
overlayIcon={showCreateButton ? <AddIcon /> : null}
overlayAction={addNamespace}
required
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("selectedStorageClass", e.target.value as string);
}}
label="Storage Class"
value={selectedStorageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12}>
<br />
<span className={classes.descriptionText}>
Check 'Advanced Mode' for additional configuration options, such as
configuring an Identity Provider, Encryption at rest, and customized
TLS/SSL Certificates.
<br />
Leave 'Advanced Mode' unchecked to use the secure default settings for
the tenant.
</span>
<br />
<br />
<FormSwitchWrapper
value="adv_mode"
id="adv_mode"
name="adv_mode"
checked={advancedMode}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
setAdvancedMode(checked);
}}
label={"Advanced Mode"}
/>
</Grid>
</React.Fragment>
</Fragment>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
tenantName: state.tenants.createTenant.fields.nameTenant.tenantName,
namespace: state.tenants.createTenant.fields.nameTenant.namespace,
selectedStorageClass:
@@ -348,7 +344,6 @@ const mapState = (state: AppState) => ({
const connector = connect(mapState, {
setModalErrorSnackMessage,
setAdvancedMode,
updateAddField,
setStorageClassesList,
setLimitSize,

View File

@@ -26,6 +26,7 @@ import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { Paper } from "@material-ui/core";
interface IPreviewProps {
classes: any;
@@ -62,7 +63,7 @@ const Preview = ({
enableTLS,
}: IPreviewProps) => {
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Review</h3>
<span className={classes.descriptionText}>
@@ -125,7 +126,7 @@ const Preview = ({
)}
</TableBody>
</Table>
</Fragment>
</Paper>
);
};

View File

@@ -17,7 +17,7 @@
import React, { useEffect, useCallback, Fragment } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { Button, Divider, Grid, Typography } from "@material-ui/core";
import { Button, Divider, Grid, Paper, Typography } from "@material-ui/core";
import {
modalBasic,
wizardCommon,
@@ -106,7 +106,7 @@ const Security = ({
}, [enableTLS, enableAutoCert, enableCustomCerts, isPageValid]);
return (
<Fragment>
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Security</h3>
</div>
@@ -286,7 +286,7 @@ const Security = ({
)}
</Fragment>
)}
</Fragment>
</Paper>
);
};

View File

@@ -0,0 +1,405 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { AppState } from "../../../../../store";
import { isPageValid, updateAddField } from "../../actions";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import {
calculateDistribution,
erasureCodeCalc,
getBytes,
niceBytes,
setMemoryResource,
} from "../../../../../common/utils";
import { ecListTransform, Opts } from "../../ListTenants/utils";
import { IMemorySize } from "../../ListTenants/types";
import {
ErrorResponseHandler,
ICapacity,
IErasureCodeCalc,
} from "../../../../../common/types";
import { commonFormValidation } from "../../../../../utils/validationFunctions";
import api from "../../../../../common/api";
import { Divider } from "@material-ui/core";
interface ISizePreviewProps {
classes: any;
updateAddField: typeof updateAddField;
isPageValid: typeof isPageValid;
advancedMode: boolean;
volumeSize: string;
sizeFactor: string;
drivesPerServer: string;
nodes: string;
memoryNode: string;
ecParity: string;
ecParityChoices: Opts[];
cleanECChoices: string[];
maxAllocableMemo: number;
memorySize: IMemorySize;
distribution: any;
ecParityCalc: IErasureCodeCalc;
limitSize: any;
selectedStorageClass: string;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
root: {
margin: 4,
},
table: {
"& .MuiTableCell-root": {
fontSize: 13,
},
},
...modalBasic,
...wizardCommon,
});
const SizePreview = ({
classes,
updateAddField,
isPageValid,
advancedMode,
volumeSize,
sizeFactor,
drivesPerServer,
nodes,
memoryNode,
ecParity,
ecParityChoices,
cleanECChoices,
maxAllocableMemo,
memorySize,
distribution,
ecParityCalc,
limitSize,
selectedStorageClass,
}: ISizePreviewProps) => {
const [errorFlag, setErrorFlag] = useState<boolean>(false);
const [nodeError, setNodeError] = useState<string>("");
const usableInformation = ecParityCalc.storageFactors.find(
(element) => element.erasureCode === ecParity
);
// Common
const updateField = useCallback(
(field: string, value: any) => {
updateAddField("tenantSize", field, value);
},
[updateAddField]
);
/*Debounce functions*/
// Storage Quotas
const validateMemorySize = useCallback(() => {
const memSize = parseInt(memoryNode) || 0;
const clusterSize = volumeSize || 0;
const maxMemSize = maxAllocableMemo || 0;
const clusterSizeFactor = sizeFactor;
const clusterSizeBytes = getBytes(
clusterSize.toString(10),
clusterSizeFactor
);
const memoSize = setMemoryResource(memSize, clusterSizeBytes, maxMemSize);
updateField("memorySize", memoSize);
}, [maxAllocableMemo, memoryNode, sizeFactor, updateField, volumeSize]);
const getMaxAllocableMemory = (nodes: string) => {
if (nodes !== "" && !isNaN(parseInt(nodes))) {
setNodeError("");
api
.invoke(
"GET",
`/api/v1/cluster/max-allocatable-memory?num_nodes=${nodes}`
)
.then((res: { max_memory: number }) => {
const maxMemory = res.max_memory ? res.max_memory : 0;
updateField("maxAllocableMemo", maxMemory);
})
.catch((err: ErrorResponseHandler) => {
setErrorFlag(true);
setNodeError(err.errorMessage);
});
}
};
useEffect(() => {
validateMemorySize();
}, [memoryNode, validateMemorySize]);
useEffect(() => {
validateMemorySize();
}, [maxAllocableMemo, validateMemorySize]);
useEffect(() => {
if (ecParityChoices.length > 0 && distribution.error === "") {
const ecCodeValidated = erasureCodeCalc(
cleanECChoices,
distribution.persistentVolumes,
distribution.pvSize,
distribution.nodes
);
updateField("ecParityCalc", ecCodeValidated);
updateField("ecParity", ecCodeValidated.defaultEC);
}
}, [ecParityChoices.length, distribution, cleanECChoices, updateField]);
/*End debounce functions*/
/*Calculate Allocation*/
useEffect(() => {
validateClusterSize();
getECValue();
getMaxAllocableMemory(nodes);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [nodes, volumeSize, sizeFactor, drivesPerServer]);
const validateClusterSize = () => {
const size = volumeSize;
const factor = sizeFactor;
const limitSize = getBytes("12", "Ti", true);
const clusterCapacity: ICapacity = {
unit: factor,
value: size.toString(),
};
const distrCalculate = calculateDistribution(
clusterCapacity,
parseInt(nodes),
parseInt(limitSize),
parseInt(drivesPerServer)
);
updateField("distribution", distrCalculate);
};
const getECValue = () => {
updateField("ecParity", "");
if (nodes.trim() !== "" && drivesPerServer.trim() !== "") {
api
.invoke("GET", `/api/v1/get-parity/${nodes}/${drivesPerServer}`)
.then((ecList: string[]) => {
updateField("ecParityChoices", ecListTransform(ecList));
updateField("cleanECChoices", ecList);
})
.catch((err: ErrorResponseHandler) => {
updateField("ecparityChoices", []);
isPageValid("tenantSize", false);
updateField("ecParity", "");
});
}
};
/*Calculate Allocation End*/
/* Validations of pages */
useEffect(() => {
const parsedSize = getBytes(volumeSize, sizeFactor, true);
const commonValidation = commonFormValidation([
{
fieldKey: "nodes",
required: true,
value: nodes,
customValidation: errorFlag,
customValidationMessage: nodeError,
},
{
fieldKey: "volume_size",
required: true,
value: volumeSize,
customValidation:
parseInt(parsedSize) < 1073741824 ||
parseInt(parsedSize) > limitSize[selectedStorageClass],
customValidationMessage: `Volume size must be greater than 1Gi and less than ${niceBytes(
limitSize[selectedStorageClass],
true
)}`,
},
{
fieldKey: "memory_per_node",
required: true,
value: memoryNode,
customValidation: parseInt(memoryNode) < 2,
customValidationMessage: "Memory size must be greater than 2Gi",
},
{
fieldKey: "drivesps",
required: true,
value: drivesPerServer,
customValidation: parseInt(drivesPerServer) < 1,
customValidationMessage: "There must be at least one drive",
},
]);
isPageValid(
"tenantSize",
!("nodes" in commonValidation) &&
!("volume_size" in commonValidation) &&
!("memory_per_node" in commonValidation) &&
!("drivesps" in commonValidation) &&
distribution.error === "" &&
ecParityCalc.error === 0 &&
memorySize.error === ""
);
}, [
nodes,
volumeSize,
sizeFactor,
memoryNode,
distribution,
drivesPerServer,
ecParityCalc,
memorySize,
limitSize,
selectedStorageClass,
isPageValid,
errorFlag,
nodeError,
]);
/* End Validation of pages */
return (
<div className={classes.root}>
<h4>Resource Allocation</h4>
<Divider />
<Table className={classes.table} aria-label="simple table" size={"small"}>
<TableBody>
<TableRow>
<TableCell scope="row">Number of Servers</TableCell>
<TableCell align="right">
{parseInt(nodes) > 0 ? nodes : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Drives per Server</TableCell>
<TableCell align="right">
{distribution ? distribution.disks : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Drive Capacity</TableCell>
<TableCell align="right">
{distribution ? niceBytes(distribution.pvSize) : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Total Volumes</TableCell>
<TableCell align="right">
{distribution ? distribution.persistentVolumes : "-"}
</TableCell>
</TableRow>
{!advancedMode && (
<TableRow>
<TableCell scope="row">Memory per Node</TableCell>
<TableCell align="right">{memoryNode} Gi</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{ecParityCalc.error === 0 && usableInformation && (
<Fragment>
<h4>Erasure Code Configuration</h4>
<Divider />
<Table
className={classes.table}
aria-label="simple table"
size={"small"}
>
<TableBody>
<TableRow>
<TableCell scope="row">EC Parity</TableCell>
<TableCell align="right">
{ecParity !== "" ? ecParity : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Raw Capacity</TableCell>
<TableCell align="right">
{niceBytes(ecParityCalc.rawCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Usable Capacity</TableCell>
<TableCell align="right">
{niceBytes(usableInformation.maxCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell scope="row">Server Failures Tolerated</TableCell>
<TableCell align="right">
{distribution
? Math.floor(
usableInformation.maxFailureTolerations /
distribution.disks
)
: "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Fragment>
)}
</div>
);
};
const mapState = (state: AppState) => ({
advancedMode: state.tenants.createTenant.advancedModeOn,
volumeSize: state.tenants.createTenant.fields.tenantSize.volumeSize,
sizeFactor: state.tenants.createTenant.fields.tenantSize.sizeFactor,
drivesPerServer: state.tenants.createTenant.fields.tenantSize.drivesPerServer,
nodes: state.tenants.createTenant.fields.tenantSize.nodes,
memoryNode: state.tenants.createTenant.fields.tenantSize.memoryNode,
ecParity: state.tenants.createTenant.fields.tenantSize.ecParity,
ecParityChoices: state.tenants.createTenant.fields.tenantSize.ecParityChoices,
cleanECChoices: state.tenants.createTenant.fields.tenantSize.cleanECChoices,
maxAllocableMemo:
state.tenants.createTenant.fields.tenantSize.maxAllocableMemo,
memorySize: state.tenants.createTenant.fields.tenantSize.memorySize,
distribution: state.tenants.createTenant.fields.tenantSize.distribution,
ecParityCalc: state.tenants.createTenant.fields.tenantSize.ecParityCalc,
limitSize: state.tenants.createTenant.fields.tenantSize.limitSize,
selectedStorageClass:
state.tenants.createTenant.fields.nameTenant.selectedStorageClass,
});
const connector = connect(mapState, {
updateAddField,
isPageValid,
});
export default withStyles(styles)(connector(SizePreview));

View File

@@ -24,10 +24,6 @@ import {
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@material-ui/core/Grid";
import Table from "@material-ui/core/Table";
import TableBody from "@material-ui/core/TableBody";
import TableCell from "@material-ui/core/TableCell";
import TableRow from "@material-ui/core/TableRow";
import {
calculateDistribution,
erasureCodeCalc,
@@ -102,9 +98,6 @@ const TenantSize = ({
const [validationErrors, setValidationErrors] = useState<any>({});
const [errorFlag, setErrorFlag] = useState<boolean>(false);
const [nodeError, setNodeError] = useState<string>("");
const usableInformation = ecParityCalc.storageFactors.find(
(element) => element.erasureCode === ecParity
);
// Common
const updateField = useCallback(
@@ -297,17 +290,23 @@ const TenantSize = ({
return (
<Fragment>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Tenant Size</h3>
<span className={classes.descriptionText}>
Please select the desired capacity
</span>
</div>
<Grid item xs={12}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Tenant Size</h3>
<span className={classes.descriptionText}>
Please select the desired capacity
</span>
</div>
</Grid>
{distribution.error !== "" && (
<div className={classes.error}>{distribution.error}</div>
<Grid item xs={12}>
<div className={classes.error}>{distribution.error}</div>
</Grid>
)}
{memorySize.error !== "" && (
<div className={classes.error}>{memorySize.error}</div>
<Grid item xs={12}>
<div className={classes.error}>{memorySize.error}</div>
</Grid>
)}
<Grid item xs={12}>
<InputBoxWrapper
@@ -319,6 +318,7 @@ const TenantSize = ({
cleanValidation("nodes");
}}
label="Number of Servers"
disabled={selectedStorageClass === ""}
value={nodes}
min="4"
required
@@ -336,6 +336,7 @@ const TenantSize = ({
}}
label="Number of Drives per Server"
value={drivesPerServer}
disabled={selectedStorageClass === ""}
min="1"
required
error={validationErrors["drivesps"] || ""}
@@ -354,6 +355,7 @@ const TenantSize = ({
}}
label="Total Size"
value={volumeSize}
disabled={selectedStorageClass === ""}
required
error={validationErrors["volume_size"] || ""}
min="0"
@@ -365,6 +367,7 @@ const TenantSize = ({
id="size_factor"
name="size_factor"
value={sizeFactor}
disabled={selectedStorageClass === ""}
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("sizeFactor", e.target.value as string);
}}
@@ -373,133 +376,43 @@ const TenantSize = ({
</div>
</div>
</Grid>
{advancedMode && (
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
id="memory_per_node"
name="memory_per_node"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("memoryNode", e.target.value);
cleanValidation("memory_per_node");
}}
label="Memory per Node [Gi]"
value={memoryNode}
required
error={validationErrors["memory_per_node"] || ""}
min="2"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="ec_parity"
name="ec_parity"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("ecParity", e.target.value as string);
}}
label="Erasure Code Parity"
value={ecParity}
options={ecParityChoices}
/>
<span className={classes.descriptionText}>
Please select the desired parity. This setting will change the max
usable capacity in the cluster
</span>
</Grid>
</Fragment>
)}
<h4>Resource Allocation</h4>
<Table className={classes.table} aria-label="simple table">
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
Number of Servers
</TableCell>
<TableCell align="right">
{parseInt(nodes) > 0 ? nodes : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Drives per Server
</TableCell>
<TableCell align="right">
{distribution ? distribution.disks : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Drive Capacity
</TableCell>
<TableCell align="right">
{distribution ? niceBytes(distribution.pvSize) : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Total Number of Volumes
</TableCell>
<TableCell align="right">
{distribution ? distribution.persistentVolumes : "-"}
</TableCell>
</TableRow>
{!advancedMode && (
<TableRow>
<TableCell component="th" scope="row">
Memory per Node
</TableCell>
<TableCell align="right">{memoryNode} Gi</TableCell>
</TableRow>
)}
</TableBody>
</Table>
{ecParityCalc.error === 0 && usableInformation && (
<Fragment>
<h4>Erasure Code Configuration</h4>
<Table className={classes.table} aria-label="simple table">
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
EC Parity
</TableCell>
<TableCell align="right">
{ecParity !== "" ? ecParity : "-"}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Raw Capacity
</TableCell>
<TableCell align="right">
{niceBytes(ecParityCalc.rawCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Usable Capacity
</TableCell>
<TableCell align="right">
{niceBytes(usableInformation.maxCapacity)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Number of server failures to tolerate
</TableCell>
<TableCell align="right">
{distribution
? Math.floor(
usableInformation.maxFailureTolerations /
distribution.disks
)
: "-"}
</TableCell>
</TableRow>
</TableBody>
</Table>
</Fragment>
)}
<Fragment>
<Grid item xs={12}>
<InputBoxWrapper
type="number"
id="memory_per_node"
name="memory_per_node"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("memoryNode", e.target.value);
cleanValidation("memory_per_node");
}}
label="Memory per Node [Gi]"
value={memoryNode}
disabled={selectedStorageClass === ""}
required
error={validationErrors["memory_per_node"] || ""}
min="2"
/>
</Grid>
<Grid item xs={12}>
<SelectWrapper
id="ec_parity"
name="ec_parity"
onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
updateField("ecParity", e.target.value as string);
}}
label="Erasure Code Parity"
disabled={selectedStorageClass === ""}
value={ecParity}
options={ecParityChoices}
/>
<span className={classes.descriptionText}>
Please select the desired parity. This setting will change the max
usable capacity in the cluster
</span>
</Grid>
</Fragment>
</Fragment>
);
};

View File

@@ -19,7 +19,7 @@ import { connect } from "react-redux";
import Grid from "@material-ui/core/Grid";
import TextField from "@material-ui/core/TextField";
import InputAdornment from "@material-ui/core/InputAdornment";
import { Button, IconButton } from "@material-ui/core";
import { IconButton } from "@material-ui/core";
import { createStyles, Theme, withStyles } from "@material-ui/core/styles";
import { ITenant, ITenantsResponse } from "./types";
import { niceBytes } from "../../../../common/utils";
@@ -27,77 +27,28 @@ import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import {
actionsTray,
searchField,
settingsCommon,
} from "../../Common/FormComponents/common/styleLibrary";
import { setErrorSnackMessage } from "../../../../actions";
import { AddIcon, CircleIcon } from "../../../../icons";
import { resetAddTenantForm } from "../actions";
import { CircleIcon, CreateIcon } from "../../../../icons";
import { ErrorResponseHandler } from "../../../../common/types";
import api from "../../../../common/api";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import DeleteTenant from "./DeleteTenant";
import AddTenant from "../AddTenant/AddTenant";
import CredentialsPrompt from "../../Common/CredentialsPrompt/CredentialsPrompt";
import history from "../../../../history";
import SlideOptions from "../../Common/SlideOptions/SlideOptions";
import BackSettingsIcon from "../../../../icons/BackSettingsIcon";
import RefreshIcon from "../../../../icons/RefreshIcon";
import SearchIcon from "../../../../icons/SearchIcon";
interface ITenantsList {
classes: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
resetAddTenantForm: typeof resetAddTenantForm;
}
const styles = (theme: Theme) =>
createStyles({
...actionsTray,
...searchField,
...settingsCommon,
settingsOptionsContainer: {
...settingsCommon.settingsOptionsContainer,
height: "calc(100vh - 150px)",
},
seeMore: {
marginTop: theme.spacing(3),
},
paper: {
display: "flex",
overflow: "auto",
flexDirection: "column",
},
addSideBar: {
width: "320px",
padding: "20px",
},
tableToolbar: {
paddingLeft: theme.spacing(2),
paddingRight: theme.spacing(0),
},
minTableHeader: {
color: "#393939",
"& tr": {
"& th": {
fontWeight: "bold",
},
},
},
actionsTray: {
...actionsTray.actionsTray,
padding: "0 38px",
},
tenantsContainer: {
padding: "15px 0",
},
customConfigurationPage: {
height: "calc(100vh - 260px)",
scrollbarWidth: "none" as const,
"&::-webkit-scrollbar": {
display: "none",
},
},
redState: {
color: theme.palette.error.main,
"& .MuiSvgIcon-root": {
@@ -136,12 +87,7 @@ const styles = (theme: Theme) =>
},
});
const ListTenants = ({
classes,
setErrorSnackMessage,
resetAddTenantForm,
}: ITenantsList) => {
const [currentPanel, setCurrentPanel] = useState<number>(0);
const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => {
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [selectedTenant, setSelectedTenant] = useState<any>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
@@ -151,15 +97,6 @@ const ListTenants = ({
const [createdAccount, setCreatedAccount] =
useState<NewServiceAccount | null>(null);
const closeAddModalAndRefresh = (reloadData: boolean) => {
setCurrentPanel(0);
resetAddTenantForm();
if (reloadData) {
setIsLoading(true);
}
};
const closeDeleteModalAndRefresh = (reloadData: boolean) => {
setDeleteOpen(false);
@@ -183,11 +120,6 @@ const ListTenants = ({
setCreatedAccount(null);
};
const backClick = () => {
setCurrentPanel(currentPanel - 1);
resetAddTenantForm();
};
const tableActions = [
{ type: "view", onClick: redirectToTenantDetails },
{ type: "delete", onClick: confirmDeleteTenant },
@@ -240,10 +172,6 @@ const ListTenants = ({
setIsLoading(true);
}, []);
const createTenant = () => {
setCurrentPanel(1);
};
const healthStatusToClass = (health_status: string) => {
switch (health_status) {
case "red":
@@ -277,104 +205,74 @@ const ListTenants = ({
/>
)}
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<IconButton
color="primary"
aria-label="Create Tenant"
component="span"
onClick={() => {
history.push("/tenants/add");
}}
>
<CreateIcon />
</IconButton>
</Grid>
<Grid item xs={12}>
<div className={classes.settingsOptionsContainer}>
<SlideOptions
slideOptions={[
<Fragment>
<Grid item xs={12} className={classes.tenantsContainer}>
<Grid item xs={12} className={classes.actionsTray}>
<TextField
placeholder="Search Tenants"
className={classes.searchField}
id="search-resource"
label=""
onChange={(val) => {
setFilterTenants(val.target.value);
}}
InputProps={{
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<IconButton
color="primary"
aria-label="Refresh Tenant List"
component="span"
onClick={() => {
setIsLoading(true);
}}
>
<RefreshIcon />
</IconButton>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
onClick={createTenant}
>
Create Tenant
</Button>
</Grid>
<Grid item xs={12} className={classes.tenantsContainer}>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFullObject: true,
renderFunction: (t) => {
return (
<React.Fragment>
<div
className={healthStatusToClass(
t.health_status
)}
>
<CircleIcon />
</div>
<div>{t.name}</div>
</React.Fragment>
);
},
},
{ label: "Namespace", elementKey: "namespace" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Pools", elementKey: "pool_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
customPaperHeight={classes.customConfigurationPage}
noBackground
/>
</Grid>
</Grid>
</Fragment>,
<Fragment>
<Grid item xs={12} className={classes.backContainer}>
<button onClick={backClick} className={classes.backButton}>
<BackSettingsIcon />
Back To Tenants List
</button>
</Grid>
<Grid item xs={12}>
{currentPanel === 1 && (
<AddTenant closeAndRefresh={closeAddModalAndRefresh} />
)}
</Grid>
</Fragment>,
]}
currentSlide={currentPanel}
/>
</div>
<TableWrapper
itemActions={tableActions}
columns={[
{
label: "Name",
elementKey: "name",
renderFullObject: true,
renderFunction: (t) => {
return (
<React.Fragment>
<div className={healthStatusToClass(t.health_status)}>
<CircleIcon />
</div>
<div>{t.name}</div>
</React.Fragment>
);
},
},
{ label: "Namespace", elementKey: "namespace" },
{ label: "Capacity", elementKey: "capacity" },
{ label: "# of Pools", elementKey: "pool_count" },
{ label: "State", elementKey: "currentState" },
]}
isLoading={isLoading}
records={filteredRecords}
entityName="Tenants"
idField="name"
/>
</Grid>
</Grid>
</Fragment>
@@ -383,7 +281,6 @@ const ListTenants = ({
const connector = connect(null, {
setErrorSnackMessage,
resetAddTenantForm,
});
export default withStyles(styles)(connector(ListTenants));

View File

@@ -61,7 +61,7 @@ const initialState: ITenantState = {
selectedStorageClass: "",
},
configure: {
customImage: false,
customImage: true,
imageName: "",
customDockerhub: false,
imageRegistry: "",