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 !== "" && (