diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NameTenantMain.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NameTenantMain.tsx
index 859b9653c..2e113e422 100644
--- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NameTenantMain.tsx
+++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NameTenantMain.tsx
@@ -14,54 +14,33 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-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({});
- const [emptyNamespace, setEmptyNamespace] = useState(true);
- const [loadingNamespaceInfo, setLoadingNamespaceInfo] =
- useState(false);
- const [showCreateButton, setShowCreateButton] = useState(false);
- const [openAddNSConfirm, setOpenAddNSConfirm] = useState(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 (
- {openAddNSConfirm && (
-
- )}
@@ -310,21 +139,7 @@ const NameTenantMain = ({ classes, formToRender }: INameTenantMainScreen) => {
- ) => {
- updateField("namespace", e.target.value);
- frmValidationCleanup("namespace");
- }}
- label="Namespace"
- value={namespace}
- error={validationErrors["namespace"] || ""}
- overlayId={"add-namespace"}
- overlayIcon={showCreateButton ? : null}
- overlayAction={addNamespace}
- required
- />
+
{formToRender === IMkEnvs.default ? (
diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NamespaceSelector.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NamespaceSelector.tsx
new file mode 100644
index 000000000..19d89a8fc
--- /dev/null
+++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/TenantResources/NamespaceSelector.tsx
@@ -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 .
+
+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 (
+
+ {openAddNSConfirm && }
+ ) => {
+ dispatch(setNamespace(e.target.value));
+ }}
+ label="Namespace"
+ value={namespace}
+ error={namespaceError || ""}
+ overlayId={"add-namespace"}
+ overlayIcon={showNSCreateButton ? : null}
+ overlayAction={addNamespace}
+ required
+ />
+
+ );
+};
+export default NamespaceSelector;
diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/helpers/AddNamespaceModal.tsx b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/helpers/AddNamespaceModal.tsx
index 813a5f39c..3e52b7729 100644
--- a/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/helpers/AddNamespaceModal.tsx
+++ b/portal-ui/src/screens/Console/Tenants/AddTenant/Steps/helpers/AddNamespaceModal.tsx
@@ -14,24 +14,23 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see .
-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(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 (
}
isLoading={addNamespaceLoading}
- onConfirm={addNamespace}
+ onConfirm={() => {
+ dispatch(createNamespaceAsync());
+ }}
onClose={() => {
- closeAddNamespaceModalAndRefresh(false);
+ dispatch(closeAddNSModal());
}}
confirmationContent={
@@ -114,4 +87,4 @@ const AddNamespaceModal = ({
);
};
-export default withStyles(styles)(AddNamespaceModal);
+export default AddNamespaceModal;
diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/createTenantSlice.ts b/portal-ui/src/screens/Console/Tenants/AddTenant/createTenantSlice.ts
index db83c0554..9e26bc64d 100644
--- a/portal-ui/src/screens/Console/Tenants/AddTenant/createTenantSlice.ts
+++ b/portal-ui/src/screens/Console/Tenants/AddTenant/createTenantSlice.ts
@@ -15,7 +15,12 @@
// along with this program. If not, see .
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) => {
state.nodeSelectorPairs = action.payload;
},
@@ -1093,6 +815,49 @@ export const createTenantSlice = createSlice({
flipValidPageInState(state, "nameTenant", isValid);
},
+ setNamespace: (state, action: PayloadAction) => {
+ 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) => {
+ 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;
diff --git a/portal-ui/src/screens/Console/Tenants/AddTenant/thunks/namespaceThunks.ts b/portal-ui/src/screens/Console/Tenants/AddTenant/thunks/namespaceThunks.ts
new file mode 100644
index 000000000..9e284978b
--- /dev/null
+++ b/portal-ui/src/screens/Console/Tenants/AddTenant/thunks/namespaceThunks.ts
@@ -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 .
+
+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);
+ });
+ }
+);