Add site in a new page (#1845)

This commit is contained in:
Prakash Senthil Vel
2022-04-14 20:19:45 +00:00
committed by GitHub
parent f2c187bf7c
commit ae34d886a9
6 changed files with 672 additions and 436 deletions

View File

@@ -163,6 +163,7 @@ export const IAM_PAGES = {
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
SITE_REPLICATION: "/settings/site-replication",
SITE_REPLICATION_STATUS: "/settings/site-replication/status",
SITE_REPLICATION_ADD: "/settings/site-replication/add",
/* Operator */
TENANTS: "/tenants",
@@ -389,6 +390,10 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.SITE_REPLICATION_ADD]: [
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";

View File

@@ -0,0 +1,627 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { AddIcon, ClustersIcon, RemoveIcon } from "../../../../icons";
import { ReplicationSite } from "./SiteReplication";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { Box, Button, LinearProgress } from "@mui/material";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import useApi from "../../Common/Hooks/useApi";
import { connect } from "react-redux";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import PageHeader from "../../Common/PageHeader/PageHeader";
import BackLink from "../../../../common/BackLink";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
import PageLayout from "../../Common/Layout/PageLayout";
import ScreenTitle from "../../Common/ScreenTitle/ScreenTitle";
import HelpBox from "../../../../common/HelpBox";
import history from "../../../../history";
type SiteInputRow = {
name: string;
endpoint: string;
};
const isValidEndPoint = (ep: string) => {
let isValidEndPointUrl = false;
try {
new URL(ep);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
if (isValidEndPointUrl || ep === "") {
return "";
} else {
return "Invalid Endpoint";
}
};
const AddReplicationSites = ({
setErrorSnackMessage,
setSnackBarMessage,
}: {
existingSites: ReplicationSite[];
onClose: () => void;
setErrorSnackMessage: (err: ErrorResponseHandler) => void;
setSnackBarMessage: (msg: string) => void;
}) => {
const [existingSites, setExistingSites] = useState<SiteInputRow[]>([]);
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [siteConfig, setSiteConfig] = useState<SiteInputRow[]>([]);
const setDefaultNewRows = () => {
const defaultNewSites = existingSites?.length
? [{ endpoint: "", name: "" }]
: [
{ endpoint: "", name: "" },
{ endpoint: "", name: "" },
];
setSiteConfig(defaultNewSites);
};
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
(res: any) => {
const { sites: siteList, name: curSiteName } = res;
// current site name to be the fist one.
const foundIdx = siteList.findIndex((el: any) => el.name === curSiteName);
if (foundIdx !== -1) {
let curSite = siteList[foundIdx];
curSite = {
...curSite,
isCurrent: true,
};
siteList.splice(foundIdx, 1, curSite);
}
siteList.sort((x: any, y: any) => {
return x.name === curSiteName ? -1 : y.name === curSiteName ? 1 : 0;
});
setExistingSites(siteList);
},
(err: any) => {
setExistingSites([]);
}
);
const getSites = () => {
invokeSiteInfoApi("GET", `api/v1/admin/site-replication`);
};
useEffect(() => {
getSites();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setDefaultNewRows();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [existingSites]);
const isAllEndpointsValid =
siteConfig.reduce((acc: string[], cv, i) => {
const epValue = siteConfig[i].endpoint;
const isEpValid = isValidEndPoint(epValue);
if (isEpValid === "" && epValue !== "") {
acc.push(isEpValid);
}
return acc;
}, []).length === siteConfig.length;
const [isAdding, invokeSiteAddApi] = useApi(
(res: any) => {
if (res.success) {
setSnackBarMessage(res.status);
resetForm();
getSites();
history.push(IAM_PAGES.SITE_REPLICATION);
} else {
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
});
}
},
(err: any) => {
setErrorSnackMessage(err);
}
);
const resetForm = () => {
setAccessKey("");
setSecretKey("");
setDefaultNewRows();
};
const addSiteReplication = () => {
const existingSitesToAdd = existingSites?.map((es, idx) => {
return {
accessKey: accessKey,
secretKey: secretKey,
name: es.name,
endpoint: es.endpoint,
};
});
const newSitesToAdd = siteConfig.reduce((acc: any, ns, idx) => {
if (ns.endpoint) {
acc.push({
accessKey: accessKey,
secretKey: secretKey,
name: ns.name || `dr-site-${idx}`,
endpoint: ns.endpoint,
});
}
return acc;
}, []);
invokeSiteAddApi("POST", `api/v1/admin/site-replication`, [
...(existingSitesToAdd || []),
...(newSitesToAdd || []),
]);
};
return (
<Fragment>
<PageHeader
label={
<BackLink
to={IAM_PAGES.SITE_REPLICATION}
label={"Add Replication Site"}
/>
}
/>
<PageLayout>
<ScreenTitle title={"Add Sites for Replication"} icon={ClustersIcon} />
{isSiteInfoLoading || isAdding ? <LinearProgress /> : null}
<Box
sx={{
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
}}
>
<Box>
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
return addSiteReplication();
}}
>
<Grid item xs={12} marginBottom={"15px"}>
<Box
sx={{
fontStyle: "italic",
display: "flex",
alignItems: "center",
fontSize: "12px",
}}
>
<Box sx={{ fontWeight: 600 }}>Note:</Box>{" "}
<Box sx={{ marginLeft: "8px" }}>
Access Key and Secret Key should be same on all sites.
</Box>
</Box>
</Grid>
<Grid
item
xs={12}
marginBottom={"15px"}
sx={{
"& label span": {
fontWeight: "normal",
},
}}
>
<InputBoxWrapper
id="add-rep-peer-accKey"
name="add-rep-peer-accKey"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(event.target.value);
}}
label="Access Key"
required={true}
value={accessKey}
error={accessKey === "" ? "Access Key is required." : ""}
data-test-id={"add-site-rep-acc-key"}
/>
</Grid>
<Grid
item
xs={12}
marginBottom={"30px"}
sx={{
"& label span": {
fontWeight: "normal",
},
}}
>
<InputBoxWrapper
id="add-rep-peer-secKey"
name="add-rep-peer-secKey"
type={"password"}
required={true}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(event.target.value);
}}
error={secretKey === "" ? "Secret Key is required." : ""}
label="Secret Key"
value={secretKey}
data-test-id={"add-site-rep-sec-key"}
/>
</Grid>
<Grid item xs={12}>
<Box
sx={{
marginBottom: "15px",
fontSize: "14px",
fontWeight: 600,
}}
>
Peer Sites
</Box>
</Grid>
<Box
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .2fr",
border: "1px solid #eaeaea",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<Box
sx={{
fontSize: "14px",
marginLeft: "5px",
}}
>
Site Name
</Box>
<Box sx={{ fontSize: "14px", marginLeft: "5px" }}>
Endpoint {"*"}
</Box>
<Box> </Box>
{existingSites?.map((si, index) => {
return (
<Fragment key={si.name}>
<Box>
<InputBoxWrapper
id={`add-rep-ex-peer-site-${index}`}
name={`add-rep-ex-peer-site-${index}`}
extraInputProps={{
readOnly: true,
}}
label=""
value={si.name}
onChange={() => {}}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-ex-peer-site-ep-${index}`}
name={`add-rep-ex-peer-site-ep-${index}`}
extraInputProps={{
readOnly: true,
}}
label=""
value={si.endpoint}
onChange={() => {}}
/>
</Box>
<Grid item xs={12}>
{" "}
</Grid>
</Fragment>
);
})}
{siteConfig.map((sci, index) => {
let isDelDisabled = false;
if (existingSites?.length && index === 0) {
isDelDisabled = true;
} else if (!existingSites?.length && index < 2) {
isDelDisabled = true;
}
return (
<Fragment key={`${index}`}>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-${index}`}
name={`add-rep-peer-site-${index}`}
placeholder={`dr-site-${index}`}
label=""
value={`${sci.name}`}
onChange={(e) => {
const nameTxt = e.target.value;
setSiteConfig((prevItems) => {
return prevItems.map((item, ix) =>
ix === index ? { ...item, name: nameTxt } : item
);
});
}}
data-test-id={`add-site-rep-peer-site-${index}`}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-ep-${index}`}
name={`add-rep-peer-site-ep-${index}`}
placeholder={`https://dr.minio-storage:900${index}`}
label=""
error={isValidEndPoint(siteConfig[index].endpoint)}
value={`${sci.endpoint}`}
onChange={(e) => {
const epTxt = e.target.value;
setSiteConfig((prevItems) => {
return prevItems.map((item, ix) =>
ix === index
? { ...item, endpoint: epTxt }
: item
);
});
}}
data-test-id={`add-site-rep-peer-ep-${index}`}
/>
</Box>
<Grid item xs={12} alignItems={"center"} display={"flex"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
alignSelf: "baseline",
marginTop: "4px",
"& button": {
borderColor: "#696969",
color: "#696969",
borderRadius: "50%",
},
}}
>
<RBIconButton
tooltip={"Add a Row"}
text={""}
variant="outlined"
color="primary"
icon={<AddIcon />}
onClick={(e) => {
e.preventDefault();
const newRows = [...siteConfig];
//add at the next index
newRows.splice(index + 1, 0, {
name: "",
endpoint: "",
});
setSiteConfig(newRows);
}}
/>
<RBIconButton
tooltip={"Remove Row"}
text={""}
variant="outlined"
disabled={isDelDisabled}
color="primary"
icon={<RemoveIcon />}
onClick={(e) => {
e.preventDefault();
setSiteConfig(
siteConfig.filter((_, idx) => idx !== index)
);
}}
/>
</Box>
</Grid>
</Fragment>
);
})}
</Box>
<Grid item xs={12}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
type="button"
variant="outlined"
color="primary"
disabled={isAdding}
onClick={resetForm}
>
Clear
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={
isAdding ||
!accessKey ||
!secretKey ||
!isAllEndpointsValid
}
>
Save
</Button>
</Box>
</Grid>
</form>
</Box>
<HelpBox
title={""}
iconComponent={null}
help={
<Fragment>
<Box
sx={{
marginTop: "-25px",
fontSize: "16px",
fontWeight: 600,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
padding: "2px",
}}
>
<Box
sx={{
backgroundColor: "#07193E",
height: "15px",
width: "15px",
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "50%",
marginRight: "18px",
padding: "3px",
"& .min-icon": {
height: "11px",
width: "11px",
fill: "#ffffff",
},
}}
>
<ClustersIcon />
</Box>
About Site Replication
</Box>
<Box
sx={{
display: "flex",
flexFlow: "column",
fontSize: "14px",
flex: "2",
"& .step-number": {
color: "#ffffff",
height: "25px",
width: "25px",
background: "#081C42",
marginRight: "10px",
textAlign: "center",
fontWeight: 600,
borderRadius: "50%",
},
"& .step-row": {
fontSize: "14px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
"&.step-text": {
fontWeight: 400,
},
"&:before": {
content: "' '",
height: "7px",
width: "7px",
backgroundColor: "#2781B0",
marginRight: "10px",
marginTop: "12px",
flexShrink: 0,
},
},
}}
>
<Box>
The following changes are replicated to all other sites
</Box>
<Box className="step-row">
<div className="step-text">
Creation and deletion of buckets and objects
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Creation and deletion of all IAM users, groups, policies
and their mappings to users or groups
</div>
</Box>
<Box className="step-row">
<div className="step-text">Creation of STS credentials</div>
</Box>
<Box className="step-row">
<div className="step-text">
Creation and deletion of service accounts (except those
owned by the root user)
</div>
</Box>
<Box className="step-row">
<div className="step-text">
Changes to Bucket features such as
<ul>
<li>Bucket Policies</li>
<li>Bucket Tags</li>
<li>Bucket Object-Lock configurations</li>
<li>Bucket Encryption configuration</li>
</ul>
</div>
</Box>
<Box className="step-row">
<div className="step-text">
The following Bucket features will NOT be replicated
<ul>
<li>Bucket notification configuration</li>
<li>Bucket lifecycle (ILM) configuration</li>
</ul>
</div>
</Box>
</Box>
</Fragment>
}
/>
</Box>
</PageLayout>
</Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
setSnackBarMessage,
});
export default connector(AddReplicationSites);

