github.com/minio/console@v1.4.1/web-app/src/screens/Console/Configurations/SiteReplication/AddReplicationSites.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2022 MinIO, Inc.
     3  //
     4  // This program is free software: you can redistribute it and/or modify
     5  // it under the terms of the GNU Affero General Public License as published by
     6  // the Free Software Foundation, either version 3 of the License, or
     7  // (at your option) any later version.
     8  //
     9  // This program is distributed in the hope that it will be useful,
    10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    12  // GNU Affero General Public License for more details.
    13  //
    14  // You should have received a copy of the GNU Affero General Public License
    15  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    16  
    17  import React, { Fragment, useEffect, useState } from "react";
    18  import { useNavigate } from "react-router-dom";
    19  import {
    20    BackLink,
    21    Button,
    22    ClustersIcon,
    23    HelpBox,
    24    PageLayout,
    25    Box,
    26    Grid,
    27    ProgressBar,
    28    InputLabel,
    29    SectionTitle,
    30  } from "mds";
    31  import useApi from "../../Common/Hooks/useApi";
    32  import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
    33  import {
    34    setErrorSnackMessage,
    35    setHelpName,
    36    setSnackBarMessage,
    37  } from "../../../../systemSlice";
    38  import { useAppDispatch } from "../../../../store";
    39  import { useSelector } from "react-redux";
    40  import { selSession } from "../../consoleSlice";
    41  import SRSiteInputRow from "./SRSiteInputRow";
    42  import { SiteInputRow } from "./Types";
    43  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    44  import HelpMenu from "../../HelpMenu";
    45  
    46  const isValidEndPoint = (ep: string) => {
    47    let isValidEndPointUrl = false;
    48  
    49    try {
    50      new URL(ep);
    51      isValidEndPointUrl = true;
    52    } catch (err) {
    53      isValidEndPointUrl = false;
    54    }
    55    if (isValidEndPointUrl) {
    56      return "";
    57    } else {
    58      return "Invalid Endpoint";
    59    }
    60  };
    61  
    62  const isEmptyValue = (value: string): boolean => {
    63    return value?.trim() === "";
    64  };
    65  
    66  const TableHeader = () => {
    67    return (
    68      <React.Fragment>
    69        <Box>
    70          <InputLabel>Site Name</InputLabel>
    71        </Box>
    72        <Box>
    73          <InputLabel>Endpoint {"*"}</InputLabel>
    74        </Box>
    75        <Box>
    76          <InputLabel>Access Key {"*"}</InputLabel>
    77        </Box>
    78        <Box>
    79          <InputLabel>Secret Key {"*"}</InputLabel>
    80        </Box>
    81        <Box> </Box>
    82      </React.Fragment>
    83    );
    84  };
    85  
    86  const SiteTypeHeader = ({ title }: { title: string }) => {
    87    return (
    88      <Grid item xs={12}>
    89        <Box
    90          sx={{
    91            marginBottom: "15px",
    92            fontSize: "14px",
    93            fontWeight: 600,
    94          }}
    95        >
    96          {title}
    97        </Box>
    98      </Grid>
    99    );
   100  };
   101  
   102  const AddReplicationSites = () => {
   103    const dispatch = useAppDispatch();
   104    const navigate = useNavigate();
   105  
   106    const { serverEndPoint = "" } = useSelector(selSession);
   107  
   108    const [currentSite, setCurrentSite] = useState<SiteInputRow[]>([
   109      {
   110        endpoint: serverEndPoint,
   111        name: "",
   112        accessKey: "",
   113        secretKey: "",
   114      },
   115    ]);
   116  
   117    const [existingSites, setExistingSites] = useState<SiteInputRow[]>([]);
   118  
   119    const setDefaultNewRows = () => {
   120      const defaultNewSites = [
   121        { endpoint: "", name: "", accessKey: "", secretKey: "" },
   122      ];
   123      setExistingSites(defaultNewSites);
   124    };
   125  
   126    const [isSiteInfoLoading, invokeSiteInfoApi] = useApi(
   127      (res: any) => {
   128        const { sites: siteList, name: curSiteName } = res;
   129        // current site name to be the fist one.
   130        const foundIdx = siteList.findIndex((el: any) => el.name === curSiteName);
   131        if (foundIdx !== -1) {
   132          let curSite = siteList[foundIdx];
   133          curSite = {
   134            ...curSite,
   135            isCurrent: true,
   136            isSaved: true,
   137          };
   138  
   139          setCurrentSite([curSite]);
   140          siteList.splice(foundIdx, 1);
   141        }
   142  
   143        siteList.sort((x: any, y: any) => {
   144          return x.name === curSiteName ? -1 : y.name === curSiteName ? 1 : 0;
   145        });
   146  
   147        let existingSiteList = siteList.map((si: any) => {
   148          return {
   149            ...si,
   150            accessKey: "",
   151            secretKey: "",
   152            isSaved: true,
   153          };
   154        });
   155  
   156        if (existingSiteList.length) {
   157          setExistingSites(existingSiteList);
   158        } else {
   159          setDefaultNewRows();
   160        }
   161      },
   162      (err: any) => {
   163        setDefaultNewRows();
   164      },
   165    );
   166  
   167    const getSites = () => {
   168      invokeSiteInfoApi("GET", `api/v1/admin/site-replication`);
   169    };
   170  
   171    useEffect(() => {
   172      getSites();
   173      // eslint-disable-next-line react-hooks/exhaustive-deps
   174    }, []);
   175  
   176    useEffect(() => {
   177      dispatch(setHelpName("add-replication-sites"));
   178      // eslint-disable-next-line react-hooks/exhaustive-deps
   179    }, []);
   180  
   181    const existingEndPointsValidity = existingSites.reduce(
   182      (acc: string[], cv, i) => {
   183        const epValue = existingSites[i].endpoint;
   184        const isEpValid = isValidEndPoint(epValue);
   185  
   186        if (isEpValid === "" && epValue !== "") {
   187          acc.push(isEpValid);
   188        }
   189        return acc;
   190      },
   191      [],
   192    );
   193  
   194    const isExistingCredsValidity = existingSites
   195      .map((site) => {
   196        return !isEmptyValue(site.accessKey) && !isEmptyValue(site.secretKey);
   197      })
   198      .filter(Boolean);
   199  
   200    const { accessKey: cAccessKey, secretKey: cSecretKey } = currentSite[0];
   201  
   202    const isCurCredsValid =
   203      !isEmptyValue(cAccessKey) && !isEmptyValue(cSecretKey);
   204    const peerEndpointsValid =
   205      existingEndPointsValidity.length === existingSites.length;
   206    const peerCredsValid =
   207      isExistingCredsValidity.length === existingSites.length;
   208  
   209    let isAllFieldsValid =
   210      isCurCredsValid && peerEndpointsValid && peerCredsValid;
   211  
   212    const [isAdding, invokeSiteAddApi] = useApi(
   213      (res: any) => {
   214        if (res.success) {
   215          dispatch(setSnackBarMessage(res.status));
   216          resetForm();
   217          getSites();
   218          navigate(IAM_PAGES.SITE_REPLICATION);
   219        } else {
   220          dispatch(
   221            setErrorSnackMessage({
   222              errorMessage: "Error",
   223              detailedError: res.status,
   224            }),
   225          );
   226        }
   227      },
   228      (err: any) => {
   229        dispatch(setErrorSnackMessage(err));
   230      },
   231    );
   232  
   233    const resetForm = () => {
   234      setDefaultNewRows();
   235      setCurrentSite((prevItems) => {
   236        return prevItems.map((item, ix) => ({
   237          ...item,
   238          accessKey: "",
   239          secretKey: "",
   240          name: "",
   241        }));
   242      });
   243    };
   244  
   245    const addSiteReplication = () => {
   246      const curSite: any[] = currentSite?.map((es, idx) => {
   247        return {
   248          accessKey: es.accessKey,
   249          secretKey: es.secretKey,
   250          name: es.name,
   251          endpoint: es.endpoint.trim(),
   252        };
   253      });
   254  
   255      const newOrExistingSitesToAdd = existingSites.reduce(
   256        (acc: any, ns, idx) => {
   257          if (ns.endpoint) {
   258            acc.push({
   259              accessKey: ns.accessKey,
   260              secretKey: ns.secretKey,
   261              name: ns.name || `dr-site-${idx}`,
   262              endpoint: ns.endpoint.trim(),
   263            });
   264          }
   265          return acc;
   266        },
   267        [],
   268      );
   269  
   270      const sitesToAdd = curSite.concat(newOrExistingSitesToAdd);
   271  
   272      invokeSiteAddApi("POST", `api/v1/admin/site-replication`, sitesToAdd);
   273    };
   274  
   275    const renderCurrentSite = () => {
   276      return (
   277        <Box
   278          sx={{
   279            marginTop: "15px",
   280          }}
   281        >
   282          <SiteTypeHeader title={"This Site"} />
   283          <Box
   284            withBorders
   285            sx={{
   286              display: "grid",
   287              gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
   288              padding: "15px",
   289              gap: "10px",
   290              maxHeight: "430px",
   291              overflowY: "auto",
   292            }}
   293          >
   294            <TableHeader />
   295  
   296            {currentSite.map((cs, index) => {
   297              const accessKeyError = isEmptyValue(cs.accessKey)
   298                ? "AccessKey is required"
   299                : "";
   300              const secretKeyError = isEmptyValue(cs.secretKey)
   301                ? "SecretKey is required"
   302                : "";
   303              return (
   304                <SRSiteInputRow
   305                  key={`current-${index}`}
   306                  rowData={cs}
   307                  rowId={index}
   308                  fieldErrors={{
   309                    accessKey: accessKeyError,
   310                    secretKey: secretKeyError,
   311                  }}
   312                  onFieldChange={(e, fieldName, index) => {
   313                    const filedValue = e.target.value;
   314                    if (fieldName !== "") {
   315                      setCurrentSite((prevItems) => {
   316                        return prevItems.map((item, ix) =>
   317                          ix === index
   318                            ? { ...item, [fieldName]: filedValue }
   319                            : item,
   320                        );
   321                      });
   322                    }
   323                  }}
   324                  showRowActions={false}
   325                />
   326              );
   327            })}
   328          </Box>
   329        </Box>
   330      );
   331    };
   332  
   333    const renderPeerSites = () => {
   334      return (
   335        <Box
   336          sx={{
   337            marginTop: "25px",
   338          }}
   339        >
   340          <SiteTypeHeader title={"Peer Sites"} />
   341          <Box
   342            withBorders
   343            sx={{
   344              display: "grid",
   345              gridTemplateColumns: ".8fr 1.2fr .8fr .8fr .2fr",
   346              padding: "15px",
   347              gap: "10px",
   348              maxHeight: "430px",
   349              overflowY: "auto",
   350            }}
   351          >
   352            <TableHeader />
   353  
   354            {existingSites.map((ps, index) => {
   355              const endPointError = isValidEndPoint(ps.endpoint);
   356  
   357              const accessKeyError = isEmptyValue(ps.accessKey)
   358                ? "AccessKey is required"
   359                : "";
   360              const secretKeyError = isEmptyValue(ps.secretKey)
   361                ? "SecretKey is required"
   362                : "";
   363  
   364              return (
   365                <SRSiteInputRow
   366                  key={`exiting-${index}`}
   367                  rowData={ps}
   368                  rowId={index}
   369                  fieldErrors={{
   370                    endpoint: endPointError,
   371                    accessKey: accessKeyError,
   372                    secretKey: secretKeyError,
   373                  }}
   374                  onFieldChange={(e, fieldName, index) => {
   375                    const filedValue = e.target.value;
   376                    setExistingSites((prevItems) => {
   377                      return prevItems.map((item, ix) =>
   378                        ix === index
   379                          ? { ...item, [fieldName]: filedValue }
   380                          : item,
   381                      );
   382                    });
   383                  }}
   384                  canAdd={true}
   385                  canRemove={index > 0 && !ps.isSaved}
   386                  onAddClick={() => {
   387                    const newRows = [...existingSites];
   388                    //add at the next index
   389                    newRows.splice(index + 1, 0, {
   390                      name: "",
   391                      endpoint: "",
   392                      accessKey: "",
   393                      secretKey: "",
   394                    });
   395  
   396                    setExistingSites(newRows);
   397                  }}
   398                  onRemoveClick={(index) => {
   399                    setExistingSites(
   400                      existingSites.filter((_, idx) => idx !== index),
   401                    );
   402                  }}
   403                />
   404              );
   405            })}
   406          </Box>
   407        </Box>
   408      );
   409    };
   410  
   411    return (
   412      <Fragment>
   413        <PageHeaderWrapper
   414          label={
   415            <BackLink
   416              label={"Add Replication Site"}
   417              onClick={() => navigate(IAM_PAGES.SITE_REPLICATION)}
   418            />
   419          }
   420          actions={<HelpMenu />}
   421        />
   422        <PageLayout>
   423          <Box
   424            sx={{
   425              display: "grid",
   426              padding: "25px",
   427              gap: "25px",
   428              gridTemplateColumns: "1fr",
   429              border: "1px solid #eaeaea",
   430            }}
   431          >
   432            <Box>
   433              <SectionTitle separator icon={<ClustersIcon />}>
   434                Add Sites for Replication
   435              </SectionTitle>
   436  
   437              {isSiteInfoLoading || isAdding ? <ProgressBar /> : null}
   438  
   439              <Box
   440                sx={{
   441                  fontSize: "14px",
   442                  fontStyle: "italic",
   443                  marginTop: "10px",
   444                  marginBottom: "10px",
   445                }}
   446              >
   447                Note: AccessKey and SecretKey values for every site is required
   448                while adding or editing peer sites
   449              </Box>
   450              <form
   451                noValidate
   452                autoComplete="off"
   453                onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   454                  e.preventDefault();
   455                  return addSiteReplication();
   456                }}
   457              >
   458                {renderCurrentSite()}
   459  
   460                {renderPeerSites()}
   461  
   462                <Grid item xs={12}>
   463                  <Box
   464                    sx={{
   465                      display: "flex",
   466                      alignItems: "center",
   467                      justifyContent: "flex-end",
   468                      marginTop: "20px",
   469                      gap: "15px",
   470                    }}
   471                  >
   472                    <Button
   473                      id={"clear"}
   474                      type="button"
   475                      variant="regular"
   476                      disabled={isAdding}
   477                      onClick={resetForm}
   478                      label={"Clear"}
   479                    />
   480  
   481                    <Button
   482                      id={"save"}
   483                      type="submit"
   484                      variant="callAction"
   485                      disabled={isAdding || !isAllFieldsValid}
   486                      label={"Save"}
   487                    />
   488                  </Box>
   489                </Grid>
   490              </form>
   491            </Box>
   492  
   493            <HelpBox
   494              title={""}
   495              iconComponent={null}
   496              help={
   497                <Fragment>
   498                  <Box
   499                    sx={{
   500                      marginTop: "-25px",
   501                      fontSize: "16px",
   502                      fontWeight: 600,
   503                      display: "flex",
   504                      alignItems: "center",
   505                      justifyContent: "flex-start",
   506                      padding: "2px",
   507                    }}
   508                  >
   509                    <Box
   510                      sx={{
   511                        backgroundColor: "#07193E",
   512                        height: "15px",
   513                        width: "15px",
   514                        display: "flex",
   515                        alignItems: "center",
   516                        justifyContent: "center",
   517                        borderRadius: "50%",
   518                        marginRight: "18px",
   519                        padding: "3px",
   520                        paddingLeft: "2px",
   521                        "& .min-icon": {
   522                          height: "11px",
   523                          width: "11px",
   524                          fill: "#ffffff",
   525                        },
   526                      }}
   527                    >
   528                      <ClustersIcon />
   529                    </Box>
   530                    About Site Replication
   531                  </Box>
   532                  <Box
   533                    sx={{
   534                      display: "flex",
   535                      flexFlow: "column",
   536                      fontSize: "14px",
   537                      flex: "2",
   538                      "& li": {
   539                        fontSize: "14px",
   540                        display: "flex",
   541                        marginTop: "15px",
   542                        marginBottom: "15px",
   543                        width: "100%",
   544  
   545                        "&.step-text": {
   546                          fontWeight: 400,
   547                        },
   548                      },
   549                    }}
   550                  >
   551                    <Box>
   552                      The following changes are replicated to all other sites
   553                    </Box>
   554                    <ul>
   555                      <li>Creation and deletion of buckets and objects</li>
   556                      <li>
   557                        Creation and deletion of all IAM users, groups, policies
   558                        and their mappings to users or groups
   559                      </li>
   560                      <li>Creation of STS credentials</li>
   561                      <li>
   562                        Creation and deletion of service accounts (except those
   563                        owned by the root user)
   564                      </li>
   565                      <li>
   566                        <Box
   567                          style={{
   568                            display: "flex",
   569                            flexFlow: "column",
   570  
   571                            justifyContent: "flex-start",
   572                          }}
   573                        >
   574                          <div
   575                            style={{
   576                              paddingTop: "1px",
   577                            }}
   578                          >
   579                            Changes to Bucket features such as
   580                          </div>
   581                          <ul>
   582                            <li>Bucket Policies</li>
   583                            <li>Bucket Tags</li>
   584                            <li>Bucket Object-Lock configurations</li>
   585                            <li>Bucket Encryption configuration</li>
   586                          </ul>
   587                        </Box>
   588                      </li>
   589  
   590                      <li>
   591                        <Box
   592                          style={{
   593                            display: "flex",
   594                            flexFlow: "column",
   595  
   596                            justifyContent: "flex-start",
   597                          }}
   598                        >
   599                          <div
   600                            style={{
   601                              paddingTop: "1px",
   602                            }}
   603                          >
   604                            The following Bucket features will NOT be replicated
   605                          </div>
   606  
   607                          <ul>
   608                            <li>Bucket notification configuration</li>
   609                            <li>Bucket lifecycle (ILM) configuration</li>
   610                          </ul>
   611                        </Box>
   612                      </li>
   613                    </ul>
   614                  </Box>
   615                </Fragment>
   616              }
   617            />
   618          </Box>
   619        </PageLayout>
   620      </Fragment>
   621    );
   622  };
   623  
   624  export default AddReplicationSites;