From 3395d1c853c93a0cd9f3f7eb0f599d0b44ddea67 Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Mon, 28 Feb 2022 09:17:57 -0700 Subject: [PATCH] Added navigation support to object versions (#1626) Signed-off-by: Benjamin Perez --- portal-ui/src/icons/NewPathIcon.tsx | 53 ++ portal-ui/src/icons/VersionsIcon.tsx | 39 ++ portal-ui/src/icons/index.ts | 2 + .../Buckets/BucketDetails/BrowserHandler.tsx | 71 +-- .../Objects/ListObjects/CreateFolderModal.tsx | 5 - .../Objects/ListObjects/DetailsListPanel.tsx | 3 +- .../Objects/ListObjects/ListObjects.tsx | 203 +++---- .../ListObjects/ListObjectsHelpers.tsx | 100 ++++ .../Objects/ListObjects/ObjectDetailPanel.tsx | 326 +++++++----- .../ListBuckets/Objects/ListObjects/utils.tsx | 133 ++--- .../Objects/ObjectDetails/FileVersionItem.tsx | 198 +++++++ .../Objects/ObjectDetails/ObjectDetails.tsx | 12 +- .../ObjectDetails/VersionsNavigator.tsx | 496 ++++++++++++++++++ .../FormSwitchWrapper/FormSwitchWrapper.tsx | 7 +- .../FormComponents/common/styleLibrary.ts | 19 +- .../screens/Console/Common/IconsScreen.tsx | 10 + .../ObjectBrowser/BrowserBreadcrumbs.tsx | 64 ++- .../screens/Console/ObjectBrowser/actions.ts | 53 +- .../screens/Console/ObjectBrowser/reducers.ts | 35 +- 19 files changed, 1429 insertions(+), 400 deletions(-) create mode 100644 portal-ui/src/icons/NewPathIcon.tsx create mode 100644 portal-ui/src/icons/VersionsIcon.tsx create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjectsHelpers.tsx create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx diff --git a/portal-ui/src/icons/NewPathIcon.tsx b/portal-ui/src/icons/NewPathIcon.tsx new file mode 100644 index 000000000..217862e09 --- /dev/null +++ b/portal-ui/src/icons/NewPathIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const NewPathIcon = (props: SVGProps) => ( + + + + + + + + + +); + +export default NewPathIcon; diff --git a/portal-ui/src/icons/VersionsIcon.tsx b/portal-ui/src/icons/VersionsIcon.tsx new file mode 100644 index 000000000..046552e0c --- /dev/null +++ b/portal-ui/src/icons/VersionsIcon.tsx @@ -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 . + +import * as React from "react"; +import { SVGProps } from "react"; + +const VersionsIcon = (props: SVGProps) => ( + + + +); + +export default VersionsIcon; diff --git a/portal-ui/src/icons/index.ts b/portal-ui/src/icons/index.ts index d93aba041..675a8572a 100644 --- a/portal-ui/src/icons/index.ts +++ b/portal-ui/src/icons/index.ts @@ -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"; diff --git a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx index 9e337000a..d549941f8 100644 --- a/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx +++ b/portal-ui/src/screens/Console/Buckets/BucketDetails/BrowserHandler.tsx @@ -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 = ({ - {fileMode ? ( - - - Buckets - {" "} - > {bucketName} - - ) : ( - - - - )} - + } actions={ - {!fileMode && ( + {!versionsMode ? ( { setSearchObjects(value); }} value={searchObjects} /> + ) : ( + + { + setSearchVersions(value); + }} + value={searchVersions} + /> + )} } /> - {fileMode ? : } + + + ); }; 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); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/CreateFolderModal.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/CreateFolderModal.tsx index 215013bc7..e6861ffc6 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/CreateFolderModal.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/CreateFolderModal.tsx @@ -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, }; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DetailsListPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DetailsListPanel.tsx index c6adca16f..2960820d6 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DetailsListPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/DetailsListPanel.tsx @@ -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, diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx index e85754bc2..83e8f1949 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjects.tsx @@ -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([]); const [loading, setLoading] = useState(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 {object.last_modified}; - }; - - 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 = ({ > + {versionsMode ? ( + + {selectedInternalPaths !== null && ( + + )} + + ) : ( + + + + )} - { 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); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjectsHelpers.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjectsHelpers.tsx new file mode 100644 index 000000000..10fa9250f --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ListObjectsHelpers.tsx @@ -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 . + +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 {object.last_modified}; +}; + +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", + }, +]; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx index 1d725957c..2ab63da35 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/ObjectDetailPanel.tsx @@ -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(true); const [shareFileModalOpen, setShareFileModalOpen] = useState(false); @@ -151,6 +158,7 @@ const ObjectDetailPanel = ({ const [selectedTag, setSelectedTag] = useState(["", ""]); const [legalholdOpen, setLegalholdOpen] = useState(false); const [actualInfo, setActualInfo] = useState(null); + const [allInfoElements, setAllInfoElements] = useState([]); const [objectToShare, setObjectToShare] = useState(null); const [versions, setVersions] = useState([]); const [deleteOpen, setDeleteOpen] = useState(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 ( {shareFileModalOpen && actualInfo && ( @@ -428,11 +448,8 @@ const ObjectDetailPanel = ({ )} -
- {objectNameArray.length > 0 - ? objectNameArray[objectNameArray.length - 1] - : actualInfo.name} -
+ +
{objectName}
  • Actions:
  • @@ -479,17 +496,22 @@ const ObjectDetailPanel = ({ onClick={() => { setDeleteOpen(true); }} - disabled={actualInfo.is_delete_marker} + disabled={actualInfo.is_delete_marker || selectedVersion !== ""} />
  • } + label={ + versionsMode ? "Hide Object Versions" : "Display Object Versions" + } + icon={} onClick={() => { - openExtraInfo(); + setVersionsModeEnabled(!versionsMode, objectName); }} + disabled={ + !(actualInfo.version_id && actualInfo.version_id !== "null") + } />
@@ -497,107 +519,150 @@ const ObjectDetailPanel = ({

Details

+ {selectedVersion !== "" && ( + + Version ID: +
+ {selectedVersion} +
+ )} - { - setTagModalOpen(true); - }} - /> - } - /> + {selectedVersion === "" ? ( + { + setTagModalOpen(true); + }} + /> + } + /> + ) : ( + + Tags: +
+ {tagKeys.length === 0 + ? "N/A" + : tagKeys.map((tagKey, index) => { + return ( + + {tagKey}:{get(actualInfo, `tags.${tagKey}`, "")} + {index < tagKeys.length - 1 ? ", " : ""} + + ); + })} +
+ )}
- { - setLegalholdOpen(true); - }} - isLoading={false} - /> - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> + {selectedVersion === "" ? ( + { + setLegalholdOpen(true); + }} + isLoading={false} + /> + ) : ( + } + label={ + + } + /> + } + /> + ) + } + /> + ) : ( + + Legal Hold: +
+ {actualInfo.legal_hold_status ? "On" : "Off"} +
+ )}
- +
+ - - ) : ( - } - label={ - - } - /> - } - /> - ) - } - /> + {selectedVersion === "" ? ( + + ) : ( + } + label={ + + } + /> + } + /> + ) + } + /> + ) : ( + + Object Retention: +
+ {actualInfo.retention_mode + ? actualInfo.retention_mode.toLowerCase() + : "None"} +
+ )}

