[FEAT] Inventario: ordinamento lista per colonna (#442)
Click su un'intestazione (Nome/Tipo/Categoria/Criticita/Rilevanza NIS2/Owner/Stato) ordina l'inventario; secondo click inverte (freccia ▲/▼, aria-sort). Criticita e Rilevanza ordinano per valore reale, non alfabetico. Client-side sull'inventario gia' caricato (no paginazione su assets), si combina coi filtri, accessibile da tastiera. Help aggiornato + cache-buster help.js. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
87fc5bf959
commit
ab3e88299e
+61
-12
@@ -105,6 +105,13 @@
|
||||
.data-table tr:last-child td { border-bottom: none; }
|
||||
.data-table tbody tr { cursor: pointer; transition: background var(--transition-fast); }
|
||||
.data-table tbody tr:hover td { background: var(--gray-50); }
|
||||
/* ── Intestazioni ordinabili (click per ordinare la lista) ── */
|
||||
.data-table th.sortable-th { cursor: pointer; user-select: none; white-space: nowrap; transition: background var(--transition-fast), color var(--transition-fast); }
|
||||
.data-table th.sortable-th:hover { background: var(--gray-100); color: var(--gray-700); }
|
||||
.data-table th.sortable-th:focus-visible { outline: 2px solid var(--primary, #0066CC); outline-offset: -2px; }
|
||||
.data-table th.sortable-th.sorted { color: var(--gray-700); }
|
||||
.data-table th.sortable-th .sort-ind { font-size: 0.7rem; opacity: 0.35; margin-left: 3px; }
|
||||
.data-table th.sortable-th .sort-ind.active { opacity: 1; color: var(--primary, #0066CC); }
|
||||
|
||||
/* ── Criticality Badges ─────────────────────────────────── */
|
||||
.criticality-badge {
|
||||
@@ -376,7 +383,7 @@
|
||||
</script>
|
||||
<script src="/js/common-bi.js?v=20260624f"></script>
|
||||
<script src="/js/i18n.js?v=20260631"></script>
|
||||
<script src="/js/help.js?v=20260625b"></script>
|
||||
<script src="/js/help.js?v=20260625c"></script>
|
||||
<script>
|
||||
// ── Auth & Init ─────────────────────────────────────────
|
||||
if (!checkAuth()) throw new Error('Not authenticated');
|
||||
@@ -535,8 +542,57 @@
|
||||
).join('');
|
||||
}
|
||||
|
||||
// ── Ordinamento colonne (client-side, sull'inventario gia' caricato) ──
|
||||
// L'inventario e' caricato per intero lato client (nessuna paginazione),
|
||||
// quindi l'ordinamento sull'array e' completo e non parziale.
|
||||
let lastView = [];
|
||||
let sortState = { key: null, dir: 1 }; // dir: 1 = crescente, -1 = decrescente
|
||||
const ASSET_COLUMNS = [
|
||||
{ key: 'name', label: 'Nome', type: 'text', val: a => (a.name || '') },
|
||||
{ key: 'type', label: 'Tipo', type: 'text', val: a => assetTipo(a) },
|
||||
{ key: 'category', label: 'Categoria', type: 'text', val: a => (a.category || '') },
|
||||
{ key: 'criticality', label: "Criticita'", type: 'num', val: a => ({ low: 1, medium: 2, high: 3, critical: 4 }[a.criticality || 'medium'] || 0) },
|
||||
{ key: 'relevance', label: 'Rilevanza NIS2', type: 'num', val: a => (a.relevance_score == null ? -1 : Number(a.relevance_score)) },
|
||||
{ key: 'owner', label: 'Owner', type: 'text', val: a => (a.owner_name || '') },
|
||||
{ key: 'status', label: 'Stato', type: 'text', val: a => (a.status || '') }
|
||||
];
|
||||
|
||||
function sortableTh(c) {
|
||||
const active = sortState.key === c.key;
|
||||
const ariaSort = active ? (sortState.dir === 1 ? 'ascending' : 'descending') : 'none';
|
||||
const ind = active ? (sortState.dir === 1 ? '▲' : '▼') : '⇅';
|
||||
const indClass = active ? 'sort-ind active' : 'sort-ind';
|
||||
return `<th class="sortable-th${active ? ' sorted' : ''}" role="button" tabindex="0" aria-sort="${ariaSort}" `
|
||||
+ `title="Ordina per ${escapeHtml(c.label)}" onclick="sortBy('${c.key}')" `
|
||||
+ `onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();sortBy('${c.key}');}">`
|
||||
+ `${escapeHtml(c.label)} <span class="${indClass}" aria-hidden="true">${ind}</span></th>`;
|
||||
}
|
||||
|
||||
function sortAssetsView(assets) {
|
||||
if (!sortState.key) return assets;
|
||||
const col = ASSET_COLUMNS.find(c => c.key === sortState.key);
|
||||
if (!col) return assets;
|
||||
const copy = assets.slice();
|
||||
copy.sort((a, b) => {
|
||||
const va = col.val(a), vb = col.val(b);
|
||||
let cmp;
|
||||
if (col.type === 'num') cmp = (Number(va) - Number(vb));
|
||||
else cmp = String(va).localeCompare(String(vb), 'it', { numeric: true, sensitivity: 'base' });
|
||||
if (cmp === 0) return 0;
|
||||
return cmp * sortState.dir;
|
||||
});
|
||||
return copy;
|
||||
}
|
||||
|
||||
function sortBy(key) {
|
||||
if (sortState.key === key) sortState.dir *= -1;
|
||||
else { sortState.key = key; sortState.dir = 1; }
|
||||
renderAssets(lastView);
|
||||
}
|
||||
|
||||
function renderAssets(assets) {
|
||||
const container = document.getElementById('assets-container');
|
||||
lastView = assets || [];
|
||||
|
||||
if (!assets || assets.length === 0) {
|
||||
container.innerHTML = `
|
||||
@@ -548,23 +604,16 @@
|
||||
return;
|
||||
}
|
||||
|
||||
const sortedAssets = sortAssetsView(assets);
|
||||
|
||||
let html = `
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Nome</th>
|
||||
<th>Tipo</th>
|
||||
<th>Categoria</th>
|
||||
<th>Criticita'</th>
|
||||
<th>Rilevanza NIS2</th>
|
||||
<th>Owner</th>
|
||||
<th>Stato</th>
|
||||
<th>Azioni</th>
|
||||
</tr>
|
||||
<tr>${ASSET_COLUMNS.map(sortableTh).join('')}<th>Azioni</th></tr>
|
||||
</thead>
|
||||
<tbody>`;
|
||||
|
||||
assets.forEach(asset => {
|
||||
sortedAssets.forEach(asset => {
|
||||
const crit = asset.criticality || 'medium';
|
||||
const st = asset.status || 'active';
|
||||
html += `
|
||||
|
||||
Reference in New Issue
Block a user