feat(patching/import): filtres client-side asset/intervenant/env sur tableau semaine

This commit is contained in:
Pierre & Lumière 2026-05-04 13:04:05 +02:00
parent 557015325b
commit 13a5625710

View File

@ -106,9 +106,19 @@
{# Tableau dynamique #} {# Tableau dynamique #}
<div id="sheet-table-wrap" class="overflow-x-auto" style="display:none;"> <div id="sheet-table-wrap" class="overflow-x-auto" style="display:none;">
{# Filtres client-side #}
<div class="flex gap-2 items-center mb-2 flex-wrap">
<input type="text" id="filter-asset" placeholder="Filtre asset…" class="text-xs px-2 py-1" style="width:160px">
<input type="text" id="filter-intervenant" placeholder="Filtre intervenant…" class="text-xs px-2 py-1" style="width:160px">
<select id="filter-env" class="text-xs px-2 py-1">
<option value="">— Tous environnements —</option>
</select>
<button id="filter-reset" class="text-xs text-gray-500 hover:text-cyber-accent">Reset</button>
<span class="text-xs text-gray-400" id="filter-count"></span>
</div>
<div class="flex gap-2 items-center mb-2 flex-wrap"> <div class="flex gap-2 items-center mb-2 flex-wrap">
<label class="text-xs"> <label class="text-xs">
<input type="checkbox" id="select-all" class="mr-1"> Tout sélectionner <input type="checkbox" id="select-all" class="mr-1"> Tout sélectionner (visibles)
</label> </label>
<span class="text-xs text-gray-400" id="selection-count">0 sélectionné(s)</span> <span class="text-xs text-gray-400" id="selection-count">0 sélectionné(s)</span>
<div class="flex-1"></div> <div class="flex-1"></div>
@ -156,6 +166,13 @@
const selCount = document.getElementById('selection-count'); const selCount = document.getElementById('selection-count');
const btnPre = document.getElementById('btn-prepatch'); const btnPre = document.getElementById('btn-prepatch');
const btnPatch = document.getElementById('btn-patch'); const btnPatch = document.getElementById('btn-patch');
const fAsset = document.getElementById('filter-asset');
const fInter = document.getElementById('filter-intervenant');
const fEnv = document.getElementById('filter-env');
const fReset = document.getElementById('filter-reset');
const fCount = document.getElementById('filter-count');
let currentRows = [];
function escapeHTML(s){ function escapeHTML(s){
if (s === null || s === undefined) return ''; if (s === null || s === undefined) return '';
@ -163,18 +180,44 @@
} }
function refreshSelection(){ function refreshSelection(){
const visible = tbody.querySelectorAll('tr:not(.row-hidden)');
const visibleCb = tbody.querySelectorAll('tr:not(.row-hidden) input.row-cb');
const checked = tbody.querySelectorAll('input.row-cb:checked').length; const checked = tbody.querySelectorAll('input.row-cb:checked').length;
const total = tbody.querySelectorAll('input.row-cb').length; selCount.textContent = checked + ' sélectionné(s) · ' + visible.length + ' visible(s)';
selCount.textContent = checked + ' / ' + total + ' sélectionné(s)'; const allVisibleChecked = visibleCb.length > 0 && Array.from(visibleCb).every(cb => cb.checked);
selAll.checked = (checked > 0 && checked === total); selAll.checked = allVisibleChecked;
selAllHead.checked = selAll.checked; selAllHead.checked = allVisibleChecked;
// Pré-patching et patch désactivés tant que les étapes 2/3 ne sont pas faites
// mais on prépare la condition pour la suite :
const hasSel = checked > 0; const hasSel = checked > 0;
btnPre.disabled = !hasSel; btnPre.disabled = !hasSel;
btnPatch.disabled = !hasSel; btnPatch.disabled = !hasSel;
} }
function applyFilters(){
const fa = (fAsset.value || '').toLowerCase().trim();
const fi = (fInter.value || '').toLowerCase().trim();
const fe = (fEnv.value || '').trim();
let visibleCount = 0;
tbody.querySelectorAll('tr').forEach(tr => {
const a = (tr.dataset.asset || '').toLowerCase();
const i = (tr.dataset.intervenant || '').toLowerCase();
const e = tr.dataset.env || '';
const ok = (!fa || a.includes(fa))
&& (!fi || i.includes(fi))
&& (!fe || e === fe);
if (ok) { tr.classList.remove('row-hidden'); tr.style.display=''; visibleCount++; }
else { tr.classList.add('row-hidden'); tr.style.display='none'; }
});
fCount.textContent = visibleCount + ' / ' + currentRows.length + ' affichées';
refreshSelection();
}
function rebuildEnvOptions(rows){
const envs = Array.from(new Set(rows.map(r => r.environnement).filter(x => x))).sort();
const cur = fEnv.value;
fEnv.innerHTML = '<option value="">— Tous environnements —</option>'
+ envs.map(e => '<option value="' + escapeHTML(e) + '"' + (cur === e ? ' selected' : '') + '>' + escapeHTML(e) + '</option>').join('');
}
async function loadSheet(name){ async function loadSheet(name){
if (!name) { wrap.style.display='none'; empty.style.display='none'; return; } if (!name) { wrap.style.display='none'; empty.style.display='none'; return; }
summary.textContent = 'Chargement…'; summary.textContent = 'Chargement…';
@ -187,12 +230,17 @@
} }
empty.style.display='none'; wrap.style.display=''; empty.style.display='none'; wrap.style.display='';
summary.textContent = j.count + ' lignes'; summary.textContent = j.count + ' lignes';
currentRows = j.rows;
rebuildEnvOptions(currentRows);
tbody.innerHTML = j.rows.map(r => { tbody.innerHTML = j.rows.map(r => {
const linkSrv = r.server_id const linkSrv = r.server_id
? '<a href="/qualys/search?field=hostname&q=' + encodeURIComponent(r.resolved_hostname || r.asset_name) + '" class="text-cyber-blue hover:underline">' + escapeHTML(r.resolved_hostname || r.asset_name) + '</a>' ? '<a href="/qualys/search?field=hostname&q=' + encodeURIComponent(r.resolved_hostname || r.asset_name) + '" class="text-cyber-blue hover:underline">' + escapeHTML(r.resolved_hostname || r.asset_name) + '</a>'
: '<span class="text-cyber-yellow" title="Pas matché en base PatchCenter">' + escapeHTML(r.asset_name || '') + ' ⚠</span>'; : '<span class="text-cyber-yellow" title="Pas matché en base PatchCenter">' + escapeHTML(r.asset_name || '') + ' ⚠</span>';
const pb = r.pb_espace_disque === true ? '<span class="text-cyber-red">⚠ Oui</span>' : (r.pb_espace_disque === false ? 'Non' : ''); const pb = r.pb_espace_disque === true ? '<span class="text-cyber-red">⚠ Oui</span>' : (r.pb_espace_disque === false ? 'Non' : '');
return '<tr class="border-b border-cyber-border/20 hover:bg-cyber-border/10">' return '<tr class="border-b border-cyber-border/20 hover:bg-cyber-border/10"'
+ ' data-asset="' + escapeHTML(r.asset_name||'') + '"'
+ ' data-intervenant="' + escapeHTML(r.intervenant||'') + '"'
+ ' data-env="' + escapeHTML(r.environnement||'') + '">'
+ '<td class="p-1"><input type="checkbox" class="row-cb" data-id="' + r.id + '" data-asset="' + escapeHTML(r.asset_name||'') + '" data-server-id="' + (r.server_id||'') + '"></td>' + '<td class="p-1"><input type="checkbox" class="row-cb" data-id="' + r.id + '" data-asset="' + escapeHTML(r.asset_name||'') + '" data-server-id="' + (r.server_id||'') + '"></td>'
+ '<td class="p-1 font-mono">' + escapeHTML(r.asset_name||'') + '</td>' + '<td class="p-1 font-mono">' + escapeHTML(r.asset_name||'') + '</td>'
+ '<td class="p-1">' + escapeHTML(r.environnement||'') + '</td>' + '<td class="p-1">' + escapeHTML(r.environnement||'') + '</td>'
@ -209,17 +257,24 @@
+ '</tr>'; + '</tr>';
}).join(''); }).join('');
tbody.querySelectorAll('input.row-cb').forEach(cb => cb.addEventListener('change', refreshSelection)); tbody.querySelectorAll('input.row-cb').forEach(cb => cb.addEventListener('change', refreshSelection));
refreshSelection(); applyFilters();
} }
sel.addEventListener('change', () => loadSheet(sel.value)); sel.addEventListener('change', () => loadSheet(sel.value));
function toggleAll(state){ function toggleAll(state){
tbody.querySelectorAll('input.row-cb').forEach(cb => cb.checked = state); tbody.querySelectorAll('tr:not(.row-hidden) input.row-cb').forEach(cb => cb.checked = state);
refreshSelection(); refreshSelection();
} }
selAll.addEventListener('change', () => toggleAll(selAll.checked)); selAll.addEventListener('change', () => toggleAll(selAll.checked));
selAllHead.addEventListener('change', () => toggleAll(selAllHead.checked)); selAllHead.addEventListener('change', () => toggleAll(selAllHead.checked));
[fAsset, fInter, fEnv].forEach(el => el.addEventListener('input', applyFilters));
fEnv.addEventListener('change', applyFilters);
fReset.addEventListener('click', () => {
fAsset.value = ''; fInter.value = ''; fEnv.value = '';
applyFilters();
});
btnPre.addEventListener('click', () => { btnPre.addEventListener('click', () => {
const ids = Array.from(tbody.querySelectorAll('input.row-cb:checked')).map(cb => cb.dataset.serverId).filter(x => x); const ids = Array.from(tbody.querySelectorAll('input.row-cb:checked')).map(cb => cb.dataset.serverId).filter(x => x);
alert('Pré-patching à brancher (étape 2) — ' + ids.length + ' serveur(s) résolu(s) en base.'); alert('Pré-patching à brancher (étape 2) — ' + ids.length + ' serveur(s) résolu(s) en base.');