Migrated AddPool modal to be a single page (#1782)

Also included extra fields configuration

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-03-31 09:24:21 -07:00
committed by GitHub
parent 301c4a83b5
commit bf461b8b27
13 changed files with 1888 additions and 289 deletions

View File

@@ -181,6 +181,8 @@ export const IAM_PAGES = {
"/namespaces/:tenantNamespace/tenants/:tenantName/trace",
NAMESPACE_TENANT_POOLS:
"/namespaces/:tenantNamespace/tenants/:tenantName/pools",
NAMESPACE_TENANT_POOLS_ADD:
"/namespaces/:tenantNamespace/tenants/:tenantName/add-pool",
NAMESPACE_TENANT_VOLUMES:
"/namespaces/:tenantNamespace/tenants/:tenantName/volumes",
NAMESPACE_TENANT_LICENSE:

View File

@@ -113,6 +113,9 @@ const License = React.lazy(() => import("./License/License"));
const ConfigurationOptions = React.lazy(
() => import("./Configurations/ConfigurationPanels/ConfigurationOptions")
);
const AddPool = React.lazy(
() => import("./Tenants/TenantDetails/Pools/AddPool")
);
const styles = (theme: Theme) =>
createStyles({
@@ -431,6 +434,11 @@ const Console = ({
path: IAM_PAGES.NAMESPACE_TENANT_POOLS,
forceDisplay: true,
},
{
component: AddPool,
path: IAM_PAGES.NAMESPACE_TENANT_POOLS_ADD,
forceDisplay: true,
},
{
component: TenantDetails,
path: IAM_PAGES.NAMESPACE_TENANT_VOLUMES,

View File

@@ -15,7 +15,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { SubnetInfo } from "../../License/types";
import { IAffinityModel } from "../../../../common/types";
import {IAffinityModel, IResourceModel, ITolerationModel} from "../../../../common/types";
import {
ICertificateInfo,
ISecurityContext,
@@ -60,6 +60,8 @@ export interface IAddPoolRequest {
volumes_per_server: number;
volume_configuration: IVolumeConfiguration;
affinity?: IAffinityModel;
tolerations?: ITolerationModel[];
securityContext?: ISecurityContext | null;
}
export interface IVolumeConfiguration {

View File

@@ -1,258 +0,0 @@
import React, { useEffect, useState } from "react";
import get from "lodash/get";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
modalStyleUtils,
} from "../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { generatePoolName, niceBytes } from "../../../../common/utils";
import { Button, LinearProgress, SelectChangeEvent } from "@mui/material";
import api from "../../../../common/api";
import { IAddPoolRequest, ITenant } from "../ListTenants/types";
import { ErrorResponseHandler, IAffinityModel } from "../../../../common/types";
import { getDefaultAffinity } from "./utils";
import SelectWrapper from "../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../ListTenants/utils";
import { NewPoolIcon } from "../../../../icons";
interface IAddPoolProps {
tenant: ITenant;
classes: any;
open: boolean;
onClosePoolAndReload: (shouldReload: boolean) => void;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
marginLeft: 30,
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...formFieldStyles,
...modalStyleUtils,
});
const AddPoolModal = ({
tenant,
classes,
open,
onClosePoolAndReload,
}: IAddPoolProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const [numberOfNodes, setNumberOfNodes] = useState<number>(0);
const [volumeSize, setVolumeSize] = useState<number>(0);
const [volumesPerServer, setVolumesPerSever] = useState<number>(0);
const [selectedStorageClass, setSelectedStorageClass] = useState<string>("");
const [storageClasses, setStorageClasses] = useState<Opts[]>([]);
const instanceCapacity: number = volumeSize * 1073741824 * volumesPerServer;
const totalCapacity: number = instanceCapacity * numberOfNodes;
useEffect(() => {
setSelectedStorageClass("");
setStorageClasses([]);
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant.namespace}/resourcequotas/${tenant.namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
setStorageClasses(newStorage);
if (newStorage.length > 0) {
setSelectedStorageClass(newStorage[0].value);
}
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
});
}, [tenant]);
return (
<ModalWrapper
onClose={() => onClosePoolAndReload(false)}
modalOpen={open}
title="Add Pool"
titleIcon={<NewPoolIcon />}
>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setAddSending(true);
const poolName = generatePoolName(tenant.pools);
const defaultAffinity: IAffinityModel = getDefaultAffinity(
tenant.name,
poolName
);
const data: IAddPoolRequest = {
name: poolName,
servers: numberOfNodes,
volumes_per_server: volumesPerServer,
volume_configuration: {
size: volumeSize * 1073741824,
storage_class_name: selectedStorageClass,
labels: null,
},
affinity: defaultAffinity,
};
api
.invoke(
"POST",
`/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/pools`,
data
)
.then(() => {
setAddSending(false);
onClosePoolAndReload(true);
})
.catch((err: ErrorResponseHandler) => {
setAddSending(false);
// setDeleteError(err);
});
}}
>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="number_of_nodes"
name="number_of_nodes"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setNumberOfNodes(parseInt(e.target.value));
}}
label="Number of Nodes"
value={numberOfNodes.toString(10)}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="pool_size"
name="pool_size"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumeSize(parseInt(e.target.value));
}}
label="Volume Size (Gi)"
value={volumeSize.toString(10)}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="volumes_per_sever"
name="volumes_per_sever"
type="number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
setVolumesPerSever(parseInt(e.target.value));
}}
label="Volumes per Server"
value={volumesPerServer.toString(10)}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: SelectChangeEvent<string>) => {
setSelectedStorageClass(e.target.value as string);
}}
label="Storage Class"
value={selectedStorageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{niceBytes(instanceCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{niceBytes(totalCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
</Grid>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
disabled={addSending}
onClick={() => onClosePoolAndReload(false)}
>
Cancel
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={addSending}
>
Save
</Button>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
</form>
</ModalWrapper>
);
};
export default withStyles(styles)(AddPoolModal);

View File

@@ -0,0 +1,332 @@
// 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 React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
modalStyleUtils,
} from "../../../Common/FormComponents/common/styleLibrary";
import Grid from "@mui/material/Grid";
import { generatePoolName, niceBytes } from "../../../../../common/utils";
import { LinearProgress } from "@mui/material";
import { IAddPoolRequest, ITenant } from "../../ListTenants/types";
import PageHeader from "../../../Common/PageHeader/PageHeader";
import PageLayout from "../../../Common/Layout/PageLayout";
import GenericWizard from "../../../Common/GenericWizard/GenericWizard";
import { IWizardElement } from "../../../Common/GenericWizard/types";
import history from "../../../../../history";
import PoolResources from "./PoolResources";
import ScreenTitle from "../../../Common/ScreenTitle/ScreenTitle";
import TenantsIcon from "../../../../../icons/TenantsIcon";
import {
isPoolPageValid,
resetPoolForm,
setPoolField,
setTenantDetailsLoad,
} from "../../actions";
import { AppState } from "../../../../../store";
import { connect } from "react-redux";
import PoolConfiguration from "./PoolConfiguration";
import PoolPodPlacement from "./PoolPodPlacement";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../common/types";
import { getDefaultAffinity, getNodeSelector } from "../utils";
import api from "../../../../../common/api";
import { ISecurityContext } from "../../types";
import BackLink from "../../../../../common/BackLink";
import { setErrorSnackMessage } from "../../../../../actions";
interface IAddPoolProps {
tenant: ITenant | null;
classes: any;
open: boolean;
match: any;
selectedStorageClass: string;
validPages: string[];
numberOfNodes: number;
volumeSize: number;
volumesPerServer: number;
affinityType: string;
nodeSelectorLabels: string;
withPodAntiAffinity: boolean;
securityContextEnabled: boolean;
tolerations: ITolerationModel[];
securityContext: ISecurityContext;
resetPoolForm: typeof resetPoolForm;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
marginLeft: 30,
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
pageBox: {
border: "1px solid #EAEAEA",
borderTop: 0,
},
addPoolTitle: {
border: "1px solid #EAEAEA",
borderBottom: 0,
},
...formFieldStyles,
...modalStyleUtils,
});
const requiredPages = ["setup", "affinity", "configure"];
const AddPool = ({
tenant,
classes,
resetPoolForm,
selectedStorageClass,
validPages,
numberOfNodes,
volumeSize,
affinityType,
nodeSelectorLabels,
withPodAntiAffinity,
tolerations,
securityContextEnabled,
securityContext,
volumesPerServer,
setTenantDetailsLoad,
}: IAddPoolProps) => {
const [addSending, setAddSending] = useState<boolean>(false);
const poolsURL = `/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/pools`;
useEffect(() => {
if (addSending && 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 data: IAddPoolRequest = {
name: 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(
"POST",
`/api/v1/namespaces/${tenant.namespace}/tenants/${tenant.name}/pools`,
data
)
.then(() => {
setAddSending(false);
resetPoolForm();
setTenantDetailsLoad(true);
history.push(poolsURL);
})
.catch((err: ErrorResponseHandler) => {
setAddSending(false);
setErrorSnackMessage(err);
});
}
}, [
addSending,
poolsURL,
resetPoolForm,
setTenantDetailsLoad,
affinityType,
nodeSelectorLabels,
numberOfNodes,
securityContext,
securityContextEnabled,
selectedStorageClass,
tenant,
tolerations,
volumeSize,
volumesPerServer,
withPodAntiAffinity,
]);
const cancelButton = {
label: "Cancel",
type: "other",
enabled: true,
action: () => {
resetPoolForm();
history.push(poolsURL);
},
};
const createButton = {
label: "Create",
type: "submit",
enabled:
!addSending &&
selectedStorageClass !== "" &&
requiredPages.every((v) => validPages.includes(v)),
action: () => {
setAddSending(true);
},
};
const wizardSteps: IWizardElement[] = [
{
label: "Setup",
componentRender: <PoolResources />,
buttons: [cancelButton, createButton],
},
{
label: "Configuration",
advancedOnly: true,
componentRender: <PoolConfiguration />,
buttons: [cancelButton, createButton],
},
{
label: "Pod Placement",
advancedOnly: true,
componentRender: <PoolPodPlacement />,
buttons: [cancelButton, createButton],
},
];
return (
<Fragment>
<Grid item xs={12}>
<PageHeader
label={
<Fragment>
<BackLink to={poolsURL} label={`Tenant Pools`} />
</Fragment>
}
/>
<PageLayout>
<Grid item xs={12} className={classes.addPoolTitle}>
<ScreenTitle
icon={<TenantsIcon />}
title={`Add New Pool to ${tenant?.name || ""}`}
subTitle={
<Fragment>
Namespace: {tenant?.namespace || ""} / Current Capacity:{" "}
{niceBytes((tenant?.total_size || 0).toString(10))}
</Fragment>
}
/>
</Grid>
{addSending && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<Grid item xs={12} className={classes.pageBox}>
<GenericWizard wizardSteps={wizardSteps} />
</Grid>
</PageLayout>
</Grid>
</Fragment>
);
};
const mapState = (state: AppState) => {
const addPool = state.tenants.addPool;
return {
tenant: state.tenants.tenantDetails.tenantInfo,
selectedStorageClass: addPool.fields.setup.storageClass,
validPages: addPool.validPages,
storageClasses: addPool.storageClasses,
numberOfNodes: addPool.fields.setup.numberOfNodes,
volumeSize: addPool.fields.setup.volumeSize,
volumesPerServer: addPool.fields.setup.volumesPerServer,
affinityType: addPool.fields.affinity.podAffinity,
nodeSelectorLabels: addPool.fields.affinity.nodeSelectorLabels,
withPodAntiAffinity: addPool.fields.affinity.withPodAntiAffinity,
tolerations: addPool.fields.tolerations,
securityContextEnabled: addPool.fields.configuration.securityContextEnabled,
securityContext: addPool.fields.configuration.securityContext,
};
};
const connector = connect(mapState, {
resetPoolForm,
setPoolField,
isPoolPageValid,
setErrorSnackMessage,
setTenantDetailsLoad,
});
export default withStyles(styles)(connector(AddPool));

View File

@@ -0,0 +1,293 @@
// 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 React, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, Paper } from "@mui/material";
import {
createTenantCommon,
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import { isPoolPageValid, setPoolField } from "../../actions";
import { AppState } from "../../../../../store";
import { clearValidationError } from "../../utils";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ISecurityContext } from "../../types";
interface IConfigureProps {
setPoolField: typeof setPoolField;
isPoolPageValid: typeof isPoolPageValid;
classes: any;
securityContextEnabled: boolean;
securityContext: ISecurityContext;
}
const styles = (theme: Theme) =>
createStyles({
configSectionItem: {
marginRight: 15,
"& .multiContainer": {
border: "1px solid red",
},
},
tenantCustomizationFields: {
marginLeft: 30, // 2nd Level(15+15)
width: "88%",
margin: "auto",
},
containerItem: {
marginRight: 15,
},
fieldGroup: {
...createTenantCommon.fieldGroup,
paddingTop: 15,
marginBottom: 25,
},
responsiveSectionItem: {
"@media (max-width: 900px)": {
flexFlow: "column",
alignItems: "flex-start",
"& div > div": {
marginBottom: 5,
marginRight: 0,
},
},
},
fieldSpaceTop: {
marginTop: 15,
},
...modalBasic,
...wizardCommon,
});
const PoolConfiguration = ({
classes,
setPoolField,
securityContextEnabled,
isPoolPageValid,
securityContext,
}: IConfigureProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
// Common
const updateField = useCallback(
(field: string, value: any) => {
setPoolField("configuration", field, value);
},
[setPoolField]
);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (securityContextEnabled) {
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "pool_securityContext_runAsUser",
required: true,
value: securityContext.runAsUser,
customValidation:
securityContext.runAsUser === "" ||
parseInt(securityContext.runAsUser) < 0,
customValidationMessage: `runAsUser must be present and be 0 or more`,
},
{
fieldKey: "pool_securityContext_runAsGroup",
required: true,
value: securityContext.runAsGroup,
customValidation:
securityContext.runAsGroup === "" ||
parseInt(securityContext.runAsGroup) < 0,
customValidationMessage: `runAsGroup must be present and be 0 or more`,
},
{
fieldKey: "pool_securityContext_fsGroup",
required: true,
value: securityContext.fsGroup,
customValidation:
securityContext.fsGroup === "" ||
parseInt(securityContext.fsGroup) < 0,
customValidationMessage: `fsGroup must be present and be 0 or more`,
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isPoolPageValid("configure", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [isPoolPageValid, securityContextEnabled, securityContext]);
const cleanValidation = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Configure</h3>
<span className={classes.descriptionText}>
Aditional Configurations for the new Pool
</span>
</div>
<Grid item xs={12} className={classes.configSectionItem}>
<FormSwitchWrapper
value="tenantConfig"
id="pool_configuration"
name="pool_configuration"
checked={securityContextEnabled}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("securityContextEnabled", checked);
}}
label={"Security Context"}
/>
</Grid>
{securityContextEnabled && (
<Grid item xs={12} className={classes.tenantCustomizationFields}>
<fieldset className={classes.fieldGroup}>
<legend className={classes.descriptionText}>
Pool's Security Context
</legend>
<Grid item xs={12} className={`${classes.configSectionItem}`}>
<div
className={`${classes.multiContainer} ${classes.responsiveSectionItem}`}
>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_runAsUser"
name="pool_securityContext_runAsUser"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
runAsUser: e.target.value,
});
cleanValidation("pool_securityContext_runAsUser");
}}
label="Run As User"
value={securityContext.runAsUser}
required
error={
validationErrors["pool_securityContext_runAsUser"] || ""
}
min="0"
/>
</div>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_runAsGroup"
name="pool_securityContext_runAsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
runAsGroup: e.target.value,
});
cleanValidation("pool_securityContext_runAsGroup");
}}
label="Run As Group"
value={securityContext.runAsGroup}
required
error={
validationErrors["pool_securityContext_runAsGroup"] ||
""
}
min="0"
/>
</div>
<div className={classes.containerItem}>
<InputBoxWrapper
type="number"
id="pool_securityContext_fsGroup"
name="pool_securityContext_fsGroup"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("securityContext", {
...securityContext,
fsGroup: e.target.value,
});
cleanValidation("pool_securityContext_fsGroup");
}}
label="FsGroup"
value={securityContext.fsGroup}
required
error={
validationErrors["pool_securityContext_fsGroup"] || ""
}
min="0"
/>
</div>
</div>
</Grid>
<br />
<Grid item xs={12} className={classes.configSectionItem}>
<div className={classes.multiContainer}>
<FormSwitchWrapper
value="securityContextRunAsNonRoot"
id="pool_securityContext_runAsNonRoot"
name="pool_securityContext_runAsNonRoot"
checked={securityContext.runAsNonRoot}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("securityContext", {
...securityContext,
runAsNonRoot: checked,
});
}}
label={"Do not run as Root"}
/>
</div>
</Grid>
</fieldset>
</Grid>
)}
</Paper>
);
};
const mapState = (state: AppState) => {
const configuration = state.tenants.addPool.fields.configuration;
return {
securityContextEnabled: configuration.securityContextEnabled,
securityContext: configuration.securityContext,
};
};
const connector = connect(mapState, {
setPoolField,
isPoolPageValid,
});
export default withStyles(styles)(connector(PoolConfiguration));

