github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/BucketDetails/BucketDetails.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 {
    19    Navigate,
    20    Route,
    21    Routes,
    22    useLocation,
    23    useNavigate,
    24    useParams,
    25  } from "react-router-dom";
    26  import {
    27    BackLink,
    28    Box,
    29    BucketsIcon,
    30    Button,
    31    FolderIcon,
    32    PageLayout,
    33    RefreshIcon,
    34    ScreenTitle,
    35    Tabs,
    36    TrashIcon,
    37  } from "mds";
    38  import { useSelector } from "react-redux";
    39  import {
    40    browseBucketPermissions,
    41    deleteBucketPermissions,
    42    IAM_PERMISSIONS,
    43    IAM_ROLES,
    44    IAM_SCOPES,
    45    permissionTooltipHelper,
    46  } from "../../../../common/SecureComponent/permissions";
    47  
    48  import {
    49    hasPermission,
    50    SecureComponent,
    51  } from "../../../../common/SecureComponent";
    52  
    53  import withSuspense from "../../Common/Components/withSuspense";
    54  import {
    55    selDistSet,
    56    selSiteRep,
    57    setErrorSnackMessage,
    58    setHelpName,
    59  } from "../../../../systemSlice";
    60  import {
    61    selBucketDetailsInfo,
    62    selBucketDetailsLoading,
    63    setBucketDetailsLoad,
    64    setBucketInfo,
    65  } from "./bucketDetailsSlice";
    66  import { useAppDispatch } from "../../../../store";
    67  import TooltipWrapper from "../../Common/TooltipWrapper/TooltipWrapper";
    68  import PageHeaderWrapper from "../../Common/PageHeaderWrapper/PageHeaderWrapper";
    69  import { api } from "api";
    70  import { errorToHandler } from "api/errors";
    71  import HelpMenu from "../../HelpMenu";
    72  
    73  const DeleteBucket = withSuspense(
    74    React.lazy(() => import("../ListBuckets/DeleteBucket")),
    75  );
    76  const AccessRulePanel = withSuspense(
    77    React.lazy(() => import("./AccessRulePanel")),
    78  );
    79  const AccessDetailsPanel = withSuspense(
    80    React.lazy(() => import("./AccessDetailsPanel")),
    81  );
    82  const BucketSummaryPanel = withSuspense(
    83    React.lazy(() => import("./BucketSummaryPanel")),
    84  );
    85  const BucketEventsPanel = withSuspense(
    86    React.lazy(() => import("./BucketEventsPanel")),
    87  );
    88  const BucketReplicationPanel = withSuspense(
    89    React.lazy(() => import("./BucketReplicationPanel")),
    90  );
    91  const BucketLifecyclePanel = withSuspense(
    92    React.lazy(() => import("./BucketLifecyclePanel")),
    93  );
    94  
    95  const BucketDetails = () => {
    96    const dispatch = useAppDispatch();
    97    const navigate = useNavigate();
    98    const params = useParams();
    99    const location = useLocation();
   100  
   101    const distributedSetup = useSelector(selDistSet);
   102    const loadingBucket = useSelector(selBucketDetailsLoading);
   103    const bucketInfo = useSelector(selBucketDetailsInfo);
   104    const siteReplicationInfo = useSelector(selSiteRep);
   105  
   106    const [iniLoad, setIniLoad] = useState<boolean>(false);
   107    const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
   108    const bucketName = params.bucketName || "";
   109  
   110    const canDelete = hasPermission(bucketName, deleteBucketPermissions);
   111    const canBrowse = hasPermission(bucketName, browseBucketPermissions);
   112  
   113    useEffect(() => {
   114      dispatch(setHelpName("bucket_details"));
   115      // eslint-disable-next-line react-hooks/exhaustive-deps
   116    }, []);
   117  
   118    useEffect(() => {
   119      if (!iniLoad) {
   120        dispatch(setBucketDetailsLoad(true));
   121        setIniLoad(true);
   122      }
   123    }, [iniLoad, dispatch, setIniLoad]);
   124  
   125    useEffect(() => {
   126      if (loadingBucket) {
   127        api.buckets
   128          .bucketInfo(bucketName)
   129          .then((res) => {
   130            dispatch(setBucketDetailsLoad(false));
   131            dispatch(setBucketInfo(res.data));
   132          })
   133          .catch((err) => {
   134            dispatch(setBucketDetailsLoad(false));
   135            dispatch(setErrorSnackMessage(errorToHandler(err)));
   136          });
   137      }
   138    }, [bucketName, loadingBucket, dispatch]);
   139  
   140    let topLevelRoute = `/buckets/${bucketName}`;
   141    const defaultRoute = "/admin/summary";
   142  
   143    const manageBucketRoutes: Record<string, any> = {
   144      events: "/admin/events",
   145      replication: "/admin/replication",
   146      lifecycle: "/admin/lifecycle",
   147      access: "/admin/access",
   148      prefix: "/admin/prefix",
   149    };
   150  
   151    const getRoutePath = (routeKey: string) => {
   152      let path = manageBucketRoutes[routeKey];
   153      if (!path) {
   154        path = `${topLevelRoute}${defaultRoute}`;
   155      } else {
   156        path = `${topLevelRoute}${path}`;
   157      }
   158      return path;
   159    };
   160  
   161    const closeDeleteModalAndRefresh = (refresh: boolean) => {
   162      setDeleteOpen(false);
   163      if (refresh) {
   164        navigate("/buckets");
   165      }
   166    };
   167  
   168    return (
   169      <Fragment>
   170        {deleteOpen && (
   171          <DeleteBucket
   172            deleteOpen={deleteOpen}
   173            selectedBucket={bucketName}
   174            closeDeleteModalAndRefresh={(refresh: boolean) => {
   175              closeDeleteModalAndRefresh(refresh);
   176            }}
   177          />
   178        )}
   179        <PageHeaderWrapper
   180          label={
   181            <BackLink label={"Buckets"} onClick={() => navigate("/buckets")} />
   182          }
   183          actions={
   184            <Fragment>
   185              <TooltipWrapper
   186                tooltip={
   187                  canBrowse
   188                    ? "Browse Bucket"
   189                    : permissionTooltipHelper(
   190                        IAM_PERMISSIONS[IAM_ROLES.BUCKET_VIEWER],
   191                        "browsing this bucket",
   192                      )
   193                }
   194              >
   195                <Button
   196                  id={"switch-browse-view"}
   197                  aria-label="Browse Bucket"
   198                  onClick={() => {
   199                    navigate(`/browser/${bucketName}`);
   200                  }}
   201                  icon={
   202                    <FolderIcon
   203                      style={{ width: 20, height: 20, marginTop: -3 }}
   204                    />
   205                  }
   206                  style={{
   207                    padding: "0 10px",
   208                  }}
   209                  disabled={!canBrowse}
   210                />
   211              </TooltipWrapper>
   212              <HelpMenu />
   213            </Fragment>
   214          }
   215        />
   216        <PageLayout>
   217          <ScreenTitle
   218            icon={
   219              <Fragment>
   220                <BucketsIcon width={40} />
   221              </Fragment>
   222            }
   223            title={bucketName}
   224            subTitle={
   225              <SecureComponent
   226                scopes={[
   227                  IAM_SCOPES.S3_GET_BUCKET_POLICY,
   228                  IAM_SCOPES.S3_GET_ACTIONS,
   229                ]}
   230                resource={bucketName}
   231              >
   232                <span style={{ fontSize: 15 }}>Access: </span>
   233                <span
   234                  style={{
   235                    fontWeight: 600,
   236                    fontSize: 15,
   237                    textTransform: "capitalize",
   238                  }}
   239                >
   240                  {bucketInfo?.access?.toLowerCase()}
   241                </span>
   242              </SecureComponent>
   243            }
   244            actions={
   245              <Fragment>
   246                <SecureComponent
   247                  scopes={deleteBucketPermissions}
   248                  resource={bucketName}
   249                  errorProps={{ disabled: true }}
   250                >
   251                  <TooltipWrapper
   252                    tooltip={
   253                      canDelete
   254                        ? ""
   255                        : permissionTooltipHelper(
   256                            [
   257                              IAM_SCOPES.S3_DELETE_BUCKET,
   258                              IAM_SCOPES.S3_FORCE_DELETE_BUCKET,
   259                            ],
   260                            "deleting this bucket",
   261                          )
   262                    }
   263                  >
   264                    <Button
   265                      id={"delete-bucket-button"}
   266                      onClick={() => {
   267                        setDeleteOpen(true);
   268                      }}
   269                      label={"Delete Bucket"}
   270                      icon={<TrashIcon />}
   271                      variant={"secondary"}
   272                      disabled={!canDelete}
   273                    />
   274                  </TooltipWrapper>
   275                </SecureComponent>
   276                <Button
   277                  id={"refresh-bucket-info"}
   278                  onClick={() => {
   279                    dispatch(setBucketDetailsLoad(true));
   280                  }}
   281                  label={"Refresh"}
   282                  icon={<RefreshIcon />}
   283                />
   284              </Fragment>
   285            }
   286            sx={{ marginBottom: 15 }}
   287          />
   288          <Box>
   289            <Tabs
   290              currentTabOrPath={location.pathname}
   291              useRouteTabs
   292              onTabClick={(tab) => {
   293                navigate(tab);
   294              }}
   295              options={[
   296                {
   297                  tabConfig: {
   298                    label: "Summary",
   299                    id: "summary",
   300                    to: getRoutePath("summary"),
   301                  },
   302                },
   303                {
   304                  tabConfig: {
   305                    label: "Events",
   306                    id: "events",
   307                    disabled: !hasPermission(bucketName, [
   308                      IAM_SCOPES.S3_GET_BUCKET_NOTIFICATIONS,
   309                      IAM_SCOPES.S3_PUT_BUCKET_NOTIFICATIONS,
   310                      IAM_SCOPES.S3_GET_ACTIONS,
   311                      IAM_SCOPES.S3_PUT_ACTIONS,
   312                    ]),
   313                    to: getRoutePath("events"),
   314                  },
   315                },
   316                {
   317                  tabConfig: {
   318                    label: "Replication",
   319                    id: "replication",
   320                    disabled:
   321                      !distributedSetup ||
   322                      (siteReplicationInfo.enabled &&
   323                        siteReplicationInfo.curSite) ||
   324                      !hasPermission(bucketName, [
   325                        IAM_SCOPES.S3_GET_REPLICATION_CONFIGURATION,
   326                        IAM_SCOPES.S3_PUT_REPLICATION_CONFIGURATION,
   327                        IAM_SCOPES.S3_GET_ACTIONS,
   328                        IAM_SCOPES.S3_PUT_ACTIONS,
   329                      ]),
   330                    to: getRoutePath("replication"),
   331                  },
   332                },
   333                {
   334                  tabConfig: {
   335                    label: "Lifecycle",
   336                    id: "lifecycle",
   337                    disabled:
   338                      !distributedSetup ||
   339                      !hasPermission(bucketName, [
   340                        IAM_SCOPES.S3_GET_LIFECYCLE_CONFIGURATION,
   341                        IAM_SCOPES.S3_PUT_LIFECYCLE_CONFIGURATION,
   342                        IAM_SCOPES.S3_GET_ACTIONS,
   343                        IAM_SCOPES.S3_PUT_ACTIONS,
   344                      ]),
   345                    to: getRoutePath("lifecycle"),
   346                  },
   347                },
   348                {
   349                  tabConfig: {
   350                    label: "Access",
   351                    id: "access",
   352                    disabled: !hasPermission(bucketName, [
   353                      IAM_SCOPES.ADMIN_GET_POLICY,
   354                      IAM_SCOPES.ADMIN_LIST_USER_POLICIES,
   355                      IAM_SCOPES.ADMIN_LIST_USERS,
   356                    ]),
   357                    to: getRoutePath("access"),
   358                  },
   359                },
   360                {
   361                  tabConfig: {
   362                    label: "Anonymous",
   363                    id: "anonymous",
   364                    disabled: !hasPermission(bucketName, [
   365                      IAM_SCOPES.S3_GET_BUCKET_POLICY,
   366                      IAM_SCOPES.S3_GET_ACTIONS,
   367                    ]),
   368                    to: getRoutePath("prefix"),
   369                  },
   370                },
   371              ]}
   372              routes={
   373                <Routes>
   374                  <Route path="summary" element={<BucketSummaryPanel />} />
   375                  <Route path="events" element={<BucketEventsPanel />} />
   376                  {distributedSetup && (
   377                    <Route
   378                      path="replication"
   379                      element={<BucketReplicationPanel />}
   380                    />
   381                  )}
   382                  {distributedSetup && (
   383                    <Route path="lifecycle" element={<BucketLifecyclePanel />} />
   384                  )}
   385  
   386                  <Route path="access" element={<AccessDetailsPanel />} />
   387                  <Route path="prefix" element={<AccessRulePanel />} />
   388                  <Route
   389                    path="*"
   390                    element={
   391                      <Navigate to={`/buckets/${bucketName}/admin/summary`} />
   392                    }
   393                  />
   394                </Routes>
   395              }
   396            />
   397          </Box>
   398        </PageLayout>
   399      </Fragment>
   400    );
   401  };
   402  
   403  export default BucketDetails;