github.com/minio/console@v1.3.0/web-app/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.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 get from "lodash/get"; 19 import { useSelector } from "react-redux"; 20 import { 21 ActionsList, 22 Box, 23 Button, 24 DeleteIcon, 25 DownloadIcon, 26 Grid, 27 InspectMenuIcon, 28 LegalHoldIcon, 29 Loader, 30 MetadataIcon, 31 ObjectInfoIcon, 32 PreviewIcon, 33 RetentionIcon, 34 ShareIcon, 35 SimpleHeader, 36 TagsIcon, 37 VersionsIcon, 38 } from "mds"; 39 import { api } from "api"; 40 import { downloadObject } from "../../../../ObjectBrowser/utils"; 41 import { BucketObject, BucketVersioningResponse } from "api/consoleApi"; 42 import { AllowedPreviews, previewObjectType } from "../utils"; 43 import { 44 decodeURLString, 45 niceBytes, 46 niceBytesInt, 47 niceDaysInt, 48 } from "../../../../../../common/utils"; 49 import { 50 IAM_SCOPES, 51 permissionTooltipHelper, 52 } from "../../../../../../common/SecureComponent/permissions"; 53 import { AppState, useAppDispatch } from "../../../../../../store"; 54 import { 55 hasPermission, 56 SecureComponent, 57 } from "../../../../../../common/SecureComponent"; 58 import { selDistSet } from "../../../../../../systemSlice"; 59 import { 60 setLoadingObjectInfo, 61 setLoadingVersions, 62 setSelectedVersion, 63 setVersionsModeEnabled, 64 } from "../../../../ObjectBrowser/objectBrowserSlice"; 65 import { displayFileIconName } from "./utils"; 66 import PreviewFileModal from "../Preview/PreviewFileModal"; 67 import ObjectMetaData from "../ObjectDetails/ObjectMetaData"; 68 import ShareFile from "../ObjectDetails/ShareFile"; 69 import SetRetention from "../ObjectDetails/SetRetention"; 70 import DeleteObject from "../ListObjects/DeleteObject"; 71 import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal"; 72 import TagsModal from "../ObjectDetails/TagsModal"; 73 import InspectObject from "./InspectObject"; 74 import RenameLongFileName from "../../../../ObjectBrowser/RenameLongFilename"; 75 import TooltipWrapper from "../../../../Common/TooltipWrapper/TooltipWrapper"; 76 77 const emptyFile: BucketObject = { 78 is_latest: true, 79 last_modified: "", 80 legal_hold_status: "", 81 name: "", 82 retention_mode: "", 83 retention_until_date: "", 84 size: 0, 85 tags: {}, 86 version_id: undefined, 87 }; 88 89 interface IObjectDetailPanelProps { 90 internalPaths: string; 91 bucketName: string; 92 versioningInfo: BucketVersioningResponse; 93 locking: boolean | undefined; 94 onClosePanel: (hardRefresh: boolean) => void; 95 } 96 97 const ObjectDetailPanel = ({ 98 internalPaths, 99 bucketName, 100 versioningInfo, 101 locking, 102 onClosePanel, 103 }: IObjectDetailPanelProps) => { 104 const dispatch = useAppDispatch(); 105 106 const distributedSetup = useSelector(selDistSet); 107 const versionsMode = useSelector( 108 (state: AppState) => state.objectBrowser.versionsMode, 109 ); 110 const selectedVersion = useSelector( 111 (state: AppState) => state.objectBrowser.selectedVersion, 112 ); 113 const loadingObjectInfo = useSelector( 114 (state: AppState) => state.objectBrowser.loadingObjectInfo, 115 ); 116 117 const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false); 118 const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false); 119 const [tagModalOpen, setTagModalOpen] = useState<boolean>(false); 120 const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false); 121 const [inspectModalOpen, setInspectModalOpen] = useState<boolean>(false); 122 const [actualInfo, setActualInfo] = useState<BucketObject | null>(null); 123 const [allInfoElements, setAllInfoElements] = useState<BucketObject[]>([]); 124 const [objectToShare, setObjectToShare] = useState<BucketObject | null>(null); 125 const [versions, setVersions] = useState<BucketObject[]>([]); 126 const [deleteOpen, setDeleteOpen] = useState<boolean>(false); 127 const [previewOpen, setPreviewOpen] = useState<boolean>(false); 128 const [totalVersionsSize, setTotalVersionsSize] = useState<number>(0); 129 const [longFileOpen, setLongFileOpen] = useState<boolean>(false); 130 const [metaData, setMetaData] = useState<any | null>(null); 131 const [loadMetadata, setLoadingMetadata] = useState<boolean>(false); 132 133 const internalPathsDecoded = decodeURLString(internalPaths) || ""; 134 const allPathData = internalPathsDecoded.split("/"); 135 const currentItem = allPathData.pop() || ""; 136 137 // calculate object name to display 138 let objectNameArray: string[] = []; 139 if (actualInfo && actualInfo.name) { 140 objectNameArray = actualInfo.name.split("/"); 141 } 142 143 useEffect(() => { 144 if (distributedSetup && allInfoElements && allInfoElements.length >= 1) { 145 let infoElement = 146 allInfoElements.find((el: BucketObject) => el.is_latest) || emptyFile; 147 148 if (selectedVersion !== "") { 149 infoElement = 150 allInfoElements.find( 151 (el: BucketObject) => el.version_id === selectedVersion, 152 ) || emptyFile; 153 } 154 155 if (!infoElement.is_delete_marker) { 156 setLoadingMetadata(true); 157 } 158 159 setActualInfo(infoElement); 160 } 161 }, [selectedVersion, distributedSetup, allInfoElements]); 162 163 useEffect(() => { 164 if (loadingObjectInfo && internalPaths !== "") { 165 api.buckets 166 .listObjects(bucketName, { 167 prefix: internalPaths, 168 with_versions: distributedSetup, 169 }) 170 .then((res) => { 171 const result: BucketObject[] = res.data.objects || []; 172 if (distributedSetup) { 173 setAllInfoElements(result); 174 setVersions(result); 175 176 const tVersionSize = result.reduce( 177 (acc: number, currValue: BucketObject): number => { 178 if (currValue?.size) { 179 return acc + currValue.size; 180 } 181 return acc; 182 }, 183 0, 184 ); 185 186 setTotalVersionsSize(tVersionSize); 187 } else { 188 const resInfo = result[0]; 189 190 setActualInfo(resInfo); 191 setVersions([]); 192 193 if (!resInfo.is_delete_marker) { 194 setLoadingMetadata(true); 195 } 196 } 197 198 dispatch(setLoadingObjectInfo(false)); 199 }) 200 .catch((err) => { 201 console.error("Error loading object details", err.error); 202 dispatch(setLoadingObjectInfo(false)); 203 }); 204 } 205 }, [ 206 loadingObjectInfo, 207 bucketName, 208 internalPaths, 209 dispatch, 210 distributedSetup, 211 selectedVersion, 212 ]); 213 214 useEffect(() => { 215 if (loadMetadata && internalPaths !== "") { 216 api.buckets 217 .getObjectMetadata(bucketName, { 218 prefix: internalPaths, 219 }) 220 .then((res) => { 221 let metadata = get(res.data, "objectMetadata", {}); 222 223 setMetaData(metadata); 224 setLoadingMetadata(false); 225 }) 226 .catch((err) => { 227 console.error("Error Getting Metadata Status: ", err.detailedError); 228 setLoadingMetadata(false); 229 }); 230 } 231 }, [bucketName, internalPaths, loadMetadata]); 232 233 let tagKeys: string[] = []; 234 235 if (actualInfo && actualInfo.tags) { 236 tagKeys = Object.keys(actualInfo.tags); 237 } 238 239 const openRetentionModal = () => { 240 setRetentionModalOpen(true); 241 }; 242 243 const closeRetentionModal = (updateInfo: boolean) => { 244 setRetentionModalOpen(false); 245 if (updateInfo) { 246 dispatch(setLoadingObjectInfo(true)); 247 } 248 }; 249 250 const shareObject = () => { 251 setShareFileModalOpen(true); 252 }; 253 254 const closeShareModal = () => { 255 setObjectToShare(null); 256 setShareFileModalOpen(false); 257 }; 258 259 const closeFileOpen = () => { 260 setLongFileOpen(false); 261 }; 262 263 const closeDeleteModal = (closeAndReload: boolean) => { 264 setDeleteOpen(false); 265 266 if (closeAndReload && selectedVersion === "") { 267 onClosePanel(true); 268 } else { 269 dispatch(setLoadingVersions(true)); 270 dispatch(setSelectedVersion("")); 271 dispatch(setLoadingObjectInfo(true)); 272 } 273 }; 274 275 const closeAddTagModal = (reloadObjectData: boolean) => { 276 setTagModalOpen(false); 277 if (reloadObjectData) { 278 dispatch(setLoadingObjectInfo(true)); 279 } 280 }; 281 282 const closeInspectModal = (reloadObjectData: boolean) => { 283 setInspectModalOpen(false); 284 if (reloadObjectData) { 285 dispatch(setLoadingObjectInfo(true)); 286 } 287 }; 288 289 const closeLegalholdModal = (reload: boolean) => { 290 setLegalholdOpen(false); 291 if (reload) { 292 dispatch(setLoadingObjectInfo(true)); 293 } 294 }; 295 296 const loaderForContainer = ( 297 <div style={{ textAlign: "center", marginTop: 35 }}> 298 <Loader /> 299 </div> 300 ); 301 302 if (!actualInfo) { 303 if (loadingObjectInfo) { 304 return loaderForContainer; 305 } 306 307 return null; 308 } 309 310 const objectName = 311 objectNameArray.length > 0 312 ? objectNameArray[objectNameArray.length - 1] 313 : actualInfo.name; 314 315 const objectResources = [ 316 bucketName, 317 currentItem, 318 [bucketName, actualInfo.name].join("/"), 319 ]; 320 const canSetLegalHold = hasPermission(bucketName, [ 321 IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, 322 IAM_SCOPES.S3_PUT_ACTIONS, 323 ]); 324 const canSetTags = hasPermission(objectResources, [ 325 IAM_SCOPES.S3_PUT_OBJECT_TAGGING, 326 IAM_SCOPES.S3_PUT_ACTIONS, 327 ]); 328 329 const canChangeRetention = hasPermission( 330 objectResources, 331 [ 332 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 333 IAM_SCOPES.S3_PUT_OBJECT_RETENTION, 334 IAM_SCOPES.S3_GET_ACTIONS, 335 IAM_SCOPES.S3_PUT_ACTIONS, 336 ], 337 true, 338 ); 339 const canInspect = hasPermission(objectResources, [ 340 IAM_SCOPES.ADMIN_INSPECT_DATA, 341 ]); 342 const canChangeVersioning = hasPermission(objectResources, [ 343 IAM_SCOPES.S3_GET_BUCKET_VERSIONING, 344 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 345 IAM_SCOPES.S3_GET_OBJECT_VERSION, 346 IAM_SCOPES.S3_GET_ACTIONS, 347 IAM_SCOPES.S3_PUT_ACTIONS, 348 ]); 349 const canGetObject = hasPermission(objectResources, [ 350 IAM_SCOPES.S3_GET_OBJECT, 351 IAM_SCOPES.S3_GET_ACTIONS, 352 ]); 353 const canDelete = hasPermission( 354 [bucketName, currentItem, [bucketName, actualInfo.name].join("/")], 355 [IAM_SCOPES.S3_DELETE_OBJECT], 356 ); 357 358 let objectType: AllowedPreviews = previewObjectType(metaData, currentItem); 359 360 const multiActionButtons = [ 361 { 362 action: () => { 363 downloadObject(dispatch, bucketName, internalPaths, actualInfo); 364 }, 365 label: "Download", 366 disabled: !!actualInfo.is_delete_marker || !canGetObject, 367 icon: <DownloadIcon />, 368 tooltip: canGetObject 369 ? "Download this Object" 370 : permissionTooltipHelper( 371 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 372 "download this object", 373 ), 374 }, 375 { 376 action: () => { 377 shareObject(); 378 }, 379 label: "Share", 380 disabled: !!actualInfo.is_delete_marker || !canGetObject, 381 icon: <ShareIcon />, 382 tooltip: canGetObject 383 ? "Share this File" 384 : permissionTooltipHelper( 385 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 386 "share this object", 387 ), 388 }, 389 { 390 action: () => { 391 setPreviewOpen(true); 392 }, 393 label: "Preview", 394 disabled: 395 !!actualInfo.is_delete_marker || 396 (objectType === "none" && !canGetObject), 397 icon: <PreviewIcon />, 398 tooltip: canGetObject 399 ? "Preview this File" 400 : permissionTooltipHelper( 401 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 402 "preview this object", 403 ), 404 }, 405 { 406 action: () => { 407 setLegalholdOpen(true); 408 }, 409 label: "Legal Hold", 410 disabled: 411 !locking || 412 !distributedSetup || 413 !!actualInfo.is_delete_marker || 414 !canSetLegalHold || 415 selectedVersion !== "", 416 icon: <LegalHoldIcon />, 417 tooltip: canSetLegalHold 418 ? locking 419 ? "Change Legal Hold rules for this File" 420 : "Object Locking must be enabled on this bucket in order to set Legal Hold" 421 : permissionTooltipHelper( 422 [IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, IAM_SCOPES.S3_PUT_ACTIONS], 423 "change legal hold settings for this object", 424 ), 425 }, 426 { 427 action: openRetentionModal, 428 label: "Retention", 429 disabled: 430 !distributedSetup || 431 !!actualInfo.is_delete_marker || 432 !canChangeRetention || 433 selectedVersion !== "" || 434 !locking, 435 icon: <RetentionIcon />, 436 tooltip: canChangeRetention 437 ? locking 438 ? "Change Retention rules for this File" 439 : "Object Locking must be enabled on this bucket in order to set Retention Rules" 440 : permissionTooltipHelper( 441 [ 442 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 443 IAM_SCOPES.S3_PUT_OBJECT_RETENTION, 444 IAM_SCOPES.S3_GET_ACTIONS, 445 IAM_SCOPES.S3_PUT_ACTIONS, 446 ], 447 "change Retention Rules for this object", 448 ), 449 }, 450 { 451 action: () => { 452 setTagModalOpen(true); 453 }, 454 label: "Tags", 455 disabled: 456 !!actualInfo.is_delete_marker || selectedVersion !== "" || !canSetTags, 457 icon: <TagsIcon />, 458 tooltip: canSetTags 459 ? "Change Tags for this File" 460 : permissionTooltipHelper( 461 [ 462 IAM_SCOPES.S3_PUT_OBJECT_TAGGING, 463 IAM_SCOPES.S3_GET_OBJECT_TAGGING, 464 IAM_SCOPES.S3_GET_ACTIONS, 465 IAM_SCOPES.S3_PUT_ACTIONS, 466 ], 467 "set Tags on this object", 468 ), 469 }, 470 { 471 action: () => { 472 setInspectModalOpen(true); 473 }, 474 label: "Inspect", 475 disabled: 476 !distributedSetup || 477 !!actualInfo.is_delete_marker || 478 selectedVersion !== "" || 479 !canInspect, 480 icon: <InspectMenuIcon />, 481 tooltip: canInspect 482 ? "Inspect this file" 483 : permissionTooltipHelper( 484 [IAM_SCOPES.ADMIN_INSPECT_DATA], 485 "inspect this file", 486 ), 487 }, 488 { 489 action: () => { 490 dispatch( 491 setVersionsModeEnabled({ 492 status: !versionsMode, 493 objectName: objectName, 494 }), 495 ); 496 }, 497 label: versionsMode ? "Hide Object Versions" : "Display Object Versions", 498 icon: <VersionsIcon />, 499 disabled: 500 !distributedSetup || 501 !(actualInfo.version_id && actualInfo.version_id !== "null") || 502 !canChangeVersioning, 503 tooltip: canChangeVersioning 504 ? actualInfo.version_id && actualInfo.version_id !== "null" 505 ? "Display Versions for this file" 506 : "" 507 : permissionTooltipHelper( 508 [ 509 IAM_SCOPES.S3_GET_BUCKET_VERSIONING, 510 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 511 IAM_SCOPES.S3_GET_OBJECT_VERSION, 512 IAM_SCOPES.S3_GET_ACTIONS, 513 IAM_SCOPES.S3_PUT_ACTIONS, 514 ], 515 "display all versions of this object", 516 ), 517 }, 518 ]; 519 520 const calculateLastModifyTime = (lastModified: string) => { 521 const currentTime = new Date(); 522 const modifiedTime = new Date(lastModified); 523 524 const difTime = currentTime.getTime() - modifiedTime.getTime(); 525 526 const formatTime = niceDaysInt(difTime, "ms"); 527 528 return formatTime.trim() !== "" ? `${formatTime} ago` : "Just now"; 529 }; 530 531 return ( 532 <Fragment> 533 {shareFileModalOpen && actualInfo && ( 534 <ShareFile 535 open={shareFileModalOpen} 536 closeModalAndRefresh={closeShareModal} 537 bucketName={bucketName} 538 dataObject={objectToShare || actualInfo} 539 /> 540 )} 541 {retentionModalOpen && actualInfo && ( 542 <SetRetention 543 open={retentionModalOpen} 544 closeModalAndRefresh={closeRetentionModal} 545 objectName={currentItem} 546 objectInfo={actualInfo} 547 bucketName={bucketName} 548 /> 549 )} 550 {deleteOpen && ( 551 <DeleteObject 552 deleteOpen={deleteOpen} 553 selectedBucket={bucketName} 554 selectedObject={internalPaths} 555 closeDeleteModalAndRefresh={closeDeleteModal} 556 versioningInfo={distributedSetup ? versioningInfo : undefined} 557 selectedVersion={selectedVersion} 558 /> 559 )} 560 {legalholdOpen && actualInfo && ( 561 <SetLegalHoldModal 562 open={legalholdOpen} 563 closeModalAndRefresh={closeLegalholdModal} 564 objectName={actualInfo.name || ""} 565 bucketName={bucketName} 566 actualInfo={actualInfo} 567 /> 568 )} 569 {previewOpen && actualInfo && ( 570 <PreviewFileModal 571 open={previewOpen} 572 bucketName={bucketName} 573 actualInfo={actualInfo} 574 onClosePreview={() => { 575 setPreviewOpen(false); 576 }} 577 /> 578 )} 579 {tagModalOpen && actualInfo && ( 580 <TagsModal 581 modalOpen={tagModalOpen} 582 bucketName={bucketName} 583 actualInfo={actualInfo} 584 onCloseAndUpdate={closeAddTagModal} 585 /> 586 )} 587 {inspectModalOpen && actualInfo && ( 588 <InspectObject 589 inspectOpen={inspectModalOpen} 590 volumeName={bucketName} 591 inspectPath={actualInfo.name || ""} 592 closeInspectModalAndRefresh={closeInspectModal} 593 /> 594 )} 595 {longFileOpen && actualInfo && ( 596 <RenameLongFileName 597 open={longFileOpen} 598 closeModal={closeFileOpen} 599 currentItem={currentItem} 600 bucketName={bucketName} 601 internalPaths={internalPaths} 602 actualInfo={actualInfo} 603 /> 604 )} 605 606 {loadingObjectInfo ? ( 607 <Fragment>{loaderForContainer}</Fragment> 608 ) : ( 609 <Box 610 sx={{ 611 "& .ObjectDetailsTitle": { 612 display: "flex", 613 alignItems: "center", 614 "& .min-icon": { 615 width: 26, 616 height: 26, 617 minWidth: 26, 618 minHeight: 26, 619 }, 620 }, 621 "& .objectNameContainer": { 622 whiteSpace: "nowrap", 623 textOverflow: "ellipsis", 624 overflow: "hidden", 625 alignItems: "center", 626 marginLeft: 10, 627 }, 628 "& .capitalizeFirst": { 629 textTransform: "capitalize", 630 }, 631 "& .detailContainer": { 632 padding: "0 22px", 633 marginBottom: 10, 634 fontSize: 14, 635 }, 636 }} 637 > 638 <ActionsList 639 title={ 640 <div className={"ObjectDetailsTitle"}> 641 {displayFileIconName(objectName || "", true)} 642 <span className={"objectNameContainer"}>{objectName}</span> 643 </div> 644 } 645 items={multiActionButtons} 646 /> 647 <TooltipWrapper 648 tooltip={ 649 canDelete 650 ? "" 651 : permissionTooltipHelper( 652 [IAM_SCOPES.S3_DELETE_OBJECT], 653 "delete this object", 654 ) 655 } 656 > 657 <Grid 658 item 659 xs={12} 660 sx={{ justifyContent: "center", display: "flex" }} 661 > 662 <SecureComponent 663 resource={[ 664 bucketName, 665 currentItem, 666 [bucketName, actualInfo.name].join("/"), 667 ]} 668 scopes={[IAM_SCOPES.S3_DELETE_OBJECT]} 669 errorProps={{ disabled: true }} 670 > 671 <Button 672 id={"delete-element-click"} 673 icon={<DeleteIcon />} 674 iconLocation={"start"} 675 fullWidth 676 variant={"secondary"} 677 onClick={() => { 678 setDeleteOpen(true); 679 }} 680 disabled={ 681 selectedVersion === "" && actualInfo.is_delete_marker 682 } 683 sx={{ 684 width: "calc(100% - 44px)", 685 margin: "8px 0", 686 }} 687 label={`Delete${selectedVersion !== "" ? " version" : ""}`} 688 /> 689 </SecureComponent> 690 </Grid> 691 </TooltipWrapper> 692 <SimpleHeader icon={<ObjectInfoIcon />} label={"Object Info"} /> 693 <Box className={"detailContainer"}> 694 <strong>Name:</strong> 695 <br /> 696 <div style={{ overflowWrap: "break-word" }}>{objectName}</div> 697 </Box> 698 {selectedVersion !== "" && ( 699 <Box className={"detailContainer"}> 700 <strong>Version ID:</strong> 701 <br /> 702 {selectedVersion} 703 </Box> 704 )} 705 <Box className={"detailContainer"}> 706 <strong>Size:</strong> 707 <br /> 708 {niceBytes(`${actualInfo.size || "0"}`)} 709 </Box> 710 {actualInfo.version_id && 711 actualInfo.version_id !== "null" && 712 selectedVersion === "" && ( 713 <Box className={"detailContainer"}> 714 <strong>Versions:</strong> 715 <br /> 716 {versions.length} version{versions.length !== 1 ? "s" : ""},{" "} 717 {niceBytesInt(totalVersionsSize)} 718 </Box> 719 )} 720 {selectedVersion === "" && ( 721 <Box className={"detailContainer"}> 722 <strong>Last Modified:</strong> 723 <br /> 724 {calculateLastModifyTime(actualInfo.last_modified || "")} 725 </Box> 726 )} 727 <Box className={"detailContainer"}> 728 <strong>ETAG:</strong> 729 <br /> 730 {actualInfo.etag || "N/A"} 731 </Box> 732 <Box className={"detailContainer"}> 733 <strong>Tags:</strong> 734 <br /> 735 {tagKeys.length === 0 736 ? "N/A" 737 : tagKeys.map((tagKey, index) => { 738 return ( 739 <span key={`key-vs-${index.toString()}`}> 740 {tagKey}:{get(actualInfo, `tags.${tagKey}`, "")} 741 {index < tagKeys.length - 1 ? ", " : ""} 742 </span> 743 ); 744 })} 745 </Box> 746 <Box className={"detailContainer"}> 747 <SecureComponent 748 scopes={[ 749 IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD, 750 IAM_SCOPES.S3_GET_ACTIONS, 751 ]} 752 resource={bucketName} 753 > 754 <Fragment> 755 <strong>Legal Hold:</strong> 756 <br /> 757 {actualInfo.legal_hold_status ? "On" : "Off"} 758 </Fragment> 759 </SecureComponent> 760 </Box> 761 <Box className={"detailContainer"}> 762 <SecureComponent 763 scopes={[ 764 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 765 IAM_SCOPES.S3_GET_ACTIONS, 766 ]} 767 resource={bucketName} 768 > 769 <Fragment> 770 <strong>Retention Policy:</strong> 771 <br /> 772 <span className={"capitalizeFirst"}> 773 {actualInfo.version_id && actualInfo.version_id !== "null" ? ( 774 <Fragment> 775 {actualInfo.retention_mode 776 ? actualInfo.retention_mode.toLowerCase() 777 : "None"} 778 </Fragment> 779 ) : ( 780 <Fragment> 781 {actualInfo.retention_mode 782 ? actualInfo.retention_mode.toLowerCase() 783 : "None"} 784 </Fragment> 785 )} 786 </span> 787 </Fragment> 788 </SecureComponent> 789 </Box> 790 {!actualInfo.is_delete_marker && ( 791 <Fragment> 792 <SimpleHeader label={"Metadata"} icon={<MetadataIcon />} /> 793 <Box className={"detailContainer"}> 794 {actualInfo && metaData ? ( 795 <ObjectMetaData metaData={metaData} /> 796 ) : null} 797 </Box> 798 </Fragment> 799 )} 800 </Box> 801 )} 802 </Fragment> 803 ); 804 }; 805 806 export default ObjectDetailPanel;