Add rate limiting to job action buttons

- Prevent multiple clicks on same job action buttons
- Disable button and show 'Processing...' while request is pending
- Re-enable button after response or 10-second timeout
- Track pending actions to prevent duplicate requests
parent ff667b1c
......@@ -32,7 +32,9 @@
let lastCompletedJobs = new Set(); // Track jobs that were already completed
function updateJobStatuses() {
fetch('/api/job_status_updates?since=' + lastUpdate)
// Add random parameter to avoid browser caching
const randomParam = Math.random().toString(36).substring(7);
fetch('/api/job_status_updates?since=' + lastUpdate + '&_=' + randomParam)
.then(response => response.json())
.then(data => {
if (data.updates && data.updates.length > 0) {
......@@ -122,6 +124,9 @@
}
}
// Track pending actions to prevent multiple clicks
const pendingActions = new Set();
function updateJobActions(jobId, status) {
const jobRow = document.querySelector(`[data-job-id="${jobId}"]`);
if (!jobRow) return;
......@@ -150,7 +155,7 @@
cancelBtn.style.cursor = 'pointer';
cancelBtn.style.marginRight = '0.5rem';
cancelBtn.textContent = 'Cancel';
cancelBtn.onclick = () => performJobAction(jobId, 'cancel');
cancelBtn.onclick = () => performJobAction(jobId, 'cancel', cancelBtn);
actionsElement.appendChild(cancelBtn);
} else if (status === 'cancelled') {
......@@ -165,7 +170,7 @@
restartBtn.style.cursor = 'pointer';
restartBtn.style.marginRight = '0.5rem';
restartBtn.textContent = 'Restart';
restartBtn.onclick = () => performJobAction(jobId, 'restart');
restartBtn.onclick = () => performJobAction(jobId, 'restart', restartBtn);
actionsElement.appendChild(restartBtn);
......@@ -179,7 +184,7 @@
deleteBtn.style.fontSize = '0.8rem';
deleteBtn.style.cursor = 'pointer';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => performJobAction(jobId, 'delete');
deleteBtn.onclick = () => performJobAction(jobId, 'delete', deleteBtn);
actionsElement.appendChild(deleteBtn);
} else if (status === 'queued') {
......@@ -193,7 +198,7 @@
deleteBtn.style.fontSize = '0.8rem';
deleteBtn.style.cursor = 'pointer';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => performJobAction(jobId, 'delete');
deleteBtn.onclick = () => performJobAction(jobId, 'delete', deleteBtn);
actionsElement.appendChild(deleteBtn);
} else if (status === 'failed') {
......@@ -207,13 +212,34 @@
deleteBtn.style.fontSize = '0.8rem';
deleteBtn.style.cursor = 'pointer';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => performJobAction(jobId, 'delete');
deleteBtn.onclick = () => performJobAction(jobId, 'delete', deleteBtn);
actionsElement.appendChild(deleteBtn);
}
}
function performJobAction(jobId, action) {
function performJobAction(jobId, action, buttonElement) {
const actionKey = `${jobId}-${action}`;
// Prevent multiple clicks on the same action
if (pendingActions.has(actionKey)) {
return;
}
// Disable the button and mark as pending
buttonElement.disabled = true;
buttonElement.textContent = 'Processing...';
pendingActions.add(actionKey);
// Set a timeout to re-enable the button after 10 seconds (in case of network issues)
const timeoutId = setTimeout(() => {
if (pendingActions.has(actionKey)) {
pendingActions.delete(actionKey);
buttonElement.disabled = false;
buttonElement.textContent = action.charAt(0).toUpperCase() + action.slice(1);
}
}, 10000);
const formData = new FormData();
formData.append('_method', 'POST');
......@@ -226,6 +252,8 @@
redirect: 'manual' // Don't follow redirects
})
.then(response => {
clearTimeout(timeoutId); // Clear the timeout since we got a response
if (response.status >= 200 && response.status < 300) {
// Success status
return response.json().then(data => {
......@@ -244,8 +272,15 @@
}
})
.catch(error => {
clearTimeout(timeoutId); // Clear the timeout on error
console.log(`Error performing ${action} on job ${jobId}:`, error);
showActionNotification(jobId, action, 'error');
})
.finally(() => {
// Re-enable the button and remove from pending actions
pendingActions.delete(actionKey);
buttonElement.disabled = false;
buttonElement.textContent = action.charAt(0).toUpperCase() + action.slice(1);
});
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment