UI site replication (#1807)

This commit is contained in:
Prakash Senthil Vel
2022-04-09 23:27:25 +00:00
committed by GitHub
parent 836090a0d5
commit ee3affd140
7 changed files with 1195 additions and 1 deletions

View File

@@ -161,6 +161,7 @@ export const IAM_PAGES = {
TIERS: "/settings/tiers",
TIERS_ADD: "/settings/tiers/add",
TIERS_ADD_SERVICE: "/settings/tiers/add/:service",
SITE_REPLICATION: "/settings/site-replication",
/* Operator */
TENANTS: "/tenants",
@@ -379,6 +380,10 @@ export const IAM_PAGES_PERMISSIONS = {
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
],
[IAM_PAGES.SITE_REPLICATION]:[
IAM_SCOPES.ADMIN_SERVER_INFO,
IAM_SCOPES.ADMIN_CONFIG_UPDATE,
]
};
export const S3_ALL_RESOURCES = "arn:aws:s3:::*";

View File

@@ -0,0 +1,419 @@
// 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

@@ -0,0 +1,464 @@
// 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, { useState } from "react";
import ListSubheader from "@mui/material/ListSubheader";
import List from "@mui/material/List";
import ListItemButton from "@mui/material/ListItemButton";
import Collapse from "@mui/material/Collapse";
import { Box, Button, DialogContentText, Tooltip } from "@mui/material";
import {
MenuCollapsedIcon,
MenuExpandedIcon,
} from "../../../../icons/SidebarMenus";
import { ReplicationSite } from "./SiteReplication";
import RBIconButton from "../../Buckets/BucketDetails/SummaryItems/RBIconButton";
import TrashIcon from "../../../../icons/TrashIcon";
import { CircleIcon, ConfirmDeleteIcon, EditIcon } from "../../../../icons";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import useApi from "../../Common/Hooks/useApi";
import { connect } from "react-redux";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import withStyles from "@mui/styles/withStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import {
formFieldStyles,
modalStyleUtils,
spacingUtils,
} from "../../Common/FormComponents/common/styleLibrary";
const styles = (theme: Theme) =>
createStyles({
...modalStyleUtils,
...formFieldStyles,
...spacingUtils,
});
const ReplicationSites = ({
sites,
onDeleteSite,
setErrorSnackMessage,
setSnackBarMessage,
onRefresh,
classes,
}: {
sites: ReplicationSite[];
onDeleteSite: (isAll: boolean, sites: string[]) => void;
setErrorSnackMessage: (err: ErrorResponseHandler) => void;
setSnackBarMessage: (msg: string) => void;
onRefresh: () => void;
classes: any;
}) => {
const [expanded, setExpanded] = React.useState<string>("");
const [deleteSiteKey, setIsDeleteSiteKey] = useState<string>("");
const [editSite, setEditSite] = useState<any>(null);
const [editEndPointName, setEditEndPointName] = useState<string>("");
const handleClick = (key: string) => {
setExpanded(key);
};
const [isEditing, invokeSiteEditApi] = useApi(
(res: any) => {
if (res.success) {
setEditSite(null);
setSnackBarMessage(res.status);
} else {
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
});
}
onRefresh();
},
(err: any) => {
setErrorSnackMessage(err);
onRefresh();
}
);
const updatePeerSite = () => {
invokeSiteEditApi("PUT", `api/v1/admin/site-replication`, {
endpoint: editEndPointName,
name: editSite.name,
deploymentId: editSite.deploymentID, // readonly
});
};
const hasExpand = false; //siteInfo.isCurrent to b
let isValidEndPointUrl = false;
try {
new URL(editEndPointName);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
return (
<Box>
<List
sx={{
width: "100%",
flex: 1,
padding: "0",
marginTop: "25px",
height: "calc( 100vh - 450px )",
border: "1px solid #eaeaea",
marginBottom: "25px",
}}
component="nav"
aria-labelledby="nested-list-subheader"
>
<Box
sx={{
fontWeight: 600,
borderBottom: "1px solid #f1f1f1",
padding: "25px 25px 25px 20px",
}}
>
List of Replicated Sites
</Box>
{sites.map((siteInfo, index) => {
const key = `${siteInfo.name}`;
const isExpanded = expanded === siteInfo.name;
const handleToggle = () => {
if (!isExpanded) {
handleClick(key);
} else {
handleClick("");
}
};
return (
<React.Fragment key={key}>
<ListItemButton
disableRipple
className={isExpanded ? "expanded" : ""}
sx={{
display: "flex",
alignItems: "center",
border: "1px solid #f1f1f1",
borderLeft: "0",
borderRight: "0",
borderTop: "0",
padding: "6px 10px 6px 20px",
"&:hover": {
background: "#bebbbb0d",
},
"&.expanded": {
marginBottom: "0",
},
}}
>
<Box
sx={{
flex: 2,
display: "grid",
gridTemplateColumns: {
sm: "1fr 1fr ",
},
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
overflow: "hidden",
}}
>
{siteInfo.name}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
overflow: "hidden",
}}
>
{siteInfo.isCurrent ? (
<Tooltip title={"This site/cluster"} placement="top">
<Box
sx={{
"& .min-icon": {
height: "12px",
fill: "green",
},
}}
>
<CircleIcon />
</Box>
</Tooltip>
) : null}
<Tooltip title={siteInfo.endpoint}>
<Box
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
marginLeft: siteInfo.isCurrent ? "" : "24px",
}}
>
{siteInfo.endpoint}
</Box>
</Tooltip>
</Box>
</Box>
<Box
sx={{
display: "flex",
marginLeft: "25px",
marginRight: "25px",
width: "60px",
flexShrink: 0,
"& button": {
borderRadius: "50%",
background: "#F8F8F8",
border: "none",
"&:hover": {
background: "#E2E2E2",
},
"& svg": {
fill: "#696565",
},
},
}}
>
<RBIconButton
tooltip={
sites.length <= 2
? "Minimum two sites are required for replication"
: "Delete Site"
}
text={""}
variant="outlined"
color="secondary"
disabled={sites.length <= 2}
icon={<TrashIcon />}
onClick={(e) => {
e.preventDefault();
setIsDeleteSiteKey(key);
}}
/>
<RBIconButton
tooltip={"Edit Endpoint"}
text={""}
variant="contained"
color="primary"
icon={<EditIcon />}
onClick={(e) => {
e.preventDefault();
setEditSite(siteInfo);
}}
/>
</Box>
{hasExpand ? (
<Box
sx={{
height: "25px",
width: "25px",
background: "#FBFAFA",
borderRadius: "2px",
border: "1px solid #eaeaea",
"&:hover": {
background: "#fafafa",
},
display: {
md: "block",
xs: "none",
},
"& .collapse-icon": {
fill: "#494949",
"& g rect": {
fill: "#ffffff",
},
},
"& .expand-icon": {
fill: "#494949",
"& rect": {
fill: "#ffffff",
},
},
}}
onClick={handleToggle}
>
{isExpanded ? (
<MenuCollapsedIcon className="collapse-icon" />
) : (
<MenuExpandedIcon className="expand-icon" />
)}
</Box>
) : (
<Box
sx={{
height: "25px",
width: "25px",
}}
/>
)}
</ListItemButton>
{isExpanded ? (
<Box
key={`${siteInfo.name}`}
sx={{
border: "1px solid #f1f1f1",
borderLeft: "0",
borderRight: "0",
borderTop: "0",
}}
>
<ListSubheader
key={`${index}-drive-details`}
component="div"
sx={{ paddingLeft: "30px" }}
>
Replication status
</ListSubheader>
<Collapse
in={isExpanded}
timeout="auto"
unmountOnExit
sx={{
width: "100%",
flex: 1,
display: "flex",
padding: { md: "15px 30px", xs: "10px 10px" },
"& .MuiCollapse-wrapperInner": {
display: "flex",
flexFlow: "column",
gap: "15px",
},
}}
>
Status info
</Collapse>
</Box>
) : null}
{deleteSiteKey === key ? (
<ConfirmDialog
title={`Delete Replication Site`}
confirmText={"Delete"}
isOpen={true}
titleIcon={<ConfirmDeleteIcon />}
isLoading={false}
onConfirm={() => {
onDeleteSite(false, [key]);
}}
onClose={() => {
setIsDeleteSiteKey("");
}}
confirmationContent={
<DialogContentText>
Are you sure you want to remove the replication site:{" "}
{key}.?
</DialogContentText>
}
/>
) : null}
{editSite?.name === key ? (
<ModalWrapper
title={`Edit Replication Endpoint `}
modalOpen={true}
titleIcon={<EditIcon />}
onClose={() => {
setEditSite(null);
}}
>
<DialogContentText>
<Box
sx={{
display: "flex",
flexFlow: "column",
marginBottom: "15px",
}}
>
<Box sx={{ marginBottom: "10px" }}>
<strong>Site:</strong> {" "}
{editSite.name}
</Box>
<Box sx={{ marginBottom: "10px" }}>
<strong>Current Endpoint:</strong> {" "}
{editSite.endpoint}
</Box>
</Box>
<Grid item xs={12}>
<Box sx={{ marginBottom: "5px" }}> New Endpoint:</Box>
<InputBoxWrapper
id="edit-rep-peer-endpoint"
name="edit-rep-peer-endpoint"
placeholder={"https://dr.minio-storage:9000"}
onChange={(
event: React.ChangeEvent<HTMLInputElement>
) => {
setEditEndPointName(event.target.value);
}}
label=""
value={editEndPointName}
/>
</Grid>
</DialogContentText>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
type="button"
variant="outlined"
color="primary"
onClick={() => {
setEditSite(null);
}}
>
Close
</Button>
<Button
type="button"
variant="contained"
color="primary"
disabled={isEditing || !isValidEndPointUrl}
onClick={() => {
updatePeerSite();
}}
>
Update
</Button>
</Grid>
</ModalWrapper>
) : null}
</React.Fragment>
);
})}
</List>
</Box>
);
};
const connector = connect(null, {
setErrorSnackMessage,
setSnackBarMessage,
});
export default connector(withStyles(styles)(ReplicationSites));

