From a060ba4e8a204c20e1c776a42273f0cbb140ebfa Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:02 +0200 Subject: [PATCH 1/7] Introduce per-file progress tracking for TUS uploads --- .../upload/useUploadBatchOperations.ts | 5 +- .../composables/upload/useUploadSubmission.ts | 6 +- client/src/utils/upload.ts | 67 +++++++++++++++++-- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/client/src/composables/upload/useUploadBatchOperations.ts b/client/src/composables/upload/useUploadBatchOperations.ts index 741bbcf1efc..c0a13c45341 100644 --- a/client/src/composables/upload/useUploadBatchOperations.ts +++ b/client/src/composables/upload/useUploadBatchOperations.ts @@ -152,8 +152,9 @@ export function useUploadBatchOperations(options: UploadBatchOperationsOptions = collectionType: batch.type, }, { - progress: (percentage) => { - ids.forEach((id) => uploadState.updateProgress(id, percentage)); + uploadIds: ids, + perFileProgress: (fileId, percentage) => { + uploadState.updateProgress(fileId, percentage); }, success: () => { ids.forEach((id) => uploadState.updateProgress(id, 100)); diff --git a/client/src/composables/upload/useUploadSubmission.ts b/client/src/composables/upload/useUploadSubmission.ts index 510b8ad9c14..f363a4c3651 100644 --- a/client/src/composables/upload/useUploadSubmission.ts +++ b/client/src/composables/upload/useUploadSubmission.ts @@ -10,7 +10,6 @@ import { markTrackedCompleted, markTrackedError, splitTrackedUploadsByType, - updateTrackedProgress, } from "@/composables/upload/uploadTracking"; import { useUploadBatchOperations } from "@/composables/upload/useUploadBatchOperations"; import { errorMessageAsString } from "@/utils/simple-error"; @@ -103,7 +102,10 @@ export function useUploadSubmission() { }, progress: (percentage) => { onProgress?.(percentage); - updateTrackedProgress(uploadState, apiIds, percentage); + }, + uploadIds: apiIds, + perFileProgress: (fileId, percentage) => { + uploadState.updateProgress(fileId, percentage); }, }; diff --git a/client/src/utils/upload.ts b/client/src/utils/upload.ts index 07bcbb29a46..01efcbb4374 100644 --- a/client/src/utils/upload.ts +++ b/client/src/utils/upload.ts @@ -198,10 +198,18 @@ export interface BuildPayloadOptions { // Configuration Types // ============================================================================ +/** Per-file progress tracking options */ +interface PerFileProgressOptions { + /** Upload item IDs corresponding to files (one per file, for per-file progress tracking) */ + uploadIds?: string[]; + /** Callback for per-file progress updates (fileId, percentage) */ + perFileProgress?: (fileId: string, percentage: number) => void; +} + /** * Configuration for upload submission. */ -export interface UploadSubmitConfig extends FetchDatasetsCallbacks { +export interface UploadSubmitConfig extends FetchDatasetsCallbacks, PerFileProgressOptions { /** The upload payload data */ data: UploadDataPayload; /** Whether this is a composite upload */ @@ -213,7 +221,7 @@ export interface UploadSubmitConfig extends FetchDatasetsCallbacks { /** * Configuration for the uploadDatasets function. */ -export interface UploadDatasetsConfig extends FetchDatasetsCallbacks, BuildPayloadOptions { +export interface UploadDatasetsConfig extends FetchDatasetsCallbacks, BuildPayloadOptions, PerFileProgressOptions { /** Chunk size for TUS uploads in bytes (default: 10MB) */ chunkSize?: number; } @@ -833,14 +841,27 @@ function toApiPayload(data: UploadPayload): FetchDataPayload { /** * Uploads files via TUS protocol, then submits the complete payload. + * + * @param data - Upload payload containing files and targets + * @param tusEndpoint - TUS upload endpoint URL + * @param chunkSize - Chunk size for TUS uploads in bytes + * @param callbacks - Standard fetch callbacks (success, error, warning, progress) + * @param uploadIds - Optional array of upload item IDs (one per file) for per-file progress tracking + * @param perFileProgress - Optional callback for per-file progress updates */ async function uploadFilesViaTus( data: UploadPayload, tusEndpoint: string, chunkSize: number, callbacks: FetchDatasetsCallbacks, + uploadIds?: string[], + perFileProgress?: (fileId: string, percentage: number) => void, ): Promise { const files = data.files || []; + const hasPerFileTracking = uploadIds && perFileProgress && uploadIds.length === files.length; + + // Track per-file progress for aggregate calculation + const fileProgressMap = new Map(); // Build API payload with TUS session info const apiPayload: Record = { @@ -857,12 +878,28 @@ async function uploadFilesViaTus( continue; } + const fileId = hasPerFileTracking ? uploadIds[index] : undefined; + const result = await createTusUpload({ file, endpoint: tusEndpoint, historyId: data.history_id, chunkSize, - onProgress: callbacks.progress || (() => {}), + onProgress: (percentage: number) => { + if (hasPerFileTracking && fileId) { + fileProgressMap.set(fileId, percentage); + perFileProgress!(fileId, percentage); + + // Compute aggregate progress from all files uploaded so far + const values = Array.from(fileProgressMap.values()); + const aggregate = Math.round( + values.reduce((sum: number, p: number) => sum + p, 0) / values.length, + ); + callbacks.progress?.(aggregate); + } else { + callbacks.progress?.(percentage); + } + }, onError: (err: Error) => { callbacks.error?.(err); }, @@ -899,6 +936,8 @@ export async function submitUpload(config: UploadSubmitConfig): Promise { progress = () => {}, isComposite = false, chunkSize = DEFAULT_CHUNK_SIZE, + uploadIds, + perFileProgress, } = config; // Initial validation @@ -915,7 +954,7 @@ export async function submitUpload(config: UploadSubmitConfig): Promise { if (hasFiles || isComposite) { // Upload files via TUS, then submit payload - await uploadFilesViaTus(data, tusEndpoint, chunkSize, callbacks); + await uploadFilesViaTus(data, tusEndpoint, chunkSize, callbacks, uploadIds, perFileProgress); } else if (data.targets && data.targets.length > 0) { const firstTarget = data.targets[0]; @@ -945,7 +984,7 @@ export async function submitUpload(config: UploadSubmitConfig): Promise { blob.name = String(firstElement.name || DEFAULT_FILE_NAME); const filesData: UploadPayload = { ...data, files: [blob] }; - await uploadFilesViaTus(filesData, tusEndpoint, chunkSize, callbacks); + await uploadFilesViaTus(filesData, tusEndpoint, chunkSize, callbacks, uploadIds, perFileProgress); } } } @@ -983,7 +1022,17 @@ export async function submitUpload(config: UploadSubmitConfig): Promise { * ``` */ export async function uploadDatasets(items: ApiUploadItem[], config: UploadDatasetsConfig = {}): Promise { - const { composite = false, compositeName, chunkSize, success, error, warning, progress } = config; + const { + composite = false, + compositeName, + chunkSize, + success, + error, + warning, + progress, + uploadIds, + perFileProgress, + } = config; try { // Build the API-ready payload from upload items @@ -1006,6 +1055,8 @@ export async function uploadDatasets(items: ApiUploadItem[], config: UploadDatas error, warning, progress, + uploadIds, + perFileProgress, }); } catch (err) { const errorMessage = errorMessageAsString(err); @@ -1071,7 +1122,7 @@ export async function uploadCollectionDatasets( collectionOptions: CollectionUploadOptions, config: UploadDatasetsConfig = {}, ): Promise { - const { chunkSize, success, error, warning, progress } = config; + const { chunkSize, success, error, warning, progress, uploadIds, perFileProgress } = config; try { const payload = buildCollectionUploadPayload(items, collectionOptions); @@ -1090,6 +1141,8 @@ export async function uploadCollectionDatasets( error, warning, progress, + uploadIds, + perFileProgress, }); } catch (err) { config.error?.(errorMessageAsString(err)); From fd2c9dea46c25d7cd3ba39b8b856a2262901f912 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:03 +0200 Subject: [PATCH 2/7] Prevents upload progress regression after completion --- client/src/components/Panels/Upload/uploadState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Panels/Upload/uploadState.ts b/client/src/components/Panels/Upload/uploadState.ts index 5f5200c22ee..98de4f4dd58 100644 --- a/client/src/components/Panels/Upload/uploadState.ts +++ b/client/src/components/Panels/Upload/uploadState.ts @@ -309,7 +309,7 @@ export function useUploadState() { */ function updateProgress(id: string, progress: number) { const item = items.value.find((u) => u.id === id); - if (item) { + if (item && item.status !== "completed") { item.progress = Math.max(0, Math.min(100, Math.round(progress))); if (item.progress >= 100 && item.status !== "error") { item.status = "completed"; From 3a79edd5a777a707064644b741890fdb5a304e31 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:03 +0200 Subject: [PATCH 3/7] Refines upload progress indicator for active states --- .../components/Panels/Upload/UploadProgressIndicator.vue | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/client/src/components/Panels/Upload/UploadProgressIndicator.vue b/client/src/components/Panels/Upload/UploadProgressIndicator.vue index fdea00a93ac..fcb4e0b5e0d 100644 --- a/client/src/components/Panels/Upload/UploadProgressIndicator.vue +++ b/client/src/components/Panels/Upload/UploadProgressIndicator.vue @@ -11,11 +11,13 @@ const emit = defineEmits<{ (e: "show-details"): void; }>(); +const hasActiveUploads = computed(() => uploads.value.some((f) => f.status !== "completed" && f.status !== "error")); + const statusIcon = computed(() => { if (errorCount.value > 0) { return faTimes; } - if (uploadingCount.value > 0) { + if (hasActiveUploads.value) { return faSpinner; } return faCheck; @@ -25,14 +27,14 @@ const statusClass = computed(() => { if (errorCount.value > 0) { return "text-danger"; } - if (uploadingCount.value > 0) { + if (hasActiveUploads.value) { return "text-primary"; } return "text-success"; }); const statusText = computed(() => { - if (uploadingCount.value > 0 || uploads.value.some((f) => f.status === "queued")) { + if (hasActiveUploads.value) { return "Uploading"; } if (uploads.value.length > 0 && completedCount.value === uploads.value.length) { From b4966b25d8c8c624cf12f4a1ddf7f6df142fd079 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:04 +0200 Subject: [PATCH 4/7] Displays newest uploads first in progress list --- client/src/components/Panels/Upload/uploadState.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/components/Panels/Upload/uploadState.ts b/client/src/components/Panels/Upload/uploadState.ts index 98de4f4dd58..28159e2ff29 100644 --- a/client/src/components/Panels/Upload/uploadState.ts +++ b/client/src/components/Panels/Upload/uploadState.ts @@ -192,7 +192,7 @@ export function useUploadState() { upload, })); - return [...batchItems, ...standaloneItems].sort((a, b) => a.createdAt - b.createdAt); + return [...batchItems, ...standaloneItems].sort((a, b) => b.createdAt - a.createdAt); }); /** From 9758728ffefbfa3cbc10d616a2fd3b93c3655b8b Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:04 +0200 Subject: [PATCH 5/7] Updates queued upload icon to reflect waiting state --- client/src/components/Panels/Upload/uploadProgressUi.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/components/Panels/Upload/uploadProgressUi.ts b/client/src/components/Panels/Upload/uploadProgressUi.ts index ab62ffc9211..68d83eff4df 100644 --- a/client/src/components/Panels/Upload/uploadProgressUi.ts +++ b/client/src/components/Panels/Upload/uploadProgressUi.ts @@ -1,6 +1,7 @@ import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; import { faCheck, + faClock, faCloud, faExclamationTriangle, faLayerGroup, @@ -45,7 +46,7 @@ export interface BatchWithProgress extends CollectionBatchState { const FILE_PROGRESS_UI: Record = { queued: { - icon: faSpinner, + icon: faClock, textClass: "text-muted", spin: false, }, From 3fd510c884dbbd0dc91cb9a3f363b89b5459a9d1 Mon Sep 17 00:00:00 2001 From: davelopez <46503462+davelopez@users.noreply.github.com> Date: Mon, 15 Jun 2026 14:02:05 +0200 Subject: [PATCH 6/7] Implements pagination for upload progress view --- .../Panels/Upload/UploadProgress.vue | 51 ++++++++----------- 1 file changed, 21 insertions(+), 30 deletions(-) diff --git a/client/src/components/Panels/Upload/UploadProgress.vue b/client/src/components/Panels/Upload/UploadProgress.vue index efa2bd08ff5..1174418b246 100644 --- a/client/src/components/Panels/Upload/UploadProgress.vue +++ b/client/src/components/Panels/Upload/UploadProgress.vue @@ -1,6 +1,7 @@