Improvements for download / upload manager (#1933)

- Changed styles on progress bars & items
- Fixed some issues in error state & handling
- Added cancel capability to objects
- Added visual indicators when new objects are added to pool

Signed-off-by: Benjamin Perez <benjamin@bexsoft.net>
This commit is contained in:
Alex
2022-04-29 22:54:12 -05:00
committed by GitHub
parent a017c71d20
commit 6069991405
20 changed files with 585 additions and 226 deletions

View File

@@ -0,0 +1,32 @@
// 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 CancelledIcon = (props: SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
className={`min-icon`}
fill={"currentcolor"}
viewBox="0 0 256 256"
{...props}
>
<path d="M126.09,0C56.45,0,0,56.45,0,126.09s56.45,126.09,126.09,126.09,126.09-56.45,126.09-126.09S195.72,0,126.09,0Zm79.61,146.23H46.48c-11.08,0-20.14-9.07-20.14-20.14h0c0-11.08,9.07-20.14,20.14-20.14H205.7c11.08,0,20.14,9.07,20.14,20.14h0c0,11.08-9.07,20.14-20.14,20.14Z"/>
</svg>
);
export default CancelledIcon;

View File

@@ -25,37 +25,12 @@ const DisabledIcon = (props: SVGProps<SVGSVGElement>) => {
viewBox="0 0 16 16"
{...props}
>
<defs>
<clipPath id="disabled-clip-path">
<rect
id="Rectángulo_1068"
data-name="Rectángulo 1068"
width="16"
height="16"
fill="none"
/>
</clipPath>
</defs>
<rect
id="Rectángulo_1065"
data-name="Rectángulo 1065"
width="16"
height="16"
fill="none"
/>
<g id="Grupo_2455" data-name="Grupo 2455">
<g
id="Grupo_2454"
data-name="Grupo 2454"
clipPath="url(#disabled-clip-path)"
>
<path
id="Trazado_7232"
data-name="Trazado 7232"
d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m3.235,5.4L8.965,8.174,10.949,10.6a.857.857,0,0,1-1.327,1.086h0L7.857,9.528,6.092,11.686A.857.857,0,0,1,4.765,10.6L6.749,8.174,4.479,5.4A.857.857,0,0,1,5.806,4.314L7.857,6.821l2.05-2.506A.857.857,0,1,1,11.235,5.4"
fill="#969fa8"
/>
</g>
<g>
<path
id="Trazado_7232"
data-name="Trazado 7232"
d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m3.235,5.4L8.965,8.174,10.949,10.6a.857.857,0,0,1-1.327,1.086h0L7.857,9.528,6.092,11.686A.857.857,0,0,1,4.765,10.6L6.749,8.174,4.479,5.4A.857.857,0,0,1,5.806,4.314L7.857,6.821l2.05-2.506A.857.857,0,1,1,11.235,5.4"
/>
</g>
</svg>
);

View File

@@ -25,21 +25,7 @@ const DownloadStatIcon = (props: SVGProps<SVGSVGElement>) => (
viewBox="0 0 256 256"
{...props}
>
<defs>
<clipPath id="prefix__a">
<path d="M0 0h256v256H0z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)">
<path fill="none" d="M0 0h256v256H0z" />
<g data-name="DownloadStatIcon">
<path
data-name="Uni\xF3n 24"
d="M0 127.996a128 128 0 0 1 128.008-128 128 128 0 0 1 128 128 128 128 0 0 1-128 128.009A128 128 0 0 1 0 127.996Zm20.484 0A107.643 107.643 0 0 0 128 235.52a107.633 107.633 0 0 0 107.512-107.523A107.631 107.631 0 0 0 128 20.487 107.641 107.641 0 0 0 20.48 127.996Zm98.063 71.518-32.336-32.338a13.192 13.192 0 0 1-3.172-14.743 14.934 14.934 0 0 1 13.3-9.235 13.542 13.542 0 0 1 9.637 4.224l8.563 8.554v-89.16c0-9.069 7.016-13.818 13.953-13.818 6.961 0 13.977 4.749 13.977 13.818v89.16l8.555-8.544a13.481 13.481 0 0 1 9.625-4.233 14.887 14.887 0 0 1 13.3 9.235 13.182 13.182 0 0 1-3.164 14.743l-32.348 32.348a14 14 0 0 1-9.906 4.155 14.085 14.085 0 0 1-9.988-4.166Z"
/>
<path data-name="Rect\xE1ngulo 893" fill="none" d="M0 0h256v256H0z" />
</g>
</g>
<path d="M125.65,0h0C56.26,0,0,56.26,0,125.65H0c0,69.4,56.26,125.65,125.65,125.65h0c69.4,0,125.65-56.26,125.65-125.65S195.05,0,125.65,0m41.51,163.77l-31.76,31.76c-5.32,5.39-14,5.45-19.39,.13-.04-.04-.09-.09-.13-.13h0l-31.74-31.76c-3.97-3.69-5.22-9.46-3.14-14.47,2.19-5.32,7.3-8.87,13.05-9.06,3.57,.06,6.97,1.55,9.42,4.15l8.4,8.4V65.26c0-7.57,6.15-13.71,13.72-13.7,7.57,0,13.7,6.14,13.7,13.7v87.52l8.4-8.39c2.45-2.6,5.85-4.1,9.42-4.16,5.76,.18,10.87,3.73,13.05,9.06,2.09,5,.83,10.78-3.14,14.47" />
</svg>
);

