141 lines
6.4 KiB
TypeScript
141 lines
6.4 KiB
TypeScript
import Link from 'next/link'
|
|
import { Card } from '@/libs/frontend-ui-primitives'
|
|
import EntityBadge from '@/components/common/EntityBadge'
|
|
import type { ChainActivityContext } from '@/utils/activityContext'
|
|
import { formatRelativeAge, formatTimestamp } from '@/utils/format'
|
|
import { Explain, useUiMode } from './UiModeContext'
|
|
|
|
function resolveTone(state: ChainActivityContext['state']): 'success' | 'warning' | 'neutral' {
|
|
switch (state) {
|
|
case 'active':
|
|
return 'success'
|
|
case 'low':
|
|
case 'inactive':
|
|
return 'warning'
|
|
default:
|
|
return 'neutral'
|
|
}
|
|
}
|
|
|
|
function resolveLabel(state: ChainActivityContext['state']): string {
|
|
switch (state) {
|
|
case 'active':
|
|
return 'active'
|
|
case 'low':
|
|
return 'low activity'
|
|
case 'inactive':
|
|
return 'inactive'
|
|
default:
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
function renderHeadline(context: ChainActivityContext): string {
|
|
if (context.transaction_visibility_unavailable) {
|
|
return 'Transaction index freshness is currently unavailable, while chain-head visibility remains live.'
|
|
}
|
|
if (context.state === 'unknown') {
|
|
return 'Recent activity context is temporarily unavailable.'
|
|
}
|
|
if (context.state === 'active') {
|
|
return 'Recent transactions are close to the visible chain tip.'
|
|
}
|
|
if (context.head_is_idle) {
|
|
return 'The chain head is advancing, but the latest visible transaction is older than the current tip.'
|
|
}
|
|
return 'Recent transaction activity is sparse right now.'
|
|
}
|
|
|
|
export default function ActivityContextPanel({
|
|
context,
|
|
title = 'Chain Activity Context',
|
|
compact = false,
|
|
}: {
|
|
context: ChainActivityContext
|
|
title?: string
|
|
compact?: boolean
|
|
}) {
|
|
const { mode } = useUiMode()
|
|
const tone = resolveTone(context.state)
|
|
const dualTimelineLabel =
|
|
context.latest_block_timestamp && context.latest_transaction_timestamp
|
|
? `${formatRelativeAge(context.latest_block_timestamp)} head · ${formatRelativeAge(context.latest_transaction_timestamp)} latest tx`
|
|
: 'Dual timeline unavailable'
|
|
|
|
return (
|
|
<Card className="border border-sky-200 bg-sky-50/60 dark:border-sky-900/40 dark:bg-sky-950/20" title={title}>
|
|
<div className={`flex flex-col ${compact ? 'gap-3' : 'gap-4'}`}>
|
|
<div className="flex flex-col gap-3 lg:flex-row lg:items-start lg:justify-between">
|
|
<div>
|
|
<div className="text-base font-semibold text-gray-900 dark:text-white">{renderHeadline(context)}</div>
|
|
<Explain>
|
|
<p className="mt-2 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
|
Use the transaction tip and last non-empty block below to distinguish a quiet chain from a broken explorer.
|
|
</p>
|
|
</Explain>
|
|
</div>
|
|
<EntityBadge label={resolveLabel(context.state)} tone={tone} />
|
|
</div>
|
|
|
|
{compact ? (
|
|
<div className="rounded-2xl border border-white/50 bg-white/70 px-4 py-3 text-sm leading-6 text-gray-700 dark:border-white/10 dark:bg-black/10 dark:text-gray-300">
|
|
Latest block{' '}
|
|
<span className="font-semibold text-gray-900 dark:text-white">
|
|
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
|
|
</span>{' '}
|
|
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
|
|
<span className="font-semibold text-gray-900 dark:text-white">
|
|
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
|
|
</span>{' '}
|
|
{formatRelativeAge(context.latest_transaction_timestamp)}.{' '}
|
|
{mode === 'guided'
|
|
? 'Use the block gap and last non-empty block above to tell low activity apart from an indexing issue.'
|
|
: dualTimelineLabel}
|
|
</div>
|
|
) : (
|
|
<div className="grid gap-3 lg:grid-cols-[minmax(0,1.4fr)_minmax(0,1fr)]">
|
|
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Timeline Summary</div>
|
|
<div className="mt-2 text-sm leading-6 text-gray-700 dark:text-gray-300">
|
|
Latest block{' '}
|
|
<span className="font-semibold text-gray-900 dark:text-white">
|
|
{context.latest_block_number != null ? `#${context.latest_block_number}` : 'Unknown'}
|
|
</span>{' '}
|
|
was {formatRelativeAge(context.latest_block_timestamp)}. Latest visible transaction was{' '}
|
|
<span className="font-semibold text-gray-900 dark:text-white">
|
|
{context.latest_transaction_block_number != null ? `#${context.latest_transaction_block_number}` : 'Unknown'}
|
|
</span>{' '}
|
|
{formatRelativeAge(context.latest_transaction_timestamp)}.
|
|
</div>
|
|
</div>
|
|
<div className="rounded-2xl border border-white/50 bg-white/70 p-4 dark:border-white/10 dark:bg-black/10">
|
|
<div className="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">Visibility Signal</div>
|
|
<div className="mt-2 text-sm leading-6 text-gray-700 dark:text-gray-300">
|
|
{mode === 'guided'
|
|
? 'Use the block gap and the last non-empty block above to tell low activity apart from an indexing issue.'
|
|
: dualTimelineLabel}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className={`flex flex-wrap text-sm text-gray-600 dark:text-gray-400 ${compact ? 'gap-x-4 gap-y-2' : 'gap-4'}`}>
|
|
{context.latest_transaction_block_number != null ? (
|
|
<Link href={`/blocks/${context.latest_transaction_block_number}`} className="text-primary-600 hover:underline">
|
|
Open latest transaction block →
|
|
</Link>
|
|
) : null}
|
|
{context.last_non_empty_block_number != null ? (
|
|
<Link href={`/blocks/${context.last_non_empty_block_number}`} className="text-primary-600 hover:underline">
|
|
Open last non-empty block →
|
|
</Link>
|
|
) : null}
|
|
{context.latest_transaction_timestamp ? (
|
|
<span>Latest visible transaction time: {formatTimestamp(context.latest_transaction_timestamp)}</span>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)
|
|
}
|