diff --git a/frontend/src/components/upload/FileList.tsx b/frontend/src/components/upload/FileList.tsx
index 98468e8a0..560670f33 100644
--- a/frontend/src/components/upload/FileList.tsx
+++ b/frontend/src/components/upload/FileList.tsx
@@ -1,10 +1,20 @@
-import { ActionIcon, Table } from "@mantine/core";
+import {
+ ActionIcon,
+ Badge,
+ Box,
+ Group,
+ Table,
+ Text,
+ Tooltip,
+} from "@mantine/core";
import { TbTrash } from "react-icons/tb";
import { GrUndo } from "react-icons/gr";
-import { FileListItem } from "../../types/File.type";
+import { FileListItem, FileUpload } from "../../types/File.type";
import { byteToHumanSizeString } from "../../utils/fileSize.util";
import UploadProgressIndicator from "./UploadProgressIndicator";
import { FormattedMessage } from "react-intl";
+import useTranslate from "../../hooks/useTranslate.hook";
+import { formatTimeRemaining } from "../../utils/time.util";
const FileListRow = ({
file,
@@ -15,6 +25,7 @@ const FileListRow = ({
onRemove?: () => void;
onRestore?: () => void;
}) => {
+ const t = useTranslate();
{
const uploadable = "uploadingProgress" in file;
const uploading = uploadable && file.uploadingProgress !== 0;
@@ -33,6 +44,17 @@ const FileListRow = ({
>
{file.name} |
{byteToHumanSizeString(+file.size)} |
+
+ {uploading &&
+ uploadable &&
+ file.estimatedTimeRemaining !== undefined && (
+
+
+ {formatTimeRemaining(file.estimatedTimeRemaining)}
+
+
+ )}
+ |
{removable && (
({
/>
));
+ // Calculate upload statistics
+ const uploadableFiles = files.filter(
+ (file) => "uploadingProgress" in file,
+ ) as unknown as FileUpload[];
+
+ const inProgressFiles = uploadableFiles.filter(
+ (file) => file.uploadingProgress > 0 && file.uploadingProgress < 100,
+ ).length;
+
+ const pendingFiles = uploadableFiles.filter(
+ (file) => file.uploadingProgress === 0,
+ ).length;
+
+ const remainingFiles = inProgressFiles + pendingFiles;
+
+ // Calculate overall estimated time
+ let maxEstimatedTime = 0;
+ if (inProgressFiles > 0) {
+ for (const file of uploadableFiles) {
+ if (
+ file.estimatedTimeRemaining &&
+ file.estimatedTimeRemaining > maxEstimatedTime
+ ) {
+ maxEstimatedTime = file.estimatedTimeRemaining;
+ }
+ }
+ }
+
return (
-
-
-
-
-
- |
-
-
- |
- |
-
-
- {rows}
-
+ <>
+ {uploadableFiles.length > 0 && remainingFiles > 0 && (
+
+
+
+
+
+ {maxEstimatedTime > 0 && (
+
+
+
+ )}
+
+
+ )}
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+ |
+
+
+ {rows}
+
+ >
);
};
diff --git a/frontend/src/components/upload/UploadProgressIndicator.tsx b/frontend/src/components/upload/UploadProgressIndicator.tsx
index a26b740da..8b848f4da 100644
--- a/frontend/src/components/upload/UploadProgressIndicator.tsx
+++ b/frontend/src/components/upload/UploadProgressIndicator.tsx
@@ -1,6 +1,13 @@
import { Loader, RingProgress } from "@mantine/core";
import { TbCircleCheck } from "react-icons/tb";
-const UploadProgressIndicator = ({ progress }: { progress: number }) => {
+
+type UploadProgressIndicatorProps = {
+ progress: number;
+};
+
+const UploadProgressIndicator = ({
+ progress,
+}: UploadProgressIndicatorProps) => {
if (progress > 0 && progress < 100) {
return (
files.map((file, callbackIndex) => {
if (fileIndex == callbackIndex) {
+ if (progress === 1 && !file.uploadStartTime) {
+ file.uploadStartTime = Date.now();
+ }
+
file.uploadingProgress = progress;
+
+ if (progress > 0 && progress < 100) {
+ const elapsedMs =
+ Date.now() - (file.uploadStartTime || Date.now());
+ const estimatedTotalMs = (elapsedMs / progress) * 100;
+ const remainingMs = estimatedTotalMs - elapsedMs;
+ file.estimatedTimeRemaining = Math.max(0, remainingMs);
+ } else if (progress >= 100) {
+ file.estimatedTimeRemaining = 0;
+ }
}
return file;
}),
diff --git a/frontend/src/types/File.type.ts b/frontend/src/types/File.type.ts
index f50cd9db9..cb0b0c15f 100644
--- a/frontend/src/types/File.type.ts
+++ b/frontend/src/types/File.type.ts
@@ -1,4 +1,8 @@
-export type FileUpload = File & { uploadingProgress: number };
+export type FileUpload = File & {
+ uploadingProgress: number;
+ uploadStartTime?: number;
+ estimatedTimeRemaining?: number;
+};
export type FileUploadResponse = { id: string; name: string };
diff --git a/frontend/src/utils/time.util.ts b/frontend/src/utils/time.util.ts
new file mode 100644
index 000000000..80f667f6f
--- /dev/null
+++ b/frontend/src/utils/time.util.ts
@@ -0,0 +1,20 @@
+/**
+ * Formats a time in milliseconds to a human-readable string
+ * @param ms Time in milliseconds
+ * @returns Formatted time string (e.g. "5h 30m" or "2m 15s")
+ */
+export const formatTimeRemaining = (ms: number): string => {
+ if (ms <= 0) return "0s";
+
+ const seconds = Math.floor(ms / 1000) % 60;
+ const minutes = Math.floor(ms / (1000 * 60)) % 60;
+ const hours = Math.floor(ms / (1000 * 60 * 60));
+
+ if (hours > 0) {
+ return `${hours}h ${minutes}m`;
+ } else if (minutes > 0) {
+ return `${minutes}m ${seconds}s`;
+ } else {
+ return `${seconds}s`;
+ }
+};
|