View File

@@ -25,37 +25,8 @@ const EnabledIcon = (props: SVGProps<SVGSVGElement>) => {
viewBox="0 0 16 16"
{...props}
>
<defs>
<clipPath id="enabled-clip-path">
<rect
id="Rectángulo_1067"
data-name="Rectángulo 1067"
width="16"
height="16"
fill="none"
/>
</clipPath>
</defs>
<rect
id="Rectángulo_1066"
data-name="Rectángulo 1066"
width="16"
height="16"
fill="none"
/>
<g id="Grupo_2453" data-name="Grupo 2453">
<g
id="Grupo_2452"
data-name="Grupo 2452"
clipPath="url(#enabled-clip-path)"
>
<path
id="Trazado_7231"
data-name="Trazado 7231"
d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m4.575,5.769-.005.005L7.837,11.69a.89.89,0,0,1-.635.284H7.185a.889.889,0,0,1-.628-.26h0L3.421,8.577a.889.889,0,1,1,1.2-1.31q.028.025.053.053L7.16,9.8l4.117-5.246.024-.026h0a.889.889,0,0,1,1.275,1.24"
fill="#969fa8"
/>
</g>
<g>
<path d="M8,0a8,8,0,1,0,8,8A8,8,0,0,0,8,0m4.575,5.769-.005.005L7.837,11.69a.89.89,0,0,1-.635.284H7.185a.889.889,0,0,1-.628-.26h0L3.421,8.577a.889.889,0,1,1,1.2-1.31q.028.025.053.053L7.16,9.8l4.117-5.246.024-.026h0a.889.889,0,0,1,1.275,1.24" />
</g>
</svg>
);

View File

@@ -26,19 +26,24 @@ const ObjectManagerIcon = (props: SVGProps<SVGSVGElement>) => {
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>
<g x="2.7" y="36.8">
<path
d="M77.2,168.6c4,4.1,10.6,4.3,14.7,0.3c0,0,0,0,0.1-0.1l0.2-0.2l29.7-29.9
c3.9-4.3,3.6-10.9-0.7-14.9c-4-3.7-10.1-3.7-14.1-0.1l-12,12V47.3h0.1c0-5.8-4.7-10.5-10.5-10.5s-10.5,4.7-10.5,10.5v88.3
l-11.9-12c-4.3-4-10.9-3.7-14.9,0.5c-3.8,4.1-3.8,10.4,0.1,14.4L77.2,168.6z"
/>
<path
d="M148.3,84.9l11.9-12v88.3h-0.1c0,5.8,4.7,10.5,10.5,10.5s10.5-4.7,10.5-10.5V72.9l11.9,12
c4.3,4,10.9,3.7,14.9-0.5c3.8-4.1,3.8-10.4-0.1-14.4l-29.7-30c-4-4.1-10.6-4.2-14.7-0.2l-0.2,0.2l-29.7,29.9
c-4,4.2-3.8,10.9,0.4,14.9C138.1,88.6,144.3,88.7,148.3,84.9"
/>
<path
d="M242.1,154.9c-6.2,0-11.2,5-11.2,11.1l0,0v27.4c0,1.9-1.6,3.5-3.5,3.5H28.5
c-1.9,0-3.5-1.6-3.5-3.5v-27.3c0.2-6.2-4.7-11.3-10.8-11.5s-11.3,4.7-11.5,10.8c0,0.2,0,0.4,0,0.7v27.4
c0,14.2,11.6,25.7,25.8,25.8h198.8c14.2,0,25.8-11.6,25.8-25.8v-27.4C253.1,159.9,248.1,154.9,242.1,154.9L242.1,154.9"
/>
</g>
</g>
</svg>
);

View File

@@ -0,0 +1,37 @@
// 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>
<path d="M216,169H83.14a34,34,0,0,1-24.09-10.15L9.56,108A33.56,33.56,0,0,1,9.56,61L59,10.1A33.91,33.91,0,0,1,83.13,0H216a33.68,33.68,0,0,1,33.65,33.65V135.37A33.68,33.68,0,0,1,216,169M83.14,9A24.93,24.93,0,0,0,65.5,16.42L16,67.36a24.54,24.54,0,0,0,0,34.29l49.5,50.92A24.91,24.91,0,0,0,83.12,160H216a24.64,24.64,0,0,0,24.66-24.62V33.65A24.64,24.64,0,0,0,216,9H83.14Z" />
<path d="M162.57,96h0a7.23,7.23,0,1,1-10,10.46l-.2-.24L138.78,92.68l-13.54,13.57a7.21,7.21,0,1,1-10.79-9.58c.12-.14.25-.27.38-.4l.24-.24,13.56-13.55L115.09,68.94a7.22,7.22,0,0,1,10.17-10.21l13.59,13.58,13.54-13.58a7.22,7.22,0,0,1,10.18,10.21L149,82.48Z" />
</g>
</svg>
);
};
export default ObjectManagerIcon;

View File

