From bfbaaf12fb7fb0f057ff90f2ea28032c23075250 Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Thu, 7 Apr 2022 20:24:37 -0600 Subject: [PATCH] Add Edit pool capability (#1806) Signed-off-by: Benjamin Perez --- operatorapi/tenants.go | 24 +- .../src/common/SecureComponent/permissions.ts | 2 + portal-ui/src/common/types.ts | 2 +- portal-ui/src/screens/Console/Console.tsx | 8 +- .../Tenants/AddTenant/Steps/Affinity.tsx | 2 +- .../Console/Tenants/ListTenants/types.ts | 17 + .../Pools/{ => AddPool}/AddPool.tsx | 38 +- .../Pools/{ => AddPool}/PoolConfiguration.tsx | 16 +- .../Pools/{ => AddPool}/PoolPodPlacement.tsx | 32 +- .../Pools/{ => AddPool}/PoolResources.tsx | 24 +- .../Pools/Details/PoolDetails.tsx | 192 ++++++- .../TenantDetails/Pools/EditPool/EditPool.tsx | 390 +++++++++++++ .../Pools/EditPool/EditPoolConfiguration.tsx | 289 ++++++++++ .../Pools/EditPool/EditPoolPlacement.tsx | 534 ++++++++++++++++++ .../Pools/EditPool/EditPoolResources.tsx | 313 ++++++++++ .../Tenants/TenantDetails/PoolsSummary.tsx | 25 +- .../Console/Tenants/TenantDetails/utils.ts | 1 - .../src/screens/Console/Tenants/actions.ts | 112 +++- .../src/screens/Console/Tenants/reducer.ts | 363 +++++++++++- .../src/screens/Console/Tenants/types.ts | 112 +++- 20 files changed, 2373 insertions(+), 123 deletions(-) rename portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/{ => AddPool}/AddPool.tsx (88%) rename portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/{ => AddPool}/PoolConfiguration.tsx (94%) rename portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/{ => AddPool}/PoolPodPlacement.tsx (94%) rename portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/{ => AddPool}/PoolResources.tsx (92%) create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx diff --git a/operatorapi/tenants.go b/operatorapi/tenants.go index cff175dc0..dfbf16157 100644 --- a/operatorapi/tenants.go +++ b/operatorapi/tenants.go @@ -2321,6 +2321,21 @@ func parseTenantPool(pool *miniov2.Pool) *models.Pool { tolerations = append(tolerations, toleration) } + var securityContext models.SecurityContext + + if pool.SecurityContext != nil { + fsGroup := strconv.Itoa(int(*pool.SecurityContext.FSGroup)) + runAsGroup := strconv.Itoa(int(*pool.SecurityContext.RunAsGroup)) + runAsUser := strconv.Itoa(int(*pool.SecurityContext.RunAsUser)) + + securityContext = models.SecurityContext{ + FsGroup: &fsGroup, + RunAsGroup: &runAsGroup, + RunAsNonRoot: pool.SecurityContext.RunAsNonRoot, + RunAsUser: &runAsUser, + } + } + poolModel := &models.Pool{ Name: pool.Name, Servers: swag.Int64(int64(pool.Servers)), @@ -2329,10 +2344,11 @@ func parseTenantPool(pool *miniov2.Pool) *models.Pool { Size: size, StorageClassName: storageClassName, }, - NodeSelector: pool.NodeSelector, - Resources: resources, - Affinity: affinity, - Tolerations: tolerations, + NodeSelector: pool.NodeSelector, + Resources: resources, + Affinity: affinity, + Tolerations: tolerations, + SecurityContext: &securityContext, } return poolModel } diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index 62a38951c..7e2107637 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -183,6 +183,8 @@ export const IAM_PAGES = { "/namespaces/:tenantNamespace/tenants/:tenantName/pools", NAMESPACE_TENANT_POOLS_ADD: "/namespaces/:tenantNamespace/tenants/:tenantName/add-pool", + NAMESPACE_TENANT_POOLS_EDIT: + "/namespaces/:tenantNamespace/tenants/:tenantName/edit-pool", NAMESPACE_TENANT_VOLUMES: "/namespaces/:tenantNamespace/tenants/:tenantName/volumes", NAMESPACE_TENANT_LICENSE: diff --git a/portal-ui/src/common/types.ts b/portal-ui/src/common/types.ts index 75a50efb8..eb2e35d04 100644 --- a/portal-ui/src/common/types.ts +++ b/portal-ui/src/common/types.ts @@ -125,7 +125,7 @@ export interface INodeAffinityTerms { } export interface INodeAffinityLabelsSelector { - matchExpressions: object[]; + matchExpressions: IMatchExpressionItem[]; } export interface IMatchExpressionItem { diff --git a/portal-ui/src/screens/Console/Console.tsx b/portal-ui/src/screens/Console/Console.tsx index 6d7756111..ddc491a24 100644 --- a/portal-ui/src/screens/Console/Console.tsx +++ b/portal-ui/src/screens/Console/Console.tsx @@ -50,6 +50,7 @@ import { import { hasPermission } from "../../common/SecureComponent"; import { IRouteRule } from "./Menu/types"; import LoadingComponent from "../../common/LoadingComponent"; +import EditPool from "./Tenants/TenantDetails/Pools/EditPool/EditPool"; const Trace = React.lazy(() => import("./Trace/Trace")); const Heal = React.lazy(() => import("./Heal/Heal")); @@ -114,7 +115,7 @@ const ConfigurationOptions = React.lazy( () => import("./Configurations/ConfigurationPanels/ConfigurationOptions") ); const AddPool = React.lazy( - () => import("./Tenants/TenantDetails/Pools/AddPool") + () => import("./Tenants/TenantDetails/Pools/AddPool/AddPool") ); const styles = (theme: Theme) => @@ -439,6 +440,11 @@ const Console = ({ path: IAM_PAGES.NAMESPACE_TENANT_POOLS_ADD, forceDisplay: true, }, + { + component: EditPool, + path: IAM_PAGES.NAMESPACE_TENANT_POOLS_EDIT, + forceDisplay: true, + }, { component: TenantDetails, path: IAM_PAGES.NAMESPACE_TENANT_VOLUMES, 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 e72bf4887..6cce6ec1f 100644 --- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx +++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/Affinity.tsx @@ -277,7 +277,7 @@ const Affinity = ({ }} selectorOptions={[ { label: "None", value: "none" }, - { label: "Default (Pod Anti-Affinnity)", value: "default" }, + { label: "Default (Pod Anti-Affinity)", value: "default" }, { label: "Node Selector", value: "nodeSelector" }, ]} /> diff --git a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts index eb2ef62ee..dab04036c 100644 --- a/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/ListTenants/types.ts @@ -54,6 +54,9 @@ export interface IPool { volumes: number; label?: string; resources?: IResources; + affinity?: IAffinityModel; + tolerations?: ITolerationModel[]; + securityContext?: ISecurityContext | null; } export interface IPodListElement { @@ -253,3 +256,17 @@ export interface CapacityValue { label: string; color: string; } + +export interface IEditPoolItem { + name: string; + servers: number; + volumes_per_server: number; + volume_configuration: IVolumeConfiguration; + affinity?: IAffinityModel; + tolerations?: ITolerationModel[]; + securityContext?: ISecurityContext | null; +} + +export interface IEditPoolRequest { + pools: IEditPoolItem[] +} \ No newline at end of file diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/AddPool.tsx similarity index 88% rename from portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool.tsx rename to portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/AddPool.tsx index 9fab9fdd8..b1445b06d 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/AddPool.tsx @@ -21,38 +21,38 @@ import withStyles from "@mui/styles/withStyles"; import { formFieldStyles, modalStyleUtils, -} from "../../../Common/FormComponents/common/styleLibrary"; +} from "../../../../Common/FormComponents/common/styleLibrary"; import Grid from "@mui/material/Grid"; -import { generatePoolName, niceBytes } from "../../../../../common/utils"; +import { generatePoolName, niceBytes } from "../../../../../../common/utils"; import { LinearProgress } from "@mui/material"; -import { IAddPoolRequest, ITenant } from "../../ListTenants/types"; -import PageHeader from "../../../Common/PageHeader/PageHeader"; -import PageLayout from "../../../Common/Layout/PageLayout"; -import GenericWizard from "../../../Common/GenericWizard/GenericWizard"; -import { IWizardElement } from "../../../Common/GenericWizard/types"; -import history from "../../../../../history"; +import { IAddPoolRequest, ITenant } from "../../../ListTenants/types"; +import PageHeader from "../../../../Common/PageHeader/PageHeader"; +import PageLayout from "../../../../Common/Layout/PageLayout"; +import GenericWizard from "../../../../Common/GenericWizard/GenericWizard"; +import { IWizardElement } from "../../../../Common/GenericWizard/types"; +import history from "../../../../../../history"; import PoolResources from "./PoolResources"; -import ScreenTitle from "../../../Common/ScreenTitle/ScreenTitle"; -import TenantsIcon from "../../../../../icons/TenantsIcon"; +import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; +import TenantsIcon from "../../../../../../icons/TenantsIcon"; import { isPoolPageValid, resetPoolForm, setPoolField, setTenantDetailsLoad, -} from "../../actions"; -import { AppState } from "../../../../../store"; +} from "../../../actions"; +import { AppState } from "../../../../../../store"; import { connect } from "react-redux"; import PoolConfiguration from "./PoolConfiguration"; import PoolPodPlacement from "./PoolPodPlacement"; import { ErrorResponseHandler, ITolerationModel, -} from "../../../../../common/types"; -import { getDefaultAffinity, getNodeSelector } from "../utils"; -import api from "../../../../../common/api"; -import { ISecurityContext } from "../../types"; -import BackLink from "../../../../../common/BackLink"; -import { setErrorSnackMessage } from "../../../../../actions"; +} from "../../../../../../common/types"; +import { getDefaultAffinity, getNodeSelector } from "../../utils"; +import api from "../../../../../../common/api"; +import { ISecurityContext } from "../../../types"; +import BackLink from "../../../../../../common/BackLink"; +import { setErrorSnackMessage } from "../../../../../../actions"; interface IAddPoolProps { tenant: ITenant | null; @@ -138,6 +138,7 @@ const AddPool = ({ securityContext, volumesPerServer, setTenantDetailsLoad, + setErrorSnackMessage, }: IAddPoolProps) => { const [addSending, setAddSending] = useState(false); @@ -220,6 +221,7 @@ const AddPool = ({ volumeSize, volumesPerServer, withPodAntiAffinity, + setErrorSnackMessage, ]); const cancelButton = { diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolConfiguration.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolConfiguration.tsx similarity index 94% rename from portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolConfiguration.tsx rename to portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolConfiguration.tsx index a066c1b9b..0b421e443 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolConfiguration.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolConfiguration.tsx @@ -24,17 +24,17 @@ import { createTenantCommon, modalBasic, wizardCommon, -} from "../../../Common/FormComponents/common/styleLibrary"; -import { isPoolPageValid, setPoolField } from "../../actions"; -import { AppState } from "../../../../../store"; -import { clearValidationError } from "../../utils"; +} from "../../../../Common/FormComponents/common/styleLibrary"; +import { isPoolPageValid, setPoolField } 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"; -import { ISecurityContext } from "../../types"; +} from "../../../../../../utils/validationFunctions"; +import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; +import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +import { ISecurityContext } from "../../../types"; interface IConfigureProps { setPoolField: typeof setPoolField; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolPodPlacement.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolPodPlacement.tsx similarity index 94% rename from portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolPodPlacement.tsx rename to portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolPodPlacement.tsx index 2d9aa7508..892a5bb0c 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolPodPlacement.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolPodPlacement.tsx @@ -20,7 +20,7 @@ import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; import withStyles from "@mui/styles/withStyles"; import { Grid, IconButton, Paper, SelectChangeEvent } from "@mui/material"; -import { AppState } from "../../../../../store"; +import { AppState } from "../../../../../../store"; import { isPoolPageValid, setPoolField, @@ -28,29 +28,29 @@ import { addNewPoolToleration, removePoolToleration, setPoolKeyValuePairs, -} from "../../actions"; -import { setModalErrorSnackMessage } from "../../../../../actions"; +} from "../../../actions"; +import { setModalErrorSnackMessage } from "../../../../../../actions"; import { modalBasic, wizardCommon, -} from "../../../Common/FormComponents/common/styleLibrary"; +} from "../../../../Common/FormComponents/common/styleLibrary"; import { commonFormValidation, IValidation, -} from "../../../../../utils/validationFunctions"; +} from "../../../../../../utils/validationFunctions"; import { ErrorResponseHandler, ITolerationModel, -} from "../../../../../common/types"; -import { LabelKeyPair } from "../../types"; -import RadioGroupSelector from "../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; -import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; -import api from "../../../../../common/api"; -import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; -import AddIcon from "../../../../../icons/AddIcon"; -import RemoveIcon from "../../../../../icons/RemoveIcon"; -import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper"; -import TolerationSelector from "../../../Common/TolerationSelector/TolerationSelector"; +} from "../../../../../../common/types"; +import { LabelKeyPair } from "../../../types"; +import RadioGroupSelector from "../../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; +import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; +import api from "../../../../../../common/api"; +import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +import AddIcon from "../../../../../../icons/AddIcon"; +import RemoveIcon from "../../../../../../icons/RemoveIcon"; +import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper"; +import TolerationSelector from "../../../../Common/TolerationSelector/TolerationSelector"; interface IAffinityProps { classes: any; @@ -277,7 +277,7 @@ const Affinity = ({ }} selectorOptions={[ { label: "None", value: "none" }, - { label: "Default (Pod Anti-Affinnity)", value: "default" }, + { label: "Default (Pod Anti-Affinity)", value: "default" }, { label: "Node Selector", value: "nodeSelector" }, ]} /> diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolResources.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolResources.tsx similarity index 92% rename from portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolResources.tsx rename to portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolResources.tsx index 2ca061401..46c2a2ef5 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/PoolResources.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/AddPool/PoolResources.tsx @@ -22,28 +22,28 @@ import withStyles from "@mui/styles/withStyles"; import { formFieldStyles, wizardCommon, -} from "../../../Common/FormComponents/common/styleLibrary"; -import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +} from "../../../../Common/FormComponents/common/styleLibrary"; +import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; import Grid from "@mui/material/Grid"; -import { niceBytes } from "../../../../../common/utils"; +import { niceBytes } from "../../../../../../common/utils"; import { Paper, SelectChangeEvent } from "@mui/material"; -import api from "../../../../../common/api"; -import { ITenant } from "../../ListTenants/types"; -import { ErrorResponseHandler } from "../../../../../common/types"; -import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper"; -import { IQuotaElement, IQuotas, Opts } from "../../ListTenants/utils"; -import { AppState } from "../../../../../store"; +import api from "../../../../../../common/api"; +import { ITenant } from "../../../ListTenants/types"; +import { ErrorResponseHandler } from "../../../../../../common/types"; +import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper"; +import { IQuotaElement, IQuotas, Opts } from "../../../ListTenants/utils"; +import { AppState } from "../../../../../../store"; import { connect } from "react-redux"; import { isPoolPageValid, setPoolField, setPoolStorageClasses, -} from "../../actions"; +} from "../../../actions"; import { commonFormValidation, IValidation, -} from "../../../../../utils/validationFunctions"; -import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; +} from "../../../../../../utils/validationFunctions"; +import InputUnitMenu from "../../../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; interface IPoolResourcesProps { tenant: ITenant | null; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/Details/PoolDetails.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/Details/PoolDetails.tsx index 8984d53e3..54defbbea 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/Details/PoolDetails.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/Details/PoolDetails.tsx @@ -34,13 +34,16 @@ import { ITenant } from "../../../ListTenants/types"; import Grid from "@mui/material/Grid"; import LabelValuePair from "../../../../Common/UsageBarWrapper/LabelValuePair"; import { niceBytesInt } from "../../../../../../common/utils"; +import StackRow from "../../../../Common/UsageBarWrapper/StackRow"; +import RBIconButton from "../../../../Buckets/BucketDetails/SummaryItems/RBIconButton"; +import { EditTenantIcon } from "../../../../../../icons"; interface IPoolDetails { classes: any; + history: any; loadingTenant: boolean; tenant: ITenant | null; selectedPool: string | null; - closeDetailsView: () => void; setTenantDetailsLoad: typeof setTenantDetailsLoad; } @@ -53,18 +56,22 @@ const styles = (theme: Theme) => ...containerForHeader(theme.spacing(4)), }); +const stylingLayout = { + border: "#EAEAEA 1px solid", + borderRadius: "3px", + padding: "0px 20px", + position: "relative", +}; + const twoColCssGridLayoutConfig = { display: "grid", gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" }, gridAutoFlow: { xs: "dense", sm: "row" }, gap: 2, + padding: "15px", }; -const PoolDetails = ({ - closeDetailsView, - tenant, - selectedPool, -}: IPoolDetails) => { +const PoolDetails = ({ tenant, selectedPool, history }: IPoolDetails) => { const poolInformation = tenant?.pools.find((pool) => pool.name === selectedPool) || null; @@ -72,17 +79,50 @@ const PoolDetails = ({ return null; } + let affinityType = "None"; + + if (poolInformation.affinity) { + if (poolInformation.affinity.nodeAffinity) { + affinityType = "Node Selector"; + } else { + affinityType = "Default (Pod Anti-Affinity)"; + } + } + + const HeaderSection = ({ title }: { title: string }) => { + return ( + +

{title}

+
+ ); + }; + return ( - - - -

Pool Configuration

-
- - + +
+ } + onClick={() => { + history.push( + `/namespaces/${tenant?.namespace || ""}/tenants/${ + tenant?.name || "" + }/edit-pool` + ); + }} + text={"Edit Pool"} + id={"editPool"} + /> +
+ - + - - -

