Added navigation support to object versions (#1626)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-02-28 09:17:57 -07:00
committed by GitHub
parent e52fb7d8b5
commit 3395d1c853
19 changed files with 1429 additions and 400 deletions

View File

@@ -0,0 +1,53 @@
// 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 NewPathIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<g>
<path
d="M23.4,121.5c-11.5,0-21.4,9.8-21.4,21.2c0.2,11.8,9.7,21.2,21.4,21.4
c11.4,0,21.2-9.9,21.2-21.4C44.3,131.1,35,121.7,23.4,121.5"
/>
<path
d="M23.4,175.4c-11.5,0-21.4,9.8-21.4,21.2c0.2,11.8,9.7,21.2,21.4,21.4
c11.4,0,21.2-9.9,21.2-21.4C44.3,184.9,35,175.6,23.4,175.4"
/>
<path
d="M158.6,40.2h-12.2c-4.3,0-8.3,2.5-10.2,6.4l-76.6,157c-2.7,5.6-0.4,12.4,5.2,15.2
c1.6,0.8,3.3,1.2,5,1.2H82c4.3,0,8.3-2.5,10.2-6.4l76.6-157c2.7-5.6,0.4-12.4-5.2-15.2C162,40.6,160.3,40.2,158.6,40.2"
/>
<path
d="M205,121.1c-1.2,0-2.4,0.1-3.6,0.1L233,56.5c2.7-5.6,0.4-12.4-5.2-15.2
c-1.6-0.8-3.3-1.2-5-1.2h-12.2c-4.3,0-8.3,2.5-10.2,6.4l-76.6,157c-2.7,5.6-0.4,12.4,5.2,15.2c1.6,0.8,3.3,1.2,5,1.2h12.2
c4.3,0,8.3-2.5,10.2-6.4L165,196c14.8,22.1,44.7,28.1,66.8,13.3s28.1-44.7,13.3-66.8C236.2,129.1,221.1,121.1,205,121.1
M205.3,207.3c-21,0-38.1-17-38.1-38.1c0-21,17-38.1,38.1-38.1c21,0,38.1,17,38.1,38.1c0,0,0,0,0,0
C243.4,190.3,226.3,207.3,205.3,207.3"
/>
<path d="M211.3,151.3h-11.9v11.9h-11.9v11.9h11.9v11.9h11.9v-11.9h11.9v-11.9h-11.9V151.3z" />
</g>
</svg>
);
export default NewPathIcon;

View File

@@ -0,0 +1,39 @@
// This file is part of MinIO Console Server
// Copyright (c) 2021 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 VersionsIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path
id="Path_7269"
d="M147.85,227.97c-2.7,0-4.89-2.19-4.89-4.89l0,0V32.93c0-2.7,2.19-4.89,4.89-4.89c0,0,0,0,0,0
h98.98c2.7,0,4.89,2.19,4.89,4.89c0,0,0,0,0,0v190.14c0,2.7-2.19,4.89-4.89,4.89l0,0H147.85z M71.37,205.43
c-2.7,0-4.89-2.19-4.89-4.89l0,0V55.48c-0.01-2.7,2.17-4.9,4.87-4.91c0.01,0,0.01,0,0.02,0h56.4c2.7,0,4.89,2.19,4.89,4.89l0,0
v145.05c0,2.7-2.19,4.89-4.89,4.89c0,0,0,0,0,0L71.37,205.43z M9.17,182.88c-2.7,0-4.88-2.18-4.89-4.87V78.02
c0-2.7,2.19-4.89,4.89-4.89h42.15c2.7,0,4.89,2.19,4.89,4.89V178c0,2.7-2.19,4.89-4.89,4.89l0,0L9.17,182.88z"
/>
</svg>
);
export default VersionsIcon;

View File

@@ -167,3 +167,5 @@ export { default as FileVideoIcon } from "./FileVideoIcon";
export { default as ChangePasswordIcon } from "./ChangePasswordIcon";
export { default as LockIcon } from "./LockIcon";
export { default as BackCaretIcon } from "./BackCaretIcon";
export { default as VersionsIcon } from "./VersionsIcon";
export { default as NewPathIcon } from "./NewPathIcon";

View File

