github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/AddLifecycleModal.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  
    19  import get from "lodash/get";
    20  import {
    21    Accordion,
    22    AlertIcon,
    23    Button,
    24    FormLayout,
    25    Grid,
    26    HelpTip,
    27    InputBox,
    28    LifecycleConfigIcon,
    29    ProgressBar,
    30    RadioGroup,
    31    Select,
    32    Switch,
    33  } from "mds";
    34  import { useSelector } from "react-redux";
    35  import { api } from "api";
    36  import { BucketVersioningResponse, Tier } from "api/consoleApi";
    37  import { errorToHandler } from "api/errors";
    38  import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
    39  import { selDistSet, setModalErrorSnackMessage } from "../../../../systemSlice";
    40  import { useAppDispatch } from "../../../../store";
    41  import { ITiersDropDown } from "../types";
    42  import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
    43  import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
    44  import InputUnitMenu from "../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
    45  import { IAM_PAGES } from "common/SecureComponent/permissions";
    46  
    47  interface IReplicationModal {
    48    open: boolean;
    49    closeModalAndRefresh: (refresh: boolean) => any;
    50    bucketName: string;
    51  }
    52  
    53  const AddLifecycleModal = ({
    54    open,
    55    closeModalAndRefresh,
    56    bucketName,
    57  }: IReplicationModal) => {
    58    const dispatch = useAppDispatch();
    59    const distributedSetup = useSelector(selDistSet);
    60    const [loadingTiers, setLoadingTiers] = useState<boolean>(true);
    61    const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]);
    62    const [addLoading, setAddLoading] = useState(false);
    63    const [versioningInfo, setVersioningInfo] =
    64      useState<BucketVersioningResponse | null>(null);
    65    const [prefix, setPrefix] = useState("");
    66    const [tags, setTags] = useState<string>("");
    67    const [storageClass, setStorageClass] = useState("");
    68  
    69    const [ilmType, setIlmType] = useState<"expiry" | "transition">("expiry");
    70    const [targetVersion, setTargetVersion] = useState<"current" | "noncurrent">(
    71      "current",
    72    );
    73    const [lifecycleDays, setLifecycleDays] = useState<string>("");
    74    const [isFormValid, setIsFormValid] = useState<boolean>(false);
    75    const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false);
    76    const [expiredAllVersionsDM, setExpiredAllVersionsDM] =
    77      useState<boolean>(false);
    78    const [loadingVersioning, setLoadingVersioning] = useState<boolean>(true);
    79    const [expandedAdv, setExpandedAdv] = useState<boolean>(false);
    80    const [expanded, setExpanded] = useState<boolean>(false);
    81    const [expiryUnit, setExpiryUnit] = useState<string>("days");
    82  
    83    /*To be removed on component replacement*/
    84    const formFieldRowFilter = {
    85      "& .MuiPaper-root": { padding: 0 },
    86    };
    87  
    88    useEffect(() => {
    89      if (loadingTiers) {
    90        api.admin
    91          .tiersList()
    92          .then((res) => {
    93            const tiersList: Tier[] | null = get(res.data, "items", []);
    94  
    95            if (tiersList !== null && tiersList.length >= 1) {
    96              const objList = tiersList.map((tier: Tier) => {
    97                const tierType = tier.type;
    98                const value = get(tier, `${tierType}.name`, "");
    99  
   100                return { label: value, value: value };
   101              });
   102  
   103              setTiersList(objList);
   104              if (objList.length > 0) {
   105                setStorageClass(objList[0].value);
   106              }
   107            }
   108            setLoadingTiers(false);
   109          })
   110          .catch(() => {
   111            setLoadingTiers(false);
   112          });
   113      }
   114    }, [loadingTiers]);
   115  
   116    useEffect(() => {
   117      let valid = true;
   118  
   119      if (ilmType !== "expiry") {
   120        if (storageClass === "") {
   121          valid = false;
   122        }
   123      }
   124      if (!lifecycleDays || parseInt(lifecycleDays) === 0) {
   125        valid = false;
   126      }
   127      if (parseInt(lifecycleDays) > 2147483647) {
   128        //values over int32 cannot be parsed
   129        valid = false;
   130      }
   131      setIsFormValid(valid);
   132    }, [ilmType, lifecycleDays, storageClass]);
   133  
   134    useEffect(() => {
   135      if (loadingVersioning && distributedSetup) {
   136        api.buckets
   137          .getBucketVersioning(bucketName)
   138          .then((res) => {
   139            setVersioningInfo(res.data);
   140            setLoadingVersioning(false);
   141          })
   142          .catch((err) => {
   143            dispatch(setModalErrorSnackMessage(errorToHandler(err)));
   144            setLoadingVersioning(false);
   145          });
   146      }
   147    }, [loadingVersioning, dispatch, bucketName, distributedSetup]);
   148  
   149    const addRecord = () => {
   150      let rules = {};
   151  
   152      if (ilmType === "expiry") {
   153        let expiry: { [key: string]: number } = {};
   154  
   155        if (targetVersion === "current") {
   156          expiry["expiry_days"] = parseInt(lifecycleDays);
   157        } else if (expiryUnit === "days") {
   158          expiry["noncurrentversion_expiration_days"] = parseInt(lifecycleDays);
   159        } else {
   160          expiry["newer_noncurrentversion_expiration_versions"] =
   161            parseInt(lifecycleDays);
   162        }
   163  
   164        rules = {
   165          ...expiry,
   166        };
   167      } else {
   168        let transition: { [key: string]: number | string } = {};
   169        if (targetVersion === "current") {
   170          transition["transition_days"] = parseInt(lifecycleDays);
   171          transition["storage_class"] = storageClass;
   172        } else if (expiryUnit === "days") {
   173          transition["noncurrentversion_transition_days"] =
   174            parseInt(lifecycleDays);
   175          transition["noncurrentversion_transition_storage_class"] = storageClass;
   176        }
   177  
   178        rules = {
   179          ...transition,
   180        };
   181      }
   182  
   183      const lifecycleInsert = {
   184        type: ilmType,
   185        prefix,
   186        tags,
   187        expired_object_delete_marker: expiredObjectDM,
   188        expired_object_delete_all: expiredAllVersionsDM,
   189        ...rules,
   190      };
   191  
   192      api.buckets
   193        .addBucketLifecycle(bucketName, lifecycleInsert)
   194        .then(() => {
   195          setAddLoading(false);
   196          closeModalAndRefresh(true);
   197        })
   198        .catch((err) => {
   199          setAddLoading(false);
   200          dispatch(setModalErrorSnackMessage(errorToHandler(err)));
   201        });
   202    };
   203    return (
   204      <ModalWrapper
   205        modalOpen={open}
   206        onClose={() => {
   207          closeModalAndRefresh(false);
   208        }}
   209        title="Add Lifecycle Rule"
   210        titleIcon={<LifecycleConfigIcon />}
   211      >
   212        {loadingTiers && (
   213          <Grid container>
   214            <Grid item xs={12}>
   215              <ProgressBar />
   216            </Grid>
   217          </Grid>
   218        )}
   219  
   220        {!loadingTiers && (
   221          <form
   222            noValidate
   223            autoComplete="off"
   224            onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   225              e.preventDefault();
   226              setAddLoading(true);
   227              addRecord();
   228            }}
   229          >
   230            <FormLayout withBorders={false} containerPadding={false}>
   231              <RadioGroup
   232                currentValue={ilmType}
   233                id="ilm_type"
   234                name="ilm_type"
   235                label="Type of Lifecycle"
   236                onChange={(e) => {
   237                  setIlmType(e.target.value as "expiry" | "transition");
   238                }}
   239                selectorOptions={[
   240                  { value: "expiry", label: "Expiry" },
   241                  { value: "transition", label: "Transition" },
   242                ]}
   243                helpTip={
   244                  <Fragment>
   245                    Select{" "}
   246                    <a
   247                      target="blank"
   248                      href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/create-lifecycle-management-expiration-rule.html"
   249                    >
   250                      Expiry
   251                    </a>{" "}
   252                    to delete Objects per this rule. Select{" "}
   253                    <a
   254                      target="blank"
   255                      href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/transition-objects-to-minio.html"
   256                    >
   257                      Transition
   258                    </a>{" "}
   259                    to move Objects to a remote storage{" "}
   260                    <a
   261                      target="blank"
   262                      href="https://min.io/docs/minio/windows/administration/object-management/transition-objects-to-minio.html#configure-the-remote-storage-tier"
   263                    >
   264                      Tier
   265                    </a>{" "}
   266                    per this rule.
   267                  </Fragment>
   268                }
   269                helpTipPlacement="right"
   270              />
   271              {versioningInfo?.status === "Enabled" && (
   272                <Select
   273                  value={targetVersion}
   274                  id="object_version"
   275                  name="object_version"
   276                  label="Object Version"
   277                  onChange={(value) => {
   278                    setTargetVersion(value as "current" | "noncurrent");
   279                  }}
   280                  options={[
   281                    { value: "current", label: "Current Version" },
   282                    { value: "noncurrent", label: "Non-Current Version" },
   283                  ]}
   284                  helpTip={
   285                    <Fragment>
   286                      Select whether to apply the rule to current or non-current
   287                      Object
   288                      <a
   289                        target="blank"
   290                        href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/create-lifecycle-management-expiration-rule.html#expire-versioned-objects"
   291                      >
   292                        {" "}
   293                        Versions
   294                      </a>
   295                    </Fragment>
   296                  }
   297                  helpTipPlacement="right"
   298                />
   299              )}
   300  
   301              <InputBox
   302                error={
   303                  lifecycleDays && !isFormValid
   304                    ? parseInt(lifecycleDays) <= 0
   305                      ? `Number of ${expiryUnit} to retain must be greater than zero`
   306                      : parseInt(lifecycleDays) > 2147483647
   307                        ? `Number of ${expiryUnit} must be less than or equal to 2147483647`
   308                        : ""
   309                    : ""
   310                }
   311                id="expiry_days"
   312                name="expiry_days"
   313                onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   314                  if (e.target.validity.valid) {
   315                    setLifecycleDays(e.target.value);
   316                  }
   317                }}
   318                pattern={"[0-9]*"}
   319                label="After"
   320                value={lifecycleDays}
   321                overlayObject={
   322                  <Fragment>
   323                    <Grid container sx={{ justifyContent: "center" }}>
   324                      <InputUnitMenu
   325                        id={"expire-current-unit"}
   326                        unitSelected={expiryUnit}
   327                        unitsList={[
   328                          { label: "Days", value: "days" },
   329                          { label: "Versions", value: "versions" },
   330                        ]}
   331                        disabled={
   332                          targetVersion !== "noncurrent" || ilmType !== "expiry"
   333                        }
   334                        onUnitChange={(newValue) => {
   335                          setExpiryUnit(newValue);
   336                        }}
   337                      />
   338                      {ilmType === "expiry" && targetVersion === "noncurrent" && (
   339                        <HelpTip
   340                          content={
   341                            <Fragment>
   342                              Select to set expiry by days or newer noncurrent
   343                              versions
   344                            </Fragment>
   345                          }
   346                          placement="right"
   347                        >
   348                          {" "}
   349                          <AlertIcon style={{ width: 15, height: 15 }} />
   350                        </HelpTip>
   351                      )}
   352                    </Grid>
   353                  </Fragment>
   354                }
   355              />
   356  
   357              {ilmType === "expiry" ? (
   358                <Fragment />
   359              ) : (
   360                <Select
   361                  label="To Tier"
   362                  id="storage_class"
   363                  name="storage_class"
   364                  value={storageClass}
   365                  onChange={(value) => {
   366                    setStorageClass(value as string);
   367                  }}
   368                  options={tiersList}
   369                  helpTip={
   370                    <Fragment>
   371                      Configure a{" "}
   372                      <a
   373                        href={IAM_PAGES.TIERS_ADD}
   374                        color="secondary"
   375                        style={{ textDecoration: "underline" }}
   376                      >
   377                        remote tier
   378                      </a>{" "}
   379                      to receive transitioned Objects
   380                    </Fragment>
   381                  }
   382                  helpTipPlacement="right"
   383                />
   384              )}
   385              <Grid item xs={12} sx={formFieldRowFilter}>
   386                <Accordion
   387                  title={"Filters"}
   388                  id={"lifecycle-filters"}
   389                  expanded={expanded}
   390                  onTitleClick={() => setExpanded(!expanded)}
   391                >
   392                  <Grid item xs={12}>
   393                    <InputBox
   394                      id="prefix"
   395                      name="prefix"
   396                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   397                        setPrefix(e.target.value);
   398                      }}
   399                      label="Prefix"
   400                      value={prefix}
   401                    />
   402                  </Grid>
   403                  <Grid item xs={12}>
   404                    <QueryMultiSelector
   405                      name="tags"
   406                      label="Tags"
   407                      elements={""}
   408                      onChange={(vl: string) => {
   409                        setTags(vl);
   410                      }}
   411                      keyPlaceholder="Tag Key"
   412                      valuePlaceholder="Tag Value"
   413                      withBorder
   414                    />
   415                  </Grid>
   416                </Accordion>
   417              </Grid>
   418              {ilmType === "expiry" && targetVersion === "noncurrent" && (
   419                <Grid item xs={12} sx={formFieldRowFilter}>
   420                  <Accordion
   421                    title={"Advanced"}
   422                    id={"lifecycle-advanced-filters"}
   423                    expanded={expandedAdv}
   424                    onTitleClick={() => setExpandedAdv(!expandedAdv)}
   425                    sx={{ marginTop: 15 }}
   426                  >
   427                    <Grid item xs={12}>
   428                      <Switch
   429                        value="expired_delete_marker"
   430                        id="expired_delete_marker"
   431                        name="expired_delete_marker"
   432                        checked={expiredObjectDM}
   433                        onChange={(
   434                          event: React.ChangeEvent<HTMLInputElement>,
   435                        ) => {
   436                          setExpiredObjectDM(event.target.checked);
   437                        }}
   438                        label={"Expire Delete Marker"}
   439                        description={
   440                          "Remove the reference to the object if no versions are left"
   441                        }
   442                      />
   443                      <Switch
   444                        value="expired_delete_all"
   445                        id="expired_delete_all"
   446                        name="expired_delete_all"
   447                        checked={expiredAllVersionsDM}
   448                        onChange={(
   449                          event: React.ChangeEvent<HTMLInputElement>,
   450                        ) => {
   451                          setExpiredAllVersionsDM(event.target.checked);
   452                        }}
   453                        label={"Expire All Versions"}
   454                        description={
   455                          "Removes all the versions of the object already expired"
   456                        }
   457                      />
   458                    </Grid>
   459                  </Accordion>
   460                </Grid>
   461              )}
   462  
   463              <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
   464                <Button
   465                  id={"reset"}
   466                  type="button"
   467                  variant="regular"
   468                  disabled={addLoading}
   469                  onClick={() => {
   470                    closeModalAndRefresh(false);
   471                  }}
   472                  label={"Cancel"}
   473                />
   474                <Button
   475                  id={"save-lifecycle"}
   476                  type="submit"
   477                  variant="callAction"
   478                  color="primary"
   479                  disabled={addLoading || !isFormValid}
   480                  label={"Save"}
   481                />
   482              </Grid>
   483              {addLoading && (
   484                <Grid item xs={12}>
   485                  <ProgressBar />
   486                </Grid>
   487              )}
   488            </FormLayout>
   489          </form>
   490        )}
   491      </ModalWrapper>
   492    );
   493  };
   494  
   495  export default AddLifecycleModal;