2025-10-17 14:46:44 +02:00
|
|
|
import { h } from 'preact';
|
|
|
|
|
import { useEffect, useRef } from 'preact/hooks';
|
2025-10-17 15:33:00 +02:00
|
|
|
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);
|
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(() => {
|
|
|
|
|
const tbody = tbodyRef.current;
|
2025-10-17 15:33:00 +02:00
|
|
|
const counter = countRef.current;
|
|
|
|
|
ensureFixedRowCount(tbody, 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-17 15:33:00 +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);
|
|
|
|
|
}
|
2025-10-17 15:33:00 +02:00
|
|
|
// 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);
|
|
|
|
|
}
|
2025-10-17 15:33:00 +02:00
|
|
|
// 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
|
|
|
}
|
|
|
|
|
|
2025-10-17 15:33:00 +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
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
const appendRow = (block, key) => {
|
2025-10-17 14:46:44 +02:00
|
|
|
const row = document.createElement('tr');
|
|
|
|
|
row.dataset.key = key;
|
|
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
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
|
|
|
|
2025-10-17 15:33:00 +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
|
|
|
|
2025-10-17 15:33:00 +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
|
|
|
|
2025-10-17 15:33:00 +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
|
|
|
|
2025-10-17 15:33:00 +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
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
row.append(cellSlot, cellRoot, cellParent, cellTxCount, cellTime);
|
2025-10-17 14:46:44 +02:00
|
|
|
tbody.insertBefore(row, tbody.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-17 14:46:44 +02:00
|
|
|
const header = raw.header ?? raw;
|
2025-10-17 15:33:00 +02:00
|
|
|
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'
|
2025-10-17 15:33:00 +02:00
|
|
|
? raw.transaction_count
|
|
|
|
|
: 0,
|
|
|
|
|
time: created,
|
2025-10-17 14:46:44 +02:00
|
|
|
};
|
2025-10-17 15:33:00 +02:00
|
|
|
};
|
2025-10-17 14:46:44 +02:00
|
|
|
|
2025-10-17 15:33:00 +02:00
|
|
|
const url = `${BLOCKS_ENDPOINT}?prefetch-limit=${encodeURIComponent(TABLE_SIZE)}`;
|
2025-10-17 14:46:44 +02:00
|
|
|
streamNdjson(
|
2025-10-17 15:33:00 +02:00
|
|
|
url,
|
2025-10-17 14:46:44 +02:00
|
|
|
(raw) => {
|
2025-10-17 15:33:00 +02:00
|
|
|
const block = normalize(raw);
|
2025-10-17 14:46:44 +02:00
|
|
|
const key = `${block.slot}:${block.id}`;
|
|
|
|
|
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);
|
|
|
|
|
appendRow(block, key);
|
|
|
|
|
},
|
|
|
|
|
{
|
2025-10-17 15:33:00 +02:00
|
|
|
signal: abortRef.current.signal,
|
|
|
|
|
onError: withBenignFilter(
|
|
|
|
|
(e) => console.error('Blocks stream error:', e),
|
|
|
|
|
abortRef.current.signal
|
|
|
|
|
)
|
2025-10-17 14:46:44 +02:00
|
|
|
},
|
2025-10-17 15:33:00 +02:00
|
|
|
).catch((err) => {
|
|
|
|
|
if (!abortRef.current.signal.aborted) console.error('Blocks stream error:', err);
|
|
|
|
|
});
|
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')),
|
|
|
|
|
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,
|
2025-10-17 15:33:00 +02:00
|
|
|
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 }),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|