View File

@@ -1,419 +0,0 @@
// 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, useState } from "react";
import { AddIcon, RecoverIcon, RemoveIcon } from "../../../../icons";
import { ReplicationSite } from "./SiteReplication";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import { Box, Button, LinearProgress } from "@mui/material";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import useApi from "../../Common/Hooks/useApi";
import { connect } from "react-redux";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
type SiteInputRow = {
name: string;
endpoint: string;
};
const isValidEndPoint = (ep: string) => {
let isValidEndPointUrl = false;
try {
new URL(ep);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
if (isValidEndPointUrl || ep === "") {
return "";
} else {
return "Invalid Endpoint";
}
};
const AddReplicationSitesModal = ({
existingSites = [],
onClose,
setErrorSnackMessage,
setSnackBarMessage,
}: {
existingSites: ReplicationSite[];
onClose: () => void;
setErrorSnackMessage: (err: ErrorResponseHandler) => void;
setSnackBarMessage: (msg: string) => void;
}) => {
const defaultNewSites = existingSites?.length
? [{ endpoint: "", name: "" }]
: [
{ endpoint: "", name: "" },
{ endpoint: "", name: "" },
];
const [accessKey, setAccessKey] = useState<string>("");
const [secretKey, setSecretKey] = useState<string>("");
const [siteConfig, setSiteConfig] = useState<SiteInputRow[]>(defaultNewSites);
const isAllEndpointsValid =
siteConfig.reduce((acc: string[], cv, i) => {
const epValue = siteConfig[i].endpoint;
const isEpValid = isValidEndPoint(epValue);
if (isEpValid === "" && epValue !== "") {
acc.push(isEpValid);
}
return acc;
}, []).length === siteConfig.length;
const [isAdding, invokeSiteAddApi] = useApi(
(res: any) => {
if (res.success) {
setSnackBarMessage(res.status);
onClose();
} else {
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
});
}
},
(err: any) => {
setErrorSnackMessage(err);
}
);
const resetForm = () => {
setAccessKey("");
setSecretKey("");
setSiteConfig(defaultNewSites);
};
const addSiteReplication = () => {
const existingSitesToAdd = existingSites?.map((es, idx) => {
return {
accessKey: accessKey,
secretKey: secretKey,
name: es.name,
endpoint: es.endpoint,
};
});
const newSitesToAdd = siteConfig.reduce((acc: any, ns, idx) => {
if (ns.endpoint) {
acc.push({
accessKey: accessKey,
secretKey: secretKey,
name: ns.name || `dr-site-${idx}`,
endpoint: ns.endpoint,
});
}
return acc;
}, []);
invokeSiteAddApi("POST", `api/v1/admin/site-replication`, [
...(existingSitesToAdd || []),
...(newSitesToAdd || []),
]);
};
return (
<ModalWrapper
modalOpen={true}
onClose={onClose}
title={`Add Sites for Replication`}
titleIcon={<RecoverIcon />}
data-test-id={"add-site-replication-modal"}
>
{isAdding ? <LinearProgress /> : null}
<form
noValidate
autoComplete="off"
onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
return addSiteReplication();
}}
>
<Grid item xs={12} marginBottom={"15px"}>
<Box
sx={{
fontStyle: "italic",
display: "flex",
alignItems: "center",
fontSize: "12px",
}}
>
<Box sx={{ fontWeight: 600 }}>Note:</Box>{" "}
<Box sx={{ marginLeft: "8px" }}>
Access Key and Secret Key should be same on all sites.
</Box>
</Box>
</Grid>
<Grid
item
xs={12}
marginBottom={"15px"}
sx={{
"& label span": {
fontWeight: "normal",
},
}}
>
<InputBoxWrapper
id="add-rep-peer-accKey"
name="add-rep-peer-accKey"
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setAccessKey(event.target.value);
}}
label="Access Key"
required={true}
value={accessKey}
error={accessKey === "" ? "Access Key is required." : ""}
data-test-id={"add-site-rep-acc-key"}
/>
</Grid>
<Grid
item
xs={12}
marginBottom={"30px"}
sx={{
"& label span": {
fontWeight: "normal",
},
}}
>
<InputBoxWrapper
id="add-rep-peer-secKey"
name="add-rep-peer-secKey"
type={"password"}
required={true}
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
setSecretKey(event.target.value);
}}
error={secretKey === "" ? "Secret Key is required." : ""}
label="Secret Key"
value={secretKey}
data-test-id={"add-site-rep-sec-key"}
/>
</Grid>
<Grid item xs={12}>
<Box sx={{ marginBottom: "15px", fontSize: "14px", fontWeight: 600 }}>
Peer Sites
</Box>
</Grid>
<Box
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .2fr",
border: "1px solid #eaeaea",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<Box
sx={{
fontSize: "14px",
marginLeft: "5px",
}}
>
Site Name
</Box>
<Box sx={{ fontSize: "14px", marginLeft: "5px" }}>Endpoint {"*"}</Box>
<Box> </Box>
{existingSites?.map((si, index) => {
return (
<Fragment key={si.name}>
<Box>
<InputBoxWrapper
id={`add-rep-ex-peer-site-${index}`}
name={`add-rep-ex-peer-site-${index}`}
extraInputProps={{
readOnly: true,
}}
label=""
value={si.name}
onChange={() => {}}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-ex-peer-site-ep-${index}`}
name={`add-rep-ex-peer-site-ep-${index}`}
extraInputProps={{
readOnly: true,
}}
label=""
value={si.endpoint}
onChange={() => {}}
/>
</Box>
<Grid item xs={12}>
{" "}
</Grid>
</Fragment>
);
})}
{siteConfig.map((sci, index) => {
let isDelDisabled = false;
if (existingSites?.length && index === 0) {
isDelDisabled = true;
} else if (!existingSites?.length && index < 2) {
isDelDisabled = true;
}
return (
<Fragment key={`${index}`}>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-${index}`}
name={`add-rep-peer-site-${index}`}
placeholder={`dr-site-${index}`}
label=""
value={`${sci.name}`}
onChange={(e) => {
const nameTxt = e.target.value;
setSiteConfig((prevItems) => {
return prevItems.map((item, ix) =>
ix === index ? { ...item, name: nameTxt } : item
);
});
}}
data-test-id={`add-site-rep-peer-site-${index}`}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-ep-${index}`}
name={`add-rep-peer-site-ep-${index}`}
placeholder={`https://dr.minio-storage:900${index}`}
label=""
error={isValidEndPoint(siteConfig[index].endpoint)}
value={`${sci.endpoint}`}
onChange={(e) => {
const epTxt = e.target.value;
setSiteConfig((prevItems) => {
return prevItems.map((item, ix) =>
ix === index ? { ...item, endpoint: epTxt } : item
);
});
}}
data-test-id={`add-site-rep-peer-ep-${index}`}
/>
</Box>
<Grid item xs={12} alignItems={"center"} display={"flex"}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
alignSelf: "baseline",
marginTop: "4px",
"& button": {
borderColor: "#696969",
color: "#696969",
borderRadius: "50%",
},
}}
>
<RBIconButton
tooltip={"Add a Row"}
text={""}
variant="outlined"
color="primary"
icon={<AddIcon />}
onClick={(e) => {
e.preventDefault();
const newRows = [...siteConfig];
//add at the next index
newRows.splice(index + 1, 0, {
name: "",
endpoint: "",
});
setSiteConfig(newRows);
}}
/>
<RBIconButton
tooltip={"Remove Row"}
text={""}
variant="outlined"
disabled={isDelDisabled}
color="primary"
icon={<RemoveIcon />}
onClick={(e) => {
e.preventDefault();
setSiteConfig(
siteConfig.filter((_, idx) => idx !== index)
);
}}
/>
</Box>
</Grid>
</Fragment>
);
})}
</Box>
<Grid item xs={12}>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
marginTop: "20px",
gap: "15px",
}}
>
<Button
type="button"
variant="outlined"
color="primary"
disabled={isAdding}
onClick={resetForm}
>
Clear
</Button>
<Button
type="submit"
variant="contained"
color="primary"
disabled={
isAdding || !accessKey || !secretKey || !isAllEndpointsValid
}
>
Save
</Button>
</Box>
</Grid>
</form>
</ModalWrapper>
);
};
const connector = connect(null, {
setErrorSnackMessage,
setSnackBarMessage,
});
export default connector(AddReplicationSitesModal);

