Skip to content

Commit be38974

Browse files
committed
Add numbered Calibration dashboard pagination
1 parent 79b64c2 commit be38974

3 files changed

Lines changed: 147 additions & 22 deletions

File tree

docs/calibration-worker-demo.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,11 @@ datasets, and coordinators use the returned `pagination.nextOffset`. Filters
8484
and text search apply to the returned page so the Worker keeps each request
8585
bounded instead of scanning the full registry for a global search.
8686

87+
The admin dashboard renders numbered pagination controls for the current table:
88+
`Previous`, discovered page numbers, and `Next`. File and reconciliation pages
89+
enable number jumps only for cursor pages the browser has already discovered or
90+
the immediate next page returned by the current registry response.
91+
8792
Append `?live=false` to any dashboard or registry endpoint when you need a
8893
route-level smoke check without making public RPC calls. Unknown dashboard
8994
routes still return `404`.

src/worker/calibration-demo.mjs

Lines changed: 133 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -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]");

test/calibration-worker.test.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,16 @@ test("Worker serves HTML and public evidence endpoints", async () => {
110110
const htmlBody = await html.text();
111111
assert.match(htmlBody, /FOC Platform Admin/);
112112
assert.match(htmlBody, /\/api\/admin\/files/);
113+
assert.match(htmlBody, /<option value="Committed" selected>Committed<\/option>/);
114+
assert.match(htmlBody, /data-page-action="previous"/);
115+
assert.match(htmlBody, /data-page-action="page"/);
116+
assert.match(htmlBody, /data-page-index/);
113117
assert.match(htmlBody, /data-page-action="next"/);
118+
assert.match(htmlBody, /function renderPager/);
119+
assert.match(htmlBody, /function paginationWindow/);
120+
assert.match(htmlBody, /function rememberPagePosition/);
121+
assert.match(htmlBody, /page\.offsets\[nextIndex\] = pagination\.nextOffset \|\| "0";/);
122+
assert.match(htmlBody, /aria-current="page"/);
114123
assert.match(htmlBody, /function renderCoordinatorView/);
115124
assert.match(htmlBody, /function renderFileRows/);
116125
assert.match(htmlBody, /data-object-id/);

0 commit comments

Comments
 (0)