diff --git a/src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.tsx b/src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.tsx index 7b4b233ab3..57b912ce21 100644 --- a/src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.tsx +++ b/src/pages/scientificServices/pipelines/components/inputs/PipelineFileInput.tsx @@ -11,6 +11,7 @@ import { resumeUpload } from 'src/pages/scientificServices/pipelines/utils/uploa export interface PipelineInputFileUploadState { signedUrl?: string; // The resumable upload session URL progress: number; // Progress percentage (0-100) + uploadEta?: number; // Estimated time remaining in seconds errorMessage?: string; // Optional error message } @@ -271,6 +272,10 @@ export const PipelineFileInput: React.FC = ({ }} /> +
+
Estimated time remaining:
+ {uploadState.uploadEta ? `${Math.round(uploadState.uploadEta)} seconds` : 'Calculating...'} +
) : ( diff --git a/src/pages/scientificServices/pipelines/utils/upload-utils.ts b/src/pages/scientificServices/pipelines/utils/upload-utils.ts index 9b42d70ca8..96478c3872 100644 --- a/src/pages/scientificServices/pipelines/utils/upload-utils.ts +++ b/src/pages/scientificServices/pipelines/utils/upload-utils.ts @@ -109,6 +109,9 @@ export async function initiateResumableUpload( signedUrl: string, setUploadState: Dispatch>> ): Promise { + // Keeps track of all progress measurements to calculate average upload rate and estimated time remaining + const uploadRates: number[] = []; + // Step 1: Initiate the resumable upload session. // Google will return a session URL in the Location header, // which we'll use to upload the file. @@ -125,17 +128,50 @@ export async function initiateResumableUpload( // Step 2: Upload the file using XMLHttpRequest for progress tracking const startTime = Date.now(); + let lastEtaUpdate = 0; // Track when we last updated the ETA return new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); xhr.upload.addEventListener('progress', (event) => { if (event.lengthComputable) { + const currentTime = Date.now(); + const elapsedTime = currentTime - startTime; + const uploadRate = event.loaded / elapsedTime; // bytes per ms + const percent = Math.round((event.loaded / event.total) * 100); - setUploadState((prev) => ({ - ...prev, - [inputName]: { progress: percent, signedUrl: sessionUrl }, - })); + + // Only update ETA every 2 seconds or on the first update, to avoid choppy ETA updates + if (currentTime - lastEtaUpdate >= 2000 || lastEtaUpdate === 0) { + uploadRates.push(uploadRate); + if (uploadRates.length > 5) { + uploadRates.shift(); // Remove the oldest measurement + } + + const averageUploadRate = uploadRates.reduce((acc, rate) => acc + rate, 0) / uploadRates.length; + const bytesRemaining = inputFile.size - event.loaded; + const estimatedTimeRemainingMs = bytesRemaining / averageUploadRate; + + setUploadState((prev) => ({ + ...prev, + [inputName]: { + progress: percent, + signedUrl: sessionUrl, + uploadEta: uploadRates.length < 5 ? undefined : estimatedTimeRemainingMs / 1000, + }, + })); + + lastEtaUpdate = currentTime; + } else { + // Just update progress without changing ETA to keep it smooth + setUploadState((prev) => ({ + ...prev, + [inputName]: { + ...prev[inputName], + progress: percent, + }, + })); + } } });