Site replication support for different accesskey and secretkey in different sites (#2350)

This commit is contained in:
Prakash Senthil Vel
2022-10-14 05:41:29 +05:30
committed by GitHub
parent 38015b4913
commit ec3deed38e
11 changed files with 713 additions and 442 deletions

View File

@@ -59,6 +59,9 @@ type SessionResponse struct {
// permissions
Permissions map[string][]string `json:"permissions,omitempty"`
// server end point
ServerEndPoint string `json:"serverEndPoint,omitempty"`
// status
// Enum: [ok]
Status string `json:"status,omitempty"`

View File

@@ -20,8 +20,7 @@ import { Box, LinearProgress } from "@mui/material";
import { useNavigate } from "react-router-dom";
import { Button } from "mds";
import useApi from "../../Common/Hooks/useApi";
import { AddIcon, ClustersIcon, RemoveIcon } from "../../../../icons";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { ClustersIcon } from "../../../../icons";
import PageHeader from "../../Common/PageHeader/PageHeader";
import BackLink from "../../../../common/BackLink";
import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
@@ -33,12 +32,10 @@ import {
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
type SiteInputRow = {
name: string;
endpoint: string;
};
import { useSelector } from "react-redux";
import { selSession } from "../../consoleSlice";
import SRSiteInputRow from "./SRSiteInputRow";
import { SiteInputRow } from "./Types";
const isValidEndPoint = (ep: string) => {
let isValidEndPointUrl = false;
@@ -49,30 +46,74 @@ const isValidEndPoint = (ep: string) => {
} catch (err) {
isValidEndPointUrl = false;
}
if (isValidEndPointUrl || ep === "") {
if (isValidEndPointUrl) {
return "";
} else {
return "Invalid Endpoint";
}
};
const isEmptyValue = (value: string): boolean => {
return value?.trim() === "";
};
const TableHeader = () => {
return (
<React.Fragment>
<Box
sx={{
fontSize: "14px",
marginLeft: "5px",
}}
>
Site Name
</Box>
<Box sx={{ fontSize: "14px", marginLeft: "5px" }}>Endpoint {"*"}</Box>
<Box sx={{ fontSize: "14px", marginLeft: "5px" }}>Access Key {"*"}</Box>
<Box sx={{ fontSize: "14px", marginLeft: "5px" }}>Secret Key {"*"}</Box>
<Box> </Box>
</React.Fragment>
);
};
const SiteTypeHeader = ({ title }: { title: string }) => {
return (
<Grid item xs={12}>
<Box
sx={{
marginBottom: "15px",
fontSize: "14px",
fontWeight: 600,
}}
>
{title}
</Box>
</Grid>
);
};
const AddReplicationSites = () => {
const dispatch = useAppDispatch();
const navigate = useNavigate();
const { serverEndPoint = "" } = useSelector(selSession);
const [currentSite, setCurrentSite] = useState<SiteInputRow[]>([
{
endpoint: serverEndPoint,
name: "",
accessKey: "",
secretKey: "",
},
]);
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 defaultNewSites = [
{ endpoint: "", name: "", accessKey: "", secretKey: "" },
];
setExistingSites(defaultNewSites);
};
const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
@@ -85,17 +126,34 @@ const AddReplicationSites = () => {
curSite = {
...curSite,
isCurrent: true,
isSaved: true,
};
siteList.splice(foundIdx, 1, curSite);
setCurrentSite([curSite]);
siteList.splice(foundIdx, 1);
}
siteList.sort((x: any, y: any) => {
return x.name === curSiteName ? -1 : y.name === curSiteName ? 1 : 0;
});
setExistingSites(siteList);
let existingSiteList = siteList.map((si: any) => {
return {
...si,
accessKey: "",
secretKey: "",
isSaved: true,
};
});
if (existingSiteList.length) {
setExistingSites(existingSiteList);
} else {
setDefaultNewRows();
}
},
(err: any) => {
setExistingSites([]);
setDefaultNewRows();
}
);
@@ -108,21 +166,36 @@ const AddReplicationSites = () => {
// 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 existingEndPointsValidity = existingSites.reduce(
(acc: string[], cv, i) => {
const epValue = existingSites[i].endpoint;
const isEpValid = isValidEndPoint(epValue);
if (isEpValid === "" && epValue !== "") {
acc.push(isEpValid);
}
return acc;
}, []).length === siteConfig.length;
},
[]
);
const isExistingCredsValidity = existingSites
.map((site) => {
return !isEmptyValue(site.accessKey) && !isEmptyValue(site.secretKey);
})
.filter(Boolean);
const { accessKey: cAccessKey, secretKey: cSecretKey } = currentSite[0];
const isCurCredsValid =
!isEmptyValue(cAccessKey) && !isEmptyValue(cSecretKey);
const peerEndpointsValid =
existingEndPointsValidity.length === existingSites.length;
const peerCredsValid =
isExistingCredsValidity.length === existingSites.length;
let isAllFieldsValid =
isCurCredsValid && peerEndpointsValid && peerCredsValid;
const [isAdding, invokeSiteAddApi] = useApi(
(res: any) => {
@@ -146,37 +219,181 @@ const AddReplicationSites = () => {
);
const resetForm = () => {
setAccessKey("");
setSecretKey("");
setDefaultNewRows();
setCurrentSite((prevItems) => {
return prevItems.map((item, ix) => ({
...item,
accessKey: "",
secretKey: "",
name: "",
}));
});
};
const addSiteReplication = () => {
const existingSitesToAdd = existingSites?.map((es, idx) => {
const curSite: any[] = currentSite?.map((es, idx) => {
return {
accessKey: accessKey,
secretKey: secretKey,
accessKey: es.accessKey,
secretKey: es.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;
}, []);
const newOrExistingSitesToAdd = existingSites.reduce(
(acc: any, ns, idx) => {
if (ns.endpoint) {
acc.push({
accessKey: ns.accessKey,
secretKey: ns.secretKey,
name: ns.name || `dr-site-${idx}`,
endpoint: ns.endpoint,
});
}
return acc;
},
[]
);
invokeSiteAddApi("POST", `api/v1/admin/site-replication`, [
...(existingSitesToAdd || []),
...(newSitesToAdd || []),
]);
const sitesToAdd = curSite.concat(newOrExistingSitesToAdd);
invokeSiteAddApi("POST", `api/v1/admin/site-replication`, sitesToAdd);
};
const renderCurrentSite = () => {
return (
<Box
sx={{
marginTop: "15px",
}}
>
<SiteTypeHeader title={"This Site"} />
<Box
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
border: "1px solid #eaeaea",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<TableHeader />
{currentSite.map((cs, index) => {
const accessKeyError = isEmptyValue(cs.accessKey)
? "AccessKey is required"
: "";
const secretKeyError = isEmptyValue(cs.secretKey)
? "SecretKey is required"
: "";
return (
<SRSiteInputRow
key={`current-${index}`}
disabledFields={["endpoint"].concat(cs.isSaved ? "name" : "")}
rowData={cs}
rowId={index}
fieldErrors={{
accessKey: accessKeyError,
secretKey: secretKeyError,
}}
onFieldChange={(e, fieldName, index) => {
const filedValue = e.target.value;
if (fieldName !== "") {
setCurrentSite((prevItems) => {
return prevItems.map((item, ix) =>
ix === index
? { ...item, [fieldName]: filedValue }
: item
);
});
}
}}
showRowActions={false}
/>
);
})}
</Box>
</Box>
);
};
const renderPeerSites = () => {
return (
<Box
sx={{
marginTop: "25px",
}}
>
<SiteTypeHeader title={"Peer Sites"} />
<Box
sx={{
display: "grid",
gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
border: "1px solid #eaeaea",
padding: "15px",
gap: "10px",
maxHeight: "430px",
overflowY: "auto",
}}
>
<TableHeader />
{existingSites.map((ps, index) => {
const endPointError = isValidEndPoint(ps.endpoint);
const accessKeyError = isEmptyValue(ps.accessKey)
? "AccessKey is required"
: "";
const secretKeyError = isEmptyValue(ps.secretKey)
? "SecretKey is required"
: "";
return (
<SRSiteInputRow
key={`exiting-${index}`}
rowData={ps}
rowId={index}
fieldErrors={{
endpoint: endPointError,
accessKey: accessKeyError,
secretKey: secretKeyError,
}}
disabledFields={ps.isSaved ? ["endpoint", "name"] : []}
onFieldChange={(e, fieldName, index) => {
const filedValue = e.target.value;
setExistingSites((prevItems) => {
return prevItems.map((item, ix) =>
ix === index ? { ...item, [fieldName]: filedValue } : item
);
});
}}
canAdd={true}
canRemove={index > 0 && !ps.isSaved}
onAddClick={() => {
const newRows = [...existingSites];
//add at the next index
newRows.splice(index + 1, 0, {
name: "",
endpoint: "",
accessKey: "",
secretKey: "",
});
setExistingSites(newRows);
}}
onRemoveClick={(index) => {
setExistingSites(
existingSites.filter((_, idx) => idx !== index)
);
}}
/>
);
})}
</Box>
</Box>
);
};
return (
@@ -195,10 +412,7 @@ const AddReplicationSites = () => {
display: "grid",
padding: "25px",
gap: "25px",
gridTemplateColumns: {
md: "2fr 1.2fr",
xs: "1fr",
},
gridTemplateColumns: "1fr",
border: "1px solid #eaeaea",
}}
>
@@ -208,6 +422,18 @@ const AddReplicationSites = () => {
</SectionTitle>
{isSiteInfoLoading || isAdding ? <LinearProgress /> : null}
<Box
sx={{
fontSize: "14px",
fontStyle: "italic",
marginTop: "10px",
marginBottom: "10px",
}}
>
Note: AccessKey and SecretKey values for every site is required
while adding or editing peer sites
</Box>
<form
noValidate
autoComplete="off"
@@ -216,254 +442,9 @@ const AddReplicationSites = () => {
return addSiteReplication();
}}
>
<Grid item xs={12} marginBottom={"15px"}>
<Box
sx={{
fontStyle: "italic",
display: "flex",
alignItems: "center",
fontSize: "12px",
marginTop: 2,
}}
>
<Box sx={{ fontWeight: 600 }}>Note:</Box>{" "}
<Box sx={{ marginLeft: 1 }}>
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>
{renderCurrentSite()}
<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%",
},
}}
>
<TooltipWrapper tooltip={"Add a Row"}>
<Button
id={`add-row-${index}`}
variant="regular"
icon={<AddIcon />}
onClick={(e) => {
e.preventDefault();
const newRows = [...siteConfig];
//add at the next index
newRows.splice(index + 1, 0, {
name: "",
endpoint: "",
});
setSiteConfig(newRows);
}}
style={{
width: 25,
height: 25,
padding: 0,
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Remove Row"}>
<Button
id={`remove-row-${index}`}
variant="regular"
disabled={isDelDisabled}
icon={<RemoveIcon />}
onClick={(e) => {
e.preventDefault();
setSiteConfig(
siteConfig.filter((_, idx) => idx !== index)
);
}}
style={{
width: 25,
height: 25,
padding: 0,
marginLeft: 8,
}}
/>
</TooltipWrapper>
</Box>
</Grid>
</Fragment>
);
})}
</Box>
{renderPeerSites()}
<Grid item xs={12}>
<Box
@@ -488,12 +469,7 @@ const AddReplicationSites = () => {
id={"save"}
type="submit"
variant="callAction"
disabled={
isAdding ||
!accessKey ||
!secretKey ||
!isAllEndpointsValid
}
disabled={isAdding || !isAllFieldsValid}
label={"Save"}
/>
</Box>
@@ -557,11 +533,12 @@ const AddReplicationSites = () => {
borderRadius: "50%",
},
"& .step-row": {
"& li": {
fontSize: "14px",
display: "flex",
marginTop: "15px",
marginBottom: "15px",
width: "100%",
"&.step-text": {
fontWeight: 400,
@@ -593,21 +570,52 @@ const AddReplicationSites = () => {
owned by the root user)
</li>
<li>
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>
<Box
style={{
display: "flex",
flexFlow: "column",
justifyContent: "flex-start",
}}
>
<div
style={{
paddingTop: "1px",
}}
>
Changes to Bucket features such as
</div>
<ul>
<li>Bucket Policies</li>
<li>Bucket Tags</li>
<li>Bucket Object-Lock configurations</li>
<li>Bucket Encryption configuration</li>
</ul>
</Box>
</li>
<li>
The following Bucket features will NOT be replicated
<ul>
<li>Bucket notification configuration</li>
<li>Bucket lifecycle (ILM) configuration</li>
</ul>
<Box
style={{
display: "flex",
flexFlow: "column",
justifyContent: "flex-start",
}}
>
<div
style={{
paddingTop: "1px",
}}
>
The following Bucket features will NOT be replicated
</div>
<ul>
<li>Bucket notification configuration</li>
<li>Bucket lifecycle (ILM) configuration</li>
</ul>
</Box>
</li>
</ul>
</Box>

View File

@@ -0,0 +1,172 @@
// 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 { EditIcon } from "../../../../icons";
import { Box, DialogContentText } from "@mui/material";
import Grid from "@mui/material/Grid";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import { Button } from "mds";
import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import useApi from "../../Common/Hooks/useApi";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
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 EditSiteEndPoint = ({
editSite = {},
onClose,
onComplete,
classes = {},
}: {
editSite: any;
onClose: () => void;
onComplete: () => void;
classes: any;
}) => {
const dispatch = useAppDispatch();
const [editEndPointName, setEditEndPointName] = useState<string>("");
const [isEditing, invokeSiteEditApi] = useApi(
(res: any) => {
if (res.success) {
dispatch(setSnackBarMessage(res.status));
} else {
dispatch(
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
})
);
}
onComplete();
},
(err: any) => {
dispatch(setErrorSnackMessage(err));
onComplete();
}
);
const updatePeerSite = () => {
invokeSiteEditApi("PUT", `api/v1/admin/site-replication`, {
endpoint: editEndPointName,
name: editSite.name,
deploymentId: editSite.deploymentID, // readonly
});
};
let isValidEndPointUrl = false;
try {
new URL(editEndPointName);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
return (
<ModalWrapper
title={`Edit Replication Endpoint `}
modalOpen={true}
titleIcon={<EditIcon />}
onClose={onClose}
>
<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>
<Grid item xs={12} marginBottom={"15px"}>
<Box
sx={{
fontStyle: "italic",
display: "flex",
alignItems: "center",
fontSize: "12px",
marginTop: 2,
}}
>
<Box sx={{ fontWeight: 600 }}>Note:</Box>{" "}
<Box sx={{ marginLeft: 1, color: "red" }}>
Access Key and Secret Key should be same on the new site/endpoint.
</Box>
</Box>
</Grid>
</DialogContentText>
<Grid item xs={12} className={classes.modalButtonBar}>
<Button
id={"close"}
type="button"
variant="regular"
onClick={onClose}
label={"Close"}
/>
<Button
id={"update"}
type="button"
variant="callAction"
disabled={isEditing || !isValidEndPointUrl}
onClick={updatePeerSite}
label={"Update"}
/>
</Grid>
</ModalWrapper>
);
};
export default withStyles(styles)(EditSiteEndPoint);

View File

@@ -23,11 +23,6 @@ import { ReplicationSite } from "./SiteReplication";
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 ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
import withStyles from "@mui/styles/withStyles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
@@ -36,12 +31,8 @@ import {
modalStyleUtils,
spacingUtils,
} from "../../Common/FormComponents/common/styleLibrary";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../systemSlice";
import { useAppDispatch } from "../../../../store";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import EditSiteEndPoint from "./EditSiteEndPoint";
const styles = (theme: Theme) =>
createStyles({
@@ -54,54 +45,14 @@ const ReplicationSites = ({
sites,
onDeleteSite,
onRefresh,
classes,
}: {
sites: ReplicationSite[];
onDeleteSite: (isAll: boolean, sites: string[]) => void;
onRefresh: () => void;
classes: any;
}) => {
const dispatch = useAppDispatch();
const [deleteSiteKey, setIsDeleteSiteKey] = useState<string>("");
const [editSite, setEditSite] = useState<any>(null);
const [editEndPointName, setEditEndPointName] = useState<string>("");
const [isEditing, invokeSiteEditApi] = useApi(
(res: any) => {
if (res.success) {
setEditSite(null);
dispatch(setSnackBarMessage(res.status));
} else {
dispatch(
setErrorSnackMessage({
errorMessage: "Error",
detailedError: res.status,
})
);
}
onRefresh();
},
(err: any) => {
dispatch(setErrorSnackMessage(err));
onRefresh();
}
);
const updatePeerSite = () => {
invokeSiteEditApi("PUT", `api/v1/admin/site-replication`, {
endpoint: editEndPointName,
name: editSite.name,
deploymentId: editSite.deploymentID, // readonly
});
};
let isValidEndPointUrl = false;
try {
new URL(editEndPointName);
isValidEndPointUrl = true;
} catch (err) {
isValidEndPointUrl = false;
}
return (
<Box>
@@ -229,17 +180,11 @@ const ReplicationSites = ({
},
}}
>
<TooltipWrapper
tooltip={
sites.length <= 2
? "Minimum two sites are required for replication"
: "Delete Site"
}
>
<TooltipWrapper tooltip="Delete Site">
<Button
id={`delete-site-${key}-${index}`}
variant="secondary"
disabled={sites.length <= 2}
disabled={siteInfo.isCurrent}
icon={<TrashIcon />}
onClick={(e) => {
e.preventDefault();
@@ -256,6 +201,7 @@ const ReplicationSites = ({
<Button
id={`edit-icon-${key}-${index}`}
variant="regular"
disabled={siteInfo.isCurrent}
icon={<EditIcon />}
onClick={(e) => {
e.preventDefault();
@@ -295,71 +241,16 @@ const ReplicationSites = ({
) : null}
{editSite?.name === key ? (
<ModalWrapper
title={`Edit Replication Endpoint `}
modalOpen={true}
titleIcon={<EditIcon />}
<EditSiteEndPoint
onComplete={() => {
setEditSite(null);
onRefresh();
}}
editSite={editSite}
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
id={"close"}
type="button"
variant="regular"
onClick={() => {
setEditSite(null);
}}
label={"Close"}
/>
<Button
id={"update"}
type="button"
variant="callAction"
disabled={isEditing || !isValidEndPointUrl}
onClick={() => {
updatePeerSite();
}}
label={"Update"}
/>
</Grid>
</ModalWrapper>
/>
) : null}
</React.Fragment>
);

View File

@@ -0,0 +1,178 @@
// 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 } from "react";
import { Box } from "@mui/material";
import InputBoxWrapper from "../../Common/FormComponents/InputBoxWrapper/InputBoxWrapper";
import Grid from "@mui/material/Grid";
import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
import { Button } from "mds";
import { AddIcon, RemoveIcon } from "../../../../icons";
import { SiteInputRow } from "./Types";
const SRSiteInputRow = ({
rowData,
rowId: index,
onFieldChange,
onAddClick,
onRemoveClick,
canAdd = true,
canRemove = true,
showRowActions = true,
disabledFields = [],
fieldErrors = {},
}: {
rowData: SiteInputRow;
rowId: number;
onFieldChange: (e: any, fieldName: string, index: number) => void;
onAddClick?: (index: number) => void;
onRemoveClick?: (index: number) => void;
canAdd?: boolean;
canRemove?: boolean;
showRowActions?: boolean;
disabledFields?: string[];
fieldErrors?: Record<string, string>;
}) => {
const { endpoint = "", accessKey = "", secretKey = "", name = "" } = rowData;
return (
<Fragment key={`${index}`}>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-${index}`}
name={`add-rep-peer-site-${index}`}
placeholder={`site-name`}
label=""
extraInputProps={{
readOnly: disabledFields.includes("name"),
}}
value={name}
onChange={(e) => {
onFieldChange(e, "name", index);
}}
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=""
extraInputProps={{
readOnly: disabledFields.includes("endpoint"),
}}
error={fieldErrors["endpoint"]}
value={endpoint}
onChange={(e) => {
onFieldChange(e, "endpoint", index);
}}
data-test-id={`add-site-rep-peer-ep-${index}`}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-ac-${index}`}
name={`add-rep-peer-site-ac-${index}`}
label=""
required={true}
disabled={disabledFields.includes("accessKey")}
value={accessKey}
error={fieldErrors["accessKey"]}
onChange={(e) => {
onFieldChange(e, "accessKey", index);
}}
data-test-id={`add-rep-peer-site-ac-${index}`}
/>
</Box>
<Box>
<InputBoxWrapper
id={`add-rep-peer-site-sk-${index}`}
name={`add-rep-peer-site-sk-${index}`}
label=""
required={true}
type={"password"}
value={secretKey}
error={fieldErrors["secretKey"]}
disabled={disabledFields.includes("secretKey")}
onChange={(e) => {
onFieldChange(e, "secretKey", index);
}}
data-test-id={`add-rep-peer-site-sk-${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%",
},
}}
>
{showRowActions ? (
<React.Fragment>
<TooltipWrapper tooltip={"Add a Row"}>
<Button
id={`add-row-${index}`}
variant="regular"
disabled={!canAdd}
icon={<AddIcon />}
onClick={(e) => {
e.preventDefault();
onAddClick?.(index);
}}
style={{
width: 25,
height: 25,
padding: 0,
}}
/>
</TooltipWrapper>
<TooltipWrapper tooltip={"Remove Row"}>
<Button
id={`remove-row-${index}`}
variant="regular"
disabled={!canRemove}
icon={<RemoveIcon />}
onClick={(e) => {
e.preventDefault();
onRemoveClick?.(index);
}}
style={{
width: 25,
height: 25,
padding: 0,
marginLeft: 8,
}}
/>
</TooltipWrapper>
</React.Fragment>
) : null}
</Box>
</Grid>
</Fragment>
);
};
export default SRSiteInputRow;

View File

@@ -0,0 +1,8 @@
export type SiteInputRow = {
name: string;
endpoint: string;
accessKey: string;
secretKey: string;
isCurrent?: boolean;
isSaved?: boolean;
};

View File

@@ -32,6 +32,7 @@ const initialState: ConsoleState = {
allowResources: null,
customStyles: null,
envConstants: null,
serverEndPoint: "",
},
};

View File

@@ -41,6 +41,7 @@ export interface ISessionResponse {
allowResources: IAllowResources[] | null;
customStyles?: string | null;
envConstants?: IEnvironmentContants | null;
serverEndPoint?: string | undefined;
}
export interface ButtonProps {

View File

@@ -7289,6 +7289,9 @@ func init() {
}
}
},
"serverEndPoint": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
@@ -15542,6 +15545,9 @@ func init() {
}
}
},
"serverEndPoint": {
"type": "string"
},
"status": {
"type": "string",
"enum": [

View File

@@ -254,6 +254,7 @@ func getSessionResponse(ctx context.Context, session *models.Principal) (*models
AllowResources: allowResources,
CustomStyles: customStyles,
EnvConstants: &envConstants,
ServerEndPoint: getMinIOServer(),
}
return sessionResp, nil
}

View File

@@ -4143,6 +4143,8 @@ definitions:
type: boolean
distributedMode:
type: boolean
serverEndPoint:
type: string
permissions:
type: object
additionalProperties: