159 lines
6.0 KiB
JavaScript
Raw Normal View History

2025-10-17 14:46:44 +02:00
import { h } from 'preact';
import { useEffect, useRef } from 'preact/hooks';
import { BLOCKS_ENDPOINT, TABLE_SIZE } from '../lib/api.js?dev=1';
import {streamNdjson, ensureFixedRowCount, shortenHex, formatTimestamp, withBenignFilter} from '../lib/utils.js?dev=1';
2025-10-17 14:46:44 +02:00
export default function BlocksTable() {
const tbodyRef = useRef(null);
const countRef = useRef(null);
const abortRef = useRef(null);
2025-10-17 14:46:44 +02:00
const seenKeysRef = useRef(new Set());
useEffect(() => {
const tbody = tbodyRef.current;
const counter = countRef.current;
ensureFixedRowCount(tbody, 5, TABLE_SIZE);
2025-10-17 14:46:44 +02:00
abortRef.current?.abort();
abortRef.current = new AbortController();
2025-10-17 14:46:44 +02:00
function pruneAndPad() {
// remove placeholders
2025-10-17 14:46:44 +02:00
for (let i = tbody.rows.length - 1; i >= 0; i--) {
if (tbody.rows[i].classList.contains('ph')) tbody.deleteRow(i);
}
// trim overflow
while ([...tbody.rows].filter((r) => !r.classList.contains('ph')).length > TABLE_SIZE) {
2025-10-17 14:46:44 +02:00
const last = tbody.rows[tbody.rows.length - 1];
const key = last?.dataset?.key;
if (key) seenKeysRef.current.delete(key);
tbody.deleteRow(-1);
}
// pad placeholders
const real = [...tbody.rows].filter((r) => !r.classList.contains('ph')).length;
ensureFixedRowCount(tbody, 5, TABLE_SIZE);
counter.textContent = String(real);
2025-10-17 14:46:44 +02:00
}
const makeLink = (href, text, title) => {
const a = document.createElement('a');
a.className = 'linkish mono';
a.href = href;
if (title) a.title = title;
a.textContent = text;
return a;
};
2025-10-17 14:46:44 +02:00
const appendRow = (block, key) => {
2025-10-17 14:46:44 +02:00
const row = document.createElement('tr');
row.dataset.key = key;
const cellSlot = document.createElement('td');
const spanSlot = document.createElement('span');
spanSlot.className = 'mono';
spanSlot.textContent = String(block.slot);
cellSlot.appendChild(spanSlot);
2025-10-17 14:46:44 +02:00
const cellRoot = document.createElement('td');
cellRoot.appendChild(makeLink(`/block/${block.root}`, shortenHex(block.root), block.root));
2025-10-17 14:46:44 +02:00
const cellParent = document.createElement('td');
cellParent.appendChild(makeLink(`/block/${block.parent}`, shortenHex(block.parent), block.parent));
2025-10-17 14:46:44 +02:00
const cellTxCount = document.createElement('td');
const spanTx = document.createElement('span');
spanTx.className = 'mono';
spanTx.textContent = String(block.transactionCount);
cellTxCount.appendChild(spanTx);
2025-10-17 14:46:44 +02:00
const cellTime = document.createElement('td');
const spanTime = document.createElement('span');
spanTime.className = 'mono';
spanTime.title = block.time ?? '';
spanTime.textContent = formatTimestamp(block.time);
cellTime.appendChild(spanTime);
2025-10-17 14:46:44 +02:00
row.append(cellSlot, cellRoot, cellParent, cellTxCount, cellTime);
2025-10-17 14:46:44 +02:00
tbody.insertBefore(row, tbody.firstChild);
pruneAndPad();
};
2025-10-17 14:46:44 +02:00
const normalize = (raw) => {
2025-10-17 14:46:44 +02:00
const header = raw.header ?? raw;
const created = raw.created_at ?? raw.header?.created_at ?? null;
2025-10-17 14:46:44 +02:00
return {
id: Number(raw.id ?? 0),
slot: Number(header?.slot ?? raw.slot ?? 0),
root: header?.block_root ?? raw.block_root ?? '',
parent: header?.parent_block ?? raw.parent_block ?? '',
transactionCount: Array.isArray(raw.transactions)
? raw.transactions.length
: typeof raw.transaction_count === 'number'
? raw.transaction_count
: 0,
time: created,
2025-10-17 14:46:44 +02:00
};
};
2025-10-17 14:46:44 +02:00
const url = `${BLOCKS_ENDPOINT}?prefetch-limit=${encodeURIComponent(TABLE_SIZE)}`;
2025-10-17 14:46:44 +02:00
streamNdjson(
url,
2025-10-17 14:46:44 +02:00
(raw) => {
const block = normalize(raw);
2025-10-17 14:46:44 +02:00
const key = `${block.slot}:${block.id}`;
if (seenKeysRef.current.has(key)) {
pruneAndPad();
2025-10-17 14:46:44 +02:00
return;
}
seenKeysRef.current.add(key);
appendRow(block, key);
},
{
signal: abortRef.current.signal,
onError: withBenignFilter(
(e) => console.error('Blocks stream error:', e),
abortRef.current.signal
)
2025-10-17 14:46:44 +02:00
},
).catch((err) => {
if (!abortRef.current.signal.aborted) console.error('Blocks stream error:', err);
});
2025-10-17 14:46:44 +02:00
return () => abortRef.current?.abort();
2025-10-17 14:46:44 +02:00
}, []);
return h(
'div',
{ class: 'card' },
h(
'div',
{ class: 'card-header' },
h('div', null, h('strong', null, 'Blocks '), h('span', { class: 'pill', ref: countRef }, '0')),
h('div', { style: 'color:var(--muted); font-size:12px;' }, '/api/v1/blocks/stream'),
2025-10-17 14:46:44 +02:00
),
h(
'div',
{ class: 'table-wrapper' },
h(
'table',
{ class: 'table--blocks' },
h(
'colgroup',
null,
h('col', { style: 'width:90px' }),
h('col', { style: 'width:260px' }),
h('col', { style: 'width:260px' }),
h('col', { style: 'width:120px' }),
h('col', { style: 'width:180px' }),
),
h(
'thead',
null,
h('tr', null, h('th', null, 'Slot'), h('th', null, 'Block Root'), h('th', null, 'Parent'), h('th', null, 'Transactions'), h('th', null, 'Time')),
2025-10-17 14:46:44 +02:00
),
h('tbody', { ref: tbodyRef }),
),
),
);
}