genmod / application_neo4j /static /js /search_progress.js
pbordescnil's picture
Remove queue waiting timer
4ede66b
Raw
History Blame Contribute Delete
10.6 kB
(() => {
const POLL_INTERVAL_MS = 750;
let activeCancelUrl = null;
function translate(key, fallback) {
return window.__I18N_DATA?.[key] || fallback;
}
function format(template, values) {
return Object.entries(values).reduce(
(text, [key, value]) => text.replace(`{${key}}`, String(value)),
template
);
}
function stageLabel(stage) {
const labels = {
queued: ["search.progress_stage_queued", "En attente du serveur…"],
preparing_graph: ["search.progress_stage_preparing", "Préparation du graphe…"],
searching_descendants: ["search.progress_stage_descendants", "Recherche des descendants…"],
searching_ancestors: ["search.progress_stage_ancestors", "Recherche des ascendants…"],
building_result: ["search.progress_stage_result", "Construction du résultat…"],
building_nodes: ["search.progress_stage_nodes", "Analyse des nœuds du résultat…"],
building_relationships: ["search.progress_stage_relationships", "Chargement des relations…"],
formatting_result: ["search.progress_stage_formatting", "Mise en forme du résultat…"],
building_highlights: ["search.progress_stage_highlights", "Calcul des modèles importants…"],
completed: ["search.progress_stage_completed", "Recherche terminée."],
failed: ["search.progress_stage_failed", "La recherche a échoué."],
cancelled: ["search.progress_stage_cancelled", "Recherche annulée."],
};
const [key, fallback] = labels[stage] || labels.queued;
return translate(key, fallback);
}
function findProgressPanel(form) {
return form.parentElement.querySelector(".search-progress");
}
function setSubmitting(form, submitting) {
form.dataset.submitting = submitting ? "true" : "false";
const submitButton = form.querySelector('button[type="submit"]');
if (submitButton) submitButton.disabled = submitting;
}
function showError(form, progress, message) {
const stage = progress.querySelector(".search-progress-stage");
const status = progress.querySelector(".search-progress-status");
const bar = progress.querySelector(".search-progress-bar");
stage.textContent = translate(
"search.progress_stage_failed",
"La recherche a échoué."
);
status.textContent = message || translate(
"search.progress_connection_error",
"Impossible d’obtenir la progression depuis le serveur."
);
bar.style.width = "0%";
progress.classList.remove("alert-info");
progress.classList.add("alert-danger");
setSubmitting(form, false);
}
function showCancelled(form, progress) {
const stage = progress.querySelector(".search-progress-stage");
const status = progress.querySelector(".search-progress-status");
const note = progress.querySelector(".search-progress-note");
const bar = progress.querySelector(".search-progress-bar");
stage.textContent = stageLabel("cancelled");
status.textContent = "";
note.textContent = "";
bar.style.width = "0%";
progress.classList.remove("alert-info", "alert-danger");
progress.classList.add("alert-secondary");
setSubmitting(form, false);
}
function cancelActiveSearch() {
if (!activeCancelUrl) return;
const cancelUrl = activeCancelUrl;
activeCancelUrl = null;
if (
navigator.sendBeacon
&& navigator.sendBeacon(
cancelUrl,
new Blob([], { type: "text/plain" })
)
) {
return;
}
fetch(cancelUrl, {
method: "POST",
credentials: "same-origin",
keepalive: true,
}).catch(() => {});
}
function renderStatus(progress, job) {
const title = progress.querySelector(".search-progress-title");
const stage = progress.querySelector(".search-progress-stage");
const status = progress.querySelector(".search-progress-status");
const note = progress.querySelector(".search-progress-note");
const bar = progress.querySelector(".search-progress-bar");
stage.textContent = stageLabel(job.stage);
if (job.status === "queued" || job.stage === "queued") {
title.textContent = translate(
"search.progress_queue_title",
"Recherche en file d’attente"
);
progress.classList.remove("alert-info", "alert-danger", "alert-secondary");
progress.classList.add("alert-warning");
const queueDetails = [];
if (job.queue_position && job.queued_jobs) {
queueDetails.push(
format(
translate(
"search.progress_queue_position",
"Position dans la file : {position}/{total}"
),
{
position: job.queue_position,
total: job.queued_jobs,
}
)
);
const jobsAhead = job.queue_position - 1;
if (jobsAhead === 0) {
queueDetails.push(
translate(
"search.progress_queue_next",
"Votre recherche est la prochaine."
)
);
} else {
queueDetails.push(
format(
translate(
jobsAhead === 1
? "search.progress_queue_ahead_one"
: "search.progress_queue_ahead_many",
jobsAhead === 1
? "1 recherche devant la vôtre"
: "{count} recherches devant la vôtre"
),
{ count: jobsAhead }
)
);
}
}
if (job.running_jobs) {
queueDetails.push(
format(
translate(
job.running_jobs === 1
? "search.progress_queue_running_one"
: "search.progress_queue_running_many",
job.running_jobs === 1
? "1 recherche en cours"
: "{count} recherches en cours"
),
{ count: job.running_jobs }
)
);
}
status.textContent = queueDetails.join(" · ");
note.textContent = translate(
"search.progress_queue_note",
"La recherche démarrera automatiquement dès qu’une capacité sera disponible."
);
bar.style.width = "0%";
bar.removeAttribute("aria-valuenow");
return;
}
title.textContent = translate(
"search.progress_title",
"Recherche en cours…"
);
progress.classList.remove("alert-warning", "alert-danger", "alert-secondary");
progress.classList.add("alert-info");
const details = [
format(
translate("search.progress_elapsed", "Temps écoulé : {seconds} s"),
{ seconds: job.elapsed_seconds }
),
];
if (job.completed_items !== null && job.total_items) {
details.push(
format(
translate(
"search.progress_items",
"Éléments traités par le serveur : {completed}/{total}"
),
{
completed: job.completed_items,
total: job.total_items,
}
)
);
}
if (job.progress_percent !== null) {
details.push(
format(
translate(
"search.progress_server_percent",
"Progression indiquée par le serveur : {percent} %"
),
{ percent: job.progress_percent }
)
);
bar.style.width = `${job.progress_percent}%`;
bar.setAttribute("aria-valuenow", String(job.progress_percent));
} else {
bar.style.width = "0%";
bar.removeAttribute("aria-valuenow");
}
if (job.remaining_seconds !== null) {
details.push(
format(
translate(
"search.progress_remaining_step",
"Temps restant estimé pour cette étape : {seconds} s"
),
{ seconds: job.remaining_seconds }
)
);
}
status.textContent = details.join(" · ");
note.textContent = translate(
"search.progress_server_note",
"La progression repose sur les opérations réellement terminées par le serveur."
);
}
async function readJson(response) {
const body = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(body.error || `${response.status} ${response.statusText}`);
}
return body;
}
async function pollJob(form, progress, statusUrl) {
try {
const job = await readJson(await fetch(statusUrl, {
credentials: "same-origin",
cache: "no-store",
}));
renderStatus(progress, job);
if (job.status === "cancelled") {
activeCancelUrl = null;
showCancelled(form, progress);
return;
}
if (job.result_url) {
activeCancelUrl = null;
window.location.assign(job.result_url);
return;
}
window.setTimeout(() => pollJob(form, progress, statusUrl), POLL_INTERVAL_MS);
} catch (error) {
cancelActiveSearch();
showError(form, progress, error.message);
}
}
async function startSearch(form, progress) {
const formData = new FormData(form);
const endpoint = form.dataset.searchJobUrl || "/api/search-jobs";
progress.classList.remove("d-none", "alert-danger");
progress.classList.add("alert-info");
progress.querySelector(".search-progress-title").textContent = translate(
"search.progress_title",
"Recherche en cours…"
);
renderStatus(progress, {
stage: "queued",
elapsed_seconds: 0,
progress_percent: null,
remaining_seconds: null,
queue_position: null,
queued_jobs: null,
running_jobs: null,
completed_items: null,
total_items: null,
});
setSubmitting(form, true);
try {
const job = await readJson(await fetch(endpoint, {
method: "POST",
body: formData,
credentials: "same-origin",
headers: { Accept: "application/json" },
}));
activeCancelUrl = job.cancel_url;
pollJob(form, progress, job.status_url);
} catch (error) {
showError(form, progress, error.message);
}
}
document.addEventListener("DOMContentLoaded", () => {
document.querySelectorAll("form[data-search-progress]").forEach((form) => {
form.addEventListener("submit", (event) => {
event.preventDefault();
if (form.dataset.submitting === "true" || !form.reportValidity()) return;
const progress = findProgressPanel(form);
if (progress) startSearch(form, progress);
});
});
});
window.addEventListener("pagehide", cancelActiveSearch);
})();