View File

@@ -0,0 +1,537 @@
// 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 React, { Fragment, useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, IconButton, Paper, SelectChangeEvent } from "@mui/material";
import { AppState } from "../../../../../store";
import {
isPoolPageValid,
setPoolField,
setPoolTolerationInfo,
addNewPoolToleration,
removePoolToleration,
setPoolKeyValuePairs,
} from "../../actions";
import { setModalErrorSnackMessage } from "../../../../../actions";
import {
modalBasic,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import {
ErrorResponseHandler,
ITolerationModel,
} from "../../../../../common/types";
import { LabelKeyPair } from "../../types";
import RadioGroupSelector from "../../../Common/FormComponents/RadioGroupSelector/RadioGroupSelector";
import FormSwitchWrapper from "../../../Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper";
import api from "../../../../../common/api";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import AddIcon from "../../../../../icons/AddIcon";
import RemoveIcon from "../../../../../icons/RemoveIcon";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import TolerationSelector from "../../../Common/TolerationSelector/TolerationSelector";
interface IAffinityProps {
classes: any;
podAffinity: string;
nodeSelectorLabels: string;
withPodAntiAffinity: boolean;
keyValuePairs: LabelKeyPair[];
tolerations: ITolerationModel[];
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
setPoolField: typeof setPoolField;
isPoolPageValid: typeof isPoolPageValid;
setPoolKeyValuePairs: typeof setPoolKeyValuePairs;
setPoolTolerationInfo: typeof setPoolTolerationInfo;
removePoolToleration: typeof removePoolToleration;
addNewPoolToleration: typeof addNewPoolToleration;
}
const styles = (theme: Theme) =>
createStyles({
overlayAction: {
marginLeft: 10,
display: "flex",
alignItems: "center",
"& svg": {
maxWidth: 15,
maxHeight: 15,
},
"& button": {
background: "#EAEAEA",
},
},
affinityConfigField: {
display: "flex",
},
affinityFieldLabel: {
display: "flex",
flexFlow: "column",
flex: 1,
},
radioField: {
display: "flex",
alignItems: "flex-start",
marginTop: 10,
"& div:first-child": {
display: "flex",
flexFlow: "column",
alignItems: "baseline",
textAlign: "left !important",
},
},
affinityLabelKey: {
"& div:first-child": {
marginBottom: 0,
},
},
affinityLabelValue: {
marginLeft: 10,
"& div:first-child": {
marginBottom: 0,
},
},
rowActions: {
display: "flex",
alignItems: "center",
},
fieldContainer: {
marginBottom: 0,
},
affinityRow: {
marginBottom: 10,
display: "flex",
},
...modalBasic,
...wizardCommon,
});
interface OptionPair {
label: string;
value: string;
}
const Affinity = ({
classes,
podAffinity,
nodeSelectorLabels,
withPodAntiAffinity,
setModalErrorSnackMessage,
keyValuePairs,
setPoolKeyValuePairs,
setPoolField,
isPoolPageValid,
tolerations,
setPoolTolerationInfo,
removePoolToleration,
addNewPoolToleration,
}: IAffinityProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const [loading, setLoading] = useState<boolean>(true);
const [keyValueMap, setKeyValueMap] = useState<{ [key: string]: string[] }>(
{}
);
const [keyOptions, setKeyOptions] = useState<OptionPair[]>([]);
// Common
const updateField = useCallback(
(field: string, value: any) => {
setPoolField("affinity", field, value);
},
[setPoolField]
);
useEffect(() => {
if (loading) {
api
.invoke("GET", `/api/v1/nodes/labels`)
.then((res: { [key: string]: string[] }) => {
setLoading(false);
setKeyValueMap(res);
let keys: OptionPair[] = [];
for (let k in res) {
keys.push({
label: k,
value: k,
});
}
setKeyOptions(keys);
})
.catch((err: ErrorResponseHandler) => {
setLoading(false);
setModalErrorSnackMessage(err);
setKeyValueMap({});
});
}
}, [setModalErrorSnackMessage, loading]);
useEffect(() => {
if (keyValuePairs) {
const vlr = keyValuePairs
.filter((kvp) => kvp.key !== "")
.map((kvp) => `${kvp.key}=${kvp.value}`)
.filter((kvs, i, a) => a.indexOf(kvs) === i);
const vl = vlr.join("&");
updateField("nodeSelectorLabels", vl);
}
}, [keyValuePairs, updateField]);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [];
if (podAffinity === "nodeSelector") {
let valid = true;
const splittedLabels = nodeSelectorLabels.split("&");
if (splittedLabels.length === 1 && splittedLabels[0] === "") {
valid = false;
}
splittedLabels.forEach((item: string, index: number) => {
const splitItem = item.split("=");
if (splitItem.length !== 2) {
valid = false;
}
if (index + 1 !== splittedLabels.length) {
if (splitItem[0] === "" || splitItem[1] === "") {
valid = false;
}
}
});
customAccountValidation = [
...customAccountValidation,
{
fieldKey: "labels",
required: true,
value: nodeSelectorLabels,
customValidation: !valid,
customValidationMessage:
"You need to add at least one label key-pair",
},
];
}
const commonVal = commonFormValidation(customAccountValidation);
isPoolPageValid("affinity", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [isPoolPageValid, podAffinity, nodeSelectorLabels]);
const updateToleration = (index: number, field: string, value: any) => {
const alterToleration = { ...tolerations[index], [field]: value };
setPoolTolerationInfo(index, alterToleration);
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>Pod Placement</h3>
<span className={classes.descriptionText}>
Configure how pods will be assigned to nodes
</span>
</div>
<Grid item xs={12} className={classes.affinityConfigField}>
<Grid item className={classes.affinityFieldLabel}>
<div className={classes.label}>Type</div>
<div
className={`${classes.descriptionText} ${classes.affinityHelpText}`}
>
MinIO supports multiple configurations for Pod Affinity
</div>
<Grid item className={classes.radioField}>
<RadioGroupSelector
currentSelection={podAffinity}
id="affinity-options"
name="affinity-options"
label={" "}
onChange={(e) => {
updateField("podAffinity", e.target.value);
}}
selectorOptions={[
{ label: "None", value: "none" },
{ label: "Default (Pod Anti-Affinnity)", value: "default" },
{ label: "Node Selector", value: "nodeSelector" },
]}
/>
</Grid>
</Grid>
</Grid>
{podAffinity === "nodeSelector" && (
<Fragment>
<br />
<Grid item xs={12}>
<FormSwitchWrapper
value="with_pod_anti_affinity"
id="with_pod_anti_affinity"
name="with_pod_anti_affinity"
checked={withPodAntiAffinity}
onChange={(e) => {
const targetD = e.target;
const checked = targetD.checked;
updateField("withPodAntiAffinity", checked);
}}
label={"With Pod Anti-Affinity"}
/>
</Grid>
<Grid item xs={12}>
<h3>Labels</h3>
<span className={classes.error}>{validationErrors["labels"]}</span>
<Grid container>
{keyValuePairs &&
keyValuePairs.map((kvp, i) => {
return (
<Grid
item
xs={12}
className={classes.affinityRow}
key={`affinity-keyVal-${i.toString()}`}
>
<Grid item xs={5} className={classes.affinityLabelKey}>
{keyOptions.length > 0 && (
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
const newKey = e.target.value as string;
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].key = e.target.value as string;
arrCp[i].value = keyValueMap[newKey][0];
setPoolKeyValuePairs(arrCp);
}}
id="select-access-policy"
name="select-access-policy"
label={""}
value={kvp.key}
options={keyOptions}
/>
)}
{keyOptions.length === 0 && (
<InputBoxWrapper
id={`nodeselector-key-${i.toString()}`}
label={""}
name={`nodeselector-${i.toString()}`}
value={kvp.key}
onChange={(e) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].key = e.target.value;
setPoolKeyValuePairs(arrCp);
}}
index={i}
placeholder={"Key"}
/>
)}
</Grid>
<Grid item xs={5} className={classes.affinityLabelValue}>
{keyOptions.length > 0 && (
<SelectWrapper
onChange={(e: SelectChangeEvent<string>) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].value = e.target.value as string;
setPoolKeyValuePairs(arrCp);
}}
id="select-access-policy"
name="select-access-policy"
label={""}
value={kvp.value}
options={
keyValueMap[kvp.key]
? keyValueMap[kvp.key].map((v) => {
return { label: v, value: v };
})
: []
}
/>
)}
{keyOptions.length === 0 && (
<InputBoxWrapper
id={`nodeselector-value-${i.toString()}`}
label={""}
name={`nodeselector-${i.toString()}`}
value={kvp.value}
onChange={(e) => {
const arrCp: LabelKeyPair[] = Object.assign(
[],
keyValuePairs
);
arrCp[i].value = e.target.value;
setPoolKeyValuePairs(arrCp);
}}
index={i}
placeholder={"value"}
/>
)}
</Grid>
<Grid item xs={2} className={classes.rowActions}>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const arrCp = Object.assign([], keyValuePairs);
if (keyOptions.length > 0) {
arrCp.push({
key: keyOptions[0].value,
value: keyValueMap[keyOptions[0].value][0],
});
} else {
arrCp.push({ key: "", value: "" });
}
setPoolKeyValuePairs(arrCp);
}}
>
<AddIcon />
</IconButton>
</div>
{keyValuePairs.length > 1 && (
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => {
const arrCp = keyValuePairs.filter(
(item, index) => index !== i
);
setPoolKeyValuePairs(arrCp);
}}
>
<RemoveIcon />
</IconButton>
</div>
)}
</Grid>
</Grid>
);
})}
</Grid>
</Grid>
</Fragment>
)}
<Grid item xs={12} className={classes.affinityConfigField}>
<Grid item className={classes.affinityFieldLabel}>
<h3>Tolerations</h3>
<span className={classes.error}>
{validationErrors["tolerations"]}
</span>
<Grid container>
{tolerations &&
tolerations.map((tol, i) => {
return (
<Grid
item
xs={12}
className={classes.affinityRow}
key={`affinity-keyVal-${i.toString()}`}
>
<TolerationSelector
effect={tol.effect}
onEffectChange={(value) => {
updateToleration(i, "effect", value);
}}
tolerationKey={tol.key}
onTolerationKeyChange={(value) => {
updateToleration(i, "key", value);
}}
operator={tol.operator}
onOperatorChange={(value) => {
updateToleration(i, "operator", value);
}}
value={tol.value}
onValueChange={(value) => {
updateToleration(i, "value", value);
}}
tolerationSeconds={tol.tolerationSeconds?.seconds || 0}
onSecondsChange={(value) => {
updateToleration(i, "tolerationSeconds", {
seconds: value,
});
}}
index={i}
/>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={addNewPoolToleration}
disabled={i !== tolerations.length - 1}
>
<AddIcon />
</IconButton>
</div>
<div className={classes.overlayAction}>
<IconButton
size={"small"}
onClick={() => removePoolToleration(i)}
disabled={tolerations.length <= 1}
>
<RemoveIcon />
</IconButton>
</div>
</Grid>
);
})}
</Grid>
</Grid>
</Grid>
</Paper>
);
};
const mapState = (state: AppState) => {
const addPool = state.tenants.addPool;
return {
podAffinity: addPool.fields.affinity.podAffinity,
nodeSelectorLabels: addPool.fields.affinity.nodeSelectorLabels,
withPodAntiAffinity: addPool.fields.affinity.withPodAntiAffinity,
keyValuePairs: addPool.fields.nodeSelectorPairs,
tolerations: addPool.fields.tolerations,
};
};
const connector = connect(mapState, {
setModalErrorSnackMessage,
setPoolField,
isPoolPageValid,
setPoolKeyValuePairs,
setPoolTolerationInfo,
addNewPoolToleration,
removePoolToleration,
});
export default withStyles(styles)(connector(Affinity));

