Move EditPool redux state to it's own slice (#2063)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-06-01 15:14:31 -07:00
committed by GitHub
parent 3da636170f
commit ba48e0c5b8
11 changed files with 564 additions and 503 deletions

View File

@@ -14,16 +14,14 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, 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<boolean>(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: <EditPoolButton />,
};
const wizardSteps: IWizardElement[] = [
@@ -339,4 +176,4 @@ const EditPool = ({ classes, open }: IEditPoolProps) => {
);
};
export default withStyles(styles)(EditPool);
export default EditPool;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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 (
<Button
id={"wizard-button-Update"}
variant="contained"
color="primary"
size="small"
onClick={() => {
dispatch(editPoolAsync());
}}
disabled={!enabled}
key={`button-EditPool-Update`}
>
Update
</Button>
);
};
export default EditPoolButton;

View File

@@ -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<any>({});

View File

@@ -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<any>({});

View File

@@ -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<any>({});

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<IPool>) => {
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<boolean>) => {
state.editPoolLoading = action.payload;
},
setEditPoolField: (state, action: PayloadAction<PageFieldValue>) => {
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<Opts[]>) => {
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<number>) => {
state.fields.tolerations = state.fields.tolerations.filter(
(_, index) => index !== action.payload
);
},
setEditPoolKeyValuePairs: (
state,
action: PayloadAction<LabelKeyPair[]>
) => {
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;

View File

@@ -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 <http://www.gnu.org/licenses/>.
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));
});
}
);

View File

@@ -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 <http://www.gnu.org/licenses/>.
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;
}

View File

@@ -15,12 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
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<boolean>) => {
state.tenantDetails.poolDetailsOpen = action.payload;
},
setInitialPoolDetails: (state, action: PayloadAction<IPool>) => {
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<boolean>) => {
state.editPool.editPoolLoading = action.payload;
},
setEditPoolField: (state, action: PayloadAction<PageFieldValue>) => {
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<Opts[]>) => {
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<number>) => {
state.editPool.fields.tolerations =
state.editPool.fields.tolerations.filter(
(_, index) => index !== action.payload
);
},
setEditPoolKeyValuePairs: (
state,
action: PayloadAction<LabelKeyPair[]>
) => {
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;

View File

@@ -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;

View File

@@ -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<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
export default store;