github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/AddBucket/AddBucket.tsx (about)

     1  // This file is part of MinIO Console Server
     2  // Copyright (c) 2022 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 styled from "styled-components";
    19  import get from "lodash/get";
    20  
    21  import { useNavigate } from "react-router-dom";
    22  import {
    23    BackLink,
    24    Box,
    25    BucketsIcon,
    26    Button,
    27    FormLayout,
    28    Grid,
    29    HelpBox,
    30    InfoIcon,
    31    InputBox,
    32    PageLayout,
    33    RadioGroup,
    34    Switch,
    35    SectionTitle,
    36    ProgressBar,
    37  } from "mds";
    38  import { k8sScalarUnitsExcluding } from "../../../../../common/utils";
    39  import { AppState, useAppDispatch } from "../../../../../store";
    40  import { useSelector } from "react-redux";
    41  import {
    42    selDistSet,
    43    selSiteRep,
    44    setErrorSnackMessage,
    45    setHelpName,
    46  } from "../../../../../systemSlice";
    47  import InputUnitMenu from "../../../Common/FormComponents/InputUnitMenu/InputUnitMenu";
    48  import TooltipWrapper from "../../../Common/TooltipWrapper/TooltipWrapper";
    49  import {
    50    resetForm,
    51    setEnableObjectLocking,
    52    setExcludedPrefixes,
    53    setExcludeFolders,
    54    setIsDirty,
    55    setName,
    56    setQuota,
    57    setQuotaSize,
    58    setQuotaUnit,
    59    setRetention,
    60    setRetentionMode,
    61    setRetentionUnit,
    62    setRetentionValidity,
    63    setVersioning,
    64  } from "./addBucketsSlice";
    65  import { addBucketAsync } from "./addBucketThunks";
    66  import AddBucketName from "./AddBucketName";
    67  import {
    68    IAM_SCOPES,
    69    permissionTooltipHelper,
    70  } from "../../../../../common/SecureComponent/permissions";
    71  import { hasPermission } from "../../../../../common/SecureComponent";
    72  import BucketNamingRules from "./BucketNamingRules";
    73  import PageHeaderWrapper from "../../../Common/PageHeaderWrapper/PageHeaderWrapper";
    74  import { api } from "../../../../../api";
    75  import { ObjectRetentionMode } from "../../../../../api/consoleApi";
    76  import { errorToHandler } from "../../../../../api/errors";
    77  import HelpMenu from "../../../HelpMenu";
    78  import CSVMultiSelector from "../../../Common/FormComponents/CSVMultiSelector/CSVMultiSelector";
    79  
    80  const ErrorBox = styled.div(({ theme }) => ({
    81    color: get(theme, "signalColors.danger", "#C51B3F"),
    82    border: `1px solid ${get(theme, "signalColors.danger", "#C51B3F")}`,
    83    padding: 8,
    84    borderRadius: 3,
    85  }));
    86  
    87  const AddBucket = () => {
    88    const dispatch = useAppDispatch();
    89    const navigate = useNavigate();
    90  
    91    const validBucketCharacters = new RegExp(
    92      `^[a-z0-9][a-z0-9\\.\\-]{1,61}[a-z0-9]$`,
    93    );
    94    const ipAddressFormat = new RegExp(`^(\\d+\\.){3}\\d+$`);
    95    const bucketName = useSelector((state: AppState) => state.addBucket.name);
    96    const isDirty = useSelector((state: AppState) => state.addBucket.isDirty);
    97    const [validationResult, setValidationResult] = useState<boolean[]>([]);
    98    const errorList = validationResult.filter((v) => !v);
    99    const hasErrors = errorList.length > 0;
   100    const [records, setRecords] = useState<string[]>([]);
   101    const versioningEnabled = useSelector(
   102      (state: AppState) => state.addBucket.versioningEnabled,
   103    );
   104    const excludeFolders = useSelector(
   105      (state: AppState) => state.addBucket.excludeFolders,
   106    );
   107    const excludedPrefixes = useSelector(
   108      (state: AppState) => state.addBucket.excludedPrefixes,
   109    );
   110    const lockingEnabled = useSelector(
   111      (state: AppState) => state.addBucket.lockingEnabled,
   112    );
   113    const quotaEnabled = useSelector(
   114      (state: AppState) => state.addBucket.quotaEnabled,
   115    );
   116    const quotaSize = useSelector((state: AppState) => state.addBucket.quotaSize);
   117    const quotaUnit = useSelector((state: AppState) => state.addBucket.quotaUnit);
   118    const retentionEnabled = useSelector(
   119      (state: AppState) => state.addBucket.retentionEnabled,
   120    );
   121    const retentionMode = useSelector(
   122      (state: AppState) => state.addBucket.retentionMode,
   123    );
   124    const retentionUnit = useSelector(
   125      (state: AppState) => state.addBucket.retentionUnit,
   126    );
   127    const retentionValidity = useSelector(
   128      (state: AppState) => state.addBucket.retentionValidity,
   129    );
   130    const addLoading = useSelector((state: AppState) => state.addBucket.loading);
   131    const invalidFields = useSelector(
   132      (state: AppState) => state.addBucket.invalidFields,
   133    );
   134    const lockingFieldDisabled = useSelector(
   135      (state: AppState) => state.addBucket.lockingFieldDisabled,
   136    );
   137    const distributedSetup = useSelector(selDistSet);
   138    const siteReplicationInfo = useSelector(selSiteRep);
   139    const navigateTo = useSelector(
   140      (state: AppState) => state.addBucket.navigateTo,
   141    );
   142  
   143    const lockingAllowed = hasPermission(
   144      "*",
   145      [
   146        IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
   147        IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION,
   148        IAM_SCOPES.S3_PUT_ACTIONS,
   149      ],
   150      true,
   151    );
   152  
   153    const versioningAllowed = hasPermission("*", [
   154      IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
   155      IAM_SCOPES.S3_PUT_ACTIONS,
   156    ]);
   157  
   158    useEffect(() => {
   159      const bucketNameErrors = [
   160        !(isDirty && (bucketName.length < 3 || bucketName.length > 63)),
   161        validBucketCharacters.test(bucketName),
   162        !(
   163          bucketName.includes(".-") ||
   164          bucketName.includes("-.") ||
   165          bucketName.includes("..")
   166        ),
   167        !ipAddressFormat.test(bucketName),
   168        !bucketName.startsWith("xn--"),
   169        !bucketName.endsWith("-s3alias"),
   170        !records.includes(bucketName),
   171      ];
   172      setValidationResult(bucketNameErrors);
   173      // eslint-disable-next-line react-hooks/exhaustive-deps
   174    }, [bucketName, isDirty]);
   175  
   176    useEffect(() => {
   177      dispatch(setName(""));
   178      dispatch(setIsDirty(false));
   179      const fetchRecords = () => {
   180        api.buckets
   181          .listBuckets()
   182          .then((res) => {
   183            if (res.data) {
   184              var bucketList: string[] = [];
   185              if (res.data.buckets != null && res.data.buckets.length > 0) {
   186                res.data.buckets.forEach((bucket) => {
   187                  bucketList.push(bucket.name);
   188                });
   189              }
   190              setRecords(bucketList);
   191            } else if (res.error) {
   192              dispatch(setErrorSnackMessage(errorToHandler(res.error)));
   193            }
   194          })
   195          .catch((err) => {
   196            dispatch(setErrorSnackMessage(errorToHandler(err)));
   197          });
   198      };
   199      fetchRecords();
   200    }, [dispatch]);
   201  
   202    const resForm = () => {
   203      dispatch(resetForm());
   204    };
   205  
   206    useEffect(() => {
   207      if (navigateTo !== "") {
   208        const goTo = `${navigateTo}`;
   209        dispatch(resetForm());
   210        navigate(goTo);
   211      }
   212    }, [navigateTo, navigate, dispatch]);
   213  
   214    useEffect(() => {
   215      dispatch(setHelpName("add_bucket"));
   216      // eslint-disable-next-line react-hooks/exhaustive-deps
   217    }, []);
   218  
   219    return (
   220      <Fragment>
   221        <PageHeaderWrapper
   222          label={
   223            <BackLink label={"Buckets"} onClick={() => navigate("/buckets")} />
   224          }
   225          actions={<HelpMenu />}
   226        />
   227        <PageLayout>
   228          <FormLayout
   229            title={"Create Bucket"}
   230            icon={<BucketsIcon />}
   231            helpBox={
   232              <HelpBox
   233                iconComponent={<BucketsIcon />}
   234                title={"Buckets"}
   235                help={
   236                  <Fragment>
   237                    MinIO uses buckets to organize objects. A bucket is similar to
   238                    a folder or directory in a filesystem, where each bucket can
   239                    hold an arbitrary number of objects.
   240                    <br />
   241                    <br />
   242                    <b>Versioning</b> allows to keep multiple versions of the same
   243                    object under the same key.
   244                    <br />
   245                    <br />
   246                    <b>Object Locking</b> prevents objects from being deleted.
   247                    Required to support retention and legal hold. Can only be
   248                    enabled at bucket creation.
   249                    <br />
   250                    <br />
   251                    <b>Quota</b> limits the amount of data in the bucket.
   252                    {lockingAllowed && (
   253                      <Fragment>
   254                        <br />
   255                        <br />
   256                        <b>Retention</b> imposes rules to prevent object deletion
   257                        for a period of time. Versioning must be enabled in order
   258                        to set bucket retention policies.
   259                      </Fragment>
   260                    )}
   261                    <br />
   262                    <br />
   263                  </Fragment>
   264                }
   265              />
   266            }
   267          >
   268            <form
   269              noValidate
   270              autoComplete="off"
   271              onSubmit={(e: React.FormEvent<HTMLFormElement>) => {
   272                e.preventDefault();
   273                dispatch(addBucketAsync());
   274              }}
   275            >
   276              <Box>
   277                <AddBucketName hasErrors={hasErrors} />
   278                <Box sx={{ margin: "10px 0" }}>
   279                  <BucketNamingRules errorList={validationResult} />
   280                </Box>
   281                <SectionTitle separator>Features</SectionTitle>
   282                <Box sx={{ marginTop: 10 }}>
   283                  {!distributedSetup && (
   284                    <Fragment>
   285                      <ErrorBox>
   286                        These features are unavailable in a single-disk setup.
   287                        <br />
   288                        Please deploy a server in{" "}
   289                        <a
   290                          href="https://min.io/docs/minio/linux/operations/install-deploy-manage/deploy-minio-multi-node-multi-drive.html?ref=con"
   291                          target="_blank"
   292                          rel="noopener"
   293                        >
   294                          Distributed Mode
   295                        </a>{" "}
   296                        to use these features.
   297                      </ErrorBox>
   298                      <br />
   299                      <br />
   300                    </Fragment>
   301                  )}
   302  
   303                  {siteReplicationInfo.enabled && (
   304                    <Fragment>
   305                      <br />
   306                      <Box
   307                        withBorders
   308                        sx={{
   309                          display: "flex",
   310                          alignItems: "center",
   311                          padding: "10px",
   312                          "& > .min-icon ": {
   313                            width: 20,
   314                            height: 20,
   315                            marginRight: 10,
   316                          },
   317                        }}
   318                      >
   319                        <InfoIcon /> Versioning setting cannot be changed as
   320                        cluster replication is enabled for this site.
   321                      </Box>
   322                      <br />
   323                    </Fragment>
   324                  )}
   325                  <Switch
   326                    value="versioned"
   327                    id="versioned"
   328                    name="versioned"
   329                    checked={versioningEnabled}
   330                    onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
   331                      dispatch(setVersioning(event.target.checked));
   332                    }}
   333                    label={"Versioning"}
   334                    disabled={
   335                      !distributedSetup ||
   336                      lockingEnabled ||
   337                      siteReplicationInfo.enabled ||
   338                      !versioningAllowed
   339                    }
   340                    tooltip={
   341                      versioningAllowed
   342                        ? ""
   343                        : permissionTooltipHelper(
   344                            [
   345                              IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
   346                              IAM_SCOPES.S3_PUT_ACTIONS,
   347                            ],
   348                            "Versioning",
   349                          )
   350                    }
   351                    helpTip={
   352                      <Fragment>
   353                        {lockingEnabled && versioningEnabled && (
   354                          <strong>
   355                            {" "}
   356                            You must disable Object Locking before Versioning can
   357                            be disabled <br />
   358                          </strong>
   359                        )}
   360                        MinIO supports keeping multiple{" "}
   361                        <a
   362                          href="https://min.io/docs/minio/kubernetes/upstream/administration/object-management/object-versioning.html#minio-bucket-versioning"
   363                          target="blank"
   364                        >
   365                          versions
   366                        </a>{" "}
   367                        of an object in a single bucket.
   368                        <br />
   369                        Versioning is required to enable{" "}
   370                        <a
   371                          href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention"
   372                          target="blank"
   373                        >
   374                          Object Locking
   375                        </a>{" "}
   376                        and{" "}
   377                        <a
   378                          href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#object-retention-modes"
   379                          target="blank"
   380                        >
   381                          Retention
   382                        </a>
   383                        .
   384                      </Fragment>
   385                    }
   386                    helpTipPlacement="right"
   387                  />
   388                  {versioningEnabled && distributedSetup && !lockingEnabled && (
   389                    <Fragment>
   390                      <Switch
   391                        id={"excludeFolders"}
   392                        label={"Exclude Folders"}
   393                        checked={excludeFolders}
   394                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   395                          dispatch(setExcludeFolders(e.target.checked));
   396                        }}
   397                        indicatorLabels={["Enabled", "Disabled"]}
   398                        helpTip={
   399                          <Fragment>
   400                            You can choose to{" "}
   401                            <a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning">
   402                              exclude folders and prefixes
   403                            </a>{" "}
   404                            from versioning if Object Locking is not enabled.
   405                            <br />
   406                            MinIO requires versioning to support replication.
   407                            <br />
   408                            Objects in excluded prefixes do not replicate to any
   409                            peer site or remote site.
   410                          </Fragment>
   411                        }
   412                        helpTipPlacement="right"
   413                      />
   414                      <CSVMultiSelector
   415                        elements={excludedPrefixes}
   416                        label={"Excluded Prefixes"}
   417                        name={"excludedPrefixes"}
   418                        onChange={(value: string | string[]) => {
   419                          let valCh = "";
   420  
   421                          if (Array.isArray(value)) {
   422                            valCh = value.join(",");
   423                          } else {
   424                            valCh = value;
   425                          }
   426                          dispatch(setExcludedPrefixes(valCh));
   427                        }}
   428                        withBorder={true}
   429                      />
   430                    </Fragment>
   431                  )}
   432                  <Switch
   433                    value="locking"
   434                    id="locking"
   435                    name="locking"
   436                    disabled={
   437                      lockingFieldDisabled || !distributedSetup || !lockingAllowed
   438                    }
   439                    checked={lockingEnabled}
   440                    onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
   441                      dispatch(setEnableObjectLocking(event.target.checked));
   442                      if (event.target.checked && !siteReplicationInfo.enabled) {
   443                        dispatch(setVersioning(true));
   444                      }
   445                    }}
   446                    label={"Object Locking"}
   447                    tooltip={
   448                      lockingAllowed
   449                        ? ``
   450                        : permissionTooltipHelper(
   451                            [
   452                              IAM_SCOPES.S3_PUT_BUCKET_VERSIONING,
   453                              IAM_SCOPES.S3_PUT_BUCKET_OBJECT_LOCK_CONFIGURATION,
   454                              IAM_SCOPES.S3_PUT_ACTIONS,
   455                            ],
   456                            "Locking",
   457                          )
   458                    }
   459                    helpTip={
   460                      <Fragment>
   461                        {retentionEnabled && (
   462                          <strong>
   463                            {" "}
   464                            You must disable Retention before Object Locking can
   465                            be disabled <br />
   466                          </strong>
   467                        )}
   468                        You can only enable{" "}
   469                        <a
   470                          href="https://min.io/docs/minio/macos/administration/object-management.html#object-retention"
   471                          target="blank"
   472                        >
   473                          Object Locking
   474                        </a>{" "}
   475                        when first creating a bucket.
   476                        <br />
   477                        <br />
   478                        <a href="https://min.io/docs/minio/windows/administration/object-management/object-versioning.html#exclude-folders-from-versioning">
   479                          Exclude folders and prefixes
   480                        </a>{" "}
   481                        options will not be available if this option is enabled.
   482                      </Fragment>
   483                    }
   484                    helpTipPlacement="right"
   485                  />
   486                  <Switch
   487                    value="bucket_quota"
   488                    id="bucket_quota"
   489                    name="bucket_quota"
   490                    checked={quotaEnabled}
   491                    onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
   492                      dispatch(setQuota(event.target.checked));
   493                    }}
   494                    label={"Quota"}
   495                    disabled={!distributedSetup}
   496                    helpTip={
   497                      <Fragment>
   498                        Setting a{" "}
   499                        <a
   500                          href="https://min.io/docs/minio/linux/reference/minio-mc/mc-quota-set.html"
   501                          target="blank"
   502                        >
   503                          quota
   504                        </a>{" "}
   505                        assigns a hard limit to a bucket beyond which MinIO does
   506                        not allow writes.
   507                      </Fragment>
   508                    }
   509                    helpTipPlacement="right"
   510                  />
   511                  {quotaEnabled && distributedSetup && (
   512                    <Fragment>
   513                      <InputBox
   514                        type="string"
   515                        id="quota_size"
   516                        name="quota_size"
   517                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   518                          dispatch(setQuotaSize(e.target.value));
   519                        }}
   520                        label="Capacity"
   521                        value={quotaSize}
   522                        required
   523                        min="1"
   524                        overlayObject={
   525                          <InputUnitMenu
   526                            id={"quota_unit"}
   527                            onUnitChange={(newValue) => {
   528                              dispatch(setQuotaUnit(newValue));
   529                            }}
   530                            unitSelected={quotaUnit}
   531                            unitsList={k8sScalarUnitsExcluding(["Ki"])}
   532                            disabled={false}
   533                          />
   534                        }
   535                        error={
   536                          invalidFields.includes("quotaSize")
   537                            ? "Please enter a valid quota"
   538                            : ""
   539                        }
   540                      />
   541                    </Fragment>
   542                  )}
   543                  {versioningEnabled && distributedSetup && lockingAllowed && (
   544                    <Switch
   545                      value="bucket_retention"
   546                      id="bucket_retention"
   547                      name="bucket_retention"
   548                      checked={retentionEnabled}
   549                      onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
   550                        dispatch(setRetention(event.target.checked));
   551                      }}
   552                      label={"Retention"}
   553                      helpTip={
   554                        <Fragment>
   555                          MinIO supports setting both{" "}
   556                          <a
   557                            href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#configure-bucket-default-object-retention"
   558                            target="blank"
   559                          >
   560                            bucket-default
   561                          </a>{" "}
   562                          and per-object retention rules.
   563                          <br />
   564                          <br /> For per-object retention settings, defer to the
   565                          documentation for the PUT operation used by your
   566                          preferred SDK.
   567                        </Fragment>
   568                      }
   569                      helpTipPlacement="right"
   570                    />
   571                  )}
   572                  {retentionEnabled && distributedSetup && (
   573                    <Fragment>
   574                      <RadioGroup
   575                        currentValue={retentionMode}
   576                        id="retention_mode"
   577                        name="retention_mode"
   578                        label="Mode"
   579                        onChange={(e: React.ChangeEvent<{ value: unknown }>) => {
   580                          dispatch(
   581                            setRetentionMode(
   582                              e.target.value as ObjectRetentionMode,
   583                            ),
   584                          );
   585                        }}
   586                        selectorOptions={[
   587                          { value: "compliance", label: "Compliance" },
   588                          { value: "governance", label: "Governance" },
   589                        ]}
   590                        helpTip={
   591                          <Fragment>
   592                            {" "}
   593                            <a
   594                              href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-compliance"
   595                              target="blank"
   596                            >
   597                              Compliance
   598                            </a>{" "}
   599                            lock protects Objects from write operations by all
   600                            users, including the MinIO root user.
   601                            <br />
   602                            <br />
   603                            <a
   604                              href="https://min.io/docs/minio/macos/administration/object-management/object-retention.html#minio-object-locking-governance"
   605                              target="blank"
   606                            >
   607                              Governance
   608                            </a>{" "}
   609                            lock protects Objects from write operations by
   610                            non-privileged users.
   611                          </Fragment>
   612                        }
   613                        helpTipPlacement="right"
   614                      />
   615                      <InputBox
   616                        type="number"
   617                        id="retention_validity"
   618                        name="retention_validity"
   619                        onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
   620                          dispatch(setRetentionValidity(e.target.valueAsNumber));
   621                        }}
   622                        label="Validity"
   623                        value={String(retentionValidity)}
   624                        required
   625                        overlayObject={
   626                          <InputUnitMenu
   627                            id={"retention_unit"}
   628                            onUnitChange={(newValue) => {
   629                              dispatch(setRetentionUnit(newValue));
   630                            }}
   631                            unitSelected={retentionUnit}
   632                            unitsList={[
   633                              { value: "days", label: "Days" },
   634                              { value: "years", label: "Years" },
   635                            ]}
   636                            disabled={false}
   637                          />
   638                        }
   639                      />
   640                    </Fragment>
   641                  )}
   642                </Box>
   643              </Box>
   644              <Grid
   645                item
   646                xs={12}
   647                sx={{
   648                  display: "flex",
   649                  justifyContent: "flex-end",
   650                  alignItems: "center",
   651                  gap: 10,
   652                  marginTop: 15,
   653                }}
   654              >
   655                <Button
   656                  id={"clear"}
   657                  type="button"
   658                  variant={"regular"}
   659                  className={"clearButton"}
   660                  onClick={resForm}
   661                  label={"Clear"}
   662                />
   663                <TooltipWrapper
   664                  tooltip={
   665                    invalidFields.length > 0 || !isDirty || hasErrors
   666                      ? "You must apply a valid name to the bucket"
   667                      : ""
   668                  }
   669                >
   670                  <Button
   671                    id={"create-bucket"}
   672                    type="submit"
   673                    variant="callAction"
   674                    color="primary"
   675                    disabled={
   676                      addLoading ||
   677                      invalidFields.length > 0 ||
   678                      !isDirty ||
   679                      hasErrors
   680                    }
   681                    label={"Create Bucket"}
   682                  />
   683                </TooltipWrapper>
   684              </Grid>
   685              {addLoading && (
   686                <Grid item xs={12}>
   687                  <ProgressBar />
   688                </Grid>
   689              )}
   690            </form>
   691          </FormLayout>
   692        </PageLayout>
   693      </Fragment>
   694    );
   695  };
   696  
   697  export default AddBucket;