github.com/minio/console@v1.4.1/web-app/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.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 { useSelector } from "react-redux";
    19  import CopyToClipboard from "react-copy-to-clipboard";
    20  import styled from "styled-components";
    21  import { Link, useNavigate } from "react-router-dom";
    22  import { encodeURLString, safeDecodeURIComponent } from "../../../common/utils";
    23  import {
    24    Button,
    25    CopyIcon,
    26    NewPathIcon,
    27    Tooltip,
    28    Breadcrumbs,
    29    breakPoints,
    30    Box,
    31  } from "mds";
    32  import { hasPermission } from "../../../common/SecureComponent";
    33  import {
    34    IAM_SCOPES,
    35    permissionTooltipHelper,
    36  } from "../../../common/SecureComponent/permissions";
    37  import withSuspense from "../Common/Components/withSuspense";
    38  import { setSnackBarMessage } from "../../../systemSlice";
    39  import { AppState, useAppDispatch } from "../../../store";
    40  import { setVersionsModeEnabled } from "./objectBrowserSlice";
    41  import { getSessionGrantsWildCard } from "../Buckets/ListBuckets/UploadPermissionUtils";
    42  
    43  const CreatePathModal = withSuspense(
    44    React.lazy(
    45      () => import("../Buckets/ListBuckets/Objects/ListObjects/CreatePathModal"),
    46    ),
    47  );
    48  
    49  const BreadcrumbsMain = styled.div(() => ({
    50    display: "flex",
    51    "& .additionalOptions": {
    52      paddingRight: "10px",
    53      display: "flex",
    54      alignItems: "center",
    55      [`@media (max-width: ${breakPoints.lg}px)`]: {
    56        display: "none",
    57      },
    58    },
    59    "& .slashSpacingStyle": {
    60      margin: "0 5px",
    61    },
    62  }));
    63  
    64  interface IObjectBrowser {
    65    bucketName: string;
    66    internalPaths: string;
    67    hidePathButton?: boolean;
    68    additionalOptions?: React.ReactNode;
    69  }
    70  
    71  const BrowserBreadcrumbs = ({
    72    bucketName,
    73    internalPaths,
    74    hidePathButton,
    75    additionalOptions,
    76  }: IObjectBrowser) => {
    77    const dispatch = useAppDispatch();
    78    const navigate = useNavigate();
    79  
    80    const rewindEnabled = useSelector(
    81      (state: AppState) => state.objectBrowser.rewind.rewindEnabled,
    82    );
    83    const versionsMode = useSelector(
    84      (state: AppState) => state.objectBrowser.versionsMode,
    85    );
    86    const versionedFile = useSelector(
    87      (state: AppState) => state.objectBrowser.versionedFile,
    88    );
    89    const anonymousMode = useSelector(
    90      (state: AppState) => state.system.anonymousMode,
    91    );
    92  
    93    const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
    94    const [canCreateSubpath, setCanCreateSubpath] = useState<boolean>(false);
    95  
    96    const putObjectPermScopes = [
    97      IAM_SCOPES.S3_PUT_OBJECT,
    98      IAM_SCOPES.S3_PUT_ACTIONS,
    99    ];
   100  
   101    const sessionGrants = useSelector((state: AppState) =>
   102      state.console.session ? state.console.session.permissions || {} : {},
   103    );
   104  
   105    let paths = internalPaths;
   106  
   107    if (internalPaths !== "") {
   108      paths = `/${internalPaths}`;
   109    }
   110  
   111    const splitPaths = paths.split("/").filter((path) => path !== "");
   112    const lastBreadcrumbsIndex = splitPaths.length - 1;
   113  
   114    const pathToCheckPerms = bucketName + paths || bucketName;
   115    const sessionGrantWildCards = getSessionGrantsWildCard(
   116      sessionGrants,
   117      pathToCheckPerms,
   118      putObjectPermScopes,
   119    );
   120  
   121    useEffect(() => {
   122      setCanCreateSubpath(false);
   123      Object.keys(sessionGrants).forEach((grant) => {
   124        grant.includes(pathToCheckPerms) &&
   125          grant.includes("/*") &&
   126          setCanCreateSubpath(true);
   127      });
   128    }, [pathToCheckPerms, internalPaths, sessionGrants]);
   129  
   130    const canCreatePath =
   131      hasPermission(
   132        [pathToCheckPerms, ...sessionGrantWildCards],
   133        putObjectPermScopes,
   134      ) ||
   135      anonymousMode ||
   136      canCreateSubpath;
   137  
   138    let breadcrumbsMap = splitPaths.map((objectItem: string, index: number) => {
   139      const subSplit = `${splitPaths.slice(0, index + 1).join("/")}/`;
   140      const route = `/browser/${bucketName}/${
   141        subSplit ? `${encodeURLString(subSplit)}` : ``
   142      }`;
   143  
   144      if (index === lastBreadcrumbsIndex && objectItem === versionedFile) {
   145        return null;
   146      }
   147  
   148      return (
   149        <Fragment key={`breadcrumbs-${index.toString()}`}>
   150          <span className={"slashSpacingStyle"}>/</span>
   151          {index === lastBreadcrumbsIndex ? (
   152            <span style={{ cursor: "default", whiteSpace: "pre" }}>
   153              {safeDecodeURIComponent(objectItem) /*Only for display*/}
   154            </span>
   155          ) : (
   156            <Link
   157              style={{
   158                whiteSpace: "pre",
   159              }}
   160              to={route}
   161              onClick={() => {
   162                dispatch(
   163                  setVersionsModeEnabled({ status: false, objectName: "" }),
   164                );
   165              }}
   166            >
   167              {
   168                safeDecodeURIComponent(
   169                  objectItem,
   170                ) /*Only for display to preserve */
   171              }
   172            </Link>
   173          )}
   174        </Fragment>
   175      );
   176    });
   177  
   178    let versionsItem: any[] = [];
   179  
   180    if (versionsMode) {
   181      versionsItem = [
   182        <Fragment key={`breadcrumbs-versionedItem`}>
   183          <span>
   184            <span className={"slashSpacingStyle"}>/</span>
   185            {versionedFile} - Versions
   186          </span>
   187        </Fragment>,
   188      ];
   189    }
   190  
   191    const listBreadcrumbs: any[] = [
   192      <Fragment key={`breadcrumbs-root-path`}>
   193        <Link
   194          to={`/browser/${bucketName}`}
   195          onClick={() => {
   196            dispatch(setVersionsModeEnabled({ status: false, objectName: "" }));
   197          }}
   198        >
   199          {bucketName}
   200        </Link>
   201      </Fragment>,
   202      ...breadcrumbsMap,
   203      ...versionsItem,
   204    ];
   205  
   206    const closeAddFolderModal = () => {
   207      setCreateFolderOpen(false);
   208    };
   209  
   210    const goBackFunction = () => {
   211      if (versionsMode) {
   212        dispatch(setVersionsModeEnabled({ status: false, objectName: "" }));
   213      } else {
   214        if (splitPaths.length === 0) {
   215          navigate("/browser");
   216  
   217          return;
   218        }
   219  
   220        const prevPath = splitPaths.slice(0, -1);
   221  
   222        navigate(
   223          `/browser/${bucketName}${
   224            prevPath.length > 0
   225              ? `/${encodeURLString(`${prevPath.join("/")}/`)}`
   226              : ""
   227          }`,
   228        );
   229      }
   230    };
   231  
   232    return (
   233      <Fragment>
   234        <BreadcrumbsMain>
   235          {createFolderOpen && (
   236            <CreatePathModal
   237              modalOpen={createFolderOpen}
   238              bucketName={bucketName}
   239              folderName={internalPaths}
   240              onClose={closeAddFolderModal}
   241              limitedSubPath={
   242                canCreateSubpath &&
   243                !(
   244                  hasPermission(
   245                    [pathToCheckPerms, ...sessionGrantWildCards],
   246                    putObjectPermScopes,
   247                  ) || anonymousMode
   248                )
   249              }
   250            />
   251          )}
   252          <Breadcrumbs
   253            sx={{
   254              whiteSpace: "pre",
   255            }}
   256            goBackFunction={goBackFunction}
   257            additionalOptions={
   258              <Fragment>
   259                <CopyToClipboard text={`${bucketName}/${splitPaths.join("/")}`}>
   260                  <Button
   261                    id={"copy-path"}
   262                    icon={
   263                      <CopyIcon
   264                        style={{
   265                          width: "12px",
   266                          height: "12px",
   267                          fill: "#969FA8",
   268                          marginTop: -1,
   269                        }}
   270                      />
   271                    }
   272                    variant={"regular"}
   273                    onClick={() => {
   274                      dispatch(setSnackBarMessage("Path copied to clipboard"));
   275                    }}
   276                    style={{
   277                      width: "28px",
   278                      height: "28px",
   279                      color: "#969FA8",
   280                      border: "#969FA8 1px solid",
   281                      marginRight: 5,
   282                    }}
   283                  />
   284                </CopyToClipboard>
   285                <Box className={"additionalOptions"}>{additionalOptions}</Box>
   286              </Fragment>
   287            }
   288          >
   289            {listBreadcrumbs}
   290          </Breadcrumbs>
   291          {!hidePathButton && (
   292            <Tooltip
   293              tooltip={
   294                canCreatePath
   295                  ? "Choose or create a new path"
   296                  : permissionTooltipHelper(
   297                      [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
   298                      "create a new path",
   299                    )
   300              }
   301            >
   302              <Button
   303                id={"new-path"}
   304                onClick={() => {
   305                  setCreateFolderOpen(true);
   306                }}
   307                disabled={anonymousMode ? false : rewindEnabled || !canCreatePath}
   308                icon={<NewPathIcon style={{ fill: "#969FA8" }} />}
   309                style={{
   310                  whiteSpace: "nowrap",
   311                }}
   312                variant={"regular"}
   313                label={"Create new path"}
   314              />
   315            </Tooltip>
   316          )}
   317        </BreadcrumbsMain>
   318        <Box
   319          sx={{
   320            display: "none",
   321            marginTop: 15,
   322            marginBottom: 5,
   323            justifyContent: "flex-start",
   324            "& > div": {
   325              fontSize: 12,
   326              fontWeight: "normal",
   327              flexDirection: "row",
   328              flexWrap: "nowrap",
   329            },
   330            [`@media (max-width: ${breakPoints.lg}px)`]: {
   331              display: "flex",
   332            },
   333          }}
   334        >
   335          {additionalOptions}
   336        </Box>
   337      </Fragment>
   338    );
   339  };
   340  
   341  export default BrowserBreadcrumbs;