2025-10-20 15:42:12 +02:00
|
|
|
// static/pages/BlocksTable.js
|
2025-10-17 14:46:44 +02:00
|
|
|
import { h } from 'preact';
|
|
|
|
|
import { useEffect, useRef } from 'preact/hooks';
|
2025-10-30 15:00:06 +01:00
|
|
|
import { PAGE, API } from '../lib/api.js';
|
|
|
|
|
import { TABLE_SIZE } from '../lib/constants.js';
|
|
|
|
|
import { streamNdjson, ensureFixedRowCount, shortenHex } from '../lib/utils.js';
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
|
|
|
export default function BlocksTable() {
|
2025-10-20 15:42:12 +02:00
|
|
|
const bodyRef = useRef(null);
|
2025-10-17 15:33:00 +02:00
|
|
|
const countRef = useRef(null);
|
|
|
|
|
const abortRef = useRef(null);
|
2025-10-17 14:46:44 +02:00
|
|
|
const seenKeysRef = useRef(new Set());
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-10-20 15:42:12 +02:00
|
|
|
const body = bodyRef.current;
|
2025-10-17 15:33:00 +02:00
|
|
|
const counter = countRef.current;
|
2025-10-20 15:42:12 +02:00
|
|
|
|
2025-12-19 10:11:49 +01:00
|
|
|
// 5 columns: Hash | Slot | Parent | Block Root | Transactions
|
|
|
|
|
ensureFixedRowCount(body, 5, TABLE_SIZE);
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
abortRef.current?.abort();
|
|
|
|
|
abortRef.current = new AbortController();
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
const pruneAndPad = () => {
|
2025-10-30 11:48:34 +01:00
|
|
|
// remove any placeholder rows that snuck in
|
2025-10-20 15:42:12 +02:00
|
|
|
for (let i = body.rows.length - 1; i >= 0; i--) {
|
|
|
|
|
if (body.rows[i].classList.contains('ph')) body.deleteRow(i);
|
2025-10-17 14:46:44 +02:00
|
|
|
}
|
2025-10-30 11:48:34 +01:00
|
|
|
// keep at most TABLE_SIZE non-placeholder rows
|
2025-10-20 15:42:12 +02:00
|
|
|
while ([...body.rows].filter((r) => !r.classList.contains('ph')).length > TABLE_SIZE) {
|
|
|
|
|
const last = body.rows[body.rows.length - 1];
|
2025-10-17 14:46:44 +02:00
|
|
|
const key = last?.dataset?.key;
|
|
|
|
|
if (key) seenKeysRef.current.delete(key);
|
2025-10-20 15:42:12 +02:00
|
|
|
body.deleteRow(-1);
|
2025-10-17 14:46:44 +02:00
|
|
|
}
|
2025-12-19 10:11:49 +01:00
|
|
|
// pad with placeholders to TABLE_SIZE (5 cols)
|
|
|
|
|
ensureFixedRowCount(body, 5, TABLE_SIZE);
|
2025-10-20 15:42:12 +02:00
|
|
|
const real = [...body.rows].filter((r) => !r.classList.contains('ph')).length;
|
2025-10-17 15:33:00 +02:00
|
|
|
counter.textContent = String(real);
|
|
|
|
|
};
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-12-19 10:11:49 +01:00
|
|
|
const navigateToBlockDetail = (blockHash) => {
|
|
|
|
|
history.pushState({}, '', PAGE.BLOCK_DETAIL(blockHash));
|
2025-10-20 15:42:12 +02:00
|
|
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const appendRow = (b, key) => {
|
|
|
|
|
const tr = document.createElement('tr');
|
|
|
|
|
tr.dataset.key = key;
|
|
|
|
|
|
2025-12-19 10:11:49 +01:00
|
|
|
// Hash (clickable, replaces ID)
|
2025-10-20 15:42:12 +02:00
|
|
|
const tdId = document.createElement('td');
|
|
|
|
|
const linkId = document.createElement('a');
|
|
|
|
|
linkId.className = 'linkish mono';
|
2025-12-19 10:11:49 +01:00
|
|
|
linkId.href = PAGE.BLOCK_DETAIL(b.hash);
|
|
|
|
|
linkId.textContent = shortenHex(b.hash);
|
|
|
|
|
linkId.title = b.hash;
|
2025-10-20 15:42:12 +02:00
|
|
|
linkId.addEventListener('click', (e) => {
|
|
|
|
|
e.preventDefault();
|
2025-12-19 10:11:49 +01:00
|
|
|
navigateToBlockDetail(b.hash);
|
2025-10-20 15:42:12 +02:00
|
|
|
});
|
|
|
|
|
tdId.appendChild(linkId);
|
|
|
|
|
|
|
|
|
|
// Slot
|
|
|
|
|
const tdSlot = document.createElement('td');
|
|
|
|
|
const spSlot = document.createElement('span');
|
|
|
|
|
spSlot.className = 'mono';
|
|
|
|
|
spSlot.textContent = String(b.slot);
|
|
|
|
|
tdSlot.appendChild(spSlot);
|
|
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
// Parent (block.parent_block_hash)
|
2025-10-20 15:42:12 +02:00
|
|
|
const tdParent = document.createElement('td');
|
2025-12-19 10:11:49 +01:00
|
|
|
const linkParent = document.createElement('a');
|
|
|
|
|
linkParent.className = 'linkish mono';
|
|
|
|
|
linkParent.href = PAGE.BLOCK_DETAIL(b.parent);
|
|
|
|
|
linkParent.textContent = shortenHex(b.parent);
|
|
|
|
|
linkParent.title = b.parent;
|
|
|
|
|
linkParent.addEventListener('click', (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
navigateToBlockDetail(b.parent, e);
|
|
|
|
|
});
|
|
|
|
|
tdParent.appendChild(linkParent);
|
2025-10-20 15:42:12 +02:00
|
|
|
|
2025-10-30 11:48:34 +01:00
|
|
|
// Block Root
|
|
|
|
|
const tdRoot = document.createElement('td');
|
|
|
|
|
const spRoot = document.createElement('span');
|
|
|
|
|
spRoot.className = 'mono';
|
|
|
|
|
spRoot.title = b.root;
|
|
|
|
|
spRoot.textContent = shortenHex(b.root);
|
|
|
|
|
tdRoot.appendChild(spRoot);
|
|
|
|
|
|
2025-10-20 15:42:12 +02:00
|
|
|
// Transactions (array length)
|
|
|
|
|
const tdCount = document.createElement('td');
|
|
|
|
|
const spCount = document.createElement('span');
|
|
|
|
|
spCount.className = 'mono';
|
|
|
|
|
spCount.textContent = String(b.transactionCount);
|
|
|
|
|
tdCount.appendChild(spCount);
|
|
|
|
|
|
2025-12-19 10:11:49 +01:00
|
|
|
tr.append(tdId, tdSlot, tdParent, tdRoot, tdCount);
|
2025-10-20 15:42:12 +02:00
|
|
|
body.insertBefore(tr, body.firstChild);
|
2025-10-17 15:33:00 +02:00
|
|
|
pruneAndPad();
|
|
|
|
|
};
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
const normalize = (raw) => {
|
2025-10-30 11:48:34 +01:00
|
|
|
// New backend:
|
|
|
|
|
// { id, hash, slot, block_root, parent_block_hash, transactions: [...] }
|
|
|
|
|
// Back-compat (header.* / raw.parent_block) just in case.
|
|
|
|
|
const header = raw.header ?? null;
|
2025-10-20 15:42:12 +02:00
|
|
|
const txLen = Array.isArray(raw.transactions)
|
|
|
|
|
? raw.transactions.length
|
|
|
|
|
: Array.isArray(raw.txs)
|
|
|
|
|
? raw.txs.length
|
|
|
|
|
: 0;
|
|
|
|
|
|
2025-10-17 14:46:44 +02:00
|
|
|
return {
|
|
|
|
|
id: Number(raw.id ?? 0),
|
2025-10-30 11:48:34 +01:00
|
|
|
slot: Number(raw.slot ?? header?.slot ?? 0),
|
|
|
|
|
hash: raw.hash ?? header?.hash ?? '',
|
|
|
|
|
parent: raw.parent_block_hash ?? header?.parent_block ?? raw.parent_block ?? '',
|
|
|
|
|
root: raw.block_root ?? header?.block_root ?? '',
|
2025-10-20 15:42:12 +02:00
|
|
|
transactionCount: txLen,
|
2025-10-17 14:46:44 +02:00
|
|
|
};
|
2025-10-17 15:33:00 +02:00
|
|
|
};
|
2025-10-17 14:46:44 +02:00
|
|
|
|
|
|
|
|
streamNdjson(
|
2025-10-20 15:42:12 +02:00
|
|
|
`${API.BLOCKS_STREAM}?prefetch-limit=${encodeURIComponent(TABLE_SIZE)}`,
|
2025-10-17 14:46:44 +02:00
|
|
|
(raw) => {
|
2025-10-20 15:42:12 +02:00
|
|
|
const b = normalize(raw);
|
|
|
|
|
const key = `${b.id}:${b.slot}`;
|
2025-10-17 14:46:44 +02:00
|
|
|
if (seenKeysRef.current.has(key)) {
|
2025-10-17 15:33:00 +02:00
|
|
|
pruneAndPad();
|
2025-10-17 14:46:44 +02:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
seenKeysRef.current.add(key);
|
2025-10-20 15:42:12 +02:00
|
|
|
appendRow(b, key);
|
2025-10-17 14:46:44 +02:00
|
|
|
},
|
|
|
|
|
{
|
2025-10-17 15:33:00 +02:00
|
|
|
signal: abortRef.current.signal,
|
2025-10-20 15:42:12 +02:00
|
|
|
onError: (e) => {
|
|
|
|
|
console.error('Blocks stream error:', e);
|
|
|
|
|
},
|
2025-10-17 14:46:44 +02:00
|
|
|
},
|
2025-10-20 15:42:12 +02:00
|
|
|
);
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
return () => abortRef.current?.abort();
|
2025-10-17 14:46:44 +02:00
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
return h(
|
|
|
|
|
'div',
|
|
|
|
|
{ class: 'card' },
|
|
|
|
|
h(
|
|
|
|
|
'div',
|
|
|
|
|
{ class: 'card-header' },
|
2025-10-17 15:33:00 +02:00
|
|
|
h('div', null, h('strong', null, 'Blocks '), h('span', { class: 'pill', ref: countRef }, '0')),
|
2025-10-20 15:42:12 +02:00
|
|
|
h('div', { style: 'color:var(--muted); fontSize:12px;' }),
|
2025-10-17 14:46:44 +02:00
|
|
|
),
|
|
|
|
|
h(
|
|
|
|
|
'div',
|
|
|
|
|
{ class: 'table-wrapper' },
|
|
|
|
|
h(
|
|
|
|
|
'table',
|
|
|
|
|
{ class: 'table--blocks' },
|
|
|
|
|
h(
|
|
|
|
|
'colgroup',
|
|
|
|
|
null,
|
2025-10-30 11:48:34 +01:00
|
|
|
h('col', { style: 'width:240px' }), // Hash
|
2025-12-19 10:11:49 +01:00
|
|
|
h('col', { style: 'width:90px' }), // Slot
|
2025-10-20 15:42:12 +02:00
|
|
|
h('col', { style: 'width:240px' }), // Parent
|
2025-10-30 11:48:34 +01:00
|
|
|
h('col', { style: 'width:240px' }), // Block Root
|
2025-10-20 15:42:12 +02:00
|
|
|
h('col', { style: 'width:120px' }), // Transactions
|
2025-10-17 14:46:44 +02:00
|
|
|
),
|
|
|
|
|
h(
|
|
|
|
|
'thead',
|
|
|
|
|
null,
|
2025-10-20 15:42:12 +02:00
|
|
|
h(
|
|
|
|
|
'tr',
|
|
|
|
|
null,
|
2025-10-30 11:48:34 +01:00
|
|
|
h('th', null, 'Hash'),
|
2025-12-19 10:11:49 +01:00
|
|
|
h('th', null, 'Slot'),
|
2025-10-20 15:42:12 +02:00
|
|
|
h('th', null, 'Parent'),
|
2025-10-30 11:48:34 +01:00
|
|
|
h('th', null, 'Block Root'),
|
2025-10-20 15:42:12 +02:00
|
|
|
h('th', null, 'Transactions'),
|
|
|
|
|
),
|
2025-10-17 14:46:44 +02:00
|
|
|
),
|
2025-10-20 15:42:12 +02:00
|
|
|
h('tbody', { ref: bodyRef }),
|
2025-10-17 14:46:44 +02:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|