github.com/minio/console@v1.4.1/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 versionID: actualInfo?.version_id || "", 220 }) 221 .then((res) => { 222 let metadata = get(res.data, "objectMetadata", {}); 223 224 setMetaData(metadata); 225 setLoadingMetadata(false); 226 }) 227 .catch((err) => { 228 console.error("Error Getting Metadata Status: ", err.detailedError); 229 setLoadingMetadata(false); 230 }); 231 } 232 }, [bucketName, internalPaths, loadMetadata, actualInfo?.version_id]); 233 234 let tagKeys: string[] = []; 235 236 if (actualInfo && actualInfo.tags) { 237 tagKeys = Object.keys(actualInfo.tags); 238 } 239 240 const openRetentionModal = () => { 241 setRetentionModalOpen(true); 242 }; 243 244 const closeRetentionModal = (updateInfo: boolean) => { 245 setRetentionModalOpen(false); 246 if (updateInfo) { 247 dispatch(setLoadingObjectInfo(true)); 248 } 249 }; 250 251 const shareObject = () => { 252 setShareFileModalOpen(true); 253 }; 254 255 const closeShareModal = () => { 256 setObjectToShare(null); 257 setShareFileModalOpen(false); 258 }; 259 260 const closeFileOpen = () => { 261 setLongFileOpen(false); 262 }; 263 264 const closeDeleteModal = (closeAndReload: boolean) => { 265 setDeleteOpen(false); 266 267 if (closeAndReload && selectedVersion === "") { 268 onClosePanel(true); 269 } else { 270 dispatch(setLoadingVersions(true)); 271 dispatch(setSelectedVersion("")); 272 dispatch(setLoadingObjectInfo(true)); 273 } 274 }; 275 276 const closeAddTagModal = (reloadObjectData: boolean) => { 277 setTagModalOpen(false); 278 if (reloadObjectData) { 279 dispatch(setLoadingObjectInfo(true)); 280 } 281 }; 282 283 const closeInspectModal = (reloadObjectData: boolean) => { 284 setInspectModalOpen(false); 285 if (reloadObjectData) { 286 dispatch(setLoadingObjectInfo(true)); 287 } 288 }; 289 290 const closeLegalholdModal = (reload: boolean) => { 291 setLegalholdOpen(false); 292 if (reload) { 293 dispatch(setLoadingObjectInfo(true)); 294 } 295 }; 296 297 const loaderForContainer = ( 298 <div style={{ textAlign: "center", marginTop: 35 }}> 299 <Loader /> 300 </div> 301 ); 302 303 if (!actualInfo) { 304 if (loadingObjectInfo) { 305 return loaderForContainer; 306 } 307 308 return null; 309 } 310 311 const objectName = 312 objectNameArray.length > 0 313 ? objectNameArray[objectNameArray.length - 1] 314 : actualInfo.name; 315 316 const objectResources = [ 317 bucketName, 318 currentItem, 319 [bucketName, actualInfo.name].join("/"), 320 ]; 321 const canSetLegalHold = hasPermission(bucketName, [ 322 IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, 323 IAM_SCOPES.S3_PUT_ACTIONS, 324 ]); 325 const canSetTags = hasPermission(objectResources, [ 326 IAM_SCOPES.S3_PUT_OBJECT_TAGGING, 327 IAM_SCOPES.S3_PUT_ACTIONS, 328 ]); 329 330 const canChangeRetention = hasPermission( 331 objectResources, 332 [ 333 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 334 IAM_SCOPES.S3_PUT_OBJECT_RETENTION, 335 IAM_SCOPES.S3_GET_ACTIONS, 336 IAM_SCOPES.S3_PUT_ACTIONS, 337 ], 338 true, 339 ); 340 const canInspect = hasPermission(objectResources, [ 341 IAM_SCOPES.ADMIN_INSPECT_DATA, 342 ]); 343 const canChangeVersioning = hasPermission(objectResources, [ 344 IAM_SCOPES.S3_GET_BUCKET_VERSIONING, 345 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 346 IAM_SCOPES.S3_GET_OBJECT_VERSION, 347 IAM_SCOPES.S3_GET_ACTIONS, 348 IAM_SCOPES.S3_PUT_ACTIONS, 349 ]); 350 const canGetObject = hasPermission(objectResources, [ 351 IAM_SCOPES.S3_GET_OBJECT, 352 IAM_SCOPES.S3_GET_ACTIONS, 353 ]); 354 const canDelete = hasPermission( 355 [bucketName, currentItem, [bucketName, actualInfo.name].join("/")], 356 [IAM_SCOPES.S3_DELETE_OBJECT], 357 ); 358 359 let objectType: AllowedPreviews = previewObjectType(metaData, currentItem); 360 361 const multiActionButtons = [ 362 { 363 action: () => { 364 downloadObject(dispatch, bucketName, internalPaths, actualInfo); 365 }, 366 label: "Download", 367 disabled: !!actualInfo.is_delete_marker || !canGetObject, 368 icon: <DownloadIcon />, 369 tooltip: canGetObject 370 ? "Download this Object" 371 : permissionTooltipHelper( 372 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 373 "download this object", 374 ), 375 }, 376 { 377 action: () => { 378 shareObject(); 379 }, 380 label: "Share", 381 disabled: !!actualInfo.is_delete_marker || !canGetObject, 382 icon: <ShareIcon />, 383 tooltip: canGetObject 384 ? "Share this File" 385 : permissionTooltipHelper( 386 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 387 "share this object", 388 ), 389 }, 390 { 391 action: () => { 392 setPreviewOpen(true); 393 }, 394 label: "Preview", 395 disabled: 396 !!actualInfo.is_delete_marker || 397 (objectType === "none" && !canGetObject), 398 icon: <PreviewIcon />, 399 tooltip: canGetObject 400 ? "Preview this File" 401 : permissionTooltipHelper( 402 [IAM_SCOPES.S3_GET_OBJECT, IAM_SCOPES.S3_GET_ACTIONS], 403 "preview this object", 404 ), 405 }, 406 { 407 action: () => { 408 setLegalholdOpen(true); 409 }, 410 label: "Legal Hold", 411 disabled: 412 !locking || 413 !distributedSetup || 414 !!actualInfo.is_delete_marker || 415 !canSetLegalHold || 416 selectedVersion !== "", 417 icon: <LegalHoldIcon />, 418 tooltip: canSetLegalHold 419 ? locking 420 ? "Change Legal Hold rules for this File" 421 : "Object Locking must be enabled on this bucket in order to set Legal Hold" 422 : permissionTooltipHelper( 423 [IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD, IAM_SCOPES.S3_PUT_ACTIONS], 424 "change legal hold settings for this object", 425 ), 426 }, 427 { 428 action: openRetentionModal, 429 label: "Retention", 430 disabled: 431 !distributedSetup || 432 !!actualInfo.is_delete_marker || 433 !canChangeRetention || 434 selectedVersion !== "" || 435 !locking, 436 icon: <RetentionIcon />, 437 tooltip: canChangeRetention 438 ? locking 439 ? "Change Retention rules for this File" 440 : "Object Locking must be enabled on this bucket in order to set Retention Rules" 441 : permissionTooltipHelper( 442 [ 443 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 444 IAM_SCOPES.S3_PUT_OBJECT_RETENTION, 445 IAM_SCOPES.S3_GET_ACTIONS, 446 IAM_SCOPES.S3_PUT_ACTIONS, 447 ], 448 "change Retention Rules for this object", 449 ), 450 }, 451 { 452 action: () => { 453 setTagModalOpen(true); 454 }, 455 label: "Tags", 456 disabled: 457 !!actualInfo.is_delete_marker || selectedVersion !== "" || !canSetTags, 458 icon: <TagsIcon />, 459 tooltip: canSetTags 460 ? "Change Tags for this File" 461 : permissionTooltipHelper( 462 [ 463 IAM_SCOPES.S3_PUT_OBJECT_TAGGING, 464 IAM_SCOPES.S3_GET_OBJECT_TAGGING, 465 IAM_SCOPES.S3_GET_ACTIONS, 466 IAM_SCOPES.S3_PUT_ACTIONS, 467 ], 468 "set Tags on this object", 469 ), 470 }, 471 { 472 action: () => { 473 setInspectModalOpen(true); 474 }, 475 label: "Inspect", 476 disabled: 477 !distributedSetup || 478 !!actualInfo.is_delete_marker || 479 selectedVersion !== "" || 480 !canInspect, 481 icon: <InspectMenuIcon />, 482 tooltip: canInspect 483 ? "Inspect this file" 484 : permissionTooltipHelper( 485 [IAM_SCOPES.ADMIN_INSPECT_DATA], 486 "inspect this file", 487 ), 488 }, 489 { 490 action: () => { 491 dispatch( 492 setVersionsModeEnabled({ 493 status: !versionsMode, 494 objectName: objectName, 495 }), 496 ); 497 }, 498 label: versionsMode ? "Hide Object Versions" : "Display Object Versions", 499 icon: <VersionsIcon />, 500 disabled: 501 !distributedSetup || 502 !(actualInfo.version_id && actualInfo.version_id !== "null") || 503 !canChangeVersioning, 504 tooltip: canChangeVersioning 505 ? actualInfo.version_id && actualInfo.version_id !== "null" 506 ? "Display Versions for this file" 507 : "" 508 : permissionTooltipHelper( 509 [ 510 IAM_SCOPES.S3_GET_BUCKET_VERSIONING, 511 IAM_SCOPES.S3_PUT_BUCKET_VERSIONING, 512 IAM_SCOPES.S3_GET_OBJECT_VERSION, 513 IAM_SCOPES.S3_GET_ACTIONS, 514 IAM_SCOPES.S3_PUT_ACTIONS, 515 ], 516 "display all versions of this object", 517 ), 518 }, 519 ]; 520 521 const calculateLastModifyTime = (lastModified: string) => { 522 const currentTime = new Date(); 523 const modifiedTime = new Date(lastModified); 524 525 const difTime = currentTime.getTime() - modifiedTime.getTime(); 526 527 const formatTime = niceDaysInt(difTime, "ms"); 528 529 return formatTime.trim() !== "" ? `${formatTime} ago` : "Just now"; 530 }; 531 532 return ( 533 <Fragment> 534 {shareFileModalOpen && actualInfo && ( 535 <ShareFile 536 open={shareFileModalOpen} 537 closeModalAndRefresh={closeShareModal} 538 bucketName={bucketName} 539 dataObject={objectToShare || actualInfo} 540 /> 541 )} 542 {retentionModalOpen && actualInfo && ( 543 <SetRetention 544 open={retentionModalOpen} 545 closeModalAndRefresh={closeRetentionModal} 546 objectName={currentItem} 547 objectInfo={actualInfo} 548 bucketName={bucketName} 549 /> 550 )} 551 {deleteOpen && ( 552 <DeleteObject 553 deleteOpen={deleteOpen} 554 selectedBucket={bucketName} 555 selectedObject={internalPaths} 556 closeDeleteModalAndRefresh={closeDeleteModal} 557 versioningInfo={distributedSetup ? versioningInfo : undefined} 558 selectedVersion={selectedVersion} 559 /> 560 )} 561 {legalholdOpen && actualInfo && ( 562 <SetLegalHoldModal 563 open={legalholdOpen} 564 closeModalAndRefresh={closeLegalholdModal} 565 objectName={actualInfo.name || ""} 566 bucketName={bucketName} 567 actualInfo={actualInfo} 568 /> 569 )} 570 {previewOpen && actualInfo && ( 571 <PreviewFileModal 572 open={previewOpen} 573 bucketName={bucketName} 574 actualInfo={actualInfo} 575 onClosePreview={() => { 576 setPreviewOpen(false); 577 }} 578 /> 579 )} 580 {tagModalOpen && actualInfo && ( 581 <TagsModal 582 modalOpen={tagModalOpen} 583 bucketName={bucketName} 584 actualInfo={actualInfo} 585 onCloseAndUpdate={closeAddTagModal} 586 /> 587 )} 588 {inspectModalOpen && actualInfo && ( 589 <InspectObject 590 inspectOpen={inspectModalOpen} 591 volumeName={bucketName} 592 inspectPath={actualInfo.name || ""} 593 closeInspectModalAndRefresh={closeInspectModal} 594 /> 595 )} 596 {longFileOpen && actualInfo && ( 597 <RenameLongFileName 598 open={longFileOpen} 599 closeModal={closeFileOpen} 600 currentItem={currentItem} 601 bucketName={bucketName} 602 internalPaths={internalPaths} 603 actualInfo={actualInfo} 604 /> 605 )} 606 607 {loadingObjectInfo ? ( 608 <Fragment>{loaderForContainer}</Fragment> 609 ) : ( 610 <Box 611 sx={{ 612 "& .ObjectDetailsTitle": { 613 display: "flex", 614 alignItems: "center", 615 "& .min-icon": { 616 width: 26, 617 height: 26, 618 minWidth: 26, 619 minHeight: 26, 620 }, 621 }, 622 "& .objectNameContainer": { 623 whiteSpace: "nowrap", 624 textOverflow: "ellipsis", 625 overflow: "hidden", 626 alignItems: "center", 627 marginLeft: 10, 628 }, 629 "& .capitalizeFirst": { 630 textTransform: "capitalize", 631 }, 632 "& .detailContainer": { 633 padding: "0 22px", 634 marginBottom: 10, 635 fontSize: 14, 636 }, 637 }} 638 > 639 <ActionsList 640 title={ 641 <div className={"ObjectDetailsTitle"}> 642 {displayFileIconName(objectName || "", true)} 643 <span className={"objectNameContainer"}>{objectName}</span> 644 </div> 645 } 646 items={multiActionButtons} 647 /> 648 <TooltipWrapper 649 tooltip={ 650 canDelete 651 ? "" 652 : permissionTooltipHelper( 653 [IAM_SCOPES.S3_DELETE_OBJECT], 654 "delete this object", 655 ) 656 } 657 > 658 <Grid 659 item 660 xs={12} 661 sx={{ justifyContent: "center", display: "flex" }} 662 > 663 <SecureComponent 664 resource={[ 665 bucketName, 666 currentItem, 667 [bucketName, actualInfo.name].join("/"), 668 ]} 669 scopes={[IAM_SCOPES.S3_DELETE_OBJECT]} 670 errorProps={{ disabled: true }} 671 > 672 <Button 673 id={"delete-element-click"} 674 icon={<DeleteIcon />} 675 iconLocation={"start"} 676 fullWidth 677 variant={"secondary"} 678 onClick={() => { 679 setDeleteOpen(true); 680 }} 681 disabled={ 682 selectedVersion === "" && actualInfo.is_delete_marker 683 } 684 sx={{ 685 width: "calc(100% - 44px)", 686 margin: "8px 0", 687 }} 688 label={`Delete${selectedVersion !== "" ? " version" : ""}`} 689 /> 690 </SecureComponent> 691 </Grid> 692 </TooltipWrapper> 693 <SimpleHeader icon={<ObjectInfoIcon />} label={"Object Info"} /> 694 <Box className={"detailContainer"}> 695 <strong>Name:</strong> 696 <br /> 697 <div style={{ overflowWrap: "break-word" }}>{objectName}</div> 698 </Box> 699 {selectedVersion !== "" && ( 700 <Box className={"detailContainer"}> 701 <strong>Version ID:</strong> 702 <br /> 703 {selectedVersion} 704 </Box> 705 )} 706 <Box className={"detailContainer"}> 707 <strong>Size:</strong> 708 <br /> 709 {niceBytes(`${actualInfo.size || "0"}`)} 710 </Box> 711 {actualInfo.version_id && 712 actualInfo.version_id !== "null" && 713 selectedVersion === "" && ( 714 <Box className={"detailContainer"}> 715 <strong>Versions:</strong> 716 <br /> 717 {versions.length} version{versions.length !== 1 ? "s" : ""},{" "} 718 {niceBytesInt(totalVersionsSize)} 719 </Box> 720 )} 721 {selectedVersion === "" && ( 722 <Box className={"detailContainer"}> 723 <strong>Last Modified:</strong> 724 <br /> 725 {calculateLastModifyTime(actualInfo.last_modified || "")} 726 </Box> 727 )} 728 <Box className={"detailContainer"}> 729 <strong>ETAG:</strong> 730 <br /> 731 {actualInfo.etag || "N/A"} 732 </Box> 733 <Box className={"detailContainer"}> 734 <strong>Tags:</strong> 735 <br /> 736 {tagKeys.length === 0 737 ? "N/A" 738 : tagKeys.map((tagKey, index) => { 739 return ( 740 <span key={`key-vs-${index.toString()}`}> 741 {tagKey}:{get(actualInfo, `tags.${tagKey}`, "")} 742 {index < tagKeys.length - 1 ? ", " : ""} 743 </span> 744 ); 745 })} 746 </Box> 747 <Box className={"detailContainer"}> 748 <SecureComponent 749 scopes={[ 750 IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD, 751 IAM_SCOPES.S3_GET_ACTIONS, 752 ]} 753 resource={bucketName} 754 > 755 <Fragment> 756 <strong>Legal Hold:</strong> 757 <br /> 758 {actualInfo.legal_hold_status ? "On" : "Off"} 759 </Fragment> 760 </SecureComponent> 761 </Box> 762 <Box className={"detailContainer"}> 763 <SecureComponent 764 scopes={[ 765 IAM_SCOPES.S3_GET_OBJECT_RETENTION, 766 IAM_SCOPES.S3_GET_ACTIONS, 767 ]} 768 resource={bucketName} 769 > 770 <Fragment> 771 <strong>Retention Policy:</strong> 772 <br /> 773 <span className={"capitalizeFirst"}> 774 {actualInfo.version_id && actualInfo.version_id !== "null" ? ( 775 <Fragment> 776 {actualInfo.retention_mode 777 ? actualInfo.retention_mode.toLowerCase() 778 : "None"} 779 </Fragment> 780 ) : ( 781 <Fragment> 782 {actualInfo.retention_mode 783 ? actualInfo.retention_mode.toLowerCase() 784 : "None"} 785 </Fragment> 786 )} 787 </span> 788 </Fragment> 789 </SecureComponent> 790 </Box> 791 {!actualInfo.is_delete_marker && ( 792 <Fragment> 793 <SimpleHeader label={"Metadata"} icon={<MetadataIcon />} /> 794 <Box className={"detailContainer"}> 795 {actualInfo && metaData ? ( 796 <ObjectMetaData metaData={metaData} /> 797 ) : null} 798 </Box> 799 </Fragment> 800 )} 801 </Box> 802 )} 803 </Fragment> 804 ); 805 }; 806 807 export default ObjectDetailPanel;