View File

@@ -0,0 +1,240 @@
// 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 PageHeader from "../../Common/PageHeader/PageHeader";
import PageLayout from "../../Common/Layout/PageLayout";
import { Box, DialogContentText } from "@mui/material";
import useApi from "../../Common/Hooks/useApi";
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 { connect } from "react-redux";
import { setErrorSnackMessage, setSnackBarMessage } from "../../../../actions";
import { ErrorResponseHandler } from "../../../../common/types";
import HelpBox from "../../../../common/HelpBox";
import ConfirmDialog from "../../Common/ModalWrapper/ConfirmDialog";
export type ReplicationSite = {
deploymentID: string;
endpoint: string;
name: string;
isCurrent?: boolean;
};
const SiteReplication = ({
setSnackBarMessage,
}: {
setSnackBarMessage: (msg: string) => void;
}) => {
const [sites, setSites] = useState([]);
const [isAddOpen, setIsAddOpen] = useState(false);
const [deleteAll, setIsDeleteAll] = useState(false);
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;
});
setSites(siteList);
},
(err: any) => {
setSites([]);
}
);
const getSites = () => {
invokeSiteInfoApi("GET", `api/v1/admin/site-replication`);
};
const [isRemoving, invokeSiteRemoveApi] = useApi(
(res: any) => {
setIsDeleteAll(false);
setSnackBarMessage(`Successfully deleted.`);
getSites();
},
(err: ErrorResponseHandler) => {
setErrorSnackMessage(err);
}
);
const removeSites = (isAll: boolean = false, delSites: string[] = []) => {
invokeSiteRemoveApi("DELETE", `api/v1/admin/site-replication`, {
all: isAll,
sites: delSites,
});
};
useEffect(() => {
getSites();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const hasSites = sites?.length;
return (
<Fragment>
<PageHeader label={"Site Replication"} />
<PageLayout>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
}}
>
{hasSites ? (
<Box>
<RBIconButton
tooltip={"Delete All"}
text={"Delete All"}
variant="outlined"
color="secondary"
disabled={isRemoving}
icon={<TrashIcon />}
onClick={() => {
setIsDeleteAll(true);
}}
/>
</Box>
) : null}
<RBIconButton
tooltip={"Add Replication Sites"}
text={"Add Sites"}
variant="contained"
color="primary"
disabled={isRemoving}
icon={<AddIcon />}
onClick={() => {
setIsAddOpen(true);
}}
/>
</Box>
{hasSites ? (
<ReplicationSites
sites={sites}
onDeleteSite={removeSites}
onRefresh={getSites}
/>
) : null}
{isSiteInfoLoading ? (
<Box
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "calc( 100vh - 450px )",
}}
>
<Loader style={{ width: 16, height: 16 }} />
</Box>
) : null}
{!hasSites && !isSiteInfoLoading ? (
<Box
sx={{
padding: "30px",
border: "1px solid #eaeaea",
marginTop: "15px",
marginBottom: "15px",
height: "calc( 100vh - 450px )",
}}
>
Site Replication is not configured.
</Box>
) : null}
{isAddOpen ? (
<AddReplicationSitesModal
existingSites={sites}
onClose={() => {
setIsAddOpen(false);
getSites();
}}
/>
) : null}
<HelpBox
title={"Site Replication"}
iconComponent={<RecoverIcon />}
help={
<Fragment>
This feature allows multiple independent MinIO sites (or clusters)
that are using the same external IDentity Provider (IDP) to be
configured as replicas. In this situation the set of replica sites
are referred to as peer sites or just sites.
<br />
<br />
You can learn more at our{" "}
<a
href="https://github.com/minio/minio/tree/master/docs/site-replication?ref=con"
target="_blank"
rel="noreferrer"
>
documentation
</a>
.
</Fragment>
}
/>
{deleteAll ? (
<ConfirmDialog
title={`Delete All`}
confirmText={"Delete"}
isOpen={true}
titleIcon={<ConfirmDeleteIcon />}
isLoading={false}
onConfirm={() => {
const siteNames = sites.map((s: any) => s.name);
removeSites(true, siteNames);
}}
onClose={() => {
setIsDeleteAll(false);
}}
confirmationContent={
<DialogContentText>
Are you sure you want to remove all the replication sites?.
</DialogContentText>
}
/>
) : null}
</PageLayout>
</Fragment>
);
};
const connector = connect(null, {
setErrorSnackMessage,
setSnackBarMessage,
});
export default connector(SiteReplication);

