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:
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) => {
|
||||
|
||||
Reference in new issue
Block a user