@@ -25,21 +25,9 @@ const UploadStatIcon = (props: SVGProps<SVGSVGElement>) => (
viewBox="0 0 256 256"
{...props}
>
<defs>
<clipPath id="prefix__a">
<path d="M0 0h256v256H0z" />
</clipPath>
</defs>
<g clipPath="url(#prefix__a)">
<path fill="none" d="M0 0h256v256H0z" />
<g data-name="UploadStatIcon">
<path
data-name="Uni\xF3n 27"
d="M256 127.997a128.006 128.006 0 0 1-128 128.006A128.008 128.008 0 0 1 0 127.997a128.007 128.007 0 0 1 128.008-128 128 128 0 0 1 127.992 128Zm-20.477 0a107.649 107.649 0 0 0-107.52-107.52 107.641 107.641 0 0 0-107.52 107.52A107.635 107.635 0 0 0 128 235.513a107.642 107.642 0 0 0 107.523-107.516Zm-97.082-77.788 32.332 32.331a13.2 13.2 0 0 1 3.184 14.751 14.916 14.916 0 0 1-13.316 9.225 13.45 13.45 0 0 1-9.617-4.216l-8.559-8.565v89.178c0 9.072-7.035 13.8-13.977 13.8s-13.965-4.731-13.965-13.8V93.738l-8.547 8.565a13.5 13.5 0 0 1-9.637 4.216 14.917 14.917 0 0 1-13.3-9.225 13.216 13.216 0 0 1 3.18-14.751l32.344-32.331a13.916 13.916 0 0 1 9.9-4.168 14.021 14.021 0 0 1 9.978 4.169Z"
/>
<path data-name="Rect\xE1ngulo 894" fill="none" d="M0 0h256v256H0z" />
</g>
</g>
<path
d="M125.65,251.3h0c69.4,0,125.65-56.26,125.65-125.65h0C251.3,56.26,195.05,0,125.65,0h0C56.26,0,0,56.26,0,125.65s56.26,125.65,125.65,125.65M84.14,87.53l31.76-31.76c5.32-5.39,14-5.45,19.39-.13,.04,.04,.09,.09,.13,.13h0l31.74,31.76c3.97,3.69,5.22,9.46,3.14,14.47-2.19,5.32-7.3,8.87-13.05,9.06-3.57-.06-6.97-1.55-9.42-4.15l-8.4-8.4v87.53c0,7.57-6.15,13.71-13.72,13.7-7.57,0-13.7-6.14-13.7-13.7V98.53l-8.4,8.39c-2.45,2.6-5.85,4.1-9.42,4.16-5.76-.18-10.87-3.73-13.05-9.06-2.09-5-.83-10.78,3.14-14.47"
/>
</svg>
);

View File

@@ -186,3 +186,5 @@ export { default as EditTenantIcon } from "./EditTenantIcon";
export { default as SuccessIcon } from "./SuccessIcon";
export { default as NetworkGetIcon } from "./NetworkGetIcon";
export { default as NetworkPutIcon } from "./NetworkPutIcon";
export { default as RemoveAllIcon } from "./RemoveAllIcon";
export { default as CancelledIcon } from "./CancelledIcon";

View File

