github.com/minio/console@v1.4.1/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>(true);
    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: "There was an error",
   128                  detailedError: itemVal.errorString,
   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; this setting is enabled by default. Please note
   205                      that objects created before replication was configured or
   206                      while replication is disabled are not synchronized to the
   207                      target deployment in case this setting is not enabled.
   208                    </Box>
   209                    <Box sx={{ paddingTop: "10px" }}>
   210                      MinIO supports replicating delete operations, where MinIO
   211                      synchronizes deleting specific object versions and new
   212                      delete markers. Delete operation replication uses the same
   213                      replication process as all other replication operations.
   214                    </Box>{" "}
   215                  </Fragment>
   216                }
   217              />
   218            }
   219          >
   220            <form
   221              noValidate
   222              autoComplete="off"
   223              onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   224                e.preventDefault();
   225                setAddLoading(true);
   226                addRecord();
   227              }}
   228            >
   229              <InputBox
   230                id="priority"
   231                name="priority"
   232                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   233                  if (e.target.validity.valid) {
   234                    setPriority(e.target.value);
   235                  }
   236                }}
   237                label="Priority"
   238                value={priority}
   239                pattern={"[0-9]*"}
   240              />
   241  
   242              <InputBox
   243                id="targetURL"
   244                name="targetURL"
   245                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   246                  setTargetURL(e.target.value);
   247                }}
   248                placeholder="play.min.io"
   249                label="Target URL"
   250                value={targetURL}
   251              />
   252  
   253              <Switch
   254                checked={useTLS}
   255                id="useTLS"
   256                name="useTLS"
   257                label="Use TLS"
   258                onChange={(e) => {
   259                  setUseTLS(e.target.checked);
   260                }}
   261                value="yes"
   262              />
   263  
   264              <InputBox
   265                id="accessKey"
   266                name="accessKey"
   267                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   268                  setAccessKey(e.target.value);
   269                }}
   270                label="Access Key"
   271                value={accessKey}
   272              />
   273  
   274              <InputBox
   275                id="secretKey"
   276                name="secretKey"
   277                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   278                  setSecretKey(e.target.value);
   279                }}
   280                label="Secret Key"
   281                value={secretKey}
   282              />
   283  
   284              <InputBox
   285                id="targetBucket"
   286                name="targetBucket"
   287                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   288                  setTargetBucket(e.target.value);
   289                }}
   290                label="Target Bucket"
   291                value={targetBucket}
   292              />
   293  
   294              <InputBox
   295                id="region"
   296                name="region"
   297                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   298                  setRegion(e.target.value);
   299                }}
   300                label="Region"
   301                value={region}
   302              />
   303  
   304              <Select
   305                id="replication_mode"
   306                name="replication_mode"
   307                onChange={(value) => {
   308                  setReplicationMode(value as "async" | "sync");
   309                }}
   310                label="Replication Mode"
   311                value={replicationMode}
   312                options={[
   313                  { label: "Asynchronous", value: "async" },
   314                  { label: "Synchronous", value: "sync" },
   315                ]}
   316              />
   317  
   318              {replicationMode === "async" && (
   319                <Box className={"inputItem"}>
   320                  <InputBox
   321                    type="number"
   322                    id="bandwidth_scalar"
   323                    name="bandwidth_scalar"
   324                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   325                      if (e.target.validity.valid) {
   326                        setBandwidthScalar(e.target.value as string);
   327                      }
   328                    }}
   329                    label="Bandwidth"
   330                    value={bandwidthScalar}
   331                    min="0"
   332                    pattern={"[0-9]*"}
   333                    overlayObject={
   334                      <InputUnitMenu
   335                        id={"quota_unit"}
   336                        onUnitChange={(newValue) => {
   337                          setBandwidthUnit(newValue);
   338                        }}
   339                        unitSelected={bandwidthUnit}
   340                        unitsList={k8sScalarUnitsExcluding(["Ki"])}
   341                        disabled={false}
   342                      />
   343                    }
   344                  />
   345                </Box>
   346              )}
   347  
   348              <InputBox
   349                id="healthCheck"
   350                name="healthCheck"
   351                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   352                  setHealthCheck(e.target.value as string);
   353                }}
   354                label="Health Check Duration"
   355                value={healthCheck}
   356              />
   357  
   358              <InputBox
   359                id="storageClass"
   360                name="storageClass"
   361                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   362                  setTargetStorageClass(e.target.value);
   363                }}
   364                placeholder="STANDARD_IA,REDUCED_REDUNDANCY etc"
   365                label="Storage Class"
   366                value={targetStorageClass}
   367              />
   368  
   369              <fieldset className={"inputItem"}>
   370                <legend>Object Filters</legend>
   371                <InputBox
   372                  id="prefix"
   373                  name="prefix"
   374                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   375                    setPrefix(e.target.value);
   376                  }}
   377                  placeholder="prefix"
   378                  label="Prefix"
   379                  value={prefix}
   380                />
   381                <QueryMultiSelector
   382                  name="tags"
   383                  label="Tags"
   384                  elements={""}
   385                  onChange={(vl: string) => {
   386                    setTags(vl);
   387                  }}
   388                  keyPlaceholder="Tag Key"
   389                  valuePlaceholder="Tag Value"
   390                  withBorder
   391                />
   392              </fieldset>
   393              <fieldset className={"inputItem"}>
   394                <legend>Replication Options</legend>
   395                <Switch
   396                  checked={repExisting}
   397                  id="repExisting"
   398                  name="repExisting"
   399                  label="Existing Objects"
   400                  onChange={(e) => {
   401                    setRepExisting(e.target.checked);
   402                  }}
   403                  description={"Replicate existing objects"}
   404                />
   405                <Switch
   406                  checked={metadataSync}
   407                  id="metadatataSync"
   408                  name="metadatataSync"
   409                  label="Metadata Sync"
   410                  onChange={(e) => {
   411                    setMetadataSync(e.target.checked);
   412                  }}
   413                  description={"Metadata Sync"}
   414                />
   415                <Switch
   416                  checked={repDeleteMarker}
   417                  id="deleteMarker"
   418                  name="deleteMarker"
   419                  label="Delete Marker"
   420                  onChange={(e) => {
   421                    setRepDeleteMarker(e.target.checked);
   422                  }}
   423                  description={"Replicate soft deletes"}
   424                />
   425                <Switch
   426                  checked={repDelete}
   427                  id="repDelete"
   428                  name="repDelete"
   429                  label="Deletes"
   430                  onChange={(e) => {
   431                    setRepDelete(e.target.checked);
   432                  }}
   433                  description={"Replicate versioned deletes"}
   434                />
   435              </fieldset>
   436              <Grid
   437                item
   438                xs={12}
   439                sx={{
   440                  display: "flex",
   441                  flexDirection: "row",
   442                  justifyContent: "end",
   443                  gap: 10,
   444                  paddingTop: 10,
   445                }}
   446              >
   447                <Button
   448                  id={"cancel"}
   449                  type="button"
   450                  variant="regular"
   451                  disabled={addLoading}
   452                  onClick={() => {
   453                    navigate(backLink);
   454                  }}
   455                  label={"Cancel"}
   456                />
   457                <Button
   458                  id={"submit"}
   459                  type="submit"
   460                  variant="callAction"
   461                  color="primary"
   462                  disabled={addLoading || !validated}
   463                  label={"Save"}
   464                />
   465              </Grid>
   466            </form>
   467          </FormLayout>
   468        </PageLayout>
   469      </Fragment>
   470    );
   471  };
   472  export default AddBucketReplication;