From 016589c414ceac5710f9881c57a6a98d9b47e969 Mon Sep 17 00:00:00 2001 From: Balazs Tasi Date: Wed, 29 Apr 2026 10:52:38 +0200 Subject: [PATCH] feat: add pagination to incident timeline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the binary Show more/fewer toggle with proper page-based pagination (4 date groups per page). Navigation uses prev/next arrow buttons with a 'Page X of Y' label in the top-right corner of the panel header, consistent with the existing Uptime History controls. - Swap showAll boolean for currentPage / ITEMS_PER_PAGE logic - Reuse .icon-button styling for prev/next arrows - Add .pagination and .pagination-label CSS - Remove bottom footer toggle — controls live only in panel header Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- site/app.js | 92 ++++++++++++++++++++++++++++++++++++++++--------- site/index.html | 16 +++++---- site/styles.css | 52 ++++++++++++++++++++++++++++ 3 files changed, 138 insertions(+), 22 deletions(-) diff --git a/site/app.js b/site/app.js index b49304a..2703fee 100644 --- a/site/app.js +++ b/site/app.js @@ -1193,12 +1193,53 @@ const render = async () => { }); const entries = Array.from(grouped.entries()); - let showAll = false; - const toggleButtons = Array.from(document.querySelectorAll('[data-toggle-timeline]')); + const ITEMS_PER_PAGE = 4; + let currentPage = 0; + let filteredEntries = entries; + + const prevButtons = Array.from(document.querySelectorAll('[data-page-prev]')); + const nextButtons = Array.from(document.querySelectorAll('[data-page-next]')); + const pageLabels = Array.from(document.querySelectorAll('[data-page-label]')); + const rangeFromInputs = Array.from(document.querySelectorAll('[data-range-from]')); + const rangeToInputs = Array.from(document.querySelectorAll('[data-range-to]')); + const rangeClearButtons = Array.from(document.querySelectorAll('[data-range-clear]')); + + // Build a lookup from each entry index to its raw Date + const entryDates = entries.map(([, list]) => incidentStartDate(list[0])); + + // Set min/max on date inputs from incident range + const toISO = (d) => d.toISOString().slice(0, 10); + if (entryDates.length) { + const newest = entryDates[0]; + const oldest = entryDates[entryDates.length - 1]; + [...rangeFromInputs, ...rangeToInputs].forEach((input) => { + input.min = toISO(oldest); + input.max = toISO(newest); + }); + } + + const getTotalPages = () => Math.max(1, Math.ceil(filteredEntries.length / ITEMS_PER_PAGE)); + + const updatePaginationControls = () => { + const totalPages = getTotalPages(); + prevButtons.forEach((btn) => (btn.disabled = currentPage <= 0)); + nextButtons.forEach((btn) => (btn.disabled = currentPage >= totalPages - 1)); + pageLabels.forEach((label) => (label.textContent = `Page ${currentPage + 1} of ${totalPages}`)); + rangeClearButtons.forEach((btn) => (btn.hidden = filteredEntries === entries)); + }; const renderTimeline = () => { timeline.innerHTML = ''; - const slice = showAll ? entries : entries.slice(0, 8); + const start = currentPage * ITEMS_PER_PAGE; + const slice = filteredEntries.slice(start, start + ITEMS_PER_PAGE); + if (slice.length === 0) { + const empty = document.createElement('p'); + empty.className = 'muted'; + empty.textContent = 'No incidents in this date range.'; + empty.style.textAlign = 'center'; + empty.style.padding = '24px 0'; + timeline.appendChild(empty); + } slice.forEach(([date, list]) => { const group = document.createElement('div'); group.className = 'incident-group'; @@ -1213,24 +1254,43 @@ const render = async () => { timeline.appendChild(group); }); + updatePaginationControls(); }; - renderTimeline(); - const updateToggleButtons = () => { - toggleButtons.forEach((button) => { - button.textContent = showAll ? 'Show fewer' : 'Show more'; - }); + const goToPage = (page) => { + const totalPages = getTotalPages(); + currentPage = Math.max(0, Math.min(totalPages - 1, page)); + renderTimeline(); }; - updateToggleButtons(); + const applyDateFilter = () => { + const fromVal = rangeFromInputs[0]?.value; + const toVal = rangeToInputs[0]?.value; + if (!fromVal && !toVal) { + filteredEntries = entries; + } else { + const from = fromVal ? new Date(fromVal + 'T00:00:00Z') : new Date(0); + const to = toVal ? new Date(toVal + 'T23:59:59Z') : new Date(); + filteredEntries = entries.filter(([, list]) => { + const d = incidentStartDate(list[0]); + return d >= from && d <= to; + }); + } + currentPage = 0; + renderTimeline(); + }; - toggleButtons.forEach((button) => { - button.addEventListener('click', () => { - showAll = !showAll; - updateToggleButtons(); - renderTimeline(); - }); - }); + prevButtons.forEach((btn) => btn.addEventListener('click', () => goToPage(currentPage - 1))); + nextButtons.forEach((btn) => btn.addEventListener('click', () => goToPage(currentPage + 1))); + rangeFromInputs.forEach((input) => input.addEventListener('change', applyDateFilter)); + rangeToInputs.forEach((input) => input.addEventListener('change', applyDateFilter)); + rangeClearButtons.forEach((btn) => btn.addEventListener('click', () => { + rangeFromInputs.forEach((i) => (i.value = '')); + rangeToInputs.forEach((i) => (i.value = '')); + applyDateFilter(); + })); + + renderTimeline(); }; const renderIncidentCard = (incident, compact = false) => { diff --git a/site/index.html b/site/index.html index afd050b..13d243f 100644 --- a/site/index.html +++ b/site/index.html @@ -154,15 +154,19 @@

Uptime history

Incident timeline

- +
-
diff --git a/site/styles.css b/site/styles.css index 5c995bc..a029883 100644 --- a/site/styles.css +++ b/site/styles.css @@ -162,6 +162,7 @@ --timeline-chip-bg: #21262d; --component-chip-bg: #11161d; --icon-button-bg: #161b22; + --date-picker-filter: invert(0.7) brightness(1.5); --badge-minor-ink: #e3b341; --badge-major-ink: #ffa198; --badge-maintenance-ink: #79c0ff; @@ -1422,6 +1423,57 @@ main { margin-top: 18px; } +.pagination { + display: flex; + align-items: center; + gap: 8px; +} + +.pagination-label { + font-weight: 600; + color: var(--muted); + min-width: 100px; + text-align: center; + user-select: none; +} + +.pagination-date { + height: 32px; + border-radius: 8px; + border: 1px solid var(--border); + background: var(--icon-button-bg); + color: var(--ink); + padding: 0 10px; + font-family: var(--mono); + font-size: 0.85rem; + cursor: pointer; +} + +.pagination-date::-webkit-calendar-picker-indicator { + filter: var(--date-picker-filter, none); + cursor: pointer; +} + +.pagination-separator { + width: 1px; + height: 20px; + background: var(--border); + margin: 0 4px; +} + +.pagination-range-sep { + color: var(--muted); + font-weight: 600; +} + +.pagination-clear { + font-size: 0.75rem; +} + +.pagination-clear[hidden] { + display: none; +} + .about-panel { background: var(--about-panel-bg); }