github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/EditLifecycleConfiguration.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 get from "lodash/get";
    19  import {
    20    Accordion,
    21    Button,
    22    FormLayout,
    23    Grid,
    24    InputBox,
    25    LifecycleConfigIcon,
    26    Loader,
    27    ProgressBar,
    28    RadioGroup,
    29    Select,
    30    Switch,
    31  } from "mds";
    32  import { api } from "api";
    33  import { ApiError, Tier } from "api/consoleApi";
    34  import { modalStyleUtils } from "../../Common/FormComponents/common/styleLibrary";
    35  import { ITiersDropDown, LifeCycleItem } from "../types";
    36  import { setErrorSnackMessage } from "../../../../systemSlice";
    37  import { useAppDispatch } from "../../../../store";
    38  import ModalWrapper from "../../Common/ModalWrapper/ModalWrapper";
    39  import QueryMultiSelector from "../../Common/FormComponents/QueryMultiSelector/QueryMultiSelector";
    40  import { errorToHandler } from "../../../../api/errors";
    41  
    42  interface IAddUserContentProps {
    43    closeModalAndRefresh: (reload: boolean) => void;
    44    selectedBucket: string;
    45    lifecycleRule: LifeCycleItem;
    46    open: boolean;
    47  }
    48  
    49  const EditLifecycleConfiguration = ({
    50    closeModalAndRefresh,
    51    selectedBucket,
    52    lifecycleRule,
    53    open,
    54  }: IAddUserContentProps) => {
    55    const dispatch = useAppDispatch();
    56    const [loadingTiers, setLoadingTiers] = useState<boolean>(true);
    57    const [addLoading, setAddLoading] = useState<boolean>(false);
    58    const [tags, setTags] = useState<string>("");
    59    const [enabled, setEnabled] = useState<boolean>(false);
    60    const [tiersList, setTiersList] = useState<ITiersDropDown[]>([]);
    61    const [prefix, setPrefix] = useState("");
    62    const [storageClass, setStorageClass] = useState("");
    63    const [NCTransitionSC, setNCTransitionSC] = useState("");
    64    const [expiredObjectDM, setExpiredObjectDM] = useState<boolean>(false);
    65    const [expiredAllVersionsDM, setExpiredAllVersionsDM] =
    66      useState<boolean>(false);
    67    const [NCExpirationDays, setNCExpirationDays] = useState<string>("0");
    68    const [NCTransitionDays, setNCTransitionDays] = useState<string>("0");
    69    const [ilmType, setIlmType] = useState<"transition" | "expiry">("expiry");
    70    const [expiryDays, setExpiryDays] = useState<string>("0");
    71    const [transitionDays, setTransitionDays] = useState<string>("0");
    72    const [isFormValid, setIsFormValid] = useState<boolean>(false);
    73    const [expandedAdv, setExpandedAdv] = useState<boolean>(false);
    74    const [expanded, setExpanded] = useState<boolean>(false);
    75  
    76    const ILM_TYPES = [
    77      { value: "expiry", label: "Expiry" },
    78      { value: "transition", label: "Transition" },
    79    ];
    80  
    81    useEffect(() => {
    82      if (loadingTiers) {
    83        api.admin
    84          .tiersList()
    85          .then((res) => {
    86            const tiersList: Tier[] | null = get(res.data, "items", []);
    87  
    88            if (tiersList !== null && tiersList.length >= 1) {
    89              const objList = tiersList.map((tier: Tier) => {
    90                const tierType = tier.type;
    91                const value = get(tier, `${tierType}.name`, "");
    92  
    93                return { label: value, value: value };
    94              });
    95  
    96              setTiersList(objList);
    97              if (objList.length > 0) {
    98                setStorageClass(objList[0].value);
    99              }
   100            }
   101            setLoadingTiers(false);
   102          })
   103          .catch(() => {
   104            setLoadingTiers(false);
   105          });
   106      }
   107    }, [loadingTiers]);
   108  
   109    useEffect(() => {
   110      let valid = true;
   111  
   112      if (ilmType !== "expiry") {
   113        if (storageClass === "") {
   114          valid = false;
   115        }
   116      }
   117      setIsFormValid(valid);
   118    }, [ilmType, expiryDays, transitionDays, storageClass]);
   119  
   120    useEffect(() => {
   121      if (lifecycleRule.status === "Enabled") {
   122        setEnabled(true);
   123      }
   124  
   125      let transitionMode = false;
   126  
   127      if (lifecycleRule.transition) {
   128        if (
   129          lifecycleRule.transition.days &&
   130          lifecycleRule.transition.days !== 0
   131        ) {
   132          setTransitionDays(lifecycleRule.transition.days.toString());
   133          setIlmType("transition");
   134          transitionMode = true;
   135        }
   136        if (
   137          lifecycleRule.transition.noncurrent_transition_days &&
   138          lifecycleRule.transition.noncurrent_transition_days !== 0
   139        ) {
   140          setNCTransitionDays(
   141            lifecycleRule.transition.noncurrent_transition_days.toString(),
   142          );
   143          setIlmType("transition");
   144          transitionMode = true;
   145        }
   146  
   147        // Fallback to old rules by date
   148        if (
   149          lifecycleRule.transition.date &&
   150          lifecycleRule.transition.date !== "0001-01-01T00:00:00Z"
   151        ) {
   152          setIlmType("transition");
   153          transitionMode = true;
   154        }
   155      }
   156  
   157      if (lifecycleRule.expiration) {
   158        if (
   159          lifecycleRule.expiration.days &&
   160          lifecycleRule.expiration.days !== 0
   161        ) {
   162          setExpiryDays(lifecycleRule.expiration.days.toString());
   163          setIlmType("expiry");
   164          transitionMode = false;
   165        }
   166        if (
   167          lifecycleRule.expiration.noncurrent_expiration_days &&
   168          lifecycleRule.expiration.noncurrent_expiration_days !== 0
   169        ) {
   170          setNCExpirationDays(
   171            lifecycleRule.expiration.noncurrent_expiration_days.toString(),
   172          );
   173          setIlmType("expiry");
   174          transitionMode = false;
   175        }
   176  
   177        // Fallback to old rules by date
   178        if (
   179          lifecycleRule.expiration.date &&
   180          lifecycleRule.expiration.date !== "0001-01-01T00:00:00Z"
   181        ) {
   182          setIlmType("expiry");
   183          transitionMode = false;
   184        }
   185      }
   186  
   187      // Transition fields
   188      if (transitionMode) {
   189        setStorageClass(lifecycleRule.transition?.storage_class || "");
   190        setNCTransitionDays(
   191          lifecycleRule.transition?.noncurrent_transition_days?.toString() || "0",
   192        );
   193        setNCTransitionSC(
   194          lifecycleRule.transition?.noncurrent_storage_class || "",
   195        );
   196      } else {
   197        // Expiry fields
   198        setNCExpirationDays(
   199          lifecycleRule.expiration?.noncurrent_expiration_days?.toString() || "0",
   200        );
   201      }
   202  
   203      setExpiredObjectDM(!!lifecycleRule.expiration?.delete_marker);
   204      setExpiredAllVersionsDM(!!lifecycleRule.expiration?.delete_all);
   205      setPrefix(lifecycleRule.prefix || "");
   206  
   207      if (lifecycleRule.tags) {
   208        const tgs = lifecycleRule.tags.reduce(
   209          (stringLab: string, currItem: any, index: number) => {
   210            return `${stringLab}${index !== 0 ? "&" : ""}${currItem.key}=${
   211              currItem.value
   212            }`;
   213          },
   214          "",
   215        );
   216  
   217        setTags(tgs);
   218      }
   219    }, [lifecycleRule]);
   220  
   221    const saveRecord = (event: React.FormEvent) => {
   222      event.preventDefault();
   223  
   224      if (addLoading) {
   225        return;
   226      }
   227      setAddLoading(true);
   228      if (selectedBucket !== null && lifecycleRule !== null) {
   229        let rules = {};
   230  
   231        if (ilmType === "expiry") {
   232          let expiry: { [key: string]: number } = {};
   233  
   234          if (
   235            lifecycleRule.expiration?.days &&
   236            lifecycleRule.expiration?.days > 0
   237          ) {
   238            expiry["expiry_days"] = parseInt(expiryDays);
   239          }
   240          if (lifecycleRule.expiration?.noncurrent_expiration_days) {
   241            expiry["noncurrentversion_expiration_days"] =
   242              parseInt(NCExpirationDays);
   243          }
   244  
   245          rules = {
   246            ...expiry,
   247          };
   248        } else {
   249          let transition: { [key: string]: number | string } = {};
   250  
   251          if (
   252            lifecycleRule.transition?.days &&
   253            lifecycleRule.transition?.days > 0
   254          ) {
   255            transition["transition_days"] = parseInt(transitionDays);
   256            transition["storage_class"] = storageClass;
   257          }
   258          if (lifecycleRule.transition?.noncurrent_transition_days) {
   259            transition["noncurrentversion_transition_days"] =
   260              parseInt(NCTransitionDays);
   261            transition["noncurrentversion_transition_storage_class"] =
   262              NCTransitionSC;
   263          }
   264  
   265          rules = {
   266            ...transition,
   267          };
   268        }
   269  
   270        const lifecycleUpdate = {
   271          type: ilmType,
   272          disable: !enabled,
   273          prefix,
   274          tags,
   275          expired_object_delete_marker: expiredObjectDM,
   276          expired_object_delete_all: expiredAllVersionsDM,
   277          ...rules,
   278        };
   279  
   280        api.buckets
   281          .updateBucketLifecycle(
   282            selectedBucket,
   283            lifecycleRule.id,
   284            lifecycleUpdate,
   285          )
   286          .then((res) => {
   287            setAddLoading(false);
   288            closeModalAndRefresh(true);
   289          })
   290          .catch(async (eRes) => {
   291            setAddLoading(false);
   292            const err = (await eRes.json()) as ApiError;
   293            dispatch(setErrorSnackMessage(errorToHandler(err)));
   294          });
   295      }
   296    };
   297  
   298    let objectVersion = "";
   299  
   300    if (lifecycleRule.expiration) {
   301      if (lifecycleRule.expiration.days > 0) {
   302        objectVersion = "Current Version";
   303      } else if (lifecycleRule.expiration.noncurrent_expiration_days) {
   304        objectVersion = "Non-Current Version";
   305      }
   306    }
   307  
   308    if (lifecycleRule.transition) {
   309      if (lifecycleRule.transition.days > 0) {
   310        objectVersion = "Current Version";
   311      } else if (lifecycleRule.transition.noncurrent_transition_days) {
   312        objectVersion = "Non-Current Version";
   313      }
   314    }
   315  
   316    return (
   317      <ModalWrapper
   318        onClose={() => {
   319          closeModalAndRefresh(false);
   320        }}
   321        modalOpen={open}
   322        title={"Edit Lifecycle Configuration"}
   323        titleIcon={<LifecycleConfigIcon />}
   324      >
   325        {!loadingTiers ? (
   326          <form
   327            noValidate
   328            autoComplete="off"
   329            onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   330              saveRecord(e);
   331            }}
   332          >
   333            <FormLayout containerPadding={false} withBorders={false}>
   334              <Switch
   335                label="Status"
   336                indicatorLabels={["Enabled", "Disabled"]}
   337                checked={enabled}
   338                value={"user_enabled"}
   339                id="rule_status"
   340                name="rule_status"
   341                onChange={(e) => {
   342                  setEnabled(e.target.checked);
   343                }}
   344              />
   345              <InputBox
   346                id="id"
   347                name="id"
   348                label="Id"
   349                value={lifecycleRule.id}
   350                onChange={() => {}}
   351                disabled
   352              />
   353              {ilmType ? (
   354                <RadioGroup
   355                  currentValue={ilmType}
   356                  id="rule_type"
   357                  name="rule_type"
   358                  label="Rule Type"
   359                  selectorOptions={ILM_TYPES}
   360                  onChange={() => {}}
   361                  disableOptions
   362                />
   363              ) : null}
   364  
   365              <InputBox
   366                id="object-version"
   367                name="object-version"
   368                label="Object Version"
   369                value={objectVersion}
   370                onChange={() => {}}
   371                disabled
   372              />
   373  
   374              {ilmType === "expiry" && lifecycleRule.expiration?.days && (
   375                <InputBox
   376                  type="number"
   377                  id="expiry_days"
   378                  name="expiry_days"
   379                  onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   380                    setExpiryDays(e.target.value);
   381                  }}
   382                  label="Expiry Days"
   383                  value={expiryDays}
   384                  min="0"
   385                />
   386              )}
   387  
   388              {ilmType === "expiry" &&
   389                lifecycleRule.expiration?.noncurrent_expiration_days && (
   390                  <InputBox
   391                    type="number"
   392                    id="noncurrentversion_expiration_days"
   393                    name="noncurrentversion_expiration_days"
   394                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   395                      setNCExpirationDays(e.target.value);
   396                    }}
   397                    label="Non-current Expiration Days"
   398                    value={NCExpirationDays}
   399                    min="0"
   400                  />
   401                )}
   402              {ilmType === "transition" && lifecycleRule.transition?.days && (
   403                <Fragment>
   404                  <InputBox
   405                    type="number"
   406                    id="transition_days"
   407                    name="transition_days"
   408                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   409                      setTransitionDays(e.target.value);
   410                    }}
   411                    label="Transition Days"
   412                    value={transitionDays}
   413                    min="0"
   414                  />
   415                  <Select
   416                    label="Tier"
   417                    id="storage_class"
   418                    name="storage_class"
   419                    value={storageClass}
   420                    onChange={(value) => {
   421                      setStorageClass(value);
   422                    }}
   423                    options={tiersList}
   424                  />
   425                </Fragment>
   426              )}
   427  
   428              {ilmType === "transition" &&
   429                lifecycleRule.transition?.noncurrent_transition_days && (
   430                  <Fragment>
   431                    <InputBox
   432                      type="number"
   433                      id="noncurrentversion_transition_days"
   434                      name="noncurrentversion_transition_days"
   435                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   436                        setNCTransitionDays(e.target.value);
   437                      }}
   438                      label="Non-current Transition Days"
   439                      value={NCTransitionDays}
   440                      min="0"
   441                    />
   442                    <InputBox
   443                      id="noncurrentversion_t_SC"
   444                      name="noncurrentversion_t_SC"
   445                      onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   446                        setNCTransitionSC(e.target.value);
   447                      }}
   448                      placeholder="Set Non-current Version Transition Storage Class"
   449                      label="Non-current Version Transition Storage Class"
   450                      value={NCTransitionSC}
   451                    />
   452                  </Fragment>
   453                )}
   454              <Grid item xs={12}>
   455                <Accordion
   456                  title={"Filters"}
   457                  id={"lifecycle-filters"}
   458                  expanded={expanded}
   459                  onTitleClick={() => setExpanded(!expanded)}
   460                >
   461                  <InputBox
   462                    id="prefix"
   463                    name="prefix"
   464                    onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   465                      setPrefix(e.target.value);
   466                    }}
   467                    label="Prefix"
   468                    value={prefix}
   469                  />
   470                  <QueryMultiSelector
   471                    name="tags"
   472                    label="Tags"
   473                    elements={tags}
   474                    onChange={(vl: string) => {
   475                      setTags(vl);
   476                    }}
   477                    keyPlaceholder="Tag Key"
   478                    valuePlaceholder="Tag Value"
   479                    withBorder
   480                  />
   481                </Accordion>
   482              </Grid>
   483              {ilmType === "expiry" &&
   484                lifecycleRule.expiration?.noncurrent_expiration_days && (
   485                  <Grid item xs={12}>
   486                    <Accordion
   487                      title={"Advanced"}
   488                      id={"lifecycle-advanced-filters"}
   489                      expanded={expandedAdv}
   490                      onTitleClick={() => setExpandedAdv(!expandedAdv)}
   491                      sx={{ marginTop: 15 }}
   492                    >
   493                      <Switch
   494                        value="expired_delete_marker"
   495                        id="expired_delete_marker"
   496                        name="expired_delete_marker"
   497                        checked={expiredObjectDM}
   498                        onChange={(
   499                          event: React.ChangeEvent<HTMLInputElement>,
   500                        ) => {
   501                          setExpiredObjectDM(event.target.checked);
   502                        }}
   503                        label={"Expired Object Delete Marker"}
   504                      />
   505                      <Switch
   506                        value="expired_delete_all"
   507                        id="expired_delete_all"
   508                        name="expired_delete_all"
   509                        checked={expiredAllVersionsDM}
   510                        onChange={(
   511                          event: React.ChangeEvent<HTMLInputElement>,
   512                        ) => {
   513                          setExpiredAllVersionsDM(event.target.checked);
   514                        }}
   515                        label={"Expired All Versions"}
   516                      />
   517                    </Accordion>
   518                  </Grid>
   519                )}
   520              <Grid item xs={12} sx={modalStyleUtils.modalButtonBar}>
   521                <Button
   522                  id={"cancel"}
   523                  type="button"
   524                  variant="regular"
   525                  disabled={addLoading}
   526                  onClick={() => {
   527                    closeModalAndRefresh(false);
   528                  }}
   529                  label={"Cancel"}
   530                />
   531                <Button
   532                  id={"save"}
   533                  type="submit"
   534                  variant="callAction"
   535                  color="primary"
   536                  disabled={addLoading || !isFormValid}
   537                  label={"Save"}
   538                />
   539              </Grid>
   540              {addLoading && (
   541                <Grid item xs={12}>
   542                  <ProgressBar />
   543                </Grid>
   544              )}
   545            </FormLayout>
   546          </form>
   547        ) : (
   548          <Loader style={{ width: 16, height: 16 }} />
   549        )}
   550      </ModalWrapper>
   551    );
   552  };
   553  
   554  export default EditLifecycleConfiguration;