mirror of
https://github.com/luxfi/explore.git
synced 2026-07-27 03:13:50 +00:00
fix(explore): real validators + honest-empty stats/DEX + dropdown clip
Investor-grade data, no fabricated placeholders: - pages/api/pchain.ts: derive node origin via new URL().origin instead of a regex that only stripped /ext/bc/C/rpc. Non-C-chain brands (hanzo: /ext/bc/hanzo/rpc) built a wrong URL -> HTML 404 -> JSON parse crash, so Validators/Total Stake/Connected/Uptime all showed 0/-. Now the P-chain proxy resolves correctly and the real validator set + stake render. Parse upstream defensively with a clear error instead of an opaque message. - lib/api/dchain/useDexData.ts: delete all hardcoded DEMO_* markets/orders/ trades/pools/overview. Return only real D-Chain (DexVM) data or honest-empty. - ui/dex/DexPage.tsx: honest empty/error states per tab (no fake rows). - stubs/stats.ts: neutralize STATS_COUNTER (title empty, value 0) so the 'Placeholder Counter 9.074M' stub can never surface as real data. - ui/pages/Stats.tsx: gate indexer-backed counters/charts on config.features.stats.isEnabled; always show the live chain-sourced Network Overview, with an honest 'being indexed' note when no stats microservice. - ui/snippets/navigation/horizontal/NavLinkGroup.tsx: popover Content defaulted to overflow-hidden + tight py, clipping the last nav item (Verified Contracts). Override to overflow-visible with padding; bottom-start placement.
This commit is contained in:
@@ -14,57 +14,6 @@ import { getDChain } from 'lib/api/luxnet/chains';
|
||||
const DEX_STALE_TIME_MS = 30_000;
|
||||
const DEX_QUERY_KEY = 'dchain:dexData' as const;
|
||||
|
||||
/* eslint-disable max-len */
|
||||
const DEMO_SYMBOLS: ReadonlyArray<DexSymbolStats> = [
|
||||
{ symbol: 'LUX/USDT', lastPrice: '24.85', change24h: 3.42, volume24h: '1,245,890', high24h: '25.10', low24h: '23.90', trades24h: 482 },
|
||||
{ symbol: 'LUX/USDC', lastPrice: '24.83', change24h: 3.18, volume24h: '892,340', high24h: '25.05', low24h: '23.88', trades24h: 356 },
|
||||
{ symbol: 'ZOO/LUX', lastPrice: '0.0842', change24h: -1.25, volume24h: '456,120', high24h: '0.0870', low24h: '0.0830', trades24h: 198 },
|
||||
{ symbol: 'HNZ/LUX', lastPrice: '0.152', change24h: 7.04, volume24h: '334,560', high24h: '0.158', low24h: '0.140', trades24h: 145 },
|
||||
{ symbol: 'SPC/LUX', lastPrice: '0.0215', change24h: -0.47, volume24h: '123,450', high24h: '0.0220', low24h: '0.0210', trades24h: 87 },
|
||||
{ symbol: 'PARS/LUX', lastPrice: '0.0034', change24h: 12.50, volume24h: '89,230', high24h: '0.0036', low24h: '0.0030', trades24h: 64 },
|
||||
];
|
||||
|
||||
const DEMO_ORDERS: ReadonlyArray<DexOrder> = [
|
||||
{ id: 'ord-001', symbol: 'LUX/USDT', side: 'buy', price: '24.50', quantity: '100.00', filled: '0.00', status: 'open', maker: '0x1a2b...3c4d', timestamp: '2026-02-26T10:30:00Z' },
|
||||
{ id: 'ord-002', symbol: 'LUX/USDT', side: 'buy', price: '24.45', quantity: '250.00', filled: '50.00', status: 'partial', maker: '0x5e6f...7g8h', timestamp: '2026-02-26T10:28:00Z' },
|
||||
{ id: 'ord-003', symbol: 'LUX/USDT', side: 'sell', price: '25.10', quantity: '75.00', filled: '0.00', status: 'open', maker: '0x9i0j...1k2l', timestamp: '2026-02-26T10:25:00Z' },
|
||||
{ id: 'ord-004', symbol: 'LUX/USDC', side: 'sell', price: '25.05', quantity: '150.00', filled: '150.00', status: 'filled', maker: '0x3m4n...5o6p', timestamp: '2026-02-26T10:20:00Z' },
|
||||
{ id: 'ord-005', symbol: 'ZOO/LUX', side: 'buy', price: '0.0840', quantity: '5000.00', filled: '0.00', status: 'open', maker: '0x7q8r...9s0t', timestamp: '2026-02-26T10:18:00Z' },
|
||||
{ id: 'ord-006', symbol: 'HNZ/LUX', side: 'buy', price: '0.150', quantity: '2000.00', filled: '800.00', status: 'partial', maker: '0x1u2v...3w4x', timestamp: '2026-02-26T10:15:00Z' },
|
||||
{ id: 'ord-007', symbol: 'LUX/USDT', side: 'sell', price: '25.20', quantity: '300.00', filled: '0.00', status: 'open', maker: '0x5y6z...7a8b', timestamp: '2026-02-26T10:12:00Z' },
|
||||
{ id: 'ord-008', symbol: 'SPC/LUX', side: 'buy', price: '0.0210', quantity: '10000.00', filled: '0.00', status: 'open', maker: '0x9c0d...1e2f', timestamp: '2026-02-26T10:10:00Z' },
|
||||
{ id: 'ord-009', symbol: 'LUX/USDT', side: 'sell', price: '25.00', quantity: '120.00', filled: '120.00', status: 'filled', maker: '0x3g4h...5i6j', timestamp: '2026-02-26T10:05:00Z' },
|
||||
{ id: 'ord-010', symbol: 'PARS/LUX', side: 'buy', price: '0.0033', quantity: '50000.00', filled: '0.00', status: 'open', maker: '0x7k8l...9m0n', timestamp: '2026-02-26T10:00:00Z' },
|
||||
];
|
||||
|
||||
const DEMO_TRADES: ReadonlyArray<DexTrade> = [
|
||||
{ id: 'trd-001', symbol: 'LUX/USDT', price: '24.85', quantity: '50.00', buyer: '0x1a2b...3c4d', seller: '0x3m4n...5o6p', fee: '0.25', timestamp: '2026-02-26T10:29:00Z', blockHeight: 1042 },
|
||||
{ id: 'trd-002', symbol: 'LUX/USDC', price: '24.83', quantity: '100.00', buyer: '0x5e6f...7g8h', seller: '0x9i0j...1k2l', fee: '0.50', timestamp: '2026-02-26T10:27:00Z', blockHeight: 1041 },
|
||||
{ id: 'trd-003', symbol: 'ZOO/LUX', price: '0.0842', quantity: '2000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.84', timestamp: '2026-02-26T10:24:00Z', blockHeight: 1040 },
|
||||
{ id: 'trd-004', symbol: 'HNZ/LUX', price: '0.152', quantity: '500.00', buyer: '0x5y6z...7a8b', seller: '0x9c0d...1e2f', fee: '0.38', timestamp: '2026-02-26T10:21:00Z', blockHeight: 1039 },
|
||||
{ id: 'trd-005', symbol: 'LUX/USDT', price: '24.80', quantity: '75.00', buyer: '0x3g4h...5i6j', seller: '0x7k8l...9m0n', fee: '0.38', timestamp: '2026-02-26T10:19:00Z', blockHeight: 1038 },
|
||||
{ id: 'trd-006', symbol: 'SPC/LUX', price: '0.0215', quantity: '8000.00', buyer: '0x1a2b...3c4d', seller: '0x5e6f...7g8h', fee: '0.86', timestamp: '2026-02-26T10:16:00Z', blockHeight: 1037 },
|
||||
{ id: 'trd-007', symbol: 'LUX/USDT', price: '24.75', quantity: '200.00', buyer: '0x9i0j...1k2l', seller: '0x3m4n...5o6p', fee: '1.00', timestamp: '2026-02-26T10:13:00Z', blockHeight: 1036 },
|
||||
{ id: 'trd-008', symbol: 'PARS/LUX', price: '0.0034', quantity: '25000.00', buyer: '0x7q8r...9s0t', seller: '0x1u2v...3w4x', fee: '0.43', timestamp: '2026-02-26T10:11:00Z', blockHeight: 1035 },
|
||||
];
|
||||
|
||||
const DEMO_POOLS: ReadonlyArray<DexPool> = [
|
||||
{ id: 'pool-001', tokenA: 'LUX', tokenB: 'USDT', reserveA: '52,340', reserveB: '1,300,652', tvl: '2,601,304', volume24h: '1,245,890', fee: '0.30%' },
|
||||
{ id: 'pool-002', tokenA: 'LUX', tokenB: 'USDC', reserveA: '38,120', reserveB: '947,162', tvl: '1,894,324', volume24h: '892,340', fee: '0.30%' },
|
||||
{ id: 'pool-003', tokenA: 'ZOO', tokenB: 'LUX', reserveA: '2,450,000', reserveB: '206,190', tvl: '412,380', volume24h: '456,120', fee: '0.50%' },
|
||||
{ id: 'pool-004', tokenA: 'HNZ', tokenB: 'LUX', reserveA: '1,200,000', reserveB: '182,400', tvl: '364,800', volume24h: '334,560', fee: '0.50%' },
|
||||
{ id: 'pool-005', tokenA: 'SPC', tokenB: 'LUX', reserveA: '5,800,000', reserveB: '124,700', tvl: '249,400', volume24h: '123,450', fee: '1.00%' },
|
||||
{ id: 'pool-006', tokenA: 'PARS', tokenB: 'LUX', reserveA: '18,000,000', reserveB: '61,200', tvl: '122,400', volume24h: '89,230', fee: '1.00%' },
|
||||
];
|
||||
/* eslint-enable max-len */
|
||||
|
||||
const DEMO_OVERVIEW: DexOverviewStats = {
|
||||
totalPairs: 6,
|
||||
volume24h: '3,141,590',
|
||||
activeOrders: 7,
|
||||
tradesToday: 1332,
|
||||
};
|
||||
|
||||
export interface UseDexDataResult {
|
||||
readonly symbols: ReadonlyArray<DexSymbolStats>;
|
||||
readonly orders: ReadonlyArray<DexOrder>;
|
||||
@@ -95,48 +44,52 @@ const EMPTY_OVERVIEW: DexOverviewStats = {
|
||||
tradesToday: 0,
|
||||
};
|
||||
|
||||
const EMPTY_PAYLOAD: DexDataPayload = {
|
||||
symbols: EMPTY_SYMBOLS,
|
||||
orders: EMPTY_ORDERS,
|
||||
trades: EMPTY_TRADES,
|
||||
pools: EMPTY_POOLS,
|
||||
overview: EMPTY_OVERVIEW,
|
||||
};
|
||||
|
||||
// Read DEX state directly from the D-Chain (DexVM) over the luxnet SDK. When the
|
||||
// D-Chain is not yet serving market data we return an honest-empty payload — the
|
||||
// UI must never present fabricated markets/orders/trades/pools to users.
|
||||
async function fetchDexData(): Promise<DexDataPayload> {
|
||||
// Try luxnet SDK D-Chain RPC first
|
||||
try {
|
||||
const dchain = getDChain();
|
||||
const stats = await dchain.getStats();
|
||||
const dchain = getDChain();
|
||||
const stats = await dchain.getStats();
|
||||
|
||||
if (stats && stats.trades24h > 0) {
|
||||
const [ pools, markets ] = await Promise.all([
|
||||
dchain.getPools(),
|
||||
dchain.getMarkets(),
|
||||
]);
|
||||
if (!stats || (stats.trades24h ?? 0) <= 0) {
|
||||
return EMPTY_PAYLOAD;
|
||||
}
|
||||
|
||||
return {
|
||||
symbols: DEMO_SYMBOLS,
|
||||
orders: DEMO_ORDERS,
|
||||
trades: DEMO_TRADES,
|
||||
pools: pools.length > 0 ? pools.map((p, i) => ({
|
||||
id: `pool-${ i }`,
|
||||
tokenA: String(p.tokenA),
|
||||
tokenB: String(p.tokenB),
|
||||
reserveA: String(p.reserveA),
|
||||
reserveB: String(p.reserveB),
|
||||
tvl: String(p.totalLiquidity),
|
||||
volume24h: DEMO_POOLS[i]?.volume24h ?? '0',
|
||||
fee: String(p.fee),
|
||||
})) : DEMO_POOLS,
|
||||
overview: {
|
||||
totalPairs: markets.length || DEMO_OVERVIEW.totalPairs,
|
||||
volume24h: stats.volume24h || DEMO_OVERVIEW.volume24h,
|
||||
activeOrders: DEMO_OVERVIEW.activeOrders,
|
||||
tradesToday: stats.trades24h || DEMO_OVERVIEW.tradesToday,
|
||||
},
|
||||
};
|
||||
}
|
||||
} catch { /* D-Chain RPC unavailable — fall through to demo data */ }
|
||||
const [ pools, markets ] = await Promise.all([
|
||||
dchain.getPools(),
|
||||
dchain.getMarkets(),
|
||||
]);
|
||||
|
||||
return {
|
||||
symbols: DEMO_SYMBOLS,
|
||||
orders: DEMO_ORDERS,
|
||||
trades: DEMO_TRADES,
|
||||
pools: DEMO_POOLS,
|
||||
overview: DEMO_OVERVIEW,
|
||||
// Markets/orders/trades are surfaced once the DexVM indexer exposes them;
|
||||
// until then the SDK returns nothing and these stay empty rather than faked.
|
||||
symbols: EMPTY_SYMBOLS,
|
||||
orders: EMPTY_ORDERS,
|
||||
trades: EMPTY_TRADES,
|
||||
pools: pools.map((p, i) => ({
|
||||
id: `pool-${ i }`,
|
||||
tokenA: String(p.tokenA),
|
||||
tokenB: String(p.tokenB),
|
||||
reserveA: String(p.reserveA),
|
||||
reserveB: String(p.reserveB),
|
||||
tvl: String(p.totalLiquidity),
|
||||
volume24h: '0',
|
||||
fee: String(p.fee),
|
||||
})),
|
||||
overview: {
|
||||
totalPairs: markets.length,
|
||||
volume24h: stats.volume24h ?? '0',
|
||||
activeOrders: 0,
|
||||
tradesToday: stats.trades24h ?? 0,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +98,7 @@ export function useDexData(): UseDexDataResult {
|
||||
queryKey: [ DEX_QUERY_KEY ],
|
||||
queryFn: fetchDexData,
|
||||
staleTime: DEX_STALE_TIME_MS,
|
||||
retry: 1,
|
||||
});
|
||||
|
||||
const symbols = React.useMemo(
|
||||
|
||||
+28
-3
@@ -5,12 +5,24 @@ import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { getEnvValue } from 'configs/app/utils';
|
||||
|
||||
const CCHAIN_RPC_PATTERN = /\/ext\/bc\/C\/rpc$/;
|
||||
const TIMEOUT_MS = 10_000;
|
||||
|
||||
// Derive the node's API origin from the chain RPC URL. The RPC URL points at a
|
||||
// specific EVM chain (e.g. `…/ext/bc/C/rpc` or `…/ext/bc/hanzo/rpc`); the P-chain
|
||||
// lives at `<origin>/ext/bc/P`, so we must reduce to the scheme+host origin and
|
||||
// never naively concatenate onto the full RPC path (which produced a malformed
|
||||
// URL → HTML 404 → "Unexpected non-whitespace character after JSON" for every
|
||||
// non-C-chain brand).
|
||||
function getApiBase(): string {
|
||||
const rpcUrl = getEnvValue('NEXT_PUBLIC_NETWORK_RPC_URL') ?? '';
|
||||
return rpcUrl.replace(CCHAIN_RPC_PATTERN, '');
|
||||
if (!rpcUrl) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
return new URL(rpcUrl).origin;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
const handler = async(req: NextApiRequest, res: NextApiResponse): Promise<void> => {
|
||||
@@ -33,7 +45,20 @@ const handler = async(req: NextApiRequest, res: NextApiResponse): Promise<void>
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
// The node returns JSON for valid P-chain RPC; a wrong URL or gateway
|
||||
// error yields HTML. Read as text and parse defensively so the client gets a
|
||||
// clear error rather than an opaque "non-whitespace character" parse crash.
|
||||
const raw = await response.text();
|
||||
let data: unknown;
|
||||
try {
|
||||
data = JSON.parse(raw);
|
||||
} catch {
|
||||
res.status(502).json({
|
||||
error: 'P-chain request failed',
|
||||
message: `Upstream returned non-JSON (HTTP ${ response.status }) from ${ base }/ext/bc/P`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.status(200).json(data);
|
||||
} catch (error) {
|
||||
res.status(502).json({
|
||||
|
||||
+6
-3
@@ -95,11 +95,14 @@ export const STATS_CHARTS = {
|
||||
sections: [ STATS_CHARTS_SECTION ],
|
||||
};
|
||||
|
||||
// Skeleton placeholder only — these fields are masked by the loading skeleton
|
||||
// and must never read as real data if they ever surface. Neutral title, zero
|
||||
// value (no fabricated "9.074M" counter shown to users).
|
||||
export const STATS_COUNTER: stats.Counter = {
|
||||
id: 'stub',
|
||||
value: '9074405',
|
||||
title: 'Placeholder Counter',
|
||||
description: 'Placeholder description',
|
||||
value: '0',
|
||||
title: '',
|
||||
description: '',
|
||||
units: '',
|
||||
};
|
||||
|
||||
|
||||
+34
-8
@@ -2,14 +2,13 @@
|
||||
// Displays market stats, orderbook, trade history, and liquidity pools
|
||||
// sourced from the DexVM indexer.
|
||||
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import React from 'react';
|
||||
|
||||
import { cn } from 'lib/utils/cn';
|
||||
|
||||
import { useDexData } from 'lib/api/dchain';
|
||||
import type { DexOrder, DexTrade, DexPool, DexSymbolStats } from 'lib/api/dchain';
|
||||
import { Skeleton } from '@luxfi/ui/skeleton';
|
||||
import { Tag } from '@luxfi/ui/tag';
|
||||
import { cn } from 'lib/utils/cn';
|
||||
import PageTitle from 'ui/shared/Page/PageTitle';
|
||||
|
||||
// ── Constants ──
|
||||
@@ -23,7 +22,8 @@ const TAB_IDS = {
|
||||
|
||||
type TabId = typeof TAB_IDS[keyof typeof TAB_IDS];
|
||||
|
||||
const ROW_BASE = 'flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] hover:bg-gray-50 dark:hover:bg-white/5 transition-[background] duration-150 gap-4 flex-wrap lg:flex-nowrap';
|
||||
const ROW_BASE = 'flex items-center py-3 px-4 border-b border-[var(--color-border-divider)] ' +
|
||||
'hover:bg-gray-50 dark:hover:bg-white/5 transition-[background] duration-150 gap-4 flex-wrap lg:flex-nowrap';
|
||||
const HEADER_BASE = 'hidden lg:flex px-4 py-2 gap-4 border-b border-[var(--color-border-divider)]';
|
||||
const COL_HEADER = 'text-[var(--color-text-secondary)] font-semibold text-xs uppercase tracking-wider';
|
||||
|
||||
@@ -39,7 +39,9 @@ const TabButton = ({ label, isActive, onClick }: TabButtonProps) => (
|
||||
<button
|
||||
className={ cn(
|
||||
'px-4 py-2 text-sm bg-transparent cursor-pointer transition-all duration-150 border-b-2 hover:text-[var(--color-text-primary)]',
|
||||
isActive ? 'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' : 'font-normal text-[var(--color-text-secondary)] border-transparent',
|
||||
isActive ?
|
||||
'font-semibold text-[var(--color-text-primary)] border-[var(--color-text-primary)]' :
|
||||
'font-normal text-[var(--color-text-secondary)] border-transparent',
|
||||
) }
|
||||
onClick={ onClick }
|
||||
>
|
||||
@@ -119,7 +121,13 @@ const OrderRow = ({ order }: OrderRowProps) => {
|
||||
<span className="text-sm text-[var(--color-text-primary)]">{ order.symbol }</span>
|
||||
</div>
|
||||
<div className="min-w-[60px] shrink-0">
|
||||
<Tag size="sm" variant="subtle" className={ isBuy ? 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' : 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }>
|
||||
<Tag
|
||||
size="sm"
|
||||
variant="subtle"
|
||||
className={ isBuy ?
|
||||
'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' :
|
||||
'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' }
|
||||
>
|
||||
{ order.side.toUpperCase() }
|
||||
</Tag>
|
||||
</div>
|
||||
@@ -221,11 +229,25 @@ const LoadingSkeleton = () => (
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Empty / error states ──
|
||||
// The DEX (D-Chain / DexVM) is not yet serving live market data on every
|
||||
// network. We render an honest message rather than fabricated rows.
|
||||
|
||||
const EmptyRow = ({ message }: { readonly message: string }) => (
|
||||
<div className="px-4 py-10 text-center text-sm text-[var(--color-text-secondary)]">
|
||||
{ message }
|
||||
</div>
|
||||
);
|
||||
|
||||
// ── Main component ──
|
||||
|
||||
const DexPage = () => {
|
||||
const [ activeTab, setActiveTab ] = React.useState<TabId>(TAB_IDS.markets);
|
||||
const { symbols, orders, trades, pools, overview, isLoading } = useDexData();
|
||||
const { symbols, orders, trades, pools, overview, isLoading, isError } = useDexData();
|
||||
|
||||
const emptyMessage = isError ?
|
||||
'DEX data is temporarily unavailable.' :
|
||||
'No market data yet — the D-Chain DEX is not reporting activity.';
|
||||
|
||||
const handleMarketsClick = React.useCallback(() => setActiveTab(TAB_IDS.markets), []);
|
||||
const handleOrderbookClick = React.useCallback(() => setActiveTab(TAB_IDS.orderbook), []);
|
||||
@@ -272,6 +294,7 @@ const DexPage = () => {
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Trades</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && symbols.length === 0 && <EmptyRow message={ emptyMessage }/> }
|
||||
{ !isLoading && symbols.map((stat) => (
|
||||
<SymbolRow key={ stat.symbol } stat={ stat }/>
|
||||
)) }
|
||||
@@ -292,6 +315,7 @@ const DexPage = () => {
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Status</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && orders.length === 0 && <EmptyRow message={ emptyMessage }/> }
|
||||
{ !isLoading && orders.map((order) => (
|
||||
<OrderRow key={ order.id } order={ order }/>
|
||||
)) }
|
||||
@@ -311,6 +335,7 @@ const DexPage = () => {
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Time</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && trades.length === 0 && <EmptyRow message={ emptyMessage }/> }
|
||||
{ !isLoading && trades.map((trade) => (
|
||||
<TradeRow key={ trade.id } trade={ trade }/>
|
||||
)) }
|
||||
@@ -329,6 +354,7 @@ const DexPage = () => {
|
||||
<div className={ cn(COL_HEADER, 'ml-auto text-right') }>Fee</div>
|
||||
</div>
|
||||
{ isLoading && <LoadingSkeleton/> }
|
||||
{ !isLoading && pools.length === 0 && <EmptyRow message={ emptyMessage }/> }
|
||||
{ !isLoading && pools.map((pool) => (
|
||||
<PoolRow key={ pool.id } pool={ pool }/>
|
||||
)) }
|
||||
|
||||
+44
-24
@@ -10,6 +10,12 @@ import NumberWidgetsList from '../stats/NumberWidgetsList';
|
||||
import StatsFilters from '../stats/StatsFilters';
|
||||
import useStats from '../stats/useStats';
|
||||
|
||||
// Aggregate counters and historical charts are served by the Blockscout stats
|
||||
// microservice. On networks where it is not deployed we show the live,
|
||||
// chain-sourced Network Overview (validators / chains / stake from the P-chain)
|
||||
// plus an honest note — never fabricated counters.
|
||||
const hasStatsService = config.features.stats.isEnabled;
|
||||
|
||||
const Stats = () => {
|
||||
const {
|
||||
isPlaceholderData,
|
||||
@@ -34,32 +40,46 @@ const Stats = () => {
|
||||
|
||||
<NetworkStats/>
|
||||
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<NumberWidgetsList/>
|
||||
</div>
|
||||
{ hasStatsService ? (
|
||||
<>
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<NumberWidgetsList/>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<StatsFilters
|
||||
isLoading={ isPlaceholderData }
|
||||
initialFilterValue={ initialFilterQuery }
|
||||
sections={ sections }
|
||||
currentSection={ currentSection }
|
||||
onSectionChange={ handleSectionChange }
|
||||
interval={ interval }
|
||||
onIntervalChange={ handleIntervalChange }
|
||||
onFilterInputChange={ handleFilterChange }
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6 sm:mb-8">
|
||||
<StatsFilters
|
||||
isLoading={ isPlaceholderData }
|
||||
initialFilterValue={ initialFilterQuery }
|
||||
sections={ sections }
|
||||
currentSection={ currentSection }
|
||||
onSectionChange={ handleSectionChange }
|
||||
interval={ interval }
|
||||
onIntervalChange={ handleIntervalChange }
|
||||
onFilterInputChange={ handleFilterChange }
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChartsWidgetsList
|
||||
initialFilterQuery={ initialFilterQuery }
|
||||
isError={ isError }
|
||||
isPlaceholderData={ isPlaceholderData }
|
||||
charts={ displayedCharts }
|
||||
interval={ interval }
|
||||
sections={ sections }
|
||||
selectedSectionId={ currentSection }
|
||||
/>
|
||||
<ChartsWidgetsList
|
||||
initialFilterQuery={ initialFilterQuery }
|
||||
isError={ isError }
|
||||
isPlaceholderData={ isPlaceholderData }
|
||||
charts={ displayedCharts }
|
||||
interval={ interval }
|
||||
sections={ sections }
|
||||
selectedSectionId={ currentSection }
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<div className="mt-8 rounded-lg border border-[var(--color-border-divider)] px-6 py-10 text-center">
|
||||
<div className="text-base font-medium text-[var(--color-text-primary)]">
|
||||
Historical statistics are being indexed
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-[var(--color-text-secondary)]">
|
||||
Live network data is shown above. Aggregate counters and charts will
|
||||
appear here once the analytics indexer is online for { config.chain.name }.
|
||||
</div>
|
||||
</div>
|
||||
) }
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Separator } from '@luxfi/ui/separator';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import React from 'react';
|
||||
|
||||
import type { NavGroupItem } from 'types/client/navigation';
|
||||
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import { Separator } from '@luxfi/ui/separator';
|
||||
import { Tooltip } from '@luxfi/ui/tooltip';
|
||||
import { useDisclosure } from 'toolkit/hooks/useDisclosure';
|
||||
import { Link } from 'toolkit/next/link';
|
||||
import IconSvg from 'ui/shared/IconSvg';
|
||||
|
||||
import LightningLabel from '../LightningLabel';
|
||||
@@ -55,8 +55,12 @@ const NavLinkGroup = ({ item }: Props) => {
|
||||
content={ content }
|
||||
onOpenChange={ onOpenChange }
|
||||
lazyMount={ false }
|
||||
// The popover Content defaults to `overflow-hidden` + tight `py-2`, which
|
||||
// clips the last nav item (e.g. "Verified Contracts") of a tall menu list.
|
||||
// Override to let the full list render and give it breathing room.
|
||||
contentProps={{ className: 'overflow-visible px-1.5 py-2' }}
|
||||
positioning={{
|
||||
placement: 'bottom',
|
||||
placement: 'bottom-start',
|
||||
offset: { mainAxis: 8 },
|
||||
}}
|
||||
interactive
|
||||
|
||||
Reference in New Issue
Block a user