@@ -1284,6 +1284,7 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
12841284 align-items: center;
12851285 }
12861286 .pager button {
1287+ min-width: 32px;
12871288 min-height: 30px;
12881289 border: 1px solid var(--line);
12891290 border-radius: 6px;
@@ -1292,11 +1293,24 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
12921293 padding: 0 10px;
12931294 cursor: pointer;
12941295 }
1296+ .pager button[aria-current="page"] {
1297+ border-color: var(--ink);
1298+ box-shadow: inset 0 0 0 1px var(--ink);
1299+ background: #fff;
1300+ color: var(--ink);
1301+ font-weight: 800;
1302+ }
12951303 .pager button:disabled {
12961304 cursor: default;
12971305 color: var(--muted);
12981306 background: #f3f4f6;
12991307 }
1308+ .pager-gap {
1309+ min-width: 18px;
1310+ color: var(--muted);
1311+ text-align: center;
1312+ font-weight: 800;
1313+ }
13001314 a {
13011315 color: var(--accent-2);
13021316 text-decoration: none;
@@ -1352,7 +1366,7 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
13521366 <input id="q" name="q" placeholder="Search ids, addresses, hashes" autocomplete="off">
13531367 <select id="status" name="status" aria-label="Status filter">
13541368 <option value="">All statuses</option>
1355- ${ UPLOAD_STATUS_LABELS . slice ( 1 ) . map ( ( label ) => `<option value="${ escapeHtml ( label ) } ">${ escapeHtml ( label ) } </option>` ) . join ( "" ) }
1369+ ${ UPLOAD_STATUS_LABELS . slice ( 1 ) . map ( ( label ) => `<option value="${ escapeHtml ( label ) } "${ label === "Committed" ? " selected" : "" } >${ escapeHtml ( label ) } </option>` ) . join ( "" ) }
13561370 </select>
13571371 <input id="provider" name="provider" placeholder="Provider id" autocomplete="off">
13581372 <select id="limit" name="limit" aria-label="Page limit">
@@ -1394,11 +1408,11 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
13941408 pagination: null,
13951409 requestSeq: 0,
13961410 pages: {
1397- files: { cursor: "0" },
1398- accounts: { offset: "0" },
1399- datasets: { offset: "0" },
1400- coordinators: { offset: "0" },
1401- reconciliation: { cursor: "0" },
1411+ files: { cursor: "0", cursors: ["0"], index: 0 },
1412+ accounts: { offset: "0", offsets: ["0"], index: 0 },
1413+ datasets: { offset: "0", offsets: ["0"], index: 0 },
1414+ coordinators: { offset: "0", offsets: ["0"], index: 0 },
1415+ reconciliation: { cursor: "0", cursors: ["0"], index: 0 },
14021416 },
14031417 };
14041418 const $ = (id) => document.getElementById(id);
@@ -1653,17 +1667,76 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
16531667 $("footer").innerHTML = '<span>' + readState + '</span>';
16541668 return;
16551669 }
1670+ rememberPagePosition(view, pagination);
16561671 const page = currentPage(view);
1657- const isFirst = cursorViews.has(view) ? page.cursor === "0" : page.offset === "0";
16581672 const position = pagination.mode === "objectIdCursor"
16591673 ? "cursor " + esc(pagination.cursorIdExclusive || "0")
16601674 : "offset " + esc(pagination.offset || "0");
1675+ const pageLabel = "page " + esc(currentPageNumber(view));
16611676 $("footer").innerHTML =
1662- '<span>' + readState + " / " + position + '</span>' +
1663- '<span class="pager">' +
1664- '<button type="button" data-page-action="first"' + (isFirst ? " disabled" : "") + '>First</button>' +
1665- '<button type="button" data-page-action="next"' + (!pagination.hasNextPage ? " disabled" : "") + '>Next</button>' +
1666- '</span>';
1677+ '<span>' + readState + " / " + position + " / " + pageLabel + '</span>' +
1678+ renderPager(view, pagination);
1679+ }
1680+ function renderPager(view, pagination) {
1681+ const page = currentPage(view);
1682+ const currentIndex = Number(page.index || 0);
1683+ const maxKnownIndex = knownPageMaxIndex(view, pagination);
1684+ const isFirst = currentIndex === 0;
1685+ const items = paginationWindow(currentIndex, maxKnownIndex);
1686+ const buttons = [
1687+ '<button type="button" data-page-action="previous"' + (isFirst ? " disabled" : "") + '>Previous</button>',
1688+ ...items.map((item) => {
1689+ if (item === "gap") return '<span class="pager-gap" aria-hidden="true">...</span>';
1690+ const pageNumber = item + 1;
1691+ const current = item === currentIndex;
1692+ return '<button type="button" data-page-action="page" data-page-index="' + esc(item) + '"' + (current ? ' aria-current="page"' : "") + '>' + esc(pageNumber) + '</button>';
1693+ }),
1694+ '<button type="button" data-page-action="next"' + (!pagination.hasNextPage ? " disabled" : "") + '>Next</button>',
1695+ ];
1696+ return '<span class="pager" aria-label="Pagination">' + buttons.join("") + '</span>';
1697+ }
1698+ function rememberPagePosition(view, pagination) {
1699+ const page = currentPage(view);
1700+ page.index = Number(page.index || 0);
1701+ if (cursorViews.has(view)) {
1702+ page.cursors = page.cursors || ["0"];
1703+ page.cursors[page.index] = page.cursor || pagination.cursorIdExclusive || "0";
1704+ if (pagination.hasNextPage && pagination.nextCursorIdExclusive) {
1705+ page.cursors[page.index + 1] = pagination.nextCursorIdExclusive;
1706+ }
1707+ }
1708+ if (offsetViews.has(view)) {
1709+ page.offsets = page.offsets || ["0"];
1710+ page.offsets[page.index] = page.offset || pagination.offset || "0";
1711+ if (pagination.hasNextPage && pagination.nextOffset) {
1712+ page.offsets[page.index + 1] = pagination.nextOffset;
1713+ }
1714+ }
1715+ }
1716+ function currentPageNumber(view = state.view) {
1717+ return Number(currentPage(view).index || 0) + 1;
1718+ }
1719+ function knownPageMaxIndex(view, pagination) {
1720+ const page = currentPage(view);
1721+ const currentIndex = Number(page.index || 0);
1722+ const positions = cursorViews.has(view) ? page.cursors : page.offsets;
1723+ const discoveredIndex = Math.max(0, (positions || ["0"]).length - 1);
1724+ const nextIndex = pagination?.hasNextPage ? currentIndex + 1 : currentIndex;
1725+ return Math.max(currentIndex, discoveredIndex, nextIndex);
1726+ }
1727+ function paginationWindow(currentIndex, maxKnownIndex) {
1728+ const selected = new Set([0, currentIndex, maxKnownIndex]);
1729+ for (let index = currentIndex - 1; index <= currentIndex + 1; index += 1) {
1730+ if (index >= 0 && index <= maxKnownIndex) selected.add(index);
1731+ }
1732+ const indexes = Array.from(selected).filter((index) => index >= 0 && index <= maxKnownIndex).sort((a, b) => a - b);
1733+ const items = [];
1734+ indexes.forEach((index) => {
1735+ const previous = items[items.length - 1];
1736+ if (typeof previous === "number" && index - previous > 1) items.push("gap");
1737+ items.push(index);
1738+ });
1739+ return items;
16671740 }
16681741 function primaryPagination(pagination) {
16691742 if (!pagination) return null;
@@ -1689,22 +1762,60 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
16891762 return state.pages[view] || {};
16901763 }
16911764 function resetPage(view = state.view) {
1692- if (cursorViews.has(view)) state.pages[view].cursor = "0";
1693- if (offsetViews.has(view)) state.pages[view].offset = "0";
1765+ state.pages[view].index = 0;
1766+ if (cursorViews.has(view)) {
1767+ state.pages[view].cursor = "0";
1768+ state.pages[view].cursors = ["0"];
1769+ }
1770+ if (offsetViews.has(view)) {
1771+ state.pages[view].offset = "0";
1772+ state.pages[view].offsets = ["0"];
1773+ }
16941774 }
16951775 function resetAllPages() {
16961776 Object.keys(state.pages).forEach((view) => resetPage(view));
16971777 }
1698- function applyPageAction(action) {
1778+ function goToPage(index, view = state.view) {
1779+ const page = currentPage(view);
1780+ const nextIndex = Math.max(0, Number(index) || 0);
1781+ if (nextIndex === Number(page.index || 0)) return;
1782+ if (cursorViews.has(view)) {
1783+ const cursor = (page.cursors || ["0"])[nextIndex];
1784+ if (cursor === undefined) return;
1785+ page.cursor = cursor;
1786+ }
1787+ if (offsetViews.has(view)) {
1788+ const offset = (page.offsets || ["0"])[nextIndex];
1789+ if (offset === undefined) return;
1790+ page.offset = offset;
1791+ }
1792+ page.index = nextIndex;
1793+ loadView();
1794+ }
1795+ function applyPageAction(action, targetIndex) {
16991796 const pagination = state.pagination;
17001797 const page = currentPage();
1701- if (action === "first") {
1702- resetPage();
1703- } else if (action === "next" && pagination?.hasNextPage) {
1704- if (pagination.mode === "objectIdCursor") page.cursor = pagination.nextCursorIdExclusive || "0";
1705- if (pagination.mode === "offset") page.offset = pagination.nextOffset || "0";
1798+ const currentIndex = Number(page.index || 0);
1799+ if (action === "previous") {
1800+ goToPage(currentIndex - 1);
1801+ return;
1802+ }
1803+ if (action === "page") {
1804+ goToPage(targetIndex);
1805+ return;
1806+ }
1807+ if (action === "next" && pagination?.hasNextPage) {
1808+ const nextIndex = currentIndex + 1;
1809+ if (pagination.mode === "objectIdCursor") {
1810+ page.cursors = page.cursors || ["0"];
1811+ page.cursors[nextIndex] = pagination.nextCursorIdExclusive || "0";
1812+ }
1813+ if (pagination.mode === "offset") {
1814+ page.offsets = page.offsets || ["0"];
1815+ page.offsets[nextIndex] = pagination.nextOffset || "0";
1816+ }
1817+ goToPage(nextIndex);
17061818 }
1707- loadView();
17081819 }
17091820 function title(view) {
17101821 return ({ files: "Files", accounts: "Accounts", datasets: "Datasets", coordinators: "Coordinators", reconciliation: "Reconciliation" })[view] || "Files";
@@ -1727,7 +1838,7 @@ function renderAdminDashboardHtml(evidence, { live = true } = {}) {
17271838 document.addEventListener("click", (event) => {
17281839 const action = event.target.closest("[data-page-action]");
17291840 if (action) {
1730- applyPageAction(action.dataset.pageAction);
1841+ applyPageAction(action.dataset.pageAction, action.dataset.pageIndex );
17311842 return;
17321843 }
17331844 const objectToggle = event.target.closest("[data-object-id]");
0 commit comments