New command center (#11899)
* some work * more info * history * mostly done * better formatting * fix: lint issues and types * chore: minor styling improvement --------- Co-authored-by: Arjun Porwal <porwalarjun95@gmail.com>
This commit is contained in:
parent
cc91700648
commit
cb90e6eb37
@ -3,208 +3,301 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { IAhOutput } from './index.js';
|
||||
import type { EnhancedEvent, IAhOutput } from './index.js';
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { CardSummary, Expander, Icon, MarkWarning, Spinner, styled, Tooltip } from '@polkadot/react-components';
|
||||
import { CardSummary, Expander, Spinner, styled } from '@polkadot/react-components';
|
||||
import { Event as EventDisplay } from '@polkadot/react-params';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
import { formatBalance, formatNumber } from '@polkadot/util';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
ahApi?: ApiPromise;
|
||||
ahOutput: IAhOutput[];
|
||||
ahOutput?: IAhOutput;
|
||||
ahEvents: EnhancedEvent[];
|
||||
ahUrl: string;
|
||||
isRelayChain: boolean
|
||||
}
|
||||
|
||||
function AssetHubSection ({ ahApi, ahOutput, ahUrl, children, isRelayChain }: Props) {
|
||||
function AssetHubSection ({ ahApi, ahEvents, ahOutput, ahUrl, children, isRelayChain }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
textTransform: 'capitalize'
|
||||
}}
|
||||
>
|
||||
{t('Asset Hub chain')}
|
||||
{children}
|
||||
</h1>
|
||||
{!ahApi && <Spinner label='Connecting to Asset Hub' />}
|
||||
<StyledSection>
|
||||
{ahOutput.map((ah) => {
|
||||
const uniquePages = Array.from(new Set(ah.multiblock.snapshotRange.map((p) => Number(p.toString())))).sort((a, b) => a - b);
|
||||
const [minPage, maxPage] = [uniquePages[0], uniquePages[uniquePages.length - 1]];
|
||||
<StyledColumn>
|
||||
<StyledInfoBox>
|
||||
<h2>
|
||||
{t('Asset Hub')}
|
||||
{children}
|
||||
</h2>
|
||||
{!ahApi && <Spinner label={t('Connecting to Asset Hub')} />}
|
||||
{ahOutput && (
|
||||
<div className='info-content'>
|
||||
<div className='block-info'>
|
||||
{!isRelayChain
|
||||
? <Link to={`/explorer/query/${ahOutput.finalizedBlock}`}>#{formatNumber(ahOutput.finalizedBlock)}</Link>
|
||||
: (
|
||||
<Link
|
||||
target='_blank'
|
||||
to={`${window.location.origin}/?rpc=${ahUrl}#/explorer/query/${ahOutput.finalizedBlock}`}
|
||||
>
|
||||
#{formatNumber(ahOutput.finalizedBlock)}
|
||||
</Link>)
|
||||
}
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Staking')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('current era')}>
|
||||
{ahOutput.staking.currentEra}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('active era')}>
|
||||
{ahOutput.staking.activeEra.index}
|
||||
{ahOutput.staking.activeEra.duration && ` (${ahOutput.staking.activeEra.duration})`}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('era start session')}>
|
||||
{ahOutput.staking.erasStartSessionIndex || 'None'}
|
||||
{ahOutput.rcClient.eraDepth && `, Era-depth: ${ahOutput.rcClient.eraDepth} sessions`}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('bonded eras')}>
|
||||
{ahOutput.staking.bondedEras && ahOutput.staking.bondedEras.length > 0
|
||||
? (() => {
|
||||
const firstEra = ahOutput.staking.bondedEras[0];
|
||||
const lastEra = ahOutput.staking.bondedEras[ahOutput.staking.bondedEras.length - 1];
|
||||
|
||||
return `[(${firstEra[0].toString()}@${firstEra[1].toString()}) -> (${lastEra[0].toString()}@${lastEra[1].toString()})]`;
|
||||
})()
|
||||
: 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('unpruned eras')}>
|
||||
{ahOutput.staking.unprunedEras}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('forcing')}>
|
||||
{ahOutput.staking.forcing || 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Staking RC Client')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('last session report')}>
|
||||
End={ahOutput.rcClient.lastSessionReportEndIndex}, Start={ahOutput.rcClient.lastSessionIndex}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Election')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('phase')}>
|
||||
{ahOutput.multiblock.phase}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('round')}>
|
||||
{ahOutput.multiblock.round}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('signed submissions')}>
|
||||
{ahOutput.multiblock.signedSubmissions}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('queued score')}>
|
||||
{ahOutput.multiblock.queuedScore || 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('snapshot range')}>
|
||||
{ahOutput.multiblock.snapshotRange.length > 0
|
||||
? (() => {
|
||||
const uniquePages = Array.from(new Set(ahOutput.multiblock.snapshotRange.map((p) => Number(p.toString())))).sort((a, b) => a - b);
|
||||
const [minPage, maxPage] = [uniquePages[0], uniquePages[uniquePages.length - 1]];
|
||||
|
||||
return `${minPage} → ${maxPage}`;
|
||||
})()
|
||||
: 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Validators/Nominators')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('wanted validators')}>
|
||||
{ahOutput.staking.validatorCount || 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('validator candidates')}>
|
||||
{ahOutput.staking.validatorCandidates
|
||||
? `${ahOutput.staking.validatorCandidates}${ahOutput.staking.maxValidatorsCount ? ` (max: ${ahOutput.staking.maxValidatorsCount})` : ''}`
|
||||
: 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('nominator candidates')}>
|
||||
{ahOutput.staking.nominatorCandidates
|
||||
? `${ahOutput.staking.nominatorCandidates}${ahOutput.staking.maxNominatorsCount ? ` (max: ${ahOutput.staking.maxNominatorsCount})` : ''}`
|
||||
: 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('min bonds')}>
|
||||
{ahOutput.staking.minNominatorBond || ahOutput.staking.minValidatorBond || ahOutput.staking.minNominatorActiveStake
|
||||
? `${ahOutput.staking.minNominatorBond ? `N: ${formatBalance(ahOutput.staking.minNominatorBond, { forceUnit: '-', withSi: true })}` : ''}${ahOutput.staking.minValidatorBond ? ` / V: ${formatBalance(ahOutput.staking.minValidatorBond, { forceUnit: '-', withSi: true })}` : ''}${ahOutput.staking.minNominatorActiveStake ? ` / Active: ${formatBalance(ahOutput.staking.minNominatorActiveStake, { forceUnit: '-', withSi: true })}` : ''}`
|
||||
: 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Bags List')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('all nodes')}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
|
||||
{ahOutput.bagsList ? ahOutput.bagsList.allNodes : 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('lock')}>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */}
|
||||
{ahOutput.bagsList ? ahOutput.bagsList.lock : 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledInfoBox>
|
||||
<StyledEventsBox>
|
||||
<h3>{t('Asset Hub Events')}</h3>
|
||||
{ahEvents.length === 0 && (
|
||||
<div className='no-events'>
|
||||
{t('No relevant events in recent blocks')}
|
||||
</div>
|
||||
)}
|
||||
{ahEvents.map((enhancedEvent, index) => {
|
||||
const { blockNumber, event, weight } = enhancedEvent;
|
||||
const eventName = `${event.section}.${event.method}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
className='assethub__chain'
|
||||
key={ah.finalizedBlock}
|
||||
>
|
||||
<div className='details'>
|
||||
<div className='session__summary'>
|
||||
<h4 className='--digits'>
|
||||
<Expander
|
||||
isLeft
|
||||
key={`${event.index.toString()}-${index}`}
|
||||
summary={
|
||||
<>
|
||||
{eventName}
|
||||
<div className='absolute --digits'>
|
||||
{weight && <span className='weight'>[{weight}]</span>}
|
||||
{!isRelayChain
|
||||
? <Link to={`/explorer/query/${ah.finalizedBlock}`}>#{formatNumber(ah.finalizedBlock)}</Link>
|
||||
? <Link to={`/explorer/query/${blockNumber}`}>#{formatNumber(blockNumber)}</Link>
|
||||
: (
|
||||
<Link
|
||||
target='_blank'
|
||||
to={`${window.location.origin}/?rpc=${ahUrl}#/explorer/query/${ah.finalizedBlock}`}
|
||||
to={`${window.location.origin}/?rpc=${ahUrl}#/explorer/query/${blockNumber}`}
|
||||
>
|
||||
#{formatNumber(ah.finalizedBlock)}
|
||||
#{formatNumber(blockNumber)}
|
||||
</Link>)
|
||||
}
|
||||
|
||||
</h4>
|
||||
</div>
|
||||
<div>
|
||||
<div className='staking__summary'>
|
||||
<CardSummary label={t('current era')}>
|
||||
{ah.staking.currentEra}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('Relay Session Index')}>
|
||||
{ah.staking.erasStartSessionIndex}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('Relay Active Era Index')}>
|
||||
{ah.staking.activeEra.index}
|
||||
</CardSummary>
|
||||
</div>
|
||||
<div className='multiblock__summary'>
|
||||
<CardSummary label={t('multiblock phase')}>
|
||||
{ah.multiblock.phase}
|
||||
</CardSummary>
|
||||
{ah.multiblock.queuedScore &&
|
||||
<CardSummary label={t('multiblock queued score')}>
|
||||
{ah.multiblock.queuedScore}
|
||||
</CardSummary>}
|
||||
<CardSummary label={t('signed submissions')}>
|
||||
{ah.multiblock.signedSubmissions}
|
||||
</CardSummary>
|
||||
{!!ah.multiblock.snapshotRange.length &&
|
||||
<CardSummary label={t('snapshot range')}>
|
||||
{`${minPage} → ${maxPage}`}
|
||||
</CardSummary>}
|
||||
</div>
|
||||
<div className='rcClient__summary'>
|
||||
<CardSummary label={t('Last Session Report End Index')}>
|
||||
{ah.rcClient.lastSessionReportEndIndex}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='events__summary'>
|
||||
<h3>{t('events')}</h3>
|
||||
{ah.events.map((event) => {
|
||||
const eventName = `${event.section}.${event.method}`;
|
||||
|
||||
return (
|
||||
<Expander
|
||||
isLeft
|
||||
key={event.index.toString()}
|
||||
summary={eventName}
|
||||
summaryMeta={event.meta}
|
||||
>
|
||||
<EventDisplay
|
||||
className='details'
|
||||
eventName={eventName}
|
||||
value={event}
|
||||
withExpander
|
||||
/>
|
||||
</Expander>
|
||||
|
||||
);
|
||||
})}
|
||||
{ah.events.length === 0 &&
|
||||
<div className='warning__tooltip'>
|
||||
<MarkWarning content={t('No events available')} />
|
||||
<Icon
|
||||
icon='info-circle'
|
||||
tooltip={`${ah.finalizedBlock}-ah-no-events`}
|
||||
/>
|
||||
<Tooltip
|
||||
text={'No relevant events found for multiBlockElection, multiBlockElectionVerifier, multiBlockElectionUnsigned, stakingRcClient or staking in the current block.'}
|
||||
trigger={`${ah.finalizedBlock}-ah-no-events`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
summaryMeta={event.meta}
|
||||
>
|
||||
<EventDisplay
|
||||
className='details'
|
||||
eventName={eventName}
|
||||
value={event}
|
||||
withExpander
|
||||
/>
|
||||
</Expander>
|
||||
);
|
||||
})}
|
||||
</StyledSection>
|
||||
</div>);
|
||||
</StyledEventsBox>
|
||||
</StyledColumn>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledSection = styled.section`
|
||||
margin-block: 1rem;
|
||||
overflow: auto;
|
||||
white-space: nowrap;
|
||||
const StyledColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
.warning {
|
||||
max-width: fit-content;
|
||||
margin-left: 0;
|
||||
const StyledInfoBox = styled.div`
|
||||
background: var(--bg-table);
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: var(--font-size-h2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ui--Labelled-content {
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
.info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.assethub__chain {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
place-items: start;
|
||||
background: var(--bg-table);
|
||||
margin-bottom: 0.35rem;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
.block-info {
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.8rem;
|
||||
|
||||
.session__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
||||
.staking__summary {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.multiblock__summary, .rcClient__summary {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-top: 1.5rem;
|
||||
.section {
|
||||
h4 {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
.events__summary {
|
||||
display: grid;
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
justify-self: center;
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-h2);
|
||||
}
|
||||
|
||||
.ui--Labelled {
|
||||
label {
|
||||
font-size: medium !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
.warning__tooltip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.ui--Labelled-content {
|
||||
font-size: small !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEventsBox = styled.div`
|
||||
background: var(--bg-table);
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-events {
|
||||
color: var(--color-text-secondary);
|
||||
font-style: italic;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui--Expander {
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.72rem;
|
||||
white-space: nowrap;
|
||||
|
||||
.weight {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--font-size-small);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,20 +2,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { AccountId32, Event } from '@polkadot/types/interfaces';
|
||||
import type { AccountId32, Event, Hash } from '@polkadot/types/interfaces';
|
||||
import type { FrameSupportDispatchPerDispatchClassWeight, PolkadotRuntimeParachainsConfigurationHostConfiguration } from '@polkadot/types/lookup';
|
||||
import type { IEventData, ITuple } from '@polkadot/types/types';
|
||||
import type { u32, Vec } from '@polkadot/types-codec';
|
||||
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import { Dropdown, styled } from '@polkadot/react-components';
|
||||
import { Button, Dropdown, Input, MarkWarning, styled } from '@polkadot/react-components';
|
||||
import { useApi } from '@polkadot/react-hooks';
|
||||
import { getApi } from '@polkadot/react-hooks/ctx/StakingAsync';
|
||||
import { formatBalance } from '@polkadot/util';
|
||||
|
||||
import AssetHubSection from './ah.js';
|
||||
import RelaySection from './relay.js';
|
||||
|
||||
const MAX_EVENTS = 25;
|
||||
// Enhanced event type with metadata
|
||||
export interface EnhancedEvent {
|
||||
event: Event;
|
||||
blockNumber: number;
|
||||
blockHash: string;
|
||||
weight?: string;
|
||||
}
|
||||
|
||||
export interface IRcOutput {
|
||||
finalizedBlock: number,
|
||||
@ -27,21 +35,42 @@ export interface IRcOutput {
|
||||
stakingAhClient: {
|
||||
mode: string
|
||||
hasNextActiveId?: number,
|
||||
hasQueuedInClient?: [number, AccountId32[]]
|
||||
hasQueuedInClient?: [number, AccountId32[]],
|
||||
validatorPoints: number
|
||||
},
|
||||
events: Event[]
|
||||
staking: {
|
||||
forceEra?: string,
|
||||
validatorCount?: number,
|
||||
electionPhase?: string
|
||||
},
|
||||
parachainConfig?: {
|
||||
maxDownwardMessageSize: number,
|
||||
maxUpwardMessageSize: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface IAhOutput {
|
||||
finalizedBlock: number,
|
||||
staking: {
|
||||
currentEra: number,
|
||||
activeEra: {index: number, start: string},
|
||||
activeEra: {index: number, start: string, duration?: string},
|
||||
erasStartSessionIndex?: number,
|
||||
bondedEras: Vec<ITuple<[u32, u32]>>
|
||||
bondedEras: Vec<ITuple<[u32, u32]>>,
|
||||
unprunedEras: string,
|
||||
validatorCount?: number,
|
||||
validatorCandidates?: number,
|
||||
nominatorCandidates?: number,
|
||||
maxValidatorsCount?: number,
|
||||
maxNominatorsCount?: number,
|
||||
minNominatorBond?: string,
|
||||
minValidatorBond?: string,
|
||||
minNominatorActiveStake?: string,
|
||||
forcing?: string
|
||||
},
|
||||
rcClient: {
|
||||
lastSessionReportEndIndex: string
|
||||
lastSessionReportEndIndex: string,
|
||||
lastSessionIndex: number,
|
||||
eraDepth?: number
|
||||
},
|
||||
multiblock: {
|
||||
phase: string,
|
||||
@ -50,14 +79,45 @@ export interface IAhOutput {
|
||||
queuedScore: string|null,
|
||||
signedSubmissions: number
|
||||
},
|
||||
events: Event[]
|
||||
bagsList?: {
|
||||
allNodes: number,
|
||||
lock: string
|
||||
}
|
||||
}
|
||||
|
||||
// Filter functions for relevant events
|
||||
const filterRcEvents = (e: IEventData): boolean => {
|
||||
const ahClientEvents = (e: IEventData) =>
|
||||
e.section === 'stakingAhClient';
|
||||
const sessionEvents = (e: IEventData) =>
|
||||
e.section === 'session' || e.section === 'historical';
|
||||
|
||||
return ahClientEvents(e) || sessionEvents(e);
|
||||
};
|
||||
|
||||
const filterAhEvents = (e: IEventData): boolean => {
|
||||
const election = (e: IEventData) =>
|
||||
e.section === 'multiBlockElection' ||
|
||||
e.section === 'multiBlockElectionVerifier' ||
|
||||
e.section === 'multiBlockElectionSigned' ||
|
||||
e.section === 'multiBlockElectionUnsigned';
|
||||
const rcClient = (e: IEventData) => e.section === 'stakingRcClient';
|
||||
const staking = (e: IEventData) =>
|
||||
e.section === 'staking' &&
|
||||
(e.method === 'EraPaid' ||
|
||||
e.method === 'SessionRotated' ||
|
||||
e.method === 'PagedElectionProceeded');
|
||||
|
||||
return election(e) || rcClient(e) || staking(e);
|
||||
};
|
||||
|
||||
const commandCenterHandler = async (
|
||||
rcApi: ApiPromise,
|
||||
ahApi: ApiPromise,
|
||||
setRcOutout: React.Dispatch<React.SetStateAction<IRcOutput[]>>,
|
||||
setAhOutout: React.Dispatch<React.SetStateAction<IAhOutput[]>>
|
||||
setRcOutput: React.Dispatch<React.SetStateAction<IRcOutput | undefined>>,
|
||||
setAhOutput: React.Dispatch<React.SetStateAction<IAhOutput | undefined>>,
|
||||
setRcEvents: React.Dispatch<React.SetStateAction<EnhancedEvent[]>>,
|
||||
setAhEvents: React.Dispatch<React.SetStateAction<EnhancedEvent[]>>
|
||||
): Promise<void> => {
|
||||
await rcApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
|
||||
// --- RC:
|
||||
@ -77,64 +137,110 @@ const commandCenterHandler = async (
|
||||
// Operating mode of the client.
|
||||
const mode = await rcApi.query.stakingAhClient.mode();
|
||||
|
||||
// Staking info from RC if available
|
||||
const forceEra = rcApi.query.staking?.forceEra ? await rcApi.query.staking.forceEra() : undefined;
|
||||
const validatorCount = rcApi.query.staking?.validatorCount ? await rcApi.query.staking.validatorCount() : undefined;
|
||||
const electionPhase = rcApi.query.electionProviderMultiPhase?.currentPhase ? await rcApi.query.electionProviderMultiPhase.currentPhase() : undefined;
|
||||
|
||||
// Validator points
|
||||
const validatorPointsKeys = await rcApi.query.stakingAhClient.validatorPoints.keys();
|
||||
|
||||
// Parachain config
|
||||
let parachainConfig;
|
||||
|
||||
try {
|
||||
const configuration = await rcApi.query.configuration.activeConfig() as PolkadotRuntimeParachainsConfigurationHostConfiguration;
|
||||
|
||||
parachainConfig = {
|
||||
maxDownwardMessageSize: configuration.maxDownwardMessageSize?.toNumber() || 0,
|
||||
maxUpwardMessageSize: configuration.maxUpwardMessageSize?.toNumber() || 0
|
||||
};
|
||||
} catch {
|
||||
parachainConfig = undefined;
|
||||
}
|
||||
|
||||
// Events that we are interested in from RC:
|
||||
const eventsOfInterest = (await (await rcApi.at(header.hash.toHex())).query.system.events())
|
||||
.map((e) => e.event)
|
||||
.filter((e) => {
|
||||
const ahClientEvents = (e: IEventData) =>
|
||||
e.section === 'stakingAhClient';
|
||||
const sessionEvents = (e: IEventData) =>
|
||||
e.section === 'session' || e.section === 'historical';
|
||||
.filter((e) => filterRcEvents(e.data));
|
||||
|
||||
return ahClientEvents(e.data) || sessionEvents(e.data);
|
||||
});
|
||||
// Wrap events with metadata
|
||||
const enhancedEvents: EnhancedEvent[] = eventsOfInterest.map((event) => ({
|
||||
blockHash: header.hash.toString(),
|
||||
blockNumber: header.number.toNumber(),
|
||||
event
|
||||
}));
|
||||
|
||||
setRcOutout((prev) => {
|
||||
const newItem = {
|
||||
events: eventsOfInterest,
|
||||
finalizedBlock: header.number.toNumber(),
|
||||
session: {
|
||||
hasQueuedInSession: hasQueuedInSession.isTrue,
|
||||
historicalRange: historicalRange.isSome
|
||||
? [historicalRange.unwrap()[0].toNumber(), historicalRange.unwrap()[1].toNumber()] as [number, number]
|
||||
: undefined,
|
||||
index: index.toNumber()
|
||||
},
|
||||
stakingAhClient: {
|
||||
hasNextActiveId: hasNextActiveId.isEmpty
|
||||
? undefined
|
||||
: rcApi.createType('Option<u32>', hasNextActiveId).unwrap().toNumber(),
|
||||
hasQueuedInClient: (() => {
|
||||
const parsed = rcApi.createType('Option<(u32,Vec<AccountId32>)>', hasQueuedInClient);
|
||||
setRcOutput({
|
||||
finalizedBlock: header.number.toNumber(),
|
||||
parachainConfig,
|
||||
session: {
|
||||
hasQueuedInSession: hasQueuedInSession.isTrue,
|
||||
historicalRange: historicalRange.isSome
|
||||
? [historicalRange.unwrap()[0].toNumber(), historicalRange.unwrap()[1].toNumber()] as [number, number]
|
||||
: undefined,
|
||||
index: index.toNumber()
|
||||
},
|
||||
staking: {
|
||||
electionPhase: electionPhase?.toString(),
|
||||
forceEra: forceEra?.toString(),
|
||||
validatorCount: validatorCount?.toNumber()
|
||||
},
|
||||
stakingAhClient: {
|
||||
hasNextActiveId: hasNextActiveId.isEmpty
|
||||
? undefined
|
||||
: rcApi.createType('Option<u32>', hasNextActiveId).unwrap().toNumber(),
|
||||
hasQueuedInClient: (() => {
|
||||
const parsed = rcApi.createType('Option<(u32,Vec<AccountId32>)>', hasQueuedInClient);
|
||||
|
||||
return parsed.isNone ? undefined : [parsed.unwrap()[0].toNumber(), parsed.unwrap()[1]] as [number, AccountId32[]];
|
||||
})(),
|
||||
mode: mode.toString()
|
||||
}
|
||||
};
|
||||
|
||||
return [newItem, ...prev]
|
||||
.reduce((acc, curr) => {
|
||||
if (!acc.find((item) => item.finalizedBlock === curr.finalizedBlock)) {
|
||||
acc.push(curr);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, [] as typeof prev)
|
||||
.slice(0, MAX_EVENTS);
|
||||
return parsed.isNone ? undefined : [parsed.unwrap()[0].toNumber(), parsed.unwrap()[1]] as [number, AccountId32[]];
|
||||
})(),
|
||||
mode: mode.toString(),
|
||||
validatorPoints: validatorPointsKeys.length
|
||||
}
|
||||
});
|
||||
|
||||
if (enhancedEvents.length > 0) {
|
||||
setRcEvents(enhancedEvents);
|
||||
}
|
||||
});
|
||||
|
||||
await ahApi.rpc.chain.subscribeFinalizedHeads(async (header) => {
|
||||
// the current planned era
|
||||
const currentEra = await rcApi.query.session.currentIndex();
|
||||
const currentEra = (await ahApi.query.staking.currentEra()).unwrap().toNumber();
|
||||
// the active era
|
||||
const activeEra = (await ahApi.query.staking.activeEra()).unwrap();
|
||||
const activeEraDuration = activeEra.start.isSome
|
||||
? (() => {
|
||||
const startTime = activeEra.start.unwrap().toNumber();
|
||||
const durationMs = Date.now() - startTime;
|
||||
const hours = Math.floor(durationMs / 3600000);
|
||||
const minutes = Math.floor((durationMs % 3600000) / 60000);
|
||||
const seconds = Math.floor((durationMs % 60000) / 1000);
|
||||
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
|
||||
})()
|
||||
: undefined;
|
||||
|
||||
// the starting index of the active era
|
||||
const bondedEras = await ahApi.query.staking.bondedEras();
|
||||
const activeEraStartSessionIndex = bondedEras.find(([e]) => e.eq(activeEra.index))?.[1];
|
||||
// the starting index of the active era
|
||||
// const erasStartSessionIndex = await ahApi.query.staking.erasStartSessionIndex(activeEra.unwrap().index);
|
||||
|
||||
// Unpruned eras
|
||||
const unprunedEras = ahApi.query.staking.eraPruningState
|
||||
? (await ahApi.query.staking.eraPruningState.entries()).map(([k]) => k.args[0]).sort().join(', ')
|
||||
: 'unimplemented!';
|
||||
|
||||
// Additional staking info
|
||||
const validatorCount = ahApi.query.staking.validatorCount ? await ahApi.query.staking.validatorCount() : undefined;
|
||||
const validatorCandidates = ahApi.query.staking.counterForValidators ? await ahApi.query.staking.counterForValidators() : undefined;
|
||||
const nominatorCandidates = ahApi.query.staking.counterForNominators ? await ahApi.query.staking.counterForNominators() : undefined;
|
||||
const maxValidatorsCount = ahApi.query.staking.maxValidatorsCount ? await ahApi.query.staking.maxValidatorsCount() : undefined;
|
||||
const maxNominatorsCount = ahApi.query.staking.maxNominatorsCount ? await ahApi.query.staking.maxNominatorsCount() : undefined;
|
||||
const minNominatorBond = ahApi.query.staking.minNominatorBond ? await ahApi.query.staking.minNominatorBond() : undefined;
|
||||
const minValidatorBond = ahApi.query.staking.minValidatorBond ? await ahApi.query.staking.minValidatorBond() : undefined;
|
||||
const minNominatorActiveStake = ahApi.query.staking.minimumActiveStake ? await ahApi.query.staking.minimumActiveStake() : undefined;
|
||||
const forcing = ahApi.query.staking.forceEra ? await ahApi.query.staking.forceEra() : undefined;
|
||||
|
||||
// the basic state of the election provider
|
||||
const phase = await ahApi.query.multiBlockElection.currentPhase();
|
||||
@ -150,57 +256,130 @@ const commandCenterHandler = async (
|
||||
await ahApi.query.multiBlockElectionSigned.sortedScores(round);
|
||||
|
||||
// The client
|
||||
const lastSessionReportEndIndex = await ahApi.query.stakingRcClient.lastSessionReportEndingIndex();
|
||||
const lastSessionReportEndIndexRaw = await ahApi.query.stakingRcClient.lastSessionReportEndingIndex();
|
||||
const lastSessionReportEndIndex = ahApi.createType('Option<BlockNumber>', lastSessionReportEndIndexRaw);
|
||||
const lastSessionIndex = lastSessionReportEndIndex.isSome ? lastSessionReportEndIndex.unwrap().toNumber() + 1 : 0;
|
||||
const eraDepth = activeEraStartSessionIndex ? lastSessionIndex - activeEraStartSessionIndex.toNumber() : undefined;
|
||||
|
||||
// Events that we are interested in from RC:
|
||||
// Bags list
|
||||
let bagsList;
|
||||
|
||||
try {
|
||||
const allNodes = await ahApi.query.voterList.counterForListNodes();
|
||||
const lock = await ahApi.query.voterList.lock();
|
||||
|
||||
bagsList = {
|
||||
allNodes: allNodes.toNumber(),
|
||||
lock: lock.toU8a().toString()
|
||||
};
|
||||
} catch {
|
||||
bagsList = undefined;
|
||||
}
|
||||
|
||||
// Events that we are interested in from AH:
|
||||
const eventsOfInterest = (await (await ahApi.at(header.hash.toHex())).query.system.events())
|
||||
.map((e) => e.event)
|
||||
.filter((e) => {
|
||||
const election = (e: IEventData) => e.section === 'multiBlockElection' || e.section === 'multiBlockElectionVerifier' || e.section === 'multiBlockElectionSigned' || e.section === 'multiBlockElectionUnsigned';
|
||||
const rcClient = (e: IEventData) => e.section === 'stakingRcClient';
|
||||
const staking = (e: IEventData) => e.section === 'staking' && (e.method === 'EraPaid' || e.method === 'SessionRotated' || e.method === 'PagedElectionProceeded');
|
||||
.filter((e) => filterAhEvents(e.data));
|
||||
|
||||
return election(e.data) || rcClient(e.data) || staking(e.data);
|
||||
});
|
||||
// Get block weight
|
||||
const weight = await (await ahApi.at(header.hash)).query.system.blockWeight();
|
||||
|
||||
setAhOutout((prev) => {
|
||||
const parsedQueuedScore = ahApi.createType('Option<SpNposElectionsElectionScore>', queuedScore);
|
||||
const formatWeight = (w: FrameSupportDispatchPerDispatchClassWeight) => {
|
||||
const normalRef = w.normal?.refTime?.toBigInt() || 0n;
|
||||
const operationalRef = w.operational?.refTime?.toBigInt() || 0n;
|
||||
const mandatoryRef = w.mandatory?.refTime?.toBigInt() || 0n;
|
||||
const totalRef = normalRef + operationalRef + mandatoryRef;
|
||||
|
||||
const newItem = {
|
||||
events: eventsOfInterest,
|
||||
finalizedBlock: header.number.toNumber(),
|
||||
multiblock: {
|
||||
phase: phase.toString(),
|
||||
queuedScore: parsedQueuedScore.isSome ? parsedQueuedScore.unwrap().toString() : null,
|
||||
round: ahApi.createType('u32', round).toNumber(),
|
||||
signedSubmissions: ahApi.createType(
|
||||
'Vec<(AccountId32,SpNposElectionsElectionScore)>',
|
||||
signedSubmissions
|
||||
).length,
|
||||
snapshotRange: snapshotRange.map((a) => a.toString())
|
||||
},
|
||||
rcClient: { lastSessionReportEndIndex: lastSessionReportEndIndex.toString() },
|
||||
staking: {
|
||||
activeEra: {
|
||||
index: activeEra.index.toNumber(),
|
||||
start: activeEra.toString()
|
||||
},
|
||||
bondedEras,
|
||||
currentEra: currentEra.toNumber(),
|
||||
erasStartSessionIndex: activeEraStartSessionIndex?.toNumber()
|
||||
const normalProof = w.normal?.proofSize?.toBigInt() || 0n;
|
||||
const operationalProof = w.operational?.proofSize?.toBigInt() || 0n;
|
||||
const mandatoryProof = w.mandatory?.proofSize?.toBigInt() || 0n;
|
||||
const totalProof = normalProof + operationalProof + mandatoryProof;
|
||||
|
||||
return `${(Number(totalRef) / 1_000_000_000_000).toFixed(2)}s / ${(Number(totalProof) / 1024).toFixed(0)}KB`;
|
||||
};
|
||||
|
||||
// Wrap events with metadata including weight
|
||||
const enhancedEvents: EnhancedEvent[] = eventsOfInterest.map((event) => ({
|
||||
blockHash: header.hash.toString(),
|
||||
blockNumber: header.number.toNumber(),
|
||||
event,
|
||||
weight: formatWeight(weight)
|
||||
}));
|
||||
|
||||
const parsedQueuedScore = ahApi.createType('Option<SpNposElectionsElectionScore>', queuedScore);
|
||||
const formattedQueuedScore = parsedQueuedScore.isSome
|
||||
? (() => {
|
||||
const score = parsedQueuedScore.unwrap();
|
||||
const minimalStake = score.minimalStake?.toString() || '0';
|
||||
const formattedMinStake = formatBalance(minimalStake, { forceUnit: '-', withSi: true });
|
||||
|
||||
return `minStake: ${formattedMinStake}, ...`;
|
||||
})()
|
||||
: null;
|
||||
|
||||
// Format phase to be more readable (e.g., "Unsigned(1)" instead of {"unsigned":1})
|
||||
const formattedPhase = (() => {
|
||||
const phaseStr = phase.toString();
|
||||
|
||||
try {
|
||||
const phaseJson = JSON.parse(phaseStr) as Record<string, string>;
|
||||
|
||||
if (typeof phaseJson === 'object' && phaseJson !== null) {
|
||||
const key = Object.keys(phaseJson)[0];
|
||||
const value = phaseJson[key];
|
||||
|
||||
return `${key.charAt(0).toUpperCase() + key.slice(1)}(${value})`;
|
||||
}
|
||||
};
|
||||
} catch {
|
||||
// If not JSON, return as is
|
||||
}
|
||||
|
||||
return [newItem, ...prev]
|
||||
.reduce((acc, curr) => {
|
||||
if (!acc.find((item) => item.finalizedBlock === curr.finalizedBlock)) {
|
||||
acc.push(curr);
|
||||
}
|
||||
return phaseStr;
|
||||
})();
|
||||
|
||||
return acc;
|
||||
}, [] as typeof prev)
|
||||
.slice(0, MAX_EVENTS);
|
||||
setAhOutput({
|
||||
bagsList,
|
||||
finalizedBlock: header.number.toNumber(),
|
||||
multiblock: {
|
||||
phase: formattedPhase,
|
||||
queuedScore: formattedQueuedScore,
|
||||
round: ahApi.createType('u32', round).toNumber(),
|
||||
signedSubmissions: ahApi.createType(
|
||||
'Vec<(AccountId32,SpNposElectionsElectionScore)>',
|
||||
signedSubmissions
|
||||
).length,
|
||||
snapshotRange: snapshotRange.map((a) => a.toString())
|
||||
},
|
||||
rcClient: {
|
||||
eraDepth,
|
||||
lastSessionIndex,
|
||||
lastSessionReportEndIndex: lastSessionReportEndIndex.isSome ? lastSessionReportEndIndex.unwrap().toString() : '0'
|
||||
},
|
||||
staking: {
|
||||
activeEra: {
|
||||
duration: activeEraDuration,
|
||||
index: activeEra.index.toNumber(),
|
||||
start: activeEra.toString()
|
||||
},
|
||||
bondedEras,
|
||||
currentEra,
|
||||
erasStartSessionIndex: activeEraStartSessionIndex?.toNumber(),
|
||||
forcing: forcing?.toString(),
|
||||
maxNominatorsCount: maxNominatorsCount?.isSome ? maxNominatorsCount.unwrap().toNumber() : undefined,
|
||||
maxValidatorsCount: maxValidatorsCount?.isSome ? maxValidatorsCount.unwrap().toNumber() : undefined,
|
||||
minNominatorActiveStake: minNominatorActiveStake?.toString(),
|
||||
minNominatorBond: minNominatorBond?.toString(),
|
||||
minValidatorBond: minValidatorBond?.toString(),
|
||||
nominatorCandidates: nominatorCandidates?.toNumber(),
|
||||
unprunedEras,
|
||||
validatorCandidates: validatorCandidates?.toNumber(),
|
||||
validatorCount: validatorCount?.toNumber()
|
||||
}
|
||||
});
|
||||
|
||||
if (enhancedEvents.length > 0) {
|
||||
setAhEvents(enhancedEvents);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@ -215,8 +394,10 @@ interface Props {
|
||||
function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi: initialRcApi, rcEndPoints }: Props) {
|
||||
const { api, apiUrl } = useApi();
|
||||
|
||||
const [rcOutput, setRcOutput] = useState<IRcOutput[]>([]);
|
||||
const [ahOutput, setAhOutput] = useState<IAhOutput[]>([]);
|
||||
const [rcOutput, setRcOutput] = useState<IRcOutput | undefined>(undefined);
|
||||
const [ahOutput, setAhOutput] = useState<IAhOutput | undefined>(undefined);
|
||||
const [rcEvents, setRcEvents] = useState<EnhancedEvent[]>([]);
|
||||
const [ahEvents, setAhEvents] = useState<EnhancedEvent[]>([]);
|
||||
|
||||
const [rcUrl, setRcUrl] = useState<string|undefined>(undefined);
|
||||
const [ahUrl, setAhUrl] = useState<string|undefined>(undefined);
|
||||
@ -224,23 +405,236 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
|
||||
const [ahApi, setAhApi] = useState<ApiPromise|undefined>(initialAhApi);
|
||||
const [rcApi, setRcApi] = useState<ApiPromise|undefined>(initialRcApi);
|
||||
|
||||
const rcEndPointOptions = useRef(rcEndPoints.map((e) => ({ text: e, value: e })));
|
||||
const ahEndPointOptions = useRef(ahEndPoints.map((e) => ({ text: e, value: e })));
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(false);
|
||||
const [rcLowestBlock, setRcLowestBlock] = useState<number | null>(null);
|
||||
const [ahLowestBlock, setAhLowestBlock] = useState<number | null>(null);
|
||||
const [loadingProgress, setLoadingProgress] = useState<{ rc: number; ah: number } | null>(null);
|
||||
|
||||
const [customRcUrl, setCustomRcUrl] = useState<string>('ws://127.0.0.1:9944');
|
||||
const [customAhUrl, setCustomAhUrl] = useState<string>('ws://127.0.0.1:9944');
|
||||
const [showCustomRcInput, setShowCustomRcInput] = useState(false);
|
||||
const [showCustomAhInput, setShowCustomAhInput] = useState(false);
|
||||
|
||||
const CUSTOM_OPTION = 'custom';
|
||||
const rcEndPointOptions = useRef([...rcEndPoints.map((e) => ({ text: e, value: e })), { text: 'Custom endpoint...', value: CUSTOM_OPTION }]);
|
||||
const ahEndPointOptions = useRef([...ahEndPoints.map((e) => ({ text: e, value: e })), { text: 'Custom endpoint...', value: CUSTOM_OPTION }]);
|
||||
|
||||
// Load historical events from RC
|
||||
const loadRcHistoricalEvents = useCallback(async (blocksToLoad: number) => {
|
||||
if (!rcApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentBlockHash: Hash;
|
||||
|
||||
if (rcLowestBlock === null) {
|
||||
const finalizedHead = await rcApi.rpc.chain.getFinalizedHead();
|
||||
|
||||
currentBlockHash = finalizedHead;
|
||||
} else if (rcLowestBlock === 0) {
|
||||
return;
|
||||
} else {
|
||||
currentBlockHash = await rcApi.rpc.chain.getBlockHash(rcLowestBlock);
|
||||
}
|
||||
|
||||
let blocksProcessed = 0;
|
||||
|
||||
while (blocksProcessed < blocksToLoad) {
|
||||
try {
|
||||
const block = await rcApi.rpc.chain.getBlock(currentBlockHash);
|
||||
const blockNumber = block.block.header.number.toNumber();
|
||||
|
||||
if (blockNumber === 0) {
|
||||
setRcLowestBlock(0);
|
||||
break;
|
||||
}
|
||||
|
||||
const events = await (await rcApi.at(currentBlockHash)).query.system.events();
|
||||
const relevantEvents = events
|
||||
.map((e) => e.event)
|
||||
.filter((e) => filterRcEvents(e.data));
|
||||
|
||||
if (relevantEvents.length > 0) {
|
||||
const enhancedEvents: EnhancedEvent[] = relevantEvents.map((event) => ({
|
||||
blockHash: currentBlockHash.toString(),
|
||||
blockNumber,
|
||||
event
|
||||
}));
|
||||
|
||||
setRcEvents((prev) => {
|
||||
// Insert new events and sort by block number (highest first)
|
||||
const combined = [...prev, ...enhancedEvents];
|
||||
|
||||
return combined.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
});
|
||||
}
|
||||
|
||||
setRcLowestBlock(blockNumber - 1);
|
||||
currentBlockHash = block.block.header.parentHash;
|
||||
blocksProcessed++;
|
||||
|
||||
// Update progress
|
||||
const remaining = blocksToLoad - blocksProcessed;
|
||||
|
||||
setLoadingProgress((prev) => ({ ah: prev?.ah ?? 0, rc: remaining }));
|
||||
} catch (error) {
|
||||
console.error(`RC: Error at block ${currentBlockHash.toHex()}:`, error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [rcApi, rcLowestBlock]);
|
||||
|
||||
// Load historical events from AH
|
||||
const loadAhHistoricalEvents = useCallback(async (blocksToLoad: number) => {
|
||||
if (!ahApi) {
|
||||
return;
|
||||
}
|
||||
|
||||
let currentBlockHash: Hash;
|
||||
|
||||
if (ahLowestBlock === null) {
|
||||
const finalizedHead = await ahApi.rpc.chain.getFinalizedHead();
|
||||
|
||||
currentBlockHash = finalizedHead;
|
||||
} else if (ahLowestBlock === 0) {
|
||||
return;
|
||||
} else {
|
||||
currentBlockHash = await ahApi.rpc.chain.getBlockHash(ahLowestBlock);
|
||||
}
|
||||
|
||||
let blocksProcessed = 0;
|
||||
|
||||
while (blocksProcessed < blocksToLoad) {
|
||||
try {
|
||||
const block = await ahApi.rpc.chain.getBlock(currentBlockHash);
|
||||
const blockNumber = block.block.header.number.toNumber();
|
||||
|
||||
if (blockNumber === 0) {
|
||||
setAhLowestBlock(0);
|
||||
break;
|
||||
}
|
||||
|
||||
const events = await (await ahApi.at(currentBlockHash)).query.system.events();
|
||||
const relevantEvents = events
|
||||
.map((e) => e.event)
|
||||
.filter((e) => filterAhEvents(e.data));
|
||||
|
||||
if (relevantEvents.length > 0) {
|
||||
// Get block weight
|
||||
const weight = await (await ahApi.at(currentBlockHash)).query.system.blockWeight();
|
||||
|
||||
const formatWeight = (w: FrameSupportDispatchPerDispatchClassWeight) => {
|
||||
const normalRef = w.normal?.refTime?.toBigInt() || 0n;
|
||||
const operationalRef = w.operational?.refTime?.toBigInt() || 0n;
|
||||
const mandatoryRef = w.mandatory?.refTime?.toBigInt() || 0n;
|
||||
const totalRef = normalRef + operationalRef + mandatoryRef;
|
||||
|
||||
const normalProof = w.normal?.proofSize?.toBigInt() || 0n;
|
||||
const operationalProof = w.operational?.proofSize?.toBigInt() || 0n;
|
||||
const mandatoryProof = w.mandatory?.proofSize?.toBigInt() || 0n;
|
||||
const totalProof = normalProof + operationalProof + mandatoryProof;
|
||||
|
||||
return `${(Number(totalRef) / 1_000_000_000_000).toFixed(2)}s / ${(Number(totalProof) / 1024).toFixed(0)}KB`;
|
||||
};
|
||||
|
||||
const enhancedEvents: EnhancedEvent[] = relevantEvents.map((event) => ({
|
||||
blockHash: currentBlockHash.toString(),
|
||||
blockNumber,
|
||||
event,
|
||||
weight: formatWeight(weight)
|
||||
}));
|
||||
|
||||
setAhEvents((prev) => {
|
||||
// Insert new events and sort by block number (highest first)
|
||||
const combined = [...prev, ...enhancedEvents];
|
||||
|
||||
return combined.sort((a, b) => b.blockNumber - a.blockNumber);
|
||||
});
|
||||
}
|
||||
|
||||
setAhLowestBlock(blockNumber - 1);
|
||||
currentBlockHash = block.block.header.parentHash;
|
||||
blocksProcessed++;
|
||||
|
||||
// Update progress
|
||||
const remaining = blocksToLoad - blocksProcessed;
|
||||
|
||||
setLoadingProgress((prev) => ({ ah: remaining, rc: prev?.rc ?? 0 }));
|
||||
} catch (error) {
|
||||
console.error(`AH: Error at block ${currentBlockHash.toHex()}:`, error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}, [ahApi, ahLowestBlock]);
|
||||
|
||||
// Load historical events (600 blocks at a time)
|
||||
const _loadHistoricalEvents = useCallback(async () => {
|
||||
if (isLoadingHistory || (!rcApi && !ahApi)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoadingHistory(true);
|
||||
setLoadingProgress({ ah: 600, rc: 600 });
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
loadRcHistoricalEvents(600),
|
||||
loadAhHistoricalEvents(600)
|
||||
]);
|
||||
|
||||
console.log('Historical events loaded');
|
||||
} catch (error) {
|
||||
console.error('Error loading historical events:', error);
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
setLoadingProgress(null);
|
||||
}
|
||||
}, [isLoadingHistory, rcApi, ahApi, loadRcHistoricalEvents, loadAhHistoricalEvents]);
|
||||
|
||||
const _onSelectAhUrl = useCallback((newAhUrl: string) => {
|
||||
if (newAhUrl !== ahUrl) {
|
||||
if (newAhUrl === CUSTOM_OPTION) {
|
||||
setShowCustomAhInput(true);
|
||||
} else if (newAhUrl !== ahUrl) {
|
||||
ahApi?.disconnect().catch(console.log);
|
||||
setShowCustomAhInput(false);
|
||||
setAhUrl(newAhUrl);
|
||||
}
|
||||
}, [ahApi, ahUrl]);
|
||||
|
||||
const _onSelectRcUrl = useCallback((newRcUrl: string) => {
|
||||
if (newRcUrl !== rcUrl) {
|
||||
if (newRcUrl === CUSTOM_OPTION) {
|
||||
setShowCustomRcInput(true);
|
||||
} else if (newRcUrl !== rcUrl) {
|
||||
rcApi?.disconnect().catch(console.log);
|
||||
setShowCustomRcInput(false);
|
||||
setRcUrl(newRcUrl);
|
||||
}
|
||||
}, [rcApi, rcUrl]);
|
||||
|
||||
const _onCustomRcUrlChange = useCallback((value: string) => {
|
||||
setCustomRcUrl(value);
|
||||
}, []);
|
||||
|
||||
const _onCustomAhUrlChange = useCallback((value: string) => {
|
||||
setCustomAhUrl(value);
|
||||
}, []);
|
||||
|
||||
const _onApplyCustomRcUrl = useCallback(() => {
|
||||
if (customRcUrl?.trim()) {
|
||||
rcApi?.disconnect().catch(console.log);
|
||||
setRcUrl(customRcUrl.trim());
|
||||
setShowCustomRcInput(false);
|
||||
}
|
||||
}, [customRcUrl, rcApi]);
|
||||
|
||||
const _onApplyCustomAhUrl = useCallback(() => {
|
||||
if (customAhUrl?.trim()) {
|
||||
ahApi?.disconnect().catch(console.log);
|
||||
setAhUrl(customAhUrl.trim());
|
||||
setShowCustomAhInput(false);
|
||||
}
|
||||
}, [customAhUrl, ahApi]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRelayChain) {
|
||||
setRcUrl(apiUrl);
|
||||
@ -258,8 +652,10 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
|
||||
useEffect(() => {
|
||||
setRcApi(undefined);
|
||||
setAhApi(undefined);
|
||||
setRcOutput([]);
|
||||
setAhOutput([]);
|
||||
setRcOutput(undefined);
|
||||
setAhOutput(undefined);
|
||||
setRcEvents([]);
|
||||
setAhEvents([]);
|
||||
|
||||
if (isRelayChain) {
|
||||
setRcApi(api);
|
||||
@ -277,43 +673,166 @@ function CommandCenter ({ ahApi: initialAhApi, ahEndPoints, isRelayChain, rcApi:
|
||||
}, [ahUrl, api, isRelayChain, rcUrl]);
|
||||
|
||||
useEffect(() => {
|
||||
ahApi && rcApi && commandCenterHandler(rcApi, ahApi, setRcOutput, setAhOutput).catch(console.log);
|
||||
ahApi && rcApi && commandCenterHandler(rcApi, ahApi, setRcOutput, setAhOutput, setRcEvents, setAhEvents).catch(console.log);
|
||||
}, [ahApi, rcApi]);
|
||||
|
||||
// Auto-load 600 blocks on page load
|
||||
useEffect(() => {
|
||||
if (ahApi && rcApi && rcLowestBlock === null && ahLowestBlock === null) {
|
||||
_loadHistoricalEvents().catch(console.error);
|
||||
}
|
||||
}, [ahApi, rcApi, rcLowestBlock, ahLowestBlock, _loadHistoricalEvents]);
|
||||
|
||||
return (
|
||||
<StyledDiv>
|
||||
<StyledWarningBanner>
|
||||
<MarkWarning content='This page is for developer debugging only.' />
|
||||
</StyledWarningBanner>
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
icon='history'
|
||||
isDisabled={isLoadingHistory || (!rcApi && !ahApi)}
|
||||
label='Load 600 Blocks History'
|
||||
onClick={_loadHistoricalEvents}
|
||||
/>
|
||||
{loadingProgress && (
|
||||
<div className='history-info progress'>
|
||||
Loading historical events... RC: {loadingProgress.rc} blocks left | AH: {loadingProgress.ah} blocks left
|
||||
</div>
|
||||
)}
|
||||
{!loadingProgress && (rcLowestBlock !== null || ahLowestBlock !== null) && (
|
||||
<div className='history-info'>
|
||||
{rcLowestBlock !== null && `RC: Indexed to Block #${rcLowestBlock}`}
|
||||
{rcLowestBlock !== null && ahLowestBlock !== null && ' | '}
|
||||
{ahLowestBlock !== null && `AH: Indexed to Block #${ahLowestBlock}`}
|
||||
</div>
|
||||
)}
|
||||
</StyledButtonContainer>
|
||||
<RelaySection
|
||||
isRelayChain={!!isRelayChain}
|
||||
rcApi={rcApi}
|
||||
rcEvents={rcEvents}
|
||||
rcOutput={rcOutput}
|
||||
rcUrl={rcUrl || ''}
|
||||
>
|
||||
<Dropdown
|
||||
defaultValue={rcUrl}
|
||||
isButton
|
||||
isDisabled={!!isRelayChain}
|
||||
onChange={_onSelectRcUrl}
|
||||
options={rcEndPointOptions.current}
|
||||
/>
|
||||
<StyledEndpointControls>
|
||||
<Dropdown
|
||||
defaultValue={rcUrl}
|
||||
isButton
|
||||
isDisabled={!!isRelayChain}
|
||||
onChange={_onSelectRcUrl}
|
||||
options={rcEndPointOptions.current}
|
||||
/>
|
||||
{showCustomRcInput && !isRelayChain && (
|
||||
<StyledCustomInput>
|
||||
<Input
|
||||
autoFocus
|
||||
label='Custom RC Endpoint'
|
||||
onChange={_onCustomRcUrlChange}
|
||||
placeholder='wss://your-relay-chain-endpoint.com'
|
||||
value={customRcUrl}
|
||||
/>
|
||||
<Button
|
||||
label='Connect'
|
||||
onClick={_onApplyCustomRcUrl}
|
||||
/>
|
||||
</StyledCustomInput>
|
||||
)}
|
||||
</StyledEndpointControls>
|
||||
</RelaySection>
|
||||
<AssetHubSection
|
||||
ahApi={ahApi}
|
||||
ahEvents={ahEvents}
|
||||
ahOutput={ahOutput}
|
||||
ahUrl={ahUrl || ''}
|
||||
isRelayChain={!!isRelayChain}
|
||||
>
|
||||
<Dropdown
|
||||
defaultValue={ahUrl}
|
||||
isButton
|
||||
isDisabled={!isRelayChain}
|
||||
onChange={_onSelectAhUrl}
|
||||
options={ahEndPointOptions.current}
|
||||
/>
|
||||
<StyledEndpointControls>
|
||||
<Dropdown
|
||||
defaultValue={ahUrl}
|
||||
isButton
|
||||
isDisabled={!isRelayChain}
|
||||
onChange={_onSelectAhUrl}
|
||||
options={ahEndPointOptions.current}
|
||||
/>
|
||||
{showCustomAhInput && isRelayChain && (
|
||||
<StyledCustomInput>
|
||||
<Input
|
||||
autoFocus
|
||||
label='Custom AH Endpoint'
|
||||
onChange={_onCustomAhUrlChange}
|
||||
placeholder='wss://your-asset-hub-endpoint.com'
|
||||
value={customAhUrl}
|
||||
/>
|
||||
<Button
|
||||
label='Connect'
|
||||
onClick={_onApplyCustomAhUrl}
|
||||
/>
|
||||
</StyledCustomInput>
|
||||
)}
|
||||
</StyledEndpointControls>
|
||||
</AssetHubSection>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledEndpointControls = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
`;
|
||||
|
||||
const StyledCustomInput = styled.div`
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: flex-end;
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledWarningBanner = styled.div`
|
||||
grid-column: 1 / -1;
|
||||
padding: 1rem 1.5rem;
|
||||
background: var(--bg-table);
|
||||
border-radius: 0.5rem;
|
||||
border: 2px solid var(--color-warning);
|
||||
|
||||
.ui--MarkWarning {
|
||||
margin: 0;
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
background: var(--bg-table);
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.history-info {
|
||||
font-size: var(--font-size-small);
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
&.progress {
|
||||
color: var(--color-text);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@ -322,7 +841,7 @@ const StyledDiv = styled.div`
|
||||
.dropdown {
|
||||
min-width: max-content !important;
|
||||
}
|
||||
|
||||
|
||||
@media screen and (max-width: 1200px){
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
}
|
||||
|
||||
@ -3,12 +3,12 @@
|
||||
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ApiPromise } from '@polkadot/api';
|
||||
import type { IRcOutput } from './index.js';
|
||||
import type { EnhancedEvent, IRcOutput } from './index.js';
|
||||
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { CardSummary, Expander, Icon, MarkWarning, Spinner, styled, Tooltip } from '@polkadot/react-components';
|
||||
import { CardSummary, Expander, Spinner, styled } from '@polkadot/react-components';
|
||||
import { Event as EventDisplay } from '@polkadot/react-params';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
@ -17,170 +17,241 @@ import { useTranslation } from '../translate.js';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
rcApi?: ApiPromise;
|
||||
rcOutput: IRcOutput[];
|
||||
rcOutput?: IRcOutput;
|
||||
rcEvents: EnhancedEvent[];
|
||||
rcUrl: string;
|
||||
isRelayChain: boolean
|
||||
}
|
||||
|
||||
function RelaySection ({ children, isRelayChain, rcApi, rcOutput, rcUrl }: Props) {
|
||||
function RelaySection ({ children, isRelayChain, rcApi, rcEvents, rcOutput, rcUrl }: Props) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1
|
||||
style={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
textTransform: 'capitalize'
|
||||
}}
|
||||
>
|
||||
{t('Relay chain')}
|
||||
{children}
|
||||
</h1>
|
||||
{!rcApi && <Spinner label='Connecting to Relay chain' />}
|
||||
<StyledSection>
|
||||
{rcOutput.map((rc) => {
|
||||
return (
|
||||
<div
|
||||
className='relay__chain'
|
||||
key={rc.finalizedBlock}
|
||||
>
|
||||
<div className='details'>
|
||||
<div className='session__summary'>
|
||||
<h4 className='--digits'>
|
||||
{isRelayChain
|
||||
? <Link to={`/explorer/query/${rc.finalizedBlock}`}>#{formatNumber(rc.finalizedBlock)}</Link>
|
||||
: (
|
||||
<Link
|
||||
target='_blank'
|
||||
to={`${window.location.origin}/?rpc=${rcUrl}#/explorer/query/${rc.finalizedBlock}`}
|
||||
>
|
||||
#{formatNumber(rc.finalizedBlock)}
|
||||
</Link>)
|
||||
}
|
||||
</h4>
|
||||
<CardSummary label={t('session')}>
|
||||
#{formatNumber(rc.session.index)}
|
||||
<StyledColumn>
|
||||
<StyledInfoBox>
|
||||
<h2>
|
||||
{t('Relay Chain')}
|
||||
{children}
|
||||
</h2>
|
||||
{!rcApi && <Spinner label={t('Connecting to Relay chain')} />}
|
||||
{rcOutput && (
|
||||
<div className='info-content'>
|
||||
<div className='block-info'>
|
||||
{isRelayChain
|
||||
? <Link to={`/explorer/query/${rcOutput.finalizedBlock}`}>#{formatNumber(rcOutput.finalizedBlock)}</Link>
|
||||
: (
|
||||
<Link
|
||||
target='_blank'
|
||||
to={`${window.location.origin}/?rpc=${rcUrl}#/explorer/query/${rcOutput.finalizedBlock}`}
|
||||
>
|
||||
#{formatNumber(rcOutput.finalizedBlock)}
|
||||
</Link>)
|
||||
}
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Session Info')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('session')}>
|
||||
#{formatNumber(rcOutput.session.index)}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('queued')}>
|
||||
{rcOutput.session.hasQueuedInSession ? 'Yes' : 'No'}
|
||||
</CardSummary>
|
||||
{rcOutput.session.historicalRange &&
|
||||
<CardSummary label={t('historical range')}>
|
||||
{`#${formatNumber(rcOutput.session.historicalRange[0])} → #${formatNumber(rcOutput.session.historicalRange[1])}`}
|
||||
</CardSummary>
|
||||
{rc.session.historicalRange &&
|
||||
<CardSummary label={t('historical range')}>
|
||||
{`${rc.session.historicalRange?.[0]} → ${rc.session.historicalRange?.[1]}`}
|
||||
</CardSummary>
|
||||
}
|
||||
</div>
|
||||
<div className='stakingAhClient__summary'>
|
||||
{rc.stakingAhClient.hasQueuedInClient &&
|
||||
<div className='stakingAhClient__hasQueuedInClient'>
|
||||
<MarkWarning content={t('There is a validator set queued in ah-client.')} />
|
||||
<CardSummary label={t('id')}>
|
||||
{rc.stakingAhClient.hasQueuedInClient[0]}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('number of validators')}>
|
||||
{rc.stakingAhClient.hasQueuedInClient[1].length}
|
||||
</CardSummary>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
<div className='events__summary'>
|
||||
<h3>{t('events')}</h3>
|
||||
{rc.events.map((event) => {
|
||||
const eventName = `${event.section}.${event.method}`;
|
||||
|
||||
return (
|
||||
<Expander
|
||||
isLeft
|
||||
key={event.index.toString()}
|
||||
summary={eventName}
|
||||
summaryMeta={event.meta}
|
||||
>
|
||||
<EventDisplay
|
||||
className='details'
|
||||
eventName={eventName}
|
||||
value={event}
|
||||
withExpander
|
||||
/>
|
||||
</Expander>
|
||||
|
||||
);
|
||||
})}
|
||||
{rc.events.length === 0 &&
|
||||
<div className='warning__tooltip'>
|
||||
<MarkWarning content={t('No events available')} />
|
||||
<Icon
|
||||
icon='info-circle'
|
||||
tooltip={`${rc.finalizedBlock}-rc-no-events`}
|
||||
/>
|
||||
<Tooltip
|
||||
text={'No relevant events found for stakingAhClient, session, or historical in the current block.'}
|
||||
trigger={`${rc.finalizedBlock}-rc-no-events`}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Staking AH Client')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('mode')}>
|
||||
{rcOutput.stakingAhClient.mode}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('next active id')}>
|
||||
{rcOutput.stakingAhClient.hasNextActiveId !== undefined ? rcOutput.stakingAhClient.hasNextActiveId : 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('queued validator points')}>
|
||||
{rcOutput.stakingAhClient.validatorPoints}
|
||||
</CardSummary>
|
||||
</div>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('queued validator set id')}>
|
||||
{rcOutput.stakingAhClient.hasQueuedInClient ? rcOutput.stakingAhClient.hasQueuedInClient[0] : 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('queued validators')}>
|
||||
{rcOutput.stakingAhClient.hasQueuedInClient ? rcOutput.stakingAhClient.hasQueuedInClient[1].length : 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Staking/Elections')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('force era')}>
|
||||
{rcOutput.staking.forceEra || 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('election phase')}>
|
||||
{rcOutput.staking.electionPhase || 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('validator count')}>
|
||||
{rcOutput.staking.validatorCount || 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
<div className='section'>
|
||||
<h4>{t('Parachain Config')}</h4>
|
||||
<div className='stats'>
|
||||
<CardSummary label={t('max downward msg size')}>
|
||||
{rcOutput.parachainConfig ? formatNumber(rcOutput.parachainConfig.maxDownwardMessageSize) : 'None'}
|
||||
</CardSummary>
|
||||
<CardSummary label={t('max upward msg size')}>
|
||||
{rcOutput.parachainConfig ? formatNumber(rcOutput.parachainConfig.maxUpwardMessageSize) : 'None'}
|
||||
</CardSummary>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</StyledInfoBox>
|
||||
<StyledEventsBox>
|
||||
<h3>{t('Relay Chain Events')}</h3>
|
||||
{rcEvents.length === 0 && (
|
||||
<div className='no-events'>
|
||||
{t('No relevant events in recent blocks')}
|
||||
</div>
|
||||
)}
|
||||
{rcEvents.map((enhancedEvent, index) => {
|
||||
const { blockNumber, event } = enhancedEvent;
|
||||
const eventName = `${event.section}.${event.method}`;
|
||||
|
||||
return (
|
||||
<Expander
|
||||
isLeft
|
||||
key={`${event.index.toString()}-${index}`}
|
||||
summary={
|
||||
<>
|
||||
{eventName}
|
||||
<div className='absolute --digits'>
|
||||
{isRelayChain
|
||||
? <Link to={`/explorer/query/${blockNumber}`}>#{formatNumber(blockNumber)}</Link>
|
||||
: (
|
||||
<Link
|
||||
target='_blank'
|
||||
to={`${window.location.origin}/?rpc=${rcUrl}#/explorer/query/${blockNumber}`}
|
||||
>
|
||||
#{formatNumber(blockNumber)}
|
||||
</Link>)
|
||||
}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
summaryMeta={event.meta}
|
||||
>
|
||||
<EventDisplay
|
||||
className='details'
|
||||
eventName={eventName}
|
||||
value={event}
|
||||
withExpander
|
||||
/>
|
||||
</Expander>
|
||||
);
|
||||
})}
|
||||
</StyledSection>
|
||||
</div>);
|
||||
</StyledEventsBox>
|
||||
</StyledColumn>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledSection = styled.section`
|
||||
margin-block: 1rem;
|
||||
overflow: auto;
|
||||
const StyledColumn = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
`;
|
||||
|
||||
.warning {
|
||||
max-width: fit-content;
|
||||
margin-left: 0;
|
||||
}
|
||||
const StyledInfoBox = styled.div`
|
||||
background: var(--bg-table);
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
|
||||
.ui--Labelled-content {
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.warning__tooltip {
|
||||
h2 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
font-size: var(--font-size-h2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.relay__chain {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
background: var(--bg-table);
|
||||
margin-bottom: 0.35rem;
|
||||
padding: 0.8rem 1rem;
|
||||
border-radius: 0.5rem;
|
||||
.info-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
.details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.8rem;
|
||||
.block-info {
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.session__summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
h4 {
|
||||
font-weight: 400;
|
||||
font-size: medium;
|
||||
}
|
||||
}
|
||||
|
||||
.stakingAhClient__summary {
|
||||
.stakingAhClient__hasQueuedInClient {
|
||||
display: flex;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
.section {
|
||||
h4 {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
}
|
||||
|
||||
.events__summary {
|
||||
justify-self: center;
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: var(--font-size-h2);
|
||||
.stats {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ui--Labelled {
|
||||
label {
|
||||
font-size: medium !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.ui--Labelled-content {
|
||||
font-size: small !important;
|
||||
text-align: left !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEventsBox = styled.div`
|
||||
background: var(--bg-table);
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.5rem;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 1rem;
|
||||
font-size: var(--font-size-h3);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.no-events {
|
||||
color: var(--color-text-secondary);
|
||||
font-style: italic;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ui--Expander {
|
||||
margin-bottom: 0.5rem;
|
||||
position: relative;
|
||||
|
||||
.absolute {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 0.72rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user