This is an automated email from the ASF dual-hosted git repository.
sbp pushed a commit to branch sbp
in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
The following commit(s) were added to refs/heads/sbp by this push:
new 03f4030d Combine multiple uploaded files into a single JS request
03f4030d is described below
commit 03f4030db5fb678c40abf1a1d6b57286934b6051
Author: Sean B. Palmer <[email protected]>
AuthorDate: Mon Apr 6 20:29:04 2026 +0100
Combine multiple uploaded files into a single JS request
---
atr/static/js/src/upload-progress-ui.js | 305 ++++++++++++++++++--------------
atr/static/js/src/upload-progress.js | 279 ++++++++---------------------
2 files changed, 247 insertions(+), 337 deletions(-)
diff --git a/atr/static/js/src/upload-progress-ui.js
b/atr/static/js/src/upload-progress-ui.js
index cf482100..0a6b4b22 100644
--- a/atr/static/js/src/upload-progress-ui.js
+++ b/atr/static/js/src/upload-progress-ui.js
@@ -18,166 +18,201 @@
*/
window.UploadUI = {
- formatBytes(b) {
- if (b < 1024) return `${b} B`;
- if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`;
- if (b < 1073741824) return `${(b / 1048576).toFixed(1)} MB`;
- return `${(b / 1073741824).toFixed(2)} GB`;
- },
-
- buildFileProgressCard(fileName, fileSizeText) {
- const div = document.createElement("div");
- div.className = "card mb-3";
-
- const cardBody = document.createElement("div");
- cardBody.className = "card-body";
-
- const headerRow = document.createElement("div");
- headerRow.className =
- "d-flex justify-content-between align-items-start mb-2";
+ buildBatchUI(container, files, onCancel) {
+ container.innerHTML = "";
+ container.classList.remove("d-none");
+
+ container.append(this.buildHeader(onCancel));
+ container.append(this.buildProgressBar());
+
+ const list = document.createElement("div");
+ list.id = "upload-file-list";
+ files.forEach((file, i) => {
+ const row = document.createElement("div");
+ row.className =
+ "d-flex justify-content-between
align-items-center py-1 border-bottom";
+ row.dataset.fileIndex = String(i);
+ const nameSpan = document.createElement("span");
+ nameSpan.textContent = `${file.name}
(${this.formatBytes(file.size)})`;
+ const statusSpan = document.createElement("small");
+ statusSpan.className = "upload-file-status text-muted";
+ statusSpan.textContent = "Pending";
+ row.append(nameSpan, statusSpan);
+ list.append(row);
+ });
+ container.append(list);
- const fileInfoDiv = document.createElement("div");
- const fileNameEl = document.createElement("strong");
- fileNameEl.className = "file-name";
- fileNameEl.textContent = fileName;
- const fileSize = document.createElement("small");
- fileSize.className = "text-muted ms-2";
- fileSize.textContent = `(${fileSizeText})`;
- fileInfoDiv.append(fileNameEl, fileSize);
+ const msgArea = document.createElement("div");
+ msgArea.id = "upload-message-area";
+ container.append(msgArea);
+ },
+ buildHeader(onCancel) {
+ const header = document.createElement("div");
+ header.className = "d-flex justify-content-between
align-items-center mb-3";
+ const statusEl = document.createElement("strong");
+ statusEl.id = "upload-batch-status";
+ statusEl.textContent = "Preparing upload";
const cancelBtn = document.createElement("button");
cancelBtn.type = "button";
- cancelBtn.className = "btn btn-sm btn-outline-secondary
cancel-btn";
- cancelBtn.textContent = "Cancel";
- headerRow.append(fileInfoDiv, cancelBtn);
+ cancelBtn.id = "upload-cancel-btn";
+ cancelBtn.className = "btn btn-sm btn-outline-secondary";
+ cancelBtn.textContent = "Cancel upload";
+ cancelBtn.addEventListener("click", onCancel);
+ header.append(statusEl, cancelBtn);
+ return header;
+ },
+ buildProgressBar() {
+ const wrap = document.createElement("div");
+ wrap.className = "mb-3";
const progress = document.createElement("progress");
+ progress.id = "upload-overall-progress";
progress.className = "w-100 mb-1";
progress.value = 0;
progress.max = 100;
-
- const statusRow = document.createElement("div");
- statusRow.className = "d-flex justify-content-between";
- const uploadStatus = document.createElement("small");
- uploadStatus.className = "upload-status text-muted";
- uploadStatus.textContent = "Preparing...";
- const uploadPercent = document.createElement("small");
- uploadPercent.className = "upload-percent";
- uploadPercent.textContent = "0%";
- statusRow.append(uploadStatus, uploadPercent);
-
- cardBody.append(headerRow, progress, statusRow);
- div.append(cardBody);
- return div;
+ const info = document.createElement("div");
+ info.className = "d-flex justify-content-between";
+ const bytes = document.createElement("small");
+ bytes.id = "upload-progress-bytes";
+ bytes.className = "text-muted";
+ const percent = document.createElement("small");
+ percent.id = "upload-progress-percent";
+ percent.textContent = "0%";
+ info.append(bytes, percent);
+ wrap.append(progress, info);
+ return wrap;
},
- createFileProgressUI(file, index, activeUploads) {
- const div = this.buildFileProgressCard(
- file.name,
- this.formatBytes(file.size),
- );
- div.id = `upload-file-${index}`;
- div.querySelector(".cancel-btn").addEventListener("click", ()
=> {
- const xhr = activeUploads.get(index);
- if (xhr) xhr.abort();
- });
- return div;
+ formatBytes(b) {
+ if (b < 1024) return `${b} B`;
+ if (b < 1048576) return `${(b / 1024).toFixed(1)} KB`;
+ if (b < 1073741824) return `${(b / 1048576).toFixed(1)} MB`;
+ return `${(b / 1073741824).toFixed(2)} GB`;
},
- markUploadDone(ui, success, msg) {
- const statusEl = ui.querySelector(".upload-status");
- statusEl.textContent = msg;
- statusEl.className = `upload-status text-${success ? "success"
: "danger"}`;
- const border = success ? "border-success" : "border-danger";
- ui.querySelector(".card-body").classList.add(
- "border-start",
- border,
- "border-3",
- );
- if (success) {
- ui.querySelector("progress").value = 100;
- ui.querySelector(".upload-percent").textContent =
"100%";
+ showAmbiguousError() {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) statusEl.textContent = "Upload failed";
+ const cancelBtn = document.getElementById("upload-cancel-btn");
+ if (cancelBtn) cancelBtn.classList.add("d-none");
+ const msgArea = document.getElementById("upload-message-area");
+ if (msgArea) {
+ msgArea.innerHTML = "";
+ const alert = document.createElement("div");
+ alert.className = "alert alert-warning mt-3";
+ alert.textContent =
+ "Your files may already have been uploaded.
Check compose before retrying.";
+ msgArea.append(alert);
}
- ui.querySelector(".cancel-btn").classList.add("d-none");
},
- resetUploadUI(ui) {
- ui.querySelector(".card-body").classList.remove(
- "border-start",
- "border-danger",
- "border-3",
- );
- const statusEl = ui.querySelector(".upload-status");
- statusEl.textContent = "Preparing...";
- statusEl.className = "upload-status text-muted";
- ui.querySelector("progress").value = 0;
- ui.querySelector(".upload-percent").textContent = "0%";
- ui.querySelector(".cancel-btn").classList.remove("d-none");
+ showCancelled() {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) statusEl.textContent = "Upload cancelled";
+ const cancelBtn = document.getElementById("upload-cancel-btn");
+ if (cancelBtn) cancelBtn.classList.add("d-none");
},
- showAllFailedSummary(container) {
- const summary = document.createElement("div");
- summary.className = "alert alert-danger mt-3";
- summary.innerHTML = `<strong>All uploads failed.</strong>
- <button type="button" class="btn btn-outline-primary
ms-3" id="retry-all-btn">Try again</button>`;
- container.append(summary);
- document
- .getElementById("retry-all-btn")
- .addEventListener("click", () => location.reload());
+ showError(message, onRetry) {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) statusEl.textContent = "Upload failed";
+ const cancelBtn = document.getElementById("upload-cancel-btn");
+ if (cancelBtn) cancelBtn.classList.add("d-none");
+ const msgArea = document.getElementById("upload-message-area");
+ if (msgArea) {
+ msgArea.innerHTML = "";
+ const alert = document.createElement("div");
+ alert.className = "alert alert-danger mt-3";
+ const msgEl = document.createElement("span");
+ msgEl.textContent = message;
+ alert.append(msgEl);
+ if (onRetry) {
+ const retryBtn =
document.createElement("button");
+ retryBtn.type = "button";
+ retryBtn.className = "btn btn-outline-primary
ms-3";
+ retryBtn.textContent = "Try again";
+ retryBtn.addEventListener("click", onRetry);
+ alert.append(retryBtn);
+ }
+ msgArea.append(alert);
+ }
},
- showPartialSuccessSummary(container, completedCount, totalFiles,
onFinalise) {
- const summary = document.createElement("div");
- summary.className = "alert alert-warning mt-3";
- summary.id = "upload-summary";
- const totalWord = totalFiles === 1 ? "file" : "files";
- const stagedWord = completedCount === 1 ? "file" : "files";
- summary.innerHTML = `<strong>${completedCount} of ${totalFiles}
${totalWord} staged.</strong>
- <span class="ms-2">You can finalise with the staged
files or retry failed uploads.</span>
- <button type="button" class="btn btn-primary ms-3"
id="finalise-btn">Finalise ${completedCount} ${stagedWord}</button>`;
- container.append(summary);
+ showProcessing() {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) statusEl.textContent = "Processing revision";
+ const cancelBtn = document.getElementById("upload-cancel-btn");
+ if (cancelBtn) cancelBtn.classList.add("d-none");
document
- .getElementById("finalise-btn")
- .addEventListener("click", onFinalise);
+ .querySelectorAll("#upload-file-list
.upload-file-status")
+ .forEach((el) => {
+ el.textContent = "Done";
+ el.className = "upload-file-status
text-success";
+ });
},
- doFinalise(container, finaliseUrl, csrfToken) {
- const existingSummary =
document.getElementById("upload-summary");
- if (existingSummary) existingSummary.remove();
- const summary = document.createElement("div");
- summary.className = "alert alert-info mt-3";
- summary.innerHTML = `<strong>Finalising upload...</strong>`;
- container.append(summary);
- const form = document.createElement("form");
- form.method = "POST";
- form.action = finaliseUrl;
- form.style.display = "none";
- const csrfInput = document.createElement("input");
- csrfInput.type = "hidden";
- csrfInput.name = "csrf_token";
- csrfInput.value = csrfToken;
- form.append(csrfInput);
- document.body.append(form);
- form.submit();
+ showSuccess(message) {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) statusEl.textContent = "Upload complete";
+ const cancelBtn = document.getElementById("upload-cancel-btn");
+ if (cancelBtn) cancelBtn.classList.add("d-none");
+ const progressBar =
document.getElementById("upload-overall-progress");
+ if (progressBar) progressBar.value = 100;
+ const percentEl =
document.getElementById("upload-progress-percent");
+ if (percentEl) percentEl.textContent = "100%";
+ const msgArea = document.getElementById("upload-message-area");
+ if (msgArea) {
+ const alert = document.createElement("div");
+ alert.className = "alert alert-success mt-3";
+ alert.textContent = message;
+ msgArea.append(alert);
+ }
},
- handleUploadProgress(e, progressBar, statusEl, percentEl, cancelBtn) {
- if (e.lengthComputable) {
- const pct = Math.round((e.loaded / e.total) * 100);
- progressBar.value = pct;
- delete progressBar.dataset.indeterminate;
- percentEl.textContent = `${pct}%`;
- if (pct >= 100) {
- statusEl.textContent = "Processing...";
- cancelBtn.classList.add("d-none");
- } else {
- statusEl.textContent =
`${this.formatBytes(e.loaded)} / ${this.formatBytes(e.total)}`;
- }
- } else {
- progressBar.removeAttribute("value");
- progressBar.dataset.indeterminate = "true";
- percentEl.textContent = "";
- statusEl.textContent = `${this.formatBytes(e.loaded)}
uploaded`;
+ showUploading(fileCount) {
+ const statusEl = document.getElementById("upload-batch-status");
+ if (statusEl) {
+ const word = fileCount === 1 ? "file" : "files";
+ statusEl.textContent = `Uploading ${fileCount} ${word}`;
+ }
+ },
+
+ updateProgress(loaded, total, cumulative, totalFileBytes) {
+ let pct = 0;
+ if (total > 0) {
+ pct = loaded >= total ? 100 : Math.floor((loaded /
total) * 100);
+ }
+ const progressBar =
document.getElementById("upload-overall-progress");
+ const percentEl =
document.getElementById("upload-progress-percent");
+ const bytesEl =
document.getElementById("upload-progress-bytes");
+
+ if (progressBar) progressBar.value = pct;
+ if (percentEl) percentEl.textContent = `${pct}%`;
+ if (bytesEl) {
+ bytesEl.textContent = `${this.formatBytes(loaded)} /
${this.formatBytes(total)}`;
+ }
+
+ if (totalFileBytes > 0) {
+ const scaled = loaded * (totalFileBytes / total);
+ document
+ .querySelectorAll("#upload-file-list
[data-file-index]")
+ .forEach((row, i) => {
+ const start = i > 0 ? cumulative[i - 1]
: 0;
+ const end = cumulative[i];
+ const statusSpan =
row.querySelector(".upload-file-status");
+ if (!statusSpan) return;
+ if (scaled >= end) {
+ statusSpan.textContent = "Done";
+ statusSpan.className =
"upload-file-status text-success";
+ } else if (scaled > start) {
+ statusSpan.textContent =
"Sending";
+ statusSpan.className =
"upload-file-status text-primary";
+ } else {
+ statusSpan.textContent =
"Pending";
+ statusSpan.className =
"upload-file-status text-muted";
+ }
+ });
}
},
};
diff --git a/atr/static/js/src/upload-progress.js
b/atr/static/js/src/upload-progress.js
index c1625502..1bc21949 100644
--- a/atr/static/js/src/upload-progress.js
+++ b/atr/static/js/src/upload-progress.js
@@ -17,227 +17,102 @@
* under the License.
*/
-function handleXhrLoad(xhr, ctx) {
- ctx.activeUploads.delete(ctx.index);
- if (xhr.status >= 200 && xhr.status < 300) {
- UploadUI.markUploadDone(ctx.ui, true, "Staged");
- ctx.state.completedCount++;
- } else {
- let msg = "Upload failed";
+(() => {
+ function computeCumulative(files) {
+ const cumulative = [];
+ let sum = 0;
+ for (const file of files) {
+ sum += file.size;
+ cumulative.push(sum);
+ }
+ return { cumulative, totalFileBytes: sum };
+ }
+
+ function handleLoad(xhr, retry) {
+ let data;
try {
- msg = JSON.parse(xhr.responseText).error || msg;
+ data = JSON.parse(xhr.responseText);
} catch {
- /* ignore */
+ UploadUI.showError(`Unexpected server response
(${xhr.status})`, retry);
+ return;
}
- UploadUI.markUploadDone(ctx.ui, false, `Failed: ${msg}`);
- ctx.state.failedCount++;
- ctx.addRetryButton(ctx.ui, ctx.index);
- }
- ctx.checkAllComplete();
-}
-function handleXhrFailure(ctx, reason, isCancelled) {
- ctx.activeUploads.delete(ctx.index);
- if (isCancelled) {
- ctx.statusEl.textContent = "Cancelled";
- ctx.statusEl.className = "upload-status text-secondary";
- ctx.cancelBtn.classList.add("d-none");
- } else {
- UploadUI.markUploadDone(ctx.ui, false, `Failed: ${reason}`);
- ctx.addRetryButton(ctx.ui, ctx.index);
+ if (data.ok) {
+ UploadUI.showSuccess(data.message || "Upload complete");
+ if (data.next_url) {
+ window.location.assign(data.next_url);
+ }
+ } else {
+ UploadUI.showError(data.message || "Upload failed",
retry);
+ }
}
- ctx.state.failedCount++;
- ctx.checkAllComplete();
-}
-function setupXhrHandlers(
- xhr,
- ui,
- index,
- state,
- addRetryButtonFn,
- checkAllCompleteFn,
-) {
- const ctx = {
- ui,
- index,
- state,
- addRetryButton: addRetryButtonFn,
- checkAllComplete: checkAllCompleteFn,
- activeUploads: state.activeUploads,
- statusEl: ui.querySelector(".upload-status"),
- cancelBtn: ui.querySelector(".cancel-btn"),
- };
- const progressBar = ui.querySelector("progress");
- const percentEl = ui.querySelector(".upload-percent");
+ function startUpload(form, container, files, formData) {
+ const { cumulative, totalFileBytes } = computeCumulative(files);
+ let reachedFull = false;
+ const xhr = new XMLHttpRequest();
- xhr.upload.addEventListener("progress", (e) =>
- UploadUI.handleUploadProgress(
- e,
- progressBar,
- ctx.statusEl,
- percentEl,
- ctx.cancelBtn,
- ),
- );
- xhr.addEventListener("load", () => handleXhrLoad(xhr, ctx));
- xhr.addEventListener("error", () =>
- handleXhrFailure(ctx, "Network error", false),
- );
- xhr.addEventListener("abort", () => handleXhrFailure(ctx, null, true));
-}
+ function retry() {
+ form.classList.remove("d-none");
+ container.classList.add("d-none");
+ }
-function checkAllComplete(state, container, finaliseUrl, csrfToken) {
- if (state.activeUploads.size > 0) return;
- if (state.completedCount === 0) {
- UploadUI.showAllFailedSummary(container);
- } else if (state.failedCount > 0) {
- UploadUI.showPartialSuccessSummary(
- container,
- state.completedCount,
- state.totalFiles,
- () => UploadUI.doFinalise(container, finaliseUrl,
csrfToken),
- );
- } else {
- UploadUI.doFinalise(container, finaliseUrl, csrfToken);
- }
-}
+ UploadUI.buildBatchUI(container, files, () => xhr.abort());
+ UploadUI.showUploading(files.length);
-function startUpload(
- file,
- index,
- state,
- stageUrl,
- csrfToken,
- addRetryButtonFn,
- checkComplete,
-) {
- const ui = document.getElementById(`upload-file-${index}`);
- const xhr = new XMLHttpRequest();
- state.activeUploads.set(index, xhr);
- setupXhrHandlers(xhr, ui, index, state, addRetryButtonFn,
checkComplete);
- const formData = new FormData();
- formData.append("csrf_token", csrfToken);
- formData.append("file", file, file.name);
- xhr.open("POST", stageUrl, true);
- xhr.send(formData);
- ui.querySelector(".upload-status").textContent = "Uploading...";
-}
+ xhr.upload.addEventListener("progress", (ev) => {
+ if (ev.lengthComputable) {
+ if (ev.loaded >= ev.total) {
+ reachedFull = true;
+ UploadUI.showProcessing();
+ }
+ UploadUI.updateProgress(
+ ev.loaded,
+ ev.total,
+ cumulative,
+ totalFileBytes,
+ );
+ }
+ });
-function addRetryButton(
- ui,
- index,
- state,
- fileInput,
- stageUrl,
- csrfToken,
- checkComplete,
-) {
- const retryBtn = document.createElement("button");
- retryBtn.type = "button";
- retryBtn.className = "btn btn-sm btn-outline-primary retry-btn ms-2";
- retryBtn.textContent = "Retry";
- retryBtn.addEventListener("click", () => {
- UploadUI.resetUploadUI(ui);
- retryBtn.remove();
- state.failedCount--;
- const file = Array.from(fileInput.files || [])[index];
- startUpload(
- file,
- index,
- state,
- stageUrl,
- csrfToken,
- addRetryButton,
- checkComplete,
- );
- });
- ui.querySelector(".upload-status").parentNode.append(retryBtn);
-}
+ xhr.addEventListener("load", () => handleLoad(xhr, retry));
-function handleFormSubmit(
- e,
- form,
- fileInput,
- container,
- state,
- stageUrl,
- csrfToken,
- checkComplete,
-) {
- e.preventDefault();
- const files = Array.from(fileInput.files || []);
- if (files.length === 0) {
- alert("Please select files to upload.");
- return;
- }
- state.totalFiles = files.length;
- state.completedCount = 0;
- state.failedCount = 0;
- form.classList.add("d-none");
- container.classList.remove("d-none");
- container.innerHTML = "";
- files.forEach((file, index) => {
- container.append(
- UploadUI.createFileProgressUI(file, index,
state.activeUploads),
- );
- startUpload(
- file,
- index,
- state,
- stageUrl,
- csrfToken,
- (ui, idx) =>
- addRetryButton(
- ui,
- idx,
- state,
- fileInput,
- stageUrl,
- csrfToken,
- checkComplete,
- ),
- checkComplete,
- );
- });
-}
+ xhr.addEventListener("error", () => {
+ if (reachedFull) {
+ UploadUI.showAmbiguousError();
+ } else {
+ UploadUI.showError("A network error occurred.",
retry);
+ }
+ });
-(() => {
- const config = document.getElementById("upload-config");
- if (!config) return;
+ xhr.addEventListener("abort", () => {
+ UploadUI.showCancelled();
+ form.classList.remove("d-none");
+ });
- const stageUrl = config.dataset.stageUrl;
- const finaliseUrl = config.dataset.finaliseUrl;
- if (!stageUrl || !finaliseUrl) return;
+ xhr.open("POST", form.action, true);
+ xhr.setRequestHeader("Accept", "application/json");
+ xhr.send(formData);
+ }
const form = document
.querySelector('input[name="variant"][value="add_files"]')
?.closest("form");
if (!form) return;
- const fileInput = form.querySelector('input[type="file"]');
+ const fileInput = form.querySelector('input[name="file_data"]');
const container = document.getElementById("upload-progress-container");
- const csrfToken = form.querySelector('input[name="csrf_token"]')?.value;
- const state = {
- activeUploads: new Map(),
- completedCount: 0,
- failedCount: 0,
- totalFiles: 0,
- };
-
- const checkComplete = () =>
- checkAllComplete(state, container, finaliseUrl, csrfToken);
+ if (!fileInput || !container) return;
- form.addEventListener("submit", (e) =>
- handleFormSubmit(
- e,
- form,
- fileInput,
- container,
- state,
- stageUrl,
- csrfToken,
- checkComplete,
- ),
- );
+ form.addEventListener("submit", (e) => {
+ e.preventDefault();
+ const files = Array.from(fileInput.files || []);
+ if (files.length === 0) {
+ alert("Please select files to upload.");
+ return;
+ }
+ const formData = new FormData(form);
+ form.classList.add("d-none");
+ startUpload(form, container, files, formData);
+ });
})();
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]