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;