github.com/minio/console@v1.3.0/web-app/src/screens/Console/Buckets/BucketDetails/AddBucketReplication.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2023 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    Box,
    22    BucketReplicationIcon,
    23    Button,
    24    FormLayout,
    25    Grid,
    26    HelpBox,
    27    InputBox,
    28    PageLayout,
    29    Select,
    30    Switch,
    31  } from "mds";
    32  import { IAM_PAGES } from "../../../../common/SecureComponent/permissions";
    33  import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
    34  import { useAppDispatch } from "../../../../store";
    35  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    36  import HelpMenu from "../../HelpMenu";
    37  import { api } from "api";
    38  import { errorToHandler } from "api/errors";
    39  import QueryMultiSelector from "screens/Console/Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
    40  import { getBytes, k8sScalarUnitsExcluding } from "common/utils";
    41  import get from "lodash/get";
    42  import InputUnitMenu from "screens/Console/Common/FormComponents/InputUnitMenu/InputUnitMenu";
    43  
    44  const AddBucketReplication = () => {
    45    const dispatch = useAppDispatch();
    46    const navigate = useNavigate();
    47    let params = new URLSearchParams(document.location.search);
    48    const bucketName = params.get("bucketName") || "";
    49    const nextPriority = params.get("nextPriority") || "1";
    50    const [addLoading, setAddLoading] = useState<boolean>(false);
    51    const [priority, setPriority] = useState<string>(nextPriority);
    52    const [accessKey, setAccessKey] = useState<string>("");
    53    const [secretKey, setSecretKey] = useState<string>("");
    54    const [targetURL, setTargetURL] = useState<string>("");
    55    const [targetStorageClass, setTargetStorageClass] = useState<string>("");
    56    const [prefix, setPrefix] = useState<string>("");
    57    const [targetBucket, setTargetBucket] = useState<string>("");
    58    const [region, setRegion] = useState<string>("");
    59    const [useTLS, setUseTLS] = useState<boolean>(true);
    60    const [repDeleteMarker, setRepDeleteMarker] = useState<boolean>(true);
    61    const [repDelete, setRepDelete] = useState<boolean>(true);
    62    const [metadataSync, setMetadataSync] = useState<boolean>(true);
    63    const [repExisting, setRepExisting] = useState<boolean>(false);
    64    const [tags, setTags] = useState<string>("");
    65    const [replicationMode, setReplicationMode] = useState<"async" | "sync">(
    66      "async",
    67    );
    68    const [bandwidthScalar, setBandwidthScalar] = useState<string>("100");
    69    const [bandwidthUnit, setBandwidthUnit] = useState<string>("Gi");
    70    const [healthCheck, setHealthCheck] = useState<string>("60");
    71    const [validated, setValidated] = useState<boolean>(false);
    72    const backLink = IAM_PAGES.BUCKETS + `/${bucketName}/admin/replication`;
    73    useEffect(() => {
    74      dispatch(setHelpName("bucket-replication-add"));
    75      // eslint-disable-next-line react-hooks/exhaustive-deps
    76    }, []);
    77  
    78    const addRecord = () => {
    79      const replicate = [
    80        {
    81          originBucket: bucketName,
    82          destinationBucket: targetBucket,
    83        },
    84      ];
    85  
    86      const hc = parseInt(healthCheck);
    87  
    88      const endURL = `${useTLS ? "https://" : "http://"}${targetURL}`;
    89  
    90      const remoteBucketsInfo = {
    91        accessKey: accessKey,
    92        secretKey: secretKey,
    93        targetURL: endURL,
    94        region: region,
    95        bucketsRelation: replicate,
    96        syncMode: replicationMode,
    97        bandwidth:
    98          replicationMode === "async"
    99            ? parseInt(getBytes(bandwidthScalar, bandwidthUnit, true))
   100            : 0,
   101        healthCheckPeriod: hc,
   102        prefix: prefix,
   103        tags: tags,
   104        replicateDeleteMarkers: repDeleteMarker,
   105        replicateDeletes: repDelete,
   106        replicateExistingObjects: repExisting,
   107        priority: parseInt(priority),
   108        storageClass: targetStorageClass,
   109        replicateMetadata: metadataSync,
   110      };
   111  
   112      api.bucketsReplication
   113        .setMultiBucketReplication(remoteBucketsInfo)
   114        .then((res) => {
   115          setAddLoading(false);
   116  
   117          const states = get(res.data, "replicationState", []);
   118  
   119          if (states.length > 0) {
   120            const itemVal = states[0];
   121  
   122            setAddLoading(false);
   123  
   124            if (itemVal.errorString && itemVal.errorString !== "") {
   125              dispatch(
   126                setErrorSnackMessage({
   127                  errorMessage: itemVal.errorString,
   128                  detailedError: "There was an error",
   129                }),
   130              );
   131              // navigate(backLink);
   132              return;
   133            }
   134            navigate(backLink);
   135            return;
   136          }
   137          dispatch(
   138            setErrorSnackMessage({
   139              errorMessage: "No changes applied",
   140              detailedError: "",
   141            }),
   142          );
   143        })
   144        .catch((err) => {
   145          console.log("this is an error!");
   146          setAddLoading(false);
   147          dispatch(setErrorSnackMessage(errorToHandler(err.error)));
   148        });
   149    };
   150  
   151    useEffect(() => {
   152      !validated &&
   153        accessKey.length >= 3 &&
   154        secretKey.length >= 8 &&
   155        targetBucket.length >= 3 &&
   156        targetURL.length > 0 &&
   157        setValidated(true);
   158    }, [targetURL, accessKey, secretKey, targetBucket, validated]);
   159  
   160    useEffect(() => {
   161      if (
   162        validated &&
   163        (accessKey.length < 3 ||
   164          secretKey.length < 8 ||
   165          targetBucket.length < 3 ||
   166          targetURL.length < 1)
   167      ) {
   168        setValidated(false);
   169      }
   170    }, [targetURL, accessKey, secretKey, targetBucket, validated]);
   171  
   172    return (
   173      <Fragment>
   174        <PageHeaderWrapper
   175          label={
   176            <BackLink
   177              label={"Add Bucket Replication Rule - " + bucketName}
   178              onClick={() => navigate(backLink)}
   179            />
   180          }
   181          actions={<HelpMenu />}
   182        />
   183        <PageLayout>
   184          <FormLayout
   185            title="Add Replication Rule"
   186            icon={<BucketReplicationIcon />}
   187            helpBox={
   188              <HelpBox
   189                iconComponent={<BucketReplicationIcon />}
   190                title="Bucket Replication Configuration"
   191                help={
   192                  <Fragment>
   193                    <Box sx={{ paddconngTop: "10px" }}>
   194                      The bucket selected in this deployment acts as the “source”
   195                      while the configured remote deployment acts as the “target”.
   196                    </Box>
   197                    <Box sx={{ paddingTop: "10px" }}>
   198                      For each write operation to this "source" bucket, MinIO
   199                      checks all configured replication rules and applies the
   200                      matching rule with highest configured priority.
   201                    </Box>
   202                    <Box sx={{ paddingTop: "10px" }}>
   203                      MinIO supports automatically replicating existing objects in
   204                      a bucket, however it does not enable existing object
   205                      replication by default. Objects created before replication
   206                      was configured or while replication is disabled are not
   207                      synchronized to the target deployment unless replication of
   208                      existing objects is enabled.
   209                    </Box>
   210                    <Box sx={{ paddingTop: "10px" }}>
   211                      MinIO supports replicating delete operations, where MinIO
   212                      synchronizes deleting specific object versions and new
   213                      delete markers. Delete operation replication uses the same
   214                      replication process as all other replication operations.
   215                    </Box>{" "}
   216                  </Fragment>
   217                }
   218              />
   219            }
   220          >
   221            <form
   222              noValidate
   223              autoComplete="off"
   224              onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   225                e.preventDefault();
   226                setAddLoading(true);
   227                addRecord();
   228              }}
   229            >
   230              <InputBox
   231                id="priority"
   232                name="priority"
   233                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   234                  if (e.target.validity.valid) {
   235                    setPriority(e.target.value);
   236                  }
   237                }}
   238                label="Priority"
   239                value={priority}
   240                pattern={"[0-9]*"}
   241              />
   242  
   243              <InputBox
   244                id="targetURL"
   245                name="targetURL"
   246                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   247                  setTargetURL(e.target.value);
   248                }}
   249                placeholder="play.min.io"
   250                label="Target URL"
   251                value={targetURL}
   252              />
   253  
   254              <Switch
   255                checked={useTLS}
   256                id="useTLS"
   257                name="useTLS"
   258                label="Use TLS"
   259                onChange={(e) => {
   260                  setUseTLS(e.target.checked);
   261                }}
   262                value="yes"
   263              />
   264  
   265              <InputBox
   266                id="accessKey"
   267                name="accessKey"
   268                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   269                  setAccessKey(e.target.value);
   270                }}
   271                label="Access Key"
   272                value={accessKey}
   273              />
   274  
   275              <InputBox
   276                id="secretKey"
   277                name="secretKey"
   278                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   279                  setSecretKey(e.target.value);
   280                }}
   281                label="Secret Key"
   282                value={secretKey}
   283              />
   284  
   285              <InputBox
   286                id="targetBucket"
   287                name="targetBucket"
   288                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   289                  setTargetBucket(e.target.value);
   290                }}
   291                label="Target Bucket"
   292                value={targetBucket}
   293              />
   294  
   295              <InputBox
   296                id="region"
   297                name="region"
   298                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   299                  setRegion(e.target.value);
   300                }}
   301                label="Region"
   302                value={region}
   303              />
   304  
   305              <Select
   306                id="replication_mode"
   307                name="replication_mode"
   308                onChange={(value) => {
   309                  setReplicationMode(value as "async" | "sync");
   310                }}
   311                label="Replication Mode"
   312                value={replicationMode}
   313                options={[
   314                  { label: "Asynchronous", value: "async" },
   315                  { label: "Synchronous", value: "sync" },
   316                ]}
   317              />
   318  
   319              {replicationMode === "async" && (
   320                <Box className={"inputItem"}>
   321                  <InputBox
   322                    type="number"
   323                    id="bandwidth_scalar"
   324                    name="bandwidth_scalar"
   325                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   326                      if (e.target.validity.valid) {
   327                        setBandwidthScalar(e.target.value as string);
   328                      }
   329                    }}
   330                    label="Bandwidth"
   331                    value={bandwidthScalar}
   332                    min="0"
   333                    pattern={"[0-9]*"}
   334                    overlayObject={
   335                      <InputUnitMenu
   336                        id={"quota_unit"}
   337                        onUnitChange={(newValue) => {
   338                          setBandwidthUnit(newValue);
   339                        }}
   340                        unitSelected={bandwidthUnit}
   341                        unitsList={k8sScalarUnitsExcluding(["Ki"])}
   342                        disabled={false}
   343                      />
   344                    }
   345                  />
   346                </Box>
   347              )}
   348  
   349              <InputBox
   350                id="healthCheck"
   351                name="healthCheck"
   352                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   353                  setHealthCheck(e.target.value as string);
   354                }}
   355                label="Health Check Duration"
   356                value={healthCheck}
   357              />
   358  
   359              <InputBox
   360                id="storageClass"
   361                name="storageClass"
   362                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   363                  setTargetStorageClass(e.target.value);
   364                }}
   365                placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
   366                label="Storage Class"
   367                value={targetStorageClass}
   368              />
   369  
   370              <fieldset className={"inputItem"}>
   371                <legend>Object Filters</legend>
   372                <InputBox
   373                  id="prefix"
   374                  name="prefix"
   375                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   376                    setPrefix(e.target.value);
   377                  }}
   378                  placeholder="prefix"
   379                  label="Prefix"
   380                  value={prefix}
   381                />
   382                <QueryMultiSelector
   383                  name="tags"
   384                  label="Tags"
   385                  elements={""}
   386                  onChange={(vl: string) => {
   387                    setTags(vl);
   388                  }}
   389                  keyPlaceholder="Tag Key"
   390                  valuePlaceholder="Tag Value"
   391                  withBorder
   392                />
   393              </fieldset>
   394              <fieldset className={"inputItem"}>
   395                <legend>Replication Options</legend>
   396                <Switch
   397                  checked={repExisting}
   398                  id="repExisting"
   399                  name="repExisting"
   400                  label="Existing Objects"
   401                  onChange={(e) => {
   402                    setRepExisting(e.target.checked);
   403                  }}
   404                  description={"Replicate existing objects"}
   405                />
   406                <Switch
   407                  checked={metadataSync}
   408                  id="metadatataSync"
   409                  name="metadatataSync"
   410                  label="Metadata Sync"
   411                  onChange={(e) => {
   412                    setMetadataSync(e.target.checked);
   413                  }}
   414                  description={"Metadata Sync"}
   415                />
   416                <Switch
   417                  checked={repDeleteMarker}
   418                  id="deleteMarker"
   419                  name="deleteMarker"
   420                  label="Delete Marker"
   421                  onChange={(e) => {
   422                    setRepDeleteMarker(e.target.checked);
   423                  }}
   424                  description={"Replicate soft deletes"}
   425                />
   426                <Switch
   427                  checked={repDelete}
   428                  id="repDelete"
   429                  name="repDelete"
   430                  label="Deletes"
   431                  onChange={(e) => {
   432                    setRepDelete(e.target.checked);
   433                  }}
   434                  description={"Replicate versioned deletes"}
   435                />
   436              </fieldset>
   437              <Grid
   438                item
   439                xs={12}
   440                sx={{
   441                  display: "flex",
   442                  flexDirection: "row",
   443                  justifyContent: "end",
   444                  gap: 10,
   445                  paddingTop: 10,
   446                }}
   447              >
   448                <Button
   449                  id={"cancel"}
   450                  type="button"
   451                  variant="regular"
   452                  disabled={addLoading}
   453                  onClick={() => {
   454                    navigate(backLink);
   455                  }}
   456                  label={"Cancel"}
   457                />
   458                <Button
   459                  id={"submit"}
   460                  type="submit"
   461                  variant="callAction"
   462                  color="primary"
   463                  disabled={addLoading || !validated}
   464                  label={"Save"}
   465                />
   466              </Grid>
   467            </form>
   468          </FormLayout>
   469        </PageLayout>
   470      </Fragment>
   471    );
   472  };
   473  export default AddBucketReplication;