Display ReceivedHeartbeats status for staking accounts (#1476)

* Display ReceivedHeartbeats status for staking accounts

* Fix

* linting error

* complete merge

* fix changed event sig

* fix recentlyOnline check
This commit is contained in:
kwingram25 2019-08-28 16:12:24 +02:00 committed by Jaco Greeff
parent d0d9397612
commit 1f51e600d1
11 changed files with 303 additions and 202 deletions

View File

@ -3,17 +3,16 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { RecentlyOfflineMap } from '../../types';
import { AccountId, Exposure, StakingLedger, ValidatorPrefs } from '@polkadot/types/interfaces';
import { DerivedBalances, DerivedStaking, DerivedStakingOnlineStatus } from '@polkadot/api-derive/types';
import { ApiProps } from '@polkadot/react-api/types';
import { DerivedBalances, DerivedStaking } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/react-components/types';
import { AccountId, BlockNumber, Exposure, StakingLedger, ValidatorPrefs } from '@polkadot/types/interfaces';
import { KeyringSectionOption } from '@polkadot/ui-keyring/options/types';
import { Popup } from 'semantic-ui-react';
import React from 'react';
import styled from 'styled-components';
import { AddressCard, AddressInfo, AddressMini, AddressRow, Button, Menu, RecentlyOffline, TxButton } from '@polkadot/react-components';
import { AddressCard, AddressInfo, AddressMini, AddressRow, Button, Menu, OnlineStatus, TxButton } from '@polkadot/react-components';
import { withCalls, withMulti } from '@polkadot/react-api';
import BondExtra from './BondExtra';
@ -26,12 +25,14 @@ import Unbond from './Unbond';
import Validate from './Validate';
import { u8aToHex, u8aConcat } from '@polkadot/util';
import { updateOnlineStatus } from '../../util';
type Props = ApiProps & I18nProps & {
accountId: string;
allStashes?: string[];
balances_all?: DerivedBalances;
className?: string;
recentlyOffline: RecentlyOfflineMap;
recentlyOnline: Record<string, BlockNumber>;
staking_info?: DerivedStaking;
stashOptions: KeyringSectionOption[];
};
@ -51,6 +52,7 @@ interface State {
isUnbondOpen: boolean;
isValidateOpen: boolean;
nominees?: string[];
onlineStatus: DerivedStakingOnlineStatus;
sessionIds: string[];
stakers?: Exposure;
stakingLedger?: StakingLedger;
@ -79,16 +81,17 @@ class Account extends React.PureComponent<Props, State> {
isStashValidating: false,
isUnbondOpen: false,
isValidateOpen: false,
onlineStatus: {},
sessionIds: [],
stashId: null
};
public static getDerivedStateFromProps ({ allStashes, staking_info }: Props): Pick<State, never> | null {
public static getDerivedStateFromProps ({ allStashes, recentlyOnline, staking_info }: Props): Pick<State, never> | null {
if (!staking_info) {
return null;
}
const { controllerId, nextSessionIds, nominators, rewardDestination, sessionIds, stakers, stakingLedger, stashId, validatorPrefs } = staking_info;
const { controllerId, nextSessionIds, nominators, online, offline, rewardDestination, sessionIds, stakers, stakingLedger, stashId, validatorPrefs } = staking_info;
const isStashNominating = nominators && nominators.length !== 0;
const _stashId = toIdString(stashId);
const isStashValidating = !!allStashes && !!_stashId && allStashes.includes(_stashId);
@ -104,6 +107,7 @@ class Account extends React.PureComponent<Props, State> {
isStashNominating,
isStashValidating,
nominees: nominators && nominators.map(toIdString),
onlineStatus: updateOnlineStatus(recentlyOnline)(sessionIds || null, { online, offline }),
sessionIds: (
nextSessionIds.length
? nextSessionIds
@ -131,7 +135,7 @@ class Account extends React.PureComponent<Props, State> {
return (
<AddressCard
buttons={this.renderButtons()}
iconInfo={this.renderOffline(stashId)}
iconInfo={this.renderOnlineStatus()}
label={t('stash')}
type='account'
value={stashId}
@ -229,7 +233,6 @@ class Account extends React.PureComponent<Props, State> {
{nominees.map((nomineeId, index): React.ReactNode => (
<AddressMini
key={index}
iconInfo={this.renderOffline(nomineeId)}
value={nomineeId}
withBalance={false}
withBonded
@ -239,13 +242,17 @@ class Account extends React.PureComponent<Props, State> {
);
}
private renderOffline (address: AccountId | string): React.ReactNode {
const { recentlyOffline } = this.props;
private renderOnlineStatus (): React.ReactNode {
const { onlineStatus, controllerId } = this.state;
if (!controllerId || !onlineStatus) {
return null;
}
return (
<RecentlyOffline
accountId={address}
offline={recentlyOffline[address.toString()]}
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
);
@ -262,7 +269,6 @@ class Account extends React.PureComponent<Props, State> {
return (
<div className='staking--Account-detail actions'>
<AddressRow
iconInfo={this.renderOffline(controllerId)}
label={t('controller')}
value={controllerId}
withAddressOrName

View File

@ -32,7 +32,7 @@ class Accounts extends React.PureComponent<Props, State> {
};
public render (): React.ReactNode {
const { allStashes, className, recentlyOffline, t } = this.props;
const { allStashes, className, recentlyOnline, t } = this.props;
const { isNewStakeOpen } = this.state;
const stashOptions = this.getStashOptions();
const myStashes = this.getMyStashes();
@ -64,7 +64,7 @@ class Accounts extends React.PureComponent<Props, State> {
allStashes={allStashes}
accountId={address}
key={index}
recentlyOffline={recentlyOffline}
recentlyOnline={recentlyOnline}
stashOptions={stashOptions}
/>
))}
@ -112,14 +112,14 @@ class Accounts extends React.PureComponent<Props, State> {
}
private toggleNewStake = (): void => {
this.setState(({ isNewStakeOpen }): State => ({
this.setState(({ isNewStakeOpen }): Pick<State, never> => ({
isNewStakeOpen: !isNewStakeOpen
}));
}
}
export default withMulti(
styled(Accounts)`
styled(Accounts as React.ComponentClass<Props, State>)`
.ui--CardGrid-buttons {
text-align: right;
}

View File

@ -3,19 +3,20 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, Balance, Exposure } from '@polkadot/types/interfaces';
import { DerivedBalancesMap, DerivedStaking } from '@polkadot/api-derive/types';
import { AccountId, Balance, BlockNumber, Exposure } from '@polkadot/types/interfaces';
import { DerivedBalancesMap, DerivedStaking, DerivedStakingOnlineStatus } from '@polkadot/api-derive/types';
import { ApiProps } from '@polkadot/react-api/types';
import { I18nProps } from '@polkadot/react-components/types';
import { ValidatorFilter, RecentlyOfflineMap } from '../types';
import { ValidatorFilter } from '../types';
import React from 'react';
import styled from 'styled-components';
import { withCalls, withMulti } from '@polkadot/react-api/with';
import { AddressCard, AddressMini, RecentlyOffline } from '@polkadot/react-components';
import { AddressCard, AddressMini, OnlineStatus } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import keyring from '@polkadot/ui-keyring';
import { formatBalance } from '@polkadot/util';
import { updateOnlineStatus } from '../util';
import translate from '../translate';
@ -24,15 +25,16 @@ interface Props extends ApiProps, I18nProps {
balances: DerivedBalancesMap;
className?: string;
defaultName: string;
filter: ValidatorFilter;
lastAuthor: string;
lastBlock: string;
recentlyOffline: RecentlyOfflineMap;
filter: ValidatorFilter;
recentlyOnline?: Record<string, BlockNumber>;
staking_info?: DerivedStaking;
}
interface State {
controllerId: string | null;
onlineStatus: DerivedStakingOnlineStatus;
stashActive: string | null;
stashTotal: string | null;
sessionId: string | null;
@ -49,6 +51,7 @@ class Address extends React.PureComponent<Props, State> {
this.state = {
controllerId: null,
onlineStatus: {},
sessionId: null,
stashActive: null,
stashId: null,
@ -57,15 +60,16 @@ class Address extends React.PureComponent<Props, State> {
};
}
public static getDerivedStateFromProps ({ staking_info }: Props, prevState: State): Pick<State, never> | null {
public static getDerivedStateFromProps ({ recentlyOnline = {}, staking_info }: Props, prevState: State): Pick<State, never> | null {
if (!staking_info) {
return null;
}
const { controllerId, nextSessionId, stakers, stashId, stakingLedger } = staking_info;
const { controllerId, nextSessionId, online, offline, stakers, stakingLedger, stashId } = staking_info;
return {
controllerId: controllerId && controllerId.toString(),
onlineStatus: updateOnlineStatus(recentlyOnline)(staking_info.sessionIds || null, { offline, online }),
sessionId: nextSessionId && nextSessionId.toString(),
stashActive: stakingLedger
? formatBalance(stakingLedger.active)
@ -98,7 +102,7 @@ class Address extends React.PureComponent<Props, State> {
buttons={this.renderKeys()}
className={className}
defaultName={defaultName}
iconInfo={this.renderOffline()}
iconInfo={this.renderOnlineStatus()}
key={stashId || controllerId || undefined}
value={stashId || address}
withBalance={{ bonded }}
@ -165,10 +169,9 @@ class Address extends React.PureComponent<Props, State> {
}
private hasWarnings (): boolean {
const { recentlyOffline } = this.props;
const { stashId } = this.state;
const { stashId, onlineStatus } = this.state;
if (!stashId || !recentlyOffline[stashId]) {
if (!stashId || !onlineStatus.offline || !onlineStatus.offline.length) {
return false;
}
@ -204,20 +207,16 @@ class Address extends React.PureComponent<Props, State> {
);
}
private renderOffline (): React.ReactNode {
const { recentlyOffline } = this.props;
const { stashId } = this.state;
if (!stashId || !recentlyOffline[stashId]) {
private renderOnlineStatus (): React.ReactNode {
const { controllerId, onlineStatus } = this.state;
if (!controllerId || !onlineStatus) {
return null;
}
const offline = recentlyOffline[stashId];
return (
<RecentlyOffline
accountId={stashId}
offline={offline}
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
);

View File

@ -2,9 +2,10 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { BlockNumber } from '@polkadot/types/interfaces';
import { DerivedBalancesMap } from '@polkadot/api-derive/types';
import { I18nProps } from '@polkadot/react-components/types';
import { ValidatorFilter, RecentlyOfflineMap } from '../types';
import { ValidatorFilter } from '../types';
import React from 'react';
import { Columar, Column, Dropdown, FilterOverlay } from '@polkadot/react-components';
@ -18,7 +19,7 @@ interface Props extends I18nProps {
lastAuthor?: string;
lastBlock: string;
next: string[];
recentlyOffline: RecentlyOfflineMap;
recentlyOnline: Record<string, BlockNumber>;
}
interface State {
@ -79,7 +80,7 @@ class CurrentList extends React.PureComponent<Props, State> {
}
private renderColumn (addresses: string[], defaultName: string): React.ReactNode {
const { balances, lastAuthor, lastBlock, recentlyOffline } = this.props;
const { balances, lastAuthor, lastBlock, recentlyOnline } = this.props;
const { filter } = this.state;
return addresses.map((address): React.ReactNode => (
@ -91,7 +92,7 @@ class CurrentList extends React.PureComponent<Props, State> {
filter={filter}
lastAuthor={lastAuthor}
lastBlock={lastBlock}
recentlyOffline={recentlyOffline}
recentlyOnline={recentlyOnline}
/>
));
}

View File

@ -22,7 +22,7 @@ interface Props extends ApiProps, BareProps, ComponentProps {
class Overview extends React.PureComponent<Props> {
public render (): React.ReactNode {
const { chain_subscribeNewHeads, allControllers, allStashes, recentlyOffline, currentValidatorsControllersV1OrStashesV2, isSubstrateV2 } = this.props;
const { chain_subscribeNewHeads, allControllers, allStashes, currentValidatorsControllersV1OrStashesV2, isSubstrateV2, recentlyOnline } = this.props;
let nextSorted: string[];
if (isSubstrateV2) {
@ -59,7 +59,7 @@ class Overview extends React.PureComponent<Props> {
lastBlock={lastBlock}
lastAuthor={lastAuthor}
next={nextSorted}
recentlyOffline={recentlyOffline}
recentlyOnline={recentlyOnline}
/>
</div>
);

View File

@ -3,15 +3,16 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId } from '@polkadot/types/interfaces';
import { AccountId, AuthorityId, BlockNumber, EventRecord } from '@polkadot/types/interfaces';
import { AppProps, I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
import { ComponentProps, RecentlyOffline, RecentlyOfflineMap } from './types';
import { ComponentProps } from './types';
import BN from 'bn.js';
import React from 'react';
import { Route, Switch } from 'react-router';
import { Option } from '@polkadot/types';
import { createType, Option } from '@polkadot/types';
import { HelpOverlay } from '@polkadot/react-components';
import Tabs, { TabItem } from '@polkadot/react-components/Tabs';
import { withCalls, withMulti, withObservable } from '@polkadot/react-api';
@ -27,15 +28,16 @@ import translate from './translate';
type Props = AppProps & ApiProps & I18nProps & {
allAccounts?: SubjectInfo;
allStashesAndControllers?: [AccountId[], Option<AccountId>[]];
chain_bestNumber?: BlockNumber;
currentValidatorsControllersV1OrStashesV2?: AccountId[];
staking_recentlyOffline?: RecentlyOffline;
recentlyOnline?: AuthorityId[];
};
interface State {
allControllers: string[];
allStashes: string[];
currentValidatorsControllersV1OrStashesV2: string[];
recentlyOffline: RecentlyOfflineMap;
recentlyOnline: Record<string, BlockNumber>;
tabs: TabItem[];
}
@ -51,7 +53,7 @@ class App extends React.PureComponent<Props, State> {
allControllers: [],
allStashes: [],
currentValidatorsControllersV1OrStashesV2: [],
recentlyOffline: {},
recentlyOnline: {},
tabs: [
{
isRoot: true,
@ -66,30 +68,29 @@ class App extends React.PureComponent<Props, State> {
};
}
public static getDerivedStateFromProps ({ allStashesAndControllers = [[], []], currentValidatorsControllersV1OrStashesV2 = [], staking_recentlyOffline = [] }: Props): Pick<State, never> {
public static getDerivedStateFromProps (props: Props, state: State): Pick<State, never> {
const { allStashesAndControllers = [[], []], chain_bestNumber, currentValidatorsControllersV1OrStashesV2 = [] } = props;
const recentlyOnline: Record<string, BlockNumber> = {
...(state.recentlyOnline || {}),
...(props.recentlyOnline || []).reduce(
(result: Record<string, BlockNumber>, authorityId): Record<string, BlockNumber> => ({
...result,
[authorityId.toString()]: chain_bestNumber || createType('BlockNumber', new BN(0))
}),
{}
)
};
return {
allControllers: allStashesAndControllers[1].filter((optId): boolean => optId.isSome).map((accountId): string =>
accountId.unwrap().toString()
),
allStashes: allStashesAndControllers[0].map((accountId): string => accountId.toString()),
allStashes: allStashesAndControllers[0].filter((): boolean => true).map((accountId): string => accountId.toString()),
currentValidatorsControllersV1OrStashesV2: currentValidatorsControllersV1OrStashesV2.map((authorityId): string =>
authorityId.toString()
),
recentlyOffline: staking_recentlyOffline.reduce(
(result, [accountId, blockNumber, count]): RecentlyOfflineMap => {
const account = accountId.toString();
if (!result[account]) {
result[account] = [];
}
result[account].push({
blockNumber,
count
});
return result;
}, {} as unknown as RecentlyOfflineMap)
recentlyOnline
};
}
@ -120,7 +121,7 @@ class App extends React.PureComponent<Props, State> {
private renderComponent (Component: React.ComponentType<ComponentProps>): () => React.ReactNode {
return (): React.ReactNode => {
const { allControllers, allStashes, currentValidatorsControllersV1OrStashesV2, recentlyOffline } = this.state;
const { allControllers, allStashes, currentValidatorsControllersV1OrStashesV2, recentlyOnline } = this.state;
const { allAccounts } = this.props;
if (!allAccounts) {
@ -133,7 +134,7 @@ class App extends React.PureComponent<Props, State> {
allControllers={allControllers}
allStashes={allStashes}
currentValidatorsControllersV1OrStashesV2={currentValidatorsControllersV1OrStashesV2}
recentlyOffline={recentlyOffline}
recentlyOnline={recentlyOnline}
/>
);
};
@ -144,9 +145,23 @@ export default withMulti(
App,
translate,
withCalls<Props>(
'derive.chain.bestNumber',
['derive.staking.controllers', { propName: 'allStashesAndControllers' }],
['query.session.validators', { propName: 'currentValidatorsControllersV1OrStashesV2' }],
'query.staking.recentlyOffline'
[
'query.system.events',
{
propName: 'recentlyOnline',
transform: (value?: EventRecord[]): AuthorityId[] =>
(value || [])
.filter(({ event: { method, section } }): boolean =>
section === 'imOnline' && method === 'HeartbeatReceived'
)
.map(
({ event: { data: [authorityId] } }): AuthorityId => authorityId as AuthorityId
)
}
]
),
withObservable(accountObservable.subject, { propName: 'allAccounts' })
);

View File

@ -2,7 +2,7 @@
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
import { BlockNumber } from '@polkadot/types/interfaces';
import { DerivedFees, DerivedBalances } from '@polkadot/api-derive/types';
import { SubjectInfo } from '@polkadot/ui-keyring/observable/types';
@ -15,7 +15,7 @@ export interface ComponentProps {
allControllers: string[];
allStashes: string[];
currentValidatorsControllersV1OrStashesV2: string[];
recentlyOffline: RecentlyOfflineMap;
recentlyOnline: Record<string, BlockNumber>;
}
export interface CalculateBalanceProps {
@ -24,15 +24,6 @@ export interface CalculateBalanceProps {
system_accountNonce?: BN;
}
export type RecentlyOffline = [AccountId, BlockNumber, BN][];
export type RecentlyOfflineMap = Record<string, OfflineStatus[]>;
export interface OfflineStatus {
blockNumber: BlockNumber;
count: BN;
}
export type AccountFilter = 'all' | 'controller' | 'session' | 'stash' | 'unbonded';
export type ValidatorFilter = 'all' | 'hasNominators' | 'noNominators' | 'hasWarnings' | 'noWarnings' | 'iNominated';

View File

@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/camelcase */
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId, BlockNumber } from '@polkadot/types/interfaces';
import { DerivedStakingOnlineStatus } from '@polkadot/api-derive/types';
export function updateOnlineStatus (recentlyOnline?: Record<string, BlockNumber>): (sessionIds: AccountId[] | null, onlineStatus: DerivedStakingOnlineStatus) => DerivedStakingOnlineStatus {
return (sessionIds: AccountId[] | null, onlineStatus: DerivedStakingOnlineStatus): DerivedStakingOnlineStatus => {
if (!recentlyOnline || !sessionIds) {
return onlineStatus;
}
const sessionId: AccountId | undefined = sessionIds.find((accountId): boolean => Object.keys(recentlyOnline).includes(accountId.toString()));
return {
...onlineStatus,
...(
sessionId
? {
online: {
blockNumber: recentlyOnline[sessionId.toString()],
isOnline: true
}
}
: {}
)
};
};
}

View File

@ -0,0 +1,175 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { DerivedStakingOnlineStatus } from '@polkadot/api-derive/types';
import { AccountId } from '@polkadot/types/interfaces';
import { I18nProps } from './types';
import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import { formatNumber } from '@polkadot/util';
import Icon from './Icon';
import Tooltip from './Tooltip';
import translate from './translate';
import { classes } from './util';
interface Props extends I18nProps {
accountId: AccountId | string;
tooltip?: boolean;
value: DerivedStakingOnlineStatus;
}
interface State {
isOpen: boolean;
version: 'online' | 'offline' | null;
}
class OnlineStatus extends React.PureComponent<Props, State> {
public static getDerivedStateFromProps ({ value = {} }: Props): Pick<State, never> {
const { online, offline } = value;
if (offline) {
return { version: 'offline' };
}
if (online && online.isOnline) {
return { version: 'online' };
}
return { version: null };
}
public state: State = {
isOpen: false,
version: null
};
public render (): React.ReactNode {
const { accountId, className, value, tooltip = false, t } = this.props;
const { isOpen, version } = this.state;
const key = accountId.toString();
const classNames: (string | false | undefined)[] = ['ui--OnlineStatus', isOpen && 'expand', tooltip && 'tooltip', className];
let text: string;
let contents: React.ReactNode;
let offline;
let count;
let blockNumbers;
let blockNumber;
switch (version) {
case 'offline':
offline = value.offline || [];
count = offline.reduce((total, { count }): BN => total.add(count), new BN(0));
blockNumbers = offline.map(({ blockNumber }): string => `#${formatNumber(blockNumber)}`);
classNames.push('offline');
contents = count.toString();
text = t('Reported offline {{count}} times, last at {{blockNumber}}', {
replace: {
count,
blockNumber: blockNumbers[blockNumbers.length - 1]
}
});
break;
case 'online':
blockNumber = value.online ? value.online.blockNumber : null;
classNames.push('online');
contents = (
<Icon name='check' />
);
text = blockNumber
? t('Reported online at #{{blockNumber}}', {
replace: {
blockNumber: formatNumber(blockNumber)
}
})
: t('Reported online in the current session');
break;
default:
return null;
}
return (
<div
className={classes(...classNames)}
{...(!tooltip ? { onClick: this.toggleOpen } : {})}
data-for={`online-status-${key}`}
data-tip={true}
data-tip-disable={!tooltip}
>
<div className='badge'>
{contents}
</div>
<div className='detail'>
{text}
</div>
<Tooltip
trigger={`online-status-${key}`}
text={text}
/>
</div>
);
}
private toggleOpen = (): void => {
this.setState(({ isOpen }): Pick<State, never> => ({
isOpen: !isOpen
}));
}
}
export default translate(styled(OnlineStatus as React.ComponentClass<Props, State>)`
border-radius: 16px;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2);
color: #eee;
cursor: help;
display: flex;
font-size: 12px;
height: 22px;
justify-content: center;
padding: 0 4px;
text-align: center;
transition: all ease .2s;
width: 22px;
&.offline {
background: red;
}
&.online {
background: green;
}
& > * {
line-height: 22px;
overflow: hidden;
transition: all ease 0.25;
}
.badge {
font-weight: bold;
width: auto;
}
.detail {
width: 0;
}
&.expand {
width: 300px;
.badge {
width: 0;
}
.detail {
width: auto;
}
}
`);

View File

@ -1,117 +0,0 @@
// Copyright 2017-2019 @polkadot/app-staking authors & contributors
// This software may be modified and distributed under the terms
// of the Apache-2.0 license. See the LICENSE file for details.
import { AccountId } from '@polkadot/types/interfaces';
import { I18nProps } from '@polkadot/react-components/types';
import { OfflineStatus } from '@polkadot/app-staking/types';
import BN from 'bn.js';
import React from 'react';
import styled from 'styled-components';
import { formatNumber } from '@polkadot/util';
import Tooltip from './Tooltip';
import translate from './translate';
import { classes } from './util';
interface Props extends I18nProps {
accountId: AccountId | string;
offline: OfflineStatus[];
tooltip?: boolean;
}
interface State {
isOpen: boolean;
}
class RecentlyOffline extends React.PureComponent<Props, State> {
public state: State = {
isOpen: false
};
public render (): React.ReactNode {
const { accountId, className, offline, tooltip = false, t } = this.props;
const { isOpen } = this.state;
if (!offline) {
return null;
}
const count = offline.reduce((total, { count }): BN => total.add(count), new BN(0));
const blockNumbers = offline.map(({ blockNumber }): string => `#${formatNumber(blockNumber)}`);
const text = t('Reported offline {{count}} times, last at {{blockNumber}}', {
replace: {
count,
blockNumber: blockNumbers[blockNumbers.length - 1]
}
});
return (
<div
className={classes('ui--RecentlyOffline', isOpen && 'expand', tooltip && 'tooltip', className)}
{...(!tooltip ? { onClick: this.toggleOpen } : {})}
data-for={`offline-${accountId}`}
data-tip={true}
data-tip-disable={!tooltip}
>
<div className='badge'>{count.toString()}</div>
<div className='detail'>{text}</div>
<Tooltip
trigger={`offline-${accountId}`}
text={text}
/>
</div>
);
}
private toggleOpen = (): void => {
this.setState(({ isOpen }): State => ({
isOpen: !isOpen
}));
}
}
export default translate(styled(RecentlyOffline)`
background: red;
border-radius: 16px;
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.2);
color: #eee;
cursor: help;
display: flex;
font-size: 12px;
height: 22px;
justify-content: center;
padding: 0;
text-align: center;
transition: all ease .2s;
width: 22px;
& > * {
line-height: 22px;
overflow: hidden;
transition: all ease 0.25;
}
.badge {
font-weight: bold;
width: auto;
}
.detail {
width: 0;
}
&.expand {
width: 300px;
.badge {
width: 0;
}
.detail {
width: auto;
}
}
`);

View File

@ -59,7 +59,7 @@ export { default as Output } from './Output';
export { default as Password } from './Password';
export { default as Progress } from './Progress';
export { default as ProposedAction } from './ProposedAction';
export { default as RecentlyOffline } from './RecentlyOffline';
export { default as OnlineStatus } from './OnlineStatus';
export { default as Row } from './Row';
export { default as Static } from './Static';
export { default as Status } from './Status';