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:
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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));
|
||||
@@ -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"
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user