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;