From 42beef408c71d2cfc9728b58ad35561f15a1d2b6 Mon Sep 17 00:00:00 2001 From: Alex <33497058+bexsoft@users.noreply.github.com> Date: Wed, 4 May 2022 11:14:52 -0500 Subject: [PATCH] Added versions multiselection & delete selected versions buttons (#1948) --- portal-ui/src/icons/LinkIcon.tsx | 2 +- .../ObjectDetails/DeleteSelectedVersions.tsx | 115 ++++++++++++++++++ .../Objects/ObjectDetails/FileVersionItem.tsx | 28 +++++ .../ObjectDetails/VersionsNavigator.tsx | 77 +++++++++++- .../CheckboxWrapper/CheckboxWrapper.tsx | 8 ++ 5 files changed, 228 insertions(+), 2 deletions(-) create mode 100644 portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx diff --git a/portal-ui/src/icons/LinkIcon.tsx b/portal-ui/src/icons/LinkIcon.tsx index 4b6dba96f..bf4621a11 100644 --- a/portal-ui/src/icons/LinkIcon.tsx +++ b/portal-ui/src/icons/LinkIcon.tsx @@ -1,5 +1,5 @@ // This file is part of MinIO Console Server -// Copyright (c) 2021 MinIO, Inc. +// 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 diff --git a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx new file mode 100644 index 000000000..1a270488d --- /dev/null +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/DeleteSelectedVersions.tsx @@ -0,0 +1,115 @@ +// 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, { useState, useEffect } from "react"; +import { connect } from "react-redux"; +import { DialogContentText } from "@mui/material"; +import { setErrorSnackMessage } from "../../../../../../actions"; +import { ErrorResponseHandler } from "../../../../../../common/types"; +import ConfirmDialog from "../../../../Common/ModalWrapper/ConfirmDialog"; +import { ConfirmDeleteIcon } from "../../../../../../icons"; +import api from "../../../../../../common/api"; + +interface IDeleteSelectedVersionsProps { + closeDeleteModalAndRefresh: (refresh: boolean) => void; + deleteOpen: boolean; + selectedVersions: string[]; + selectedObject: string; + selectedBucket: string; + setErrorSnackMessage: typeof setErrorSnackMessage; +} + +const DeleteObject = ({ + closeDeleteModalAndRefresh, + deleteOpen, + selectedBucket, + selectedVersions, + selectedObject, + setErrorSnackMessage, +}: IDeleteSelectedVersionsProps) => { + const [deleteLoading, setDeleteLoading] = useState(false); + + const onClose = () => closeDeleteModalAndRefresh(false); + const onConfirmDelete = () => { + setDeleteLoading(true); + }; + + useEffect(() => { + if (deleteLoading) { + const selectedObjectsRequest = selectedVersions.map((versionID) => { + return { + path: selectedObject, + versionID: versionID, + recursive: false, + }; + }); + + if (selectedObjectsRequest.length > 0) { + api + .invoke( + "POST", + `/api/v1/buckets/${selectedBucket}/delete-objects?all_versions=false`, + selectedObjectsRequest + ) + .then(() => { + setDeleteLoading(false); + closeDeleteModalAndRefresh(true); + }) + .catch((error: ErrorResponseHandler) => { + setErrorSnackMessage(error); + setDeleteLoading(false); + }); + } + } + }, [ + deleteLoading, + closeDeleteModalAndRefresh, + selectedBucket, + selectedObject, + selectedVersions, + setErrorSnackMessage, + ]); + + if (!selectedVersions) { + return null; + } + + return ( + } + isLoading={deleteLoading} + onConfirm={onConfirmDelete} + onClose={onClose} + confirmationContent={ + + Are you sure you want to delete the selected {selectedVersions.length}{" "} + versions for {selectedObject}? + + } + /> + ); +}; + +const mapDispatchToProps = { + setErrorSnackMessage, +}; + +const connector = connect(null, mapDispatchToProps); + +export default connector(DeleteObject); 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 index 7d76d2403..3da616536 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/FileVersionItem.tsx @@ -31,12 +31,16 @@ import { } from "../../../../../../icons"; import { niceBytes } from "../../../../../../common/utils"; import SpecificVersionPill from "./SpecificVersionPill"; +import CheckboxWrapper from "../../../../Common/FormComponents/CheckboxWrapper/CheckboxWrapper"; interface IFileVersionItem { fileName: string; versionInfo: IFileInfo; index: number; isSelected?: boolean; + checkable: boolean; + isChecked: boolean; + onCheck: (versionID: string) => void; onShare: (versionInfo: IFileInfo) => void; onDownload: (versionInfo: IFileInfo) => void; onRestore: (versionInfo: IFileInfo) => void; @@ -112,6 +116,9 @@ const FileVersionItem = ({ fileName, versionInfo, isSelected, + checkable, + isChecked, + onCheck, onShare, onDownload, onRestore, @@ -180,6 +187,27 @@ const FileVersionItem = ({ + {checkable && ( + { + e.stopPropagation(); + e.preventDefault(); + onCheck(versionInfo.version_id || ""); + }} + value={versionInfo.version_id || ""} + disabled={versionInfo.is_delete_marker} + overrideCheckboxStyles={{ + paddingLeft: 0, + height: 34, + width: 25, + }} + noTopMargin + /> + )} {displayFileIconName(fileName, true)} v{index.toString()} {pill && } 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 index f626fcb43..99aac3703 100644 --- a/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx +++ b/portal-ui/src/screens/Console/Buckets/ListBuckets/Objects/ObjectDetails/VersionsNavigator.tsx @@ -61,7 +61,12 @@ import { } from "../../../../ObjectBrowser/actions"; import { AppState } from "../../../../../../store"; -import { DeleteNonCurrentIcon, VersionsIcon } from "../../../../../../icons"; +import { + DeleteIcon, + DeleteNonCurrentIcon, + SelectMultipleIcon, + VersionsIcon, +} from "../../../../../../icons"; import VirtualizedList from "../../../../Common/VirtualizedList/VirtualizedList"; import FileVersionItem from "./FileVersionItem"; import SelectWrapper from "../../../../Common/FormComponents/SelectWrapper/SelectWrapper"; @@ -69,6 +74,7 @@ import PreviewFileModal from "../Preview/PreviewFileModal"; import RBIconButton from "../../../BucketDetails/SummaryItems/RBIconButton"; import DeleteNonCurrent from "../ListObjects/DeleteNonCurrent"; import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs"; +import DeleteSelectedVersions from "./DeleteSelectedVersions"; const styles = (theme: Theme) => createStyles({ @@ -174,6 +180,9 @@ const VersionsNavigator = ({ const [previewOpen, setPreviewOpen] = useState(false); const [deleteNonCurrentOpen, setDeleteNonCurrentOpen] = useState(false); + const [selectEnabled, setSelectEnabled] = useState(false); + const [selectedItems, setSelectedItems] = useState([]); + const [delSelectedVOpen, setDelSelectedVOpen] = useState(false); // calculate object name to display let objectNameArray: string[] = []; @@ -318,6 +327,17 @@ const VersionsNavigator = ({ } }; + const closeSelectedVersions = (reloadOnComplete: boolean) => { + setDelSelectedVOpen(false); + + if (reloadOnComplete) { + setLoadingVersions(true); + setSelectedVersion(""); + setLoadingObjectInfo(true); + setSelectedItems([]); + } + }; + const totalSpace = versions.reduce((acc: number, currValue: IFileInfo) => { if (currValue.size) { return acc + parseInt(currValue.size); @@ -352,6 +372,23 @@ const VersionsNavigator = ({ } }); + const onCheckVersion = (selectedVersion: string) => { + if (selectedItems.includes(selectedVersion)) { + const filteredItems = selectedItems.filter( + (element) => element !== selectedVersion + ); + + setSelectedItems(filteredItems); + + return; + } + + const cloneState = [...selectedItems]; + cloneState.push(selectedVersion); + + setSelectedItems(cloneState); + }; + const renderVersion = (elementIndex: number) => { const item = filteredRecords[elementIndex]; const versOrd = versions.length - versions.indexOf(item); @@ -367,6 +404,9 @@ const VersionsNavigator = ({ onPreview={onPreviewItem} globalClick={onGlobalClick} isSelected={selectedVersion === item.version_id} + checkable={selectEnabled} + onCheck={onCheckVersion} + isChecked={selectedItems.includes(item.version_id || "")} /> ); }; @@ -419,6 +459,15 @@ const VersionsNavigator = ({ selectedObject={internalPaths} /> )} + {delSelectedVOpen && ( + + )} {!actualInfo && ( @@ -468,6 +517,32 @@ const VersionsNavigator = ({ } actions={ + { + setSelectEnabled(!selectEnabled); + }} + text={""} + icon={} + color="primary" + variant={selectEnabled ? "contained" : "outlined"} + style={{ marginRight: 8 }} + /> + {selectEnabled && ( + { + setDelSelectedVOpen(true); + }} + text={""} + icon={} + color="secondary" + style={{ marginRight: 8 }} + disabled={selectedItems.length === 0} + /> + )} { return ( @@ -94,6 +96,12 @@ const CheckboxWrapper = ({ checkedIcon={} icon={} disabled={disabled} + disableRipple + disableFocusRipple + focusRipple={false} + centerRipple={false} + disableTouchRipple + style={overrideCheckboxStyles || {}} /> {label !== "" && (