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:
45
portal-ui/src/icons/ClosePanelIcon.tsx
Normal file
45
portal-ui/src/icons/ClosePanelIcon.tsx
Normal 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;
|
||||
@@ -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 **/
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
@@ -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));
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user