Create Tenant Namespace Field move to Thunks (#2052)

Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
Daniel Valdivia
2022-05-30 20:06:42 -07:00
committed by GitHub
parent 45715293ea
commit 80391b867c
5 changed files with 356 additions and 544 deletions

View File

@@ -14,54 +14,33 @@
// 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,
useMemo,
useState,
} from "react";
import React, { Fragment, useCallback, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import get from "lodash/get";
import debounce from "lodash/debounce";
import Grid from "@mui/material/Grid";
import {
formFieldStyles,
modalBasic,
wizardCommon,
} from "../../../../Common/FormComponents/common/styleLibrary";
import {
getLimitSizes,
IQuotaElement,
IQuotas,
} from "../../../ListTenants/utils";
import { AppState } from "../../../../../../store";
import { commonFormValidation } from "../../../../../../utils/validationFunctions";
import { clearValidationError } from "../../../utils";
import { ErrorResponseHandler } from "../../../../../../common/types";
import api from "../../../../../../common/api";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
import AddIcon from "../../../../../../icons/AddIcon";
import AddNamespaceModal from "../helpers/AddNamespaceModal";
import SizePreview from "../SizePreview";
import TenantSize from "./TenantSize";
import { Paper, SelectChangeEvent } from "@mui/material";
import { IMkEnvs, mkPanelConfigurations } from "./utils";
import { setModalErrorSnackMessage } from "../../../../../../systemSlice";
import {
isPageValid,
setLimitSize,
setStorageClassesList,
setStorageType,
setTenantName,
updateAddField,
} from "../../createTenantSlice";
import { selFeatures } from "../../../../consoleSlice";
import NamespaceSelector from "./NamespaceSelector";
const styles = (theme: Theme) =>
createStyles({
@@ -110,9 +89,6 @@ interface INameTenantMainScreen {
const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
const dispatch = useDispatch();
const namespace = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.namespace
);
const selectedStorageClass = useSelector(
(state: AppState) =>
state.createTenant.fields.nameTenant.selectedStorageClass
@@ -126,13 +102,6 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
);
const features = useSelector(selFeatures);
const [validationErrors, setValidationErrors] = useState<any>({});
const [emptyNamespace, setEmptyNamespace] = useState<boolean>(true);
const [loadingNamespaceInfo, setLoadingNamespaceInfo] =
useState<boolean>(false);
const [showCreateButton, setShowCreateButton] = useState<boolean>(false);
const [openAddNSConfirm, setOpenAddNSConfirm] = useState<boolean>(false);
// Common
const updateField = useCallback(
(field: string, value: string) => {
@@ -143,157 +112,17 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
[dispatch]
);
// Storage classes retrieval
const getNamespaceInformation = useCallback(() => {
setShowCreateButton(false);
// Empty tenantValidation
api
.invoke("GET", `/api/v1/namespaces/${namespace}/tenants`)
.then((res: any[]) => {
const tenantsList = get(res, "tenants", []);
if (tenantsList && tenantsList.length > 0) {
setEmptyNamespace(false);
setLoadingNamespaceInfo(false);
return;
}
setEmptyNamespace(true);
// Storagequotas retrieval
api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/resourcequotas/${namespace}-storagequota`
)
.then((res: IQuotas) => {
const elements: IQuotaElement[] = get(res, "elements", []);
dispatch(setLimitSize(getLimitSizes(res)));
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
dispatch(setStorageClassesList(newStorage));
const stExists = newStorage.findIndex(
(storageClass) => storageClass.value === selectedStorageClass
);
if (newStorage.length > 0 && stExists === -1) {
updateField("selectedStorageClass", newStorage[0].value);
} else if (newStorage.length === 0) {
updateField("selectedStorageClass", "");
dispatch(setStorageClassesList([]));
}
setLoadingNamespaceInfo(false);
})
.catch((err: ErrorResponseHandler) => {
setLoadingNamespaceInfo(false);
setShowCreateButton(true);
updateField("selectedStorageClass", "");
dispatch(setStorageClassesList([]));
console.error("Namespace error: ", err);
});
})
.catch((err: ErrorResponseHandler) => {
dispatch(
setModalErrorSnackMessage({
errorMessage: "Error validating if namespace already has tenants",
detailedError: err.detailedError,
})
);
});
}, [namespace, dispatch, updateField, selectedStorageClass]);
const debounceNamespace = useMemo(
() => debounce(getNamespaceInformation, 500),
[getNamespaceInformation]
);
useEffect(() => {
if (namespace !== "") {
debounceNamespace();
setLoadingNamespaceInfo(true);
// Cancel previous debounce calls during useEffect cleanup.
return debounceNamespace.cancel;
}
}, [debounceNamespace, namespace]);
// Validation
useEffect(() => {
let customNamespaceError = false;
let errorMessage = "";
if (!emptyNamespace && !loadingNamespaceInfo) {
customNamespaceError = true;
errorMessage = "You can only create one tenant per namespace";
} else if (
storageClasses.length < 1 &&
emptyNamespace &&
!loadingNamespaceInfo
) {
customNamespaceError = true;
errorMessage = "Please enter a valid namespace";
}
const commonValidation = commonFormValidation([
{
fieldKey: "namespace",
required: true,
value: namespace,
customValidation: customNamespaceError,
customValidationMessage: errorMessage,
},
]);
const isValid =
!("namespace" in commonValidation) &&
((formToRender === IMkEnvs.default && storageClasses.length > 0) ||
(formToRender !== IMkEnvs.default && selectedStorageType !== ""));
(formToRender === IMkEnvs.default && storageClasses.length > 0) ||
(formToRender !== IMkEnvs.default && selectedStorageType !== "");
dispatch(isPageValid({ pageName: "nameTenant", valid: isValid }));
setValidationErrors(commonValidation);
}, [
storageClasses,
namespace,
dispatch,
emptyNamespace,
loadingNamespaceInfo,
selectedStorageType,
formToRender,
]);
const frmValidationCleanup = (fieldName: string) => {
setValidationErrors(clearValidationError(validationErrors, fieldName));
};
const addNamespace = () => {
setOpenAddNSConfirm(true);
};
const closeAddNamespace = (refresh: boolean) => {
setOpenAddNSConfirm(false);
if (refresh) {
debounceNamespace();
}
};
}, [storageClasses, dispatch, selectedStorageType, formToRender]);
return (
<Fragment>
{openAddNSConfirm && (
<AddNamespaceModal
addNamespaceOpen={openAddNSConfirm}
closeAddNamespaceModalAndRefresh={closeAddNamespace}
namespace={namespace}
/>
)}
<Grid container>
<Grid item xs={8} md={9}>
<Paper className={classes.paperWrapper} sx={{ minHeight: 550 }}>
@@ -310,21 +139,7 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
</div>
</Grid>
<Grid item xs={12} className={classes.formFieldRow}>
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
updateField("namespace", e.target.value);
frmValidationCleanup("namespace");
}}
label="Namespace"
value={namespace}
error={validationErrors["namespace"] || ""}
overlayId={"add-namespace"}
overlayIcon={showCreateButton ? <AddIcon /> : null}
overlayAction={addNamespace}
required
/>
<NamespaceSelector formToRender={formToRender} />
</Grid>
{formToRender === IMkEnvs.default ? (
<Grid item xs={12} className={classes.formFieldRow}>

View File

@@ -0,0 +1,86 @@
// 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, useMemo } from "react";
import AddIcon from "../../../../../../icons/AddIcon";
import InputBoxWrapper from "../../../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { openAddNSModal, setNamespace } from "../../createTenantSlice";
import { useDispatch, useSelector } from "react-redux";
import { AppState } from "../../../../../../store";
import AddNamespaceModal from "../helpers/AddNamespaceModal";
import debounce from "lodash/debounce";
import { IMkEnvs } from "./utils";
import { validateNamespaceAsync } from "../../thunks/namespaceThunks";
const NamespaceSelector = ({ formToRender }: { formToRender?: IMkEnvs }) => {
const dispatch = useDispatch();
const namespace = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.namespace
);
const showNSCreateButton = useSelector(
(state: AppState) => state.createTenant.showNSCreateButton
);
const namespaceError = useSelector(
(state: AppState) => state.createTenant.validationErrors["namespace"]
);
const openAddNSConfirm = useSelector(
(state: AppState) => state.createTenant.addNSOpen
);
const debounceNamespace = useMemo(
() =>
debounce(() => {
dispatch(validateNamespaceAsync());
}, 500),
[dispatch]
);
useEffect(() => {
if (namespace !== "") {
debounceNamespace();
// Cancel previous debounce calls during useEffect cleanup.
return debounceNamespace.cancel;
}
}, [debounceNamespace, namespace]);
const addNamespace = () => {
dispatch(openAddNSModal());
};
return (
<Fragment>
{openAddNSConfirm && <AddNamespaceModal />}
<InputBoxWrapper
id="namespace"
name="namespace"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
dispatch(setNamespace(e.target.value));
}}
label="Namespace"
value={namespace}
error={namespaceError || ""}
overlayId={"add-namespace"}
overlayIcon={showNSCreateButton ? <AddIcon /> : null}
overlayAction={addNamespace}
required
/>
</Fragment>
);
};
export default NamespaceSelector;

View File

@@ -14,24 +14,23 @@
// 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 { useDispatch } from "react-redux";
import React from "react";
import { useDispatch, useSelector } from "react-redux";
import { DialogContentText, LinearProgress } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import {
deleteDialogStyles,
modalBasic,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { ErrorResponseHandler } from "../../../../../../common/types";
import api from "../../../../../../common/api";
import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog";
import { ConfirmModalIcon } from "../../../../../../icons";
import { setErrorSnackMessage } from "../../../../../../systemSlice";
import { AppState } from "../../../../../../store";
import { closeAddNSModal } from "../../createTenantSlice";
import makeStyles from "@mui/styles/makeStyles";
import { createNamespaceAsync } from "../../thunks/namespaceThunks";
const styles = (theme: Theme) =>
const useStyles = makeStyles((theme: Theme) =>
createStyles({
wrapText: {
maxWidth: "200px",
@@ -40,50 +39,22 @@ const styles = (theme: Theme) =>
},
...modalBasic,
...deleteDialogStyles,
});
})
);
interface IAddNamespace {
classes: any;
namespace: string;
addNamespaceOpen: boolean;
closeAddNamespaceModalAndRefresh: (reloadNamespaceData: boolean) => void;
}
const AddNamespaceModal = ({
classes,
namespace,
addNamespaceOpen,
closeAddNamespaceModalAndRefresh,
}: IAddNamespace) => {
const AddNamespaceModal = () => {
const dispatch = useDispatch();
const [addNamespaceLoading, setAddNamespaceLoading] =
useState<boolean>(false);
const classes = useStyles();
useEffect(() => {
if (addNamespaceLoading) {
api
.invoke("POST", "/api/v1/namespace", {
name: namespace,
})
.then((res) => {
setAddNamespaceLoading(false);
closeAddNamespaceModalAndRefresh(true);
})
.catch((err: ErrorResponseHandler) => {
setAddNamespaceLoading(false);
dispatch(setErrorSnackMessage(err));
});
}
}, [
addNamespaceLoading,
closeAddNamespaceModalAndRefresh,
namespace,
dispatch,
]);
const addNamespace = () => {
setAddNamespaceLoading(true);
};
const namespace = useSelector(
(state: AppState) => state.createTenant.fields.nameTenant.namespace
);
const addNamespaceLoading = useSelector(
(state: AppState) => state.createTenant.addNSLoading
);
const addNamespaceOpen = useSelector(
(state: AppState) => state.createTenant.addNSOpen
);
return (
<ConfirmDialog
@@ -96,9 +67,11 @@ const AddNamespaceModal = ({
isOpen={addNamespaceOpen}
titleIcon={<ConfirmModalIcon />}
isLoading={addNamespaceLoading}
onConfirm={addNamespace}
onConfirm={() => {
dispatch(createNamespaceAsync());
}}
onClose={() => {
closeAddNamespaceModalAndRefresh(false);
dispatch(closeAddNSModal());
}}
confirmationContent={
<React.Fragment>
@@ -114,4 +87,4 @@ const AddNamespaceModal = ({
);
};
export default withStyles(styles)(AddNamespaceModal);
export default AddNamespaceModal;

View File

@@ -15,7 +15,12 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
import { KeyPair, Opts } from "../ListTenants/utils";
import {
getLimitSizes,
IQuotaElement,
KeyPair,
Opts,
} from "../ListTenants/utils";
import {
ITolerationEffect,
ITolerationModel,
@@ -37,6 +42,11 @@ import { NewServiceAccount } from "../../Common/CredentialsPrompt/types";
import { createTenantAsync } from "./thunks/createTenantThunk";
import { commonFormValidation } from "../../../../utils/validationFunctions";
import { flipValidPageInState } from "./sliceUtils";
import {
createNamespaceAsync,
namespaceResourcesAsync,
validateNamespaceAsync,
} from "./thunks/namespaceThunks";
export interface ICreateTenant {
addingTenant: boolean;
@@ -52,6 +62,12 @@ export interface ICreateTenant {
// after creation states
createdAccount: NewServiceAccount | null;
showNewCredentials: boolean;
//namespace logic
emptyNamespace: boolean;
loadingNamespaceInfo: boolean;
showNSCreateButton: boolean;
addNSOpen: boolean;
addNSLoading: boolean;
}
const initialState: ICreateTenant = {
@@ -353,6 +369,11 @@ const initialState: ICreateTenant = {
],
createdAccount: null,
showNewCredentials: false,
emptyNamespace: true,
loadingNamespaceInfo: false,
showNSCreateButton: false,
addNSOpen: false,
addNSLoading: false,
};
export const createTenantSlice = createSlice({
@@ -668,306 +689,7 @@ export const createTenantSlice = createSlice({
encoded_cert: action.payload.value,
};
},
resetAddTenantForm: (state) => {
state.addingTenant = false;
state.page = 0;
// We can assume all the other pages are valid with default configuration except for 'nameTenant'
// because the user still have to choose a namespace and a name for the tenant
state.validPages = [
"tenantSize",
"configure",
"affinity",
"identityProvider",
"security",
"encryption",
];
state.validationErrors = {};
state.storageClasses = [];
state.limitSize = {};
state.fields = {
nameTenant: {
tenantName: "",
namespace: "",
selectedStorageClass: "",
selectedStorageType: "",
},
configure: {
customImage: false,
imageName: "",
customDockerhub: false,
imageRegistry: "",
imageRegistryUsername: "",
imageRegistryPassword: "",
exposeMinIO: true,
exposeConsole: true,
tenantCustom: false,
logSearchEnabled: true,
prometheusEnabled: true,
logSearchVolumeSize: "5",
logSearchSizeFactor: "Gi",
logSearchSelectedStorageClass: "default",
logSearchImage: "",
kesImage: "",
logSearchPostgresImage: "",
logSearchPostgresInitImage: "",
prometheusVolumeSize: "5",
prometheusSizeFactor: "Gi",
prometheusSelectedStorageClass: "default",
prometheusImage: "",
prometheusSidecarImage: "",
prometheusInitImage: "",
setDomains: false,
consoleDomain: "",
minioDomains: [""],
tenantSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
logSearchSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
logSearchPostgresSecurityContext: {
runAsUser: "999",
runAsGroup: "999",
fsGroup: "999",
runAsNonRoot: true,
},
prometheusSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
identityProvider: {
idpSelection: "Built-in",
accessKeys: [getRandomString(16)],
secretKeys: [getRandomString(32)],
openIDConfigurationURL: "",
openIDClientID: "",
openIDSecretID: "",
openIDCallbackURL: "",
openIDClaimName: "",
openIDScopes: "",
ADURL: "",
ADSkipTLS: false,
ADServerInsecure: false,
ADGroupSearchBaseDN: "",
ADGroupSearchFilter: "",
ADUserDNs: [""],
ADLookupBindDN: "",
ADLookupBindPassword: "",
ADUserDNSearchBaseDN: "",
ADUserDNSearchFilter: "",
ADServerStartTLS: false,
},
security: {
enableAutoCert: true,
enableCustomCerts: false,
enableTLS: true,
},
encryption: {
enableEncryption: false,
encryptionType: "vault",
gemaltoEndpoint: "",
gemaltoToken: "",
gemaltoDomain: "",
gemaltoRetry: "0",
awsEndpoint: "",
awsRegion: "",
awsKMSKey: "",
awsAccessKey: "",
awsSecretKey: "",
awsToken: "",
vaultEndpoint: "",
vaultEngine: "",
vaultNamespace: "",
vaultPrefix: "",
vaultAppRoleEngine: "",
vaultId: "",
vaultSecret: "",
vaultRetry: "0",
vaultPing: "0",
azureEndpoint: "",
azureTenantID: "",
azureClientID: "",
azureClientSecret: "",
gcpProjectID: "",
gcpEndpoint: "",
gcpClientEmail: "",
gcpClientID: "",
gcpPrivateKeyID: "",
gcpPrivateKey: "",
enableCustomCertsForKES: false,
replicas: "1",
kesSecurityContext: {
runAsUser: "1000",
runAsGroup: "1000",
fsGroup: "1000",
runAsNonRoot: true,
},
},
tenantSize: {
volumeSize: "1024",
sizeFactor: "Gi",
drivesPerServer: "4",
nodes: "4",
memoryNode: "2",
ecParity: "",
ecParityChoices: [],
cleanECChoices: [],
untouchedECField: true,
distribution: {
error: "",
nodes: 0,
persistentVolumes: 0,
disks: 0,
},
ecParityCalc: {
error: 0,
defaultEC: "",
erasureCodeSet: 0,
maxEC: "",
rawCapacity: "0",
storageFactors: [],
},
limitSize: {},
cpuToUse: "0",
// resource request
resourcesSpecifyLimit: false,
resourcesCPURequestError: "",
resourcesCPURequest: "",
resourcesCPULimitError: "",
resourcesCPULimit: "",
resourcesMemoryRequestError: "",
resourcesMemoryRequest: "",
resourcesMemoryLimitError: "",
resourcesMemoryLimit: "",
resourcesSize: {
error: "",
memoryRequest: 0,
memoryLimit: 0,
cpuRequest: 0,
cpuLimit: 0,
},
maxAllocatableResources: {
min_allocatable_mem: 0,
min_allocatable_cpu: 0,
cpu_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
mem_priority: {
max_allocatable_cpu: 0,
max_allocatable_mem: 0,
},
},
maxCPUsUse: "0",
maxMemorySize: "0",
integrationSelection: {
driveSize: { driveSize: "0", sizeUnit: "B" },
CPU: 0,
typeSelection: "",
memory: 0,
drivesPerServer: 0,
storageClass: "",
},
},
affinity: {
nodeSelectorLabels: "",
podAffinity: "default",
withPodAntiAffinity: true,
},
};
state.certificates = {
minioCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
caCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCaCertificates: [
{
id: Date.now().toString(),
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
],
consoleCertificate: {
id: "console_cert_pair",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
serverCertificate: {
id: "encryptionServerCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
clientCertificate: {
id: "encryptionClientCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCertificate: {
id: "encryptionVaultCertificate",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
vaultCA: {
id: "encryptionVaultCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
gemaltoCA: {
id: "encryptionGemaltoCA",
key: "",
cert: "",
encoded_key: "",
encoded_cert: "",
},
};
state.nodeSelectorPairs = [{ key: "", value: "" }];
state.tolerations = [
{
key: "",
tolerationSeconds: { seconds: 0 },
value: "",
effect: ITolerationEffect.NoSchedule,
operator: ITolerationOperator.Equal,
},
];
state.createdAccount = null;
state.showNewCredentials = false;
},
resetAddTenantForm: () => initialState,
setKeyValuePairs: (state, action: PayloadAction<LabelKeyPair[]>) => {
state.nodeSelectorPairs = action.payload;
},
@@ -1093,6 +815,49 @@ export const createTenantSlice = createSlice({
flipValidPageInState(state, "nameTenant", isValid);
},
setNamespace: (state, action: PayloadAction<string>) => {
state.fields.nameTenant.namespace = action.payload;
delete state.validationErrors["namespace"];
let customNamespaceError = false;
let errorMessage = "";
if (
state.storageClasses.length < 1 &&
state.emptyNamespace &&
!state.loadingNamespaceInfo
) {
customNamespaceError = true;
errorMessage = "Please enter a valid namespace";
}
const commonValidation = commonFormValidation([
{
fieldKey: "namespace",
required: true,
value: action.payload,
customValidation: customNamespaceError,
customValidationMessage: errorMessage,
},
]);
let isValid = false;
if ("namespace" in commonValidation) {
isValid = true;
state.validationErrors["namespace"] = commonValidation["namespace"];
}
flipValidPageInState(state, "nameTenant", isValid);
},
showNSCreate: (state, action: PayloadAction<boolean>) => {
state.showNSCreateButton = action.payload;
},
openAddNSModal: (state) => {
state.addNSOpen = true;
},
closeAddNSModal: (state) => {
state.addNSOpen = false;
},
},
extraReducers: (builder) => {
builder
@@ -1108,6 +873,73 @@ export const createTenantSlice = createSlice({
state.addingTenant = false;
state.createdAccount = action.payload;
state.showNewCredentials = true;
})
.addCase(validateNamespaceAsync.pending, (state, action) => {
state.loadingNamespaceInfo = true;
state.showNSCreateButton = false;
delete state.validationErrors["namespace"];
})
.addCase(validateNamespaceAsync.rejected, (state, action) => {
state.loadingNamespaceInfo = false;
state.showNSCreateButton = true;
})
.addCase(validateNamespaceAsync.fulfilled, (state, action) => {
state.showNSCreateButton = false;
state.emptyNamespace = action.payload;
if (!state.emptyNamespace) {
state.validationErrors["namespace"] =
"You can only create one tenant per namespace";
}
})
.addCase(namespaceResourcesAsync.pending, (state, action) => {
state.loadingNamespaceInfo = true;
})
.addCase(namespaceResourcesAsync.rejected, (state, action) => {
state.loadingNamespaceInfo = false;
state.showNSCreateButton = true;
state.fields.nameTenant.selectedStorageClass = "";
state.storageClasses = [];
state.validationErrors["namespace"] = "Please enter a valid namespace";
})
.addCase(namespaceResourcesAsync.fulfilled, (state, action) => {
state.loadingNamespaceInfo = false;
const elements: IQuotaElement[] = get(action.payload, "elements", []);
state.limitSize = getLimitSizes(action.payload!);
const newStorage = elements.map((storageClass: any) => {
const name = get(storageClass, "name", "").split(
".storageclass.storage.k8s.io/requests.storage"
)[0];
return { label: name, value: name };
});
state.storageClasses = newStorage;
const stExists = newStorage.findIndex(
(storageClass) =>
storageClass.value === state.fields.nameTenant.selectedStorageClass
);
if (newStorage.length > 0 && stExists === -1) {
state.fields.nameTenant.selectedStorageClass = newStorage[0].value;
} else if (newStorage.length === 0) {
state.fields.nameTenant.selectedStorageClass = "";
state.storageClasses = [];
}
})
.addCase(createNamespaceAsync.pending, (state, action) => {
state.addNSLoading = true;
})
.addCase(createNamespaceAsync.rejected, (state, action) => {
state.addNSLoading = false;
})
.addCase(createNamespaceAsync.fulfilled, (state, action) => {
state.addNSLoading = false;
state.addNSOpen = false;
delete state.validationErrors["namespace"];
});
},
});
@@ -1150,6 +982,10 @@ export const {
removeIDPADUsrAtIndex,
setIDP,
setTenantName,
setNamespace,
showNSCreate,
openAddNSModal,
closeAddNSModal,
} = createTenantSlice.actions;
export default createTenantSlice.reducer;

View File

@@ -0,0 +1,102 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import { createAsyncThunk } from "@reduxjs/toolkit";
import { AppState } from "../../../../../store";
import {
setErrorSnackMessage,
setModalErrorSnackMessage,
} from "../../../../../systemSlice";
import api from "../../../../../common/api";
import { ErrorResponseHandler } from "../../../../../common/types";
import { IQuotas } from "../../ListTenants/utils";
import get from "lodash/get";
export const validateNamespaceAsync = createAsyncThunk(
"createTenant/validateNamespaceAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const namespace = state.createTenant.fields.nameTenant.namespace;
return api
.invoke("GET", `/api/v1/namespaces/${namespace}/tenants`)
.then((res: any[]): boolean => {
const tenantsList = get(res, "tenants", []);
let nsEmpty = true;
if (tenantsList && tenantsList.length > 0) {
return false;
}
if (nsEmpty) {
dispatch(namespaceResourcesAsync());
}
// it's true it's empty
return nsEmpty;
})
.catch((err) => {
dispatch(
setModalErrorSnackMessage({
errorMessage: "Error validating if namespace already has tenants",
detailedError: err.detailedError,
})
);
return rejectWithValue(false);
});
}
);
export const namespaceResourcesAsync = createAsyncThunk(
"createTenant/namespaceResourcesAsync",
async (_, { getState, rejectWithValue }) => {
const state = getState() as AppState;
const namespace = state.createTenant.fields.nameTenant.namespace;
return api
.invoke(
"GET",
`/api/v1/namespaces/${namespace}/resourcequotas/${namespace}-storagequota`
)
.then((res: IQuotas): IQuotas => {
return res;
})
.catch((err) => {
console.error("Namespace quota error: ", err);
return rejectWithValue(null);
});
}
);
export const createNamespaceAsync = createAsyncThunk(
"createTenant/createNamespaceAsync",
async (_, { getState, rejectWithValue, dispatch }) => {
const state = getState() as AppState;
const namespace = state.createTenant.fields.nameTenant.namespace;
return api
.invoke("POST", "/api/v1/namespace", {
name: namespace,
})
.then((res) => {
// revalidate the name to have the storage classes populated
dispatch(validateNamespaceAsync());
return true;
})
.catch((err: ErrorResponseHandler) => {
dispatch(setErrorSnackMessage(err));
rejectWithValue(false);
});
}
);