feat: add foreign key constraints and metrics for blockchain node
This commit is contained in:
@ -6,6 +6,45 @@
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.page {
|
||||
padding: 1.5rem 1rem 3rem;
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.site-header__controls {
|
||||
align-items: stretch;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-header__nav {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.site-header__nav a {
|
||||
flex: 1 1 45%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.addresses__input-group,
|
||||
.receipts__input-group {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: auto;
|
||||
bottom: 1rem;
|
||||
width: min(90vw, 360px);
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.site-header__inner {
|
||||
margin: 0 auto;
|
||||
max-width: 1200px;
|
||||
@ -80,6 +119,37 @@
|
||||
padding: 2rem 1.5rem 4rem;
|
||||
}
|
||||
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
top: 1.25rem;
|
||||
right: 1.25rem;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
z-index: 1200;
|
||||
}
|
||||
|
||||
.toast {
|
||||
opacity: 0;
|
||||
transform: translateY(-6px);
|
||||
transition: opacity 150ms ease, transform 180ms ease;
|
||||
border-radius: 0.75rem;
|
||||
padding: 0.75rem 1rem;
|
||||
font-size: 0.9rem;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.toast--error {
|
||||
background: rgba(255, 102, 102, 0.16);
|
||||
border: 1px solid rgba(255, 102, 102, 0.35);
|
||||
color: #ffd3d3;
|
||||
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.toast.is-visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0px);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.site-header__inner {
|
||||
justify-content: space-between;
|
||||
|
||||
34
apps/explorer-web/src/components/notifications.ts
Normal file
34
apps/explorer-web/src/components/notifications.ts
Normal file
@ -0,0 +1,34 @@
|
||||
const TOAST_DURATION_MS = 4000;
|
||||
|
||||
let container: HTMLDivElement | null = null;
|
||||
|
||||
export function initNotifications(): void {
|
||||
if (!container) {
|
||||
container = document.createElement("div");
|
||||
container.className = "toast-container";
|
||||
document.body.appendChild(container);
|
||||
}
|
||||
}
|
||||
|
||||
export function notifyError(message: string): void {
|
||||
if (!container) {
|
||||
initNotifications();
|
||||
}
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const toast = document.createElement("div");
|
||||
toast.className = "toast toast--error";
|
||||
toast.textContent = message;
|
||||
container.appendChild(toast);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add("is-visible");
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove("is-visible");
|
||||
setTimeout(() => toast.remove(), 250);
|
||||
}, TOAST_DURATION_MS);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import { CONFIG, type DataMode } from "../config";
|
||||
import { notifyError } from "../components/notifications";
|
||||
import type {
|
||||
BlockListResponse,
|
||||
TransactionListResponse,
|
||||
@ -35,6 +36,7 @@ export async function fetchBlocks(): Promise<BlockSummary[]> {
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live block data", error);
|
||||
notifyError("Unable to load live block data. Displaying placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -54,6 +56,7 @@ export async function fetchTransactions(): Promise<TransactionSummary[]> {
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live transaction data", error);
|
||||
notifyError("Unable to load live transaction data. Displaying placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -73,6 +76,7 @@ export async function fetchAddresses(): Promise<AddressSummary[]> {
|
||||
return Array.isArray(data) ? data : data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live address data", error);
|
||||
notifyError("Unable to load live address data. Displaying placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -92,6 +96,7 @@ export async function fetchReceipts(): Promise<ReceiptSummary[]> {
|
||||
return data.items;
|
||||
} catch (error) {
|
||||
console.warn("[Explorer] Failed to fetch live receipt data", error);
|
||||
notifyError("Unable to load live receipt data. Displaying placeholders.");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -107,6 +112,7 @@ async function fetchMock<T>(resource: string): Promise<T> {
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
console.warn(`[Explorer] Failed to fetch mock data from ${url}`, error);
|
||||
notifyError("Mock data is unavailable. Please verify development assets.");
|
||||
return [] as unknown as T;
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import { addressesTitle, renderAddressesPage, initAddressesPage } from "./pages/
|
||||
import { receiptsTitle, renderReceiptsPage, initReceiptsPage } from "./pages/receipts";
|
||||
import { initDataModeToggle } from "./components/dataModeToggle";
|
||||
import { getDataMode } from "./lib/mockData";
|
||||
import { initNotifications } from "./components/notifications";
|
||||
|
||||
type PageConfig = {
|
||||
title: string;
|
||||
@ -49,14 +50,13 @@ const routes: Record<string, PageConfig> = {
|
||||
};
|
||||
|
||||
function render(): void {
|
||||
initNotifications();
|
||||
const root = document.querySelector<HTMLDivElement>("#app");
|
||||
if (!root) {
|
||||
console.warn("[Explorer] Missing #app root element");
|
||||
return;
|
||||
}
|
||||
|
||||
document.documentElement.dataset.mode = getDataMode();
|
||||
|
||||
const currentPath = window.location.pathname.replace(/\/$/, "");
|
||||
const normalizedPath = currentPath === "" ? "/" : currentPath;
|
||||
const page = routes[normalizedPath] ?? null;
|
||||
|
||||
@ -40,7 +40,6 @@ export async function initOverviewPage(): Promise<void> {
|
||||
fetchTransactions(),
|
||||
fetchReceipts(),
|
||||
]);
|
||||
|
||||
const blockStats = document.querySelector<HTMLUListElement>(
|
||||
"#overview-block-stats",
|
||||
);
|
||||
@ -54,13 +53,12 @@ export async function initOverviewPage(): Promise<void> {
|
||||
<li><strong>Time:</strong> ${new Date(latest.timestamp).toLocaleString()}</li>
|
||||
`;
|
||||
} else {
|
||||
blockStats.innerHTML = `<li class="placeholder">No mock block data available.</li>`;
|
||||
blockStats.innerHTML = `
|
||||
<li class="placeholder">No blocks available. Try switching data mode.</li>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
const txStats = document.querySelector<HTMLUListElement>(
|
||||
"#overview-transaction-stats",
|
||||
);
|
||||
const txStats = document.querySelector<HTMLUListElement>("#overview-transaction-stats");
|
||||
if (txStats) {
|
||||
if (transactions.length > 0) {
|
||||
const succeeded = transactions.filter((tx) => tx.status === "Succeeded");
|
||||
@ -70,7 +68,7 @@ export async function initOverviewPage(): Promise<void> {
|
||||
<li><strong>Pending:</strong> ${transactions.length - succeeded.length}</li>
|
||||
`;
|
||||
} else {
|
||||
txStats.innerHTML = `<li class="placeholder">No mock transaction data available.</li>`;
|
||||
txStats.innerHTML = `<li class="placeholder">No transactions available. Try switching data mode.</li>`;
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +84,7 @@ export async function initOverviewPage(): Promise<void> {
|
||||
<li><strong>Pending:</strong> ${receipts.length - attested.length}</li>
|
||||
`;
|
||||
} else {
|
||||
receiptStats.innerHTML = `<li class="placeholder">No mock receipt data available.</li>`;
|
||||
receiptStats.innerHTML = `<li class="placeholder">No receipts available. Try switching data mode.</li>`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user