@@ -52,7 +52,9 @@ import {
import { Badge, Typography } from "@mui/material";
import BrowserBreadcrumbs from "../../../../ObjectBrowser/BrowserBreadcrumbs";
import {
cancelObjectInList,
completeObject,
failObject,
openList,
resetRewind,
setLoadingObjectInfo,
@@ -241,6 +243,8 @@ interface IListObjectsProps {
setSelectedObjectView: typeof setSelectedObjectView;
setLoadingObjectInfo: typeof setLoadingObjectInfo;
setLoadingObjectsList: typeof setLoadingObjectsList;
failObject: typeof failObject;
cancelObjectInList: typeof cancelObjectInList;
}
function useInterval(callback: any, delay: number) {
@@ -300,6 +304,8 @@ const ListObjects = ({
setSelectedObjectView,
setLoadingObjectInfo,
setLoadingObjectsList,
failObject,
cancelObjectInList,
}: IListObjectsProps) => {
const [records, setRecords] = useState<BucketObjectItem[]>([]);
const [deleteMultipleOpen, setDeleteMultipleOpen] = useState<boolean>(false);
@@ -738,17 +744,7 @@ const ListObjects = ({
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
const downloadCall = download(
bucketName,
encodeFileName(object.name),
object.version_id,
@@ -758,8 +754,29 @@ const ListObjects = ({
},
() => {
completeObject(identityDownload);
},
() => {
failObject(identityDownload);
},
() => {
cancelObjectInList(identityDownload);
}
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
call: downloadCall,
});
downloadCall.send();
};
const openPath = (idElement: string) => {
@@ -824,16 +841,6 @@ const ListObjects = ({
`${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();
xhr.open("POST", uploadUrl, true);
@@ -864,12 +871,14 @@ const ListObjects = ({
errorMessage = "something went wrong";
}
}
failObject(identity);
reject({ status: xhr.status, message: errorMessage });
}
};
xhr.upload.addEventListener("error", (event) => {
reject(errorMessage);
failObject(identity);
return;
});
@@ -881,6 +890,7 @@ const ListObjects = ({
xhr.onerror = () => {
reject(errorMessage);
failObject(identity);
return;
};
xhr.onloadend = () => {
@@ -888,10 +898,27 @@ const ListObjects = ({
setLoadingObjectsList(true);
}
};
xhr.onabort = () => {
cancelObjectInList(identity);
};
const formData = new FormData();
if (file.size !== undefined) {
formData.append(file.size.toString(), blobFile, fileName);
setNewObject({
bucketName,
done: false,
instanceID: identity,
percentage: 0,
prefix: `${decodeFileName(encodedPath)}${fileName}`,
type: "upload",
waitingForFile: false,
failed: false,
cancelled: false,
call: xhr,
});
xhr.send(formData);
}
});
@@ -934,6 +961,8 @@ const ListObjects = ({
setErrorSnackMessage,
updateProgress,
setLoadingObjectsList,
cancelObjectInList,
failObject,
]
);
@@ -1485,6 +1514,7 @@ const mapDispatchToProps = {
updateProgress,
completeObject,
openList,
failObject,
setSearchObjects,
setVersionsModeEnabled,
setShowDeletedObjects,
@@ -1493,6 +1523,7 @@ const mapDispatchToProps = {
setSelectedObjectView,
setLoadingObjectInfo,
setLoadingObjectsList,
cancelObjectInList,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -44,7 +44,9 @@ import {
} from "../../../../../../common/utils";
import { IAM_SCOPES } from "../../../../../../common/SecureComponent/permissions";
import {
cancelObjectInList,
completeObject,
failObject,
setLoadingObjectInfo,
setLoadingVersions,
setNewObject,
@@ -137,6 +139,8 @@ interface IObjectDetailPanelProps {
setLoadingObjectInfo: typeof setLoadingObjectInfo;
setLoadingVersions: typeof setLoadingVersions;
setSelectedVersion: typeof setSelectedVersion;
failObject: typeof failObject;
cancelObjectInList: typeof cancelObjectInList;
}
const emptyFile: IFileInfo = {
@@ -170,6 +174,8 @@ const ObjectDetailPanel = ({
setLoadingObjectInfo,
setLoadingVersions,
setSelectedVersion,
failObject,
cancelObjectInList,
}: IObjectDetailPanelProps) => {
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [retentionModalOpen, setRetentionModalOpen] = useState<boolean>(false);
@@ -294,17 +300,7 @@ const ObjectDetailPanel = ({
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
const downloadCall = download(
bucketName,
internalPaths,
object.version_id,
@@ -314,8 +310,29 @@ const ObjectDetailPanel = ({
},
() => {
completeObject(identityDownload);
},
() => {
failObject(identityDownload);
},
() => {
cancelObjectInList(identityDownload);
}
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
call: downloadCall,
});
downloadCall.send();
};
const closeDeleteModal = (closeAndReload: boolean) => {
@@ -731,6 +748,8 @@ const mapDispatchToProps = {
setLoadingObjectInfo,
setLoadingVersions,
setSelectedVersion,
failObject,
cancelObjectInList,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -50,7 +50,9 @@ import {
import ScreenTitle from "../../../../Common/ScreenTitle/ScreenTitle";
import RestoreFileVersion from "./RestoreFileVersion";
import {
cancelObjectInList,
completeObject,
failObject,
setLoadingObjectInfo,
setLoadingVersions,
setNewObject,
@@ -128,6 +130,8 @@ interface IVersionsNavigatorProps {
setSelectedVersion: typeof setSelectedVersion;
setLoadingVersions: typeof setLoadingVersions;
setLoadingObjectInfo: typeof setLoadingObjectInfo;
failObject: typeof failObject;
cancelObjectInList: typeof cancelObjectInList;
}
const emptyFile: IFileInfo = {
@@ -157,6 +161,8 @@ const VersionsNavigator = ({
setSelectedVersion,
setLoadingVersions,
setLoadingObjectInfo,
failObject,
cancelObjectInList,
}: IVersionsNavigatorProps) => {
const [shareFileModalOpen, setShareFileModalOpen] = useState<boolean>(false);
const [actualInfo, setActualInfo] = useState<IFileInfo | null>(null);
@@ -227,17 +233,7 @@ const VersionsNavigator = ({
`${bucketName}-${object.name}-${new Date().getTime()}-${Math.random()}`
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
});
download(
const downloadCall = download(
bucketName,
internalPaths,
object.version_id,
@@ -247,8 +243,29 @@ const VersionsNavigator = ({
},
() => {
completeObject(identityDownload);
},
() => {
failObject(identityDownload);
},
() => {
cancelObjectInList(identityDownload);
}
);
setNewObject({
bucketName,
done: false,
instanceID: identityDownload,
percentage: 0,
prefix: object.name,
type: "download",
waitingForFile: true,
failed: false,
cancelled: false,
call: downloadCall,
});
downloadCall.send();
};
const onShareItem = (item: IFileInfo) => {
@@ -517,6 +534,8 @@ const mapDispatchToProps = {
setSelectedVersion,
setLoadingVersions,
setLoadingObjectInfo,
failObject,
cancelObjectInList,
};
const connector = connect(mapStateToProps, mapDispatchToProps);

View File

@@ -22,7 +22,9 @@ export const download = (
versionID: any,
fileSize: number,
progressCallback: (progress: number) => void,
completeCallback: () => void
completeCallback: () => void,
errorCallback: () => void,
abortCallback: () => void,
) => {
const anchor = document.createElement("a");
document.body.appendChild(anchor);
@@ -68,7 +70,19 @@ export const download = (
document.body.removeChild(link);
}
};
req.send();
req.onerror = () => {
if(errorCallback) {
errorCallback();
}
};
req.onabort = () => {
if(abortCallback) {
abortCallback();
}
};
//req.send();
return req;
};
// Review file extension by name & returns the type of preview browser that can be used

View File

@@ -235,6 +235,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
CallHomeFeatureIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.CancelledIcon />
<br />
CancelledIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.ChangeAccessPolicyIcon />
<br />
@@ -847,6 +853,12 @@ const IconsScreen = ({ classes }: IIconsScreenSimple) => {
RefreshIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.RemoveAllIcon />
<br />
RemoveAllIcon
</Grid>
<Grid item xs={3} sm={2} md={1}>
<cicons.RemoveIcon />
<br />

View File

@@ -21,7 +21,13 @@ import createStyles from "@mui/styles/createStyles";
import withStyles from "@mui/styles/withStyles";
import { IFileItem } from "../../ObjectBrowser/types";
import ProgressBarWrapper from "../ProgressBarWrapper/ProgressBarWrapper";
import { DownloadStatIcon, UploadStatIcon } from "../../../../icons";
import {
DisabledIcon,
DownloadStatIcon,
EnabledIcon,
UploadStatIcon,
CancelledIcon,
} from "../../../../icons";
import clsx from "clsx";
interface IObjectHandled {
@@ -35,15 +41,15 @@ const styles = (theme: Theme) =>
container: {
borderBottom: "#E2E2E2 1px solid",
padding: "15px 5px",
margin: "0 15px",
margin: "0 30px",
position: "relative",
"& .showOnHover": {
opacity: 0,
opacity: 1,
transitionDuration: "0.2s",
},
"&.inProgress": {
"& .hideOnProgress": {
visibility: "hidden",
//visibility: "hidden",
},
},
"&:hover": {
@@ -53,13 +59,19 @@ const styles = (theme: Theme) =>
},
},
headItem: {
color: "#868686",
fontSize: 12,
color: "#000",
fontSize: 14,
fontWeight: "bold",
width: "100%",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
overflow: "hidden",
},
downloadHeader: {
display: "flex",
alignItems: "center",
width: "100%",
},
progressContainer: {
marginTop: 5,
},
@@ -71,42 +83,65 @@ const styles = (theme: Theme) =>
paddingTop: 5,
marginRight: 5,
"& svg": {
width: 20,
height: 20,
width: 16,
height: 16,
},
},
download: {
color: "rgb(113,200,150)",
completedSuccess: {
color: "#4CCB92",
},
upload: {
color: "rgb(66,127,172)",
inProgress: {
color: "#2781B0",
},
completedError: {
color: "#C83B51",
},
cancelledAction: {
color: "#FFBD62",
},
closeIcon: {
backgroundColor: "#E9EDEE",
display: "block",
width: 18,
height: 18,
borderRadius: "100%",
"&:hover": {
backgroundColor: "#cecbcb",
},
"&::before": {
width: 1,
height: 12,
height: 9,
top: "50%",
content: "' '",
position: "absolute",
transform: "rotate(45deg)",
borderLeft: "#9c9c9c 2px solid",
transform: "translate(-50%, -50%) rotate(45deg)",
borderLeft: "#000 2px solid",
},
"&::after": {
width: 1,
height: 12,
height: 9,
top: "50%",
content: "' '",
position: "absolute",
transform: "rotate(-45deg)",
borderLeft: "#9c9c9c 2px solid",
transform: "translate(-50%, -50%) rotate(-45deg)",
borderLeft: "#000 2px solid",
},
},
closeButton: {
backgroundColor: "transparent",
border: 0,
right: 0,
top: 5,
marginTop: 15,
position: "absolute",
},
fileName: {
width: 230,
width: 295,
},
bucketName: {
fontSize: 12,
color: "#696969",
fontWeight: "normal",
},
});
@@ -126,35 +161,74 @@ const ObjectHandled = ({
<div className={classes.clearListIcon}>
<button
onClick={() => {
deleteFromList(objectToDisplay.instanceID);
if (!objectToDisplay.done) {
console.log("//abort");
objectToDisplay.call?.abort();
} else {
deleteFromList(objectToDisplay.instanceID);
}
}}
className={`${classes.closeButton} hideOnProgress showOnHover`}
disabled={objectToDisplay.percentage !== 100}
className={`${classes.closeButton} hideOnProgress`}
>
<span className={classes.closeIcon} />
</button>
</div>
<div className={classes.objectDetails}>
<div
className={clsx(classes.iconContainer, {
[classes.download]: objectToDisplay.type === "download",
[classes.upload]: objectToDisplay.type !== "download",
})}
>
{objectToDisplay.type === "download" ? (
<DownloadStatIcon />
) : (
<UploadStatIcon />
)}
</div>
<div className={classes.fileName}>
<div className={classes.headItem}>
<Tooltip title={prefix} placement="top-start">
<div className={classes.downloadHeader}>
<span
className={clsx(classes.iconContainer, {
[classes.inProgress]:
!objectToDisplay.done &&
!objectToDisplay.failed &&
!objectToDisplay.cancelled,
[classes.completedSuccess]:
objectToDisplay.done &&
!objectToDisplay.failed &&
!objectToDisplay.cancelled,
[classes.completedError]: objectToDisplay.failed,
[classes.cancelledAction]: objectToDisplay.cancelled,
})}
>
{objectToDisplay.cancelled ? (
<CancelledIcon />
) : (
<Fragment>
{objectToDisplay.failed ? (
<DisabledIcon />
) : (
<Fragment>
{objectToDisplay.done ? (
<EnabledIcon />
) : (
<Fragment>
{objectToDisplay.type === "download" ? (
<DownloadStatIcon />
) : (
<UploadStatIcon />
)}
</Fragment>
)}
</Fragment>
)}
</Fragment>
)}
</span>
<span
className={clsx(classes.headItem, {
[classes.completedError]: objectToDisplay.failed,
})}
>
{prefix}
</span>
</div>
</Tooltip>
<span className={classes.bucketName}>
<strong>Bucket: </strong>
{objectToDisplay.bucketName}
</div>
<Tooltip title={prefix} placement="top-start">
<div className={classes.headItem}>{prefix}</div>
</Tooltip>
</span>
</div>
</div>
<div className={classes.progressContainer}>
@@ -164,6 +238,8 @@ const ObjectHandled = ({
<ProgressBarWrapper
value={objectToDisplay.percentage}
ready={objectToDisplay.done}
error={objectToDisplay.failed}
cancelled={objectToDisplay.cancelled}
withLabel
/>
)}

View File

@@ -23,7 +23,7 @@ import { Tooltip, IconButton } from "@mui/material";
import { AppState } from "../../../../store";
import { IFileItem } from "../../ObjectBrowser/types";
import { deleteFromList, cleanList } from "../../ObjectBrowser/actions";
import { TrashIcon } from "../../../../icons";
import { RemoveAllIcon } from "../../../../icons";
import ObjectHandled from "./ObjectHandled";
interface IObjectManager {
@@ -38,12 +38,12 @@ const styles = (theme: Theme) =>
createStyles({
downloadContainer: {
border: "#EAEDEE 1px solid",
boxShadow: "rgba(0, 0, 0, 0.08) 0 3px 10px",
boxShadow: "rgba(0, 0, 0, 0.08) 0 2px 10px",
backgroundColor: "#fff",
position: "absolute",
right: 0,
top: 80,
width: 300,
right: 20,
top: 60,
width: 400,
overflowY: "hidden",
overflowX: "hidden",
borderRadius: 3,
@@ -58,13 +58,13 @@ const styles = (theme: Theme) =>
},
},
title: {
fontSize: 14,
fontSize: 16,
fontWeight: "bold",
textAlign: "center",
marginBottom: 5,
paddingBottom: 12,
textAlign: "left",
paddingBottom: 20,
borderBottom: "#E2E2E2 1px solid",
margin: "15px 15px 5px 15px",
margin: "25px 30px 5px 30px",
color: "#000",
},
actionsContainer: {
overflowY: "auto",
@@ -77,12 +77,12 @@ const styles = (theme: Theme) =>
},
cleanIcon: {
position: "absolute",
right: 14,
top: 12,
right: 28,
top: 25,
},
cleanButton: {
"& svg": {
width: 20,
width: 25,
},
},
});
@@ -110,7 +110,7 @@ const ObjectManager = ({
onClick={cleanList}
className={classes.cleanButton}
>
<TrashIcon />
<RemoveAllIcon />
</IconButton>
</Tooltip>
</div>

View File

@@ -14,7 +14,7 @@
// 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 React, { Fragment, useEffect, useState } from "react";
import { Theme } from "@mui/material/styles";
import { connect } from "react-redux";
import Grid from "@mui/material/Grid";
@@ -26,7 +26,7 @@ import OperatorLogo from "../../../../icons/OperatorLogo";
import ConsoleLogo from "../../../../icons/ConsoleLogo";
import { IFileItem } from "../../ObjectBrowser/types";
import { toggleList } from "../../ObjectBrowser/actions";
import { ObjectManagerIcon } from "../../../../icons";
import { CircleIcon, ObjectManagerIcon } from "../../../../icons";
import { Box } from "@mui/material";
interface IPageHeader {
@@ -35,10 +35,12 @@ interface IPageHeader {
operatorMode?: boolean;
label: any;
actions?: any;
managerObjects?: IFileItem[];
managerObjects: IFileItem[];
toggleList: typeof toggleList;
middleComponent?: React.ReactNode;
features: string[];
managerOpen: boolean;
newItems: boolean;
}
const styles = (theme: Theme) =>
@@ -71,6 +73,31 @@ const styles = (theme: Theme) =>
justifyContent: "center",
alignItems: "center",
},
indicator: {
position: "absolute",
display: "block",
width: 15,
height: 15,
top: 0,
right: 2,
marginTop: -16,
transitionDuration: "0.2s",
color: "#32C787",
"& svg": {
width: 10,
height: 10,
top: "50%",
left: "50%",
transitionDuration: "0.2s",
},
"&.newItem": {
color: "#2781B0",
"& svg": {
width: 15,
height: 15,
},
},
},
});
const PageHeader = ({
@@ -83,7 +110,20 @@ const PageHeader = ({
toggleList,
middleComponent,
features,
managerOpen,
newItems,
}: IPageHeader) => {
const [newObject, setNewObject] = useState<boolean>(false);
useEffect(() => {
if (managerObjects.length > 0 && !managerOpen) {
setNewObject(true);
setTimeout(() => {
setNewObject(false);
}, 300);
}
}, [managerObjects.length, managerOpen]);
if (features.includes("hide-menu")) {
return <Fragment />;
}
@@ -151,7 +191,29 @@ const PageHeader = ({
}}
id="object-manager-toggle"
size="large"
sx={{
marginRight: "20px",
color: "#5E5E5E",
position: "relative",
border: "#E2E2E2 1px solid",
borderRadius: "3px",
width: "40px",
height: "40px",
backgroundColor: "#F8F8F8",
padding: 0,
"&>svg": {
width: "25px",
},
}}
>
<div
className={`${classes.indicator} ${newObject ? "newItem" : ""}`}
style={{
opacity: managerObjects.length > 0 && newItems ? 1 : 0,
}}
>
<CircleIcon />
</div>
<ObjectManagerIcon />
</IconButton>
)}
@@ -165,6 +227,8 @@ const mapState = (state: AppState) => ({
operatorMode: state.system.operatorMode,
managerObjects: state.objectBrowser.objectManager.objectsToManage,
features: state.console.session.features,
managerOpen: state.objectBrowser.objectManager.managerOpen,
newItems: state.objectBrowser.objectManager.newItems,
});
const mapDispatchToProps = {

View File

@@ -14,7 +14,7 @@
// 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 React, { Fragment } from "react";
import { styled } from "@mui/material/styles";
import LinearProgress, {
linearProgressClasses,
@@ -28,6 +28,8 @@ interface IProgressBarWrapper {
indeterminate?: boolean;
withLabel?: boolean;
size?: string;
error?: boolean;
cancelled?: boolean;
}
const BorderLinearProgress = styled(LinearProgress)(() => ({
@@ -48,14 +50,42 @@ const SmallBorderLinearProgress = styled(BorderLinearProgress)(() => ({
},
}));
function LinearProgressWithLabel(props: LinearProgressProps) {
function LinearProgressWithLabel(
props: { error: boolean; cancelled: boolean } & LinearProgressProps
) {
let color = "#000";
let size = 18;
if(props.error) {
color = "#C83B51"
size = 14
}
else if(props.cancelled) {
color = "#FFBD62"
size = 14
}
return (
<Box sx={{ display: "flex", alignItems: "center" }}>
<Box sx={{ width: "100%", mr: 1 }}>
<Box sx={{ width: "100%", mr: 3 }}>
<BorderLinearProgress variant="determinate" {...props} />
</Box>
<Box sx={{ minWidth: 35, fontSize: 14 }} className={"value"}>
{`${Math.round(props.value || 0)}%`}
<Box
sx={{
minWidth: 35,
fontSize: size,
color: color,
}}
className={"value"}
>
{props.cancelled ? (
"Cancelled"
) : (
<Fragment>
{props.error ? "Failed" : `${Math.round(props.value || 0)}%`}
</Fragment>
)}
</Box>
</Box>
);
@@ -67,22 +97,32 @@ const ProgressBarWrapper = ({
indeterminate,
withLabel,
size = "regular",
error,
cancelled,
}: IProgressBarWrapper) => {
let color: any;
if (value === 100 && ready) {
color = "success";
} else if (value === 100 && !ready) {
if (error) {
color = "error";
} else if (cancelled) {
color = "warning";
}else if (value === 100 && ready) {
color = "success";
} else {
color = "primary";
}
const propsComponent: LinearProgressProps = {
variant: indeterminate && !ready ? "indeterminate" : "determinate",
variant: indeterminate && !ready && !cancelled ? "indeterminate" : "determinate",
value: ready ? 100 : value,
color: color,
};
if (withLabel) {
return <LinearProgressWithLabel {...propsComponent} />;
return (
<LinearProgressWithLabel
{...propsComponent}
error={!!error}
cancelled={!!cancelled}
/>
);
}
if (size === "small") {
return <SmallBorderLinearProgress {...propsComponent} />;

View File

@@ -31,11 +31,13 @@ import {
OBJECT_MANAGER_SET_SEARCH_OBJECT,
OBJECT_MANAGER_TOGGLE_LIST,
OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT,
OBJECT_MANAGER_SET_LOADING,
OBJECT_MANAGER_ERROR_IN_OBJECT,
OBJECT_MANAGER_CANCEL_OBJECT,
REWIND_RESET_REWIND,
REWIND_SET_ENABLE,
IFileItem,
BUCKET_BROWSER_SET_SELECTED_OBJECT,
OBJECT_MANAGER_SET_LOADING,
IFileItem,
} from "./types";
export const setRewindEnable = (
@@ -90,6 +92,13 @@ export const completeObject = (instanceID: string) => {
};
};
export const failObject = (instanceID: string) => {
return {
type: OBJECT_MANAGER_ERROR_IN_OBJECT,
instanceID,
};
};
export const deleteFromList = (instanceID: string) => {
return {
type: OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST,
@@ -128,6 +137,20 @@ export const setSearchObjects = (searchString: string) => {
};
};
export const setLoadingObjectsList = (status: boolean) => {
return {
type: OBJECT_MANAGER_SET_LOADING,
status,
};
};
export const cancelObjectInList = (instanceID: string) => {
return {
type: OBJECT_MANAGER_CANCEL_OBJECT,
instanceID,
};
};
export const setSearchVersions = (searchString: string) => {
return {
type: BUCKET_BROWSER_VERSIONS_SET_SEARCH,
@@ -176,10 +199,3 @@ export const setSelectedObjectView = (object: string | null) => {
object,
};
};
export const setLoadingObjectsList = (status: boolean) => {
return {
type: OBJECT_MANAGER_SET_LOADING,
status,
};
};

View File

@@ -37,6 +37,8 @@ import {
ObjectBrowserActionTypes,
BUCKET_BROWSER_SET_SELECTED_OBJECT,
OBJECT_MANAGER_SET_LOADING,
OBJECT_MANAGER_ERROR_IN_OBJECT,
OBJECT_MANAGER_CANCEL_OBJECT,
} from "./types";
const defaultRewind = {
@@ -57,6 +59,7 @@ const initialState: ObjectBrowserState = {
objectManager: {
objectsToManage: [],
managerOpen: false,
newItems: false,
},
searchObjects: "",
versionedFile: "",
@@ -106,6 +109,7 @@ export function objectBrowserReducer(
objectManager: {
objectsToManage: cloneObjects,
managerOpen: state.objectManager.managerOpen,
newItems: true,
},
};
case OBJECT_MANAGER_UPDATE_PROGRESS_OBJECT:
@@ -127,6 +131,7 @@ export function objectBrowserReducer(
objectManager: {
objectsToManage: copyManager,
managerOpen: state.objectManager.managerOpen,
newItems: state.objectManager.newItems,
},
};
case OBJECT_MANAGER_COMPLETE_OBJECT:
@@ -149,6 +154,51 @@ export function objectBrowserReducer(
objectManager: {
objectsToManage: copyObject,
managerOpen: state.objectManager.managerOpen,
newItems: state.objectManager.newItems,
},
};
case OBJECT_MANAGER_ERROR_IN_OBJECT:
const objectItems = [...state.objectManager.objectsToManage];
const objectToFail = state.objectManager.objectsToManage.findIndex(
(item) => item.instanceID === action.instanceID
);
if (objectToFail === -1) {
return { ...state };
}
objectItems[objectToFail].failed = true;
return {
...state,
objectManager: {
objectsToManage: objectItems,
managerOpen: state.objectManager.managerOpen,
newItems: state.objectManager.newItems,
},
};
case OBJECT_MANAGER_CANCEL_OBJECT:
const objectsListFind = [...state.objectManager.objectsToManage];
const objectToCancel = state.objectManager.objectsToManage.findIndex(
(item) => item.instanceID === action.instanceID
);
if (objectToCancel === -1) {
return { ...state };
}
objectsListFind[objectToCancel].cancelled = true;
objectsListFind[objectToCancel].done = true;
objectsListFind[objectToCancel].percentage = 0;
return {
...state,
objectManager: {
objectsToManage: objectsListFind,
managerOpen: state.objectManager.managerOpen,
newItems: state.objectManager.newItems,
},
};
case OBJECT_MANAGER_DELETE_FROM_OBJECT_LIST:
@@ -162,6 +212,7 @@ export function objectBrowserReducer(
objectsToManage: notObject,
managerOpen:
notObject.length === 0 ? false : state.objectManager.managerOpen,
newItems: state.objectManager.newItems,
},
};
case OBJECT_MANAGER_CLEAN_LIST:
@@ -177,6 +228,7 @@ export function objectBrowserReducer(
nonCompletedList.length === 0
? false
: state.objectManager.managerOpen,
newItems: false,
},
};
case OBJECT_MANAGER_TOGGLE_LIST:
@@ -185,6 +237,7 @@ export function objectBrowserReducer(
objectManager: {
...state.objectManager,
managerOpen: !state.objectManager.managerOpen,
newItems: false,
},
};
case OBJECT_MANAGER_OPEN_LIST:

View File

@@ -22,6 +22,7 @@ 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_ERROR_IN_OBJECT = "OBJECT_MANAGER/ERROR_IN_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";
@@ -30,6 +31,7 @@ export const OBJECT_MANAGER_OPEN_LIST = "OBJECT_MANAGER/OPEN_LIST";
export const OBJECT_MANAGER_CLOSE_LIST = "OBJECT_MANAGER/CLOSE_LIST";
export const OBJECT_MANAGER_SET_SEARCH_OBJECT =
"OBJECT_MANAGER/SET_SEARCH_OBJECT";
export const OBJECT_MANAGER_CANCEL_OBJECT = "OBJECT_MANAGER/CANCEL_OBJECT";
export const BUCKET_BROWSER_VERSIONS_MODE_ENABLED =
"BUCKET_BROWSER/VERSIONS_MODE_ENABLED";
@@ -81,6 +83,7 @@ export interface ObjectBrowserReducer {
export interface ObjectManager {
objectsToManage: IFileItem[];
managerOpen: boolean;
newItems: boolean;
}
export interface IFileItem {
@@ -91,6 +94,9 @@ export interface IFileItem {
percentage: number;
done: boolean;
waitingForFile: boolean;
failed: boolean;
cancelled: boolean;
call?: XMLHttpRequest;
}
interface RewindSetEnabled {
@@ -147,6 +153,12 @@ interface OMCloseList {
type: typeof OBJECT_MANAGER_CLOSE_LIST;
}
interface OMSetObjectError {
type: typeof OBJECT_MANAGER_ERROR_IN_OBJECT;
status: boolean;
instanceID: string;
}
interface SetSearchObjects {
type: typeof OBJECT_MANAGER_SET_SEARCH_OBJECT;
searchString: string;
@@ -192,6 +204,11 @@ interface SetObjectManagerLoading {
status: boolean;
}
interface CancelObjectInManager {
type: typeof OBJECT_MANAGER_CANCEL_OBJECT;
instanceID: string;
}
export type ObjectBrowserActionTypes =
| RewindSetEnabled
| RewindReset
@@ -204,6 +221,7 @@ export type ObjectBrowserActionTypes =
| OMToggleList
| OMOpenList
| OMCloseList
| OMSetObjectError
| SetSearchObjects
| SetSearchVersions
| SetSelectedversion
@@ -212,4 +230,5 @@ export type ObjectBrowserActionTypes =
| SetLoadingObjectInfo
| SetObjectDetailsState
| SetSelectedObject
| SetObjectManagerLoading;
| SetObjectManagerLoading
| CancelObjectInManager;