View File

@@ -0,0 +1,316 @@
// 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 React, { useEffect, useState } from "react";
import get from "lodash/get";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
formFieldStyles,
wizardCommon,
} from "../../../Common/FormComponents/common/styleLibrary";
import InputBoxWrapper from "../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { niceBytes } from "../../../../../common/utils";
import { Paper, SelectChangeEvent } from "@mui/material";
import api from "../../../../../common/api";
import { ITenant } from "../../ListTenants/types";
import { ErrorResponseHandler } from "../../../../../common/types";
import SelectWrapper from "../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import { IQuotaElement, IQuotas, Opts } from "../../ListTenants/utils";
import { AppState } from "../../../../../store";
import { connect } from "react-redux";
import {
isPoolPageValid,
setPoolField,
setPoolStorageClasses,
} from "../../actions";
import {
commonFormValidation,
IValidation,
} from "../../../../../utils/validationFunctions";
import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
interface IPoolResourcesProps {
tenant: ITenant | null;
classes: any;
storageClasses: Opts[];
numberOfNodes: string;
storageClass: string;
volumeSize: string;
volumesPerServer: string;
setPoolField: typeof setPoolField;
isPoolPageValid: typeof isPoolPageValid;
setPoolStorageClasses: typeof setPoolStorageClasses;
}
const styles = (theme: Theme) =>
createStyles({
buttonContainer: {
textAlign: "right",
},
bottomContainer: {
display: "flex",
flexGrow: 1,
alignItems: "center",
margin: "auto",
justifyContent: "center",
"& div": {
width: 150,
"@media (max-width: 900px)": {
flexFlow: "column",
},
},
},
factorElements: {
display: "flex",
justifyContent: "flex-start",
marginLeft: 30,
},
sizeNumber: {
fontSize: 35,
fontWeight: 700,
textAlign: "center",
},
sizeDescription: {
fontSize: 14,
color: "#777",
textAlign: "center",
},
...formFieldStyles,
...wizardCommon,
});
const PoolResources = ({
tenant,
classes,
storageClasses,
numberOfNodes,
storageClass,
volumeSize,
volumesPerServer,
setPoolField,
setPoolStorageClasses,
isPoolPageValid,
}: IPoolResourcesProps) => {
const [validationErrors, setValidationErrors] = useState<any>({});
const instanceCapacity: number =
parseInt(volumeSize) * 1073741824 * parseInt(volumesPerServer);
const totalCapacity: number = instanceCapacity * parseInt(numberOfNodes);
// Validation
useEffect(() => {
let customAccountValidation: IValidation[] = [
{
fieldKey: "number_of_nodes",
required: true,
value: numberOfNodes.toString(),
customValidation:
parseInt(numberOfNodes) < 1 || isNaN(parseInt(numberOfNodes)),
customValidationMessage: "Number of servers must be at least 1",
},
{
fieldKey: "pool_size",
required: true,
value: volumeSize.toString(),
customValidation:
parseInt(volumeSize) < 1 || isNaN(parseInt(volumeSize)),
customValidationMessage: "Pool Size cannot be 0",
},
{
fieldKey: "volumes_per_server",
required: true,
value: volumesPerServer.toString(),
customValidation:
parseInt(volumesPerServer) < 1 || isNaN(parseInt(volumesPerServer)),
customValidationMessage: "1 volume or more are required",
},
];
const commonVal = commonFormValidation(customAccountValidation);
isPoolPageValid("setup", Object.keys(commonVal).length === 0);
setValidationErrors(commonVal);
}, [
isPoolPageValid,
numberOfNodes,
volumeSize,
volumesPerServer,
storageClass,
]);
useEffect(() => {
if (storageClasses.length === 0 && tenant) {
api
.invoke(
"GET",
`/api/v1/namespaces/${tenant.namespace}/resourcequotas/${tenant.namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
setPoolField("setup", "storageClass", newStorage[0].value);
setPoolStorageClasses(newStorage);
})
.catch((err: ErrorResponseHandler) => {
console.error(err);
});
}
}, [tenant, storageClasses, setPoolStorageClasses, setPoolField]);
const setFieldInfo = (fieldName: string, value: any) => {
setPoolField("setup", fieldName, value);
};
return (
<Paper className={classes.paperWrapper}>
<div className={classes.headerElement}>
<h3 className={classes.h3Section}>New Pool Configuration</h3>
<span className={classes.descriptionText}>
Configure a new Pool to expand MinIO storage
</span>
</div>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="number_of_nodes"
name="number_of_nodes"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("numberOfNodes", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("numberOfNodes", 0);
}
}}
label="Number of Servers"
value={numberOfNodes}
error={validationErrors["number_of_nodes"] || ""}
pattern={"[0-9]*"}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="pool_size"
name="pool_size"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("volumeSize", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("volumeSize", 0);
}
}}
label="Volume Size"
value={volumeSize}
error={validationErrors["pool_size"] || ""}
pattern={"[0-9]*"}
overlayObject={
<InputUnitMenu
id={"quota_unit"}
onUnitChange={() => {}}
unitSelected={"Gi"}
unitsList={[{ label: "Gi", value: "Gi" }]}
disabled={true}
/>
}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="volumes_per_sever"
name="volumes_per_sever"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const intValue = parseInt(e.target.value);
if (e.target.validity.valid && !isNaN(intValue)) {
setFieldInfo("volumesPerServer", intValue);
} else if (isNaN(intValue)) {
setFieldInfo("volumesPerServer", 0);
}
}}
label="Volumes per Server"
value={volumesPerServer}
error={validationErrors["volumes_per_server"] || ""}
pattern={"[0-9]*"}
/>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<SelectWrapper
id="storage_class"
name="storage_class"
onChange={(e: SelectChangeEvent<string>) => {
setFieldInfo("storageClasses", e.target.value as string);
}}
label="Storage Class"
value={storageClass}
options={storageClasses}
disabled={storageClasses.length < 1}
/>
</Grid>
<Grid item xs={12} className={classes.bottomContainer}>
<div className={classes.factorElements}>
<div>
<div className={classes.sizeNumber}>
{niceBytes(instanceCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Instance Capacity</div>
</div>
<div>
<div className={classes.sizeNumber}>
{niceBytes(totalCapacity.toString(10))}
</div>
<div className={classes.sizeDescription}>Total Capacity</div>
</div>
</div>
</Grid>
</Paper>
);
};
const mapState = (state: AppState) => {
const setupFields = state.tenants.addPool.fields.setup;
return {
tenant: state.tenants.tenantDetails.tenantInfo,
storageClasses: state.tenants.addPool.storageClasses,
numberOfNodes: setupFields.numberOfNodes.toString(),
storageClass: setupFields.storageClass,
volumeSize: setupFields.volumeSize.toString(),
volumesPerServer: setupFields.volumesPerServer.toString(),
};
};
const connector = connect(mapState, {
setPoolField,
isPoolPageValid,
setPoolStorageClasses,
});
export default withStyles(styles)(connector(PoolResources));

View File

@@ -31,7 +31,6 @@ import { AddIcon } from "../../../../icons";
import { IPool, ITenant } from "../ListTenants/types";
import { setErrorSnackMessage } from "../../../../actions";
import TableWrapper from "../../Common/TableWrapper/TableWrapper";
import AddPoolModal from "./AddPoolModal";
import InputAdornment from "@mui/material/InputAdornment";
import { AppState } from "../../../../store";
import { setTenantDetailsLoad } from "../actions";
@@ -42,6 +41,7 @@ interface IPoolsSummary {
classes: any;
tenant: ITenant | null;
loadingTenant: boolean;
history: any;
setErrorSnackMessage: typeof setErrorSnackMessage;
setTenantDetailsLoad: typeof setTenantDetailsLoad;
}
@@ -59,9 +59,9 @@ const PoolsSummary = ({
tenant,
loadingTenant,
setTenantDetailsLoad,
history,
}: IPoolsSummary) => {
const [pools, setPools] = useState<IPool[]>([]);
const [addPoolOpen, setAddPool] = useState<boolean>(false);
const [filter, setFilter] = useState<string>("");
useEffect(() => {
@@ -71,14 +71,6 @@ const PoolsSummary = ({
}
}, [tenant]);
const onClosePoolAndRefresh = (reload: boolean) => {
setAddPool(false);
if (reload) {
setTenantDetailsLoad(true);
}
};
const filteredPools = pools.filter((pool) => {
if (pool.name.toLowerCase().includes(filter.toLowerCase())) {
return true;
@@ -89,14 +81,6 @@ const PoolsSummary = ({
return (
<Fragment>
{addPoolOpen && tenant !== null && (
<AddPoolModal
open={addPoolOpen}
onClosePoolAndReload={onClosePoolAndRefresh}
tenant={tenant}
/>
)}
<h1 className={classes.sectionTitle}>Pools</h1>
<Grid container>
<Grid item xs={12} className={classes.actionsTray}>
@@ -123,7 +107,11 @@ const PoolsSummary = ({
tooltip={"Expand Tenant"}
text={"Expand Tenant"}
onClick={() => {
setAddPool(true);
history.push(
`/namespaces/${tenant?.namespace || ""}/tenants/${
tenant?.name || ""
}/add-pool`
);
}}
icon={<AddIcon />}
color="primary"

View File

@@ -174,7 +174,6 @@ const TenantDetails = ({
match,
history,
loadingTenant,
currentTab,
selectedTenant,
tenantInfo,
selectedNamespace,
@@ -183,7 +182,6 @@ const TenantDetails = ({
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
setTenantTab,
}: ITenantDetailsProps) => {
const [yamlScreenOpen, setYamlScreenOpen] = useState<boolean>(false);
@@ -589,7 +587,6 @@ const TenantDetails = ({
const mapState = (state: AppState) => ({
loadingTenant: state.tenants.tenantDetails.loadingTenant,
currentTab: state.tenants.tenantDetails.currentTab,
selectedTenant: state.tenants.tenantDetails.currentTenant,
selectedNamespace: state.tenants.tenantDetails.currentNamespace,
tenantInfo: state.tenants.tenantDetails.tenantInfo,
@@ -601,7 +598,6 @@ const connector = connect(mapState, {
setTenantDetailsLoad,
setTenantName,
setTenantInfo,
setTenantTab,
});
export default withStyles(styles)(connector(TenantDetails));

View File

@@ -39,15 +39,24 @@ import {
ADD_TENANT_SET_STORAGE_CLASSES_LIST,
ADD_TENANT_SET_STORAGE_TYPE,
ADD_TENANT_UPDATE_FIELD,
ADD_TENANT_SET_KEY_PAIR_VALUE,
ADD_TENANT_SET_TOLERATION_VALUE,
ADD_TENANT_ADD_NEW_TOLERATION,
ADD_TENANT_REMOVE_TOLERATION_ROW,
TENANT_DETAILS_SET_CURRENT_TENANT,
TENANT_DETAILS_SET_LOADING,
TENANT_DETAILS_SET_TAB,
TENANT_DETAILS_SET_TENANT,
ADD_TENANT_SET_KEY_PAIR_VALUE,
ADD_TENANT_SET_TOLERATION_VALUE,
ADD_TENANT_ADD_NEW_TOLERATION,
LabelKeyPair,
ADD_TENANT_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_LOADING,
ADD_POOL_RESET_FORM,
ADD_POOL_SET_VALUE,
IAddPoolFields,
ADD_POOL_SET_PAGE_VALID,
ADD_POOL_SET_POOL_STORAGE_CLASSES,
ADD_POOL_SET_TOLERATION_VALUE,
ADD_POOL_ADD_NEW_TOLERATION,
ADD_POOL_REMOVE_TOLERATION_ROW, ADD_POOL_SET_KEY_PAIR_VALUE,
} from "./types";
import { ITolerationModel } from "../../../common/types";
@@ -323,3 +332,77 @@ export const removeToleration = (index: number) => {
index,
};
};
// Add Pool
export const setPoolLoading = (state: boolean) => {
return {
type: ADD_POOL_SET_LOADING,
state,
};
};
export const resetPoolForm = () => {
return {
type: ADD_POOL_RESET_FORM,
};
};
export const setPoolField = (
page: keyof IAddPoolFields,
field: string,
value: any
) => {
return {
type: ADD_POOL_SET_VALUE,
page,
field,
value,
};
};
export const isPoolPageValid = (page: string, status: boolean) => {
return {
type: ADD_POOL_SET_PAGE_VALID,
page,
status,
};
};
export const setPoolStorageClasses = (storageClasses: Opts[]) => {
return {
type: ADD_POOL_SET_POOL_STORAGE_CLASSES,
storageClasses,
};
};
export const setPoolTolerationInfo = (
index: number,
tolerationValue: ITolerationModel
) => {
return {
type: ADD_POOL_SET_TOLERATION_VALUE,
index,
toleration: tolerationValue,
};
};
export const addNewPoolToleration = () => {
return {
type: ADD_POOL_ADD_NEW_TOLERATION,
};
};
export const removePoolToleration = (index: number) => {
return {
type: ADD_POOL_REMOVE_TOLERATION_ROW,
index,
};
};
export const setPoolKeyValuePairs = (newArray: LabelKeyPair[]) => {
return {
type: ADD_POOL_SET_KEY_PAIR_VALUE,
newArray,
};
};

View File

@@ -42,12 +42,21 @@ import {
ADD_TENANT_SET_STORAGE_TYPE,
ADD_TENANT_SET_TOLERATION_VALUE,
ADD_TENANT_UPDATE_FIELD,
ITenantState,
TENANT_DETAILS_SET_CURRENT_TENANT,
TENANT_DETAILS_SET_LOADING,
TENANT_DETAILS_SET_TAB,
TENANT_DETAILS_SET_TENANT,
ADD_POOL_SET_LOADING,
ADD_POOL_SET_VALUE,
ADD_POOL_RESET_FORM,
ITenantState,
TenantsManagementTypes,
ADD_POOL_SET_PAGE_VALID,
ADD_POOL_SET_POOL_STORAGE_CLASSES,
ADD_POOL_ADD_NEW_TOLERATION,
ADD_POOL_SET_TOLERATION_VALUE,
ADD_POOL_REMOVE_TOLERATION_ROW,
ADD_POOL_SET_KEY_PAIR_VALUE,
} from "./types";
import { KeyPair } from "./ListTenants/utils";
import { getRandomString } from "./utils";
@@ -355,6 +364,44 @@ const initialState: ITenantState = {
tenantInfo: null,
currentTab: "summary",
},
addPool: {
addPoolLoading: false,
validPages: ["affinity", "configure"],
storageClasses: [],
limitSize: {},
fields: {
setup: {
numberOfNodes: 0,
storageClass: "",
volumeSize: 0,
volumesPerServer: 0,
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
configuration: {
securityContextEnabled: false,
securityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
nodeSelectorPairs: [{ key: "", value: "" }],
tolerations: [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
],
},
},
};
export function tenantsReducer(
@@ -995,7 +1042,7 @@ export function tenantsReducer(
const cleanTolerationArray = state.createTenant.tolerations.filter(
(_, index) => index !== action.index
);
return {
...state,
createTenant: {
@@ -1003,7 +1050,161 @@ export function tenantsReducer(
tolerations: [...cleanTolerationArray],
},
};
case ADD_POOL_SET_LOADING:
return {
...state,
addPool: {
...state.addPool,
addPoolLoading: action.state,
},
};
case ADD_POOL_SET_VALUE:
if (has(newState.addPool.fields, `${action.page}.${action.field}`)) {
const originPageNameItems = get(
newState.addPool.fields,
`${action.page}`,
{}
);
let newValue: any = {};
newValue[action.field] = action.value;
const joinValue = { ...originPageNameItems, ...newValue };
newState.addPool.fields[action.page] = { ...joinValue };
return { ...newState };
}
return state;
case ADD_POOL_SET_PAGE_VALID:
const nvPoolPV = [...state.addPool.validPages];
if (action.status) {
if (!nvPoolPV.includes(action.page)) {
nvPoolPV.push(action.page);
newState.addPool.validPages = [...nvPoolPV];
}
} else {
const newSetOfPages = nvPoolPV.filter((elm) => elm !== action.page);
newState.addPool.validPages = [...newSetOfPages];
}
return { ...newState };
case ADD_POOL_SET_POOL_STORAGE_CLASSES:
return {
...newState,
addPool: {
...newState.addPool,
storageClasses: action.storageClasses,
},
};
case ADD_POOL_SET_TOLERATION_VALUE:
const newPoolTolerationValue = [...state.addPool.fields.tolerations];
newPoolTolerationValue[action.index] = action.toleration;
return {
...state,
addPool: {
...state.addPool,
fields: {
...state.addPool.fields,
tolerations: [...newPoolTolerationValue],
},
},
};
case ADD_POOL_ADD_NEW_TOLERATION:
const newPoolTolerationArray = [
...state.addPool.fields.tolerations,
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
];
return {
...state,
addPool: {
...state.addPool,
fields: {
...state.addPool.fields,
tolerations: [...newPoolTolerationArray],
},
},
};
case ADD_POOL_REMOVE_TOLERATION_ROW:
const cleanPoolTolerationArray = state.addPool.fields.tolerations.filter(
(_, index) => index !== action.index
);
return {
...state,
addPool: {
...state.addPool,
fields: {
...state.addPool.fields,
tolerations: [...cleanPoolTolerationArray],
},
},
};
case ADD_POOL_SET_KEY_PAIR_VALUE:
return {
...state,
addPool: {
...state.addPool,
fields: {
...state.addPool.fields,
nodeSelectorPairs: action.newArray,
},
},
};
case ADD_POOL_RESET_FORM:
return {
...state,
addPool: {
addPoolLoading: false,
validPages: ["affinity", "configure"],
storageClasses: [],
limitSize: {},
fields: {
setup: {
numberOfNodes: 0,
storageClass: "",
volumeSize: 0,
volumesPerServer: 0,
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
configuration: {
securityContextEnabled: false,
securityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
nodeSelectorPairs: [{ key: "", value: "" }],
tolerations: [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
],
},
},
};
default:
return state;
}

View File

@@ -84,6 +84,20 @@ export const TENANT_DETAILS_SET_CURRENT_TENANT =
export const TENANT_DETAILS_SET_TENANT = "TENANT_DETAILS/SET_TENANT";
export const TENANT_DETAILS_SET_TAB = "TENANT_DETAILS/SET_TAB";
// Add Pool
export const ADD_POOL_SET_POOL_STORAGE_CLASSES =
"ADD_POOL/SET_POOL_STORAGE_CLASSES";
export const ADD_POOL_SET_PAGE_VALID = "ADD_POOL/SET_PAGE_VALID";
export const ADD_POOL_SET_VALUE = "ADD_POOL/SET_VALUE";
export const ADD_POOL_SET_LOADING = "ADD_POOL/SET_LOADING";
export const ADD_POOL_RESET_FORM = "ADD_POOL/RESET_FORM";
export const ADD_POOL_SET_KEY_PAIR_VALUE = "ADD_POOL/SET_KEY_PAIR_VALUE";
// Pool Tolerations
export const ADD_POOL_SET_TOLERATION_VALUE = "ADD_POOL/SET_TOLERATION_VALUE";
export const ADD_POOL_ADD_NEW_TOLERATION = "ADD_POOL/ADD_NEW_TOLERATION";
export const ADD_POOL_REMOVE_TOLERATION_ROW = "ADD_POOL/REMOVE_TOLERATION_ROW";
export interface ICertificateInfo {
name: string;
serialNumber: string;
@@ -355,6 +369,7 @@ export interface ITenantDetails {
export interface ITenantState {
createTenant: ICreateTenant;
tenantDetails: ITenantDetails;
addPool: IAddPool;
}
export interface ILabelKeyPair {
@@ -374,6 +389,34 @@ export interface NodeMaxAllocatableResources {
max_allocatable_mem: number;
}
export interface IAddPoolSetup {
numberOfNodes: number;
volumeSize: number;
volumesPerServer: number;
storageClass: string;
}
export interface IPoolConfiguration {
securityContextEnabled: boolean;
securityContext: ISecurityContext;
}
export interface IAddPoolFields {
setup: IAddPoolSetup;
affinity: ITenantAffinity;
configuration: IPoolConfiguration;
tolerations: ITolerationModel[];
nodeSelectorPairs: LabelKeyPair[];
}
export interface IAddPool {
addPoolLoading: boolean;
validPages: string[];
storageClasses: Opts[];
limitSize: any;
fields: IAddPoolFields;
}
interface SetTenantWizardPage {
type: typeof ADD_TENANT_SET_CURRENT_PAGE;
page: number;
@@ -545,6 +588,53 @@ interface RemoveTolerationRow {
index: number;
}
interface SetPoolLoading {
type: typeof ADD_POOL_SET_LOADING;
state: boolean;
}
interface ResetPoolForm {
type: typeof ADD_POOL_RESET_FORM;
}
interface SetFieldValue {
type: typeof ADD_POOL_SET_VALUE;
page: keyof IAddPoolFields;
field: string;
value: any;
}
interface SetPoolPageValid {
type: typeof ADD_POOL_SET_PAGE_VALID;
page: string;
status: boolean;
}
interface SetPoolStorageClasses {
type: typeof ADD_POOL_SET_POOL_STORAGE_CLASSES;
storageClasses: Opts[];
}
interface SetPoolTolerationValue {
type: typeof ADD_POOL_SET_TOLERATION_VALUE;
index: number;
toleration: ITolerationModel;
}
interface AddNewPoolToleration {
type: typeof ADD_POOL_ADD_NEW_TOLERATION;
}
interface RemovePoolTolerationRow {
type: typeof ADD_POOL_REMOVE_TOLERATION_ROW;
index: number;
}
interface SetPoolSelectorKeyPairValueArray {
type: typeof ADD_POOL_SET_KEY_PAIR_VALUE;
newArray: LabelKeyPair[];
}
export type FieldsToHandle = INameTenantFields;
export type TenantsManagementTypes =
@@ -577,4 +667,13 @@ export type TenantsManagementTypes =
| SetTenantTab
| SetTolerationValue
| AddNewToleration
| RemoveTolerationRow;
| RemoveTolerationRow
| SetPoolLoading
| ResetPoolForm
| SetFieldValue
| SetPoolPageValid
| SetPoolStorageClasses
| SetPoolTolerationValue
| AddNewPoolToleration
| RemovePoolTolerationRow
| SetPoolSelectorKeyPairValueArray;