@@ -19,16 +19,15 @@ import { connect } from "react-redux";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Link } from "react-router-dom";
import { Grid, IconButton, Tooltip } from "@mui/material";
import get from "lodash/get";
import { AppState } from "../../../../store";
import { containerForHeader } from "../../Common/FormComponents/common/styleLibrary";
import {
setFileModeEnabled,
setSearchObjects,
setVersionsModeEnabled,
setSearchVersions,
} from "../../ObjectBrowser/actions";
import ObjectDetails from "../ListBuckets/Objects/ObjectDetails/ObjectDetails";
import ListObjects from "../ListBuckets/Objects/ListObjects/ListObjects";
import PageHeader from "../../Common/PageHeader/PageHeader";
import SettingsIcon from "../../../../icons/SettingsIcon";
@@ -44,15 +43,18 @@ import SearchBox from "../../Common/SearchBox";
import BackLink from "../../../../common/BackLink";
interface IBrowserHandlerProps {
fileMode: boolean;
versionsMode: boolean;
match: any;
history: any;
classes: any;
setFileModeEnabled: typeof setFileModeEnabled;
setVersionsModeEnabled: typeof setVersionsModeEnabled;
setErrorSnackMessage: typeof setErrorSnackMessage;
bucketInfo: BucketInfo | null;
searchObjects: string;
versionedFile: string;
searchVersions: string;
setSearchObjects: typeof setSearchObjects;
setSearchVersions: typeof setSearchVersions;
}
const styles = (theme: Theme) =>
@@ -71,20 +73,23 @@ const styles = (theme: Theme) =>
});
const BrowserHandler = ({
fileMode,
versionsMode,
match,
history,
classes,
setFileModeEnabled,
setVersionsModeEnabled,
searchObjects,
setSearchObjects,
setSearchVersions,
versionedFile,
searchVersions,
}: IBrowserHandlerProps) => {
const bucketName = match.params["bucketName"];
const internalPaths = get(match.params, "subpaths", "");
useEffect(() => {
setFileModeEnabled(false);
}, [internalPaths, setFileModeEnabled]);
setVersionsModeEnabled(false);
}, [internalPaths, setVersionsModeEnabled]);
const openBucketConfiguration = () => {
history.push(`/buckets/${bucketName}/admin`);
@@ -94,24 +99,11 @@ const BrowserHandler = ({
<Fragment>
<PageHeader
label={
<Fragment>
{fileMode ? (
<Fragment>
<Link to={"/buckets"} className={classes.breadcrumLink}>
Buckets
</Link>{" "}
&gt; {bucketName}
</Fragment>
) : (
<Fragment>
<BackLink
label={"Back to Buckets"}
to={"/buckets"}
className={classes.backToBuckets}
/>
</Fragment>
)}
</Fragment>
<BackLink
label={"Back to Buckets"}
to={"/buckets"}
className={classes.backToBuckets}
/>
}
actions={
<SecureComponent
@@ -134,40 +126,55 @@ const BrowserHandler = ({
}
middleComponent={
<Fragment>
{!fileMode && (
{!versionsMode ? (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<SearchBox
placeholder={"Start typing to filter objects in bucket"}
placeholder={"Start typing to filter objects in the bucket"}
onChange={(value) => {
setSearchObjects(value);
}}
value={searchObjects}
/>
</SecureComponent>
) : (
<Fragment>
<SearchBox
placeholder={`Start typing to filter versions of ${versionedFile}`}
onChange={(value) => {
setSearchVersions(value);
}}
value={searchVersions}
/>
</Fragment>
)}
</Fragment>
}
/>
<Grid>{fileMode ? <ObjectDetails /> : <ListObjects />}</Grid>
<Grid>
<ListObjects />
</Grid>
</Fragment>
);
};
const mapStateToProps = ({ objectBrowser, buckets }: AppState) => ({
fileMode: get(objectBrowser, "fileMode", false),
versionsMode: get(objectBrowser, "versionsMode", false),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
bucketInfo: buckets.bucketDetails.bucketInfo,
searchObjects: objectBrowser.searchObjects,
versionedFile: objectBrowser.versionedFile,
searchVersions: objectBrowser.searchVersions,
});
const mapDispatchToProps = {
setFileModeEnabled,
setVersionsModeEnabled,
setErrorSnackMessage,
setSearchObjects,
setSearchVersions,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -26,7 +26,6 @@ import {
modalStyleUtils,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { connect } from "react-redux";
import { setFileModeEnabled } from "../../../../ObjectBrowser/actions";
import history from "../../../../../../history";
import { decodeFileName, encodeFileName } from "../../../../../../common/utils";
import { setModalErrorSnackMessage } from "../../../../../../actions";
@@ -38,7 +37,6 @@ interface ICreateFolder {
modalOpen: boolean;
bucketName: string;
folderName: string;
setFileModeEnabled: typeof setFileModeEnabled;
setModalErrorSnackMessage: typeof setModalErrorSnackMessage;
onClose: () => any;
existingFiles: BucketObject[];
@@ -55,7 +53,6 @@ const CreateFolderModal = ({
folderName,
bucketName,
onClose,
setFileModeEnabled,
setModalErrorSnackMessage,
classes,
existingFiles,
@@ -90,7 +87,6 @@ const CreateFolderModal = ({
`${folderPath}${pathUrl}`
)}/`;
history.push(newPath);
setFileModeEnabled(false);
onClose();
};
@@ -162,7 +158,6 @@ const CreateFolderModal = ({
};
const mapDispatchToProps = {
setFileModeEnabled,
setModalErrorSnackMessage,
};

View File

@@ -46,7 +46,8 @@ const styles = (theme: Theme) =>
opacity: 0,
marginLeft: -1,
"&.open": {
width: 400,
width: 300,
minWidth: 300,
borderTopWidth: 1,
borderBottomWidth: 1,
borderRightWidth: 1,

View File

@@ -43,7 +43,6 @@ import TableWrapper, {
import {
decodeFileName,
encodeFileName,
niceBytes,
niceBytesInt,
} from "../../../../../../common/utils";
@@ -51,19 +50,19 @@ import {
actionsTray,
containerForHeader,
objectBrowserCommon,
objectBrowserExtras,
searchField,
tableStyles,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { Badge, Typography } from "@mui/material";
import * as reactMoment from "react-moment";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import {
completeObject,
openList,
resetRewind,
setFileModeEnabled,
setNewObject,
setSearchObjects,
setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/actions";
import { Route } from "../../../../ObjectBrowser/reducers";
@@ -89,7 +88,6 @@ import {
} from "../../../../../../common/SecureComponent";
import withSuspense from "../../../../Common/Components/withSuspense";
import { displayName } from "./utils";
import {
BucketsIcon,
DownloadIcon,
@@ -101,6 +99,8 @@ import DetailsListPanel from "./DetailsListPanel";
import ObjectDetailPanel from "./ObjectDetailPanel";
import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton";
import MultiSelectionPanel from "./MultiSelectionPanel";
import { listModeColumns, rewindModeColumns } from "./ListObjectsHelpers";
import VersionsNavigator from "../ObjectDetails/VersionsNavigator";
const HistoryIcon = React.lazy(
() => import("../../../../../../icons/HistoryIcon")
@@ -169,17 +169,7 @@ const styles = (theme: Theme) =>
borderBottom: 0,
padding: "0.8rem 15px 0",
},
titleSpacer: {
marginLeft: 10,
},
listIcon: {
display: "block",
marginTop: "-10px",
"& .min-icon": {
width: 20,
height: 20,
},
},
...objectBrowserExtras,
...objectBrowserCommon,
...containerForHeader(theme.spacing(4)),
});
@@ -216,16 +206,17 @@ interface IListObjectsProps {
setSnackBarMessage: typeof setSnackBarMessage;
setErrorSnackMessage: typeof setErrorSnackMessage;
resetRewind: typeof resetRewind;
setFileModeEnabled: typeof setFileModeEnabled;
loadingBucket: boolean;
setBucketInfo: typeof setBucketInfo;
bucketInfo: BucketInfo | null;
versionsMode: boolean;
setBucketDetailsLoad: typeof setBucketDetailsLoad;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
openList: typeof openList;
setSearchObjects: typeof setSearchObjects;
setVersionsModeEnabled: typeof setVersionsModeEnabled;
}
function useInterval(callback: any, delay: number) {
@@ -263,7 +254,6 @@ const ListObjects = ({
setSnackBarMessage,
setErrorSnackMessage,
resetRewind,
setFileModeEnabled,
setBucketDetailsLoad,
loadingBucket,
setBucketInfo,
@@ -273,7 +263,9 @@ const ListObjects = ({
completeObject,
setSearchObjects,
searchObjects,
versionsMode,
openList,
setVersionsModeEnabled,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
@@ -546,14 +538,14 @@ const ListObjects = ({
.then((res: RewindObjectList) => {
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (res.objects === null) {
setFileModeEnabled(true);
//setFileModeEnabled(true);
setLoadingRewind(false);
setLoading(false);
} else {
// It is a folder, we remove loader
setLoadingRewind(false);
setLoading(false);
setFileModeEnabled(false);
//setFileModeEnabled(false);
}
})
.catch((err: ErrorResponseHandler) => {
@@ -573,7 +565,7 @@ const ListObjects = ({
//It is a file since it has elements in the object, setting file flag and waiting for component mount
if (!res.objects) {
// It is a folder, we remove loader
setFileModeEnabled(false);
//setFileModeEnabled(false);
setLoading(false);
} else {
// This code prevents the program from opening a file when a substring of that file is entered as a new folder.
@@ -594,9 +586,9 @@ const ListObjects = ({
res.objects[0].name.endsWith("/")) ||
!found
) {
setFileModeEnabled(false);
//setFileModeEnabled(false);
} else {
setFileModeEnabled(true);
//setFileModeEnabled(true);
}
setLoading(false);
@@ -608,7 +600,7 @@ const ListObjects = ({
});
}
} else {
setFileModeEnabled(false);
//setFileModeEnabled(false);
setLoading(false);
}
})
@@ -629,7 +621,6 @@ const ListObjects = ({
rewindEnabled,
rewindDate,
internalPaths,
setFileModeEnabled,
bucketInfo,
displayListObjects,
]);
@@ -684,24 +675,6 @@ const ListObjects = ({
uploadObject(newFiles, "");
};
const displayParsedDate = (object: BucketObject) => {
if (object.name.endsWith("/")) {
return "";
}
return <reactMoment.default>{object.last_modified}</reactMoment.default>;
};
const displayNiceBytes = (object: BucketObject) => {
if (object.name.endsWith("/")) {
return "";
}
return niceBytes(String(object.size));
};
const displayDeleteFlag = (state: boolean) => {
return state ? "Yes" : "No";
};
const downloadObject = (object: BucketObject | RewindObject) => {
const identityDownload = encodeFileName(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
@@ -969,15 +942,6 @@ const ListObjects = ({
setSelectedPreview(null);
};
const tableActions: ItemActions[] = [
{
type: "view",
label: "View",
onClick: openPath,
sendOnlyId: true,
},
];
const filteredRecords = records.filter((b: BucketObject) => {
if (searchObjects === "") {
return true;
@@ -1027,67 +991,6 @@ const ListObjects = ({
setLoading(true);
};
const renderName = (element: string) => {
return displayName(element);
};
const listModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: renderName,
enableSort: true,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
enableSort: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
enableSort: true,
},
];
const rewindModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: renderName,
enableSort: true,
},
{
label: "Object Date",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
enableSort: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
enableSort: true,
},
{
label: "Deleted",
elementKey: "delete_flag",
renderFunction: displayDeleteFlag,
width: 60,
contentTextAlign: "center",
},
];
const pageTitle = decodeFileName(internalPaths);
const currentPath = pageTitle.split("/").filter((i: string) => i !== "");
@@ -1136,6 +1039,15 @@ const ListObjects = ({
uploadPath = uploadPath.concat(currentPath);
}
const tableActions: ItemActions[] = [
{
type: "view",
label: "View",
onClick: openPath,
sendOnlyId: true,
},
];
const multiActionButtons = [
{
action: downloadSelected,
@@ -1345,40 +1257,58 @@ const ListObjects = ({
>
<input {...getInputProps()} />
<Grid item xs={12} className={classes.tableBlock}>
{versionsMode ? (
<Fragment>
{selectedInternalPaths !== null && (
<VersionsNavigator
internalPaths={selectedInternalPaths}
bucketName={bucketName}
/>
)}
</Fragment>
) : (
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={rewindEnabled ? loadingRewind : loading}
loadingMessage={loadingMessage}
entityName="Objects"
idField="name"
records={payload}
customPaperHeight={`${classes.browsePaper} ${
detailsOpen ? "actionsPanelOpen" : ""
}`}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={`This location is empty${
!rewindEnabled ? ", please try uploading a new file" : ""
}`}
sortConfig={{
currentSort: currentSortField,
currentDirection: sortDirection,
triggerSort: sortChange,
}}
onSelectAll={selectAllItems}
/>
</SecureComponent>
)}
<SecureComponent
scopes={[IAM_SCOPES.S3_LIST_BUCKET]}
resource={bucketName}
errorProps={{ disabled: true }}
>
<TableWrapper
itemActions={tableActions}
columns={rewindEnabled ? rewindModeColumns : listModeColumns}
isLoading={rewindEnabled ? loadingRewind : loading}
loadingMessage={loadingMessage}
entityName="Objects"
idField="name"
records={payload}
customPaperHeight={`${classes.browsePaper} ${
detailsOpen ? "actionsPanelOpen" : ""
}`}
selectedItems={selectedObjects}
onSelect={selectListObjects}
customEmptyMessage={`This location is empty${
!rewindEnabled ? ", please try uploading a new file" : ""
}`}
sortConfig={{
currentSort: currentSortField,
currentDirection: sortDirection,
triggerSort: sortChange,
}}
onSelectAll={selectAllItems}
/>
<DetailsListPanel
open={detailsOpen}
closePanel={() => {
setDetailsOpen(false);
setSelectedInternalPaths(null);
setSelectedObjects([]);
setVersionsModeEnabled(false);
}}
>
{selectedObjects.length > 0 && (
@@ -1408,6 +1338,7 @@ const mapStateToProps = ({ objectBrowser, buckets }: AppState) => ({
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
versionsMode: get(objectBrowser, "versionsMode", false),
loadingBucket: buckets.bucketDetails.loadingBucket,
bucketInfo: buckets.bucketDetails.bucketInfo,
searchObjects: objectBrowser.searchObjects,
@@ -1416,7 +1347,6 @@ const mapStateToProps = ({ objectBrowser, buckets }: AppState) => ({
const mapDispatchToProps = {
setSnackBarMessage,
setErrorSnackMessage,
setFileModeEnabled,
resetRewind,
setBucketDetailsLoad,
setBucketInfo,
@@ -1425,6 +1355,7 @@ const mapDispatchToProps = {
completeObject,
openList,
setSearchObjects,
setVersionsModeEnabled,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,100 @@
// 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 * as reactMoment from "react-moment";
import { BucketObject } from "./types";
import { niceBytes } from "../../../../../../common/utils";
import { displayFileIconName } from "./utils";
// Functions
export const displayParsedDate = (object: BucketObject) => {
if (object.name.endsWith("/")) {
return "";
}
return <reactMoment.default>{object.last_modified}</reactMoment.default>;
};
export const displayNiceBytes = (object: BucketObject) => {
if (object.name.endsWith("/")) {
return "";
}
return niceBytes(String(object.size));
};
export const displayDeleteFlag = (state: boolean) => {
return state ? "Yes" : "No";
};
// Table Props
export const listModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: displayFileIconName,
enableSort: true,
},
{
label: "Last Modified",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
enableSort: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
enableSort: true,
},
];
export const rewindModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: displayFileIconName,
enableSort: true,
},
{
label: "Object Date",
elementKey: "last_modified",
renderFunction: displayParsedDate,
renderFullObject: true,
enableSort: true,
},
{
label: "Size",
elementKey: "size",
renderFunction: displayNiceBytes,
renderFullObject: true,
width: 60,
contentTextAlign: "right",
enableSort: true,
},
{
label: "Deleted",
elementKey: "delete_flag",
renderFunction: displayDeleteFlag,
width: 60,
contentTextAlign: "center",
},
];

View File

@@ -44,13 +44,14 @@ import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions
import {
completeObject,
setNewObject,
setVersionsModeEnabled,
updateProgress,
} from "../../../../ObjectBrowser/actions";
import { AppState } from "../../../../../../store";
import {
DisabledIcon,
NextArrowIcon,
PreviewIcon,
VersionsIcon,
} from "../../../../../../icons";
import { ShareIcon, DownloadIcon, DeleteIcon } from "../../../../../../icons";
import history from "../../../../../../history";
@@ -114,11 +115,14 @@ interface IObjectDetailPanelProps {
rewindDate: any;
bucketToRewind: string;
distributedSetup: boolean;
versionsMode: boolean;
selectedVersion: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
setVersionsModeEnabled: typeof setVersionsModeEnabled;
}
const emptyFile: IFileInfo = {
@@ -142,6 +146,9 @@ const ObjectDetailPanel = ({
setNewObject,
updateProgress,
completeObject,
versionsMode,
selectedVersion,
setVersionsModeEnabled,
}: IObjectDetailPanelProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
@@ -151,6 +158,7 @@ const ObjectDetailPanel = ({
const [selectedTag, setSelectedTag] = useState<string[]>(["", ""]);
const [legalholdOpen, setLegalholdOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [allInfoElements, setAllInfoElements] = useState<IFileInfo[]>([]);
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [deleteOpen, setDeleteOpen] = useState<boolean>(false);
@@ -175,6 +183,22 @@ const ObjectDetailPanel = ({
}
}, [internalPaths, bucketName]);
useEffect(() => {
if (distributedSetup && allInfoElements.length >= 1) {
let infoElement =
allInfoElements.find((el: IFileInfo) => el.is_latest) || emptyFile;
if (selectedVersion !== "") {
infoElement =
allInfoElements.find(
(el: IFileInfo) => el.version_id === selectedVersion
) || emptyFile;
}
setActualInfo(infoElement);
}
}, [selectedVersion, distributedSetup, allInfoElements]);
useEffect(() => {
if (loadObjectData && internalPaths !== "") {
api
@@ -187,9 +211,7 @@ const ObjectDetailPanel = ({
.then((res: IFileInfo[]) => {
const result = get(res, "objects", []);
if (distributedSetup) {
setActualInfo(
result.find((el: IFileInfo) => el.is_latest) || emptyFile
);
setAllInfoElements(result);
setVersions(result);
const tVersionSize = result.reduce(
(acc: number, currValue: IFileInfo) => {
@@ -220,6 +242,7 @@ const ObjectDetailPanel = ({
internalPaths,
setErrorSnackMessage,
distributedSetup,
selectedVersion,
]);
let tagKeys: string[] = [];
@@ -329,18 +352,15 @@ const ObjectDetailPanel = ({
setPreviewOpen(false);
};
const openExtraInfo = () => {
const newPath = `/buckets/${bucketName}/browse${
internalPaths !== "" ? `/${internalPaths}` : ``
}`;
history.push(newPath);
};
if (!actualInfo) {
return null;
}
const objectName =
objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name;
return (
<Fragment>
{shareFileModalOpen && actualInfo && (
@@ -428,11 +448,8 @@ const ObjectDetailPanel = ({
<LinearProgress />
</Grid>
)}
<div className={classes.titleLabel}>
{objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name}
</div>
<div className={classes.titleLabel}>{objectName}</div>
<ul className={classes.objectActions}>
<li>Actions:</li>
@@ -479,17 +496,22 @@ const ObjectDetailPanel = ({
onClick={() => {
setDeleteOpen(true);
}}
disabled={actualInfo.is_delete_marker}
disabled={actualInfo.is_delete_marker || selectedVersion !== ""}
/>
</li>
</SecureComponent>
<li>
<ObjectActionButton
label={"Expand Details"}
icon={<NextArrowIcon />}
label={
versionsMode ? "Hide Object Versions" : "Display Object Versions"
}
icon={<VersionsIcon />}
onClick={() => {
openExtraInfo();
setVersionsModeEnabled(!versionsMode, objectName);
}}
disabled={
!(actualInfo.version_id && actualInfo.version_id !== "null")
}
/>
</li>
</ul>
@@ -497,107 +519,150 @@ const ObjectDetailPanel = ({
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
{selectedVersion !== "" && (
<Box className={classes.detailContainer}>
<strong>Version ID:</strong>
<br />
{selectedVersion}
</Box>
)}
<Box className={classes.detailContainer}>
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
{selectedVersion === "" ? (
<LabelValuePair
label={"Tags:"}
value={
<ObjectTags
objectInfo={actualInfo}
tagKeys={tagKeys}
bucketName={bucketName}
onDeleteTag={deleteTag}
onAddTagClick={() => {
setTagModalOpen(true);
}}
/>
}
/>
) : (
<Fragment>
<strong>Tags: </strong>
<br />
{tagKeys.length === 0
? "N/A"
: tagKeys.map((tagKey, index) => {
return (
<span key={`key-vs-${index.toString()}`}>
{tagKey}:{get(actualInfo, `tags.${tagKey}`, "")}
{index < tagKeys.length - 1 ? ", " : ""}
</span>
);
})}
</Fragment>
)}
</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>
}
/>
}
/>
)
}
/>
{selectedVersion === "" ? (
<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>
}
/>
}
/>
)
}
/>
) : (
<Fragment>
<strong>Legal Hold:</strong>
<br />
{actualInfo.legal_hold_status ? "On" : "Off"}
</Fragment>
)}
</SecureComponent>
</Box>
<Box className={classes.detailContainer}>
<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>
}
/>
}
/>
)
}
/>
{selectedVersion === "" ? (
<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>
}
/>
}
/>
)
}
/>
) : (
<Fragment>
<strong>Object Retention:</strong>
<br />
{actualInfo.retention_mode
? actualInfo.retention_mode.toLowerCase()
: "None"}
</Fragment>
)}
</SecureComponent>
</Box>
<hr className={classes.hrClass} />
@@ -616,25 +681,27 @@ const ObjectDetailPanel = ({
</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}
{actualInfo.version_id &&
actualInfo.version_id !== "null" &&
selectedVersion === "" && (
<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>
<Box className={classes.metadataLinear}>
<strong>Versions Stored size:</strong>
<br />
{niceBytesInt(totalVersionsSize)}
</Box>
</Box>
</Fragment>
)}
</Fragment>
)}
</Fragment>
);
};
@@ -644,6 +711,8 @@ const mapStateToProps = ({ objectBrowser, system }: AppState) => ({
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
bucketToRewind: get(objectBrowser, "rewind.bucketToRewind", ""),
distributedSetup: get(system, "distributedSetup", false),
versionsMode: get(objectBrowser, "versionsMode", false),
selectedVersion: get(objectBrowser, "selectedVersion", ""),
});
const mapDispatchToProps = {
@@ -652,6 +721,7 @@ const mapDispatchToProps = {
setNewObject,
updateProgress,
completeObject,
setVersionsModeEnabled,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -66,7 +66,71 @@ const FileZipIcon = React.lazy(
() => import("../../../../../../icons/FileZipIcon")
);
export const displayName = (element: string) => {
interface IExtToIcon {
icon: any;
extensions: string[];
}
export const extensionToIcon: IExtToIcon[] = [
{
icon: <FileVideoIcon />,
extensions: ["mp4", "mov", "avi", "mpeg", "mpg"],
},
{
icon: <FileMusicIcon />,
extensions: ["mp3", "m4a", "aac"],
},
{
icon: <FilePdfIcon />,
extensions: ["pdf"],
},
{
icon: <FilePptIcon />,
extensions: ["ppt", "pptx"],
},
{
icon: <FileXlsIcon />,
extensions: ["xls", "xlsx"],
},
{
icon: <FileLockIcon />,
extensions: ["cer", "crt", "pem"],
},
{
icon: <FileCodeIcon />,
extensions: ["html", "xml", "css", "py", "go", "php", "cpp", "h", "java"],
},
{
icon: <FileConfigIcon />,
extensions: ["cfg", "yaml"],
},
{
icon: <FileDbIcon />,
extensions: ["sql"],
},
{
icon: <FileFontIcon />,
extensions: ["ttf", "otf"],
},
{
icon: <FileTxtIcon />,
extensions: ["txt"],
},
{
icon: <FileZipIcon />,
extensions: ["zip", "rar", "tar", "gz"],
},
{
icon: <FileBookIcon />,
extensions: ["epub", "mobi", "azw", "azw3"],
},
{
icon: <FileImageIcon />,
extensions: ["jpeg", "jpg", "gif", "tiff", "png", "heic", "dng"],
},
];
export const displayFileIconName = (element: string, returnOnlyIcon: boolean = false) => {
let elementString = element;
let icon = <ObjectBrowserIcon />;
// Element is a folder
@@ -75,69 +139,6 @@ export const displayName = (element: string) => {
elementString = element.substr(0, element.length - 1);
}
interface IExtToIcon {
icon: any;
extensions: string[];
}
const extensionToIcon: IExtToIcon[] = [
{
icon: <FileVideoIcon />,
extensions: ["mp4", "mov", "avi", "mpeg", "mpg"],
},
{
icon: <FileMusicIcon />,
extensions: ["mp3", "m4a", "aac"],
},
{
icon: <FilePdfIcon />,
extensions: ["pdf"],
},
{
icon: <FilePptIcon />,
extensions: ["ppt", "pptx"],
},
{
icon: <FileXlsIcon />,
extensions: ["xls", "xlsx"],
},
{
icon: <FileLockIcon />,
extensions: ["cer", "crt", "pem"],
},
{
icon: <FileCodeIcon />,
extensions: ["html", "xml", "css", "py", "go", "php", "cpp", "h", "java"],
},
{
icon: <FileConfigIcon />,
extensions: ["cfg", "yaml"],
},
{
icon: <FileDbIcon />,
extensions: ["sql"],
},
{
icon: <FileFontIcon />,
extensions: ["ttf", "otf"],
},
{
icon: <FileTxtIcon />,
extensions: ["txt"],
},
{
icon: <FileZipIcon />,
extensions: ["zip", "rar", "tar", "gz"],
},
{
icon: <FileBookIcon />,
extensions: ["epub", "mobi", "azw", "azw3"],
},
{
icon: <FileImageIcon />,
extensions: ["jpeg", "jpg", "gif", "tiff", "png", "heic", "dng"],
},
];
const lowercaseElement = element.toLowerCase();
for (const etc of extensionToIcon) {
for (const ext of etc.extensions) {
@@ -153,5 +154,9 @@ export const displayName = (element: string) => {
const splitItem = elementString.split("/");
if(returnOnlyIcon) {
return icon;
}
return <IconWithLabel icon={icon} strings={splitItem} />;
};

View File

@@ -0,0 +1,198 @@
// 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 * as reactMoment from "react-moment";
import Grid from "@mui/material/Grid";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { withStyles } from "@mui/styles";
import { displayFileIconName } from "../ListObjects/utils";
import { IFileInfo } from "./types";
import { IconButton, Tooltip } from "@mui/material";
import { DownloadIcon, RecoverIcon, ShareIcon } from "../../../../../../icons";
interface IFileVersionItem {
fileName: string;
versionInfo: IFileInfo;
index: number;
onShare: (versionInfo: IFileInfo) => void;
onDownload: (versionInfo: IFileInfo) => void;
onRestore: (versionInfo: IFileInfo) => void;
globalClick: (versionInfo: IFileInfo) => void;
classes: any;
}
const styles = (theme: Theme) =>
createStyles({
mainFileVersionItem: {
borderBottom: "#E2E2E2 1px solid",
padding: "1rem 0",
margin: "0 2rem 0 3.5rem",
cursor: "pointer",
},
versionContainer: {
fontSize: 16,
fontWeight: "bold",
color: "#000",
display: "flex",
alignItems: "center",
"& svg.min-icon": {
width: 18,
height: 18,
marginRight: 10,
},
},
buttonContainer: {
textAlign: "right",
"& button": {
marginLeft: "1.5rem",
},
},
versionID: {
fontSize: "12px",
color: "#000",
margin: "2px 0",
},
versionData: {
marginRight: "10px",
fontSize: 12,
color: "#868686",
},
ctrItem: {
position: "relative",
"&::before": {
content: "' '",
display: "block",
position: "absolute",
width: "2px",
height: "calc(100% + 2px)",
backgroundColor: "#F8F8F8",
left: "24px",
},
},
});
const FileVersionItem = ({
classes,
fileName,
versionInfo,
onShare,
onDownload,
onRestore,
globalClick,
index,
}: IFileVersionItem) => {
const disableButtons = versionInfo.is_delete_marker;
const versionItemButtons = [
{
icon: <DownloadIcon />,
action: onDownload,
tooltip: "Download this version",
},
{
icon: <ShareIcon />,
action: onShare,
tooltip: "Share this version",
},
{
icon: <RecoverIcon />,
action: onRestore,
tooltip: "Restore this version",
},
];
return (
<Grid
container
flex={1}
className={classes.ctrItem}
onClick={() => {
globalClick(versionInfo);
}}
>
<Grid item xs={12} className={classes.mainFileVersionItem}>
<Grid item xs={12} justifyContent={"space-between"}>
<Grid container>
<Grid item xs={4} className={classes.versionContainer}>
{displayFileIconName(fileName, true)} v{index.toString()}
</Grid>
<Grid item xs={8} className={classes.buttonContainer}>
{versionItemButtons.map((button, index) => {
return (
<Tooltip
title={button.tooltip}
key={`version-action-${button.tooltip}-${index.toString()}`}
>
<IconButton
size={"small"}
id={`version-action-${
button.tooltip
}-${index.toString()}`}
className={`${classes.spacing} ${
disableButtons ? classes.buttonDisabled : ""
}`}
disabled={disableButtons}
onClick={(e) => {
e.stopPropagation();
if (!disableButtons) {
button.action(versionInfo);
} else {
e.preventDefault();
}
}}
sx={{
backgroundColor: "#F8F8F8",
borderRadius: "100%",
width: "28px",
height: "28px",
padding: "5px",
"& .min-icon": {
width: "14px",
height: "14px",
},
}}
>
{button.icon}
</IconButton>
</Tooltip>
);
})}
</Grid>
</Grid>
</Grid>
<Grid item xs={12} className={classes.versionID}>
{versionInfo.version_id}
</Grid>
<Grid item xs={12}>
<span className={classes.versionData}>
<strong>Last modified:</strong>{" "}
<reactMoment.default>
{versionInfo.last_modified}
</reactMoment.default>
</span>
<span className={classes.versionData}>
<strong>Deleted:</strong>{" "}
{versionInfo.is_delete_marker ? "Yes" : "No"}
</span>
</Grid>
</Grid>
</Grid>
);
};
export default withStyles(styles)(FileVersionItem);

View File

@@ -429,7 +429,7 @@ const ObjectDetails = ({
};
return (
<React.Fragment>
<Fragment>
{shareFileModalOpen && actualInfo && (
<ShareFile
open={shareFileModalOpen}
@@ -587,7 +587,7 @@ const ObjectDetails = ({
label: "Details",
},
content: (
<React.Fragment>
<Fragment>
<div className={classes.actionsTray}>
<h1 className={classes.sectionTitle}>Details</h1>
</div>
@@ -717,7 +717,7 @@ const ObjectDetails = ({
actualInfo={actualInfo}
/>
) : null}
</React.Fragment>
</Fragment>
),
}}
{{
@@ -797,7 +797,7 @@ const ObjectDetails = ({
disabled: extensionPreview(currentItem) === "none",
},
content: (
<React.Fragment>
<Fragment>
{actualInfo && (
<PreviewFileContent
bucketName={bucketName}
@@ -811,14 +811,14 @@ const ObjectDetails = ({
isFullscreen
/>
)}
</React.Fragment>
</Fragment>
),
}}
</VerticalTabs>
</Fragment>
)}
</PageLayout>
</React.Fragment>
</Fragment>
);
};

View File

@@ -0,0 +1,496 @@
// 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 get from "lodash/get";
import { connect } from "react-redux";
import { withStyles } from "@mui/styles";
import { Theme } from "@mui/material/styles";
import createStyles from "@mui/styles/createStyles";
import { LinearProgress, SelectChangeEvent } from "@mui/material";
import Grid from "@mui/material/Grid";
import ShareFile from "./ShareFile";
import {
actionsTray,
buttonsStyles,
containerForHeader,
hrClass,
tableStyles,
spacingUtils,
textStyleUtils,
objectBrowserExtras,
objectBrowserCommon,
} from "../../../../Common/FormComponents/common/styleLibrary";
import { IFileInfo } from "./types";
import { download } from "../utils";
import api from "../../../../../../common/api";
import { ErrorResponseHandler } from "../../../../../../common/types";
import {
setErrorSnackMessage,
setSnackBarMessage,
} from "../../../../../../actions";
import { encodeFileName, niceBytesInt } from "../../../../../../common/utils";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import RestoreFileVersion from "./RestoreFileVersion";
import {
completeObject,
setNewObject,
setSelectedVersion,
updateProgress,
} from "../../../../ObjectBrowser/actions";
import { AppState } from "../../../../../../store";
import { VersionsIcon } from "../../../../../../icons";
import VirtualizedList from "../../../../Common/VirtualizedList/VirtualizedList";
import FileVersionItem from "./FileVersionItem";
import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper";
const styles = (theme: Theme) =>
createStyles({
propertiesIcon: {
marginLeft: 5,
"& .min-icon": {
height: 12,
},
},
tag: {
marginRight: 6,
fontSize: 10,
fontWeight: 700,
"&.MuiChip-sizeSmall": {
height: 18,
},
"& .min-icon": {
height: 10,
width: 10,
},
},
search: {
marginBottom: 8,
"&.MuiFormControl-root": {
marginRight: 0,
},
},
capitalizeFirst: {
textTransform: "capitalize",
"& .min-icon": {
width: 16,
height: 16,
},
},
titleCol: {
width: "25%",
},
titleItem: {
width: "35%",
},
versionsContainer: {
border: "#EAEDEE 1px solid",
padding: 10,
},
"@global": {
".progressDetails": {
paddingTop: 3,
display: "inline-block",
position: "relative",
width: 18,
height: 18,
},
".progressDetails > .MuiCircularProgress-root": {
position: "absolute",
left: 0,
top: 3,
},
},
tabsContainer: {
border: "1px solid #eaeaea",
borderTop: 0,
},
noBottomBorder: {
borderBottom: 0,
},
versionsVirtualPanel: {
flexGrow: 1,
height: "calc(100% - 120px)",
overflow: "auto",
},
screenTitleContainer: {
position: "relative",
"&::before": {
content: "' '",
display: "block",
position: "absolute",
width: "2px",
backgroundColor: "#F8F8F8",
left: "24px",
height: "52px",
bottom: 0,
},
},
sortByLabel: {
color: "#838383",
fontWeight: "bold",
whiteSpace: "nowrap",
marginRight: 12,
fontSize: 14,
},
...hrClass,
...buttonsStyles,
...actionsTray,
...tableStyles,
...spacingUtils,
...textStyleUtils,
...objectBrowserCommon,
...objectBrowserExtras,
...containerForHeader(theme.spacing(4)),
});
interface IVersionsNavigatorProps {
classes: any;
distributedSetup: boolean;
internalPaths: string;
bucketName: string;
searchVersions: string;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
setSelectedVersion: typeof setSelectedVersion;
}
const emptyFile: IFileInfo = {
is_latest: true,
last_modified: "",
legal_hold_status: "",
name: "",
retention_mode: "",
retention_until_date: "",
size: "0",
tags: {},
version_id: null,
};
const VersionsNavigator = ({
classes,
distributedSetup,
setErrorSnackMessage,
setNewObject,
updateProgress,
searchVersions,
completeObject,
internalPaths,
bucketName,
setSelectedVersion,
}: IVersionsNavigatorProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
const [objectToShare, setObjectToShare] = useState<IFileInfo | null>(null);
const [versions, setVersions] = useState<IFileInfo[]>([]);
const [restoreVersionOpen, setRestoreVersionOpen] = useState<boolean>(false);
const [restoreVersion, setRestoreVersion] = useState<string>("");
const [sortValue, setSortValue] = useState<string>("date");
// calculate object name to display
let objectNameArray: string[] = [];
if (actualInfo) {
objectNameArray = actualInfo.name.split("/");
}
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);
} else {
setActualInfo(result[0]);
setVersions([]);
}
setLoadObjectData(false);
})
.catch((error: ErrorResponseHandler) => {
setErrorSnackMessage(error);
setLoadObjectData(false);
});
}
}, [
loadObjectData,
bucketName,
internalPaths,
setErrorSnackMessage,
distributedSetup,
]);
const shareObject = () => {
setShareFileModalOpen(true);
};
const closeShareModal = () => {
setObjectToShare(null);
setShareFileModalOpen(false);
};
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 onShareItem = (item: IFileInfo) => {
setObjectToShare(item);
shareObject();
};
const onRestoreItem = (item: IFileInfo) => {
setRestoreVersion(item.version_id || "");
setRestoreVersionOpen(true);
};
const onDownloadItem = (item: IFileInfo) => {
downloadObject(item);
};
const onGlobalClick = (item: IFileInfo) => {
setSelectedVersion(item.version_id || "");
};
const filteredRecords = versions.filter((version) => {
if (version.version_id) {
return version.version_id.includes(searchVersions);
}
return false;
});
const closeRestoreModal = (reloadObjectData: boolean) => {
setRestoreVersionOpen(false);
setRestoreVersion("");
if (reloadObjectData) {
setLoadObjectData(true);
}
};
const totalSpace = versions.reduce((acc: number, currValue: IFileInfo) => {
if (currValue.size) {
return acc + parseInt(currValue.size);
}
return acc;
}, 0);
filteredRecords.sort((a, b) => {
switch (sortValue) {
case "version":
if (a.version_id && b.version_id) {
if (a.version_id < b.version_id) {
return -1;
}
if (a.version_id > b.version_id) {
return 1;
}
return 0;
}
return 0;
case "deleted":
if (a.is_delete_marker && !b.is_delete_marker) {
return -1;
}
if (!a.is_delete_marker && b.is_delete_marker) {
return 1;
}
return 0;
default:
const dateA = new Date(a.last_modified).getTime();
const dateB = new Date(b.last_modified).getTime();
if (dateA < dateB) {
return 1;
}
if (dateA > dateB) {
return -1;
}
return 0;
}
});
const renderVersion = (elementIndex: number) => {
const item = filteredRecords[elementIndex];
const versOrd = versions.length - versions.indexOf(item);
return (
<FileVersionItem
fileName={actualInfo?.name || ""}
versionInfo={item}
index={versOrd}
onDownload={onDownloadItem}
onRestore={onRestoreItem}
onShare={onShareItem}
globalClick={onGlobalClick}
/>
);
};
return (
<Fragment>
{shareFileModalOpen && actualInfo && (
<ShareFile
open={shareFileModalOpen}
closeModalAndRefresh={closeShareModal}
bucketName={bucketName}
dataObject={objectToShare || actualInfo}
/>
)}
{restoreVersionOpen && actualInfo && (
<RestoreFileVersion
restoreOpen={restoreVersionOpen}
bucketName={bucketName}
versionID={restoreVersion}
objectPath={actualInfo.name}
onCloseAndUpdate={closeRestoreModal}
/>
)}
<Grid container className={classes.versionsContainer}>
{!actualInfo && (
<Grid item xs={12}>
<LinearProgress />
</Grid>
)}
{actualInfo && (
<Fragment>
<Grid item xs={12} className={classes.screenTitleContainer}>
<ScreenTitle
icon={
<span className={classes.listIcon}>
<VersionsIcon />
</span>
}
title={
<span className={classes.titleSpacer}>
{objectNameArray.length > 0
? objectNameArray[objectNameArray.length - 1]
: actualInfo.name}{" "}
Versions
</span>
}
subTitle={
<Fragment>
<Grid item xs={12} className={classes.bucketDetails}>
<span className={classes.detailsSpacer}>
<strong>
{versions.length} Version
{versions.length === 1 ? "" : "s"}&nbsp;&nbsp;&nbsp;
</strong>
</span>
<span className={classes.detailsSpacer}>
<strong>{niceBytesInt(totalSpace)}</strong>
</span>
</Grid>
</Fragment>
}
actions={
<Fragment>
<span className={classes.sortByLabel}>Sort by</span>
<SelectWrapper
id={"sort-by"}
label={""}
value={sortValue}
onChange={(e: SelectChangeEvent<string>) => {
setSortValue(e.target.value as string);
}}
name={"sort-by"}
options={[
{ label: "Date", value: "date" },
{
label: "Version ID",
value: "version",
},
{ label: "Deleted", value: "deleted" },
]}
/>
</Fragment>
}
className={classes.noBottomBorder}
/>
</Grid>
<Grid item xs={12} className={classes.versionsVirtualPanel}>
{actualInfo.version_id && actualInfo.version_id !== "null" && (
<VirtualizedList
rowRenderFunction={renderVersion}
totalItems={filteredRecords.length}
defaultHeight={110}
/>
)}
</Grid>
</Fragment>
)}
</Grid>
</Fragment>
);
};
const mapStateToProps = ({ system, objectBrowser }: AppState) => ({
distributedSetup: get(system, "distributedSetup", false),
searchVersions: objectBrowser.searchVersions,
});
const mapDispatchToProps = {
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
setSelectedVersion,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default connector(withStyles(styles)(VersionsNavigator));

View File

@@ -175,7 +175,12 @@ const FormSwitchWrapper = ({
<Grid container alignItems={"center"}>
<Grid item xs>
<Grid container>
<Grid item xs={12} sm={10} md={9}>
<Grid
item
xs={12}
sm={description !== "" ? 4 : 10}
md={description !== "" ? 3 : 9}
>
{label !== "" && (
<InputLabel htmlFor={id} className={classes.inputLabel}>
<span>{label}</span>

View File

@@ -379,8 +379,8 @@ export const objectBrowserCommon = {
},
},
"& .min-icon": {
width: 14,
minWidth: 14,
width: 16,
minWidth: 16,
},
},
smallLabel: {
@@ -404,7 +404,6 @@ export const objectBrowserCommon = {
textAlign: "left" as const,
marginLeft: 15,
marginRight: 10,
lineHeight: 35,
},
};
@@ -1360,6 +1359,20 @@ export const detailsPanel: any = {
},
};
export const objectBrowserExtras = {
listIcon: {
display: "block",
marginTop: "-10px",
"& .min-icon": {
width: 20,
height: 20,
},
},
titleSpacer: {
marginLeft: 10,
},
}
// These classes are meant to be used as React.CSSProperties for TableWrapper
export const TableRowPredefStyles: any = {
deleted: {

View File

@@ -569,6 +569,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
NewAccountIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.NewPathIcon /> <br />
NewPathIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.NewPoolIcon /> <br />
NewPoolIcon
@@ -834,6 +839,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
VersionIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.VersionsIcon /> <br />
VersionsIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.WarnIcon /> <br />
WarnIcon

View File

@@ -25,13 +25,14 @@ import { ObjectBrowserState } from "./reducers";
import { objectBrowserCommon } from "../Common/FormComponents/common/styleLibrary";
import { Link } from "react-router-dom";
import { encodeFileName } from "../../../common/utils";
import { BackCaretIcon, FolderIcon } from "../../../icons";
import { BackCaretIcon, NewPathIcon } from "../../../icons";
import { IconButton, Tooltip } from "@mui/material";
import history from "../../../history";
import { hasPermission } from "../../../common/SecureComponent";
import { IAM_SCOPES } from "../../../common/SecureComponent/permissions";
import withSuspense from "../Common/Components/withSuspense";
import { BucketObject } from "../Buckets/ListBuckets/Objects/ListObjects/types";
import { setVersionsModeEnabled } from "./actions";
const CreateFolderModal = withSuspense(
React.lazy(
@@ -48,8 +49,10 @@ interface IObjectBrowser {
bucketName: string;
internalPaths: string;
rewindEnabled?: boolean;
rewindDate?: any;
versionsMode: boolean;
versionedFile: string;
existingFiles: BucketObject[];
setVersionsModeEnabled: typeof setVersionsModeEnabled;
}
const styles = (theme: Theme) =>
@@ -62,8 +65,10 @@ const BrowserBreadcrumbs = ({
bucketName,
internalPaths,
rewindEnabled,
rewindDate,
existingFiles,
versionsMode,
versionedFile,
setVersionsModeEnabled,
}: IObjectBrowser) => {
const [createFolderOpen, setCreateFolderOpen] = useState<boolean>(false);
@@ -82,22 +87,55 @@ const BrowserBreadcrumbs = ({
return (
<Fragment key={`breadcrumbs-${index.toString()}`}>
<span> / </span>
<Link to={route}>{objectItem}</Link>
<Link
to={route}
onClick={() => {
setVersionsModeEnabled(false);
}}
>
{objectItem}
</Link>
</Fragment>
);
});
let versionsItem: any[] = [];
if (versionsMode) {
versionsItem = [
<Fragment key={`breadcrumbs-versionedItem`}>
<span> / {versionedFile} - Versions</span>
</Fragment>,
];
}
const listBreadcrumbs: any[] = [
<Fragment key={`breadcrumbs-root-path`}>
<Link to={`/buckets/${bucketName}/browse`}>{bucketName}</Link>
<Link
to={`/buckets/${bucketName}/browse`}
onClick={() => {
setVersionsModeEnabled(false);
}}
>
{bucketName}
</Link>
</Fragment>,
...breadcrumbsMap,
...versionsItem,
];
const closeAddFolderModal = () => {
setCreateFolderOpen(false);
};
const goBackFunction = () => {
if (versionsMode) {
setVersionsModeEnabled(false);
} else {
history.goBack();
}
};
return (
<React.Fragment>
{createFolderOpen && (
@@ -111,14 +149,11 @@ const BrowserBreadcrumbs = ({
)}
<Grid item xs={12} className={`${classes.breadcrumbs}`}>
<IconButton
onClick={() => {
history.goBack();
}}
onClick={goBackFunction}
sx={{
border: "#EAEDEE 1px solid",
backgroundColor: "#fff",
borderLeft: 0,
borderBottom: 0,
borderRadius: 0,
width: 39,
height: 39,
@@ -145,7 +180,7 @@ const BrowserBreadcrumbs = ({
paddingLeft: "6px",
}}
>
<FolderIcon />
<NewPathIcon />
</IconButton>
</Tooltip>
<div className={classes.breadcrumbsList} dir="rtl">
@@ -158,9 +193,14 @@ const BrowserBreadcrumbs = ({
const mapStateToProps = ({ objectBrowser }: ObjectBrowserReducer) => ({
rewindEnabled: get(objectBrowser, "rewind.rewindEnabled", false),
rewindDate: get(objectBrowser, "rewind.dateToRewind", null),
versionsMode: get(objectBrowser, "versionsMode", false),
versionedFile: get(objectBrowser, "versionedFile", ""),
});
const connector = connect(mapStateToProps, null);
const mapDispatchToProps = {
setVersionsModeEnabled,
};
const connector = connect(mapStateToProps, mapDispatchToProps);
export default withStyles(styles)(connector(BrowserBreadcrumbs));

View File

@@ -18,7 +18,6 @@ import { IFileItem } from "./reducers";
export const REWIND_SET_ENABLE = "REWIND/SET_ENABLE";
export const REWIND_RESET_REWIND = "REWIND/RESET_REWIND";
export const REWIND_FILE_MODE_ENABLED = "BUCKET_BROWSER/FILE_MODE_ENABLED";
export const OBJECT_MANAGER_NEW_OBJECT = "OBJECT_MANAGER/NEW_OBJECT";
export const OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT =
@@ -33,6 +32,13 @@ export const OBJECT_MANAGER_CLOSE_LIST = "OBJECT_MANAGER/CLOSE_LIST";
export const OBJECT_MANAGER_SET_SEARCH_OBJECT =
"OBJECT_MANAGER/SET_SEARCH_OBJECT";
export const BUCKET_BROWSER_VERSIONS_MODE_ENABLED =
"BUCKET_BROWSER/VERSIONS_MODE_ENABLED";
export const BUCKET_BROWSER_VERSIONS_SET_SEARCH =
"BUCKET_BROWSER/VERSIONS_SET_SEARCH";
export const BUCKET_BROWSER_SET_SELECTED_VERSION =
"BUCKET_BROWSER/SET_SELECTED_VERSION";
interface RewindSetEnabled {
type: typeof REWIND_SET_ENABLE;
bucket: string;
@@ -44,9 +50,10 @@ interface RewindReset {
type: typeof REWIND_RESET_REWIND;
}
interface FileModeEnabled {
type: typeof REWIND_FILE_MODE_ENABLED;
interface VersionsModeEnabled {
type: typeof BUCKET_BROWSER_VERSIONS_MODE_ENABLED;
status: boolean;
objectName: string;
}
interface OMNewObject {
@@ -77,9 +84,11 @@ interface OMCleanList {
interface OMToggleList {
type: typeof OBJECT_MANAGER_TOGGLE_LIST;
}
interface OMOpenList {
type: typeof OBJECT_MANAGER_OPEN_LIST;
}
interface OMCloseList {
type: typeof OBJECT_MANAGER_CLOSE_LIST;
}
@@ -89,10 +98,20 @@ interface SetSearchObjects {
searchString: string;
}
interface SetSearchVersions {
type: typeof BUCKET_BROWSER_VERSIONS_SET_SEARCH;
searchString: string;
}
interface SetSelectedversion {
type: typeof BUCKET_BROWSER_SET_SELECTED_VERSION;
selectedVersion: string;
}
export type ObjectBrowserActionTypes =
| RewindSetEnabled
| RewindReset
| FileModeEnabled
| VersionsModeEnabled
| OMNewObject
| OMUpdateProgress
| OMCompleteObject
@@ -101,7 +120,9 @@ export type ObjectBrowserActionTypes =
| OMToggleList
| OMOpenList
| OMCloseList
| SetSearchObjects;
| SetSearchObjects
| SetSearchVersions
| SetSelectedversion;
export const setRewindEnable = (
state: boolean,
@@ -122,10 +143,14 @@ export const resetRewind = () => {
};
};
export const setFileModeEnabled = (status: boolean) => {
export const setVersionsModeEnabled = (
status: boolean,
objectName: string = ""
) => {
return {
type: REWIND_FILE_MODE_ENABLED,
type: BUCKET_BROWSER_VERSIONS_MODE_ENABLED,
status,
objectName,
};
};
@@ -188,3 +213,17 @@ export const setSearchObjects = (searchString: string) => {
searchString,
};
};
export const setSearchVersions = (searchString: string) => {
return {
type: BUCKET_BROWSER_VERSIONS_SET_SEARCH,
searchString,
};
};
export const setSelectedVersion = (selectedVersion: string) => {
return {
type: BUCKET_BROWSER_SET_SELECTED_VERSION,
selectedVersion,
};
};

View File

@@ -16,7 +16,7 @@
import {
REWIND_SET_ENABLE,
REWIND_RESET_REWIND,
REWIND_FILE_MODE_ENABLED,
BUCKET_BROWSER_VERSIONS_MODE_ENABLED,
ObjectBrowserActionTypes,
OBJECT_MANAGER_NEW_OBJECT,
OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT,
@@ -27,6 +27,8 @@ import {
OBJECT_MANAGER_CLOSE_LIST,
OBJECT_MANAGER_OPEN_LIST,
OBJECT_MANAGER_SET_SEARCH_OBJECT,
BUCKET_BROWSER_VERSIONS_SET_SEARCH,
BUCKET_BROWSER_SET_SELECTED_VERSION,
} from "./actions";
export interface Route {
@@ -42,10 +44,13 @@ export interface RewindItem {
}
export interface ObjectBrowserState {
fileMode: boolean;
rewind: RewindItem;
objectManager: ObjectManager;
searchObjects: string;
versionsMode: boolean;
versionedFile: string;
searchVersions: string;
selectedVersion: string;
}
export interface ObjectBrowserReducer {
@@ -74,7 +79,7 @@ const defaultRewind = {
};
const initialState: ObjectBrowserState = {
fileMode: false,
versionsMode: false,
rewind: {
...defaultRewind,
},
@@ -83,6 +88,9 @@ const initialState: ObjectBrowserState = {
managerOpen: false,
},
searchObjects: "",
versionedFile: "",
searchVersions: "",
selectedVersion: "",
};
export function objectBrowserReducer(
@@ -105,8 +113,15 @@ export function objectBrowserReducer(
dateToRewind: null,
};
return { ...state, rewind: resetItem };
case REWIND_FILE_MODE_ENABLED:
return { ...state, fileMode: action.status };
case BUCKET_BROWSER_VERSIONS_MODE_ENABLED:
const objectN = !action.status ? "" : action.objectName;
return {
...state,
versionsMode: action.status,
versionedFile: objectN,
selectedVersion: "",
};
case OBJECT_MANAGER_NEW_OBJECT:
const cloneObjects = [
action.newObject,
@@ -220,6 +235,16 @@ export function objectBrowserReducer(
...state,
searchObjects: action.searchString,
};
case BUCKET_BROWSER_VERSIONS_SET_SEARCH:
return {
...state,
searchVersions: action.searchString,
};
case BUCKET_BROWSER_SET_SELECTED_VERSION:
return {
...state,
selectedVersion: action.selectedVersion,
};
default:
return state;
}