From ba48e0c5b8986a7c7a0df7ab9156f9606c9c4806 Mon Sep 17 00:00:00 2001 From: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> Date: Wed, 1 Jun 2022 15:14:31 -0700 Subject: [PATCH] Move EditPool redux state to it's own slice (#2063) Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com> --- .../TenantDetails/Pools/EditPool/EditPool.tsx | 193 +----------- .../Pools/EditPool/EditPoolButton.tsx | 60 ++++ .../Pools/EditPool/EditPoolConfiguration.tsx | 7 +- .../Pools/EditPool/EditPoolPlacement.tsx | 14 +- .../Pools/EditPool/EditPoolResources.tsx | 14 +- .../Pools/EditPool/editPoolSlice.ts | 277 +++++++++++++++++ .../Pools/EditPool/thunks/editPoolAsync.ts | 139 +++++++++ .../TenantDetails/Pools/EditPool/types.ts | 53 ++++ .../screens/Console/Tenants/tenantsSlice.ts | 283 +----------------- .../src/screens/Console/Tenants/types.ts | 24 -- portal-ui/src/store.ts | 3 +- 11 files changed, 564 insertions(+), 503 deletions(-) create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolButton.tsx create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice.ts create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/thunks/editPoolAsync.ts create mode 100644 portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/types.ts 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 index fa4a086d9..6b436dcfd 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPool.tsx @@ -14,16 +14,14 @@ // 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 React, { Fragment, useEffect } from "react"; import { useDispatch, useSelector } 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"; @@ -33,30 +31,18 @@ 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 { niceBytes } from "../../../../../../common/utils"; import { formFieldStyles, modalStyleUtils, } from "../../../../Common/FormComponents/common/styleLibrary"; -import { IEditPoolItem, IEditPoolRequest } from "../../../ListTenants/types"; import { AppState } from "../../../../../../store"; -import { ErrorResponseHandler } from "../../../../../../common/types"; -import { getDefaultAffinity, getNodeSelector } from "../../utils"; -import { setErrorSnackMessage } from "../../../../../../systemSlice"; -import { - resetPoolForm, - setInitialPoolDetails, - setTenantDetailsLoad, -} from "../../../tenantsSlice"; +import { resetEditPoolForm, setInitialPoolDetails } from "./editPoolSlice"; +import EditPoolButton from "./EditPoolButton"; +import makeStyles from "@mui/styles/makeStyles"; -interface IEditPoolProps { - classes: any; - open: boolean; - match: any; -} - -const styles = (theme: Theme) => +const useStyles = makeStyles((theme: Theme) => createStyles({ bottomContainer: { display: "flex", @@ -81,12 +67,12 @@ const styles = (theme: Theme) => }, ...formFieldStyles, ...modalStyleUtils, - }); + }) +); -const requiredPages = ["setup", "affinity", "configure"]; - -const EditPool = ({ classes, open }: IEditPoolProps) => { +const EditPool = () => { const dispatch = useDispatch(); + const classes = useStyles(); const tenant = useSelector( (state: AppState) => state.tenants.tenantDetails.tenantInfo @@ -94,47 +80,11 @@ const EditPool = ({ classes, open }: IEditPoolProps) => { const selectedPool = useSelector( (state: AppState) => state.tenants.tenantDetails.selectedPool ); - const selectedStorageClass = useSelector( - (state: AppState) => state.tenants.editPool.fields.setup.storageClass - ); - const validPages = useSelector( - (state: AppState) => state.tenants.editPool.validPages - ); - const numberOfNodes = useSelector( - (state: AppState) => state.tenants.editPool.fields.setup.numberOfNodes - ); - const volumeSize = useSelector( - (state: AppState) => state.tenants.editPool.fields.setup.volumeSize - ); - const volumesPerServer = useSelector( - (state: AppState) => state.tenants.editPool.fields.setup.volumesPerServer - ); - const affinityType = useSelector( - (state: AppState) => state.tenants.editPool.fields.affinity.podAffinity - ); - const nodeSelectorLabels = useSelector( - (state: AppState) => - state.tenants.editPool.fields.affinity.nodeSelectorLabels - ); - const withPodAntiAffinity = useSelector( - (state: AppState) => - state.tenants.editPool.fields.affinity.withPodAntiAffinity - ); - const tolerations = useSelector( - (state: AppState) => state.tenants.editPool.fields.tolerations - ); - const securityContextEnabled = useSelector( - (state: AppState) => - state.tenants.editPool.fields.configuration.securityContextEnabled - ); - const securityContext = useSelector( - (state: AppState) => - state.tenants.editPool.fields.configuration.securityContext + const editSending = useSelector( + (state: AppState) => state.editPool.editSending ); - const [editSending, setEditSending] = useState(false); - const poolsURL = `/namespaces/${tenant?.namespace || ""}/tenants/${ tenant?.name || "" }/pools`; @@ -153,131 +103,18 @@ const EditPool = ({ classes, open }: IEditPoolProps) => { } }, [selectedPool, dispatch, 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); - dispatch(resetPoolForm()); - dispatch(setTenantDetailsLoad(true)); - history.push(poolsURL); - }) - .catch((err: ErrorResponseHandler) => { - setEditSending(false); - dispatch(setErrorSnackMessage(err)); - }); - } - }, [ - selectedPool, - dispatch, - editSending, - poolsURL, - affinityType, - nodeSelectorLabels, - numberOfNodes, - securityContext, - securityContextEnabled, - selectedStorageClass, - tenant, - tolerations, - volumeSize, - volumesPerServer, - withPodAntiAffinity, - ]); - const cancelButton = { label: "Cancel", type: "other", enabled: true, action: () => { - dispatch(resetPoolForm()); + dispatch(resetEditPoolForm()); history.push(poolsURL); }, }; const createButton = { - label: "Update", - type: "submit", - enabled: - !editSending && - selectedStorageClass !== "" && - requiredPages.every((v) => validPages.includes(v)), - action: () => { - setEditSending(true); - }, + componentRender: , }; const wizardSteps: IWizardElement[] = [ @@ -339,4 +176,4 @@ const EditPool = ({ classes, open }: IEditPoolProps) => { ); }; -export default withStyles(styles)(EditPool); +export default EditPool; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolButton.tsx b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolButton.tsx new file mode 100644 index 000000000..56cd55eab --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolButton.tsx @@ -0,0 +1,60 @@ +// 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 { Button } from "@mui/material"; +import React from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { AppState } from "../../../../../../store"; +import { editPoolAsync } from "./thunks/editPoolAsync"; + +const EditPoolButton = () => { + const dispatch = useDispatch(); + const requiredPages = ["setup", "affinity", "configure"]; + + const selectedStorageClass = useSelector( + (state: AppState) => state.editPool.fields.setup.storageClass + ); + const validPages = useSelector( + (state: AppState) => state.editPool.validPages + ); + + const editSending = useSelector( + (state: AppState) => state.editPool.editSending + ); + + const enabled = + !editSending && + selectedStorageClass !== "" && + requiredPages.every((v) => validPages.includes(v)); + + return ( + + ); +}; + +export default EditPoolButton; 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 index 7f7e207c5..098eda5c8 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolConfiguration.tsx @@ -34,7 +34,7 @@ import { } from "../../../../../../utils/validationFunctions"; import FormSwitchWrapper from "../../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper"; import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper"; -import { isEditPoolPageValid, setEditPoolField } from "../../../tenantsSlice"; +import { isEditPoolPageValid, setEditPoolField } from "./editPoolSlice"; interface IConfigureProps { classes: any; @@ -83,11 +83,10 @@ const PoolConfiguration = ({ classes }: IConfigureProps) => { const securityContextEnabled = useSelector( (state: AppState) => - state.tenants.editPool.fields.configuration.securityContextEnabled + state.editPool.fields.configuration.securityContextEnabled ); const securityContext = useSelector( - (state: AppState) => - state.tenants.editPool.fields.configuration.securityContext + (state: AppState) => state.editPool.fields.configuration.securityContext ); const [validationErrors, setValidationErrors] = useState({}); 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 index 0c5892cfa..86fa38c11 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolPlacement.tsx @@ -48,7 +48,7 @@ import { setEditPoolField, setEditPoolKeyValuePairs, setEditPoolTolerationInfo, -} from "../../../tenantsSlice"; +} from "./editPoolSlice"; interface IAffinityProps { classes: any; @@ -119,21 +119,19 @@ const Affinity = ({ classes }: IAffinityProps) => { const dispatch = useDispatch(); const podAffinity = useSelector( - (state: AppState) => state.tenants.editPool.fields.affinity.podAffinity + (state: AppState) => state.editPool.fields.affinity.podAffinity ); const nodeSelectorLabels = useSelector( - (state: AppState) => - state.tenants.editPool.fields.affinity.nodeSelectorLabels + (state: AppState) => state.editPool.fields.affinity.nodeSelectorLabels ); const withPodAntiAffinity = useSelector( - (state: AppState) => - state.tenants.editPool.fields.affinity.withPodAntiAffinity + (state: AppState) => state.editPool.fields.affinity.withPodAntiAffinity ); const keyValuePairs = useSelector( - (state: AppState) => state.tenants.editPool.fields.nodeSelectorPairs + (state: AppState) => state.editPool.fields.nodeSelectorPairs ); const tolerations = useSelector( - (state: AppState) => state.tenants.editPool.fields.tolerations + (state: AppState) => state.editPool.fields.tolerations ); const [validationErrors, setValidationErrors] = useState({}); 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 index 01cc476d6..86ff5bd21 100644 --- a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/EditPoolResources.tsx @@ -43,7 +43,7 @@ import { isEditPoolPageValid, setEditPoolField, setEditPoolStorageClasses, -} from "../../../tenantsSlice"; +} from "./editPoolSlice"; interface IPoolResourcesProps { classes: any; @@ -58,7 +58,7 @@ const styles = (theme: Theme) => margin: "auto", justifyContent: "center", "& div": { - width: 150, + width: 200, "@media (max-width: 900px)": { flexFlow: "column", }, @@ -90,19 +90,19 @@ const PoolResources = ({ classes }: IPoolResourcesProps) => { (state: AppState) => state.tenants.tenantDetails.tenantInfo ); const storageClasses = useSelector( - (state: AppState) => state.tenants.editPool.storageClasses + (state: AppState) => state.editPool.storageClasses ); const numberOfNodes = useSelector((state: AppState) => - state.tenants.editPool.fields.setup.numberOfNodes.toString() + state.editPool.fields.setup.numberOfNodes.toString() ); const storageClass = useSelector( - (state: AppState) => state.tenants.editPool.fields.setup.storageClass + (state: AppState) => state.editPool.fields.setup.storageClass ); const volumeSize = useSelector((state: AppState) => - state.tenants.editPool.fields.setup.volumeSize.toString() + state.editPool.fields.setup.volumeSize.toString() ); const volumesPerServer = useSelector((state: AppState) => - state.tenants.editPool.fields.setup.volumesPerServer.toString() + state.editPool.fields.setup.volumesPerServer.toString() ); const [validationErrors, setValidationErrors] = useState({}); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice.ts new file mode 100644 index 000000000..fd59f5967 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice.ts @@ -0,0 +1,277 @@ +// 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 { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { IEditPool, IEditPoolFields, PageFieldValue } from "./types"; +import { + ITolerationEffect, + ITolerationModel, + ITolerationOperator, +} from "../../../../../../common/types"; +import { IPool } from "../../../ListTenants/types"; +import { LabelKeyPair } from "../../../types"; +import { has } from "lodash"; +import get from "lodash/get"; +import { Opts } from "../../../ListTenants/utils"; +import { editPoolAsync } from "./thunks/editPoolAsync"; + +const initialState: IEditPool = { + 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, + }, + ], + }, + editSending: false, +}; + +export const editPoolSlice = createSlice({ + name: "editPool", + initialState, + reducers: { + setInitialPoolDetails: (state, action: PayloadAction) => { + 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.payload.affinity?.nodeAffinity) { + podAffinity = "nodeSelector"; + if (action.payload.affinity?.podAntiAffinity) { + withPodAntiAffinity = true; + } + } else if (action.payload.affinity?.podAntiAffinity) { + podAffinity = "default"; + } + + if (action.payload.affinity?.nodeAffinity) { + let labelItems: string[] = []; + nodeSelectorPairs = []; + + action.payload.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.payload.securityContext) { + securityContextOption = + !!action.payload.securityContext.runAsUser || + !!action.payload.securityContext.runAsGroup || + !!action.payload.securityContext.fsGroup; + } + + if (action.payload.tolerations) { + tolerations = action.payload.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.payload.volume_configuration.size / 1073741824; + + const newPoolInfoFields: IEditPoolFields = { + setup: { + numberOfNodes: action.payload.servers, + storageClass: action.payload.volume_configuration.storage_class_name, + volumeSize: volSizeVars, + volumesPerServer: action.payload.volumes_per_server, + }, + configuration: { + securityContextEnabled: securityContextOption, + securityContext: { + runAsUser: action.payload.securityContext?.runAsUser || "", + runAsGroup: action.payload.securityContext?.runAsGroup || "", + fsGroup: action.payload.securityContext?.fsGroup || "", + runAsNonRoot: !!action.payload.securityContext?.runAsNonRoot, + }, + }, + affinity: { + podAffinity, + withPodAntiAffinity, + nodeSelectorLabels, + }, + tolerations, + nodeSelectorPairs, + }; + + state.fields = { + ...state.fields, + ...newPoolInfoFields, + }; + }, + setEditPoolLoading: (state, action: PayloadAction) => { + state.editPoolLoading = action.payload; + }, + setEditPoolField: (state, action: PayloadAction) => { + if (has(state.fields, `${action.payload.page}.${action.payload.field}`)) { + const originPageNameItems = get( + state.fields, + `${action.payload.page}`, + {} + ); + + let newValue: any = {}; + newValue[action.payload.field] = action.payload.value; + + const joinValue = { ...originPageNameItems, ...newValue }; + + state.fields[action.payload.page] = { ...joinValue }; + } + }, + isEditPoolPageValid: ( + state, + action: PayloadAction<{ + page: string; + status: boolean; + }> + ) => { + const edPoolPV = [...state.validPages]; + + if (action.payload.status) { + if (!edPoolPV.includes(action.payload.page)) { + edPoolPV.push(action.payload.page); + + state.validPages = [...edPoolPV]; + } + } else { + const newSetOfPages = edPoolPV.filter( + (elm) => elm !== action.payload.page + ); + + state.validPages = [...newSetOfPages]; + } + }, + setEditPoolStorageClasses: (state, action: PayloadAction) => { + state.storageClasses = action.payload; + }, + setEditPoolTolerationInfo: ( + state, + action: PayloadAction<{ + index: number; + tolerationValue: ITolerationModel; + }> + ) => { + const editPoolTolerationValue = [...state.fields.tolerations]; + + editPoolTolerationValue[action.payload.index] = + action.payload.tolerationValue; + state.fields.tolerations = editPoolTolerationValue; + }, + addNewEditPoolToleration: (state) => { + state.fields.tolerations.push({ + key: "", + tolerationSeconds: { seconds: 0 }, + value: "", + effect: ITolerationEffect.NoSchedule, + operator: ITolerationOperator.Equal, + }); + }, + removeEditPoolToleration: (state, action: PayloadAction) => { + state.fields.tolerations = state.fields.tolerations.filter( + (_, index) => index !== action.payload + ); + }, + setEditPoolKeyValuePairs: ( + state, + action: PayloadAction + ) => { + state.fields.nodeSelectorPairs = action.payload; + }, + resetEditPoolForm: () => initialState, + }, + extraReducers: (builder) => { + builder + .addCase(editPoolAsync.pending, (state, action) => { + state.editSending = true; + }) + .addCase(editPoolAsync.rejected, (state, action) => { + state.editSending = false; + }) + .addCase(editPoolAsync.fulfilled, (state, action) => { + state.editSending = false; + }); + }, +}); + +export const { + setInitialPoolDetails, + setEditPoolLoading, + resetEditPoolForm, + setEditPoolField, + isEditPoolPageValid, + setEditPoolStorageClasses, + setEditPoolTolerationInfo, + addNewEditPoolToleration, + removeEditPoolToleration, + setEditPoolKeyValuePairs, +} = editPoolSlice.actions; + +export default editPoolSlice.reducer; diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/thunks/editPoolAsync.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/thunks/editPoolAsync.ts new file mode 100644 index 000000000..6752ce35b --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/thunks/editPoolAsync.ts @@ -0,0 +1,139 @@ +// 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 { createAsyncThunk } from "@reduxjs/toolkit"; +import { AppState } from "../../../../../../../store"; +import api from "../../../../../../../common/api"; +import { ErrorResponseHandler } from "../../../../../../../common/types"; +import { setErrorSnackMessage } from "../../../../../../../systemSlice"; +import { generatePoolName } from "../../../../../../../common/utils"; +import { getDefaultAffinity, getNodeSelector } from "../../../utils"; +import { IEditPoolItem, IEditPoolRequest } from "../../../../ListTenants/types"; +import history from "../../../../../../../history"; +import { resetEditPoolForm } from "../editPoolSlice"; +import { setTenantDetailsLoad } from "../../../../tenantsSlice"; + +export const editPoolAsync = createAsyncThunk( + "editPool/editPoolAsync", + async (_, { getState, rejectWithValue, dispatch }) => { + const state = getState() as AppState; + + const tenant = state.tenants.tenantDetails.tenantInfo; + const selectedPool = state.tenants.tenantDetails.selectedPool; + const selectedStorageClass = state.editPool.fields.setup.storageClass; + const numberOfNodes = state.editPool.fields.setup.numberOfNodes; + const volumeSize = state.editPool.fields.setup.volumeSize; + const volumesPerServer = state.editPool.fields.setup.volumesPerServer; + const affinityType = state.editPool.fields.affinity.podAffinity; + const nodeSelectorLabels = + state.editPool.fields.affinity.nodeSelectorLabels; + const withPodAntiAffinity = + state.editPool.fields.affinity.withPodAntiAffinity; + const tolerations = state.editPool.fields.tolerations; + const securityContextEnabled = + state.editPool.fields.configuration.securityContextEnabled; + const securityContext = state.editPool.fields.configuration.securityContext; + if (!tenant) { + return; + } + + 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, + }, + ], + }; + const poolsURL = `/namespaces/${tenant?.namespace || ""}/tenants/${ + tenant?.name || "" + }/pools`; + + return api + .invoke( + "PUT", + `/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/pools`, + data + ) + .then(() => { + dispatch(resetEditPoolForm()); + dispatch(setTenantDetailsLoad(true)); + history.push(poolsURL); + }) + .catch((err: ErrorResponseHandler) => { + dispatch(setErrorSnackMessage(err)); + }); + } +); diff --git a/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/types.ts b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/types.ts new file mode 100644 index 000000000..5730d63c4 --- /dev/null +++ b/portal-ui/src/screens/Console/Tenants/TenantDetails/Pools/EditPool/types.ts @@ -0,0 +1,53 @@ +// 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 { ITolerationModel } from "../../../../../../common/types"; +import { Opts } from "../../../ListTenants/utils"; +import { + IPoolConfiguration, + ITenantAffinity, + LabelKeyPair, +} from "../../../types"; + +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; + editSending: boolean; +} + +export interface PageFieldValue { + page: keyof IEditPoolFields; + field: string; + value: any; +} diff --git a/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts b/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts index f2190fb4e..2aeaa1c12 100644 --- a/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts +++ b/portal-ui/src/screens/Console/Tenants/tenantsSlice.ts @@ -15,12 +15,7 @@ // along with this program. If not, see . import { createSlice, PayloadAction } from "@reduxjs/toolkit"; -import { - IAddPoolFields, - IEditPoolFields, - ITenantState, - LabelKeyPair, -} from "./types"; +import { IAddPoolFields, ITenantState, LabelKeyPair } from "./types"; import { ITolerationEffect, ITolerationModel, @@ -29,7 +24,7 @@ import { import get from "lodash/get"; import { has } from "lodash"; import { Opts } from "./ListTenants/utils"; -import { IPool, ITenant } from "./ListTenants/types"; +import { ITenant } from "./ListTenants/types"; export interface FileValue { fileName: string; @@ -49,12 +44,6 @@ export interface CertificateFile { value: string; } -export interface PageFieldValue { - page: keyof IEditPoolFields; - field: string; - value: any; -} - const initialState: ITenantState = { tenantDetails: { currentTenant: "", @@ -103,44 +92,6 @@ 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 const tenantSlice = createSlice({ @@ -295,226 +246,6 @@ export const tenantSlice = createSlice({ setOpenPoolDetails: (state, action: PayloadAction) => { state.tenantDetails.poolDetailsOpen = action.payload; }, - setInitialPoolDetails: (state, action: PayloadAction) => { - 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.payload.affinity?.nodeAffinity) { - podAffinity = "nodeSelector"; - if (action.payload.affinity?.podAntiAffinity) { - withPodAntiAffinity = true; - } - } else if (action.payload.affinity?.podAntiAffinity) { - podAffinity = "default"; - } - - if (action.payload.affinity?.nodeAffinity) { - let labelItems: string[] = []; - nodeSelectorPairs = []; - - action.payload.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.payload.securityContext) { - securityContextOption = - !!action.payload.securityContext.runAsUser || - !!action.payload.securityContext.runAsGroup || - !!action.payload.securityContext.fsGroup; - } - - if (action.payload.tolerations) { - tolerations = action.payload.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.payload.volume_configuration.size / 1073741824; - - const newPoolInfoFields: IEditPoolFields = { - setup: { - numberOfNodes: action.payload.servers, - storageClass: action.payload.volume_configuration.storage_class_name, - volumeSize: volSizeVars, - volumesPerServer: action.payload.volumes_per_server, - }, - configuration: { - securityContextEnabled: securityContextOption, - securityContext: { - runAsUser: action.payload.securityContext?.runAsUser || "", - runAsGroup: action.payload.securityContext?.runAsGroup || "", - fsGroup: action.payload.securityContext?.fsGroup || "", - runAsNonRoot: !!action.payload.securityContext?.runAsNonRoot, - }, - }, - affinity: { - podAffinity, - withPodAntiAffinity, - nodeSelectorLabels, - }, - tolerations, - nodeSelectorPairs, - }; - - state.editPool.fields = { - ...state.editPool.fields, - ...newPoolInfoFields, - }; - }, - setEditPoolLoading: (state, action: PayloadAction) => { - state.editPool.editPoolLoading = action.payload; - }, - setEditPoolField: (state, action: PayloadAction) => { - if ( - has( - state.editPool.fields, - `${action.payload.page}.${action.payload.field}` - ) - ) { - const originPageNameItems = get( - state.editPool.fields, - `${action.payload.page}`, - {} - ); - - let newValue: any = {}; - newValue[action.payload.field] = action.payload.value; - - const joinValue = { ...originPageNameItems, ...newValue }; - - state.editPool.fields[action.payload.page] = { ...joinValue }; - } - }, - isEditPoolPageValid: ( - state, - action: PayloadAction<{ - page: string; - status: boolean; - }> - ) => { - const edPoolPV = [...state.editPool.validPages]; - - if (action.payload.status) { - if (!edPoolPV.includes(action.payload.page)) { - edPoolPV.push(action.payload.page); - - state.editPool.validPages = [...edPoolPV]; - } - } else { - const newSetOfPages = edPoolPV.filter( - (elm) => elm !== action.payload.page - ); - - state.editPool.validPages = [...newSetOfPages]; - } - }, - setEditPoolStorageClasses: (state, action: PayloadAction) => { - state.editPool.storageClasses = action.payload; - }, - setEditPoolTolerationInfo: ( - state, - action: PayloadAction<{ - index: number; - tolerationValue: ITolerationModel; - }> - ) => { - const editPoolTolerationValue = [...state.editPool.fields.tolerations]; - - editPoolTolerationValue[action.payload.index] = - action.payload.tolerationValue; - state.editPool.fields.tolerations = editPoolTolerationValue; - }, - addNewEditPoolToleration: (state) => { - state.editPool.fields.tolerations.push({ - key: "", - tolerationSeconds: { seconds: 0 }, - value: "", - effect: ITolerationEffect.NoSchedule, - operator: ITolerationOperator.Equal, - }); - }, - removeEditPoolToleration: (state, action: PayloadAction) => { - state.editPool.fields.tolerations = - state.editPool.fields.tolerations.filter( - (_, index) => index !== action.payload - ); - }, - setEditPoolKeyValuePairs: ( - state, - action: PayloadAction - ) => { - state.editPool.fields.nodeSelectorPairs = action.payload; - }, - resetEditPoolForm: (state) => { - 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, - }, - ], - }, - }; - }, }, }); @@ -535,16 +266,6 @@ export const { setPoolKeyValuePairs, setSelectedPool, setOpenPoolDetails, - setInitialPoolDetails, - setEditPoolLoading, - resetEditPoolForm, - setEditPoolField, - isEditPoolPageValid, - setEditPoolStorageClasses, - setEditPoolTolerationInfo, - addNewEditPoolToleration, - removeEditPoolToleration, - setEditPoolKeyValuePairs, } = tenantSlice.actions; export default tenantSlice.reducer; diff --git a/portal-ui/src/screens/Console/Tenants/types.ts b/portal-ui/src/screens/Console/Tenants/types.ts index 4fecd3a9f..c05db103d 100644 --- a/portal-ui/src/screens/Console/Tenants/types.ts +++ b/portal-ui/src/screens/Console/Tenants/types.ts @@ -291,7 +291,6 @@ export interface ITenantDetails { export interface ITenantState { tenantDetails: ITenantDetails; addPool: IAddPool; - editPool: IEditPool; } export interface ILabelKeyPair { @@ -339,29 +338,6 @@ 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; diff --git a/portal-ui/src/store.ts b/portal-ui/src/store.ts index bccbd95e3..01a271388 100644 --- a/portal-ui/src/store.ts +++ b/portal-ui/src/store.ts @@ -27,6 +27,7 @@ import tenantsReducer from "./screens/Console/Tenants/tenantsSlice"; import dashboardReducer from "./screens/Console/Dashboard/dashboardSlice"; import { configureStore } from "@reduxjs/toolkit"; import createTenantReducer from "./screens/Console/Tenants/AddTenant/createTenantSlice"; +import editPoolReducer from "./screens/Console/Tenants/TenantDetails/Pools/EditPool/editPoolSlice"; export const store = configureStore({ reducer: { @@ -43,12 +44,12 @@ export const store = configureStore({ // Operator Reducers tenants: tenantsReducer, createTenant: createTenantReducer, + editPool: editPoolReducer, }, }); export type AppState = ReturnType; -// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState} export type AppDispatch = typeof store.dispatch; export default store;