github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/BulkReplicationModal.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2021 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 {
    19    Box,
    20    CheckCircleIcon,
    21    FormLayout,
    22    InputBox,
    23    ReadBox,
    24    Select,
    25    Switch,
    26    Tooltip,
    27    WarnIcon,
    28    Wizard,
    29  } from "mds";
    30  import get from "lodash/get";
    31  import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
    32  import { getBytes, k8sScalarUnitsExcluding } from "../../../../common/utils";
    33  import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
    34  import { setModalErrorSnackMessage } from "../../../../systemSlice";
    35  import { useAppDispatch } from "../../../../store";
    36  import { api } from "api";
    37  import { MultiBucketResponseItem } from "api/consoleApi";
    38  import { errorToHandler } from "api/errors";
    39  import { SelectorTypes } from "../../../../common/types";
    40  
    41  interface IBulkReplicationModal {
    42    open: boolean;
    43    closeModalAndRefresh: (clearSelection: boolean) => any;
    44    buckets: string[];
    45  }
    46  
    47  const AddBulkReplicationModal = ({
    48    open,
    49    closeModalAndRefresh,
    50    buckets,
    51  }: IBulkReplicationModal) => {
    52    const dispatch = useAppDispatch();
    53    const [bucketsToAlter, setBucketsToAlter] = useState<string[]>([]);
    54    const [addLoading, setAddLoading] = useState<boolean>(false);
    55    const [externalLoading, setExternalLoading] = useState<boolean>(false);
    56    const [accessKey, setAccessKey] = useState<string>("");
    57    const [secretKey, setSecretKey] = useState<string>("");
    58    const [targetURL, setTargetURL] = useState<string>("");
    59    const [region, setRegion] = useState<string>("");
    60    const [useTLS, setUseTLS] = useState<boolean>(true);
    61    const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
    62      "async",
    63    );
    64    const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
    65    const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
    66    const [healthCheck, setHealthCheck] = useState<string>("60");
    67    const [relationBuckets, setRelationBuckets] = useState<string[]>([]);
    68    const [remoteBucketsOpts, setRemoteBucketOpts] = useState<string[]>([]);
    69    const [responseItem, setResponseItem] = useState<
    70      MultiBucketResponseItem[] | undefined
    71    >([]);
    72  
    73    const optionsForBucketsDrop: SelectorTypes[] = remoteBucketsOpts.map(
    74      (remoteBucketName: string) => {
    75        return {
    76          label: remoteBucketName,
    77          value: remoteBucketName,
    78        };
    79      },
    80    );
    81  
    82    useEffect(() => {
    83      if (relationBuckets.length === 0) {
    84        const bucketsAlter: string[] = [];
    85        const relationBucketsAlter: string[] = [];
    86  
    87        buckets.forEach((item: string) => {
    88          bucketsAlter.push(item);
    89          relationBucketsAlter.push("");
    90        });
    91  
    92        setRelationBuckets(relationBucketsAlter);
    93        setBucketsToAlter(bucketsAlter);
    94      }
    95    }, [buckets, relationBuckets.length]);
    96  
    97    const addRecord = () => {
    98      setAddLoading(true);
    99      const replicate = bucketsToAlter.map((bucketName, index) => {
   100        return {
   101          originBucket: bucketName,
   102          destinationBucket: relationBuckets[index],
   103        };
   104      });
   105  
   106      const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
   107      const hc = parseInt(healthCheck);
   108  
   109      const remoteBucketsInfo = {
   110        accessKey: accessKey,
   111        secretKey: secretKey,
   112        targetURL: endURL,
   113        region: region,
   114        bucketsRelation: replicate,
   115        syncMode: replicationMode,
   116        bandwidth:
   117          replicationMode === "async"
   118            ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
   119            : 0,
   120        healthCheckPeriod: hc,
   121      };
   122  
   123      api.bucketsReplication
   124        .setMultiBucketReplication(remoteBucketsInfo)
   125        .then((response) => {
   126          setAddLoading(false);
   127  
   128          const states = response.data.replicationState;
   129          setResponseItem(states);
   130  
   131          const filterErrors = states?.filter(
   132            (itm) => itm.errorString && itm.errorString !== "",
   133          );
   134  
   135          if (filterErrors?.length === 0) {
   136            closeModalAndRefresh(true);
   137          } else {
   138            setTimeout(() => {
   139              removeSuccessItems(states);
   140            }, 500);
   141          }
   142        })
   143        .catch((err) => {
   144          setAddLoading(false);
   145          dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
   146        });
   147    };
   148  
   149    const retrieveRemoteBuckets = (
   150      wizardPageJump: (page: number | string) => void,
   151    ) => {
   152      const remoteConnectInfo = {
   153        accessKey: accessKey,
   154        secretKey: secretKey,
   155        targetURL: targetURL,
   156        useTLS,
   157      };
   158      setExternalLoading(true);
   159  
   160      api.listExternalBuckets
   161        .listExternalBuckets(remoteConnectInfo)
   162        .then((res) => {
   163          const buckets = get(res.data, "buckets", []);
   164  
   165          if (buckets && buckets.length > 0) {
   166            const arrayReplaceBuckets = buckets.map((element: any) => {
   167              return element.name;
   168            });
   169  
   170            setRemoteBucketOpts(arrayReplaceBuckets);
   171          }
   172  
   173          wizardPageJump("++");
   174          setExternalLoading(false);
   175        })
   176        .catch((err) => {
   177          setExternalLoading(false);
   178          dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
   179        });
   180    };
   181  
   182    const stateOfItem = (initialBucket: string) => {
   183      if (responseItem && responseItem.length > 0) {
   184        const bucketResponse = responseItem.find(
   185          (item) => item.originBucket === initialBucket,
   186        );
   187  
   188        if (bucketResponse) {
   189          const errString = get(bucketResponse, "errorString", "");
   190  
   191          if (errString) {
   192            return errString;
   193          }
   194  
   195          return "";
   196        }
   197      }
   198      return "n/a";
   199    };
   200  
   201    const LogoToShow = ({ errString }: { errString: string }) => {
   202      switch (errString) {
   203        case "":
   204          return (
   205            <Box
   206              sx={{
   207                color: "#42C91A",
   208              }}
   209            >
   210              <CheckCircleIcon />
   211            </Box>
   212          );
   213        case "n/a":
   214          return null;
   215        default:
   216          if (errString) {
   217            return (
   218              <Box
   219                sx={{
   220                  color: "#C72C48",
   221                }}
   222              >
   223                <Tooltip tooltip={errString} placement="top">
   224                  <WarnIcon />
   225                </Tooltip>
   226              </Box>
   227            );
   228          }
   229      }
   230      return null;
   231    };
   232  
   233    const updateItem = (indexItem: number, value: string) => {
   234      const updatedList = [...relationBuckets];
   235      updatedList[indexItem] = value;
   236      setRelationBuckets(updatedList);
   237    };
   238  
   239    const itemDisplayBulk = (indexItem: number) => {
   240      if (remoteBucketsOpts.length > 0) {
   241        return (
   242          <Fragment>
   243            <Select
   244              label=""
   245              id={`assign-bucket-${indexItem}`}
   246              name={`assign-bucket-${indexItem}`}
   247              value={relationBuckets[indexItem]}
   248              onChange={(value) => {
   249                updateItem(indexItem, value);
   250              }}
   251              options={optionsForBucketsDrop}
   252              disabled={addLoading}
   253            />
   254          </Fragment>
   255        );
   256      }
   257      return (
   258        <Fragment>
   259          <InputBox
   260            id={`assign-bucket-${indexItem}`}
   261            name={`assign-bucket-${indexItem}`}
   262            label=""
   263            onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
   264              updateItem(indexItem, event.target.value);
   265            }}
   266            value={relationBuckets[indexItem]}
   267            disabled={addLoading}
   268          />
   269        </Fragment>
   270      );
   271    };
   272  
   273    const removeSuccessItems = (
   274      responseItem: MultiBucketResponseItem[] | undefined,
   275    ) => {
   276      let newBucketsToAlter = [...bucketsToAlter];
   277      let newRelationBuckets = [...relationBuckets];
   278  
   279      responseItem?.forEach((successElement) => {
   280        const errorString = get(successElement, "errorString", "");
   281  
   282        if (!errorString || errorString === "") {
   283          const indexToRemove = newBucketsToAlter.indexOf(
   284            successElement.originBucket || "",
   285          );
   286  
   287          newBucketsToAlter.splice(indexToRemove, 1);
   288          newRelationBuckets.splice(indexToRemove, 1);
   289        }
   290      });
   291  
   292      setBucketsToAlter(newBucketsToAlter);
   293      setRelationBuckets(newRelationBuckets);
   294    };
   295  
   296    return (
   297      <ModalWrapper
   298        modalOpen={open}
   299        onClose={() => {
   300          closeModalAndRefresh(false);
   301        }}
   302        title="Set Multiple Bucket Replication"
   303      >
   304        <Wizard
   305          loadingStep={addLoading || externalLoading}
   306          wizardSteps={[
   307            {
   308              label: "Remote Configuration",
   309              componentRender: (
   310                <Fragment>
   311                  <FormLayout containerPadding={false} withBorders={false}>
   312                    <ReadBox
   313                      label="Local Buckets to replicate"
   314                      sx={{ maxWidth: "440px", width: "100%" }}
   315                    >
   316                      {bucketsToAlter.join(", ")}
   317                    </ReadBox>
   318                    <h4>Remote Endpoint Configuration</h4>
   319                    <span style={{ fontSize: 14 }}>
   320                      Please avoid the use of root credentials for this feature
   321                      <br />
   322                      <br />
   323                    </span>
   324                    <InputBox
   325                      id="accessKey"
   326                      name="accessKey"
   327                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   328                        setAccessKey(e.target.value);
   329                      }}
   330                      label="Access Key"
   331                      value={accessKey}
   332                    />
   333                    <InputBox
   334                      id="secretKey"
   335                      name="secretKey"
   336                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   337                        setSecretKey(e.target.value);
   338                      }}
   339                      label="Secret Key"
   340                      value={secretKey}
   341                    />
   342                    <InputBox
   343                      id="targetURL"
   344                      name="targetURL"
   345                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   346                        setTargetURL(e.target.value);
   347                      }}
   348                      placeholder="play.min.io:9000"
   349                      label="Target URL"
   350                      value={targetURL}
   351                    />
   352                    <Switch
   353                      checked={useTLS}
   354                      id="useTLS"
   355                      name="useTLS"
   356                      label="Use TLS"
   357                      onChange={(e) => {
   358                        setUseTLS(e.target.checked);
   359                      }}
   360                      value="yes"
   361                    />
   362                    <InputBox
   363                      id="region"
   364                      name="region"
   365                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   366                        setRegion(e.target.value);
   367                      }}
   368                      label="Region"
   369                      value={region}
   370                    />
   371                    <Select
   372                      id="replication_mode"
   373                      name="replication_mode"
   374                      onChange={(value) => {
   375                        setReplicationMode(value as "sync" | "async");
   376                      }}
   377                      label="Replication Mode"
   378                      value={replicationMode}
   379                      options={[
   380                        { label: "Asynchronous", value: "async" },
   381                        { label: "Synchronous", value: "sync" },
   382                      ]}
   383                    />
   384                    {replicationMode === "async" && (
   385                      <InputBox
   386                        type="number"
   387                        id="bandwidth_scalar"
   388                        name="bandwidth_scalar"
   389                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   390                          if (e.target.validity.valid) {
   391                            setBandwidthScalar(e.target.value as string);
   392                          }
   393                        }}
   394                        label="Bandwidth"
   395                        value={bandwidthScalar}
   396                        min="0"
   397                        pattern={"[0-9]*"}
   398                        overlayObject={
   399                          <InputUnitMenu
   400                            id={"quota_unit"}
   401                            onUnitChange={(newValue) => {
   402                              setBandwidthUnit(newValue);
   403                            }}
   404                            unitSelected={bandwidthUnit}
   405                            unitsList={k8sScalarUnitsExcluding(["Ki"])}
   406                            disabled={false}
   407                          />
   408                        }
   409                      />
   410                    )}
   411                    <InputBox
   412                      id="healthCheck"
   413                      name="healthCheck"
   414                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   415                        setHealthCheck(e.target.value as string);
   416                      }}
   417                      label="Health Check Duration"
   418                      value={healthCheck}
   419                    />
   420                  </FormLayout>
   421                </Fragment>
   422              ),
   423              buttons: [
   424                {
   425                  type: "custom",
   426                  label: "Next",
   427                  enabled: !externalLoading,
   428                  action: retrieveRemoteBuckets,
   429                },
   430              ],
   431            },
   432            {
   433              label: "Bucket Assignments",
   434              componentRender: (
   435                <Fragment>
   436                  <h3>Remote Bucket Assignments</h3>
   437                  <span style={{ fontSize: 14 }}>
   438                    Please select / type the desired remote bucket were you want
   439                    the local data to be replicated.
   440                  </span>
   441                  <Box
   442                    sx={{
   443                      display: "grid",
   444                      gridTemplateColumns: "auto auto 45px",
   445                      alignItems: "center",
   446                      justifyContent: "stretch",
   447                      "& .hide": {
   448                        opacity: 0,
   449                        transitionDuration: "0.3s",
   450                      },
   451                    }}
   452                  >
   453                    {bucketsToAlter.map((bucketName: string, index: number) => {
   454                      const errorItem = stateOfItem(bucketName);
   455                      return (
   456                        <Fragment
   457                          key={`buckets-assignation-${index.toString()}-${bucketName}`}
   458                        >
   459                          <div className={errorItem === "" ? "hide" : ""}>
   460                            {bucketName}
   461                          </div>
   462                          <div className={errorItem === "" ? "hide" : ""}>
   463                            {itemDisplayBulk(index)}
   464                          </div>
   465                          <div className={errorItem === "" ? "hide" : ""}>
   466                            {responseItem && responseItem.length > 0 && (
   467                              <LogoToShow errString={errorItem} />
   468                            )}
   469                          </div>
   470                        </Fragment>
   471                      );
   472                    })}
   473                  </Box>
   474                </Fragment>
   475              ),
   476              buttons: [
   477                {
   478                  type: "back",
   479                  label: "Back",
   480                  enabled: true,
   481                },
   482                {
   483                  type: "next",
   484                  label: "Create",
   485                  enabled: !addLoading,
   486                  action: addRecord,
   487                },
   488              ],
   489            },
   490          ]}
   491          forModal
   492        />
   493      </ModalWrapper>
   494    );
   495  };
   496  
   497  export default AddBulkReplicationModal;