Added versions multiselection & delete selected versions buttons (#1948)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 !== "" && (
|
||||
|
||||
Reference in New Issue
Block a user