Add site in a new page (#1845)
This commit is contained in:
committed by
GitHub
parent
f2c187bf7c
commit
ae34d886a9
@@ -163,6 +163,7 @@ export const IAM_PAGES = {
|
|||||||
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
|
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
|
||||||
SITE_REPLICATION: "/settings/site-replication",
|
SITE_REPLICATION: "/settings/site-replication",
|
||||||
SITE_REPLICATION_STATUS: "/settings/site-replication/status",
|
SITE_REPLICATION_STATUS: "/settings/site-replication/status",
|
||||||
|
SITE_REPLICATION_ADD: "/settings/site-replication/add",
|
||||||
|
|
||||||
/* Operator */
|
/* Operator */
|
||||||
TENANTS: "/tenants",
|
TENANTS: "/tenants",
|
||||||
@@ -389,6 +390,10 @@ export const IAM_PAGES_PERMISSIONS = {
|
|||||||
IAM_SCOPES.ADMIN_SERVER_INFO,
|
IAM_SCOPES.ADMIN_SERVER_INFO,
|
||||||
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
|
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:::*";
|
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";
|
||||||
|
|||||||
@@ -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);
|
||||||
@@ -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);
|
|
||||||
@@ -128,7 +128,7 @@ const ReplicationSites = ({
|
|||||||
const key = `${siteInfo.name}`;
|
const key = `${siteInfo.name}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment key={key}>
|
<React.Fragment key={`${key}-${index}`}>
|
||||||
<ListItemButton
|
<ListItemButton
|
||||||
disableRipple
|
disableRipple
|
||||||
sx={{
|
sx={{
|
||||||
|
|||||||
@@ -23,8 +23,12 @@ import ReplicationSites from "./ReplicationSites";
|
|||||||
import TrashIcon from "../../../../icons/TrashIcon";
|
import TrashIcon from "../../../../icons/TrashIcon";
|
||||||
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
|
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
|
||||||
import Loader from "../../Common/Loader/Loader";
|
import Loader from "../../Common/Loader/Loader";
|
||||||
import { AddIcon, ConfirmDeleteIcon, RecoverIcon } from "../../../../icons";
|
import {
|
||||||
import AddReplicationSitesModal from "./AddReplicationSitesModal";
|
AddIcon,
|
||||||
|
ClustersIcon,
|
||||||
|
ConfirmDeleteIcon,
|
||||||
|
RecoverIcon,
|
||||||
|
} from "../../../../icons";
|
||||||
import { connect } from "react-redux";
|
import { connect } from "react-redux";
|
||||||
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
|
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
|
||||||
import { ErrorResponseHandler } from "../../../../common/types";
|
import { ErrorResponseHandler } from "../../../../common/types";
|
||||||
@@ -47,8 +51,6 @@ const SiteReplication = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const [sites, setSites] = useState([]);
|
const [sites, setSites] = useState([]);
|
||||||
|
|
||||||
const [isAddOpen, setIsAddOpen] = useState(false);
|
|
||||||
|
|
||||||
const [deleteAll, setIsDeleteAll] = useState(false);
|
const [deleteAll, setIsDeleteAll] = useState(false);
|
||||||
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
|
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
|
||||||
(res: any) => {
|
(res: any) => {
|
||||||
@@ -148,7 +150,7 @@ const SiteReplication = ({
|
|||||||
disabled={isRemoving}
|
disabled={isRemoving}
|
||||||
icon={<AddIcon />}
|
icon={<AddIcon />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsAddOpen(true);
|
history.push(IAM_PAGES.SITE_REPLICATION_ADD);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@@ -185,19 +187,9 @@ const SiteReplication = ({
|
|||||||
</Box>
|
</Box>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{isAddOpen ? (
|
|
||||||
<AddReplicationSitesModal
|
|
||||||
existingSites={sites}
|
|
||||||
onClose={() => {
|
|
||||||
setIsAddOpen(false);
|
|
||||||
getSites();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<HelpBox
|
<HelpBox
|
||||||
title={"Site Replication"}
|
title={"Site Replication"}
|
||||||
iconComponent={<RecoverIcon />}
|
iconComponent={<ClustersIcon />}
|
||||||
help={
|
help={
|
||||||
<Fragment>
|
<Fragment>
|
||||||
This feature allows multiple independent MinIO sites (or clusters)
|
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
|
configured as replicas. In this situation the set of replica sites
|
||||||
are referred to as peer sites or just sites.
|
are referred to as peer sites or just sites.
|
||||||
<br />
|
<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 />
|
<br />
|
||||||
You can learn more at our{" "}
|
You can learn more at our{" "}
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -124,6 +124,11 @@ const SiteReplication = React.lazy(
|
|||||||
const SiteReplicationStatus = React.lazy(
|
const SiteReplicationStatus = React.lazy(
|
||||||
() => import("./Configurations/SiteReplication/SiteReplicationStatus")
|
() => import("./Configurations/SiteReplication/SiteReplicationStatus")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const AddReplicationSites = React.lazy(
|
||||||
|
() => import("./Configurations/SiteReplication/AddReplicationSites")
|
||||||
|
);
|
||||||
|
|
||||||
const styles = (theme: Theme) =>
|
const styles = (theme: Theme) =>
|
||||||
createStyles({
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
@@ -380,6 +385,10 @@ const Console = ({
|
|||||||
component: SiteReplicationStatus,
|
component: SiteReplicationStatus,
|
||||||
path: IAM_PAGES.SITE_REPLICATION_STATUS,
|
path: IAM_PAGES.SITE_REPLICATION_STATUS,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
component: AddReplicationSites,
|
||||||
|
path: IAM_PAGES.SITE_REPLICATION_ADD,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
component: Account,
|
component: Account,
|
||||||
path: IAM_PAGES.ACCOUNT,
|
path: IAM_PAGES.ACCOUNT,
|
||||||
|
|||||||
Reference in New Issue
Block a user