github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/AddReplicationModal.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, { useEffect, useState } from "react";
    18  import get from "lodash/get";
    19  import {
    20    Box,
    21    BucketReplicationIcon,
    22    Button,
    23    FormLayout,
    24    Grid,
    25    InputBox,
    26    Select,
    27    Switch,
    28  } from "mds";
    29  import { api } from "api";
    30  import { errorToHandler } from "api/errors";
    31  import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
    32  import { BucketReplicationRule } from "../types";
    33  import { getBytes, k8sScalarUnitsExcluding } from "../../../../common/utils";
    34  import { setModalErrorSnackMessage } from "../../../../systemSlice";
    35  import { useAppDispatch } from "../../../../store";
    36  import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
    37  import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
    38  import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
    39  
    40  interface IReplicationModal {
    41    open: boolean;
    42    closeModalAndRefresh: () => any;
    43    bucketName: string;
    44  
    45    setReplicationRules: BucketReplicationRule[];
    46  }
    47  
    48  const AddReplicationModal = ({
    49    open,
    50    closeModalAndRefresh,
    51    bucketName,
    52    setReplicationRules,
    53  }: IReplicationModal) => {
    54    const dispatch = useAppDispatch();
    55    const [addLoading, setAddLoading] = useState<boolean>(false);
    56    const [priority, setPriority] = useState<string>("1");
    57    const [accessKey, setAccessKey] = useState<string>("");
    58    const [secretKey, setSecretKey] = useState<string>("");
    59    const [targetURL, setTargetURL] = useState<string>("");
    60    const [targetStorageClass, setTargetStorageClass] = useState<string>("");
    61    const [prefix, setPrefix] = useState<string>("");
    62    const [targetBucket, setTargetBucket] = useState<string>("");
    63    const [region, setRegion] = useState<string>("");
    64    const [useTLS, setUseTLS] = useState<boolean>(true);
    65    const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true);
    66    const [repDelete, setRepDelete] = useState<boolean>(true);
    67    const [metadataSync, setMetadataSync] = useState<boolean>(true);
    68    const [tags, setTags] = useState<string>("");
    69    const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
    70      "async",
    71    );
    72    const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
    73    const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
    74    const [healthCheck, setHealthCheck] = useState<string>("60");
    75  
    76    useEffect(() => {
    77      if (setReplicationRules.length === 0) {
    78        setPriority("1");
    79        return;
    80      }
    81  
    82      const greatestValue = setReplicationRules.reduce((prevAcc, currValue) => {
    83        if (currValue.priority > prevAcc) {
    84          return currValue.priority;
    85        }
    86        return prevAcc;
    87      }, 0);
    88  
    89      const nextPriority = greatestValue + 1;
    90      setPriority(nextPriority.toString());
    91    }, [setReplicationRules]);
    92  
    93    const addRecord = () => {
    94      const replicate = [
    95        {
    96          originBucket: bucketName,
    97          destinationBucket: targetBucket,
    98        },
    99      ];
   100  
   101      const hc = parseInt(healthCheck);
   102  
   103      const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
   104  
   105      const remoteBucketsInfo = {
   106        accessKey: accessKey,
   107        secretKey: secretKey,
   108        targetURL: endURL,
   109        region: region,
   110        bucketsRelation: replicate,
   111        syncMode: replicationMode,
   112        bandwidth:
   113          replicationMode === "async"
   114            ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
   115            : 0,
   116        healthCheckPeriod: hc,
   117        prefix: prefix,
   118        tags: tags,
   119        replicateDeleteMarkers: repDeleteMarker,
   120        replicateDeletes: repDelete,
   121        priority: parseInt(priority),
   122        storageClass: targetStorageClass,
   123        replicateMetadata: metadataSync,
   124      };
   125  
   126      api.bucketsReplication
   127        .setMultiBucketReplication(remoteBucketsInfo)
   128        .then((res) => {
   129          setAddLoading(false);
   130  
   131          const states = get(res.data, "replicationState", []);
   132  
   133          if (states.length > 0) {
   134            const itemVal = states[0];
   135  
   136            setAddLoading(false);
   137  
   138            if (itemVal.errorString && itemVal.errorString !== "") {
   139              dispatch(
   140                setModalErrorSnackMessage({
   141                  errorMessage: itemVal.errorString,
   142                  detailedError: "",
   143                }),
   144              );
   145              return;
   146            }
   147  
   148            closeModalAndRefresh();
   149  
   150            return;
   151          }
   152          dispatch(
   153            setModalErrorSnackMessage({
   154              errorMessage: "No changes applied",
   155              detailedError: "",
   156            }),
   157          );
   158        })
   159        .catch((err) => {
   160          setAddLoading(false);
   161          dispatch(setModalErrorSnackMessage(errorToHandler(err.error)));
   162        });
   163    };
   164  
   165    return (
   166      <ModalWrapper
   167        modalOpen={open}
   168        onClose={() => {
   169          closeModalAndRefresh();
   170        }}
   171        title="Set Bucket Replication"
   172        titleIcon={<BucketReplicationIcon />}
   173      >
   174        <form
   175          noValidate
   176          autoComplete="off"
   177          onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   178            e.preventDefault();
   179            setAddLoading(true);
   180            addRecord();
   181          }}
   182        >
   183          <FormLayout withBorders={false} containerPadding={false}>
   184            <InputBox
   185              id="priority"
   186              name="priority"
   187              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   188                if (e.target.validity.valid) {
   189                  setPriority(e.target.value);
   190                }
   191              }}
   192              label="Priority"
   193              value={priority}
   194              pattern={"[0-9]*"}
   195            />
   196  
   197            <InputBox
   198              id="targetURL"
   199              name="targetURL"
   200              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   201                setTargetURL(e.target.value);
   202              }}
   203              placeholder="play.min.io"
   204              label="Target URL"
   205              value={targetURL}
   206            />
   207  
   208            <Switch
   209              checked={useTLS}
   210              id="useTLS"
   211              name="useTLS"
   212              label="Use TLS"
   213              onChange={(e) => {
   214                setUseTLS(e.target.checked);
   215              }}
   216              value="yes"
   217            />
   218  
   219            <InputBox
   220              id="accessKey"
   221              name="accessKey"
   222              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   223                setAccessKey(e.target.value);
   224              }}
   225              label="Access Key"
   226              value={accessKey}
   227            />
   228  
   229            <InputBox
   230              id="secretKey"
   231              name="secretKey"
   232              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   233                setSecretKey(e.target.value);
   234              }}
   235              label="Secret Key"
   236              value={secretKey}
   237            />
   238  
   239            <InputBox
   240              id="targetBucket"
   241              name="targetBucket"
   242              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   243                setTargetBucket(e.target.value);
   244              }}
   245              label="Target Bucket"
   246              value={targetBucket}
   247            />
   248  
   249            <InputBox
   250              id="region"
   251              name="region"
   252              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   253                setRegion(e.target.value);
   254              }}
   255              label="Region"
   256              value={region}
   257            />
   258  
   259            <Select
   260              id="replication_mode"
   261              name="replication_mode"
   262              onChange={(value) => {
   263                setReplicationMode(value as "async" | "sync");
   264              }}
   265              label="Replication Mode"
   266              value={replicationMode}
   267              options={[
   268                { label: "Asynchronous", value: "async" },
   269                { label: "Synchronous", value: "sync" },
   270              ]}
   271            />
   272  
   273            {replicationMode === "async" && (
   274              <Box className={"inputItem"}>
   275                <InputBox
   276                  type="number"
   277                  id="bandwidth_scalar"
   278                  name="bandwidth_scalar"
   279                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   280                    if (e.target.validity.valid) {
   281                      setBandwidthScalar(e.target.value as string);
   282                    }
   283                  }}
   284                  label="Bandwidth"
   285                  value={bandwidthScalar}
   286                  min="0"
   287                  pattern={"[0-9]*"}
   288                  overlayObject={
   289                    <InputUnitMenu
   290                      id={"quota_unit"}
   291                      onUnitChange={(newValue) => {
   292                        setBandwidthUnit(newValue);
   293                      }}
   294                      unitSelected={bandwidthUnit}
   295                      unitsList={k8sScalarUnitsExcluding(["Ki"])}
   296                      disabled={false}
   297                    />
   298                  }
   299                />
   300              </Box>
   301            )}
   302  
   303            <InputBox
   304              id="healthCheck"
   305              name="healthCheck"
   306              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   307                setHealthCheck(e.target.value as string);
   308              }}
   309              label="Health Check Duration"
   310              value={healthCheck}
   311            />
   312  
   313            <InputBox
   314              id="storageClass"
   315              name="storageClass"
   316              onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   317                setTargetStorageClass(e.target.value);
   318              }}
   319              placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
   320              label="Storage Class"
   321              value={targetStorageClass}
   322            />
   323  
   324            <fieldset className={"inputItem"}>
   325              <legend>Object Filters</legend>
   326              <InputBox
   327                id="prefix"
   328                name="prefix"
   329                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   330                  setPrefix(e.target.value);
   331                }}
   332                placeholder="prefix"
   333                label="Prefix"
   334                value={prefix}
   335              />
   336              <QueryMultiSelector
   337                name="tags"
   338                label="Tags"
   339                elements={""}
   340                onChange={(vl: string) => {
   341                  setTags(vl);
   342                }}
   343                keyPlaceholder="Tag Key"
   344                valuePlaceholder="Tag Value"
   345                withBorder
   346              />
   347            </fieldset>
   348            <fieldset className={"inputItem"}>
   349              <legend>Replication Options</legend>
   350              <Switch
   351                checked={metadataSync}
   352                id="metadatataSync"
   353                name="metadatataSync"
   354                label="Metadata Sync"
   355                onChange={(e) => {
   356                  setMetadataSync(e.target.checked);
   357                }}
   358                description={"Metadata Sync"}
   359              />
   360              <Switch
   361                checked={repDeleteMarker}
   362                id="deleteMarker"
   363                name="deleteMarker"
   364                label="Delete Marker"
   365                onChange={(e) => {
   366                  setRepDeleteMarker(e.target.checked);
   367                }}
   368                description={"Replicate soft deletes"}
   369              />
   370              <Switch
   371                checked={repDelete}
   372                id="repDelete"
   373                name="repDelete"
   374                label="Deletes"
   375                onChange={(e) => {
   376                  setRepDelete(e.target.checked);
   377                }}
   378                description={"Replicate versioned deletes"}
   379              />
   380            </fieldset>
   381            <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
   382              <Button
   383                id={"cancel"}
   384                type="button"
   385                variant="regular"
   386                disabled={addLoading}
   387                onClick={() => {
   388                  closeModalAndRefresh();
   389                }}
   390                label={"Cancel"}
   391              />
   392              <Button
   393                id={"submit"}
   394                type="submit"
   395                variant="callAction"
   396                color="primary"
   397                disabled={addLoading}
   398                label={"Save"}
   399              />
   400            </Grid>
   401          </FormLayout>
   402        </form>
   403      </ModalWrapper>
   404    );
   405  };
   406  
   407  export default AddReplicationModal;