Added object details panel (#1489)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>

Co-authored-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-01-28 21:05:23 -07:00
committed by GitHub
parent 4a1ccf19a0
commit 31f63a387e
11 changed files with 1007 additions and 39 deletions

View File

@@ -0,0 +1,45 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const ClosePanelIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<g transform="translate(14.827 15.767) rotate(180)">
<path
fill={"currentcolor"}
d="M-147.9-183c-4.1-4.1-10.8-4.1-14.9,0c0,0,0,0,0,0l-63.3,63.3c-4.1,4.1-4.1,10.8,0,14.9
c0,0,0,0,0,0l63.3,63.3c4.1,4.1,10.8,4.1,14.9,0c4.1-4.1,4.1-10.8,0-14.9l-55.9-55.9l55.9-55.9C-143.7-172.2-143.7-178.9-147.9-183
C-147.9-183-147.9-183-147.9-183L-147.9-183z"
/>
<path
fill={"currentcolor"}
d="M-60.4-112.2c0-5.8-4.7-10.5-10.5-10.5h-137.1c-5.8,0-10.6,4.7-10.6,10.6
c0,5.8,4.7,10.6,10.6,10.6h137.1C-65.1-101.7-60.4-106.4-60.4-112.2C-60.4-112.2-60.4-112.2-60.4-112.2z M-7.6,14.4
c-5.8,0-10.5-4.7-10.5-10.5v-232.2c0-5.8,4.7-10.6,10.6-10.6c5.8,0,10.6,4.7,10.6,10.6V3.9C2.9,9.7-1.8,14.4-7.6,14.4L-7.6,14.4z"
/>
</g>
</svg>
);
export default ClosePanelIcon;

View File

@@ -118,6 +118,7 @@ export { default as VersionIcon } from "./VersionIcon";
export { default as WarnIcon } from "./WarnIcon";
export { default as WarpIcon } from "./WarpIcon";
export { default as WatchIcon } from "./WatchIcon";
export { default as ClosePanelIcon } from "./ClosePanelIcon";
export { default as LoginMinIOLogo } from "./LoginMinIOLogo";
/*Modal Title Icons **/

View File

@@ -0,0 +1,78 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Grid, IconButton } from "@mui/material";
import { ClosePanelIcon } from "../../../../../../icons";
interface IDetailsListPanel {
classes: any;
open: boolean;
closePanel: () => void;
children: React.ReactNode;
}
const styles = (theme: Theme) =>
createStyles({
detailsList: {
borderColor: "#EAEDEE",
backgroundColor: "#fff",
borderWidth: 0,
borderStyle: "solid",
borderRadius: 3,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: 0,
transitionDuration: "0.3s",
overflowX: "hidden",
overflowY: "auto",
position:"relative",
opacity: 0,
marginLeft: -1,
"&.open": {
width: 400,
borderTopWidth: 1,
borderBottomWidth: 1,
borderRightWidth: 1,
borderLeftWidth: 1,
opacity: 1,
},
},
closePanel: {
position: "absolute",
right: 0,
top: 8,
"& .min-icon": {
width: 14,
}
}
});
const DetailsListPanel = ({ classes, open, closePanel, children }: IDetailsListPanel) => {
return (
<Grid item className={`${classes.detailsList} ${open ? "open" : ""}`}>
<IconButton onClick={closePanel} className={classes.closePanel}>
<ClosePanelIcon />
</IconButton>
{children}
</Grid>
);
};
export default withStyles(styles)(DetailsListPanel);

View File