View File

@@ -118,7 +118,7 @@ const ConfigurationOptions = React.lazy(
const AddPool = React.lazy(
() => import("./Tenants/TenantDetails/Pools/AddPool/AddPool")
);
const SiteReplication = React.lazy(() => import("./Configurations/SiteReplication/SiteReplication"));
const styles = (theme: Theme) =>
createStyles({
root: {
@@ -368,6 +368,10 @@ const Console = ({
component: ListTiersConfiguration,
path: IAM_PAGES.TIERS,
},
{
component: SiteReplication,
path: IAM_PAGES.SITE_REPLICATION,
},
{
component: Account,
path: IAM_PAGES.ACCOUNT,

View File

@@ -49,6 +49,7 @@ import {
DocumentationIcon,
LambdaIcon,
LicenseIcon,
RecoverIcon,
TenantsOutlineIcon,
TiersIcon,
} from "../../icons";
@@ -245,6 +246,13 @@ export const validRoutes = (
icon: TiersIcon,
id: "tiers",
},
{
component: NavLink,
to: IAM_PAGES.SITE_REPLICATION,
name: "Site Replication",
icon: RecoverIcon,
id: "sitereplication",
},
],
},
{

View File

@@ -0,0 +1,54 @@
// 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 * as roles from "../utils/roles";
import { settingsElement } from "../utils/elements-menu";
import { IAM_PAGES } from "../../src/common/SecureComponent/permissions";
import { Selector } from "testcafe";
let testDomainUrl = "http://localhost:9090";
const screenUrl = `${testDomainUrl}${IAM_PAGES.SITE_REPLICATION}`;
const siteReplicationEl = Selector("span").withText("Site Replication");
export const addSitesBtn = Selector("button").withText("Add Sites");
/* Begin Local Testing config block */
// For local Testing Create users and assign policies then update here.
// Command to invoke the test locally: testcafe chrome tests/permissions/site-replication.ts
/* End Local Testing config block */
fixture("Site Replication for user with Admin permissions")
.page(testDomainUrl)
.beforeEach(async (t) => {
await t.useRole(roles.settings);
});
test("Site replication sidebar item exists", async (t) => {
await t
.expect(settingsElement.exists)
.ok()
.click(settingsElement)
.expect(siteReplicationEl.exists)
.ok();
});
test("Add Sites button exists", async (t) => {
const addSitesBtnExists = addSitesBtn.exists;
await t.navigateTo(screenUrl).expect(addSitesBtnExists).ok();
});
test("Add Sites button is clickable", async (t) => {
await t.navigateTo(screenUrl).click(addSitesBtn);
});