View File

@@ -128,7 +128,7 @@ const ReplicationSites = ({
const key = `${siteInfo.name}`;
return (
<React.Fragment key={key}>
<React.Fragment key={`${key}-${index}`}>
<ListItemButton
disableRipple
sx={{

View File

@@ -23,8 +23,12 @@ import ReplicationSites from "./ReplicationSites";
import TrashIcon from "../../../../icons/TrashIcon";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import Loader from "../../Common/Loader/Loader";
import { AddIcon, ConfirmDeleteIcon, RecoverIcon } from "../../../../icons";
import AddReplicationSitesModal from "./AddReplicationSitesModal";
import {
AddIcon,
ClustersIcon,
ConfirmDeleteIcon,
RecoverIcon,
} from "../../../../icons";
import { connect } from "react-redux";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
@@ -47,8 +51,6 @@ const SiteReplication = ({
}) => {
const [sites, setSites] = useState([]);
const [isAddOpen, setIsAddOpen] = useState(false);
const [deleteAll, setIsDeleteAll] = useState(false);
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
(res: any) => {
@@ -148,7 +150,7 @@ const SiteReplication = ({
disabled={isRemoving}
icon={<AddIcon />}
onClick={() => {
setIsAddOpen(true);
history.push(IAM_PAGES.SITE_REPLICATION_ADD);
}}
/>
</Box>
@@ -185,19 +187,9 @@ const SiteReplication = ({
</Box>
) : null}
{isAddOpen ? (
<AddReplicationSitesModal
existingSites={sites}
onClose={() => {
setIsAddOpen(false);
getSites();
}}
/>
) : null}
<HelpBox
title={"Site Replication"}
iconComponent={<RecoverIcon />}
iconComponent={<ClustersIcon />}
help={
<Fragment>
This feature allows multiple independent MinIO sites (or clusters)
@@ -205,6 +197,28 @@ const SiteReplication = ({
configured as replicas. In this situation the set of replica sites
are referred to as peer sites or just sites.
<br />
<Box>
<ul>
<li>
Initially, only one of the sites added for replication may
have data. After site-replication is successfully
configured, this data is replicated to the other (initially
empty) sites. Subsequently, objects may be written to any of
the sites, and they will be replicated to all other sites.
</li>
<li>
All sites must have the same deployment credentials (i.e.
MINIO_ROOT_USER, MINIO_ROOT_PASSWORD).
</li>
<li>
All sites must be using the same external IDP(s) if any.
</li>
<li>
For SSE-S3 or SSE-KMS encryption via KMS, all sites must
have access to a central KMS deployment. server.
</li>
</ul>
</Box>
<br />
You can learn more at our{" "}
<a

View File

@@ -124,6 +124,11 @@ const SiteReplication = React.lazy(
const SiteReplicationStatus = React.lazy(
() => import("./Configurations/SiteReplication/SiteReplicationStatus")
);
const AddReplicationSites = React.lazy(
() => import("./Configurations/SiteReplication/AddReplicationSites")
);
const styles = (theme: Theme) =>
createStyles({
root: {
@@ -380,6 +385,10 @@ const Console = ({
component: SiteReplicationStatus,
path: IAM_PAGES.SITE_REPLICATION_STATUS,
},
{
component: AddReplicationSites,
path: IAM_PAGES.SITE_REPLICATION_ADD,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,