Resources

-
- - + {poolInformation.resources && ( @@ -121,6 +156,123 @@ const PoolDetails = ({ value={poolInformation.volume_configuration.storage_class_name} /> + {poolInformation.securityContext && + (poolInformation.securityContext.runAsNonRoot || + poolInformation.securityContext.runAsUser || + poolInformation.securityContext.runAsGroup || + poolInformation.securityContext.fsGroup) && ( + + + + {poolInformation.securityContext.runAsNonRoot !== null && ( + + + + )} + + {poolInformation.securityContext.runAsUser && ( + + )} + {poolInformation.securityContext.runAsGroup && ( + + )} + {poolInformation.securityContext.fsGroup && ( + + )} + + + + )} + + + + + {poolInformation.affinity?.nodeAffinity && + poolInformation.affinity?.podAntiAffinity ? ( + + ) : ( + + )} + + {poolInformation.affinity?.nodeAffinity && ( + + +
    + {poolInformation.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.map( + (term) => { + return term.matchExpressions.map((trm) => { + return ( +
  • + {trm.key} - {trm.values.join(", ")} +
  • + ); + }); + } + )} +
+
+ )} +
+ {poolInformation.tolerations && poolInformation.tolerations.length > 0 && ( + + + +
    + {poolInformation.tolerations.map((tolItem) => { + return ( +
  • + {tolItem.operator === "Equal" ? ( + + If {tolItem.key} is equal to{" "} + {tolItem.value} then{" "} + {tolItem.effect} after{" "} + + {tolItem.tolerationSeconds?.seconds || 0} + {" "} + seconds + + ) : ( + + If {tolItem.key} exists then{" "} + {tolItem.effect} after{" "} + + {tolItem.tolerationSeconds?.seconds || 0} + {" "} + seconds + + )} +
  • + ); + })} +
+
+
+ )}
); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx new file mode 100644 index 000000000..4bb05723a --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx @@ -0,0 +1,390 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 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, useEffect, useState } from "react"; +import { connect } from "react-redux"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import Grid from "@mui/material/Grid"; +import PageHeader from "../../../../Common/PageHeader/PageHeader"; +import PageLayout from "../../../../Common/Layout/PageLayout"; +import GenericWizard from "../../../../Common/GenericWizard/GenericWizard"; +import api from "../../../../../../common/api"; +import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle"; +import TenantsIcon from "../../../../../../icons/TenantsIcon"; +import BackLink from "../../../../../../common/BackLink"; +import EditPoolResources from "./EditPoolResources"; +import EditPoolConfiguration from "./EditPoolConfiguration"; +import EditPoolPlacement from "./EditPoolPlacement"; +import history from "../../../../../../history"; +import { IWizardElement } from "../../../../Common/GenericWizard/types"; +import { LinearProgress } from "@mui/material"; +import { generatePoolName, niceBytes } from "../../../../../../common/utils"; +import { + formFieldStyles, + modalStyleUtils, +} from "../../../../Common/FormComponents/common/styleLibrary"; +import { + IEditPoolItem, + IEditPoolRequest, + ITenant, +} from "../../../ListTenants/types"; +import { + isPoolPageValid, + resetPoolForm, + setInitialPoolDetails, + setPoolField, + setTenantDetailsLoad, +} from "../../../actions"; +import { AppState } from "../../../../../../store"; +import { + ErrorResponseHandler, + ITolerationModel, +} from "../../../../../../common/types"; +import { getDefaultAffinity, getNodeSelector } from "../../utils"; +import { ISecurityContext } from "../../../types"; +import { setErrorSnackMessage } from "../../../../../../actions"; + +interface IEditPoolProps { + tenant: ITenant | null; + classes: any; + open: boolean; + match: any; + selectedStorageClass: string; + selectedPool: string | null; + validPages: string[]; + numberOfNodes: number; + volumeSize: number; + volumesPerServer: number; + affinityType: string; + nodeSelectorLabels: string; + withPodAntiAffinity: boolean; + securityContextEnabled: boolean; + tolerations: ITolerationModel[]; + securityContext: ISecurityContext; + resetPoolForm: typeof resetPoolForm; + setErrorSnackMessage: typeof setErrorSnackMessage; + setTenantDetailsLoad: typeof setTenantDetailsLoad; + setInitialPoolDetails: typeof setInitialPoolDetails; +} + +const styles = (theme: Theme) => + createStyles({ + buttonContainer: { + textAlign: "right", + }, + bottomContainer: { + display: "flex", + flexGrow: 1, + alignItems: "center", + margin: "auto", + justifyContent: "center", + "& div": { + width: 150, + "@media (max-width: 900px)": { + flexFlow: "column", + }, + }, + }, + factorElements: { + display: "flex", + justifyContent: "flex-start", + marginLeft: 30, + }, + sizeNumber: { + fontSize: 35, + fontWeight: 700, + textAlign: "center", + }, + sizeDescription: { + fontSize: 14, + color: "#777", + textAlign: "center", + }, + pageBox: { + border: "1px solid #EAEAEA", + borderTop: 0, + }, + editPoolTitle: { + border: "1px solid #EAEAEA", + borderBottom: 0, + }, + ...formFieldStyles, + ...modalStyleUtils, + }); + +const requiredPages = ["setup", "affinity", "configure"]; + +const EditPool = ({ + tenant, + classes, + resetPoolForm, + selectedPool, + selectedStorageClass, + validPages, + numberOfNodes, + volumeSize, + affinityType, + nodeSelectorLabels, + withPodAntiAffinity, + tolerations, + securityContextEnabled, + securityContext, + volumesPerServer, + setTenantDetailsLoad, + setInitialPoolDetails, + setErrorSnackMessage, +}: IEditPoolProps) => { + const [editSending, setEditSending] = useState(false); + + const poolsURL = `/namespaces/${tenant?.namespace || ""}/tenants/${ + tenant?.name || "" + }/pools`; + + useEffect(() => { + if (selectedPool) { + const poolDetails = tenant?.pools.find( + (pool) => pool.name === selectedPool + ); + + if (poolDetails) { + setInitialPoolDetails(poolDetails); + } else { + history.push("/tenants"); + } + } + }, [selectedPool, setInitialPoolDetails, tenant]); + + useEffect(() => { + if (editSending && tenant) { + const poolName = generatePoolName(tenant.pools); + + let affinityObject = {}; + + switch (affinityType) { + case "default": + affinityObject = { + affinity: getDefaultAffinity(tenant.name, poolName), + }; + break; + case "nodeSelector": + affinityObject = { + affinity: getNodeSelector( + nodeSelectorLabels, + withPodAntiAffinity, + tenant.name, + poolName + ), + }; + break; + } + + const tolerationValues = tolerations.filter( + (toleration) => toleration.key.trim() !== "" + ); + + const cleanPools = tenant.pools + .filter((pool) => pool.name !== selectedPool) + .map((pool) => { + let securityContextOption = null; + + if (pool.securityContext) { + if ( + !!pool.securityContext.runAsUser || + !!pool.securityContext.runAsGroup || + !!pool.securityContext.fsGroup + ) { + securityContextOption = { ...pool.securityContext }; + } + } + + const request: IEditPoolItem = { + ...pool, + securityContext: securityContextOption, + }; + + return request; + }); + + const data: IEditPoolRequest = { + pools: [ + ...cleanPools, + { + name: selectedPool || poolName, + servers: numberOfNodes, + volumes_per_server: volumesPerServer, + volume_configuration: { + size: volumeSize * 1073741824, + storage_class_name: selectedStorageClass, + labels: null, + }, + tolerations: tolerationValues, + securityContext: securityContextEnabled ? securityContext : null, + ...affinityObject, + }, + ], + }; + + api + .invoke( + "PUT", + `/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/pools`, + data + ) + .then(() => { + setEditSending(false); + resetPoolForm(); + setTenantDetailsLoad(true); + history.push(poolsURL); + }) + .catch((err: ErrorResponseHandler) => { + setEditSending(false); + setErrorSnackMessage(err); + }); + } + }, [ + selectedPool, + setErrorSnackMessage, + editSending, + poolsURL, + resetPoolForm, + setTenantDetailsLoad, + affinityType, + nodeSelectorLabels, + numberOfNodes, + securityContext, + securityContextEnabled, + selectedStorageClass, + tenant, + tolerations, + volumeSize, + volumesPerServer, + withPodAntiAffinity, + ]); + + const cancelButton = { + label: "Cancel", + type: "other", + enabled: true, + action: () => { + resetPoolForm(); + history.push(poolsURL); + }, + }; + + const createButton = { + label: "Update", + type: "submit", + enabled: + !editSending && + selectedStorageClass !== "" && + requiredPages.every((v) => validPages.includes(v)), + action: () => { + setEditSending(true); + }, + }; + + const wizardSteps: IWizardElement[] = [ + { + label: "Pool Resources", + componentRender: , + buttons: [cancelButton, createButton], + }, + { + label: "Configuration", + advancedOnly: true, + componentRender: , + buttons: [cancelButton, createButton], + }, + { + label: "Pod Placement", + advancedOnly: true, + componentRender: , + buttons: [cancelButton, createButton], + }, + ]; + + return ( + + + + + + } + /> + + + } + title={`Edit Pool - ${selectedPool}`} + subTitle={ + + Namespace: {tenant?.namespace || ""} / Current Capacity:{" "} + {niceBytes((tenant?.total_size || 0).toString(10))} / Tenant:{" "} + {tenant?.name || ""} + + } + /> + + + {editSending && ( + + + + )} + + + + + + + ); +}; + +const mapState = (state: AppState) => { + const editPool = state.tenants.editPool; + return { + tenant: state.tenants.tenantDetails.tenantInfo, + selectedPool: state.tenants.tenantDetails.selectedPool, + selectedStorageClass: editPool.fields.setup.storageClass, + validPages: editPool.validPages, + storageClasses: editPool.storageClasses, + numberOfNodes: editPool.fields.setup.numberOfNodes, + volumeSize: editPool.fields.setup.volumeSize, + volumesPerServer: editPool.fields.setup.volumesPerServer, + affinityType: editPool.fields.affinity.podAffinity, + nodeSelectorLabels: editPool.fields.affinity.nodeSelectorLabels, + withPodAntiAffinity: editPool.fields.affinity.withPodAntiAffinity, + tolerations: editPool.fields.tolerations, + securityContextEnabled: + editPool.fields.configuration.securityContextEnabled, + securityContext: editPool.fields.configuration.securityContext, + }; +}; + +const connector = connect(mapState, { + resetPoolForm, + setPoolField, + isPoolPageValid, + setErrorSnackMessage, + setTenantDetailsLoad, + setInitialPoolDetails, +}); + +export default withStyles(styles)(connector(EditPool)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx new file mode 100644 index 000000000..74ffe21b8 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx @@ -0,0 +1,289 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 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, { useCallback, useEffect, useState } from "react"; +import { connect } from "react-redux"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import { Grid, Paper } from "@mui/material"; +import { + createTenantCommon, + modalBasic, + wizardCommon, +} from "../../../../Common/FormComponents/common/styleLibrary"; +import { isEditPoolPageValid, setEditPoolField } 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"; +import { ISecurityContext } from "../../../types"; + +interface IConfigureProps { + setEditPoolField: typeof setEditPoolField; + isEditPoolPageValid: typeof isEditPoolPageValid; + classes: any; + securityContextEnabled: boolean; + securityContext: ISecurityContext; +} + +const styles = (theme: Theme) => + createStyles({ + configSectionItem: { + marginRight: 15, + + "& .multiContainer": { + border: "1px solid red", + }, + }, + tenantCustomizationFields: { + marginLeft: 30, // 2nd Level(15+15) + width: "88%", + margin: "auto", + }, + containerItem: { + marginRight: 15, + }, + fieldGroup: { + ...createTenantCommon.fieldGroup, + paddingTop: 15, + marginBottom: 25, + }, + responsiveSectionItem: { + "@media (max-width: 900px)": { + flexFlow: "column", + alignItems: "flex-start", + + "& div > div": { + marginBottom: 5, + marginRight: 0, + }, + }, + }, + + fieldSpaceTop: { + marginTop: 15, + }, + + ...modalBasic, + ...wizardCommon, + }); + +const PoolConfiguration = ({ + classes, + setEditPoolField, + securityContextEnabled, + isEditPoolPageValid, + securityContext, +}: IConfigureProps) => { + const [validationErrors, setValidationErrors] = useState({}); + + // Common + const updateField = useCallback( + (field: string, value: any) => { + setEditPoolField("configuration", field, value); + }, + [setEditPoolField] + ); + + // Validation + useEffect(() => { + let customAccountValidation: IValidation[] = []; + if (securityContextEnabled) { + customAccountValidation = [ + ...customAccountValidation, + { + fieldKey: "pool_securityContext_runAsUser", + required: true, + value: securityContext.runAsUser, + customValidation: + securityContext.runAsUser === "" || + parseInt(securityContext.runAsUser) < 0, + customValidationMessage: `runAsUser must be present and be 0 or more`, + }, + { + fieldKey: "pool_securityContext_runAsGroup", + required: true, + value: securityContext.runAsGroup, + customValidation: + securityContext.runAsGroup === "" || + parseInt(securityContext.runAsGroup) < 0, + customValidationMessage: `runAsGroup must be present and be 0 or more`, + }, + { + fieldKey: "pool_securityContext_fsGroup", + required: true, + value: securityContext.fsGroup, + customValidation: + securityContext.fsGroup === "" || + parseInt(securityContext.fsGroup) < 0, + customValidationMessage: `fsGroup must be present and be 0 or more`, + }, + ]; + } + + const commonVal = commonFormValidation(customAccountValidation); + + isEditPoolPageValid("configure", Object.keys(commonVal).length === 0); + + setValidationErrors(commonVal); + }, [isEditPoolPageValid, securityContextEnabled, securityContext]); + + const cleanValidation = (fieldName: string) => { + setValidationErrors(clearValidationError(validationErrors, fieldName)); + }; + + return ( + +
+

Configure

+
+ + { + const targetD = e.target; + const checked = targetD.checked; + + updateField("securityContextEnabled", checked); + }} + label={"Security Context"} + /> + + {securityContextEnabled && ( + +
+ + Pool's Security Context + + +
+
+ ) => { + updateField("securityContext", { + ...securityContext, + runAsUser: e.target.value, + }); + cleanValidation("pool_securityContext_runAsUser"); + }} + label="Run As User" + value={securityContext.runAsUser} + required + error={ + validationErrors["pool_securityContext_runAsUser"] || "" + } + min="0" + /> +
+
+ ) => { + updateField("securityContext", { + ...securityContext, + runAsGroup: e.target.value, + }); + cleanValidation("pool_securityContext_runAsGroup"); + }} + label="Run As Group" + value={securityContext.runAsGroup} + required + error={ + validationErrors["pool_securityContext_runAsGroup"] || "" + } + min="0" + /> +
+
+ ) => { + updateField("securityContext", { + ...securityContext, + fsGroup: e.target.value, + }); + cleanValidation("pool_securityContext_fsGroup"); + }} + label="FsGroup" + value={securityContext.fsGroup} + required + error={ + validationErrors["pool_securityContext_fsGroup"] || "" + } + min="0" + /> +
+
+
+
+ +
+ { + const targetD = e.target; + const checked = targetD.checked; + updateField("securityContext", { + ...securityContext, + runAsNonRoot: checked, + }); + }} + label={"Do not run as Root"} + /> +
+
+
+
+ )} +
+ ); +}; + +const mapState = (state: AppState) => { + const configuration = state.tenants.editPool.fields.configuration; + + return { + securityContextEnabled: configuration.securityContextEnabled, + securityContext: configuration.securityContext, + }; +}; + +const connector = connect(mapState, { + setEditPoolField, + isEditPoolPageValid, +}); + +export default withStyles(styles)(connector(PoolConfiguration)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx new file mode 100644 index 000000000..063eb3cb6 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx @@ -0,0 +1,534 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 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 { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import { Grid, IconButton, Paper, SelectChangeEvent } from "@mui/material"; +import { AppState } from "../../../../../../store"; +import { + setEditPoolField, + isEditPoolPageValid, + setEditPoolKeyValuePairs, + setEditPoolTolerationInfo, + addNewEditPoolToleration, + removeEditPoolToleration, +} from "../../../actions"; +import { setModalErrorSnackMessage } from "../../../../../../actions"; +import { + modalBasic, + wizardCommon, +} from "../../../../Common/FormComponents/common/styleLibrary"; +import { + commonFormValidation, + IValidation, +} from "../../../../../../utils/validationFunctions"; +import { + ErrorResponseHandler, + ITolerationModel, +} from "../../../../../../common/types"; +import { LabelKeyPair } from "../../../types"; +import RadioGroupSelector from "../../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector"; +import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; +import api from "../../../../../../common/api"; +import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +import AddIcon from "../../../../../../icons/AddIcon"; +import RemoveIcon from "../../../../../../icons/RemoveIcon"; +import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper"; +import TolerationSelector from "../../../../Common/TolerationSelector/TolerationSelector"; + +interface IAffinityProps { + classes: any; + podAffinity: string; + nodeSelectorLabels: string; + withPodAntiAffinity: boolean; + keyValuePairs: LabelKeyPair[]; + tolerations: ITolerationModel[]; + setModalErrorSnackMessage: typeof setModalErrorSnackMessage; + setEditPoolField: typeof setEditPoolField; + isEditPoolPageValid: typeof isEditPoolPageValid; + setEditPoolKeyValuePairs: typeof setEditPoolKeyValuePairs; + setEditPoolTolerationInfo: typeof setEditPoolTolerationInfo; + removeEditPoolToleration: typeof removeEditPoolToleration; + addNewEditPoolToleration: typeof addNewEditPoolToleration; +} + +const styles = (theme: Theme) => + createStyles({ + overlayAction: { + marginLeft: 10, + display: "flex", + alignItems: "center", + "& svg": { + maxWidth: 15, + maxHeight: 15, + }, + "& button": { + background: "#EAEAEA", + }, + }, + affinityConfigField: { + display: "flex", + }, + affinityFieldLabel: { + display: "flex", + flexFlow: "column", + flex: 1, + }, + radioField: { + display: "flex", + alignItems: "flex-start", + marginTop: 10, + "& div:first-child": { + display: "flex", + flexFlow: "column", + alignItems: "baseline", + textAlign: "left !important", + }, + }, + affinityLabelKey: { + "& div:first-child": { + marginBottom: 0, + }, + }, + affinityLabelValue: { + marginLeft: 10, + "& div:first-child": { + marginBottom: 0, + }, + }, + rowActions: { + display: "flex", + alignItems: "center", + }, + fieldContainer: { + marginBottom: 0, + }, + affinityRow: { + marginBottom: 10, + display: "flex", + }, + ...modalBasic, + ...wizardCommon, + }); + +interface OptionPair { + label: string; + value: string; +} + +const Affinity = ({ + classes, + podAffinity, + nodeSelectorLabels, + withPodAntiAffinity, + setModalErrorSnackMessage, + keyValuePairs, + setEditPoolField, + isEditPoolPageValid, + setEditPoolKeyValuePairs, + setEditPoolTolerationInfo, + addNewEditPoolToleration, + removeEditPoolToleration, + tolerations, +}: IAffinityProps) => { + const [validationErrors, setValidationErrors] = useState({}); + const [loading, setLoading] = useState(true); + const [keyValueMap, setKeyValueMap] = useState<{ [key: string]: string[] }>( + {} + ); + const [keyOptions, setKeyOptions] = useState([]); + + // Common + const updateField = useCallback( + (field: string, value: any) => { + setEditPoolField("affinity", field, value); + }, + [setEditPoolField] + ); + + useEffect(() => { + if (loading) { + api + .invoke("GET", `/api/v1/nodes/labels`) + .then((res: { [key: string]: string[] }) => { + setLoading(false); + setKeyValueMap(res); + let keys: OptionPair[] = []; + for (let k in res) { + keys.push({ + label: k, + value: k, + }); + } + setKeyOptions(keys); + }) + .catch((err: ErrorResponseHandler) => { + setLoading(false); + setModalErrorSnackMessage(err); + setKeyValueMap({}); + }); + } + }, [setModalErrorSnackMessage, loading]); + + useEffect(() => { + if (keyValuePairs) { + const vlr = keyValuePairs + .filter((kvp) => kvp.key !== "") + .map((kvp) => `${kvp.key}=${kvp.value}`) + .filter((kvs, i, a) => a.indexOf(kvs) === i); + const vl = vlr.join("&"); + updateField("nodeSelectorLabels", vl); + } + }, [keyValuePairs, updateField]); + + // Validation + useEffect(() => { + let customAccountValidation: IValidation[] = []; + + if (podAffinity === "nodeSelector") { + let valid = true; + + const splittedLabels = nodeSelectorLabels.split("&"); + + if (splittedLabels.length === 1 && splittedLabels[0] === "") { + valid = false; + } + + splittedLabels.forEach((item: string, index: number) => { + const splitItem = item.split("="); + + if (splitItem.length !== 2) { + valid = false; + } + + if (index + 1 !== splittedLabels.length) { + if (splitItem[0] === "" || splitItem[1] === "") { + valid = false; + } + } + }); + + customAccountValidation = [ + ...customAccountValidation, + { + fieldKey: "labels", + required: true, + value: nodeSelectorLabels, + customValidation: !valid, + customValidationMessage: + "You need to add at least one label key-pair", + }, + ]; + } + + const commonVal = commonFormValidation(customAccountValidation); + + isEditPoolPageValid("affinity", Object.keys(commonVal).length === 0); + + setValidationErrors(commonVal); + }, [isEditPoolPageValid, podAffinity, nodeSelectorLabels]); + + const updateToleration = (index: number, field: string, value: any) => { + const alterToleration = { ...tolerations[index], [field]: value }; + + setEditPoolTolerationInfo(index, alterToleration); + }; + + return ( + +
+

Pod Placement

+
+ + +
Type
+
+ MinIO supports multiple configurations for Pod Affinity +
+ + { + updateField("podAffinity", e.target.value); + }} + selectorOptions={[ + { label: "None", value: "none" }, + { label: "Default (Pod Anti-Affinity)", value: "default" }, + { label: "Node Selector", value: "nodeSelector" }, + ]} + /> + +
+
+ {podAffinity === "nodeSelector" && ( + +
+ + { + const targetD = e.target; + const checked = targetD.checked; + + updateField("withPodAntiAffinity", checked); + }} + label={"With Pod Anti-Affinity"} + /> + + +

Labels

+ {validationErrors["labels"]} + + {keyValuePairs && + keyValuePairs.map((kvp, i) => { + return ( + + + {keyOptions.length > 0 && ( + ) => { + const newKey = e.target.value as string; + const arrCp: LabelKeyPair[] = Object.assign( + [], + keyValuePairs + ); + + arrCp[i].key = e.target.value as string; + arrCp[i].value = keyValueMap[newKey][0]; + setEditPoolKeyValuePairs(arrCp); + }} + id="select-access-policy" + name="select-access-policy" + label={""} + value={kvp.key} + options={keyOptions} + /> + )} + {keyOptions.length === 0 && ( + { + const arrCp: LabelKeyPair[] = Object.assign( + [], + keyValuePairs + ); + arrCp[i].key = e.target.value; + setEditPoolKeyValuePairs(arrCp); + }} + index={i} + placeholder={"Key"} + /> + )} + + + {keyOptions.length > 0 && ( + ) => { + const arrCp: LabelKeyPair[] = Object.assign( + [], + keyValuePairs + ); + arrCp[i].value = e.target.value as string; + setEditPoolKeyValuePairs(arrCp); + }} + id="select-access-policy" + name="select-access-policy" + label={""} + value={kvp.value} + options={ + keyValueMap[kvp.key] + ? keyValueMap[kvp.key].map((v) => { + return { label: v, value: v }; + }) + : [] + } + /> + )} + {keyOptions.length === 0 && ( + { + const arrCp: LabelKeyPair[] = Object.assign( + [], + keyValuePairs + ); + arrCp[i].value = e.target.value; + setEditPoolKeyValuePairs(arrCp); + }} + index={i} + placeholder={"value"} + /> + )} + + +
+ { + const arrCp = Object.assign([], keyValuePairs); + if (keyOptions.length > 0) { + arrCp.push({ + key: keyOptions[0].value, + value: keyValueMap[keyOptions[0].value][0], + }); + } else { + arrCp.push({ key: "", value: "" }); + } + + setEditPoolKeyValuePairs(arrCp); + }} + > + + +
+ {keyValuePairs.length > 1 && ( +
+ { + const arrCp = keyValuePairs.filter( + (item, index) => index !== i + ); + setEditPoolKeyValuePairs(arrCp); + }} + > + + +
+ )} +
+
+ ); + })} +
+
+
+ )} + + +

