Added Object Manager feature for Uploads & downloads (#1265)

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2021-11-30 12:06:01 -06:00
committed by GitHub
parent 347c6aba3b
commit c529a6d127
14 changed files with 972 additions and 222 deletions

View File

@@ -0,0 +1,47 @@
// 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 <http://www.gnu.org/licenses/>.
import * as React from "react";
import { SVGProps } from "react";
const ObjectManagerIcon = (props: SVGProps<SVGSVGElement>) => {
return (
<svg
{...props}
className={`min-icon`}
fill={"currentcolor"}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 256 256"
>
<g id="Layer 1">
<path
d="M217.452+193.452L217.452+224.458L38.4601+224.458L38.4601+193.452L0.104767+193.452L0.104767+255.464L255.807+255.464L255.807+193.452L217.452+193.452Z"
opacity="1"
/>
<path
d="M70.1156+194.746L98.6658+194.746L98.6658+97.0605L120.994+97.0605L84.3907+51.995L47.7878+97.0605L70.1156+97.0605L70.1156+194.746Z"
opacity="1"
/>
<path
d="M183.757+52.6023L155.207+52.6922L155.515+150.377L133.187+150.448L169.932+195.398L206.392+150.217L184.065+150.288L183.757+52.6023Z"
opacity="1"
/>
</g>
</svg>
);
};
export default ObjectManagerIcon;

View File

@@ -77,6 +77,7 @@ export { default as CircleIcon } from "./CircleIcon";
export { default as PreviewIcon } from "./PreviewIcon";
export { default as LockIcon } from "./LockIcon";
export { default as VersionIcon } from "./VersionIcon";
export { default as ObjectManagerIcon } from "./ObjectManagerIcon";
export { default as FileLockIcon } from "./FileLockIcon";
export { default as FileXlsIcon } from "./FileXlsIcon";

View File

@@ -64,8 +64,6 @@ import { ErrorResponseHandler } from "../../../../../../common/types";
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import AddFolderIcon from "../../../../../../icons/AddFolderIcon";
import HistoryIcon from "../../../../../../icons/HistoryIcon";
import ObjectBrowserIcon from "../../../../../../icons/ObjectBrowserIcon";
import ObjectBrowserFolderIcon from "../../../../../../icons/ObjectBrowserFolderIcon";
import FolderIcon from "../../../../../../icons/FolderIcon";
import RefreshIcon from "../../../../../../icons/RefreshIcon";
import UploadIcon from "../../../../../../icons/UploadIcon";
@@ -73,24 +71,7 @@ import { setBucketDetailsLoad, setBucketInfo } from "../../../actions";
import { AppState } from "../../../../../../store";
import PageLayout from "../../../../Common/Layout/PageLayout";
import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton";
import {
DeleteIcon,
FileBookIcon,
FileCodeIcon,
FileConfigIcon,
FileDbIcon,
FileFontIcon,
FileImageIcon,
FileLockIcon,
FileMissingIcon,
FileMusicIcon,
FilePdfIcon,
FilePptIcon,
FileTxtIcon,
FileVideoIcon,
FileXlsIcon,
FileZipIcon,
} from "../../../../../../icons";
import { DeleteIcon } from "../../../../../../icons";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import SecureComponent, {
hasPermission,
@@ -98,6 +79,12 @@ import SecureComponent, {
import SearchBox from "../../../../Common/SearchBox";
import withSuspense from "../../../../Common/Components/withSuspense";
import {
setNewObject,
updateProgress,
completeObject,
} from "../../../../ObjectBrowser/actions";
import { displayName } from "./utils";
const CreateFolderModal = withSuspense(
React.lazy(() => import("./CreateFolderModal"))
@@ -233,6 +220,9 @@ interface IListObjectsProps {
setBucketInfo: typeof setBucketInfo;
bucketInfo: BucketInfo | null;
setBucketDetailsLoad: typeof setBucketDetailsLoad;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
}
function useInterval(callback: any, delay: number) {
@@ -277,6 +267,9 @@ const ListObjects = ({
loadingBucket,
setBucketInfo,
bucketInfo,
setNewObject,
updateProgress,
completeObject,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObject[]>([]);
const [loading, setLoading] = useState<boolean>(true);
@@ -616,62 +609,86 @@ const ListObjects = ({
return;
}
e.preventDefault();
let files = e.target.files;
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
let xhr = new XMLHttpRequest();
const areMultipleFiles = files.length > 1;
const errorMessage = `An error occurred while uploading the file${
areMultipleFiles ? "s" : ""
}.`;
const okMessage = `Object${
areMultipleFiles ? "s" : ``
} uploaded successfully.`;
xhr.open("POST", uploadUrl, true);
if (files.length > 0) {
let uploadUrl = `api/v1/buckets/${bucketName}/objects/upload`;
xhr.withCredentials = false;
xhr.onload = function (event) {
if (
xhr.status === 401 ||
xhr.status === 403 ||
xhr.status === 400 ||
xhr.status === 500
) {
setSnackBarMessage(errorMessage);
if (encodedPath !== "") {
uploadUrl = `${uploadUrl}?prefix=${encodedPath}`;
}
if (xhr.status === 200) {
setSnackBarMessage(okMessage);
for (let file of files) {
const fileName = file.name;
const blobFile = new Blob([file], { type: file.type });
const identity = btoa(
`${bucketName}-${encodedPath}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identity,
percentage: 0,
prefix: `${decodeFileName(encodedPath)}${fileName}`,
type: "upload",
waitingForFile: false,
});
let xhr = new XMLHttpRequest();
const areMultipleFiles = files.length > 1;
const errorMessage = `An error occurred while uploading the file${
areMultipleFiles ? "s" : ""
}.`;
const okMessage = `Object${
areMultipleFiles ? "s" : ``
} uploaded successfully.`;
xhr.open("POST", uploadUrl, true);
xhr.withCredentials = false;
xhr.onload = function (event) {
if (
xhr.status === 401 ||
xhr.status === 403 ||
xhr.status === 400 ||
xhr.status === 500
) {
setSnackBarMessage(errorMessage);
}
if (xhr.status === 200) {
completeObject(identity);
setSnackBarMessage(okMessage);
}
};
xhr.upload.addEventListener("error", (event) => {
setSnackBarMessage(errorMessage);
});
xhr.upload.addEventListener("progress", (event) => {
const progress = Math.floor((event.loaded * 100) / event.total);
updateProgress(identity, progress);
});
xhr.onerror = () => {
setSnackBarMessage(errorMessage);
};
xhr.onloadend = () => {
setLoading(true);
setLoadingProgress(100);
};
const formData = new FormData();
formData.append(file.size, blobFile, fileName);
xhr.send(formData);
}
};
xhr.upload.addEventListener("error", (event) => {
setSnackBarMessage(errorMessage);
});
xhr.upload.addEventListener("progress", (event) => {
setLoadingProgress(Math.floor((event.loaded * 100) / event.total));
});
xhr.onerror = () => {
setSnackBarMessage(errorMessage);
};
xhr.onloadend = () => {
setLoading(true);
setLoadingProgress(100);
};
const formData = new FormData();
for (let file of files) {
const fileName = file.name;
const blobFile = new Blob([file], { type: file.type });
formData.append(file.size, blobFile, fileName);
}
xhr.send(formData);
e.target.value = null;
};
@@ -699,14 +716,32 @@ const ListObjects = ({
};
const downloadObject = (object: BucketObject) => {
if (object.size > 104857600) {
// If file is bigger than 100MB we show a notification
setSnackBarMessage(
"Download process started, it may take a few moments to complete"
);
}
const identityDownload = btoa(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
download(bucketName, encodeFileName(object.name), object.version_id);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
bucketName,
encodeFileName(object.name),
object.version_id,
object.size,
(progress) => {
updateProgress(identityDownload, progress);
},
() => {
completeObject(identityDownload);
}
);
};
const openPath = (idElement: string) => {
@@ -783,113 +818,6 @@ const ListObjects = ({
});
}
const displayName = (element: string) => {
let elementString = element;
let icon = <ObjectBrowserIcon />;
// Element is a folder
if (element.endsWith("/")) {
icon = <ObjectBrowserFolderIcon />;
elementString = element.substr(0, element.length - 1);
}
interface IExtToIcon {
icon: any;
extensions: string[];
}
const extensionToIcon: IExtToIcon[] = [
{
icon: <FileVideoIcon />,
extensions: ["mp4", "mov", "avi", "mpeg", "mpg"],
},
{
icon: <FileMusicIcon />,
extensions: ["mp3", "m4a", "aac"],
},
{
icon: <FilePdfIcon />,
extensions: ["pdf"],
},
{
icon: <FilePptIcon />,
extensions: ["ppt", "pptx"],
},
{
icon: <FileXlsIcon />,
extensions: ["xls", "xlsx"],
},
{
icon: <FileLockIcon />,
extensions: ["cer", "crt", "pem"],
},
{
icon: <FileCodeIcon />,
extensions: [
"html",
"xml",
"css",
"py",
"go",
"php",
"cpp",
"h",
"java",
],
},
{
icon: <FileConfigIcon />,
extensions: ["cfg", "yaml"],
},
{
icon: <FileDbIcon />,
extensions: ["sql"],
},
{
icon: <FileFontIcon />,
extensions: ["ttf", "otf"],
},
{
icon: <FileTxtIcon />,
extensions: ["txt"],
},
{
icon: <FileZipIcon />,
extensions: ["zip", "rar", "tar", "gz"],
},
{
icon: <FileBookIcon />,
extensions: ["epub", "mobi", "azw", "azw3"],
},
{
icon: <FileImageIcon />,
extensions: ["jpeg", "jpg", "gif", "tiff", "png", "heic", "dng"],
},
];
const lowercaseElement = element.toLowerCase();
for (const etc of extensionToIcon) {
for (const ext of etc.extensions) {
if (lowercaseElement.endsWith(`.${ext}`)) {
icon = etc.icon;
}
}
}
if (!element.endsWith("/") && element.indexOf(".") < 0) {
icon = <FileMissingIcon />;
}
const splitItem = elementString.split("/");
return (
<div className={classes.fileName}>
{icon}
<span className={classes.fileNameText}>
{splitItem[splitItem.length - 1]}
</span>
</div>
);
};
const filteredRecords = records.filter((b: BucketObject) => {
if (filterObjects === "") {
return true;
@@ -938,11 +866,15 @@ const ListObjects = ({
setLoading(true);
};
const renderName = (element: string) => {
return displayName(element, classes);
};
const listModeColumns = [
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
renderFunction: renderName,
enableSort: true,
},
{
@@ -967,7 +899,7 @@ const ListObjects = ({
{
label: "Name",
elementKey: "name",
renderFunction: displayName,
renderFunction: renderName,
enableSort: true,
},
{
@@ -1118,7 +1050,7 @@ const ListObjects = ({
</BoxIconButton>
<input
type="file"
multiple={true}
multiple
onChange={(e) => uploadObject(e)}
id="file-input"
style={{ display: "none" }}
@@ -1244,6 +1176,9 @@ const mapDispatchToProps = {
resetRewind,
setBucketDetailsLoad,
setBucketInfo,
setNewObject,
updateProgress,
completeObject,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -0,0 +1,133 @@
// 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 <http://www.gnu.org/licenses/>.
import React from "react";
import {
FileBookIcon,
FileCodeIcon,
FileConfigIcon,
FileDbIcon,
FileFontIcon,
FileImageIcon,
FileLockIcon,
FileMissingIcon,
FileMusicIcon,
FilePdfIcon,
FilePptIcon,
FileTxtIcon,
FileVideoIcon,
FileXlsIcon,
FileZipIcon,
} from "../../../../../../icons";
import ObjectBrowserIcon from "../../../../../../icons/ObjectBrowserIcon";
import ObjectBrowserFolderIcon from "../../../../../../icons/ObjectBrowserFolderIcon";
export const displayName = (element: string, classes: any) => {
let elementString = element;
let icon = <ObjectBrowserIcon />;
// Element is a folder
if (element.endsWith("/")) {
icon = <ObjectBrowserFolderIcon />;
elementString = element.substr(0, element.length - 1);
}
interface IExtToIcon {
icon: any;
extensions: string[];
}
const extensionToIcon: IExtToIcon[] = [
{
icon: <FileVideoIcon />,
extensions: ["mp4", "mov", "avi", "mpeg", "mpg"],
},
{
icon: <FileMusicIcon />,
extensions: ["mp3", "m4a", "aac"],
},
{
icon: <FilePdfIcon />,
extensions: ["pdf"],
},
{
icon: <FilePptIcon />,
extensions: ["ppt", "pptx"],
},
{
icon: <FileXlsIcon />,
extensions: ["xls", "xlsx"],
},
{
icon: <FileLockIcon />,
extensions: ["cer", "crt", "pem"],
},
{
icon: <FileCodeIcon />,
extensions: ["html", "xml", "css", "py", "go", "php", "cpp", "h", "java"],
},
{
icon: <FileConfigIcon />,
extensions: ["cfg", "yaml"],
},
{
icon: <FileDbIcon />,
extensions: ["sql"],
},
{
icon: <FileFontIcon />,
extensions: ["ttf", "otf"],
},
{
icon: <FileTxtIcon />,
extensions: ["txt"],
},
{
icon: <FileZipIcon />,
extensions: ["zip", "rar", "tar", "gz"],
},
{
icon: <FileBookIcon />,
extensions: ["epub", "mobi", "azw", "azw3"],
},
{
icon: <FileImageIcon />,
extensions: ["jpeg", "jpg", "gif", "tiff", "png", "heic", "dng"],
},
];
const lowercaseElement = element.toLowerCase();
for (const etc of extensionToIcon) {
for (const ext of etc.extensions) {
if (lowercaseElement.endsWith(`.${ext}`)) {
icon = etc.icon;
}
}
}
if (!element.endsWith("/") && element.indexOf(".") < 0) {
icon = <FileMissingIcon />;
}
const splitItem = elementString.split("/");
return (
<div className={classes.fileName}>
{icon}
<span className={classes.fileNameText}>
{splitItem[splitItem.length - 1]}
</span>
</div>
);
};

View File

@@ -81,6 +81,11 @@ import VerticalTabs from "../../../../Common/VerticalTabs/VerticalTabs";
import BoxIconButton from "../../../../Common/BoxIconButton/BoxIconButton";
import { RecoverIcon } from "../../../../../../icons";
import SecureComponent from "../../../../../../common/SecureComponent/SecureComponent";
import {
setNewObject,
updateProgress,
completeObject,
} from "../../../../ObjectBrowser/actions";
const styles = (theme: Theme) =>
createStyles({
@@ -225,6 +230,9 @@ interface IObjectDetailsProps {
distributedSetup: boolean;
setErrorSnackMessage: typeof setErrorSnackMessage;
setSnackBarMessage: typeof setSnackBarMessage;
setNewObject: typeof setNewObject;
updateProgress: typeof updateProgress;
completeObject: typeof completeObject;
}
const emptyFile: IFileInfo = {
@@ -249,6 +257,9 @@ const ObjectDetails = ({
bucketToRewind,
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
}: IObjectDetailsProps) => {
const [loadObjectData, setLoadObjectData] = useState<boolean>(true);
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
@@ -367,14 +378,32 @@ const ObjectDetails = ({
};
const downloadObject = (object: IFileInfo) => {
if (object.size && parseInt(object.size) > 104857600) {
// If file is bigger than 100MB we show a notification
setSnackBarMessage(
"Download process started, it may take a few moments to complete"
);
}
const identityDownload = btoa(
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
download(bucketName, internalPaths, object.version_id);
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 tableActions: ItemActions[] = [
@@ -951,6 +980,9 @@ const mapStateToProps = ({ objectBrowser, system }: AppState) => ({
const mapDispatchToProps = {
setErrorSnackMessage,
setSnackBarMessage,
setNewObject,
updateProgress,
completeObject,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -19,7 +19,10 @@ import { BucketObject, RewindObject } from "./ListObjects/types";
export const download = (
bucketName: string,
objectPath: string,
versionID: any
versionID: any,
fileSize: number,
progressCallback: (progress: number) => void,
completeCallback: () => void
) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
@@ -27,7 +30,46 @@ export const download = (
if (versionID) {
path = path.concat(`&version_id=${versionID}`);
}
window.location.href = path;
var req = new XMLHttpRequest();
req.open("GET", path, true);
req.addEventListener(
"progress",
function (evt) {
var percentComplete = Math.round((evt.loaded / fileSize) * 100);
if (progressCallback) {
progressCallback(percentComplete);
}
},
false
);
req.responseType = "blob";
req.onreadystatechange = () => {
if (req.readyState === 4 && req.status === 200) {
const rspHeader = req.getResponseHeader("Content-Disposition");
let filename = "download";
if (rspHeader) {
filename = rspHeader.split('"')[1];
}
if (completeCallback) {
completeCallback();
}
var link = document.createElement("a");
link.href = window.URL.createObjectURL(req.response);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
req.send();
};
// Review file extension by name & returns the type of preview browser that can be used
@@ -50,7 +92,7 @@ export const extensionPreview = (
"png",
"heic",
];
const textExtensions = ["pdf", "txt"];
const textExtensions = ["pdf", "txt", "json"];
const audioExtensions = ["wav", "mp3", "alac", "aiff", "dsd", "pcm"];
const videoExtensions = [
"mp4",

View File

@@ -117,6 +117,7 @@ import {
VersionIcon,
WarpIcon,
WatchIcon,
ObjectManagerIcon,
} from "../../../icons";
import WarnIcon from "../../../icons/WarnIcon";
@@ -717,6 +718,11 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
WarnIcon
</Grid>
</Grid>
<Grid item>
<ObjectManagerIcon />
<br />
ObjectManagerIcon
</Grid>
</div>
);
};

View File

@@ -0,0 +1,164 @@
// 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 <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import { Tooltip } from "@mui/material";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IFileItem } from "../../ObjectBrowser/reducers";
import ProgressBarWrapper from "../ProgressBarWrapper/ProgressBarWrapper";
import { DownloadStatIcon, UploadStatIcon } from "../../../../icons";
interface IObjectHandled {
classes: any;
objectToDisplay: IFileItem;
deleteFromList: (instanceID: string) => void;
}
const styles = (theme: Theme) =>
createStyles({
container: {
borderBottom: "#E2E2E2 1px solid",
padding: "15px 5px",
margin: "0 15px",
position: "relative",
"& .showOnHover": {
opacity: 0,
transitionDuration: "0.2s",
},
"&.inProgress": {
"& .hideOnProgress": {
visibility: "hidden",
},
},
"&:hover": {
"& .showOnHover": {
opacity: 1,
},
},
},
headItem: {
color: "#868686",
fontSize: 12,
width: "100%",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
},
progressContainer: {
marginTop: 5,
},
objectDetails: {
display: "flex",
alignItems: "center",
},
iconContainer: {
paddingTop: 5,
marginRight: 5,
"& svg": {
width: 20,
height: 20,
},
},
closeIcon: {
"&::before": {
width: 1,
height: 12,
content: "' '",
position: "absolute",
transform: "rotate(45deg)",
borderLeft: "#9c9c9c 2px solid",
},
"&::after": {
width: 1,
height: 12,
content: "' '",
position: "absolute",
transform: "rotate(-45deg)",
borderLeft: "#9c9c9c 2px solid",
},
},
closeButton: {
backgroundColor: "transparent",
border: 0,
right: 0,
position: "absolute",
},
fileName: {
width: 230,
},
});
const ObjectHandled = ({
classes,
objectToDisplay,
deleteFromList,
}: IObjectHandled) => {
const prefix = `/${objectToDisplay.prefix}`;
return (
<Fragment>
<div
className={`${classes.container} ${
!objectToDisplay.done ? "inProgress" : ""
}`}
>
<div className={classes.clearListIcon}>
<button
onClick={() => {
deleteFromList(objectToDisplay.instanceID);
}}
className={`${classes.closeButton} hideOnProgress showOnHover`}
disabled={!objectToDisplay.done}
>
<span className={classes.closeIcon}></span>
</button>
</div>
<div className={classes.objectDetails}>
<div className={classes.iconContainer}>
{objectToDisplay.type === "download" ? (
<DownloadStatIcon />
) : (
<UploadStatIcon />
)}
</div>
<div className={classes.fileName}>
<div className={classes.headItem}>
<strong>Bucket: </strong>
{objectToDisplay.bucketName}
</div>
<Tooltip title={prefix} placement="top-start">
<div className={classes.headItem}>{prefix}</div>
</Tooltip>
</div>
</div>
<div className={classes.progressContainer}>
{objectToDisplay.waitingForFile ? (
<ProgressBarWrapper indeterminate value={0} ready={false} />
) : (
<ProgressBarWrapper
value={objectToDisplay.percentage}
ready={objectToDisplay.done}
withLabel
/>
)}
</div>
</div>
</Fragment>
);
};
export default withStyles(styles)(ObjectHandled);

View File

@@ -0,0 +1,136 @@
// 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 <http://www.gnu.org/licenses/>.
import React, { Fragment } from "react";
import { Theme } from "@mui/material/styles";
import { connect } from "react-redux";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { Tooltip, IconButton } from "@mui/material";
import { AppState } from "../../../../store";
import { IFileItem } from "../../ObjectBrowser/reducers";
import { deleteFromList, cleanList } from "../../ObjectBrowser/actions";
import { TrashIcon } from "../../../../icons";
import ObjectHandled from "./ObjectHandled";
interface IObjectManager {
objects: IFileItem[];
classes: any;
managerOpen: boolean;
deleteFromList: typeof deleteFromList;
cleanList: typeof cleanList;
}
const styles = (theme: Theme) =>
createStyles({
downloadContainer: {
border: "#EAEDEE 1px solid",
boxShadow: "rgba(0, 0, 0, 0.08) 0 3px 10px",
backgroundColor: "#fff",
position: "absolute",
right: 0,
top: 80,
width: 300,
overflowY: "hidden",
overflowX: "hidden",
borderRadius: 3,
zIndex: 1000,
padding: 0,
height: 0,
transitionDuration: "0.3s",
visibility: "hidden",
"&.open": {
visibility: "visible",
minHeight: 400,
},
},
title: {
fontSize: 14,
fontWeight: "bold",
textAlign: "center",
marginBottom: 5,
paddingBottom: 12,
borderBottom: "#E2E2E2 1px solid",
margin: "15px 15px 5px 15px",
},
actionsContainer: {
overflowY: "auto",
overflowX: "hidden",
minHeight: 250,
maxHeight: 335,
width: "100%",
display: "flex",
flexDirection: "column",
},
cleanIcon: {
position: "absolute",
right: 14,
top: 12,
},
cleanButton: {
"& svg": {
width: 20,
},
},
});
const ObjectManager = ({
objects,
classes,
managerOpen,
deleteFromList,
cleanList,
}: IObjectManager) => {
return (
<Fragment>
<div
className={`${classes.downloadContainer} ${managerOpen ? "open" : ""}`}
>
<div className={classes.cleanIcon}>
<Tooltip title={"Clean Completed Objects"} placement="bottom-start">
<IconButton
aria-label={"Clear Completed List"}
size={"small"}
onClick={cleanList}
className={classes.cleanButton}
>
<TrashIcon />
</IconButton>
</Tooltip>
</div>
<div className={classes.title}>Object Manager</div>
<div className={classes.actionsContainer}>
{objects.map((object, key) => (
<ObjectHandled
objectToDisplay={object}
key={`object-handled-${object.instanceID}`}
deleteFromList={deleteFromList}
/>
))}
</div>
</div>
</Fragment>
);
};
const mapState = (state: AppState) => ({
objects: state.objectBrowser.objectManager.objectsToManage,
managerOpen: state.objectBrowser.objectManager.managerOpen,
});
const connector = connect(mapState, { deleteFromList, cleanList });
export default withStyles(styles)(connector(ObjectManager));

View File

@@ -1,14 +1,28 @@
import React from "react";
import Grid from "@mui/material/Grid";
import { Theme } from "@mui/material/styles";
import { connect } from "react-redux";
import { Box } from "@mui/material";
import Grid from "@mui/material/Grid";
import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import Typography from "@mui/material/Typography";
import IconButton from "@mui/material/IconButton";
import { AppState } from "../../../../store";
import { connect } from "react-redux";
import OperatorLogo from "../../../../icons/OperatorLogo";
import ConsoleLogo from "../../../../icons/ConsoleLogo";
import { Box } from "@mui/material";
import { IFileItem } from "../../ObjectBrowser/reducers";
import { toggleList } from "../../ObjectBrowser/actions";
import { ObjectManagerIcon } from "../../../../icons";
interface IPageHeader {
classes: any;
sidebarOpen?: boolean;
operatorMode?: boolean;
label: any;
actions?: any;
managerObjects?: IFileItem[];
toggleList: typeof toggleList;
}
const styles = (theme: Theme) =>
createStyles({
@@ -45,20 +59,14 @@ const styles = (theme: Theme) =>
},
});
interface IPageHeader {
classes: any;
sidebarOpen?: boolean;
operatorMode?: boolean;
label: any;
actions?: any;
}
const PageHeader = ({
classes,
label,
actions,
sidebarOpen,
operatorMode,
managerObjects,
toggleList,
}: IPageHeader) => {
return (
<Grid
@@ -82,11 +90,22 @@ const PageHeader = ({
{label}
</Typography>
</Grid>
{actions && (
<Grid item xs={12} sm={12} md={6} className={classes.rightMenu}>
{actions}
</Grid>
)}
<Grid item xs={12} sm={12} md={6} className={classes.rightMenu}>
{actions && actions}
{managerObjects && managerObjects.length > 0 && (
<IconButton
color="primary"
aria-label="Refresh List"
component="span"
onClick={() => {
toggleList();
}}
size="large"
>
<ObjectManagerIcon />
</IconButton>
)}
</Grid>
</Grid>
);
};
@@ -94,8 +113,13 @@ const PageHeader = ({
const mapState = (state: AppState) => ({
sidebarOpen: state.system.sidebarOpen,
operatorMode: state.system.operatorMode,
managerObjects: state.objectBrowser.objectManager.objectsToManage,
});
const connector = connect(mapState, null);
const mapDispatchToProps = {
toggleList,
};
const connector = connect(mapState, mapDispatchToProps);
export default connector(withStyles(styles)(PageHeader));

View File

@@ -18,12 +18,15 @@ import React from "react";
import { styled } from "@mui/material/styles";
import LinearProgress, {
linearProgressClasses,
LinearProgressProps,
} from "@mui/material/LinearProgress";
import Box from "@mui/material/Box";
interface IProgressBarWrapper {
value: number;
ready: boolean;
indeterminate?: boolean;
withLabel?: boolean;
}
const BorderLinearProgress = styled(LinearProgress)(() => ({
@@ -37,18 +40,35 @@ const BorderLinearProgress = styled(LinearProgress)(() => ({
},
}));
function LinearProgressWithLabel(props: LinearProgressProps) {
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<BorderLinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35, fontSize: 14 }} className={"value"}>
{`${Math.round(props.value || 0)}%`}
</Box>
</Box>
);
}
const ProgressBarWrapper = ({
value,
ready,
indeterminate,
withLabel,
}: IProgressBarWrapper) => {
return (
<BorderLinearProgress
variant={indeterminate && !ready ? "indeterminate" : "determinate"}
value={ready ? 100 : value}
color={ready ? "success" : "primary"}
/>
);
const propsComponent: LinearProgressProps = {
variant: indeterminate && !ready ? "indeterminate" : "determinate",
value: ready ? 100 : value,
color: ready ? "success" : "primary",
};
if (withLabel) {
return <LinearProgressWithLabel {...propsComponent} />;
}
return <BorderLinearProgress {...propsComponent} />;
};
export default ProgressBarWrapper;

View File

@@ -86,6 +86,10 @@ const IconsScreen = React.lazy(() => import("./Common/IconsScreen"));
const Speedtest = React.lazy(() => import("./Speedtest/Speedtest"));
const ObjectManager = React.lazy(
() => import("./Common/ObjectManager/ObjectManager")
);
const drawerWidth = 245;
const Buckets = React.lazy(() => import("./Buckets/Buckets"));
@@ -539,6 +543,9 @@ const Console = ({
}}
/>
</div>
<Suspense fallback={<div>Loading...</div>}>
<ObjectManager />
</Suspense>
<Router history={history}>
<Switch>
{allowedRoutes.map((route: any) => (

View File

@@ -14,11 +14,21 @@
// 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 { 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 =
"OBJECT_MANAGER/UPDATE_PROGRESS_OBJECT";
export const OBJECT_MANAGER_COMPLETE_OBJECT = "OBJECT_MANAGER/COMPLETE_OBJECT";
export const OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST =
"OBJECT_MANAGER/DELETE_FROM_OBJECT_LIST";
export const OBJECT_MANAGER_CLEAN_LIST = "OBJECT_MANAGER/CLEAN_LIST";
export const OBJECT_MANAGER_TOGGLE_LIST = "OBJECT_MANAGER/OPEN_LIST";
interface RewindSetEnabled {
type: typeof REWIND_SET_ENABLE;
bucket: string;
@@ -35,10 +45,45 @@ interface FileModeEnabled {
status: boolean;
}
interface OMNewObject {
type: typeof OBJECT_MANAGER_NEW_OBJECT;
newObject: IFileItem;
}
interface OMUpdateProgress {
type: typeof OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT;
instanceID: string;
progress: number;
}
interface OMCompleteObject {
type: typeof OBJECT_MANAGER_COMPLETE_OBJECT;
instanceID: string;
}
interface OMDeleteFromList {
type: typeof OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST;
instanceID: string;
}
interface OMCleanList {
type: typeof OBJECT_MANAGER_CLEAN_LIST;
}
interface OMToggleList {
type: typeof OBJECT_MANAGER_TOGGLE_LIST;
}
export type ObjectBrowserActionTypes =
| RewindSetEnabled
| RewindReset
| FileModeEnabled;
| FileModeEnabled
| OMNewObject
| OMUpdateProgress
| OMCompleteObject
| OMDeleteFromList
| OMCleanList
| OMToggleList;
export const setRewindEnable = (
state: boolean,
@@ -65,3 +110,44 @@ export const setFileModeEnabled = (status: boolean) => {
status,
};
};
export const setNewObject = (newObject: IFileItem) => {
return {
type: OBJECT_MANAGER_NEW_OBJECT,
newObject,
};
};
export const updateProgress = (instanceID: string, progress: number) => {
return {
type: OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT,
instanceID,
progress,
};
};
export const completeObject = (instanceID: string) => {
return {
type: OBJECT_MANAGER_COMPLETE_OBJECT,
instanceID,
};
};
export const deleteFromList = (instanceID: string) => {
return {
type: OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST,
instanceID,
};
};
export const cleanList = () => {
return {
type: OBJECT_MANAGER_CLEAN_LIST,
};
};
export const toggleList = () => {
return {
type: OBJECT_MANAGER_TOGGLE_LIST,
};
};

View File

@@ -18,6 +18,12 @@ import {
REWIND_RESET_REWIND,
REWIND_FILE_MODE_ENABLED,
ObjectBrowserActionTypes,
OBJECT_MANAGER_NEW_OBJECT,
OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT,
OBJECT_MANAGER_COMPLETE_OBJECT,
OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST,
OBJECT_MANAGER_CLEAN_LIST,
OBJECT_MANAGER_TOGGLE_LIST,
} from "./actions";
export interface Route {
@@ -35,12 +41,28 @@ export interface RewindItem {
export interface ObjectBrowserState {
fileMode: boolean;
rewind: RewindItem;
objectManager: ObjectManager;
}
export interface ObjectBrowserReducer {
objectBrowser: ObjectBrowserState;
}
export interface ObjectManager {
objectsToManage: IFileItem[];
managerOpen: boolean;
}
export interface IFileItem {
type: "download" | "upload";
instanceID: string;
bucketName: string;
prefix: string;
percentage: number;
done: boolean;
waitingForFile: boolean;
}
const defaultRewind = {
rewindEnabled: false,
bucketToRewind: "",
@@ -52,6 +74,10 @@ const initialState: ObjectBrowserState = {
rewind: {
...defaultRewind,
},
objectManager: {
objectsToManage: [],
managerOpen: false,
},
};
export function objectBrowserReducer(
@@ -76,6 +102,97 @@ export function objectBrowserReducer(
return { ...state, rewind: resetItem };
case REWIND_FILE_MODE_ENABLED:
return { ...state, fileMode: action.status };
case OBJECT_MANAGER_NEW_OBJECT:
const cloneObjects = [...state.objectManager.objectsToManage];
cloneObjects.push(action.newObject);
return {
...state,
objectManager: {
objectsToManage: cloneObjects,
managerOpen: true,
},
};
case OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT:
const copyManager = [...state.objectManager.objectsToManage];
const itemUpdate = state.objectManager.objectsToManage.findIndex(
(item) => item.instanceID === action.instanceID
);
if (itemUpdate === -1) {
return { ...state };
}
copyManager[itemUpdate].percentage = action.progress;
copyManager[itemUpdate].waitingForFile = false;
return {
...state,
objectManager: {
objectsToManage: copyManager,
managerOpen: state.objectManager.managerOpen,
},
};
case OBJECT_MANAGER_COMPLETE_OBJECT:
const copyObject = [...state.objectManager.objectsToManage];
const objectToComplete = state.objectManager.objectsToManage.findIndex(
(item) => item.instanceID === action.instanceID
);
if (objectToComplete === -1) {
return { ...state };
}
copyObject[objectToComplete].percentage = 100;
copyObject[objectToComplete].waitingForFile = false;
copyObject[objectToComplete].done = true;
return {
...state,
objectManager: {
objectsToManage: copyObject,
managerOpen: state.objectManager.managerOpen,
},
};
case OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST:
const notObject = state.objectManager.objectsToManage.filter(
(element) => element.instanceID !== action.instanceID
);
return {
...state,
objectManager: {
objectsToManage: notObject,
managerOpen:
notObject.length === 0 ? false : state.objectManager.managerOpen,
},
};
case OBJECT_MANAGER_CLEAN_LIST:
const nonCompletedList = state.objectManager.objectsToManage.filter(
(item) => !item.done
);
return {
...state,
objectManager: {
objectsToManage: nonCompletedList,
managerOpen:
nonCompletedList.length === 0
? false
: state.objectManager.managerOpen,
},
};
case OBJECT_MANAGER_TOGGLE_LIST:
return {
...state,
objectManager: {
...state.objectManager,
managerOpen: !state.objectManager.managerOpen,
},
};
default:
return state;
}