From e093efa93173f89996b8b1da6137f4bc6e53b4f9 Mon Sep 17 00:00:00 2001 From: Prakash Senthil Vel <23444145+prakashsvmx@users.noreply.github.com> Date: Thu, 27 Jan 2022 17:23:21 +0000 Subject: [PATCH] UX Object Details page (#1475) Co-authored-by: Lenin Alevski --- portal-ui/src/icons/RecoverIcon.tsx | 19 +- .../SummaryItems/EditablePropertyItem.tsx | 21 +- .../Objects/ObjectDetails/AddTagModal.tsx | 4 +- .../Objects/ObjectDetails/ObjectDetails.tsx | 406 +++++++----------- .../Objects/ObjectDetails/ObjectMetaData.tsx | 125 ++++++ .../Objects/ObjectDetails/ObjectTags.tsx | 93 ++++ .../ObjectDetails/RestoreFileVersion.tsx | 60 +-- 7 files changed, 420 insertions(+), 308 deletions(-) create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectMetaData.tsx create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx diff --git a/portal-ui/src/icons/RecoverIcon.tsx b/portal-ui/src/icons/RecoverIcon.tsx index 5423ae585..c788ca602 100644 --- a/portal-ui/src/icons/RecoverIcon.tsx +++ b/portal-ui/src/icons/RecoverIcon.tsx @@ -22,21 +22,14 @@ const RecoverIcon = (props: SVGProps) => ( xmlns="http://www.w3.org/2000/svg" className={`min-icon`} fill={"currentcolor"} - viewBox="0 0 256 256" + viewBox="0 0 256 255.999" {...props} > - - - - - - - - - - - - + ); diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx index 44bec014e..e1caec175 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/SummaryItems/EditablePropertyItem.tsx @@ -21,29 +21,33 @@ import ActionLink from "./ActionLink"; import { Box } from "@mui/material"; import EditActionButton from "./EditActionButton"; -type PolicyItemProps = { +type EditablePropertyItemProps = { isLoading: boolean; resourceName: string; iamScopes: string[]; property: any; value: any; onEdit: () => void; + secureCmpProps?: Record; }; const SecureAction = ({ resourceName, iamScopes, + secureCmpProps = {}, children, }: { resourceName: string; iamScopes: string[]; children: any; + secureCmpProps?: Record; }) => { return ( {children} @@ -54,10 +58,11 @@ const EditablePropertyItem = ({ isLoading = true, resourceName = "", iamScopes, + secureCmpProps = {}, property = null, value = null, onEdit, -}: PolicyItemProps) => { +}: EditablePropertyItemProps) => { return ( + } /> - + { - setIsSending(false); onCloseAndUpdate(true); + setIsSending(false); }) .catch((error: ErrorResponseHandler) => { setModalErrorSnackMessage(error); @@ -109,7 +109,7 @@ const AddTagModal = ({ modalOpen={modalOpen} title="Add New Tag to the Object" onClose={() => { - onCloseAndUpdate(false); + onCloseAndUpdate(true); }} titleIcon={} > diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index c4c27b3c4..f5738995d 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -15,38 +15,24 @@ // along with this program. If not, see . import React, { Fragment, useEffect, useState } from "react"; -import { connect } from "react-redux"; -import { withRouter } from "react-router-dom"; + import get from "lodash/get"; import * as reactMoment from "react-moment"; -import clsx from "clsx"; import { Theme } from "@mui/material/styles"; import createStyles from "@mui/styles/createStyles"; -import withStyles from "@mui/styles/withStyles"; -import { - CircularProgress, - LinearProgress, - Table, - TableBody, - TableCell, - TableRow, -} from "@mui/material"; +import { Box, CircularProgress, LinearProgress } from "@mui/material"; import Grid from "@mui/material/Grid"; -import Chip from "@mui/material/Chip"; -import TextField from "@mui/material/TextField"; -import IconButton from "@mui/material/IconButton"; -import InputAdornment from "@mui/material/InputAdornment"; -import AddIcon from "@mui/icons-material/Add"; -import CloseIcon from "@mui/icons-material/Close"; import ShareFile from "./ShareFile"; import { actionsTray, buttonsStyles, containerForHeader, hrClass, - searchField, + tableStyles, + spacingUtils, + textStyleUtils, } from "../../../../Common/FormComponents/common/styleLibrary"; -import { IFileInfo, MetadataResponse } from "./types"; +import { IFileInfo } from "./types"; import { download, extensionPreview } from "../utils"; import history from "../../../../../../history"; import api from "../../../../../../common/api"; @@ -54,7 +40,6 @@ import api from "../../../../../../common/api"; import TableWrapper, { ItemActions, } from "../../../../Common/TableWrapper/TableWrapper"; -import { AppState } from "../../../../../../store"; import { ErrorResponseHandler } from "../../../../../../common/types"; import { setErrorSnackMessage, @@ -81,6 +66,14 @@ import { updateProgress, } from "../../../../ObjectBrowser/actions"; import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton"; +import SearchBox from "../../../../Common/SearchBox"; +import ObjectTags from "./ObjectTags"; +import { AppState } from "../../../../../../store"; +import { connect } from "react-redux"; +import { withRouter } from "react-router-dom"; +import { withStyles } from "@mui/styles"; +import { DisabledIcon } from "../../../../../../icons"; +import LabelWithIcon from "../../../BucketDetails/SummaryItems/LabelWithIcon"; const RecoverIcon = React.lazy( () => import("../../../../../../icons/RecoverIcon") @@ -92,13 +85,17 @@ const DownloadIcon = React.lazy( const DeleteIcon = React.lazy( () => import("../../../../../../icons/DeleteIcon") ); -const EditIcon = React.lazy(() => import("../../../../../../icons/EditIcon")); -const SearchIcon = React.lazy( - () => import("../../../../../../icons/SearchIcon") -); + const ObjectBrowserIcon = React.lazy( () => import("../../../../../../icons/ObjectBrowserIcon") ); +const ObjectMetaData = React.lazy(() => import("./ObjectMetaData")); +const EditablePropertyItem = React.lazy( + () => import("../../../BucketDetails/SummaryItems/EditablePropertyItem") +); +const LabelValuePair = React.lazy( + () => import("../../../../Common/UsageBarWrapper/LabelValuePair") +); const styles = (theme: Theme) => createStyles({ @@ -161,7 +158,9 @@ const styles = (theme: Theme) => ...hrClass, ...buttonsStyles, ...actionsTray, - ...searchField, + ...tableStyles, + ...spacingUtils, + ...textStyleUtils, ...containerForHeader(theme.spacing(4)), }); @@ -192,6 +191,12 @@ const emptyFile: IFileInfo = { version_id: null, }; +const twoColCssGridLayoutConfig = { + display: "grid", + gridTemplateColumns: { xs: "1fr", sm: "2fr 1fr" }, + gridAutoFlow: { xs: "dense", sm: "row" }, + gap: 2, +}; const ObjectDetails = ({ classes, downloadingFiles, @@ -218,8 +223,6 @@ const ObjectDetails = ({ const [versions, setVersions] = useState([]); const [filterVersion, setFilterVersion] = useState(""); const [deleteOpen, setDeleteOpen] = useState(false); - const [metadataLoad, setMetadataLoad] = useState(true); - const [metadata, setMetadata] = useState({}); const [restoreVersionOpen, setRestoreVersionOpen] = useState(false); const [restoreVersion, setRestoreVersion] = useState(""); @@ -271,25 +274,6 @@ const ObjectDetails = ({ distributedSetup, ]); - useEffect(() => { - if (metadataLoad && internalPaths !== "") { - api - .invoke( - "GET", - `/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}` - ) - .then((res: MetadataResponse) => { - let metadata = get(res, "objectMetadata", {}); - - setMetadata(metadata); - setMetadataLoad(false); - }) - .catch((error: ErrorResponseHandler) => { - setMetadataLoad(false); - }); - } - }, [bucketName, metadataLoad, internalPaths]); - let tagKeys: string[] = []; if (actualInfo && actualInfo.tags) { @@ -421,7 +405,6 @@ const ObjectDetails = ({ const closeAddTagModal = (reloadObjectData: boolean) => { setTagModalOpen(false); - if (reloadObjectData) { setLoadObjectData(true); } @@ -429,7 +412,6 @@ const ObjectDetails = ({ const closeLegalholdModal = (reload: boolean) => { setLegalholdOpen(false); - if (reload) { setLoadObjectData(true); } @@ -437,7 +419,6 @@ const ObjectDetails = ({ const closeDeleteTagModal = (reloadObjectData: boolean) => { setDeleteTagModalOpen(false); - if (reloadObjectData) { setLoadObjectData(true); } @@ -449,7 +430,6 @@ const ObjectDetails = ({ if (reloadObjectData) { setLoadObjectData(true); - setMetadataLoad(true); } }; @@ -625,190 +605,131 @@ const ObjectDetails = ({

Details


- - - - - - - - - - - - - - - - - - - - - - -
Legal Hold: - {actualInfo.version_id && - actualInfo.version_id !== "null" ? ( - - {actualInfo.legal_hold_status - ? actualInfo.legal_hold_status.toLowerCase() - : "Off"} - - { - setLegalholdOpen(true); - }} - > - - - - - ) : ( - "Disabled" - )} -
Retention: - {actualInfo.retention_mode - ? actualInfo.retention_mode.toLowerCase() - : "None"} - - { - openRetentionModal(); - }} - > - - - -
Tags: - {tagKeys && - tagKeys.map((tagKey, index) => { - const tag = get( - actualInfo, - `tags.${tagKey}`, - "" - ); - if (tag !== "") { - return ( - - } - onDelete={() => { - deleteTag(tagKey, tag); - }} - /> - - ); - } - return null; - })} - - } - clickable - size="small" - label="Add tag" - color="primary" - variant="outlined" - onClick={() => { - setTagModalOpen(true); - }} - /> - -
-
- - -

