Create Tenant Namespace Field move to Thunks (#2052)
Signed-off-by: Daniel Valdivia <18384552+dvaldivia@users.noreply.github.com>
This commit is contained in:
@@ -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}>
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user