refactor: rename SolaceScanScout to Solace and update related configurations
- Updated branding from "SolaceScanScout" to "Solace" across various files including deployment scripts, API responses, and documentation. - Changed default base URL for Playwright tests and updated security headers to reflect the new branding. - Enhanced README and API documentation to include new authentication endpoints and product access details. This refactor aligns the project branding and improves clarity in the API documentation.
This commit is contained in:
315
frontend/src/components/explorer/OperationsHubPage.tsx
Normal file
315
frontend/src/components/explorer/OperationsHubPage.tsx
Normal file
@@ -0,0 +1,315 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { Card } from '@/libs/frontend-ui-primitives'
|
||||
import { explorerFeaturePages } from '@/data/explorerOperations'
|
||||
import { configApi, type CapabilitiesResponse, type NetworksConfigResponse, type TokenListResponse } from '@/services/api/config'
|
||||
import { getMissionControlRelays, missionControlApi, type MissionControlBridgeStatusResponse } from '@/services/api/missionControl'
|
||||
import { routesApi, type RouteMatrixResponse } from '@/services/api/routes'
|
||||
|
||||
function relativeAge(isoString?: string): string {
|
||||
if (!isoString) return 'Unknown'
|
||||
const parsed = Date.parse(isoString)
|
||||
if (!Number.isFinite(parsed)) return 'Unknown'
|
||||
const seconds = Math.max(0, Math.round((Date.now() - parsed) / 1000))
|
||||
if (seconds < 60) return `${seconds}s ago`
|
||||
const minutes = Math.round(seconds / 60)
|
||||
if (minutes < 60) return `${minutes}m ago`
|
||||
const hours = Math.round(minutes / 60)
|
||||
return `${hours}h ago`
|
||||
}
|
||||
|
||||
function ActionLink({
|
||||
href,
|
||||
label,
|
||||
external,
|
||||
}: {
|
||||
href: string
|
||||
label: string
|
||||
external?: boolean
|
||||
}) {
|
||||
const className = 'inline-flex items-center text-sm font-semibold text-primary-600 hover:underline'
|
||||
const text = `${label} ->`
|
||||
|
||||
if (external) {
|
||||
return (
|
||||
<a href={href} className={className} target="_blank" rel="noopener noreferrer">
|
||||
{text}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href={href} className={className}>
|
||||
{text}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
interface OperationsHubPageProps {
|
||||
initialBridgeStatus?: MissionControlBridgeStatusResponse | null
|
||||
initialRouteMatrix?: RouteMatrixResponse | null
|
||||
initialNetworksConfig?: NetworksConfigResponse | null
|
||||
initialTokenList?: TokenListResponse | null
|
||||
initialCapabilities?: CapabilitiesResponse | null
|
||||
}
|
||||
|
||||
export default function OperationsHubPage({
|
||||
initialBridgeStatus = null,
|
||||
initialRouteMatrix = null,
|
||||
initialNetworksConfig = null,
|
||||
initialTokenList = null,
|
||||
initialCapabilities = null,
|
||||
}: OperationsHubPageProps) {
|
||||
const [bridgeStatus, setBridgeStatus] = useState<MissionControlBridgeStatusResponse | null>(initialBridgeStatus)
|
||||
const [routeMatrix, setRouteMatrix] = useState<RouteMatrixResponse | null>(initialRouteMatrix)
|
||||
const [networksConfig, setNetworksConfig] = useState<NetworksConfigResponse | null>(initialNetworksConfig)
|
||||
const [tokenList, setTokenList] = useState<TokenListResponse | null>(initialTokenList)
|
||||
const [capabilities, setCapabilities] = useState<CapabilitiesResponse | null>(initialCapabilities)
|
||||
const [loadingError, setLoadingError] = useState<string | null>(null)
|
||||
const page = explorerFeaturePages.operations
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
const load = async () => {
|
||||
const [bridgeResult, routesResult, networksResult, tokenListResult, capabilitiesResult] =
|
||||
await Promise.allSettled([
|
||||
missionControlApi.getBridgeStatus(),
|
||||
routesApi.getRouteMatrix(),
|
||||
configApi.getNetworks(),
|
||||
configApi.getTokenList(),
|
||||
configApi.getCapabilities(),
|
||||
])
|
||||
|
||||
if (cancelled) return
|
||||
|
||||
if (bridgeResult.status === 'fulfilled') setBridgeStatus(bridgeResult.value)
|
||||
if (routesResult.status === 'fulfilled') setRouteMatrix(routesResult.value)
|
||||
if (networksResult.status === 'fulfilled') setNetworksConfig(networksResult.value)
|
||||
if (tokenListResult.status === 'fulfilled') setTokenList(tokenListResult.value)
|
||||
if (capabilitiesResult.status === 'fulfilled') setCapabilities(capabilitiesResult.value)
|
||||
|
||||
const failedCount = [
|
||||
bridgeResult,
|
||||
routesResult,
|
||||
networksResult,
|
||||
tokenListResult,
|
||||
capabilitiesResult,
|
||||
].filter((result) => result.status === 'rejected').length
|
||||
|
||||
if (failedCount === 5) {
|
||||
setLoadingError('Public explorer operations data is temporarily unavailable.')
|
||||
}
|
||||
}
|
||||
|
||||
load().catch((error) => {
|
||||
if (!cancelled) {
|
||||
setLoadingError(
|
||||
error instanceof Error ? error.message : 'Public explorer operations data is temporarily unavailable.'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
const relayCount = useMemo(() => {
|
||||
const relays = getMissionControlRelays(bridgeStatus)
|
||||
return relays ? Object.keys(relays).length : 0
|
||||
}, [bridgeStatus])
|
||||
|
||||
const totalQueue = useMemo(() => {
|
||||
const relays = getMissionControlRelays(bridgeStatus)
|
||||
if (!relays) return 0
|
||||
return Object.values(relays).reduce((sum, relay) => {
|
||||
const queueSize = relay.url_probe?.body?.queue?.size ?? relay.file_snapshot?.queue?.size ?? 0
|
||||
return sum + queueSize
|
||||
}, 0)
|
||||
}, [bridgeStatus])
|
||||
|
||||
const tokenChainCoverage = useMemo(() => {
|
||||
return new Set((tokenList?.tokens || []).map((token) => token.chainId).filter(Boolean)).size
|
||||
}, [tokenList])
|
||||
|
||||
const topSymbols = useMemo(() => {
|
||||
return Array.from(
|
||||
new Set((tokenList?.tokens || []).map((token) => token.symbol).filter(Boolean) as string[])
|
||||
).slice(0, 8)
|
||||
}, [tokenList])
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-6 sm:py-8">
|
||||
<div className="mb-6 max-w-4xl sm:mb-8">
|
||||
<div className="mb-3 inline-flex rounded-full border border-violet-200 bg-violet-50 px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] text-violet-700">
|
||||
{page.eyebrow}
|
||||
</div>
|
||||
<h1 className="mb-3 text-3xl font-bold text-gray-900 dark:text-white sm:text-4xl">
|
||||
{page.title}
|
||||
</h1>
|
||||
<p className="text-base leading-7 text-gray-600 dark:text-gray-400 sm:text-lg sm:leading-8">
|
||||
{page.description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{page.note ? (
|
||||
<Card className="mb-6 border border-amber-200 bg-amber-50/70 dark:border-amber-900/50 dark:bg-amber-950/20">
|
||||
<p className="text-sm leading-6 text-amber-950 dark:text-amber-100">
|
||||
{page.note}
|
||||
</p>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
{loadingError ? (
|
||||
<Card className="mb-6 border border-red-200 bg-red-50/70 dark:border-red-900/50 dark:bg-red-950/20">
|
||||
<p className="text-sm leading-6 text-red-900 dark:text-red-100">{loadingError}</p>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="mb-6 grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<Card className="border border-sky-200 bg-sky-50/70 dark:border-sky-900/50 dark:bg-sky-950/20">
|
||||
<div className="text-sm font-semibold uppercase tracking-wide text-sky-800 dark:text-sky-100">
|
||||
Bridge Fleet
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{bridgeStatus?.data?.status || 'unknown'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{relayCount} managed lanes · queue {totalQueue}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-emerald-200 bg-emerald-50/70 dark:border-emerald-900/50 dark:bg-emerald-950/20">
|
||||
<div className="text-sm font-semibold uppercase tracking-wide text-emerald-800 dark:text-emerald-100">
|
||||
Route Coverage
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{routeMatrix?.counts?.filteredLiveRoutes ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
{routeMatrix?.counts?.liveSwapRoutes ?? 0} swaps · {routeMatrix?.counts?.liveBridgeRoutes ?? 0} bridges
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
|
||||
Wallet Surface
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{networksConfig?.chains?.length ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Chains · {(tokenList?.tokens || []).length} tokens across {tokenChainCoverage} networks
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="text-sm font-semibold uppercase tracking-wide text-gray-700 dark:text-gray-300">
|
||||
RPC Capabilities
|
||||
</div>
|
||||
<div className="mt-2 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{capabilities?.http?.supportedMethods?.length ?? 0}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
HTTP methods · {capabilities?.tracing?.supportedMethods?.length ?? 0} tracing methods
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mb-8 grid gap-6 lg:grid-cols-[1fr_1fr]">
|
||||
<Card title="Operations Snapshot">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Bridge checked</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{relativeAge(bridgeStatus?.data?.checked_at)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Public mission-control snapshot freshness.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Route matrix updated</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{relativeAge(routeMatrix?.updated)}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Token-aggregation route inventory timestamp.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Default chain</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{networksConfig?.defaultChainId ?? 'Unknown'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Wallet onboarding points at Chain 138 by default.
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Wallet support</div>
|
||||
<div className="mt-1 text-lg font-semibold text-gray-900 dark:text-white">
|
||||
{capabilities?.walletSupport?.walletAddEthereumChain && capabilities?.walletSupport?.walletWatchAsset
|
||||
? 'Ready'
|
||||
: 'Partial'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
`wallet_addEthereumChain` and `wallet_watchAsset` compatibility.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card title="Public Config Highlights">
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Featured symbols</div>
|
||||
<div className="mt-2 flex flex-wrap gap-2">
|
||||
{topSymbols.map((symbol) => (
|
||||
<span
|
||||
key={symbol}
|
||||
className="rounded-full bg-primary-50 px-3 py-1 text-xs font-semibold uppercase tracking-wide text-primary-700 dark:bg-primary-900/30 dark:text-primary-300"
|
||||
>
|
||||
{symbol}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-2xl border border-gray-200 bg-gray-50 p-4 dark:border-gray-700 dark:bg-gray-900/40">
|
||||
<div className="text-sm text-gray-500 dark:text-gray-400">Tracing posture</div>
|
||||
<div className="mt-2 text-sm text-gray-700 dark:text-gray-300">
|
||||
Supported: {(capabilities?.tracing?.supportedMethods || []).join(', ') || 'None'}
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Unsupported: {(capabilities?.tracing?.unsupportedMethods || []).join(', ') || 'None'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 lg:grid-cols-2">
|
||||
{page.actions.map((action) => (
|
||||
<Card key={`${action.title}-${action.href}`} className="border border-gray-200 dark:border-gray-700">
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="text-base font-semibold text-gray-900 dark:text-white">
|
||||
{action.title}
|
||||
</div>
|
||||
<p className="mt-2 flex-1 text-sm leading-6 text-gray-600 dark:text-gray-400">
|
||||
{action.description}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<ActionLink
|
||||
href={action.href}
|
||||
label={action.label}
|
||||
external={'external' in action ? action.external : undefined}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user