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: 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: 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} 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;