github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/ListBuckets.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 { useNavigate } from "react-router-dom";
    19  import {
    20    AddIcon,
    21    BucketsIcon,
    22    Button,
    23    HelpBox,
    24    LifecycleConfigIcon,
    25    MultipleBucketsIcon,
    26    PageLayout,
    27    RefreshIcon,
    28    SelectAllIcon,
    29    SelectMultipleIcon,
    30    Grid,
    31    breakPoints,
    32    ProgressBar,
    33    ActionLink,
    34  } from "mds";
    35  
    36  import { actionsTray } from "../../Common/FormComponents/common/styleLibrary";
    37  import { SecureComponent } from "../../../../common/SecureComponent";
    38  import {
    39    CONSOLE_UI_RESOURCE,
    40    IAM_PAGES,
    41    IAM_PERMISSIONS,
    42    IAM_ROLES,
    43    IAM_SCOPES,
    44    permissionTooltipHelper,
    45  } from "../../../../common/SecureComponent/permissions";
    46  import { setErrorSnackMessage, setHelpName } from "../../../../systemSlice";
    47  import { useAppDispatch } from "../../../../store";
    48  import { useSelector } from "react-redux";
    49  import { selFeatures } from "../../consoleSlice";
    50  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    51  import { api } from "../../../../api";
    52  import { Bucket } from "../../../../api/consoleApi";
    53  import { errorToHandler } from "../../../../api/errors";
    54  import HelpMenu from "../../HelpMenu";
    55  import AutoColorIcon from "../../Common/Components/AutoColorIcon";
    56  import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
    57  import SearchBox from "../../Common/SearchBox";
    58  import VirtualizedList from "../../Common/VirtualizedList/VirtualizedList";
    59  import BulkLifecycleModal from "./BulkLifecycleModal";
    60  import hasPermission from "../../../../common/SecureComponent/accessControl";
    61  import BucketListItem from "./BucketListItem";
    62  import BulkReplicationModal from "./BulkReplicationModal";
    63  
    64  const ListBuckets = () => {
    65    const dispatch = useAppDispatch();
    66    const navigate = useNavigate();
    67  
    68    const [records, setRecords] = useState<Bucket[]>([]);
    69    const [loading, setLoading] = useState<boolean>(true);
    70    const [filterBuckets, setFilterBuckets] = useState<string>("");
    71    const [selectedBuckets, setSelectedBuckets] = useState<string[]>([]);
    72    const [replicationModalOpen, setReplicationModalOpen] =
    73      useState<boolean>(false);
    74    const [lifecycleModalOpen, setLifecycleModalOpen] = useState<boolean>(false);
    75    const [canPutLifecycle, setCanPutLifecycle] = useState<boolean>(false);
    76    const [bulkSelect, setBulkSelect] = useState<boolean>(false);
    77  
    78    const features = useSelector(selFeatures);
    79    const obOnly = !!features?.includes("object-browser-only");
    80  
    81    useEffect(() => {
    82      dispatch(setHelpName("ob_bucket_list"));
    83    }, [dispatch]);
    84  
    85    useEffect(() => {
    86      if (loading) {
    87        const fetchRecords = () => {
    88          setLoading(true);
    89          api.buckets.listBuckets().then((res) => {
    90            if (res.data) {
    91              setLoading(false);
    92              setRecords(res.data.buckets || []);
    93            } else if (res.error) {
    94              setLoading(false);
    95              dispatch(setErrorSnackMessage(errorToHandler(res.error)));
    96            }
    97          });
    98        };
    99        fetchRecords();
   100      }
   101    }, [loading, dispatch]);
   102  
   103    const filteredRecords = records.filter((b: Bucket) => {
   104      if (filterBuckets === "") {
   105        return true;
   106      } else {
   107        return b.name.indexOf(filterBuckets) >= 0;
   108      }
   109    });
   110  
   111    const hasBuckets = records.length > 0;
   112  
   113    const selectListBuckets = (e: React.ChangeEvent<HTMLInputElement>) => {
   114      const targetD = e.target;
   115      const value = targetD.value;
   116      const checked = targetD.checked;
   117  
   118      let elements: string[] = [...selectedBuckets]; // We clone the selectedBuckets array
   119  
   120      if (checked) {
   121        // If the user has checked this field we need to push this to selectedBucketsList
   122        elements.push(value);
   123      } else {
   124        // User has unchecked this field, we need to remove it from the list
   125        elements = elements.filter((element) => element !== value);
   126      }
   127      setSelectedBuckets(elements);
   128  
   129      return elements;
   130    };
   131  
   132    const closeBulkReplicationModal = (unselectAll: boolean) => {
   133      setReplicationModalOpen(false);
   134  
   135      if (unselectAll) {
   136        setSelectedBuckets([]);
   137      }
   138    };
   139  
   140    const closeBulkLifecycleModal = (unselectAll: boolean) => {
   141      setLifecycleModalOpen(false);
   142  
   143      if (unselectAll) {
   144        setSelectedBuckets([]);
   145      }
   146    };
   147  
   148    useEffect(() => {
   149      var failLifecycle = false;
   150      selectedBuckets.forEach((bucket: string) => {
   151        hasPermission(bucket, IAM_PERMISSIONS[IAM_ROLES.BUCKET_LIFECYCLE], true)
   152          ? setCanPutLifecycle(true)
   153          : (failLifecycle = true);
   154      });
   155      failLifecycle ? setCanPutLifecycle(false) : setCanPutLifecycle(true);
   156    }, [selectedBuckets]);
   157  
   158    const renderItemLine = (index: number) => {
   159      const bucket = filteredRecords[index] || null;
   160      if (bucket) {
   161        return (
   162          <BucketListItem
   163            bucket={bucket}
   164            onSelect={selectListBuckets}
   165            selected={selectedBuckets.includes(bucket.name)}
   166            bulkSelect={bulkSelect}
   167          />
   168        );
   169      }
   170      return null;
   171    };
   172  
   173    const selectAllBuckets = () => {
   174      if (selectedBuckets.length === filteredRecords.length) {
   175        setSelectedBuckets([]);
   176        return;
   177      }
   178  
   179      const selectAllBuckets = filteredRecords.map((bucket) => {
   180        return bucket.name;
   181      });
   182  
   183      setSelectedBuckets(selectAllBuckets);
   184    };
   185  
   186    const canCreateBucket = hasPermission("*", [IAM_SCOPES.S3_CREATE_BUCKET]);
   187    const canListBuckets = hasPermission("*", [
   188      IAM_SCOPES.S3_LIST_BUCKET,
   189      IAM_SCOPES.S3_ALL_LIST_BUCKET,
   190    ]);
   191  
   192    return (
   193      <Fragment>
   194        {replicationModalOpen && (
   195          <BulkReplicationModal
   196            open={replicationModalOpen}
   197            buckets={selectedBuckets}
   198            closeModalAndRefresh={closeBulkReplicationModal}
   199          />
   200        )}
   201        {lifecycleModalOpen && (
   202          <BulkLifecycleModal
   203            buckets={selectedBuckets}
   204            closeModalAndRefresh={closeBulkLifecycleModal}
   205            open={lifecycleModalOpen}
   206          />
   207        )}
   208        {!obOnly && (
   209          <PageHeaderWrapper label={"Buckets"} actions={<HelpMenu />} />
   210        )}
   211  
   212        <PageLayout>
   213          <Grid item xs={12} sx={actionsTray.actionsTray}>
   214            {obOnly && (
   215              <Grid item xs>
   216                <AutoColorIcon marginRight={15} marginTop={10} />
   217              </Grid>
   218            )}
   219            {hasBuckets && (
   220              <SearchBox
   221                onChange={setFilterBuckets}
   222                placeholder="Search Buckets"
   223                value={filterBuckets}
   224                sx={{
   225                  minWidth: 380,
   226                  [`@media (max-width: ${breakPoints.md}px)`]: {
   227                    minWidth: 220,
   228                  },
   229                }}
   230              />
   231            )}
   232  
   233            <Grid
   234              item
   235              xs={12}
   236              sx={{
   237                display: "flex",
   238                alignItems: "center",
   239                justifyContent: "flex-end",
   240                gap: 5,
   241              }}
   242            >
   243              {!obOnly && (
   244                <Fragment>
   245                  <TooltipWrapper
   246                    tooltip={
   247                      !hasBuckets
   248                        ? ""
   249                        : bulkSelect
   250                          ? "Unselect Buckets"
   251                          : "Select Multiple Buckets"
   252                    }
   253                  >
   254                    <Button
   255                      id={"multiple-bucket-seection"}
   256                      onClick={() => {
   257                        setBulkSelect(!bulkSelect);
   258                        setSelectedBuckets([]);
   259                      }}
   260                      icon={<SelectMultipleIcon />}
   261                      variant={bulkSelect ? "callAction" : "regular"}
   262                      disabled={!hasBuckets}
   263                    />
   264                  </TooltipWrapper>
   265  
   266                  {bulkSelect && (
   267                    <TooltipWrapper
   268                      tooltip={
   269                        !hasBuckets
   270                          ? ""
   271                          : selectedBuckets.length === filteredRecords.length
   272                            ? "Unselect All Buckets"
   273                            : "Select All Buckets"
   274                      }
   275                    >
   276                      <Button
   277                        id={"select-all-buckets"}
   278                        onClick={selectAllBuckets}
   279                        icon={<SelectAllIcon />}
   280                        variant={"regular"}
   281                      />
   282                    </TooltipWrapper>
   283                  )}
   284  
   285                  <TooltipWrapper
   286                    tooltip={
   287                      !hasBuckets
   288                        ? ""
   289                        : !canPutLifecycle
   290                          ? permissionTooltipHelper(
   291                              IAM_PERMISSIONS[IAM_ROLES.BUCKET_LIFECYCLE],
   292                              "configure lifecycle for the selected buckets",
   293                            )
   294                          : selectedBuckets.length === 0
   295                            ? bulkSelect
   296                              ? "Please select at least one bucket on which to configure Lifecycle"
   297                              : "Use the Select Multiple Buckets button to choose buckets on which to configure Lifecycle"
   298                            : "Set Lifecycle"
   299                    }
   300                  >
   301                    <Button
   302                      id={"set-lifecycle"}
   303                      onClick={() => {
   304                        setLifecycleModalOpen(true);
   305                      }}
   306                      icon={<LifecycleConfigIcon />}
   307                      variant={"regular"}
   308                      disabled={selectedBuckets.length === 0 || !canPutLifecycle}
   309                    />
   310                  </TooltipWrapper>
   311  
   312                  <TooltipWrapper
   313                    tooltip={
   314                      !hasBuckets
   315                        ? ""
   316                        : selectedBuckets.length === 0
   317                          ? bulkSelect
   318                            ? "Please select at least one bucket on which to configure Replication"
   319                            : "Use the Select Multiple Buckets button to choose buckets on which to configure Replication"
   320                          : "Set Replication"
   321                    }
   322                  >
   323                    <Button
   324                      id={"set-replication"}
   325                      onClick={() => {
   326                        setReplicationModalOpen(true);
   327                      }}
   328                      icon={<MultipleBucketsIcon />}
   329                      variant={"regular"}
   330                      disabled={selectedBuckets.length === 0}
   331                    />
   332                  </TooltipWrapper>
   333                </Fragment>
   334              )}
   335  
   336              <TooltipWrapper tooltip={"Refresh"}>
   337                <Button
   338                  id={"refresh-buckets"}
   339                  onClick={() => {
   340                    setLoading(true);
   341                  }}
   342                  icon={<RefreshIcon />}
   343                  variant={"regular"}
   344                />
   345              </TooltipWrapper>
   346  
   347              {!obOnly && (
   348                <TooltipWrapper
   349                  tooltip={
   350                    canCreateBucket
   351                      ? ""
   352                      : permissionTooltipHelper(
   353                          [IAM_SCOPES.S3_CREATE_BUCKET],
   354                          "create a bucket",
   355                        )
   356                  }
   357                >
   358                  <Button
   359                    id={"create-bucket"}
   360                    onClick={() => {
   361                      navigate(IAM_PAGES.ADD_BUCKETS);
   362                    }}
   363                    icon={<AddIcon />}
   364                    variant={"callAction"}
   365                    disabled={!canCreateBucket}
   366                    label={"Create Bucket"}
   367                  />
   368                </TooltipWrapper>
   369              )}
   370            </Grid>
   371          </Grid>
   372  
   373          {loading && <ProgressBar />}
   374          {!loading && (
   375            <Grid
   376              item
   377              xs={12}
   378              sx={{
   379                marginTop: 25,
   380                height: "calc(100vh - 211px)",
   381                "&.isEmbedded": {
   382                  height: "calc(100vh - 128px)",
   383                },
   384              }}
   385              className={obOnly ? "isEmbedded" : ""}
   386            >
   387              {filteredRecords.length !== 0 && (
   388                <VirtualizedList
   389                  rowRenderFunction={renderItemLine}
   390                  totalItems={filteredRecords.length}
   391                />
   392              )}
   393              {filteredRecords.length === 0 && filterBuckets !== "" && (
   394                <Grid container>
   395                  <Grid item xs={8}>
   396                    <HelpBox
   397                      iconComponent={<BucketsIcon />}
   398                      title={"No Results"}
   399                      help={
   400                        <Fragment>
   401                          No buckets match the filtering condition
   402                        </Fragment>
   403                      }
   404                    />
   405                  </Grid>
   406                </Grid>
   407              )}
   408              {!hasBuckets && (
   409                <Grid container>
   410                  <Grid item xs={8}>
   411                    <HelpBox
   412                      iconComponent={<BucketsIcon />}
   413                      title={"Buckets"}
   414                      help={
   415                        <Fragment>
   416                          MinIO uses buckets to organize objects. A bucket is
   417                          similar to a folder or directory in a filesystem, where
   418                          each bucket can hold an arbitrary number of objects.
   419                          <br />
   420                          {canListBuckets ? (
   421                            ""
   422                          ) : (
   423                            <Fragment>
   424                              <br />
   425                              {permissionTooltipHelper(
   426                                [
   427                                  IAM_SCOPES.S3_LIST_BUCKET,
   428                                  IAM_SCOPES.S3_ALL_LIST_BUCKET,
   429                                ],
   430                                "view the buckets on this server",
   431                              )}
   432                              <br />
   433                            </Fragment>
   434                          )}
   435                          <SecureComponent
   436                            scopes={[IAM_SCOPES.S3_CREATE_BUCKET]}
   437                            resource={CONSOLE_UI_RESOURCE}
   438                          >
   439                            <br />
   440                            To get started,&nbsp;
   441                            <ActionLink
   442                              onClick={() => {
   443                                navigate(IAM_PAGES.ADD_BUCKETS);
   444                              }}
   445                            >
   446                              Create a Bucket.
   447                            </ActionLink>
   448                          </SecureComponent>
   449                        </Fragment>
   450                      }
   451                    />
   452                  </Grid>
   453                </Grid>
   454              )}
   455            </Grid>
   456          )}
   457        </PageLayout>
   458      </Fragment>
   459    );
   460  };
   461  
   462  export default ListBuckets;