Object Metadata

-
-
- - + + - - {Object.keys(metadata).map((element, index) => { - const renderItem = Array.isArray( - metadata[element] + { + setLegalholdOpen(true); + }} + isLoading={false} + /> + ) : ( + } + label={ + + } + /> + } + /> ) - ? metadata[element] - .map(decodeURIComponent) - .join(", ") - : decodeURIComponent(metadata[element]); + } + /> + - return ( - - - {element} - - - {renderItem} - - - ); - })} - -
-
-
+ + + ) : ( + } + label={ + + } + /> + } + /> + ) + } + /> + +
+ + + + { + setTagModalOpen(true); + }} + /> + } + /> + + + {actualInfo ? ( + + ) : null} ), }} @@ -828,30 +749,13 @@ const ObjectDetails = ({ {actualInfo.version_id && actualInfo.version_id !== "null" && ( - { - setFilterVersion(val.target.value); - }} - InputProps={{ - disableUnderline: true, - startAdornment: ( - - - - ), - }} - variant="standard" + onChange={setFilterVersion} /> )} - + {actualInfo.version_id && actualInfo.version_id !== "null" && ( + createStyles({ + propertiesIcon: { + marginLeft: 5, + "& .min-icon": { + height: 12, + }, + }, + + capitalizeFirst: { + textTransform: "capitalize", + "& .min-icon": { + width: 16, + height: 16, + }, + }, + titleCol: { + width: "25%", + }, + titleItem: { + width: "35%", + }, + + ...spacingUtils, + }); + +const ObjectMetaData = ({ + bucketName, + internalPaths, + classes, + actualInfo, +}: { + bucketName: string; + internalPaths: string; + classes?: any; + actualInfo: any; +}) => { + const [metaData, setMetaData] = useState({}); + + const onMetaDataSuccess = (res: MetadataResponse) => { + let metadata = get(res, "objectMetadata", {}); + + setMetaData(metadata); + }; + const onMetaDataError = (err: ErrorResponseHandler) => false; + + const [, invokeMetaDataApi] = useApi(onMetaDataSuccess, onMetaDataError); + + const metaKeys = Object.keys(metaData); + const loadMetaData = useCallback(() => { + invokeMetaDataApi( + "GET", + `/api/v1/buckets/${bucketName}/objects/metadata?prefix=${internalPaths}` + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [bucketName, internalPaths, actualInfo]); + + useEffect(() => { + if (actualInfo) { + loadMetaData(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actualInfo, loadMetaData]); + + return ( + + +

+ Object Metadata +

+
+ + + + + {metaKeys.map((element: string, index: number) => { + const renderItem = Array.isArray(metaData[element]) + ? metaData[element].map(decodeURIComponent).join(", ") + : decodeURIComponent(metaData[element]); + + return ( + + + {element} + + {renderItem} + + ); + })} + +
+
+
+ ); +}; + +export default withStyles(styles)(ObjectMetaData); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx new file mode 100644 index 000000000..8c5965fa6 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectTags.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions"; +import { Box } from "@mui/material"; +import get from "lodash/get"; +import SecureComponent from "../../../../../../common/SecureComponent/SecureComponent"; +import Chip from "@mui/material/Chip"; +import CloseIcon from "@mui/icons-material/Close"; +import AddIcon from "@mui/icons-material/Add"; + +const ObjectTags = ({ + tagKeys, + bucketName, + onDeleteTag, + onAddTagClick, + objectInfo, +}: { + tagKeys: any; + bucketName: string; + onDeleteTag: (key: string, v: string) => void; + onAddTagClick: () => void; + objectInfo: any; +}) => { + return ( + + + + + {tagKeys && + tagKeys.map((tagKey: string, index: number) => { + const tag = get(objectInfo, `tags.${tagKey}`, ""); + if (tag !== "") { + return ( + + } + onDelete={() => { + onDeleteTag(tagKey, tag); + }} + /> + + ); + } + return null; + })} + + + + } + clickable + size="small" + label="Add tag" + color="primary" + variant="outlined" + onClick={onAddTagClick} + /> + + + + + ); +}; + +export default ObjectTags; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/RestoreFileVersion.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/RestoreFileVersion.tsx index 1e76a9395..f9d4919c4 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/RestoreFileVersion.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/RestoreFileVersion.tsx @@ -15,14 +15,7 @@ // along with this program. If not, see . import React, { useState } from "react"; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogContentText, - DialogTitle, -} from "@mui/material"; +import { DialogContentText } from "@mui/material"; import { Theme } from "@mui/material/styles"; import { connect } from "react-redux"; import createStyles from "@mui/styles/createStyles"; @@ -32,6 +25,8 @@ import { setErrorSnackMessage } from "../../../../../../actions"; import { ErrorResponseHandler } from "../../../../../../common/types"; import { encodeFileName } from "../../../../../../common/utils"; import api from "../../../../../../common/api"; +import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog"; +import RecoverIcon from "../../../../../../icons/RecoverIcon"; interface IRestoreFileVersion { classes: any; @@ -79,41 +74,30 @@ const RestoreFileVersion = ({ }; return ( - } + onConfirm={restoreVersion} + confirmButtonProps={{ + color: "secondary", + variant: "outlined", + disabled: restoreLoading, + }} onClose={() => { onCloseAndUpdate(false); }} - aria-labelledby="alert-dialog-title" - aria-describedby="alert-dialog-description" - > - Restore File Version - + confirmationContent={ - Are you sure you want to restore {objectPath}
with - Version ID: {versionID}? + Are you sure you want to restore
+ {objectPath}
with Version ID: +
+ {versionID}?
-
- - - - -
+ } + /> ); };