Added versions multiselection & delete selected versions buttons (#1948)

This commit is contained in:
Alex
2022-05-04 11:14:52 -05:00
committed by GitHub
parent c43d84f14b
commit 42beef408c
5 changed files with 228 additions and 2 deletions

View File

@@ -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

View File

@@ -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 <http://www.gnu.org/licenses/>.
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<boolean>(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 (
<ConfirmDialog
title={`Delete Selected Versions`}
confirmText={"Delete"}
isOpen={deleteOpen}
titleIcon={<ConfirmDeleteIcon />}
isLoading={deleteLoading}
onConfirm={onConfirmDelete}
onClose={onClose}
confirmationContent={
<DialogContentText>
Are you sure you want to delete the selected {selectedVersions.length}{" "}
versions for <strong>{selectedObject}</strong>?
</DialogContentText>
}
/>
);
};
const mapDispatchToProps = {
setErrorSnackMessage,
};
const connector = connect(null, mapDispatchToProps);
export default connector(DeleteObject);

View File

@@ -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 = ({
<Grid item xs={12} justifyContent={"space-between"}>
<Grid container>
<Grid item xs={4} className={classes.versionContainer}>
{checkable && (
<CheckboxWrapper
checked={isChecked}
id={`select-${versionInfo.version_id}`}
label={""}
name={`select-${versionInfo.version_id}`}
onChange={(e) => {
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 && <SpecificVersionPill type={pill} />}
</Grid>

View File

@@ -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<boolean>(false);
const [deleteNonCurrentOpen, setDeleteNonCurrentOpen] =
useState<boolean>(false);
const [selectEnabled, setSelectEnabled] = useState<boolean>(false);
const [selectedItems, setSelectedItems] = useState<string[]>([]);
const [delSelectedVOpen, setDelSelectedVOpen] = useState<boolean>(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 && (
<DeleteSelectedVersions
selectedBucket={bucketName}
selectedObject={decodeFileName(internalPaths)}
deleteOpen={delSelectedVOpen}
selectedVersions={selectedItems}
closeDeleteModalAndRefresh={closeSelectedVersions}
/>
)}
<Grid container className={classes.versionsContainer}>
{!actualInfo && (
<Grid item xs={12}>
@@ -468,6 +517,32 @@ const VersionsNavigator = ({
}
actions={
<Fragment>
<RBIconButton
id={"select-multiple-versions"}
tooltip={"Select Multiple Versions"}
onClick={() => {
setSelectEnabled(!selectEnabled);
}}
text={""}
icon={<SelectMultipleIcon />}
color="primary"
variant={selectEnabled ? "contained" : "outlined"}
style={{ marginRight: 8 }}
/>
{selectEnabled && (
<RBIconButton
id={"delete-multiple-versions"}
tooltip={"Delete Selected Versions"}
onClick={() => {
setDelSelectedVOpen(true);
}}
text={""}
icon={<DeleteIcon />}
color="secondary"
style={{ marginRight: 8 }}
disabled={selectedItems.length === 0}
/>
)}
<RBIconButton
id={"delete-non-current"}
tooltip={"Delete Non Current Versions"}

View File

@@ -35,6 +35,7 @@ interface CheckBoxProps {
disabled?: boolean;
tooltip?: string;
overrideLabelClasses?: string;
overrideCheckboxStyles?: React.CSSProperties;
index?: number;
noTopMargin?: boolean;
checked: boolean;
@@ -71,6 +72,7 @@ const CheckboxWrapper = ({
noTopMargin = false,
tooltip = "",
overrideLabelClasses = "",
overrideCheckboxStyles,
classes,
}: CheckBoxProps) => {
return (
@@ -94,6 +96,12 @@ const CheckboxWrapper = ({
checkedIcon={<span className={classes.checkedIcon} />}
icon={<span className={classes.unCheckedIcon} />}
disabled={disabled}
disableRipple
disableFocusRipple
focusRipple={false}
centerRipple={false}
disableTouchRipple
style={overrideCheckboxStyles || {}}
/>
</div>
{label !== "" && (