github.com/minio/console@v1.4.1/web-app/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.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, {
    18    Fragment,
    19    useCallback,
    20    useEffect,
    21    useMemo,
    22    useRef,
    23    useState,
    24  } from "react";
    25  import get from "lodash/get";
    26  import {
    27    AccessRuleIcon,
    28    ActionsList,
    29    Badge,
    30    Box,
    31    BucketsIcon,
    32    Button,
    33    Checkbox,
    34    DeleteIcon,
    35    DownloadIcon,
    36    Grid,
    37    HistoryIcon,
    38    PageLayout,
    39    PreviewIcon,
    40    RefreshIcon,
    41    ScreenTitle,
    42    ShareIcon,
    43  } from "mds";
    44  import { api } from "api";
    45  import { errorToHandler } from "api/errors";
    46  import { BucketQuota } from "api/consoleApi";
    47  import { useSelector } from "react-redux";
    48  import { useLocation, useNavigate, useParams } from "react-router-dom";
    49  import { useDropzone } from "react-dropzone";
    50  import { DateTime } from "luxon";
    51  import {
    52    decodeURLString,
    53    encodeURLString,
    54    niceBytesInt,
    55  } from "../../../../../../common/utils";
    56  import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
    57  import { AllowedPreviews, previewObjectType } from "../utils";
    58  import { ErrorResponseHandler } from "../../../../../../common/types";
    59  import { AppState, useAppDispatch } from "../../../../../../store";
    60  import {
    61    IAM_SCOPES,
    62    permissionTooltipHelper,
    63  } from "../../../../../../common/SecureComponent/permissions";
    64  import {
    65    hasPermission,
    66    SecureComponent,
    67  } from "../../../../../../common/SecureComponent";
    68  import {
    69    setErrorSnackMessage,
    70    setSnackBarMessage,
    71  } from "../../../../../../systemSlice";
    72  import { isVersionedMode } from "../../../../../../utils/validationFunctions";
    73  import {
    74    extractFileExtn,
    75    getPolicyAllowedFileExtensions,
    76    getSessionGrantsWildCard,
    77  } from "../../UploadPermissionUtils";
    78  import {
    79    makeid,
    80    removeTrace,
    81    storeCallForObjectWithID,
    82    storeFormDataWithID,
    83  } from "../../../../ObjectBrowser/transferManager";
    84  import {
    85    cancelObjectInList,
    86    completeObject,
    87    failObject,
    88    openList,
    89    resetMessages,
    90    resetRewind,
    91    setAnonymousAccessOpen,
    92    setDownloadRenameModal,
    93    setLoadingVersions,
    94    setNewObject,
    95    setObjectDetailsView,
    96    setPreviewOpen,
    97    setReloadObjectsList,
    98    setRetentionConfig,
    99    setSelectedObjects,
   100    setSelectedObjectView,
   101    setSelectedPreview,
   102    setShareFileModalOpen,
   103    setShowDeletedObjects,
   104    setVersionsModeEnabled,
   105    updateProgress,
   106  } from "../../../../ObjectBrowser/objectBrowserSlice";
   107  import {
   108    selBucketDetailsInfo,
   109    selBucketDetailsLoading,
   110    setBucketDetailsLoad,
   111    setBucketInfo,
   112  } from "../../../BucketDetails/bucketDetailsSlice";
   113  import {
   114    downloadSelected,
   115    openAnonymousAccess,
   116    openPreview,
   117    openShare,
   118  } from "../../../../ObjectBrowser/objectBrowserThunks";
   119  import withSuspense from "../../../../Common/Components/withSuspense";
   120  import UploadFilesButton from "../../UploadFilesButton";
   121  import DetailsListPanel from "./DetailsListPanel";
   122  import ObjectDetailPanel from "./ObjectDetailPanel";
   123  import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
   124  import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename";
   125  import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper";
   126  import ListObjectsTable from "./ListObjectsTable";
   127  import FilterObjectsSB from "../../../../ObjectBrowser/FilterObjectsSB";
   128  import AddAccessRule from "../../../BucketDetails/AddAccessRule";
   129  
   130  const DeleteMultipleObjects = withSuspense(
   131    React.lazy(() => import("./DeleteMultipleObjects")),
   132  );
   133  const ShareFile = withSuspense(
   134    React.lazy(() => import("../ObjectDetails/ShareFile")),
   135  );
   136  const RewindEnable = withSuspense(React.lazy(() => import("./RewindEnable")));
   137  const PreviewFileModal = withSuspense(
   138    React.lazy(() => import("../Preview/PreviewFileModal")),
   139  );
   140  
   141  const baseDnDStyle = {
   142    borderWidth: 2,
   143    borderRadius: 2,
   144    borderColor: "transparent",
   145    outline: "none",
   146  };
   147  
   148  const activeDnDStyle = {
   149    borderStyle: "dashed",
   150    backgroundColor: "transparent",
   151    borderColor: "#2196f3",
   152  };
   153  
   154  const acceptDnDStyle = {
   155    borderStyle: "dashed",
   156    backgroundColor: "transparent",
   157    borderColor: "#00e676",
   158  };
   159  
   160  const ListObjects = () => {
   161    const dispatch = useAppDispatch();
   162    const params = useParams();
   163    const navigate = useNavigate();
   164    const location = useLocation();
   165  
   166    const rewindEnabled = useSelector(
   167      (state: AppState) => state.objectBrowser.rewind.rewindEnabled,
   168    );
   169    const bucketToRewind = useSelector(
   170      (state: AppState) => state.objectBrowser.rewind.bucketToRewind,
   171    );
   172    const versionsMode = useSelector(
   173      (state: AppState) => state.objectBrowser.versionsMode,
   174    );
   175    const showDeleted = useSelector(
   176      (state: AppState) => state.objectBrowser.showDeleted,
   177    );
   178    const detailsOpen = useSelector(
   179      (state: AppState) => state.objectBrowser.objectDetailsOpen,
   180    );
   181    const selectedInternalPaths = useSelector(
   182      (state: AppState) => state.objectBrowser.selectedInternalPaths,
   183    );
   184    const requestInProgress = useSelector(
   185      (state: AppState) => state.objectBrowser.requestInProgress,
   186    );
   187    const simplePath = useSelector(
   188      (state: AppState) => state.objectBrowser.simplePath,
   189    );
   190    const versioningConfig = useSelector(
   191      (state: AppState) => state.objectBrowser.versionInfo,
   192    );
   193    const lockingEnabled = useSelector(
   194      (state: AppState) => state.objectBrowser.lockingEnabled,
   195    );
   196    const downloadRenameModal = useSelector(
   197      (state: AppState) => state.objectBrowser.downloadRenameModal,
   198    );
   199    const selectedPreview = useSelector(
   200      (state: AppState) => state.objectBrowser.selectedPreview,
   201    );
   202    const shareFileModalOpen = useSelector(
   203      (state: AppState) => state.objectBrowser.shareFileModalOpen,
   204    );
   205    const previewOpen = useSelector(
   206      (state: AppState) => state.objectBrowser.previewOpen,
   207    );
   208    const selectedBucket = useSelector(
   209      (state: AppState) => state.objectBrowser.selectedBucket,
   210    );
   211    const anonymousMode = useSelector(
   212      (state: AppState) => state.system.anonymousMode,
   213    );
   214    const anonymousAccessOpen = useSelector(
   215      (state: AppState) => state.objectBrowser.anonymousAccessOpen,
   216    );
   217  
   218    const records = useSelector(
   219      (state: AppState) => state.objectBrowser?.records || [],
   220    );
   221  
   222    const loadingBucket = useSelector(selBucketDetailsLoading);
   223    const bucketInfo = useSelector(selBucketDetailsInfo);
   224  
   225    const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
   226    const [rewindSelect, setRewindSelect] = useState<boolean>(false);
   227    const [iniLoad, setIniLoad] = useState<boolean>(false);
   228    const [canShareFile, setCanShareFile] = useState<boolean>(false);
   229    const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
   230    const [quota, setQuota] = useState<BucketQuota | null>(null);
   231    const [metaData, setMetaData] = useState<any>(null);
   232    const [isMetaDataLoaded, setIsMetaDataLoaded] = useState(false);
   233  
   234    const isVersioningApplied = isVersionedMode(versioningConfig.status);
   235  
   236    const bucketName = params.bucketName || "";
   237    const pathSegment = location.pathname.split(`/browser/${bucketName}/`);
   238    const internalPaths = pathSegment.length === 2 ? pathSegment[1] : "";
   239  
   240    const pageTitle = decodeURLString(internalPaths);
   241    const currentPath = pageTitle.split("/").filter((i: string) => i !== "");
   242  
   243    let uploadPath = [bucketName];
   244    if (currentPath.length > 0) {
   245      uploadPath = uploadPath.concat(currentPath);
   246    }
   247  
   248    const fileUpload = useRef<HTMLInputElement>(null);
   249    const folderUpload = useRef<HTMLInputElement>(null);
   250  
   251    const sessionGrants = useSelector((state: AppState) =>
   252      state.console.session ? state.console.session.permissions || {} : {},
   253    );
   254  
   255    const putObjectPermScopes = [
   256      IAM_SCOPES.S3_PUT_OBJECT,
   257      IAM_SCOPES.S3_PUT_ACTIONS,
   258    ];
   259  
   260    const pathAsResourceInPolicy = uploadPath.join("/");
   261    const allowedFileExtensions = getPolicyAllowedFileExtensions(
   262      sessionGrants,
   263      pathAsResourceInPolicy,
   264      putObjectPermScopes,
   265    );
   266  
   267    const sessionGrantWildCards = getSessionGrantsWildCard(
   268      sessionGrants,
   269      pathAsResourceInPolicy,
   270      putObjectPermScopes,
   271    );
   272  
   273    const canDownload = hasPermission(
   274      [pathAsResourceInPolicy, ...sessionGrantWildCards],
   275      [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS],
   276    );
   277    const canRewind = hasPermission(bucketName, [
   278      IAM_SCOPES.S3_GET_OBJECT,
   279      IAM_SCOPES.S3_GET_ACTIONS,
   280      IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
   281    ]);
   282    const canDelete = hasPermission(
   283      [pathAsResourceInPolicy, ...sessionGrantWildCards],
   284      [IAM_SCOPES.S3_DELETE_OBJECT],
   285    );
   286    const canUpload =
   287      hasPermission(
   288        [pathAsResourceInPolicy, ...sessionGrantWildCards],
   289        putObjectPermScopes,
   290      ) || anonymousMode;
   291  
   292    const canSetAnonymousAccess = hasPermission(bucketName, [
   293      IAM_SCOPES.S3_GET_BUCKET_POLICY,
   294      IAM_SCOPES.S3_PUT_BUCKET_POLICY,
   295      IAM_SCOPES.S3_GET_ACTIONS,
   296      IAM_SCOPES.S3_PUT_ACTIONS,
   297    ]);
   298  
   299    const selectedObjects = useSelector(
   300      (state: AppState) => state.objectBrowser.selectedObjects,
   301    );
   302  
   303    const checkForDelMarker = (): boolean => {
   304      let isObjDelMarker = false;
   305      if (selectedObjects.length === 1) {
   306        let matchingRec = records.find((obj) => {
   307          return obj.name === `${selectedObjects[0]}` && obj.delete_flag;
   308        });
   309  
   310        isObjDelMarker = !!matchingRec;
   311      }
   312      return isObjDelMarker;
   313    };
   314  
   315    const isSelObjectDelMarker = checkForDelMarker();
   316  
   317    const fetchMetadata = useCallback(() => {
   318      const objectName = selectedObjects[0];
   319      const encodedPath = encodeURLString(objectName);
   320  
   321      if (!isMetaDataLoaded && encodedPath) {
   322        api.buckets
   323          .getObjectMetadata(bucketName, {
   324            prefix: encodedPath,
   325          })
   326          .then((res) => {
   327            let metadata = get(res.data, "objectMetadata", {});
   328            setIsMetaDataLoaded(true);
   329            setMetaData(metadata);
   330          })
   331          .catch((err) => {
   332            console.error(
   333              "Error Getting Metadata Status: ",
   334              err,
   335              err?.detailedError,
   336            );
   337            setIsMetaDataLoaded(true);
   338          });
   339      }
   340    }, [bucketName, selectedObjects, isMetaDataLoaded]);
   341  
   342    useEffect(() => {
   343      if (bucketName && !isSelObjectDelMarker) {
   344        fetchMetadata();
   345      }
   346    }, [bucketName, selectedObjects, fetchMetadata, isSelObjectDelMarker]);
   347  
   348    useEffect(() => {
   349      if (rewindEnabled) {
   350        if (bucketToRewind !== bucketName) {
   351          dispatch(resetRewind());
   352          return;
   353        }
   354      }
   355    }, [rewindEnabled, bucketToRewind, bucketName, dispatch]);
   356  
   357    useEffect(() => {
   358      if (folderUpload.current !== null) {
   359        folderUpload.current.setAttribute("directory", "");
   360        folderUpload.current.setAttribute("webkitdirectory", "");
   361      }
   362    }, [folderUpload]);
   363  
   364    useEffect(() => {
   365      if (selectedObjects.length === 1) {
   366        const objectName = selectedObjects[0];
   367        const isPrefix = objectName.endsWith("/");
   368  
   369        let objectType: AllowedPreviews = previewObjectType(metaData, objectName);
   370  
   371        if (objectType !== "none" && canDownload) {
   372          setCanPreviewFile(true);
   373        } else {
   374          setCanPreviewFile(false);
   375        }
   376  
   377        if (canDownload && !isPrefix) {
   378          setCanShareFile(true);
   379        } else {
   380          setCanShareFile(false);
   381        }
   382      } else {
   383        setCanShareFile(false);
   384        setCanPreviewFile(false);
   385      }
   386    }, [selectedObjects, canDownload, metaData]);
   387  
   388    useEffect(() => {
   389      if (!quota && !anonymousMode) {
   390        api.buckets
   391          .getBucketQuota(bucketName)
   392          .then((res) => {
   393            let quotaVals = null;
   394  
   395            if (res.data.quota) {
   396              quotaVals = res.data;
   397            }
   398  
   399            setQuota(quotaVals);
   400          })
   401          .catch((err) => {
   402            console.error(
   403              "Error Getting Quota Status: ",
   404              err.error.detailedMessage,
   405            );
   406            setQuota(null);
   407          });
   408      }
   409    }, [quota, bucketName, anonymousMode]);
   410  
   411    useEffect(() => {
   412      if (selectedObjects.length > 0) {
   413        dispatch(setObjectDetailsView(true));
   414        return;
   415      }
   416  
   417      if (
   418        selectedObjects.length === 0 &&
   419        selectedInternalPaths === null &&
   420        !requestInProgress
   421      ) {
   422        dispatch(setObjectDetailsView(false));
   423      }
   424    }, [selectedObjects, selectedInternalPaths, dispatch, requestInProgress]);
   425  
   426    useEffect(() => {
   427      if (!iniLoad) {
   428        dispatch(setBucketDetailsLoad(true));
   429        setIniLoad(true);
   430      }
   431    }, [iniLoad, dispatch, setIniLoad]);
   432  
   433    // bucket info
   434    useEffect(() => {
   435      if ((requestInProgress || loadingBucket) && !anonymousMode) {
   436        api.buckets
   437          .bucketInfo(bucketName)
   438          .then((res) => {
   439            dispatch(setBucketDetailsLoad(false));
   440            dispatch(setBucketInfo(res.data));
   441          })
   442          .catch((err) => {
   443            dispatch(setBucketDetailsLoad(false));
   444            dispatch(setErrorSnackMessage(errorToHandler(err)));
   445          });
   446      }
   447    }, [bucketName, loadingBucket, dispatch, anonymousMode, requestInProgress]);
   448  
   449    // Load retention Config
   450  
   451    useEffect(() => {
   452      if (selectedBucket !== "") {
   453        api.buckets
   454          .getBucketRetentionConfig(selectedBucket)
   455          .then((res) => {
   456            dispatch(setRetentionConfig(res.data));
   457          })
   458          .catch(() => {
   459            dispatch(setRetentionConfig(null));
   460          });
   461      }
   462    }, [selectedBucket, dispatch]);
   463  
   464    const closeDeleteMultipleModalAndRefresh = (refresh: boolean) => {
   465      setDeleteMultipleOpen(false);
   466  
   467      if (refresh) {
   468        dispatch(setSnackBarMessage(`Objects deleted successfully.`));
   469        dispatch(setSelectedObjects([]));
   470        dispatch(setReloadObjectsList(true));
   471      }
   472    };
   473  
   474    const handleUploadButton = (e: any) => {
   475      if (
   476        e === null ||
   477        e === undefined ||
   478        e.target.files === null ||
   479        e.target.files === undefined
   480      ) {
   481        return;
   482      }
   483      e.preventDefault();
   484      var newFiles: File[] = [];
   485  
   486      for (let i = 0; i < e.target.files.length; i++) {
   487        newFiles.push(e.target.files[i]);
   488      }
   489      uploadObject(newFiles, "");
   490  
   491      e.target.value = "";
   492    };
   493  
   494    const uploadObject = useCallback(
   495      (files: File[], folderPath: string): void => {
   496        let pathPrefix = "";
   497        if (simplePath) {
   498          pathPrefix = simplePath.endsWith("/") ? simplePath : simplePath + "/";
   499        }
   500  
   501        const upload = (
   502          files: File[],
   503          bucketName: string,
   504          path: string,
   505          folderPath: string,
   506        ) => {
   507          let uploadPromise = (file: File) => {
   508            return new Promise((resolve, reject) => {
   509              let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
   510              const fileName = file.name;
   511  
   512              const blobFile = new Blob([file], { type: file.type });
   513  
   514              let encodedPath = "";
   515  
   516              const filePath = get(file, "path", "");
   517              const fileWebkitRelativePath = get(file, "webkitRelativePath", "");
   518  
   519              let relativeFolderPath = folderPath;
   520              const ID = makeid(8);
   521  
   522              // File was uploaded via drag & drop
   523              if (filePath !== "") {
   524                relativeFolderPath = filePath;
   525              } else if (fileWebkitRelativePath !== "") {
   526                // File was uploaded using upload button
   527                relativeFolderPath = fileWebkitRelativePath;
   528              }
   529  
   530              let prefixPath = "";
   531  
   532              if (path !== "" || relativeFolderPath !== "") {
   533                const finalFolderPath = relativeFolderPath
   534                  .split("/")
   535                  .slice(0, -1)
   536                  .join("/");
   537  
   538                const pathClean = path.endsWith("/") ? path.slice(0, -1) : path;
   539  
   540                prefixPath = `${pathClean}${
   541                  !pathClean.endsWith("/") &&
   542                  finalFolderPath !== "" &&
   543                  !finalFolderPath.startsWith("/")
   544                    ? "/"
   545                    : ""
   546                }${finalFolderPath}${
   547                  !finalFolderPath.endsWith("/") ||
   548                  (finalFolderPath.trim() === "" && !path.endsWith("/"))
   549                    ? "/"
   550                    : ""
   551                }`;
   552              }
   553  
   554              if (prefixPath !== "") {
   555                uploadUrl = `${uploadUrl}?prefix=${encodeURLString(
   556                  prefixPath + fileName,
   557                )}`;
   558              } else {
   559                uploadUrl = `${uploadUrl}?prefix=${encodeURLString(fileName)}`;
   560              }
   561  
   562              encodedPath = encodeURLString(prefixPath);
   563  
   564              const identity = encodeURLString(
   565                `${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`,
   566              );
   567  
   568              let xhr = new XMLHttpRequest();
   569              xhr.open("POST", uploadUrl, true);
   570              if (anonymousMode) {
   571                xhr.setRequestHeader("X-Anonymous", "1");
   572              }
   573              // xhr.setRequestHeader("X-Anonymous", "1");
   574  
   575              const areMultipleFiles = files.length > 1;
   576              let errorMessage = `An error occurred while uploading the file${
   577                areMultipleFiles ? "s" : ""
   578              }.`;
   579  
   580              const errorMessages: any = {
   581                413: "Error - File size too large",
   582              };
   583  
   584              xhr.withCredentials = false;
   585              xhr.onload = function () {
   586                // resolve promise only when HTTP code is ok
   587                if (xhr.status >= 200 && xhr.status < 300) {
   588                  dispatch(completeObject(identity));
   589                  resolve({ status: xhr.status });
   590  
   591                  removeTrace(ID);
   592                } else {
   593                  // reject promise if there was a server error
   594                  if (errorMessages[xhr.status]) {
   595                    errorMessage = errorMessages[xhr.status];
   596                  } else if (xhr.response) {
   597                    try {
   598                      const err = JSON.parse(xhr.response);
   599                      errorMessage = err.detailedMessage;
   600                    } catch (e) {
   601                      errorMessage = "something went wrong";
   602                    }
   603                  }
   604  
   605                  dispatch(
   606                    failObject({
   607                      instanceID: identity,
   608                      msg: errorMessage,
   609                    }),
   610                  );
   611                  reject({ status: xhr.status, message: errorMessage });
   612  
   613                  removeTrace(ID);
   614                }
   615              };
   616  
   617              xhr.upload.addEventListener("error", () => {
   618                reject(errorMessage);
   619                dispatch(
   620                  failObject({
   621                    instanceID: identity,
   622                    msg: "A network error occurred.",
   623                  }),
   624                );
   625                return;
   626              });
   627  
   628              xhr.upload.addEventListener("progress", (event) => {
   629                const progress = Math.floor((event.loaded * 100) / event.total);
   630  
   631                dispatch(
   632                  updateProgress({
   633                    instanceID: identity,
   634                    progress: progress,
   635                  }),
   636                );
   637              });
   638  
   639              xhr.onerror = () => {
   640                reject(errorMessage);
   641                dispatch(
   642                  failObject({
   643                    instanceID: identity,
   644                    msg: "A network error occurred.",
   645                  }),
   646                );
   647                return;
   648              };
   649              xhr.onloadend = () => {
   650                if (files.length === 0) {
   651                  dispatch(setReloadObjectsList(true));
   652                }
   653              };
   654              xhr.onabort = () => {
   655                dispatch(cancelObjectInList(identity));
   656              };
   657  
   658              const formData = new FormData();
   659              if (file.size !== undefined) {
   660                formData.append(file.size.toString(), blobFile, fileName);
   661                storeCallForObjectWithID(ID, xhr);
   662                dispatch(
   663                  setNewObject({
   664                    ID,
   665                    bucketName,
   666                    done: false,
   667                    instanceID: identity,
   668                    percentage: 0,
   669                    prefix: `${decodeURLString(encodedPath)}${fileName}`,
   670                    type: "upload",
   671                    waitingForFile: false,
   672                    failed: false,
   673                    cancelled: false,
   674                    errorMessage: "",
   675                  }),
   676                );
   677                storeFormDataWithID(ID, formData);
   678              }
   679            });
   680          };
   681  
   682          const uploadFilePromises: any = [];
   683          // open object manager
   684          dispatch(openList());
   685          for (let i = 0; i < files.length; i++) {
   686            const file = files[i];
   687            uploadFilePromises.push(uploadPromise(file));
   688          }
   689          Promise.allSettled(uploadFilePromises).then((results: Array<any>) => {
   690            const errors = results.filter(
   691              (result) => result.status === "rejected",
   692            );
   693            if (errors.length > 0) {
   694              const totalFiles = uploadFilePromises.length;
   695              const successUploadedFiles =
   696                uploadFilePromises.length - errors.length;
   697              const err: ErrorResponseHandler = {
   698                errorMessage: "There were some errors during file upload",
   699                detailedError: `Uploaded files ${successUploadedFiles}/${totalFiles}`,
   700              };
   701              dispatch(setErrorSnackMessage(err));
   702            }
   703            // We force objects list reload after all promises were handled
   704            dispatch(setReloadObjectsList(true));
   705          });
   706        };
   707  
   708        upload(files, bucketName, pathPrefix, folderPath);
   709      },
   710      [bucketName, dispatch, simplePath, anonymousMode],
   711    );
   712  
   713    const onDrop = useCallback(
   714      (acceptedFiles: any[]) => {
   715        if (acceptedFiles && acceptedFiles.length > 0 && canUpload) {
   716          let newFolderPath: string = acceptedFiles[0].path;
   717          //Should we filter by allowed file extensions if any?.
   718          let allowedFiles = acceptedFiles;
   719  
   720          if (allowedFileExtensions.length > 0) {
   721            allowedFiles = acceptedFiles.filter((file) => {
   722              const fileExtn = extractFileExtn(file.name);
   723              return allowedFileExtensions.includes(fileExtn);
   724            });
   725          }
   726  
   727          if (allowedFiles.length) {
   728            uploadObject(allowedFiles, newFolderPath);
   729            console.log(
   730              `${allowedFiles.length} Allowed Files Processed out of ${acceptedFiles.length}.`,
   731              pathAsResourceInPolicy,
   732              ...sessionGrantWildCards,
   733            );
   734  
   735            if (allowedFiles.length !== acceptedFiles.length) {
   736              dispatch(
   737                setErrorSnackMessage({
   738                  errorMessage: "Upload is restricted.",
   739                  detailedError: permissionTooltipHelper(
   740                    [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
   741                    "upload objects to this location",
   742                  ),
   743                }),
   744              );
   745            }
   746          } else {
   747            dispatch(
   748              setErrorSnackMessage({
   749                errorMessage: "Could not process drag and drop.",
   750                detailedError: permissionTooltipHelper(
   751                  [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
   752                  "upload objects to this location",
   753                ),
   754              }),
   755            );
   756  
   757            console.error(
   758              "Could not process drag and drop . upload may be restricted.",
   759              pathAsResourceInPolicy,
   760              ...sessionGrantWildCards,
   761            );
   762          }
   763        }
   764        if (!canUpload) {
   765          dispatch(
   766            setErrorSnackMessage({
   767              errorMessage: "Upload not allowed",
   768              detailedError: permissionTooltipHelper(
   769                [IAM_SCOPES.S3_PUT_OBJECT, IAM_SCOPES.S3_PUT_ACTIONS],
   770                "upload objects to this location",
   771              ),
   772            }),
   773          );
   774        }
   775      },
   776      // eslint-disable-next-line react-hooks/exhaustive-deps
   777      [uploadObject],
   778    );
   779  
   780    const { getRootProps, getInputProps, isDragActive, isDragAccept } =
   781      useDropzone({
   782        noClick: true,
   783        onDrop,
   784      });
   785  
   786    const dndStyles = useMemo(
   787      () => ({
   788        ...baseDnDStyle,
   789        ...(isDragActive ? activeDnDStyle : {}),
   790        ...(isDragAccept ? acceptDnDStyle : {}),
   791      }),
   792      [isDragActive, isDragAccept],
   793    );
   794  
   795    const closeShareModal = () => {
   796      dispatch(setShareFileModalOpen(false));
   797      dispatch(setSelectedPreview(null));
   798    };
   799  
   800    const rewindCloseModal = () => {
   801      setRewindSelect(false);
   802    };
   803  
   804    const closePreviewWindow = () => {
   805      dispatch(setPreviewOpen(false));
   806      dispatch(setSelectedPreview(null));
   807    };
   808  
   809    const onClosePanel = (forceRefresh: boolean) => {
   810      dispatch(setSelectedObjectView(null));
   811      dispatch(setVersionsModeEnabled({ status: false }));
   812      if (detailsOpen && selectedInternalPaths !== null) {
   813        // We change URL to be the contained folder
   814  
   815        const decodedPath = decodeURLString(internalPaths);
   816        const splitURLS = decodedPath.split("/");
   817  
   818        // We remove the last section of the URL as it should be a file
   819        splitURLS.pop();
   820  
   821        let URLItem = "";
   822  
   823        if (splitURLS && splitURLS.length > 0) {
   824          URLItem = `${splitURLS.join("/")}/`;
   825        }
   826  
   827        navigate(`/browser/${bucketName}/${encodeURLString(URLItem)}`);
   828      }
   829  
   830      dispatch(setObjectDetailsView(false));
   831  
   832      if (forceRefresh) {
   833        dispatch(setReloadObjectsList(true));
   834      }
   835    };
   836  
   837    const setDeletedAction = () => {
   838      dispatch(resetMessages());
   839      dispatch(setShowDeletedObjects(!showDeleted));
   840      onClosePanel(true);
   841    };
   842  
   843    const closeRenameModal = () => {
   844      dispatch(setDownloadRenameModal(null));
   845    };
   846  
   847    const closeAddAccessRule = () => {
   848      dispatch(setAnonymousAccessOpen(false));
   849    };
   850  
   851    let createdTime = DateTime.now();
   852  
   853    if (bucketInfo?.creation_date) {
   854      createdTime = DateTime.fromISO(bucketInfo.creation_date) as DateTime<true>;
   855    }
   856  
   857    const downloadToolTip =
   858      selectedObjects?.length <= 1
   859        ? "Download Selected"
   860        : ` Download selected objects as Zip. Any Deleted objects in the selection would be skipped from download.`;
   861  
   862    const multiActionButtons = [
   863      {
   864        action: () => {
   865          dispatch(downloadSelected(bucketName));
   866        },
   867        label: "Download",
   868        disabled: !canDownload || isSelObjectDelMarker,
   869        icon: <DownloadIcon />,
   870        tooltip: canDownload
   871          ? downloadToolTip
   872          : permissionTooltipHelper(
   873              [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS],
   874              "download objects from this bucket",
   875            ),
   876      },
   877      {
   878        action: () => {
   879          dispatch(openShare());
   880        },
   881        label: "Share",
   882        disabled:
   883          selectedObjects.length !== 1 || !canShareFile || isSelObjectDelMarker,
   884        icon: <ShareIcon />,
   885        tooltip: canShareFile ? "Share Selected File" : "Sharing unavailable",
   886      },
   887      {
   888        action: () => {
   889          dispatch(openPreview());
   890        },
   891        label: "Preview",
   892        disabled:
   893          selectedObjects.length !== 1 || !canPreviewFile || isSelObjectDelMarker,
   894        icon: <PreviewIcon />,
   895        tooltip: canPreviewFile ? "Preview Selected File" : "Preview unavailable",
   896      },
   897      {
   898        action: () => {
   899          dispatch(openAnonymousAccess());
   900        },
   901        label: "Anonymous Access",
   902        disabled:
   903          selectedObjects.length !== 1 ||
   904          !selectedObjects[0].endsWith("/") ||
   905          !canSetAnonymousAccess,
   906        icon: <AccessRuleIcon />,
   907        tooltip:
   908          selectedObjects.length === 1 && selectedObjects[0].endsWith("/")
   909            ? "Set Anonymous Access to this Folder"
   910            : "Anonymous Access unavailable",
   911      },
   912      {
   913        action: () => {
   914          setDeleteMultipleOpen(true);
   915        },
   916        label: "Delete",
   917        icon: <DeleteIcon />,
   918        disabled: !canDelete || selectedObjects.length === 0,
   919        tooltip: canDelete
   920          ? "Delete Selected Files"
   921          : permissionTooltipHelper(
   922              [IAM_SCOPES.S3_DELETE_OBJECT],
   923              "delete objects in this bucket",
   924            ),
   925      },
   926    ];
   927  
   928    return (
   929      <Fragment>
   930        {shareFileModalOpen && selectedPreview && (
   931          <ShareFile
   932            open={shareFileModalOpen}
   933            closeModalAndRefresh={closeShareModal}
   934            bucketName={bucketName}
   935            dataObject={{
   936              name: selectedPreview.name,
   937              last_modified: "",
   938              version_id: selectedPreview.version_id,
   939            }}
   940          />
   941        )}
   942        {deleteMultipleOpen && (
   943          <DeleteMultipleObjects
   944            deleteOpen={deleteMultipleOpen}
   945            selectedBucket={bucketName}
   946            selectedObjects={selectedObjects}
   947            closeDeleteModalAndRefresh={closeDeleteMultipleModalAndRefresh}
   948            versioning={versioningConfig}
   949          />
   950        )}
   951        {rewindSelect && (
   952          <RewindEnable
   953            open={rewindSelect}
   954            closeModalAndRefresh={rewindCloseModal}
   955            bucketName={bucketName}
   956          />
   957        )}
   958        {previewOpen && selectedPreview && (
   959          <PreviewFileModal
   960            open={previewOpen}
   961            bucketName={bucketName}
   962            actualInfo={{
   963              name: selectedPreview.name || "",
   964              last_modified: "",
   965              version_id: selectedPreview.version_id || "",
   966              size: selectedPreview.size || 0,
   967            }}
   968            onClosePreview={closePreviewWindow}
   969          />
   970        )}
   971        {!!downloadRenameModal && (
   972          <RenameLongFileName
   973            open={!!downloadRenameModal}
   974            closeModal={closeRenameModal}
   975            currentItem={downloadRenameModal.name.split("/")?.pop() || ""}
   976            bucketName={bucketName}
   977            internalPaths={internalPaths}
   978            actualInfo={{
   979              name: downloadRenameModal.name,
   980              last_modified: "",
   981              version_id: downloadRenameModal.version_id,
   982              size: downloadRenameModal.size,
   983            }}
   984          />
   985        )}
   986        {anonymousAccessOpen && (
   987          <AddAccessRule
   988            onClose={closeAddAccessRule}
   989            bucket={bucketName}
   990            modalOpen={anonymousAccessOpen}
   991            prefilledRoute={`${selectedObjects[0]}*`}
   992          />
   993        )}
   994  
   995        <PageLayout variant={"full"}>
   996          {anonymousMode && (
   997            <div style={{ paddingBottom: 16 }}>
   998              <FilterObjectsSB />
   999            </div>
  1000          )}
  1001          <Box withBorders sx={{ padding: "0 5px" }}>
  1002            <ScreenTitle
  1003              icon={
  1004                <span>
  1005                  <BucketsIcon style={{ width: 30 }} />
  1006                </span>
  1007              }
  1008              title={bucketName}
  1009              subTitle={
  1010                !anonymousMode ? (
  1011                  <Box
  1012                    sx={{
  1013                      "& .detailsSpacer": {
  1014                        marginRight: 18,
  1015                        "@media (max-width: 600px)": {
  1016                          marginRight: 0,
  1017                        },
  1018                      },
  1019                    }}
  1020                  >
  1021                    <span className={"detailsSpacer"}>
  1022                      Created on:&nbsp;
  1023                      <strong>
  1024                        {bucketInfo?.creation_date
  1025                          ? createdTime.toFormat(
  1026                              "ccc, LLL dd yyyy HH:mm:ss (ZZZZ)",
  1027                            )
  1028                          : ""}
  1029                      </strong>
  1030                    </span>
  1031                    <span className={"detailsSpacer"}>
  1032                      Access:&nbsp;&nbsp;
  1033                      <strong>{bucketInfo?.access || ""}</strong>
  1034                    </span>
  1035                    {bucketInfo && (
  1036                      <Fragment>
  1037                        <span className={"detailsSpacer"}>
  1038                          {bucketInfo.size && (
  1039                            <Fragment>{niceBytesInt(bucketInfo.size)}</Fragment>
  1040                          )}
  1041                          {bucketInfo.size && quota && (
  1042                            <Fragment>
  1043                              {" "}
  1044                              / {niceBytesInt(quota.quota || 0)}
  1045                            </Fragment>
  1046                          )}
  1047                          {bucketInfo.size && bucketInfo.objects ? " - " : ""}
  1048                          {bucketInfo.objects && (
  1049                            <Fragment>
  1050                              {bucketInfo.objects}&nbsp;Object
  1051                              {bucketInfo.objects && bucketInfo.objects !== 1
  1052                                ? "s"
  1053                                : ""}
  1054                            </Fragment>
  1055                          )}
  1056                        </span>
  1057                      </Fragment>
  1058                    )}
  1059                  </Box>
  1060                ) : null
  1061              }
  1062              actions={
  1063                <Fragment>
  1064                  {!anonymousMode && (
  1065                    <TooltipWrapper
  1066                      tooltip={
  1067                        canRewind
  1068                          ? "Rewind Bucket"
  1069                          : permissionTooltipHelper(
  1070                              [
  1071                                IAM_SCOPES.S3_GET_OBJECT,
  1072                                IAM_SCOPES.S3_GET_ACTIONS,
  1073                                IAM_SCOPES.S3_GET_BUCKET_VERSIONING,
  1074                              ],
  1075                              "apply rewind in this bucket",
  1076                            )
  1077                      }
  1078                    >
  1079                      <Button
  1080                        id={"rewind-objects-list"}
  1081                        label={"Rewind"}
  1082                        icon={
  1083                          <Badge color="alert" dotOnly invisible={!rewindEnabled}>
  1084                            <HistoryIcon
  1085                              style={{
  1086                                minWidth: 16,
  1087                                minHeight: 16,
  1088                                width: 16,
  1089                                height: 16,
  1090                                marginTop: -3,
  1091                              }}
  1092                            />
  1093                          </Badge>
  1094                        }
  1095                        variant={"regular"}
  1096                        onClick={() => {
  1097                          setRewindSelect(true);
  1098                        }}
  1099                        disabled={!isVersioningApplied || !canRewind}
  1100                      />
  1101                    </TooltipWrapper>
  1102                  )}
  1103                  <TooltipWrapper tooltip={"Reload List"}>
  1104                    <Button
  1105                      id={"refresh-objects-list"}
  1106                      label={"Refresh"}
  1107                      icon={<RefreshIcon />}
  1108                      variant={"regular"}
  1109                      onClick={() => {
  1110                        if (versionsMode) {
  1111                          dispatch(setLoadingVersions(true));
  1112                        } else {
  1113                          dispatch(resetMessages());
  1114                          dispatch(setReloadObjectsList(true));
  1115                        }
  1116                      }}
  1117                      disabled={
  1118                        anonymousMode
  1119                          ? false
  1120                          : !hasPermission(bucketName, [
  1121                              IAM_SCOPES.S3_LIST_BUCKET,
  1122                              IAM_SCOPES.S3_ALL_LIST_BUCKET,
  1123                            ]) || rewindEnabled
  1124                      }
  1125                    />
  1126                  </TooltipWrapper>
  1127                  <input
  1128                    type="file"
  1129                    multiple
  1130                    accept={
  1131                      allowedFileExtensions ? allowedFileExtensions : undefined
  1132                    }
  1133                    onChange={handleUploadButton}
  1134                    style={{ display: "none" }}
  1135                    ref={fileUpload}
  1136                  />
  1137                  <input
  1138                    type="file"
  1139                    multiple
  1140                    onChange={handleUploadButton}
  1141                    style={{ display: "none" }}
  1142                    ref={folderUpload}
  1143                  />
  1144                  <UploadFilesButton
  1145                    bucketName={bucketName}
  1146                    uploadPath={pathAsResourceInPolicy}
  1147                    uploadFileFunction={(closeMenu) => {
  1148                      if (fileUpload && fileUpload.current) {
  1149                        fileUpload.current.click();
  1150                      }
  1151                      closeMenu();
  1152                    }}
  1153                    uploadFolderFunction={(closeMenu) => {
  1154                      if (folderUpload && folderUpload.current) {
  1155                        folderUpload.current.click();
  1156                      }
  1157                      closeMenu();
  1158                    }}
  1159                  />
  1160                </Fragment>
  1161              }
  1162              bottomBorder={false}
  1163            />
  1164          </Box>
  1165          <div
  1166            id="object-list-wrapper"
  1167            {...getRootProps({ style: { ...dndStyles } })}
  1168          >
  1169            <input {...getInputProps()} />
  1170            <Box
  1171              withBorders
  1172              sx={{
  1173                display: "flex",
  1174                borderTop: 0,
  1175                padding: 0,
  1176                "& .hideListOnSmall": {
  1177                  "@media (max-width: 799px)": {
  1178                    display: "none",
  1179                  },
  1180                },
  1181              }}
  1182            >
  1183              {versionsMode ? (
  1184                <Fragment>
  1185                  {selectedInternalPaths !== null && (
  1186                    <VersionsNavigator
  1187                      internalPaths={selectedInternalPaths}
  1188                      bucketName={bucketName}
  1189                    />
  1190                  )}
  1191                </Fragment>
  1192              ) : (
  1193                <SecureComponent
  1194                  scopes={[
  1195                    IAM_SCOPES.S3_LIST_BUCKET,
  1196                    IAM_SCOPES.S3_ALL_LIST_BUCKET,
  1197                  ]}
  1198                  resource={bucketName}
  1199                  errorProps={{ disabled: true }}
  1200                >
  1201                  <Grid
  1202                    item
  1203                    xs={12}
  1204                    sx={{
  1205                      width: "100%",
  1206                      position: "relative",
  1207                      "&.detailsOpen": {
  1208                        "@media (max-width: 799px)": {
  1209                          display: "none",
  1210                        },
  1211                      },
  1212                    }}
  1213                    className={detailsOpen ? "detailsOpen" : ""}
  1214                  >
  1215                    {!anonymousMode && (
  1216                      <Grid
  1217                        item
  1218                        xs={12}
  1219                        sx={{
  1220                          padding: "12px 14px 5px",
  1221                        }}
  1222                      >
  1223                        <BrowserBreadcrumbs
  1224                          bucketName={bucketName}
  1225                          internalPaths={pageTitle}
  1226                          additionalOptions={
  1227                            !isVersioningApplied || rewindEnabled ? null : (
  1228                              <Checkbox
  1229                                name={"deleted_objects"}
  1230                                id={"showDeletedObjects"}
  1231                                value={"deleted_on"}
  1232                                label={"Show deleted objects"}
  1233                                onChange={setDeletedAction}
  1234                                checked={showDeleted}
  1235                                sx={{
  1236                                  marginLeft: 5,
  1237                                  "@media (max-width: 600px)": {
  1238                                    marginLeft: 0,
  1239                                    flexDirection: "row" as const,
  1240                                  },
  1241                                }}
  1242                              />
  1243                            )
  1244                          }
  1245                          hidePathButton={false}
  1246                        />
  1247                      </Grid>
  1248                    )}
  1249                    <ListObjectsTable />
  1250                  </Grid>
  1251                </SecureComponent>
  1252              )}
  1253              {!anonymousMode && (
  1254                <SecureComponent
  1255                  scopes={[
  1256                    IAM_SCOPES.S3_LIST_BUCKET,
  1257                    IAM_SCOPES.S3_ALL_LIST_BUCKET,
  1258                  ]}
  1259                  resource={bucketName}
  1260                  errorProps={{ disabled: true }}
  1261                >
  1262                  <DetailsListPanel
  1263                    open={detailsOpen}
  1264                    closePanel={() => {
  1265                      onClosePanel(false);
  1266                    }}
  1267                    className={`${versionsMode ? "hideListOnSmall" : ""}`}
  1268                  >
  1269                    {selectedObjects.length > 0 && (
  1270                      <ActionsList
  1271                        items={multiActionButtons}
  1272                        title={"Selected Objects:"}
  1273                      />
  1274                    )}
  1275                    {selectedInternalPaths !== null && (
  1276                      <ObjectDetailPanel
  1277                        internalPaths={selectedInternalPaths}
  1278                        bucketName={bucketName}
  1279                        onClosePanel={onClosePanel}
  1280                        versioningInfo={versioningConfig}
  1281                        locking={lockingEnabled}
  1282                      />
  1283                    )}
  1284                  </DetailsListPanel>
  1285                </SecureComponent>
  1286              )}
  1287            </Box>
  1288          </div>
  1289        </PageLayout>
  1290      </Fragment>
  1291    );
  1292  };
  1293  
  1294  export default ListObjects;