From 0fdf5ee0fc8fba424b4d9ff9b5e9c61d38d87fb1 Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Mon, 13 Sep 2021 16:00:44 -0700 Subject: [PATCH] Add Tenant in non-linear way (#1027) * Add Tenant in non-linear way Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- pkg/acl/endpoints.go | 4 + pkg/acl/endpoints_test.go | 76 +-- portal-ui/src/icons/ConsoleLogo.tsx | 5 - portal-ui/src/icons/OperatorLogo.tsx | 3 - .../FormComponents/common/styleLibrary.ts | 3 + .../Common/GenericWizard/GenericWizard.tsx | 64 +-- .../Common/GenericWizard/WizardPage.tsx | 2 +- .../Console/Common/PageHeader/PageHeader.tsx | 3 +- portal-ui/src/screens/Console/Console.tsx | 9 +- portal-ui/src/screens/Console/Menu/Menu.tsx | 22 +- .../Console/Tenants/AddTenant/AddTenant.tsx | 152 ++--- .../Tenants/AddTenant/Steps/Affinity.tsx | 12 +- .../Tenants/AddTenant/Steps/Configure.tsx | 201 +------ .../Tenants/AddTenant/Steps/Encryption.tsx | 6 +- .../AddTenant/Steps/IdentityProvider.tsx | 12 +- .../Tenants/AddTenant/Steps/Images.tsx | 543 ++++++++++++++++++ .../Tenants/AddTenant/Steps/NameTenant.tsx | 171 +++--- .../Tenants/AddTenant/Steps/Preview.tsx | 5 +- .../Tenants/AddTenant/Steps/Security.tsx | 6 +- .../Tenants/AddTenant/Steps/SizePreview.tsx | 405 +++++++++++++ .../Tenants/AddTenant/Steps/TenantSize.tsx | 197 ++----- .../Tenants/ListTenants/ListTenants.tsx | 243 +++----- .../src/screens/Console/Tenants/reducer.ts | 2 +- 23 files changed, 1282 insertions(+), 864 deletions(-) create mode 100644 portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Images.tsx create mode 100644 portal-ui/src/screens/Console/Tenants/AddTenant/Steps/SizePreview.tsx diff --git a/pkg/acl/endpoints.go b/pkg/acl/endpoints.go index 4ebe91350..610844a02 100644 --- a/pkg/acl/endpoints.go +++ b/pkg/acl/endpoints.go @@ -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, diff --git a/pkg/acl/endpoints_test.go b/pkg/acl/endpoints_test.go index 186c10718..fe55a21dc 100644 --- a/pkg/acl/endpoints_test.go +++ b/pkg/acl/endpoints_test.go @@ -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) - } - }) - } -} diff --git a/portal-ui/src/icons/ConsoleLogo.tsx b/portal-ui/src/icons/ConsoleLogo.tsx index f95432781..32b37660f 100644 --- a/portal-ui/src/icons/ConsoleLogo.tsx +++ b/portal-ui/src/icons/ConsoleLogo.tsx @@ -15,11 +15,6 @@ // along with this program. If not, see . import React from "react"; -import { SvgIcon } from "@material-ui/core"; - -interface IConsoleLogo { - width?: number; -} const ConsoleLogo = () => { return ( diff --git a/portal-ui/src/icons/OperatorLogo.tsx b/portal-ui/src/icons/OperatorLogo.tsx index ebd0158e5..c8cedb0b7 100644 --- a/portal-ui/src/icons/OperatorLogo.tsx +++ b/portal-ui/src/icons/OperatorLogo.tsx @@ -27,9 +27,6 @@ const OperatorLogo = ({ width = 120 }: IOperatorLogo) => { viewBox="0 0 606.583 134.691" width={width} > - - - 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 ( + + + {wizardSteps.map((step, index) => { + return ( + pageChange(index)} + key={`wizard-${index.toString()}`} + selected={currentStep === index} + > + + + ); + })} + + + ); + }; + const stepsListModal = () => { return (
    {wizardSteps.map((step, index) => { @@ -186,16 +187,13 @@ const GenericWizard = ({
    Steps
    -
    {stepsList()}
    +
    {stepsListModal()}
    ) : ( - -
    - Steps - {stepsList()} -
    + + {stepsList()}
    )} @@ -203,9 +201,9 @@ const GenericWizard = ({ diff --git a/portal-ui/src/screens/Console/Common/GenericWizard/WizardPage.tsx b/portal-ui/src/screens/Console/Common/GenericWizard/WizardPage.tsx index a8e6f9d37..6cfedbe9e 100644 --- a/portal-ui/src/screens/Console/Common/GenericWizard/WizardPage.tsx +++ b/portal-ui/src/screens/Console/Common/GenericWizard/WizardPage.tsx @@ -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%", }, diff --git a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx index 63964b38e..bca3b5b80 100644 --- a/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx +++ b/portal-ui/src/screens/Console/Common/PageHeader/PageHeader.tsx @@ -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"; diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 39c88a428..a5434224b 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -14,12 +14,10 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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", diff --git a/portal-ui/src/screens/Console/Menu/Menu.tsx b/portal-ui/src/screens/Console/Menu/Menu.tsx index 0ba3f028d..7545618f4 100644 --- a/portal-ui/src/screens/Console/Menu/Menu.tsx +++ b/portal-ui/src/screens/Console/Menu/Menu.tsx @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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 ( 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(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: , - buttons: [ - cancelButton, - { - label: "Next", - type: "next", - enabled: validPages.includes("nameTenant"), - }, - ], + buttons: [cancelButton, createButton], }, { label: "Configure", advancedOnly: true, componentRender: , - 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: , + buttons: [cancelButton, createButton], + }, + { + label: "Pod Placement", advancedOnly: true, componentRender: , - 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: , - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { - label: "Next", - type: "next", - enabled: validPages.includes("identityProvider"), - }, - ], + buttons: [cancelButton, createButton], }, { label: "Security", advancedOnly: true, componentRender: , - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { - label: "Next", - type: "next", - enabled: validPages.includes("security"), - }, - ], + buttons: [cancelButton, createButton], }, { label: "Encryption", advancedOnly: true, componentRender: , - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { - label: "Next", - type: "next", - enabled: validPages.includes("encryption"), - }, - ], + buttons: [cancelButton, createButton], }, { - label: "Tenant Size", - componentRender: , - buttons: [ - cancelButton, - { label: "Back", type: "back", enabled: true }, - { - label: "Next", - type: "next", - enabled: validPages.includes("tenantSize"), - }, - ], - }, - { - label: "Preview Configuration", + label: "Review", componentRender: , - 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 ( - - Create New Tenant - - {addSending && ( - - - - )} {showNewCredentials && ( )} - + + + {addSending && ( + + + + )} @@ -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)); diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx index 75c3129de..19c3533c9 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx @@ -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 ( - +
    -

    Pod Affinity

    +

    Pod Placement

    Configure how pods will be assigned to nodes @@ -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 {podAffinity === "nodeSelector" && ( @@ -371,7 +371,7 @@ const Affinity = ({ )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Configure.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Configure.tsx index 54f2bbb60..dcced5cc7 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Configure.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Configure.tsx @@ -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 ( - +

    Configure

    Basic configurations for tenant management
    - - - { - const targetD = e.target; - const checked = targetD.checked; - updateField("customImage", checked); - }} - label={"Use custom image"} - /> - - {customImage && ( - - Please enter the MinIO docker image to use - - ) => { - 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" - /> - - - ) => { - 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" - /> - - - ) => { - updateField("kesImage", e.target.value); - cleanValidation("kesImage"); - }} - label="KES Image" - value={kesImage} - error={validationErrors["kesImage"] || ""} - placeholder="E.g. minio/kes:v0.14.0" - /> - - - ) => { - updateField("logSearchPostgresImage", e.target.value); - cleanValidation("logSearchPostgresImage"); - }} - label="Log Search Postgres's Image" - value={logSearchPostgresImage} - error={validationErrors["logSearchPostgresImage"] || ""} - placeholder="E.g. library/postgres:13" - /> - - - ) => { - 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" - /> - - - ) => { - updateField("prometheusImage", e.target.value); - cleanValidation("prometheusImage"); - }} - label="Prometheus Image" - value={prometheusImage} - error={validationErrors["prometheusImage"] || ""} - placeholder="E.g. quay.io/prometheus/prometheus:latest" - /> - - - ) => { - updateField("prometheusSidecarImage", e.target.value); - cleanValidation("prometheusSidecarImage"); - }} - label="Prometheus Sidecar Image" - value={prometheusSidecarImage} - error={validationErrors["prometheusSidecarImage"] || ""} - placeholder="E.g. quay.io/prometheus/prometheus:latest" - /> - - - ) => { - updateField("prometheusInitImage", e.target.value); - cleanValidation("prometheusInitImage"); - }} - label="Prometheus Init Image" - value={prometheusInitImage} - error={validationErrors["prometheusInitImage"] || ""} - placeholder="E.g. quay.io/prometheus/prometheus:latest" - /> - - - )} - {customImage && ( - - - { - const targetD = e.target; - const checked = targetD.checked; - - updateField("customDockerhub", checked); - }} - label={"Set/Update Image Registry"} - /> - - - )} - {customDockerhub && ( - - - ) => { - updateField("imageRegistry", e.target.value); - }} - label="Endpoint" - value={imageRegistry} - error={validationErrors["registry"] || ""} - placeholder="E.g. https://index.docker.io/v1/" - required - /> - - - ) => { - updateField("imageRegistryUsername", e.target.value); - }} - label="Username" - value={imageRegistryUsername} - error={validationErrors["registryUsername"] || ""} - required - /> - - - ) => { - updateField("imageRegistryPassword", e.target.value); - }} - label="Password" - value={imageRegistryPassword} - error={validationErrors["registryPassword"] || ""} - required - /> - - - )}

    Expose Services

    @@ -662,7 +467,7 @@ const Configure = ({
    )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Encryption.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Encryption.tsx index 6e7cfc7bb..221c5f09a 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Encryption.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Encryption.tsx @@ -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 ( - +

    Encryption

    @@ -862,7 +862,7 @@ const Encryption = ({ )} )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/IdentityProvider.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/IdentityProvider.tsx index b0d59e275..ce7c57c13 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/IdentityProvider.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/IdentityProvider.tsx @@ -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 ( - +

    Identity Provider

    @@ -696,7 +702,7 @@ const IdentityProvider = ({ )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Images.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Images.tsx new file mode 100644 index 000000000..491779c01 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Images.tsx @@ -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 . + +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({}); + + // 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 ( + +
    +

    Container Images

    + + Images used by the Tenant Deployment + +
    + + + + ) => { + 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" + /> + + + ) => { + 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" + /> + + + ) => { + updateField("kesImage", e.target.value); + cleanValidation("kesImage"); + }} + label="KES Image" + value={kesImage} + error={validationErrors["kesImage"] || ""} + placeholder="E.g. minio/kes:v0.14.0" + /> + + + ) => { + updateField("logSearchPostgresImage", e.target.value); + cleanValidation("logSearchPostgresImage"); + }} + label="Log Search Postgres's Image" + value={logSearchPostgresImage} + error={validationErrors["logSearchPostgresImage"] || ""} + placeholder="E.g. library/postgres:13" + /> + + + ) => { + 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" + /> + + + ) => { + updateField("prometheusImage", e.target.value); + cleanValidation("prometheusImage"); + }} + label="Prometheus Image" + value={prometheusImage} + error={validationErrors["prometheusImage"] || ""} + placeholder="E.g. quay.io/prometheus/prometheus:latest" + /> + + + ) => { + updateField("prometheusSidecarImage", e.target.value); + cleanValidation("prometheusSidecarImage"); + }} + label="Prometheus Sidecar Image" + value={prometheusSidecarImage} + error={validationErrors["prometheusSidecarImage"] || ""} + placeholder="E.g. quay.io/prometheus/prometheus:latest" + /> + + + ) => { + updateField("prometheusInitImage", e.target.value); + cleanValidation("prometheusInitImage"); + }} + label="Prometheus Init Image" + value={prometheusInitImage} + error={validationErrors["prometheusInitImage"] || ""} + placeholder="E.g. quay.io/prometheus/prometheus:latest" + /> + + + + {customImage && ( + + + { + const targetD = e.target; + const checked = targetD.checked; + + updateField("customDockerhub", checked); + }} + label={"Set/Update Image Registry"} + /> + + + )} + {customDockerhub && ( + + + ) => { + updateField("imageRegistry", e.target.value); + }} + label="Endpoint" + value={imageRegistry} + error={validationErrors["registry"] || ""} + placeholder="E.g. https://index.docker.io/v1/" + required + /> + + + ) => { + updateField("imageRegistryUsername", e.target.value); + }} + label="Username" + value={imageRegistryUsername} + error={validationErrors["registryUsername"] || ""} + required + /> + + + ) => { + updateField("imageRegistryPassword", e.target.value); + }} + label="Password" + value={imageRegistryPassword} + error={validationErrors["registryPassword"] || ""} + required + /> + + + )} +
    + ); +}; + +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)); diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/NameTenant.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/NameTenant.tsx index 6e5d859ee..f0f2fd777 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/NameTenant.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/NameTenant.tsx @@ -14,7 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -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 ( - + {openAddNSConfirm && ( )} -
    -

    Name Tenant

    - - How would you like to name this new tenant? - -
    - - ) => { - updateField("tenantName", e.target.value); - frmValidationCleanup("tenant-name"); - }} - label="Name" - value={tenantName} - required - error={validationErrors["tenant-name"] || ""} - /> + + + + + +
    +

    Name Tenant

    + + How would you like to name this new tenant? + +
    + ) => { + updateField("tenantName", e.target.value); + frmValidationCleanup("tenant-name"); + }} + label="Name" + value={tenantName} + required + error={validationErrors["tenant-name"] || ""} + /> +
    + + ) => { + updateField("namespace", e.target.value); + frmValidationCleanup("namespace"); + }} + label="Namespace" + value={namespace} + error={validationErrors["namespace"] || ""} + overlayIcon={showCreateButton ? : null} + overlayAction={addNamespace} + required + /> + + + ) => { + updateField( + "selectedStorageClass", + e.target.value as string + ); + }} + label="Storage Class" + value={selectedStorageClass} + options={storageClasses} + disabled={storageClasses.length < 1} + /> + + +
    +
    +
    + +
    + +
    +
    - - ) => { - updateField("namespace", e.target.value); - frmValidationCleanup("namespace"); - }} - label="Namespace" - value={namespace} - error={validationErrors["namespace"] || ""} - overlayIcon={showCreateButton ? : null} - overlayAction={addNamespace} - required - /> - - - ) => { - updateField("selectedStorageClass", e.target.value as string); - }} - label="Storage Class" - value={selectedStorageClass} - options={storageClasses} - disabled={storageClasses.length < 1} - /> - - -
    - - Check 'Advanced Mode' for additional configuration options, such as - configuring an Identity Provider, Encryption at rest, and customized - TLS/SSL Certificates. -
    - Leave 'Advanced Mode' unchecked to use the secure default settings for - the tenant. -
    -
    -
    - { - const targetD = e.target; - const checked = targetD.checked; - - setAdvancedMode(checked); - }} - label={"Advanced Mode"} - /> -
    -
    + ); }; 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, diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Preview.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Preview.tsx index b62bf668a..d1c701d8c 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Preview.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Preview.tsx @@ -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 ( - +

    Review

    @@ -125,7 +126,7 @@ const Preview = ({ )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Security.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Security.tsx index 75ad3b66f..2b1ccd4ae 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Security.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Security.tsx @@ -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 ( - +

    Security

    @@ -286,7 +286,7 @@ const Security = ({ )}
    )} - + ); }; diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/SizePreview.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/SizePreview.tsx new file mode 100644 index 000000000..5ed220a4d --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/SizePreview.tsx @@ -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 . + +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(false); + const [nodeError, setNodeError] = useState(""); + 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 ( +
    +

    Resource Allocation

    + + + + + Number of Servers + + {parseInt(nodes) > 0 ? nodes : "-"} + + + + Drives per Server + + {distribution ? distribution.disks : "-"} + + + + Drive Capacity + + {distribution ? niceBytes(distribution.pvSize) : "-"} + + + + Total Volumes + + {distribution ? distribution.persistentVolumes : "-"} + + + {!advancedMode && ( + + Memory per Node + {memoryNode} Gi + + )} + +
    + {ecParityCalc.error === 0 && usableInformation && ( + +

    Erasure Code Configuration

    + + + + + EC Parity + + {ecParity !== "" ? ecParity : "-"} + + + + Raw Capacity + + {niceBytes(ecParityCalc.rawCapacity)} + + + + Usable Capacity + + {niceBytes(usableInformation.maxCapacity)} + + + + Server Failures Tolerated + + {distribution + ? Math.floor( + usableInformation.maxFailureTolerations / + distribution.disks + ) + : "-"} + + + +
    +
    + )} +
    + ); +}; + +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)); diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx index 6b97d263f..03ce292c5 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantSize.tsx @@ -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({}); const [errorFlag, setErrorFlag] = useState(false); const [nodeError, setNodeError] = useState(""); - const usableInformation = ecParityCalc.storageFactors.find( - (element) => element.erasureCode === ecParity - ); // Common const updateField = useCallback( @@ -297,17 +290,23 @@ const TenantSize = ({ return ( -
    -

    Tenant Size

    - - Please select the desired capacity - -
    + +
    +

    Tenant Size

    + + Please select the desired capacity + +
    +
    {distribution.error !== "" && ( -
    {distribution.error}
    + +
    {distribution.error}
    +
    )} {memorySize.error !== "" && ( -
    {memorySize.error}
    + +
    {memorySize.error}
    +
    )} ) => { updateField("sizeFactor", e.target.value as string); }} @@ -373,133 +376,43 @@ const TenantSize = ({
    - {advancedMode && ( - - - ) => { - updateField("memoryNode", e.target.value); - cleanValidation("memory_per_node"); - }} - label="Memory per Node [Gi]" - value={memoryNode} - required - error={validationErrors["memory_per_node"] || ""} - min="2" - /> - - - ) => { - updateField("ecParity", e.target.value as string); - }} - label="Erasure Code Parity" - value={ecParity} - options={ecParityChoices} - /> - - Please select the desired parity. This setting will change the max - usable capacity in the cluster - - - - )} -

    Resource Allocation

    - - - - - Number of Servers - - - {parseInt(nodes) > 0 ? nodes : "-"} - - - - - Drives per Server - - - {distribution ? distribution.disks : "-"} - - - - - Drive Capacity - - - {distribution ? niceBytes(distribution.pvSize) : "-"} - - - - - Total Number of Volumes - - - {distribution ? distribution.persistentVolumes : "-"} - - - {!advancedMode && ( - - - Memory per Node - - {memoryNode} Gi - - )} - -
    - {ecParityCalc.error === 0 && usableInformation && ( - -

    Erasure Code Configuration

    - - - - - EC Parity - - - {ecParity !== "" ? ecParity : "-"} - - - - - Raw Capacity - - - {niceBytes(ecParityCalc.rawCapacity)} - - - - - Usable Capacity - - - {niceBytes(usableInformation.maxCapacity)} - - - - - Number of server failures to tolerate - - - {distribution - ? Math.floor( - usableInformation.maxFailureTolerations / - distribution.disks - ) - : "-"} - - - -
    -
    - )} + + + + ) => { + 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" + /> + + + ) => { + updateField("ecParity", e.target.value as string); + }} + label="Erasure Code Parity" + disabled={selectedStorageClass === ""} + value={ecParity} + options={ecParityChoices} + /> + + Please select the desired parity. This setting will change the max + usable capacity in the cluster + + +
    ); }; diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/ListTenants.tsx b/portal-ui/src/screens/Console/Tenants/ListTenants/ListTenants.tsx index b98f0a6de..25ccc2389 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/ListTenants.tsx +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/ListTenants.tsx @@ -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(0); +const ListTenants = ({ classes, setErrorSnackMessage }: ITenantsList) => { const [deleteOpen, setDeleteOpen] = useState(false); const [selectedTenant, setSelectedTenant] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -151,15 +97,6 @@ const ListTenants = ({ const [createdAccount, setCreatedAccount] = useState(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 = ({ /> )} + + { + setFilterTenants(val.target.value); + }} + InputProps={{ + disableUnderline: true, + startAdornment: ( + + + + ), + }} + /> + { + setIsLoading(true); + }} + > + + + { + history.push("/tenants/add"); + }} + > + + + -
    - - - - { - setFilterTenants(val.target.value); - }} - InputProps={{ - disableUnderline: true, - startAdornment: ( - - - - ), - }} - /> - { - setIsLoading(true); - }} - > - - - - - - { - return ( - -
    - -
    -
    {t.name}
    -
    - ); - }, - }, - { 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 - /> -
    -
    - , - - - - - - {currentPanel === 1 && ( - - )} - - , - ]} - currentSlide={currentPanel} - /> -
    + { + return ( + +
    + +
    +
    {t.name}
    +
    + ); + }, + }, + { 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" + />
    @@ -383,7 +281,6 @@ const ListTenants = ({ const connector = connect(null, { setErrorSnackMessage, - resetAddTenantForm, }); export default withStyles(styles)(connector(ListTenants)); diff --git a/portal-ui/src/screens/Console/Tenants/reducer.ts b/portal-ui/src/screens/Console/Tenants/reducer.ts index 25001d62c..c0a38ca53 100644 --- a/portal-ui/src/screens/Console/Tenants/reducer.ts +++ b/portal-ui/src/screens/Console/Tenants/reducer.ts @@ -61,7 +61,7 @@ const initialState: ITenantState = { selectedStorageClass: "", }, configure: { - customImage: false, + customImage: true, imageName: "", customDockerhub: false, imageRegistry: "",