v2.0.8 — oumta timeline: continuous fading line, multi-select filter

- Connector: moved from per-entry pseudo-elements to a single
  linear-gradient line on the body. Fades in/out 28px at each end
  and no longer has gaps at entry boundaries. Dots and the date
  rect's solid background sit naturally on top; everything else
  stays transparent so the line threads through cleanly.
- Layout: entry is now a 2-row grid. The optional date rect lives
  in row 1 (justify-self:start, left-aligned), while the dot-row
  and the events column both pin to row 2 so the time label always
  lines up with the first event of the cluster — even when a date
  rect sits above. Time+dot `align-self: start` keeps them at the
  top of the event section instead of vertically centred through
  the whole cluster.
- Filters: state.timelineFilter is a Set now, so clicking multiple
  workload pills (or the overflow entries) stacks them as an
  any-of filter. "all" clears the set. The overflow dropdown stays
  open on pick so several can be toggled without reopening it each
  time. Scope label shows "filtered · N" with the active count.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
thandClaude Opus 4.7 committed 2026-04-18 17:21:18 +02:00
1 parent 9f90ea516a
commit 4aca21812e
4 files changed
+92 -37

No files matched your search

@@ -23,7 +23,7 @@ const state = {
pollHandle: null,
sseHandle: null,
data: { overview: null, workloads: [], timeline: [], scopeWorkload: null },
timelineFilter: null, // null = all events, else workloadId (pill quick-pick)
timelineFilter: new Set(), // empty = all events; else any-of workloadIds (multi-select pills)
};
// ── Central API surface ─────────────────────────────────────────
@@ -314,14 +314,15 @@ function renderTimeline() {
scopeEl.textContent = 'events for ' + scope;
backEl.classList.remove('hidden');
} else {
scopeEl.textContent = state.timelineFilter ? 'filtered' : 'all events';
const n = state.timelineFilter.size;
scopeEl.textContent = n === 0 ? 'all events' : `filtered · ${n}`;
backEl.classList.add('hidden');
}
const filter = state.timelineFilter;
const events = state.data.timeline.filter(e => {
if (scope) return e.workloadId === scope;
if (filter) return e.workloadId === filter;
return true;
if (filter.size === 0) return true;
return filter.has(e.workloadId);
});
renderTimelineFilters();
@@ -345,10 +346,10 @@ function renderTimeline() {
</div>`
: '';
const left = `
<div class="oumta-tl-left">
${badge}
<div class="oumta-tl-dot" data-sev="${sev}"></div>
<div class="oumta-tl-time">${escapeHtml(formatTimeShort(cluster.ts))}</div>
${badge}
<div class="oumta-tl-row">
<span class="oumta-tl-time">${escapeHtml(formatTimeShort(cluster.ts))}</span>
<span class="oumta-tl-dot" data-sev="${sev}"></span>
</div>`;
const right = `
<div class="oumta-tl-right">
@@ -370,14 +371,19 @@ function renderTimeline() {
return `<div class="oumta-timeline-entry">${left}${right}</div>`;
}).join('');
// Tapping a workload name inside an event filters the timeline to it.
// Tapping a workload name inside an event toggles it in the filter set.
body.querySelectorAll('.oumta-tl-workload').forEach(el => {
el.addEventListener('click', () => setTimelineFilter(el.dataset.wlid));
el.addEventListener('click', () => toggleTimelineFilter(el.dataset.wlid));
});
}
function setTimelineFilter(workloadId) {
state.timelineFilter = workloadId || null;
/** Toggle a workload in the multi-select filter set. Passing an empty
* value clears the set (the "all" pill). */
function toggleTimelineFilter(workloadId) {
const set = state.timelineFilter;
if (!workloadId) { set.clear(); }
else if (set.has(workloadId)) { set.delete(workloadId); }
else { set.add(workloadId); }
renderTimeline();
}
@@ -402,26 +408,29 @@ function renderTimelineFilters() {
const INLINE_LIMIT = 6;
const inline = workloads.slice(0, INLINE_LIMIT);
const overflow = workloads.slice(INLINE_LIMIT);
const active = state.timelineFilter;
const active = state.timelineFilter; // Set<workloadId>
const pill = (id, label, isActive) =>
`<button class="oumta-tl-pill${isActive ? ' active' : ''}" data-wlid="${escapeAttr(id)}">${escapeHtml(label)}</button>`;
const overflowActive = overflow.includes(active);
// "all" is active whenever the filter set is empty; picking it clears
// the set. The individual pills toggle their workload in the set so
// multiple filters can stack (any-of match).
const overflowActiveCount = overflow.reduce((n, id) => n + (active.has(id) ? 1 : 0), 0);
host.innerHTML = [
pill('', 'all', !active),
...inline.map(id => pill(id, workloadShort(id), id === active)),
pill('', 'all', active.size === 0),
...inline.map(id => pill(id, workloadShort(id), active.has(id))),
overflow.length ? `
<div class="oumta-tl-more">
<button class="oumta-tl-pill${overflowActive ? ' active' : ''}" data-tl-more="1">+${overflow.length} more<i class="ph ph-caret-down" style="font-size:10px;margin-left:4px;"></i></button>
<button class="oumta-tl-pill${overflowActiveCount ? ' active' : ''}" data-tl-more="1">+${overflow.length} more${overflowActiveCount ? ` (${overflowActiveCount})` : ''}<i class="ph ph-caret-down" style="font-size:10px;margin-left:4px;"></i></button>
<div class="oumta-tl-more-menu hidden">
${overflow.map(id => `<button class="${id === active ? 'active' : ''}" data-wlid="${escapeAttr(id)}">${escapeHtml(workloadShort(id))}</button>`).join('')}
${overflow.map(id => `<button class="${active.has(id) ? 'active' : ''}" data-wlid="${escapeAttr(id)}">${escapeHtml(workloadShort(id))}</button>`).join('')}
</div>
</div>` : '',
].filter(Boolean).join('');
host.querySelectorAll('.oumta-tl-pill[data-wlid]').forEach(btn => {
btn.addEventListener('click', () => setTimelineFilter(btn.dataset.wlid || null));
btn.addEventListener('click', () => toggleTimelineFilter(btn.dataset.wlid || ''));
});
const moreBtn = host.querySelector('[data-tl-more]');
const moreMenu = host.querySelector('.oumta-tl-more-menu');
@@ -430,10 +439,12 @@ function renderTimelineFilters() {
e.stopPropagation();
moreMenu.classList.toggle('hidden');
});
// Overflow picks don't close the menu — multi-select UX relies on
// being able to tick several without the menu collapsing every time.
moreMenu.querySelectorAll('button[data-wlid]').forEach(btn => {
btn.addEventListener('click', () => {
setTimelineFilter(btn.dataset.wlid || null);
moreMenu.classList.add('hidden');
btn.addEventListener('click', (e) => {
e.stopPropagation();
toggleTimelineFilter(btn.dataset.wlid || '');
});
});
document.addEventListener('click', (e) => {