Page routes, Home link, documented-app filters, Overview colors and list fixes
- Every nav page has its own address (/tracked, /status, /devices, ...); the logo and name go to the Overview; back/forward work. - List filters gain an "Only ticked" mode; software columns get a one-click "devices with only documented apps" pick and an easiest-first sort. - Overview: the device list uses the dashboard's table header (sticky, with Excel filters); what opened it is clearly highlighted; locations colored by how much is done (red to green); stages read red / amber / green. - Details: one-line title bar; presence and monitors only in Advanced View. - Presence sweep pings only completed pairs (everything else is pinged by the sweep itself), hourly and at midnight. - Custom View scrollbar smaller, in the table header blue. - Docs: no sideways scroll in the article list; no blank lines around code.
This commit is contained in:
1 parent
e2125c760a
commit
f72e39892c
13 files changed
+332
-117
No files matched your search
+74
-14
@@ -69,6 +69,17 @@
|
||||
sortDir: i => { const s = sortStates['software']; return s && s.col === i ? s.dir : 'none'; }
|
||||
};
|
||||
}
|
||||
if (id === 'ov-table' && window.Overview) {
|
||||
const key = Overview.listKey();
|
||||
return {
|
||||
key, native: true,
|
||||
colKey: i => i,
|
||||
rerun: () => Overview.renderList(),
|
||||
sortable: () => true,
|
||||
sort: (i, dir) => { nativeSort[key] = { col: i, dir }; Overview.renderList(); },
|
||||
sortDir: i => { const s = nativeSort[key]; return s && s.col === i ? s.dir : 'none'; }
|
||||
};
|
||||
}
|
||||
if (table.closest('#stackGrid')) {
|
||||
const sid = id.replace(/^table-/, '');
|
||||
const key = 'stack-' + sid;
|
||||
@@ -166,7 +177,11 @@
|
||||
if (allowed.free.op === 'ge' ? sp.free < allowed.free.gb : sp.free >= allowed.free.gb) return false;
|
||||
}
|
||||
const vals = cellValues(td, col);
|
||||
if (col.list && allowed.only) {
|
||||
if (col.list && allowed.only === 'subset') {
|
||||
// only ticked: nothing outside the ticked items (some of them is fine)
|
||||
if (!vals.every(v => allowed.has(v))) return false;
|
||||
}
|
||||
else if (col.list && allowed.only) {
|
||||
// exact set: the row's items are precisely the ticked ones, no more, no fewer
|
||||
const own = new Set(vals);
|
||||
if (own.size !== allowed.size || !vals.every(v => allowed.has(v))) return false;
|
||||
@@ -206,12 +221,29 @@
|
||||
decorate(table, ad, cols);
|
||||
}
|
||||
|
||||
function isDocumentedApp(v) {
|
||||
const docs = (typeof availableDocs !== 'undefined' && Array.isArray(availableDocs)) ? availableDocs : [];
|
||||
const l = String(v).toLowerCase();
|
||||
return docs.some(d => d.toLowerCase() === l);
|
||||
}
|
||||
|
||||
function nativeSortRows(table, cols, s) {
|
||||
const col = cols[s.col];
|
||||
if (!col) return;
|
||||
const body = table.tBodies[0];
|
||||
const rows = dataRows(table);
|
||||
const num = tr => { const sp = parseSpace(tr.cells[s.col]); return sp ? sp[s.by] : null; };
|
||||
if (s.by === 'docs') {
|
||||
// undocumented apps, then all apps: a device with only documented apps sorts first
|
||||
const key = tr => {
|
||||
const vals = cellValues(tr.cells[s.col], col).filter(v => v !== 'None' && v !== BLANK);
|
||||
return [vals.filter(v => !isDocumentedApp(v)).length, vals.length];
|
||||
};
|
||||
const keyed = rows.map(r => [r, key(r)]);
|
||||
keyed.sort((a, b) => (a[1][0] - b[1][0]) * (s.dir === 'desc' ? -1 : 1) || a[1][1] - b[1][1]);
|
||||
keyed.forEach(([r]) => body.appendChild(r));
|
||||
return;
|
||||
}
|
||||
rows.sort((a, b) => {
|
||||
if (s.by) { // disk size / free space: no reading always last
|
||||
const x = num(a), y = num(b);
|
||||
@@ -422,6 +454,15 @@
|
||||
menu.appendChild(el('div', 'xf-sep'));
|
||||
} else if (col.sortable) {
|
||||
const dir = sortDirOf(ad, idx);
|
||||
if (col.list && /software/i.test(col.label)) {
|
||||
// "Easy" devices first: fewest apps without a documentation article
|
||||
const ns = nativeSort[ad.key];
|
||||
const cur = ns && ns.col === idx && ns.by === 'docs' && ns.dir !== 'none' ? ns.dir : '';
|
||||
item(SORT_ASC_SVG, 'Easiest first (fewest undocumented apps)', () => { closeMenu(); applySort(ad, idx, cur === 'asc' ? 'none' : 'asc', 'docs'); });
|
||||
if (cur === 'asc') menu.lastChild.classList.add('xf-checked');
|
||||
item(SORT_DESC_SVG, 'Hardest first (most undocumented apps)', () => { closeMenu(); applySort(ad, idx, cur === 'desc' ? 'none' : 'desc', 'docs'); });
|
||||
if (cur === 'desc') menu.lastChild.classList.add('xf-checked');
|
||||
}
|
||||
item(SORT_ASC_SVG, 'Sort A to Z', () => { closeMenu(); applySort(ad, idx, dir === 'asc' ? 'none' : 'asc'); });
|
||||
if (dir === 'asc') menu.lastChild.classList.add('xf-checked');
|
||||
item(SORT_DESC_SVG, 'Sort Z to A', () => { closeMenu(); applySort(ad, idx, dir === 'desc' ? 'none' : 'desc'); });
|
||||
@@ -472,20 +513,22 @@
|
||||
};
|
||||
}
|
||||
|
||||
// List columns: match rows having ANY ticked item, or ONLY ticked items
|
||||
let onlyToggle = null;
|
||||
// List columns: rows having ANY ticked item, ONLY ticked items (nothing else), or
|
||||
// EXACTLY the ticked items
|
||||
let listMode = null, setMode = null;
|
||||
if (col.list) {
|
||||
const seg = el('div', 'xf-seg');
|
||||
seg.title = 'Any: devices with at least one ticked item. Exactly: devices whose list is exactly the ticked items, nothing more and nothing less.';
|
||||
const mk = (text, only) => {
|
||||
const b = el('button', 'xf-seg-btn', text); b.type = 'button';
|
||||
b.onclick = () => { seg.querySelectorAll('.xf-seg-btn').forEach(x => x.classList.toggle('on', x === b)); };
|
||||
if (!!(current && current.only) === only) b.classList.add('on');
|
||||
const seg = el('div', 'xf-seg xf-seg3');
|
||||
seg.title = 'Any: at least one ticked item. Only: nothing except ticked items. Exactly: precisely the ticked items, nothing more and nothing less.';
|
||||
const curMode = !current || !current.only ? 'any' : (current.only === 'subset' ? 'subset' : 'exact');
|
||||
const btns = {};
|
||||
setMode = m => { listMode = m; Object.entries(btns).forEach(([k, b]) => b.classList.toggle('on', k === m)); };
|
||||
[['any', 'Any ticked', 'At least one ticked item'], ['subset', 'Only ticked', 'Nothing except ticked items'], ['exact', 'Exactly ticked', 'Precisely the ticked items']].forEach(([m, text, tip]) => {
|
||||
const b = el('button', 'xf-seg-btn', text); b.type = 'button'; b.title = tip;
|
||||
b.onclick = () => setMode(m);
|
||||
btns[m] = b;
|
||||
seg.appendChild(b);
|
||||
return b;
|
||||
};
|
||||
mk('Has any ticked', false);
|
||||
onlyToggle = mk('Exactly ticked', true);
|
||||
});
|
||||
setMode(curMode);
|
||||
menu.appendChild(seg);
|
||||
}
|
||||
|
||||
@@ -553,6 +596,20 @@
|
||||
};
|
||||
pick('Documented', isDocumented);
|
||||
pick('Undocumented', v => !isDocumented(v));
|
||||
// One click: devices whose apps ALL have an article ("easy" devices)
|
||||
if (setMode) {
|
||||
const b = el('button', 'xf-pick xf-pick-main'); b.type = 'button';
|
||||
b.textContent = 'Devices with only documented apps';
|
||||
b.title = 'Show devices where every app in this column has a documentation article (and devices with none)';
|
||||
b.onclick = () => {
|
||||
search.value = '';
|
||||
opts.forEach(o => { o.row.hidden = false; o.cb.checked = o.v === 'None' || (o.v !== BLANK && isDocumented(o.v)); });
|
||||
setMode('subset');
|
||||
sync();
|
||||
ok.click();
|
||||
};
|
||||
pickBar.appendChild(b);
|
||||
}
|
||||
}
|
||||
search.oninput = () => {
|
||||
const q = search.value.trim().toLowerCase();
|
||||
@@ -570,7 +627,7 @@
|
||||
const chosen = new Set(visibleOpts().filter(o => o.cb.checked).map(o => o.v));
|
||||
const free = freeRule ? freeRule() : null;
|
||||
if (free) chosen.free = free;
|
||||
if (onlyToggle && onlyToggle.classList.contains('on')) chosen.only = true;
|
||||
if (listMode === 'subset' || listMode === 'exact') chosen.only = listMode;
|
||||
filters[ad.key] = filters[ad.key] || {};
|
||||
// everything ticked and no extra rule = no filter
|
||||
if (!q && chosen.size === values.length && !free) delete filters[ad.key][key];
|
||||
@@ -749,6 +806,9 @@
|
||||
schedule();
|
||||
}
|
||||
|
||||
// Tables rendered outside the dashboard's own filter passes (the Overview list) call this
|
||||
window.ExcelFilter = { refresh: table => { try { afterFilterPass(table); } catch (e) { console.error('[excel-filter]', e); } } };
|
||||
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
Reference in new issue
Block a user