Tolerations

+ + {validationErrors["tolerations"]} + + + {tolerations && + tolerations.map((tol, i) => { + return ( + + { + updateToleration(i, "effect", value); + }} + tolerationKey={tol.key} + onTolerationKeyChange={(value) => { + updateToleration(i, "key", value); + }} + operator={tol.operator} + onOperatorChange={(value) => { + updateToleration(i, "operator", value); + }} + value={tol.value} + onValueChange={(value) => { + updateToleration(i, "value", value); + }} + tolerationSeconds={tol.tolerationSeconds?.seconds || 0} + onSecondsChange={(value) => { + updateToleration(i, "tolerationSeconds", { + seconds: value, + }); + }} + index={i} + /> +
+ + + +
+ +
+ removeEditPoolToleration(i)} + disabled={tolerations.length <= 1} + > + + +
+
+ ); + })} +
+
+
+
+ ); +}; + +const mapState = (state: AppState) => { + const editPool = state.tenants.editPool; + + return { + podAffinity: editPool.fields.affinity.podAffinity, + nodeSelectorLabels: editPool.fields.affinity.nodeSelectorLabels, + withPodAntiAffinity: editPool.fields.affinity.withPodAntiAffinity, + keyValuePairs: editPool.fields.nodeSelectorPairs, + tolerations: editPool.fields.tolerations, + }; +}; + +const connector = connect(mapState, { + setModalErrorSnackMessage, + setEditPoolField, + isEditPoolPageValid, + setEditPoolKeyValuePairs, + setEditPoolTolerationInfo, + addNewEditPoolToleration, + removeEditPoolToleration, +}); + +export default withStyles(styles)(connector(Affinity)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx new file mode 100644 index 000000000..704f675db --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx @@ -0,0 +1,313 @@ +// This file is part of MinIO Console Server +// Copyright (c) 2022 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, { useEffect, useState } from "react"; +import get from "lodash/get"; +import { Theme } from "@mui/material/styles"; +import createStyles from "@mui/styles/createStyles"; +import withStyles from "@mui/styles/withStyles"; +import { + formFieldStyles, + wizardCommon, +} from "../../../../Common/FormComponents/common/styleLibrary"; +import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; +import Grid from "@mui/material/Grid"; +import { niceBytes } from "../../../../../../common/utils"; +import { Paper, SelectChangeEvent } from "@mui/material"; +import api from "../../../../../../common/api"; +import { ITenant } from "../../../ListTenants/types"; +import { ErrorResponseHandler } from "../../../../../../common/types"; +import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper"; +import { IQuotaElement, IQuotas, Opts } from "../../../ListTenants/utils"; +import { AppState } from "../../../../../../store"; +import { connect } from "react-redux"; +import { + setEditPoolField, + isEditPoolPageValid, + setEditPoolStorageClasses, +} from "../../../actions"; +import { + commonFormValidation, + IValidation, +} from "../../../../../../utils/validationFunctions"; +import InputUnitMenu from "../../../../Common/FormComponents/InputUnitMenu/InputUnitMenu"; + +interface IPoolResourcesProps { + tenant: ITenant | null; + classes: any; + storageClasses: Opts[]; + numberOfNodes: string; + storageClass: string; + volumeSize: string; + volumesPerServer: string; + setEditPoolField: typeof setEditPoolField; + isEditPoolPageValid: typeof isEditPoolPageValid; + setEditPoolStorageClasses: typeof setEditPoolStorageClasses; +} + +const styles = (theme: Theme) => + createStyles({ + buttonContainer: { + textAlign: "right", + }, + bottomContainer: { + display: "flex", + flexGrow: 1, + alignItems: "center", + margin: "auto", + justifyContent: "center", + "& div": { + width: 150, + "@media (max-width: 900px)": { + flexFlow: "column", + }, + }, + }, + factorElements: { + display: "flex", + justifyContent: "flex-start", + marginLeft: 30, + }, + sizeNumber: { + fontSize: 35, + fontWeight: 700, + textAlign: "center", + }, + sizeDescription: { + fontSize: 14, + color: "#777", + textAlign: "center", + }, + ...formFieldStyles, + ...wizardCommon, + }); + +const PoolResources = ({ + tenant, + classes, + storageClasses, + numberOfNodes, + storageClass, + volumeSize, + volumesPerServer, + setEditPoolField, + setEditPoolStorageClasses, + isEditPoolPageValid, +}: IPoolResourcesProps) => { + const [validationErrors, setValidationErrors] = useState({}); + + const instanceCapacity: number = + parseInt(volumeSize) * 1073741824 * parseInt(volumesPerServer); + const totalCapacity: number = instanceCapacity * parseInt(numberOfNodes); + + // Validation + useEffect(() => { + let customAccountValidation: IValidation[] = [ + { + fieldKey: "number_of_nodes", + required: true, + value: numberOfNodes.toString(), + customValidation: + parseInt(numberOfNodes) < 1 || isNaN(parseInt(numberOfNodes)), + customValidationMessage: "Number of servers must be at least 1", + }, + { + fieldKey: "pool_size", + required: true, + value: volumeSize.toString(), + customValidation: + parseInt(volumeSize) < 1 || isNaN(parseInt(volumeSize)), + customValidationMessage: "Pool Size cannot be 0", + }, + { + fieldKey: "volumes_per_server", + required: true, + value: volumesPerServer.toString(), + customValidation: + parseInt(volumesPerServer) < 1 || isNaN(parseInt(volumesPerServer)), + customValidationMessage: "1 volume or more are required", + }, + ]; + + const commonVal = commonFormValidation(customAccountValidation); + + isEditPoolPageValid("setup", Object.keys(commonVal).length === 0); + + setValidationErrors(commonVal); + }, [ + isEditPoolPageValid, + numberOfNodes, + volumeSize, + volumesPerServer, + storageClass, + ]); + + useEffect(() => { + if (storageClasses.length === 0 && tenant) { + api + .invoke( + "GET", + `/api/v1/namespaces/${tenant.namespace}/resourcequotas/${tenant.namespace}-storagequota` + ) + .then((res: IQuotas) => { + const elements: IQuotaElement[] = get(res, "elements", []); + + const newStorage = elements.map((storageClass: any) => { + const name = get(storageClass, "name", "").split( + ".storageclass.storage.k8s.io/requests.storage" + )[0]; + + return { label: name, value: name }; + }); + + setEditPoolField("setup", "storageClass", newStorage[0].value); + + setEditPoolStorageClasses(newStorage); + }) + .catch((err: ErrorResponseHandler) => { + console.error(err); + }); + } + }, [tenant, storageClasses, setEditPoolStorageClasses, setEditPoolField]); + + const setFieldInfo = (fieldName: string, value: any) => { + setEditPoolField("setup", fieldName, value); + }; + + return ( + +
+

Pool Resources

+
+ + + ) => { + const intValue = parseInt(e.target.value); + + if (e.target.validity.valid && !isNaN(intValue)) { + setFieldInfo("numberOfNodes", intValue); + } else if (isNaN(intValue)) { + setFieldInfo("numberOfNodes", 0); + } + }} + label="Number of Servers" + value={numberOfNodes} + error={validationErrors["number_of_nodes"] || ""} + pattern={"[0-9]*"} + /> + + + ) => { + const intValue = parseInt(e.target.value); + + if (e.target.validity.valid && !isNaN(intValue)) { + setFieldInfo("volumeSize", intValue); + } else if (isNaN(intValue)) { + setFieldInfo("volumeSize", 0); + } + }} + label="Volume Size" + value={volumeSize} + error={validationErrors["pool_size"] || ""} + pattern={"[0-9]*"} + overlayObject={ + {}} + unitSelected={"Gi"} + unitsList={[{ label: "Gi", value: "Gi" }]} + disabled={true} + /> + } + /> + + + ) => { + const intValue = parseInt(e.target.value); + + if (e.target.validity.valid && !isNaN(intValue)) { + setFieldInfo("volumesPerServer", intValue); + } else if (isNaN(intValue)) { + setFieldInfo("volumesPerServer", 0); + } + }} + label="Volumes per Server" + value={volumesPerServer} + error={validationErrors["volumes_per_server"] || ""} + pattern={"[0-9]*"} + /> + + + ) => { + setFieldInfo("storageClasses", e.target.value as string); + }} + label="Storage Class" + value={storageClass} + options={storageClasses} + disabled={storageClasses.length < 1} + /> + + +
+
+
+ {niceBytes(instanceCapacity.toString(10))} +
+
Instance Capacity
+
+
+
+ {niceBytes(totalCapacity.toString(10))} +
+
Total Capacity
+
+
+
+
+ ); +}; + +const mapState = (state: AppState) => { + const setupFields = state.tenants.editPool.fields.setup; + return { + tenant: state.tenants.tenantDetails.tenantInfo, + storageClasses: state.tenants.editPool.storageClasses, + numberOfNodes: setupFields.numberOfNodes.toString(), + storageClass: setupFields.storageClass, + volumeSize: setupFields.volumeSize.toString(), + volumesPerServer: setupFields.volumesPerServer.toString(), + }; +}; + +const connector = connect(mapState, { + setEditPoolField, + isEditPoolPageValid, + setEditPoolStorageClasses, +}); + +export default withStyles(styles)(connector(PoolResources)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/PoolsSummary.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/PoolsSummary.tsx index 9b2661461..b648012a6 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/PoolsSummary.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/PoolsSummary.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, { Fragment } from "react"; import { connect } from "react-redux"; import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; @@ -28,7 +28,11 @@ import { import Grid from "@mui/material/Grid"; import { setErrorSnackMessage } from "../../../../actions"; import { AppState } from "../../../../store"; -import { setSelectedPool, setTenantDetailsLoad } from "../actions"; +import { + setOpenPoolDetails, + setSelectedPool, + setTenantDetailsLoad, +} from "../actions"; import PoolsListing from "./Pools/Details/PoolsListing"; import PoolDetails from "./Pools/Details/PoolDetails"; import BackLink from "../../../../common/BackLink"; @@ -39,9 +43,11 @@ interface IPoolsSummary { history: any; match: any; selectedPool: string | null; + poolDetailsOpen: boolean; setErrorSnackMessage: typeof setErrorSnackMessage; setTenantDetailsLoad: typeof setTenantDetailsLoad; setSelectedPool: typeof setSelectedPool; + setOpenPoolDetails: typeof setOpenPoolDetails; } const styles = (theme: Theme) => @@ -57,15 +63,16 @@ const PoolsSummary = ({ history, selectedPool, match, + poolDetailsOpen, + setOpenPoolDetails, }: IPoolsSummary) => { - const [poolDetailsOpen, setPoolDetailsOpen] = useState(false); return ( {poolDetailsOpen && ( { - setPoolDetailsOpen(false); + setOpenPoolDetails(false); }} label={"Back to Pools list"} to={match.url} @@ -77,15 +84,11 @@ const PoolsSummary = ({ {poolDetailsOpen ? ( - { - setPoolDetailsOpen(false); - }} - /> + ) : ( { - setPoolDetailsOpen(true); + setOpenPoolDetails(true); }} history={history} /> @@ -100,12 +103,14 @@ const mapState = (state: AppState) => ({ selectedTenant: state.tenants.tenantDetails.currentTenant, selectedPool: state.tenants.tenantDetails.selectedPool, tenant: state.tenants.tenantDetails.tenantInfo, + poolDetailsOpen: state.tenants.tenantDetails.poolDetailsOpen, }); const connector = connect(mapState, { setErrorSnackMessage, setTenantDetailsLoad, setSelectedPool, + setOpenPoolDetails, }); export default withStyles(styles)(connector(PoolsSummary)); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/utils.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/utils.ts index daeaf6cb1..fab382051 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/utils.ts +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/utils.ts @@ -78,6 +78,5 @@ export const getNodeSelector = ( const def = getDefaultAffinity(tenantName, poolName); nodeSelector.podAntiAffinity = def.podAntiAffinity; } - console.log(nodeSelector); return nodeSelector; }; diff --git a/portal-ui/src/screens/Console/Tenants/actions.ts b/portal-ui/src/screens/Console/Tenants/actions.ts index fbb514aee..a47d40fdf 100644 --- a/portal-ui/src/screens/Console/Tenants/actions.ts +++ b/portal-ui/src/screens/Console/Tenants/actions.ts @@ -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 { ITenant } from "./ListTenants/types"; +import { IPool, ITenant } from "./ListTenants/types"; import { Opts } from "./ListTenants/utils"; import { ADD_TENANT_ADD_CA_KEYPAIR, @@ -33,7 +33,6 @@ import { ADD_TENANT_ENCRYPTION_VAULT_CA, ADD_TENANT_ENCRYPTION_VAULT_CERT, ADD_TENANT_RESET_FORM, - ADD_TENANT_SET_CURRENT_PAGE, ADD_TENANT_SET_LIMIT_SIZE, ADD_TENANT_SET_PAGE_VALID, ADD_TENANT_SET_STORAGE_CLASSES_LIST, @@ -59,17 +58,22 @@ import { ADD_POOL_REMOVE_TOLERATION_ROW, ADD_POOL_SET_KEY_PAIR_VALUE, POOL_DETAILS_SET_SELECTED_POOL, + POOL_DETAILS_SET_OPEN_DETAILS, + EDIT_POOL_SET_INITIAL_INFO, + EDIT_POOL_SET_LOADING, + EDIT_POOL_RESET_FORM, + IEditPoolFields, + EDIT_POOL_SET_VALUE, + EDIT_POOL_SET_PAGE_VALID, + EDIT_POOL_SET_POOL_STORAGE_CLASSES, + EDIT_POOL_SET_TOLERATION_VALUE, + EDIT_POOL_ADD_NEW_TOLERATION, + EDIT_POOL_REMOVE_TOLERATION_ROW, + EDIT_POOL_SET_KEY_PAIR_VALUE, } from "./types"; import { ITolerationModel } from "../../../common/types"; // Basic actions -export const setWizardPage = (page: number) => { - return { - type: ADD_TENANT_SET_CURRENT_PAGE, - page, - }; -}; - export const updateAddField = ( pageName: string, fieldName: string, @@ -409,9 +413,99 @@ export const setPoolKeyValuePairs = (newArray: LabelKeyPair[]) => { }; }; +//Pool Details + export const setSelectedPool = (newPool: string | null) => { return { type: POOL_DETAILS_SET_SELECTED_POOL, pool: newPool, }; }; + +export const setOpenPoolDetails = (state: boolean) => { + return { + type: POOL_DETAILS_SET_OPEN_DETAILS, + state, + }; +}; + +// Edit Pool + +export const setInitialPoolDetails = (pool: IPool) => { + return { + type: EDIT_POOL_SET_INITIAL_INFO, + pool, + }; +}; + +export const setEditPoolLoading = (state: boolean) => { + return { + type: EDIT_POOL_SET_LOADING, + state, + }; +}; + +export const resetEditPoolForm = () => { + return { + type: EDIT_POOL_RESET_FORM, + }; +}; + +export const setEditPoolField = ( + page: keyof IEditPoolFields, + field: string, + value: any +) => { + return { + type: EDIT_POOL_SET_VALUE, + page, + field, + value, + }; +}; + +export const isEditPoolPageValid = (page: string, status: boolean) => { + return { + type: EDIT_POOL_SET_PAGE_VALID, + page, + status, + }; +}; + +export const setEditPoolStorageClasses = (storageClasses: Opts[]) => { + return { + type: EDIT_POOL_SET_POOL_STORAGE_CLASSES, + storageClasses, + }; +}; + +export const setEditPoolTolerationInfo = ( + index: number, + tolerationValue: ITolerationModel +) => { + return { + type: EDIT_POOL_SET_TOLERATION_VALUE, + index, + toleration: tolerationValue, + }; +}; + +export const addNewEditPoolToleration = () => { + return { + type: EDIT_POOL_ADD_NEW_TOLERATION, + }; +}; + +export const removeEditPoolToleration = (index: number) => { + return { + type: EDIT_POOL_REMOVE_TOLERATION_ROW, + index, + }; +}; + +export const setEditPoolKeyValuePairs = (newArray: LabelKeyPair[]) => { + return { + type: EDIT_POOL_SET_KEY_PAIR_VALUE, + newArray, + }; +}; diff --git a/portal-ui/src/screens/Console/Tenants/reducer.ts b/portal-ui/src/screens/Console/Tenants/reducer.ts index 80df2515c..d5ff8fbc3 100644 --- a/portal-ui/src/screens/Console/Tenants/reducer.ts +++ b/portal-ui/src/screens/Console/Tenants/reducer.ts @@ -16,6 +16,15 @@ import has from "lodash/has"; import get from "lodash/get"; import { + ADD_POOL_ADD_NEW_TOLERATION, + ADD_POOL_REMOVE_TOLERATION_ROW, + ADD_POOL_RESET_FORM, + ADD_POOL_SET_KEY_PAIR_VALUE, + ADD_POOL_SET_LOADING, + ADD_POOL_SET_PAGE_VALID, + ADD_POOL_SET_POOL_STORAGE_CLASSES, + ADD_POOL_SET_TOLERATION_VALUE, + ADD_POOL_SET_VALUE, ADD_TENANT_ADD_CA_KEYPAIR, ADD_TENANT_ADD_CONSOLE_CA_KEYPAIR, ADD_TENANT_ADD_CONSOLE_CERT, @@ -42,27 +51,35 @@ import { ADD_TENANT_SET_STORAGE_TYPE, ADD_TENANT_SET_TOLERATION_VALUE, ADD_TENANT_UPDATE_FIELD, + EDIT_POOL_ADD_NEW_TOLERATION, + EDIT_POOL_REMOVE_TOLERATION_ROW, + EDIT_POOL_RESET_FORM, + EDIT_POOL_SET_INITIAL_INFO, + EDIT_POOL_SET_KEY_PAIR_VALUE, + EDIT_POOL_SET_LOADING, + EDIT_POOL_SET_PAGE_VALID, + EDIT_POOL_SET_POOL_STORAGE_CLASSES, + EDIT_POOL_SET_TOLERATION_VALUE, + EDIT_POOL_SET_VALUE, + IEditPoolFields, + ITenantState, + LabelKeyPair, + POOL_DETAILS_SET_OPEN_DETAILS, + POOL_DETAILS_SET_SELECTED_POOL, TENANT_DETAILS_SET_CURRENT_TENANT, TENANT_DETAILS_SET_LOADING, TENANT_DETAILS_SET_TAB, TENANT_DETAILS_SET_TENANT, - ADD_POOL_SET_LOADING, - ADD_POOL_SET_VALUE, - ADD_POOL_RESET_FORM, - ITenantState, TenantsManagementTypes, - ADD_POOL_SET_PAGE_VALID, - ADD_POOL_SET_POOL_STORAGE_CLASSES, - ADD_POOL_ADD_NEW_TOLERATION, - ADD_POOL_SET_TOLERATION_VALUE, - ADD_POOL_REMOVE_TOLERATION_ROW, - ADD_POOL_SET_KEY_PAIR_VALUE, - POOL_DETAILS_SET_SELECTED_POOL, } from "./types"; import { KeyPair } from "./ListTenants/utils"; import { getRandomString } from "./utils"; import { addTenantSetStorageTypeReducer } from "./reducers/add-tenant-reducer"; -import { ITolerationEffect, ITolerationOperator } from "../../../common/types"; +import { + ITolerationEffect, + ITolerationModel, + ITolerationOperator, +} from "../../../common/types"; const initialState: ITenantState = { createTenant: { @@ -365,6 +382,7 @@ const initialState: ITenantState = { tenantInfo: null, currentTab: "summary", selectedPool: null, + poolDetailsOpen: false, }, addPool: { addPoolLoading: false, @@ -404,6 +422,44 @@ const initialState: ITenantState = { ], }, }, + editPool: { + editPoolLoading: false, + validPages: ["setup", "affinity", "configure"], + storageClasses: [], + limitSize: {}, + fields: { + setup: { + numberOfNodes: 0, + storageClass: "", + volumeSize: 0, + volumesPerServer: 0, + }, + affinity: { + nodeSelectorLabels: "", + podAffinity: "default", + withPodAntiAffinity: true, + }, + configuration: { + securityContextEnabled: false, + securityContext: { + runAsUser: "1000", + runAsGroup: "1000", + fsGroup: "1000", + runAsNonRoot: true, + }, + }, + nodeSelectorPairs: [{ key: "", value: "" }], + tolerations: [ + { + key: "", + tolerationSeconds: { seconds: 0 }, + value: "", + effect: ITolerationEffect.NoSchedule, + operator: ITolerationOperator.Equal, + }, + ], + }, + }, }; export function tenantsReducer( @@ -1165,14 +1221,6 @@ export function tenantsReducer( }, }, }; - case POOL_DETAILS_SET_SELECTED_POOL: - return { - ...state, - tenantDetails: { - ...state.tenantDetails, - selectedPool: action.pool, - }, - }; case ADD_POOL_RESET_FORM: return { ...state, @@ -1215,6 +1263,281 @@ export function tenantsReducer( }, }, }; + case POOL_DETAILS_SET_SELECTED_POOL: + return { + ...state, + tenantDetails: { + ...state.tenantDetails, + selectedPool: action.pool, + }, + }; + case POOL_DETAILS_SET_OPEN_DETAILS: + return { + ...state, + tenantDetails: { + ...state.tenantDetails, + poolDetailsOpen: action.state, + }, + }; + case EDIT_POOL_SET_INITIAL_INFO: + let podAffinity: "default" | "nodeSelector" | "none" = "none"; + let withPodAntiAffinity = false; + let nodeSelectorLabels = ""; + let tolerations: ITolerationModel[] = [ + { + key: "", + tolerationSeconds: { seconds: 0 }, + value: "", + effect: ITolerationEffect.NoSchedule, + operator: ITolerationOperator.Equal, + }, + ]; + let nodeSelectorPairs: LabelKeyPair[] = [{ key: "", value: "" }]; + + if (action.pool.affinity?.nodeAffinity) { + podAffinity = "nodeSelector"; + if (action.pool.affinity?.podAntiAffinity) { + withPodAntiAffinity = true; + } + } else if (action.pool.affinity?.podAntiAffinity) { + podAffinity = "default"; + } + + if (action.pool.affinity?.nodeAffinity) { + let labelItems: string[] = []; + nodeSelectorPairs = []; + + action.pool.affinity.nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms.forEach( + (labels) => { + labels.matchExpressions.forEach((exp) => { + labelItems.push(`${exp.key}=${exp.values.join(",")}`); + nodeSelectorPairs.push({ + key: exp.key, + value: exp.values.join(", "), + }); + }); + } + ); + nodeSelectorLabels = labelItems.join("&"); + } + + let securityContextOption = false; + + if (action.pool.securityContext) { + securityContextOption = + !!action.pool.securityContext.runAsUser || + !!action.pool.securityContext.runAsGroup || + !!action.pool.securityContext.fsGroup; + } + + if (action.pool.tolerations) { + tolerations = action.pool.tolerations?.map((toleration) => { + const tolerationItem: ITolerationModel = { + key: toleration.key, + tolerationSeconds: toleration.tolerationSeconds, + value: toleration.value, + effect: toleration.effect, + operator: toleration.operator, + }; + return tolerationItem; + }); + } + + const volSizeVars = action.pool.volume_configuration.size / 1073741824; + + const newPoolInfoFields: IEditPoolFields = { + setup: { + numberOfNodes: action.pool.servers, + storageClass: action.pool.volume_configuration.storage_class_name, + volumeSize: volSizeVars, + volumesPerServer: action.pool.volumes_per_server, + }, + configuration: { + securityContextEnabled: securityContextOption, + securityContext: { + runAsUser: action.pool.securityContext?.runAsUser || "", + runAsGroup: action.pool.securityContext?.runAsGroup || "", + fsGroup: action.pool.securityContext?.fsGroup || "", + runAsNonRoot: !!action.pool.securityContext?.runAsNonRoot, + }, + }, + affinity: { + podAffinity, + withPodAntiAffinity, + nodeSelectorLabels, + }, + tolerations, + nodeSelectorPairs, + }; + + return { + ...state, + editPool: { + ...state.editPool, + fields: { + ...state.editPool.fields, + ...newPoolInfoFields, + }, + }, + }; + + case EDIT_POOL_SET_LOADING: + return { + ...state, + editPool: { + ...state.editPool, + editPoolLoading: action.state, + }, + }; + case EDIT_POOL_SET_VALUE: + if (has(newState.editPool.fields, `${action.page}.${action.field}`)) { + const originPageNameItems = get( + newState.editPool.fields, + `${action.page}`, + {} + ); + + let newValue: any = {}; + newValue[action.field] = action.value; + + const joinValue = { ...originPageNameItems, ...newValue }; + + newState.editPool.fields[action.page] = { ...joinValue }; + + return { ...newState }; + } + + return state; + case EDIT_POOL_SET_PAGE_VALID: + const edPoolPV = [...state.editPool.validPages]; + + if (action.status) { + if (!edPoolPV.includes(action.page)) { + edPoolPV.push(action.page); + + newState.editPool.validPages = [...edPoolPV]; + } + } else { + const newSetOfPages = edPoolPV.filter((elm) => elm !== action.page); + + newState.editPool.validPages = [...newSetOfPages]; + } + + return { ...newState }; + case EDIT_POOL_SET_POOL_STORAGE_CLASSES: + return { + ...newState, + editPool: { + ...newState.editPool, + storageClasses: action.storageClasses, + }, + }; + case EDIT_POOL_SET_TOLERATION_VALUE: + const editPoolTolerationValue = [...state.editPool.fields.tolerations]; + + editPoolTolerationValue[action.index] = action.toleration; + + return { + ...state, + editPool: { + ...state.editPool, + fields: { + ...state.editPool.fields, + tolerations: [...editPoolTolerationValue], + }, + }, + }; + case EDIT_POOL_ADD_NEW_TOLERATION: + const editPoolTolerationArray = [ + ...state.editPool.fields.tolerations, + { + key: "", + tolerationSeconds: { seconds: 0 }, + value: "", + effect: ITolerationEffect.NoSchedule, + operator: ITolerationOperator.Equal, + }, + ]; + return { + ...state, + editPool: { + ...state.editPool, + fields: { + ...state.editPool.fields, + tolerations: [...editPoolTolerationArray], + }, + }, + }; + case EDIT_POOL_REMOVE_TOLERATION_ROW: + const removePoolTolerationArray = + state.editPool.fields.tolerations.filter( + (_, index) => index !== action.index + ); + + return { + ...state, + editPool: { + ...state.editPool, + fields: { + ...state.editPool.fields, + tolerations: [...removePoolTolerationArray], + }, + }, + }; + case EDIT_POOL_SET_KEY_PAIR_VALUE: + return { + ...state, + editPool: { + ...state.editPool, + fields: { + ...state.editPool.fields, + nodeSelectorPairs: action.newArray, + }, + }, + }; + case EDIT_POOL_RESET_FORM: + return { + ...state, + editPool: { + editPoolLoading: false, + validPages: ["setup", "affinity", "configure"], + storageClasses: [], + limitSize: {}, + fields: { + setup: { + numberOfNodes: 0, + storageClass: "", + volumeSize: 0, + volumesPerServer: 0, + }, + affinity: { + nodeSelectorLabels: "", + podAffinity: "default", + withPodAntiAffinity: true, + }, + configuration: { + securityContextEnabled: false, + securityContext: { + runAsUser: "1000", + runAsGroup: "1000", + fsGroup: "1000", + runAsNonRoot: true, + }, + }, + nodeSelectorPairs: [{ key: "", value: "" }], + tolerations: [ + { + key: "", + tolerationSeconds: { seconds: 0 }, + value: "", + effect: ITolerationEffect.NoSchedule, + operator: ITolerationOperator.Equal, + }, + ], + }, + }, + }; + default: return state; } diff --git a/portal-ui/src/screens/Console/Tenants/types.ts b/portal-ui/src/screens/Console/Tenants/types.ts index 60649aaa9..03208d7c0 100644 --- a/portal-ui/src/screens/Console/Tenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/types.ts @@ -22,7 +22,7 @@ import { IGemaltoCredentials, ITolerationModel, } from "../../../common/types"; -import { IResourcesSize, ITenant } from "./ListTenants/types"; +import { IPool, IResourcesSize, ITenant } from "./ListTenants/types"; import { KeyPair, Opts } from "./ListTenants/utils"; import { IntegrationConfiguration } from "./AddTenant/Steps/TenantResources/utils"; @@ -99,8 +99,23 @@ export const ADD_POOL_ADD_NEW_TOLERATION = "ADD_POOL/ADD_NEW_TOLERATION"; export const ADD_POOL_REMOVE_TOLERATION_ROW = "ADD_POOL/REMOVE_TOLERATION_ROW"; // Pool Details +export const POOL_DETAILS_SET_OPEN_DETAILS = "POOL_DETAILS/SET_OPEN_DETAILS"; export const POOL_DETAILS_SET_SELECTED_POOL = "POOL_DETAILS/SET_SELECTED_POOL"; +// Edit Pool +export const EDIT_POOL_SET_INITIAL_INFO = "EDIT_POOL/SET_INITIAL_INFO"; +export const EDIT_POOL_SET_POOL_STORAGE_CLASSES = + "EDIT_POOL/SET_POOL_STORAGE_CLASSES"; +export const EDIT_POOL_SET_PAGE_VALID = "EDIT_POOL/SET_PAGE_VALID"; +export const EDIT_POOL_SET_VALUE = "EDIT_POOL/SET_VALUE"; +export const EDIT_POOL_SET_LOADING = "EDIT_POOL/SET_LOADING"; +export const EDIT_POOL_RESET_FORM = "EDIT_POOL/RESET_FORM"; +export const EDIT_POOL_SET_KEY_PAIR_VALUE = "EDIT_POOL/SET_KEY_PAIR_VALUE"; +export const EDIT_POOL_SET_TOLERATION_VALUE = "EDIT_POOL/SET_TOLERATION_VALUE"; +export const EDIT_POOL_ADD_NEW_TOLERATION = "EDIT_POOL/ADD_NEW_TOLERATION"; +export const EDIT_POOL_REMOVE_TOLERATION_ROW = + "EDIT_POOL/REMOVE_TOLERATION_ROW"; + export interface ICertificateInfo { name: string; serialNumber: string; @@ -367,6 +382,7 @@ export interface ITenantDetails { loadingTenant: boolean; tenantInfo: ITenant | null; currentTab: string; + poolDetailsOpen: boolean; selectedPool: string | null; } @@ -374,6 +390,7 @@ export interface ITenantState { createTenant: ICreateTenant; tenantDetails: ITenantDetails; addPool: IAddPool; + editPool: IEditPool; } export interface ILabelKeyPair { @@ -421,6 +438,29 @@ export interface IAddPool { fields: IAddPoolFields; } +export interface IEditPoolSetup { + numberOfNodes: number; + volumeSize: number; + volumesPerServer: number; + storageClass: string; +} + +export interface IEditPoolFields { + setup: IEditPoolSetup; + affinity: ITenantAffinity; + configuration: IPoolConfiguration; + tolerations: ITolerationModel[]; + nodeSelectorPairs: LabelKeyPair[]; +} + +export interface IEditPool { + editPoolLoading: boolean; + validPages: string[]; + storageClasses: Opts[]; + limitSize: any; + fields: IEditPoolFields; +} + export interface ITenantIdentityProviderResponse { oidc?: { callback_url: string; @@ -662,11 +702,68 @@ interface SetPoolSelectorKeyPairValueArray { newArray: LabelKeyPair[]; } +interface SetDetailsOpen { + type: typeof POOL_DETAILS_SET_OPEN_DETAILS; + state: boolean; +} + interface SetSelectedPool { type: typeof POOL_DETAILS_SET_SELECTED_POOL; pool: string | null; } +interface EditPoolSetInitialInformation { + type: typeof EDIT_POOL_SET_INITIAL_INFO; + pool: IPool; +} + +interface EditPoolLoading { + type: typeof EDIT_POOL_SET_LOADING; + state: boolean; +} + +interface ResetEditPoolForm { + type: typeof EDIT_POOL_RESET_FORM; +} + +interface SetEditPoolFieldValue { + type: typeof EDIT_POOL_SET_VALUE; + page: keyof IAddPoolFields; + field: string; + value: any; +} + +interface EditPoolPageValid { + type: typeof EDIT_POOL_SET_PAGE_VALID; + page: string; + status: boolean; +} + +interface EditPoolStorageClasses { + type: typeof EDIT_POOL_SET_POOL_STORAGE_CLASSES; + storageClasses: Opts[]; +} + +interface EditPoolTolerationValue { + type: typeof EDIT_POOL_SET_TOLERATION_VALUE; + index: number; + toleration: ITolerationModel; +} + +interface EditPoolToleration { + type: typeof EDIT_POOL_ADD_NEW_TOLERATION; +} + +interface EditRemovePoolTolerationRow { + type: typeof EDIT_POOL_REMOVE_TOLERATION_ROW; + index: number; +} + +interface EditPoolSelectorKeyPairValueArray { + type: typeof EDIT_POOL_SET_KEY_PAIR_VALUE; + newArray: LabelKeyPair[]; +} + export type FieldsToHandle = INameTenantFields; export type TenantsManagementTypes = @@ -709,4 +806,15 @@ export type TenantsManagementTypes = | AddNewPoolToleration | RemovePoolTolerationRow | SetPoolSelectorKeyPairValueArray - | SetSelectedPool; + | SetSelectedPool + | SetDetailsOpen + | EditPoolLoading + | ResetEditPoolForm + | SetEditPoolFieldValue + | EditPoolPageValid + | EditPoolStorageClasses + | EditPoolTolerationValue + | EditPoolToleration + | EditRemovePoolTolerationRow + | EditPoolSelectorKeyPairValueArray + | EditPoolSetInitialInformation;