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;