@@ -616,25 +681,27 @@ const ObjectDetailPanel = ({
- {actualInfo.version_id && actualInfo.version_id !== "null" && ( - -
-

Versions

-
- - - Total available versions -
- {versions.length} + {actualInfo.version_id && + actualInfo.version_id !== "null" && + selectedVersion === "" && ( + +
+

Versions

+
+ + + Total available versions +
+ {versions.length} +
+ + Versions Stored size: +
+ {niceBytesInt(totalVersionsSize)} +
- - Versions Stored size: -
- {niceBytesInt(totalVersionsSize)} -
-
-
- )} + + )} ); }; @@ -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); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/utils.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/utils.tsx index 399c3e732..a0b9da032 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/utils.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ListObjects/utils.tsx @@ -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: , + extensions: ["mp4", "mov", "avi", "mpeg", "mpg"], + }, + { + icon: , + extensions: ["mp3", "m4a", "aac"], + }, + { + icon: , + extensions: ["pdf"], + }, + { + icon: , + extensions: ["ppt", "pptx"], + }, + { + icon: , + extensions: ["xls", "xlsx"], + }, + { + icon: , + extensions: ["cer", "crt", "pem"], + }, + { + icon: , + extensions: ["html", "xml", "css", "py", "go", "php", "cpp", "h", "java"], + }, + { + icon: , + extensions: ["cfg", "yaml"], + }, + { + icon: , + extensions: ["sql"], + }, + { + icon: , + extensions: ["ttf", "otf"], + }, + { + icon: , + extensions: ["txt"], + }, + { + icon: , + extensions: ["zip", "rar", "tar", "gz"], + }, + { + icon: , + extensions: ["epub", "mobi", "azw", "azw3"], + }, + { + icon: , + extensions: ["jpeg", "jpg", "gif", "tiff", "png", "heic", "dng"], + }, +]; + +export const displayFileIconName = (element: string, returnOnlyIcon: boolean = false) => { let elementString = element; let icon = ; // 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: , - extensions: ["mp4", "mov", "avi", "mpeg", "mpg"], - }, - { - icon: , - extensions: ["mp3", "m4a", "aac"], - }, - { - icon: , - extensions: ["pdf"], - }, - { - icon: , - extensions: ["ppt", "pptx"], - }, - { - icon: , - extensions: ["xls", "xlsx"], - }, - { - icon: , - extensions: ["cer", "crt", "pem"], - }, - { - icon: , - extensions: ["html", "xml", "css", "py", "go", "php", "cpp", "h", "java"], - }, - { - icon: , - extensions: ["cfg", "yaml"], - }, - { - icon: , - extensions: ["sql"], - }, - { - icon: , - extensions: ["ttf", "otf"], - }, - { - icon: , - extensions: ["txt"], - }, - { - icon: , - extensions: ["zip", "rar", "tar", "gz"], - }, - { - icon: , - extensions: ["epub", "mobi", "azw", "azw3"], - }, - { - icon: , - 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 ; }; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx new file mode 100644 index 000000000..e825290e2 --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx @@ -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 . + +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: , + action: onDownload, + tooltip: "Download this version", + }, + { + icon: , + action: onShare, + tooltip: "Share this version", + }, + { + icon: , + action: onRestore, + tooltip: "Restore this version", + }, + ]; + + return ( + { + globalClick(versionInfo); + }} + > + + + + + {displayFileIconName(fileName, true)} v{index.toString()} + + + {versionItemButtons.map((button, index) => { + return ( + + { + 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} + + + ); + })} + + + + + {versionInfo.version_id} + + + + Last modified:{" "} + + {versionInfo.last_modified} + + + + Deleted:{" "} + {versionInfo.is_delete_marker ? "Yes" : "No"} + + + + + ); +}; + +export default withStyles(styles)(FileVersionItem); diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx index feccf7756..8db153ed8 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/ObjectDetails.tsx @@ -429,7 +429,7 @@ const ObjectDetails = ({ }; return ( - + {shareFileModalOpen && actualInfo && ( +

Details

@@ -717,7 +717,7 @@ const ObjectDetails = ({ actualInfo={actualInfo} /> ) : null} -
+ ), }} {{ @@ -797,7 +797,7 @@ const ObjectDetails = ({ disabled: extensionPreview(currentItem) === "none", }, content: ( - + {actualInfo && ( )} - + ), }} )} - + ); }; diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx new file mode 100644 index 000000000..a8c04eefe --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx @@ -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 . + +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(true); + const [shareFileModalOpen, setShareFileModalOpen] = useState(false); + const [actualInfo, setActualInfo] = useState(null); + const [objectToShare, setObjectToShare] = useState(null); + const [versions, setVersions] = useState([]); + const [restoreVersionOpen, setRestoreVersionOpen] = useState(false); + const [restoreVersion, setRestoreVersion] = useState(""); + const [sortValue, setSortValue] = useState("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 ( + + ); + }; + + return ( + + {shareFileModalOpen && actualInfo && ( + + )} + {restoreVersionOpen && actualInfo && ( + + )} + + {!actualInfo && ( + + + + )} + + {actualInfo && ( + + + + + + } + title={ + + {objectNameArray.length > 0 + ? objectNameArray[objectNameArray.length - 1] + : actualInfo.name}{" "} + Versions + + } + subTitle={ + + + + + {versions.length} Version + {versions.length === 1 ? "" : "s"}    + + + + {niceBytesInt(totalSpace)} + + + + } + actions={ + + Sort by + ) => { + setSortValue(e.target.value as string); + }} + name={"sort-by"} + options={[ + { label: "Date", value: "date" }, + { + label: "Version ID", + value: "version", + }, + { label: "Deleted", value: "deleted" }, + ]} + /> + + } + className={classes.noBottomBorder} + /> + + + {actualInfo.version_id && actualInfo.version_id !== "null" && ( + + )} + + + )} + + + ); +}; + +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)); diff --git a/portal-ui/src/screens/Console/Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper.tsx b/portal-ui/src/screens/Console/Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper.tsx index a40990cd3..1837fe279 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper.tsx +++ b/portal-ui/src/screens/Console/Common/FormComponents/FormSwitchWrapper/FormSwitchWrapper.tsx @@ -175,7 +175,12 @@ const FormSwitchWrapper = ({ - + {label !== "" && ( {label} diff --git a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts index 86063db8f..b95c71f3f 100644 --- a/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts +++ b/portal-ui/src/screens/Console/Common/FormComponents/common/styleLibrary.ts @@ -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: { diff --git a/portal-ui/src/screens/Console/Common/IconsScreen.tsx b/portal-ui/src/screens/Console/Common/IconsScreen.tsx index 5842ece8b..6b42498dd 100644 --- a/portal-ui/src/screens/Console/Common/IconsScreen.tsx +++ b/portal-ui/src/screens/Console/Common/IconsScreen.tsx @@ -569,6 +569,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { NewAccountIcon + +
+ NewPathIcon +
+
NewPoolIcon @@ -834,6 +839,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => { VersionIcon
+ +
+ VersionsIcon +
+
WarnIcon diff --git a/portal-ui/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.tsx b/portal-ui/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.tsx index 1b75c6622..5e157eacf 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.tsx +++ b/portal-ui/src/screens/Console/ObjectBrowser/BrowserBreadcrumbs.tsx @@ -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(false); @@ -82,22 +87,55 @@ const BrowserBreadcrumbs = ({ return ( / - {objectItem} + { + setVersionsModeEnabled(false); + }} + > + {objectItem} + ); }); + let versionsItem: any[] = []; + + if (versionsMode) { + versionsItem = [ + + / {versionedFile} - Versions + , + ]; + } + const listBreadcrumbs: any[] = [ - {bucketName} + { + setVersionsModeEnabled(false); + }} + > + {bucketName} + , ...breadcrumbsMap, + ...versionsItem, ]; const closeAddFolderModal = () => { setCreateFolderOpen(false); }; + const goBackFunction = () => { + if (versionsMode) { + setVersionsModeEnabled(false); + } else { + history.goBack(); + } + }; + return ( {createFolderOpen && ( @@ -111,14 +149,11 @@ const BrowserBreadcrumbs = ({ )} { - 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", }} > - +
@@ -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)); diff --git a/portal-ui/src/screens/Console/ObjectBrowser/actions.ts b/portal-ui/src/screens/Console/ObjectBrowser/actions.ts index 432706ceb..5b1a3bd29 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/actions.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/actions.ts @@ -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, + }; +}; diff --git a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts index cc40ecdca..515fcaa9f 100644 --- a/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts +++ b/portal-ui/src/screens/Console/ObjectBrowser/reducers.ts @@ -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; }