@@ -91,6 +91,8 @@ import withSuspense from "../../../../Common/Components/withSuspense";
import { displayName } from "./utils";
import { DownloadIcon, PreviewIcon, ShareIcon } from "../../../../../../icons";
import UploadFilesButton from "../../UploadFilesButton";
import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
const AddFolderIcon = React.lazy(
() => import("../../../../../../icons/AddFolderIcon")
@@ -126,6 +128,9 @@ const styles = (theme: Theme) =>
createStyles({
browsePaper: {
height: "calc(100vh - 280px)",
"&.actionsPanelOpen": {
height: "100%",
},
},
"@global": {
".rowLine:hover .iconFileElm": {
@@ -276,6 +281,10 @@ const ListObjects = ({
const [iniLoad, setIniLoad] = useState<boolean>(false);
const [canShareFile, setCanShareFile] = useState<boolean>(false);
const [canPreviewFile, setCanPreviewFile] = useState<boolean>(false);
const [detailsOpen, setDetailsOpen] = useState<boolean>(false);
const [selectedInternalPaths, setSelectedInternalPaths] = useState<
string | null
>(null);
const internalPaths = get(match.params, "subpaths", "");
const bucketName = match.params["bucketName"];
@@ -676,11 +685,10 @@ const ListObjects = ({
};
const openPath = (idElement: string) => {
const newPath = `/buckets/${bucketName}/browse${
idElement ? `/${encodeFileName(idElement)}` : ``
}`;
history.push(newPath);
return;
setDetailsOpen(true);
setSelectedInternalPaths(
`${idElement ? `${encodeFileName(idElement)}` : ``}`
);
};
const uploadObject = useCallback(
@@ -1208,7 +1216,9 @@ const ListObjects = ({
entityName="Objects"
idField="name"
records={payload}
customPaperHeight={classes.browsePaper}
customPaperHeight={`${classes.browsePaper} ${
detailsOpen ? "actionsPanelOpen" : ""
}`}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={`This location is empty${
@@ -1303,6 +1313,20 @@ const ListObjects = ({
},
]}
/>
<DetailsListPanel
open={detailsOpen}
closePanel={() => {
setDetailsOpen(false);
setSelectedInternalPaths(null);
}}
>
{selectedInternalPaths !== null && (
<ObjectDetailPanel
internalPaths={selectedInternalPaths}
bucketName={bucketName}
/>
)}
</DetailsListPanel>
</SecureComponent>
</Grid>
</div>

View File

@@ -0,0 +1,72 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React from "react";
import { Button } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
type ObjectActionButtonProps = {
disabled?: boolean;
onClick: () => void | any;
icon: React.ReactNode;
label: string;
[x: string]: any;
};
const styles = (theme: Theme) =>
createStyles({
root: {
padding: "0 15px",
height: 22,
margin: 0,
color: "#5E5E5E",
fontWeight: "normal",
fontSize: 14,
whiteSpace: "nowrap",
width: "100%",
justifyContent: "flex-start",
"&:hover": {
backgroundColor: "transparent",
color: "#000",
},
"& .min-icon": {
width: 11,
},
"&:disabled": {
color: "#EBEBEB",
borderColor: "#EBEBEB",
},
},
});
const ObjectActionButton = ({
disabled,
onClick,
icon,
label,
classes,
...restProps
}: ObjectActionButtonProps) => {
return (
<Button {...restProps} onClick={onClick} className={classes.root} startIcon={icon}>
<span className={"buttonItem"}>{label}</span>
</Button>
);
};
export default withStyles(styles)(ObjectActionButton);

View File

@@ -0,0 +1,659 @@
// This file is part of MinIO Console Server
// Copyright (c) 2022 MinIO, Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
import React, { Fragment, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Box, LinearProgress } from "@mui/material";
import { withStyles } from "@mui/styles";
import createStyles from "@mui/styles/createStyles";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import {
actionsTray,
buttonsStyles,
spacingUtils,
textStyleUtils,
detailsPanel,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "../ObjectDetails/types";
import { download } from "../utils";
import { ErrorResponseHandler } from "../../../../../../common/types";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../../../actions";
import {
decodeFileName,
encodeFileName,
niceBytesInt,
} from "../../../../../../common/utils";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import {
completeObject,
setNewObject,
updateProgress,
} from "../../../../ObjectBrowser/actions";
import { AppState } from "../../../../../../store";
import {
DisabledIcon,
NextArrowIcon,
PreviewIcon,
} from "../../../../../../icons";
import { ShareIcon, DownloadIcon, DeleteIcon } from "../../../../../../icons";
import history from "../../../../../../history";
import api from "../../../../../../common/api";
import ShareFile from "../ObjectDetails/ShareFile";
import SetRetention from "../ObjectDetails/SetRetention";
import DeleteObject from "../ListObjects/DeleteObject";
import AddTagModal from "../ObjectDetails/AddTagModal";
import DeleteTagModal from "../ObjectDetails/DeleteTagModal";
import SetLegalHoldModal from "../ObjectDetails/SetLegalHoldModal";
import RestoreFileVersion from "../ObjectDetails/RestoreFileVersion";
import SecureComponent from "../../../../../../common/SecureComponent/SecureComponent";
import ObjectTags from "../ObjectDetails/ObjectTags";
import LabelWithIcon from "../../../BucketDetails/SummaryItems/LabelWithIcon";
import PreviewFileModal from "../Preview/PreviewFileModal";
import ObjectActionButton from "./ObjectActionButton";
import ObjectMetaData from "../ObjectDetails/ObjectMetaData";
import EditablePropertyItem from "../../../BucketDetails/SummaryItems/EditablePropertyItem";
import LabelValuePair from "../../../../Common/UsageBarWrapper/LabelValuePair";
const styles = () =>
createStyles({
tag: {
marginRight: 6,
fontSize: 10,
fontWeight: 700,
"&.MuiChip-sizeSmall": {
height: 18,
},
"& .min-icon": {
height: 10,
width: 10,
},
},
"@global": {
".progressDetails": {
paddingTop: 3,
display: "inline-block",
position: "relative",
width: 18,
height: 18,
},
".progressDetails > .MuiCircularProgress-root": {
position: "absolute",
left: 0,
top: 3,
},
},
...buttonsStyles,
...actionsTray,
...spacingUtils,
...textStyleUtils,
...detailsPanel,
});
interface IObjectDetailPanelProps {
classes: any;
internalPaths: string;
bucketName: string;
rewindEnabled: boolean;
rewindDate: any;
bucketToRewind: string;
distributedSetup: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
}
const emptyFile: IFileInfo = {
is_latest: true,
last_modified: "",
legal_hold_status: "",
name: "",
retention_mode: "",
retention_until_date: "",
size: "0",
tags: {},
version_id: null,
};
const ObjectDetailPanel = ({
classes,
internalPaths,
bucketName,
distributedSetup,
setErrorSnackMessage,
setNewObject,
updateProgress,
completeObject,
}: IObjectDetailPanelProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
const [tagModalOpen, setTagModalOpen] = useState<boolean>(false);
const [deleteTagModalOpen, setDeleteTagModalOpen] = useState<boolean>(false);
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
const [restoreVersionOpen, setRestoreVersionOpen] = useState<boolean>(false);
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
const [restoreVersion, setRestoreVersion] = useState<string>("");
const [totalVersionsSize, setTotalVersionsSize] = useState<number>(0);
const internalPathsDecoded = decodeFileName(internalPaths) || "";
const allPathData = internalPathsDecoded.split("/");
const currentItem = allPathData.pop() || "";
// calculate object name to display
let objectNameArray: string[] = [];
if (actualInfo) {
objectNameArray = actualInfo.name.split("/");
}
useEffect(() => {
if (bucketName !== "" && internalPaths) {
setLoadObjectData(true);
}
}, [internalPaths, bucketName]);
useEffect(() => {
if (loadObjectData && internalPaths !== "") {
api
.invoke(
"GET",
`/api/v1/buckets/${bucketName}/objects?prefix=${internalPaths}${
distributedSetup ? "&with_versions=true" : ""
}`
)
.then((res: IFileInfo[]) => {
const result = get(res, "objects", []);
if (distributedSetup) {
setActualInfo(
result.find((el: IFileInfo) => el.is_latest) || emptyFile
);
setVersions(result);
const tVersionSize = result.reduce(
(acc: number, currValue: IFileInfo) => {
if (currValue?.size) {
return acc + currValue.size;
}
return acc;
},
0
);
setTotalVersionsSize(tVersionSize);
} else {
setActualInfo(result[0]);
setVersions([]);
}
setLoadObjectData(false);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setLoadObjectData(false);
});
}
}, [
loadObjectData,
bucketName,
internalPaths,
setErrorSnackMessage,
distributedSetup,
]);
let tagKeys: string[] = [];
if (actualInfo && actualInfo.tags) {
tagKeys = Object.keys(actualInfo.tags);
}
const openRetentionModal = () => {
setRetentionModalOpen(true);
};
const closeRetentionModal = (updateInfo: boolean) => {
setRetentionModalOpen(false);
if (updateInfo) {
setLoadObjectData(true);
}
};
const shareObject = () => {
setShareFileModalOpen(true);
};
const closeShareModal = () => {
setObjectToShare(null);
setShareFileModalOpen(false);
};
const deleteTag = (tagKey: string, tagLabel: string) => {
setSelectedTag([tagKey, tagLabel]);
setDeleteTagModalOpen(true);
};
const downloadObject = (object: IFileInfo) => {
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
bucketName,
internalPaths,
object.version_id,
parseInt(object.size || "0"),
(progress) => {
updateProgress(identityDownload, progress);
},
() => {
completeObject(identityDownload);
}
);
};
const closeDeleteModal = (redirectBack: boolean) => {
setDeleteOpen(false);
if (redirectBack) {
const newPath = allPathData.join("/");
history.push(
`/buckets/${bucketName}/browse${
newPath === "" ? "" : `/${encodeFileName(newPath)}`
}`
);
}
};
const closeAddTagModal = (reloadObjectData: boolean) => {
setTagModalOpen(false);
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closeLegalholdModal = (reload: boolean) => {
setLegalholdOpen(false);
if (reload) {
setLoadObjectData(true);
}
};
const closeDeleteTagModal = (reloadObjectData: boolean) => {
setDeleteTagModalOpen(false);
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closeRestoreModal = (reloadObjectData: boolean) => {
setRestoreVersionOpen(false);
setRestoreVersion("");
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const closePreviewWindow = () => {
setPreviewOpen(false);
};
const openExtraInfo = () => {
const newPath = `/buckets/${bucketName}/browse${
internalPaths !== "" ? `/${internalPaths}` : ``
}`;
history.push(newPath);
};
if (!actualInfo) {
return null;
}
return (
<Fragment>
{shareFileModalOpen && actualInfo && (
<ShareFile
open={shareFileModalOpen}
closeModalAndRefresh={closeShareModal}
bucketName={bucketName}
dataObject={objectToShare || actualInfo}
/>
)}
{retentionModalOpen && actualInfo && (
<SetRetention
open={retentionModalOpen}
closeModalAndRefresh={closeRetentionModal}
objectName={currentItem}
objectInfo={actualInfo}
bucketName={bucketName}
/>
)}
{deleteOpen && (
<DeleteObject
deleteOpen={deleteOpen}
selectedBucket={bucketName}
selectedObject={internalPaths}
closeDeleteModalAndRefresh={closeDeleteModal}
versioning={distributedSetup}
/>
)}
{tagModalOpen && actualInfo && (
<AddTagModal
modalOpen={tagModalOpen}
currentTags={actualInfo.tags}
selectedObject={internalPaths}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeAddTagModal}
/>
)}
{deleteTagModalOpen && actualInfo && (
<DeleteTagModal
deleteOpen={deleteTagModalOpen}
currentTags={actualInfo.tags}
selectedObject={actualInfo.name}
versionId={actualInfo.version_id}
bucketName={bucketName}
onCloseAndUpdate={closeDeleteTagModal}
selectedTag={selectedTag}
/>
)}
{legalholdOpen && actualInfo && (
<SetLegalHoldModal
open={legalholdOpen}
closeModalAndRefresh={closeLegalholdModal}
objectName={actualInfo.name}
bucketName={bucketName}
actualInfo={actualInfo}
/>
)}
{restoreVersionOpen && actualInfo && (
<RestoreFileVersion
restoreOpen={restoreVersionOpen}
bucketName={bucketName}
versionID={restoreVersion}
objectPath={actualInfo.name}
onCloseAndUpdate={closeRestoreModal}
/>
)}
{previewOpen && actualInfo && (
<PreviewFileModal
open={previewOpen}
bucketName={bucketName}
object={{
name: actualInfo.name,
version_id: actualInfo.version_id || "null",
size: parseInt(actualInfo.size || "0"),
content_type: "",
last_modified: new Date(actualInfo.last_modified),
}}
onClosePreview={closePreviewWindow}
/>
)}
{!actualInfo && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
<div className={classes.titleLabel}>
{objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name}
</div>
<ul className={classes.objectActions}>
<li>Object Actions:</li>
<li>
<ObjectActionButton
label={"Download"}
icon={<DownloadIcon />}
onClick={() => {
downloadObject(actualInfo);
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<li>
<ObjectActionButton
label={"Share"}
icon={<ShareIcon />}
onClick={() => {
shareObject();
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<li>
<ObjectActionButton
label={"Preview"}
icon={<PreviewIcon />}
onClick={() => {
setPreviewOpen(true);
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
<SecureComponent
scopes={[IAM_SCOPES.S3_DELETE_OBJECT]}
resource={bucketName}
matchAll
errorProps={{ disabled: true }}
>
<li>
<ObjectActionButton
label={"Delete"}
icon={<DeleteIcon />}
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
/>
</li>
</SecureComponent>
<li>
<ObjectActionButton
label={"Expand Details"}
icon={<NextArrowIcon />}
onClick={() => {
openExtraInfo();
}}
/>
</li>
</ul>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
<Box className={classes.detailContainer}>
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
</Box>
<Box className={classes.detailContainer}>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_LEGAL_HOLD]}
resource={bucketName}
>
<LabelValuePair
label={""}
value={
actualInfo.version_id && actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_OBJECT_LEGAL_HOLD]}
secureCmpProps={{
matchAll: false,
errorProps: {
disabled: true,
onClick: null,
},
}}
resourceName={bucketName}
property={"Legal Hold:"}
value={
actualInfo.legal_hold_status
? actualInfo.legal_hold_status.toLowerCase()
: "Off"
}
onEdit={() => {
setLegalholdOpen(true);
}}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Legal Hold:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>Disabled</label>
}
/>
}
/>
)
}
/>
</SecureComponent>
<SecureComponent
scopes={[IAM_SCOPES.S3_GET_OBJECT_RETENTION]}
resource={bucketName}
>
<LabelValuePair
label={""}
value={
actualInfo.version_id && actualInfo.version_id !== "null" ? (
<EditablePropertyItem
iamScopes={[IAM_SCOPES.S3_PUT_OBJECT_RETENTION]}
secureCmpProps={{
matchAll: false,
}}
resourceName={bucketName}
property={"Retention:"}
value={
actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"
}
onEdit={openRetentionModal}
isLoading={false}
/>
) : (
<LabelValuePair
label={"Retention:"}
value={
<LabelWithIcon
icon={<DisabledIcon />}
label={
<label className={classes.textMuted}>Disabled</label>
}
/>
}
/>
)
}
/>
</SecureComponent>
</Box>
<hr className={classes.hrClass} />
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Object Metadata</h1>
</div>
<Box className={classes.detailContainer}>
{actualInfo ? (
<ObjectMetaData
bucketName={bucketName}
internalPaths={internalPaths}
actualInfo={actualInfo}
linear
/>
) : null}
</Box>
<hr className={classes.hrClass} />
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Versions</h1>
</div>
<Box className={classes.detailContainer}>
<Box className={classes.metadataLinear}>
<strong>Total available versions</strong>
<br />
{versions.length}
</Box>
<Box className={classes.metadataLinear}>
<strong>Versions Stored size:</strong>
<br />
{niceBytesInt(totalVersionsSize)}
</Box>
</Box>
</Fragment>
)}
</Fragment>
);
};
const mapStateToProps = ({ objectBrowser, system }: AppState) => ({
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
distributedSetup: get(system, "distributedSetup", false),
});
const mapDispatchToProps = {
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(ObjectDetailPanel));

View File

@@ -200,16 +200,12 @@ const twoColCssGridLayoutConfig = {
const ObjectDetails = ({
classes,
downloadingFiles,
rewindEnabled,
rewindDate,
distributedSetup,
match,
bucketToRewind,
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
match,
}: IObjectDetailsProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);

View File

@@ -1,15 +1,23 @@
import React, { useCallback, useEffect, useState } from "react";
import React, { useCallback, useEffect, useState, Fragment } from "react";
import useApi from "../../../../Common/Hooks/useApi";
import { ErrorResponseHandler } from "../../../../../../common/types";
import { MetadataResponse } from "./types";
import get from "lodash/get";
import Grid from "@mui/material/Grid";
import { Table, TableBody, TableCell, TableRow } from "@mui/material";
import { Box, Table, TableBody, TableCell, TableRow } from "@mui/material";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { spacingUtils } from "../../../../Common/FormComponents/common/styleLibrary";
import {detailsPanel, spacingUtils} from "../../../../Common/FormComponents/common/styleLibrary";
import { withStyles } from "@mui/styles";
interface IObjectMetadata {
bucketName: string;
internalPaths: string;
classes?: any;
actualInfo: any;
linear?: boolean;
}
const styles = (theme: Theme) =>
createStyles({
propertiesIcon: {
@@ -32,8 +40,8 @@ const styles = (theme: Theme) =>
titleItem: {
width: "35%",
},
...spacingUtils,
...detailsPanel,
});
const ObjectMetaData = ({
@@ -41,12 +49,8 @@ const ObjectMetaData = ({
internalPaths,
classes,
actualInfo,
}: {
bucketName: string;
internalPaths: string;
classes?: any;
actualInfo: any;
}) => {
linear = false,
}: IObjectMetadata) => {
const [metaData, setMetaData] = useState<any>({});
const onMetaDataSuccess = (res: MetadataResponse) => {
@@ -74,6 +78,26 @@ const ObjectMetaData = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [actualInfo, loadMetaData]);
if (linear) {
return (
<Fragment>
{metaKeys.map((element: string, index: number) => {
const renderItem = Array.isArray(metaData[element])
? metaData[element].map(decodeURIComponent).join(", ")
: decodeURIComponent(metaData[element]);
return (
<Box className={classes.metadataLinear}>
<strong>{element}</strong>
<br />
{renderItem}
</Box>
);
})}
</Fragment>
);
}
return (
<Grid container>
<Grid

View File

@@ -1074,6 +1074,8 @@ export const serviceAccountStyles: any = {
export const tableStyles: any = {
tableBlock: {
display: "flex",
flexDirection: "row",
"& .ReactVirtualized__Table__headerRow.rowLine, .ReactVirtualized__Table__row.rowLine":
{
borderBottom: "1px solid #EAEAEA",
@@ -1267,6 +1269,65 @@ export const textStyleUtils: any = {
},
};
export const detailsPanel: any = {
metadataLinear: {
marginBottom: 15,
fontSize: 14,
maxHeight: 180,
overflowY: "auto",
},
hrClass: {
borderTop: 0,
borderLeft: 0,
borderRight: 0,
borderColor: "#E2E2E2",
backgroundColor: "transparent",
},
sectionTitle: {
fontSize: 18,
color: "#000",
fontWeight: "bold",
borderBottom: "#E2E2E2 1px solid",
margin: "10px 22px",
paddingBottom: 18,
width: "100%",
},
detailContainer: {
padding: "0 22px",
marginBottom: 20,
fontSize: 14,
},
titleLabel: {
fontSize: 14,
fontWeight: "bold",
color: "#000",
padding: "12px 22px 8px 22px",
},
objectActions: {
backgroundColor: "#F8F8F8",
border: "#F1F1F1 1px solid",
borderRadius: 3,
margin: "8px 22px",
padding: 0,
color: "#696969",
"& li": {
listStyle: "none",
padding: 6,
margin: 0,
borderBottom: "#E5E5E5 1px solid",
fontSize: 14,
"&:first-of-type": {
padding: 10,
fontWeight: "bold",
color: "#000",
},
"&:last-of-type": {
borderBottom: 0,
},
},
},
};
// These classes are meant to be used as React.CSSProperties for TableWrapper
export const TableRowPredefStyles: any = {
deleted: {

View File

@@ -31,6 +31,7 @@ import {
BucketsIcon,
CalendarIcon,
CircleIcon,
ClosePanelIcon,
ClustersIcon,
CollapseIcon,
ComputerLineIcon,
@@ -184,6 +185,10 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
<CircleIcon /> <br />
CircleIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<ClosePanelIcon /> <br />
ClosePanelIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<ClustersIcon /> <br />
ClustersIcon

View File

@@ -30,6 +30,7 @@ const styles = (theme: Theme) =>
color: "#5E5E5E",
fontWeight: "normal",
fontSize: 14,
whiteSpace: "nowrap",
borderRight: "#E5E5E5 1px solid",
borderStyle: "solid",
borderRadius: 0,
@@ -105,26 +106,28 @@ const TopActionButton = ({
}: ITopActionButton) => {
return (
<Tooltip title={tooltip || ""}>
<Button
{...rest}
className={clsx(classes.root, {
[classes.contained]: variant === "contained",
})}
sx={{
"& span.buttonItem": {
"@media (max-width: 1279px)": {
display: "none",
<span>
<Button
{...rest}
className={clsx(classes.root, {
[classes.contained]: variant === "contained",
})}
sx={{
"& span.buttonItem": {
"@media (max-width: 1279px)": {
display: "none",
},
},
},
"& span": {
"@media (max-width: 1279px)": {
margin: 0,
"& span": {
"@media (max-width: 1279px)": {
margin: 0,
},
},
},
}}
>
<span className={"buttonItem"}>{children}</span>
</Button>
}}
>
<span className={"buttonItem"}>{children}</span>
</Button>
</span>
</Tooltip>
);
};