added a scrollbox for each app logs
This commit is contained in:
1 parent
6c345ccf81
commit
99422d8cfc
4 files changed
+54
-4
No files matched your search
@@ -12,3 +12,4 @@
|
|||||||
.last-log{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
.last-log{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||||
summary{display:grid;grid-template-columns:auto minmax(0,1fr) repeat(3,max-content);align-items:center;column-gap:10px;row-gap:5px}
|
summary{display:grid;grid-template-columns:auto minmax(0,1fr) repeat(3,max-content);align-items:center;column-gap:10px;row-gap:5px}
|
||||||
summary .last-log{grid-column:1/-1;min-width:0}
|
summary .last-log{grid-column:1/-1;min-width:0}
|
||||||
|
.rows{max-height:min(45vh,420px);overflow-y:auto;scrollbar-gutter:stable;overflow-anchor:none}
|
||||||
@@ -16,6 +16,7 @@
|
|||||||
let entries = [], latest, appliedMessage, displayZone = 'Europe/Berlin', paused = false, selected = saved.selected, mode = saved.mode || 'graph', origin = saved.origin || saved.selected;
|
let entries = [], latest, appliedMessage, displayZone = 'Europe/Berlin', paused = false, selected = saved.selected, mode = saved.mode || 'graph', origin = saved.origin || saved.selected;
|
||||||
const expanded = new Set(saved.expanded || []);
|
const expanded = new Set(saved.expanded || []);
|
||||||
const visibleEventGroups = new Map();
|
const visibleEventGroups = new Map();
|
||||||
|
const logScrollPositions = new Map();
|
||||||
const recentEvents = new Set();
|
const recentEvents = new Set();
|
||||||
const recentApps = new Set();
|
const recentApps = new Set();
|
||||||
const appFlashTokens = new Map();
|
const appFlashTokens = new Map();
|
||||||
@@ -138,6 +139,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
function renderApps() {
|
function renderApps() {
|
||||||
|
const appList = $('apps').parentElement;
|
||||||
|
const appListTop = appList.scrollTop;
|
||||||
|
document.querySelectorAll('#apps details[data-app]').forEach(detail => {
|
||||||
|
const rows = detail.querySelector('.rows');
|
||||||
|
if (!rows) return;
|
||||||
|
logScrollPositions.set(detail.dataset.app, {
|
||||||
|
top: rows.scrollTop,
|
||||||
|
follow: rows.scrollHeight - rows.clientHeight - rows.scrollTop <= 24
|
||||||
|
});
|
||||||
|
});
|
||||||
visibleEventGroups.clear();
|
visibleEventGroups.clear();
|
||||||
const container = $('apps'); container.replaceChildren();
|
const container = $('apps'); container.replaceChildren();
|
||||||
const groups = new Map();
|
const groups = new Map();
|
||||||
@@ -195,15 +206,23 @@
|
|||||||
? (entries.find(e => e.app === app && e.initialization_id === unit.id && e.kind === 'initialization_started') || unit.entries[0]).time
|
? (entries.find(e => e.app === app && e.initialization_id === unit.id && e.kind === 'initialization_started') || unit.entries[0]).time
|
||||||
: unit.entries[0].time;
|
: unit.entries[0].time;
|
||||||
units.sort((a, b) => startTime(a) - startTime(b));
|
units.sort((a, b) => startTime(a) - startTime(b));
|
||||||
|
if (units.length > 150) rows.append(node('div', `${units.length - 150} older rows/groups: narrow the time range or search.`, 'more'));
|
||||||
units.slice(-150).forEach(unit => {
|
units.slice(-150).forEach(unit => {
|
||||||
appendEventRows(rows, unit.entries, unit.id ? `initialization:${unit.id}` : undefined);
|
appendEventRows(rows, unit.entries, unit.id ? `initialization:${unit.id}` : undefined);
|
||||||
});
|
});
|
||||||
if (units.length > 150) rows.append(node('div', `${units.length - 150} older rows/groups: narrow the time range or search.`, 'more'));
|
|
||||||
detail.append(rows);
|
detail.append(rows);
|
||||||
|
const position = logScrollPositions.get(app);
|
||||||
|
rows.scrollTop = !position || position.follow ? rows.scrollHeight : position.top;
|
||||||
}
|
}
|
||||||
detail.addEventListener('toggle', event => { if (event.target !== detail) return; detail.open ? expanded.add(app) : expanded.delete(app); save(); fill(); });
|
detail.addEventListener('toggle', event => {
|
||||||
fill(); container.append(detail);
|
if (event.target !== detail) return;
|
||||||
|
if (detail.open) expanded.add(app);
|
||||||
|
else { expanded.delete(app); logScrollPositions.delete(app); }
|
||||||
|
save(); fill();
|
||||||
|
});
|
||||||
|
container.append(detail); fill();
|
||||||
});
|
});
|
||||||
|
appList.scrollTop = appListTop;
|
||||||
}
|
}
|
||||||
function renderDetail() {
|
function renderDetail() {
|
||||||
$('graph-mode').setAttribute('aria-pressed', String(mode === 'graph'));
|
$('graph-mode').setAttribute('aria-pressed', String(mode === 'graph'));
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ test('English interface, latest timestamps, sorting and separate initialization
|
|||||||
assert.match(d.querySelector('[data-app="alpha"] .last-age').title,/10:00:10/);
|
assert.match(d.querySelector('[data-app="alpha"] .last-age').title,/10:00:10/);
|
||||||
assert.match(d.querySelector('[data-app="alpha"] summary .last-log').textContent,/ : Température déjà disponible$/);
|
assert.match(d.querySelector('[data-app="alpha"] summary .last-log').textContent,/ : Température déjà disponible$/);
|
||||||
const summary=d.querySelector('[data-app="alpha"] summary');
|
const summary=d.querySelector('[data-app="alpha"] summary');
|
||||||
assert.deepEqual([...summary.children].map(child=>child.className),['app-label','badge INFO','badge WARNING','badge ERROR','last-log']);
|
assert.deepEqual([...summary.children].slice(0,4).map(child=>child.className),['app-label','badge INFO','badge WARNING','badge ERROR']);
|
||||||
|
assert.ok(summary.children[4].classList.contains('last-log'));
|
||||||
assert.equal(summary.querySelector('.app-label .last-log'),null);
|
assert.equal(summary.querySelector('.app-label .last-log'),null);
|
||||||
d.getElementById('sort').value='oldest';d.getElementById('sort').dispatchEvent(new dom.window.Event('input'));
|
d.getElementById('sort').value='oldest';d.getElementById('sort').dispatchEvent(new dom.window.Event('input'));
|
||||||
assert.equal(d.querySelector('[data-app]').dataset.app,'alpha');
|
assert.equal(d.querySelector('[data-app]').dataset.app,'alpha');
|
||||||
@@ -122,6 +123,35 @@ test('pulses an app when a new log arrives and refreshes its last-log age',async
|
|||||||
dom.window.close();
|
dom.window.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('expanded logs follow new events at the bottom and keep position while reading history',async()=>{
|
||||||
|
const {dom,d,entries,send}=setup();
|
||||||
|
Object.defineProperty(dom.window.HTMLElement.prototype,'scrollHeight',{configurable:true,get(){return this.classList.contains('rows') ? this.children.length*100 : 0;}});
|
||||||
|
Object.defineProperty(dom.window.HTMLElement.prototype,'clientHeight',{configurable:true,get(){return this.classList.contains('rows') ? 100 : 0;}});
|
||||||
|
const app=d.querySelector('[data-app="alpha"]');app.open=true;app.dispatchEvent(new dom.window.Event('toggle'));
|
||||||
|
const rows=()=>d.querySelector('[data-app="alpha"] .rows');
|
||||||
|
assert.equal(rows().scrollTop,rows().scrollHeight);
|
||||||
|
const first={...entries[3],event_id:'new-tail',time:entries[3].time+1000,message:'Newest'};
|
||||||
|
send([...entries,first]);
|
||||||
|
assert.equal(rows().scrollTop,rows().scrollHeight);
|
||||||
|
rows().scrollTop=20;
|
||||||
|
const second={...entries[3],event_id:'new-tail-2',time:entries[3].time+2000,message:'Newer'};
|
||||||
|
send([...entries,first,second]);
|
||||||
|
assert.equal(rows().scrollTop,20);
|
||||||
|
assert.match(rows().textContent,/Newer/);
|
||||||
|
dom.window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the newest log stays at the bottom when older groups are omitted',()=>{
|
||||||
|
const {dom,d,entries,send}=setup();
|
||||||
|
const base=entries[3];
|
||||||
|
send(Array.from({length:152},(_,index)=>({...base,event_id:`event-${index}`,time:base.time+index*1000,message:`Message ${index}`})));
|
||||||
|
const app=d.querySelector('[data-app="alpha"]');app.open=true;app.dispatchEvent(new dom.window.Event('toggle'));
|
||||||
|
const rows=app.querySelector('.rows');
|
||||||
|
assert.ok(rows.firstElementChild.classList.contains('more'));
|
||||||
|
assert.match(rows.lastElementChild.textContent,/Message 151/);
|
||||||
|
dom.window.close();
|
||||||
|
});
|
||||||
|
|
||||||
test('distinguishes log lines from supplemental records when configured',()=>{
|
test('distinguishes log lines from supplemental records when configured',()=>{
|
||||||
const {dom,d,entries,send}=setup();
|
const {dom,d,entries,send}=setup();
|
||||||
const trace={...entries[3],event_id:'trace-evaluation',kind:'evaluation',message:'Condition was evaluated'};
|
const trace={...entries[3],event_id:'trace-evaluation',kind:'evaluation',message:'Condition was evaluated'};
|
||||||
|
|||||||
Binary file not shown.
Reference in new issue
Block a user