From ae34d886a92eabcb2e0898613b4232aaea928cb1 Mon Sep 17 00:00:00 2001 From: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com> Date: Thu, 14 Apr 2022 20:19:45 +0000 Subject: [PATCH] Add site in a new page (#1845) --- .../src/common/SecureComponent/permissions.ts | 5 + .../SiteReplication/AddReplicationSites.tsx | 627 ++++++++++++++++++ .../AddReplicationSitesModal.tsx | 419 ------------ .../SiteReplication/ReplicationSites.tsx | 2 +- .../SiteReplication/SiteReplication.tsx | 46 +- portal-ui/src/screens/Console/Console.tsx | 9 + 6 files changed, 672 insertions(+), 436 deletions(-) create mode 100644 portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSites.tsx delete mode 100644 portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSitesModal.tsx diff --git a/portal-ui/src/common/SecureComponent/permissions.ts b/portal-ui/src/common/SecureComponent/permissions.ts index 59f60484e..aabc35c3e 100644 --- a/portal-ui/src/common/SecureComponent/permissions.ts +++ b/portal-ui/src/common/SecureComponent/permissions.ts @@ -163,6 +163,7 @@ export const IAM_PAGES = { TIERS_ADD_SERVICE: "/settings/tiers/add/:service", SITE_REPLICATION: "/settings/site-replication", SITE_REPLICATION_STATUS: "/settings/site-replication/status", + SITE_REPLICATION_ADD: "/settings/site-replication/add", /* Operator */ TENANTS: "/tenants", @@ -389,6 +390,10 @@ export const IAM_PAGES_PERMISSIONS = { IAM_SCOPES.ADMIN_SERVER_INFO, IAM_SCOPES.ADMIN_CONFIG_UPDATE, ], + [IAM_PAGES.SITE_REPLICATION_ADD]: [ + IAM_SCOPES.ADMIN_SERVER_INFO, + IAM_SCOPES.ADMIN_CONFIG_UPDATE, + ], }; export const S3_ALL_RESOURCES = "arn:aws:s3:::*"; diff --git a/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSites.tsx b/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSites.tsx new file mode 100644 index 000000000..7c418dc40 --- /dev/null +++ b/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSites.tsx @@ -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 . + +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([]); + + const [accessKey, setAccessKey] = useState(""); + const [secretKey, setSecretKey] = useState(""); + const [siteConfig, setSiteConfig] = useState([]); + + 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 ( + + + } + /> + + + + {isSiteInfoLoading || isAdding ? : null} + + + +
) => { + e.preventDefault(); + return addSiteReplication(); + }} + > + + + Note:{" "} + + Access Key and Secret Key should be same on all sites. + + + + + ) => { + 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"} + /> + + + ) => { + setSecretKey(event.target.value); + }} + error={secretKey === "" ? "Secret Key is required." : ""} + label="Secret Key" + value={secretKey} + data-test-id={"add-site-rep-sec-key"} + /> + + + + + Peer Sites + + + + + + Site Name + + + Endpoint {"*"} + + + {existingSites?.map((si, index) => { + return ( + + + {}} + /> + + + {}} + /> + + + {" "} + + + ); + })} + + {siteConfig.map((sci, index) => { + let isDelDisabled = false; + + if (existingSites?.length && index === 0) { + isDelDisabled = true; + } else if (!existingSites?.length && index < 2) { + isDelDisabled = true; + } + + return ( + + + { + 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}`} + /> + + + { + 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}`} + /> + + + + } + onClick={(e) => { + e.preventDefault(); + const newRows = [...siteConfig]; + //add at the next index + newRows.splice(index + 1, 0, { + name: "", + endpoint: "", + }); + + setSiteConfig(newRows); + }} + /> + } + onClick={(e) => { + e.preventDefault(); + setSiteConfig( + siteConfig.filter((_, idx) => idx !== index) + ); + }} + /> + + + + ); + })} + + + + + + + + + +
+
+ + + + + + + About Site Replication + + + + The following changes are replicated to all other sites + + +
+ Creation and deletion of buckets and objects +
+
+ +
+ Creation and deletion of all IAM users, groups, policies + and their mappings to users or groups +
+
+ +
Creation of STS credentials
+
+ +
+ Creation and deletion of service accounts (except those + owned by the root user) +
+
+ +
+ Changes to Bucket features such as +
    +
  • Bucket Policies
  • +
  • Bucket Tags
  • +
  • Bucket Object-Lock configurations
  • +
  • Bucket Encryption configuration
  • +
+
+
+ + +
+ The following Bucket features will NOT be replicated +
    +
  • Bucket notification configuration
  • +
  • Bucket lifecycle (ILM) configuration
  • +
+
+
+
+
+ } + /> + + + + ); +}; + +const connector = connect(null, { + setErrorSnackMessage, + setSnackBarMessage, +}); +export default connector(AddReplicationSites); diff --git a/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSitesModal.tsx b/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSitesModal.tsx deleted file mode 100644 index 36dabd513..000000000 --- a/portal-ui/src/screens/Console/Configurations/SiteReplication/AddReplicationSitesModal.tsx +++ /dev/null @@ -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 . - -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(""); - const [secretKey, setSecretKey] = useState(""); - const [siteConfig, setSiteConfig] = useState(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 ( - } - data-test-id={"add-site-replication-modal"} - > - {isAdding ? : null} -
) => { - e.preventDefault(); - return addSiteReplication(); - }} - > - - - Note:{" "} - - Access Key and Secret Key should be same on all sites. - - - - - ) => { - 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"} - /> - - - ) => { - setSecretKey(event.target.value); - }} - error={secretKey === "" ? "Secret Key is required." : ""} - label="Secret Key" - value={secretKey} - data-test-id={"add-site-rep-sec-key"} - /> - - - - - Peer Sites - - - - - - Site Name - - Endpoint {"*"} - - {existingSites?.map((si, index) => { - return ( - - - {}} - /> - - - {}} - /> - - - {" "} - - - ); - })} - - {siteConfig.map((sci, index) => { - let isDelDisabled = false; - - if (existingSites?.length && index === 0) { - isDelDisabled = true; - } else if (!existingSites?.length && index < 2) { - isDelDisabled = true; - } - - return ( - - - { - 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}`} - /> - - - { - 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}`} - /> - - - - } - onClick={(e) => { - e.preventDefault(); - const newRows = [...siteConfig]; - //add at the next index - newRows.splice(index + 1, 0, { - name: "", - endpoint: "", - }); - - setSiteConfig(newRows); - }} - /> - } - onClick={(e) => { - e.preventDefault(); - setSiteConfig( - siteConfig.filter((_, idx) => idx !== index) - ); - }} - /> - - - - ); - })} - - - - - - - - - -
-
- ); -}; - -const connector = connect(null, { - setErrorSnackMessage, - setSnackBarMessage, -}); -export default connector(AddReplicationSitesModal); diff --git a/portal-ui/src/screens/Console/Configurations/SiteReplication/ReplicationSites.tsx b/portal-ui/src/screens/Console/Configurations/SiteReplication/ReplicationSites.tsx index 23371709e..78d3fc9f1 100644 --- a/portal-ui/src/screens/Console/Configurations/SiteReplication/ReplicationSites.tsx +++ b/portal-ui/src/screens/Console/Configurations/SiteReplication/ReplicationSites.tsx @@ -128,7 +128,7 @@ const ReplicationSites = ({ const key = `${siteInfo.name}`; return ( - + { const [sites, setSites] = useState([]); - const [isAddOpen, setIsAddOpen] = useState(false); - const [deleteAll, setIsDeleteAll] = useState(false); const [isSiteInfoLoading, invokeSiteInfoApi] = useApi( (res: any) => { @@ -148,7 +150,7 @@ const SiteReplication = ({ disabled={isRemoving} icon={} onClick={() => { - setIsAddOpen(true); + history.push(IAM_PAGES.SITE_REPLICATION_ADD); }} /> @@ -185,19 +187,9 @@ const SiteReplication = ({ ) : null} - {isAddOpen ? ( - { - setIsAddOpen(false); - getSites(); - }} - /> - ) : null} - } + iconComponent={} help={ This feature allows multiple independent MinIO sites (or clusters) @@ -205,6 +197,28 @@ const SiteReplication = ({ configured as replicas. In this situation the set of replica sites are referred to as peer sites or just sites.
+ +
    +
  • + 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. +
  • +
  • + All sites must have the same deployment credentials (i.e. + MINIO_ROOT_USER, MINIO_ROOT_PASSWORD). +
  • +
  • + All sites must be using the same external IDP(s) if any. +
  • +
  • + For SSE-S3 or SSE-KMS encryption via KMS, all sites must + have access to a central KMS deployment. server. +
  • +
+

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