Staking updates (#1815)

* Staking updates

* Bump API

* So currentElected first in next up

* Linting

* Remove unused state entries
This commit is contained in:
Jaco Greeff 2019-10-30 15:53:52 +01:00 committed by GitHub
parent cc4c193497
commit df095c2829
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 330 additions and 281 deletions

View File

@ -10,10 +10,10 @@
"packages/*"
],
"resolutions": {
"@polkadot/api": "^0.96.0-beta.14",
"@polkadot/api-contract": "^0.96.0-beta.14",
"@polkadot/api": "^0.96.0-beta.15",
"@polkadot/api-contract": "^0.96.0-beta.15",
"@polkadot/keyring": "^1.6.1",
"@polkadot/types": "^0.96.0-beta.14",
"@polkadot/types": "^0.96.0-beta.15",
"@polkadot/util": "^1.6.1",
"@polkadot/util-crypto": "^1.6.1",
"babel-core": "^7.0.0-bridge.0",

View File

@ -142,7 +142,7 @@ function Create ({ className, onClose, onStatusChange, seed: propsSeed, t, type:
setAddress(generateSeed(null, derivePath, newSeedType, pairType));
}
};
const _onChangeName = (name: string): void => setName({ isNameValid: name.trim().length !== 0, name });
const _onChangeName = (name: string): void => setName({ isNameValid: !!name.trim(), name });
const _toggleConfirmation = (): void => setIsConfirmationOpen(!isConfirmationOpen);
const _onCommit = (): void => {

View File

@ -23,11 +23,10 @@ interface Props extends I18nProps, ModalProps {
}
function QrModal ({ className, onClose, onStatusChange, t }: Props): React.ReactElement<Props> {
const [name, setName] = useState('');
const [{ isNameValid, name }, setName] = useState({ isNameValid: false, name: '' });
const [scanned, setScanned] = useState<Scanned | null>(null);
const isNameValid = !!name;
const _onNameChange = (name: string): void => setName(name.trim());
const _onNameChange = (name: string): void => setName({ isNameValid: !!name.trim(), name });
const _onSave = (): void => {
if (!scanned || !isNameValid) {
return;
@ -35,7 +34,7 @@ function QrModal ({ className, onClose, onStatusChange, t }: Props): React.React
const { address, genesisHash } = scanned;
keyring.addExternal(address, { genesisHash, name });
keyring.addExternal(address, { genesisHash, name: name.trim() });
InputAddress.setLastValue('account', address);
onStatusChange({

View File

@ -40,7 +40,7 @@ function Create ({ onClose, onStatusChange, t }: Props): React.ReactElement<Prop
isAddressExisting = true;
isAddressValid = true;
setName({ isNameValid: (newName || '').trim().length !== 0, name: newName });
setName({ isNameValid: !!(newName || '').trim(), name: newName });
}
}
} catch (error) {

View File

@ -11,7 +11,7 @@
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@polkadot/api-contract": "^0.96.0-beta.14",
"@polkadot/api-contract": "^0.96.0-beta.15",
"@polkadot/react-components": "^0.37.0-beta.24"
}
}

View File

@ -70,6 +70,14 @@ const DEFAULT_BALANCES = {
unlocking: false
};
const CONTROLLER_BALANCES = {
available: true,
bonded: false,
free: false,
redeemable: false,
unlocking: false
};
function toIdString (id?: AccountId | null): string | null {
return id
? id.toString()
@ -133,7 +141,7 @@ class Account extends React.PureComponent<Props, State> {
public render (): React.ReactNode {
const { className, isSubstrateV2, t } = this.props;
const { controllerId, hexSessionId, isBondExtraOpen, isInjectOpen, isStashValidating, isUnbondOpen, nominees, sessionIds, stashId } = this.state;
const { controllerId, hexSessionId, isBondExtraOpen, isInjectOpen, isStashValidating, isUnbondOpen, nominees, onlineStatus, sessionIds, stashId } = this.state;
if (!stashId) {
return null;
@ -146,7 +154,12 @@ class Account extends React.PureComponent<Props, State> {
return (
<AddressCard
buttons={this.renderButtons()}
iconInfo={this.renderOnlineStatus()}
iconInfo={onlineStatus && (
<OnlineStatus
isTooltip
value={onlineStatus}
/>
)}
label={t('stash')}
type='account'
value={stashId}
@ -176,7 +189,16 @@ class Account extends React.PureComponent<Props, State> {
{this.renderValidate()}
<div className={className}>
<div className='staking--Accounts'>
{this.renderControllerAccount()}
{controllerId && (
<div className='staking--Account-detail actions'>
<AddressRow
label={t('controller')}
value={controllerId}
withAddressOrName
withBalance={CONTROLLER_BALANCES}
/>
</div>
)}
{!isSubstrateV2 && !!sessionIds.length && (
<div className='staking--Account-detail actions'>
<AddressRow
@ -229,49 +251,6 @@ class Account extends React.PureComponent<Props, State> {
);
}
private renderOnlineStatus (): React.ReactNode {
const { onlineStatus, controllerId } = this.state;
if (!controllerId || !onlineStatus) {
return null;
}
return (
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
);
}
private renderControllerAccount (): React.ReactNode {
const { t } = this.props;
const { controllerId } = this.state;
if (!controllerId) {
return null;
}
return (
<div className='staking--Account-detail actions'>
<AddressRow
label={t('controller')}
value={controllerId}
withAddressOrName
withBalance={{
available: true,
bonded: false,
free: false,
redeemable: false,
unlocking: false
}}
/>
</div>
);
}
private renderNominate (): React.ReactNode {
const { stashOptions } = this.props;
const { controllerId, isNominateOpen, nominees, stashId } = this.state;

View File

@ -11,10 +11,10 @@ import BN from 'bn.js';
import React, { useContext, useEffect, useState } from 'react';
import styled from 'styled-components';
import { ApiContext, withCalls, withMulti } from '@polkadot/react-api';
import { AddressCard, AddressMini, OnlineStatus } from '@polkadot/react-components';
import { AddressCard, AddressMini, Badge, Icon, OnlineStatus } from '@polkadot/react-components';
import { classes } from '@polkadot/react-components/util';
import keyring from '@polkadot/ui-keyring';
import { formatBalance, formatNumber } from '@polkadot/util';
import { formatNumber } from '@polkadot/util';
import { updateOnlineStatus } from '../util';
import translate from '../translate';
@ -23,6 +23,7 @@ interface Props extends I18nProps {
address: string;
authorsMap: Record<string, string>;
className?: string;
currentElected: string[];
defaultName: string;
filter: ValidatorFilter;
lastAuthor?: string;
@ -36,9 +37,11 @@ interface StakingState {
balanceOpts: { bonded: boolean | BN[] };
controllerId?: string;
hasNominators: boolean;
isNominatorMe: boolean;
isSelected: boolean;
nominators: [AccountId, Balance][];
stashActive: string | null;
stashTotal: string | null;
// stashActive: string | null;
// stashTotal: string | null;
sessionId?: string;
stashId?: string;
}
@ -50,20 +53,21 @@ interface OnlineState {
const WITH_VALIDATOR_PREFS = { validatorPayment: true };
function Address ({ authorsMap, className, defaultName, filter, lastAuthor, points, recentlyOnline, stakingInfo, t, withNominations }: Props): React.ReactElement<Props> | null {
function Address ({ address, authorsMap, className, currentElected, defaultName, filter, lastAuthor, points, recentlyOnline, stakingInfo, t, withNominations }: Props): React.ReactElement<Props> | null {
const { isSubstrateV2 } = useContext(ApiContext);
const [extraInfo, setExtraInfo] = useState<[React.ReactNode, React.ReactNode][] | undefined>();
const [isNominatorMe, seIsNominatorMe] = useState(false);
const [{ hasOfflineWarnings, onlineStatus }, setOnlineStatus] = useState<OnlineState>({
hasOfflineWarnings: false,
onlineStatus: {}
});
const [{ balanceOpts, controllerId, hasNominators, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
const [{ balanceOpts, controllerId, hasNominators, isNominatorMe, isSelected, nominators, sessionId, stashId }, setStakingState] = useState<StakingState>({
balanceOpts: { bonded: true },
hasNominators: false,
nominators: [],
stashActive: null,
stashTotal: null
isNominatorMe: false,
isSelected: false,
nominators: []
// stashActive: null,
// stashTotal: null
});
useEffect((): void => {
@ -78,15 +82,13 @@ function Address ({ authorsMap, className, defaultName, filter, lastAuthor, poin
useEffect((): void => {
if (stakingInfo) {
const { controllerId, nextSessionId, stakers, stakingLedger, stashId } = stakingInfo;
const { controllerId, nextSessionId, stakers, stashId } = stakingInfo;
const nominators = stakers
? stakers.others.map(({ who, value }): [AccountId, Balance] => [who, value.unwrap()])
: [];
const myAccounts = keyring.getAccounts().map(({ address }): string => address);
const _stashId = stashId && stashId.toString();
seIsNominatorMe(nominators.some(([who]): boolean =>
myAccounts.includes(who.toString())
));
setStakingState({
balanceOpts: {
bonded: stakers && !stakers.own.isEmpty
@ -95,18 +97,22 @@ function Address ({ authorsMap, className, defaultName, filter, lastAuthor, poin
},
controllerId: controllerId && controllerId.toString(),
hasNominators: nominators.length !== 0,
isNominatorMe: nominators.some(([who]): boolean =>
myAccounts.includes(who.toString())
),
isSelected: !!(_stashId && currentElected && currentElected.includes(_stashId)),
nominators,
sessionId: nextSessionId && nextSessionId.toString(),
stashActive: stakingLedger
? formatBalance(stakingLedger.active)
: null,
stashId: stashId && stashId.toString(),
stashTotal: stakingLedger
? formatBalance(stakingLedger.total)
: null
// stashActive: stakingLedger
// ? formatBalance(stakingLedger.active)
// : null,
// stashTotal: stakingLedger
// ? formatBalance(stakingLedger.total)
// : null,
stashId: _stashId
});
}
}, [stakingInfo]);
}, [currentElected, stakingInfo]);
useEffect((): void => {
if (stakingInfo) {
@ -120,14 +126,27 @@ function Address ({ authorsMap, className, defaultName, filter, lastAuthor, poin
}
}, [recentlyOnline, stakingInfo]);
if (!stashId || (filter === 'hasNominators' && !hasNominators) ||
if ((filter === 'hasNominators' && !hasNominators) ||
(filter === 'noNominators' && hasNominators) ||
(filter === 'hasWarnings' && !hasOfflineWarnings) ||
(filter === 'noWarnings' && hasOfflineWarnings) ||
(filter === 'iNominated' && !isNominatorMe)) {
(filter === 'iNominated' && !isNominatorMe) ||
(filter === 'nextSet' && !isSelected)) {
return null;
}
if (!stashId) {
return (
<AddressCard
className={className}
defaultName={defaultName}
isDisabled
value={address}
withBalance={false}
/>
);
}
const lastBlockNumber = authorsMap[stashId];
const isAuthor = lastAuthor === stashId;
// isDisabled={!!points && points.isEmpty}
@ -156,14 +175,24 @@ function Address ({ authorsMap, className, defaultName, filter, lastAuthor, poin
className={className}
defaultName={defaultName}
extraInfo={extraInfo}
iconInfo={controllerId && onlineStatus && (
<OnlineStatus
accountId={controllerId}
value={onlineStatus}
tooltip
/>
)}
key={stashId}
iconInfo={
<>
{controllerId && onlineStatus && (
<OnlineStatus
value={onlineStatus}
isTooltip
/>
)}
{isSelected && (
<Badge
hover={t('Selected for the next session')}
info={<Icon name='check' />}
isTooltip
type='next'
/>
)}
</>
}
value={stashId}
withBalance={balanceOpts}
withValidatorPrefs={WITH_VALIDATOR_PREFS}

View File

@ -15,6 +15,7 @@ import Address from './Address';
interface Props extends I18nProps {
authorsMap: Record<string, string>;
currentElected: string[];
currentValidators: string[];
eraPoints?: EraPoints;
lastAuthor?: string;
@ -22,11 +23,12 @@ interface Props extends I18nProps {
recentlyOnline?: DerivedHeartbeats;
}
function renderColumn (addresses: string[], defaultName: string, withExpanded: boolean, filter: string, { authorsMap, eraPoints, lastAuthor, recentlyOnline }: Props): React.ReactNode {
return addresses.map((address, index): React.ReactNode => (
function renderColumn (addresses: string[], defaultName: string, withExpanded: boolean, filter: string, without: string[], { authorsMap, currentElected, eraPoints, lastAuthor, recentlyOnline }: Props): React.ReactNode {
return addresses.filter((address): boolean => !without.includes(address)).map((address, index): React.ReactNode => (
<Address
address={address}
authorsMap={authorsMap}
currentElected={currentElected}
defaultName={defaultName}
filter={filter}
lastAuthor={lastAuthor}
@ -44,7 +46,7 @@ function renderColumn (addresses: string[], defaultName: string, withExpanded: b
function CurrentList (props: Props): React.ReactElement<Props> {
const [filter, setFilter] = useState<ValidatorFilter>('all');
const { currentValidators, next, t } = props;
const { currentElected, currentValidators, next, t } = props;
return (
<div>
@ -57,7 +59,8 @@ function CurrentList (props: Props): React.ReactElement<Props> {
{ text: t('Show only with nominators'), value: 'hasNominators' },
{ text: t('Show only without nominators'), value: 'noNominators' },
{ text: t('Show only with warnings'), value: 'hasWarnings' },
{ text: t('Show only without warnings'), value: 'noWarnings' }
{ text: t('Show only without warnings'), value: 'noWarnings' },
{ text: t('Show only elected for next session'), value: 'nextSet' }
]}
value={filter}
withLabel={false}
@ -68,13 +71,14 @@ function CurrentList (props: Props): React.ReactElement<Props> {
emptyText={t('No addresses found')}
headerText={t('validators')}
>
{renderColumn(currentValidators, t('validator'), true, filter, props)}
{renderColumn(currentValidators, t('validator'), true, filter, [], props)}
</Column>
<Column
emptyText={t('No addresses found')}
headerText={t('next up')}
>
{renderColumn(next, t('intention'), false, filter, props)}
{renderColumn(currentElected, t('intention'), false, filter, currentValidators, props)}
{renderColumn(next, t('intention'), false, filter, currentElected || [], props)}
</Column>
</Columar>
</div>

View File

@ -18,14 +18,13 @@ interface Props extends BareProps, ComponentProps {
eraPoints?: EraPoints;
}
export default function Overview (props: Props): React.ReactElement<Props> {
export default function Overview ({ allControllers, allStashes, currentElected, currentValidators, eraPoints, recentlyOnline }: Props): React.ReactElement<Props> {
const { isSubstrateV2 } = useContext(ApiContext);
const { byAuthor, lastBlockAuthor, lastBlockNumber } = useContext(BlockAuthorsContext);
const [nextSorted, setNextSorted] = useState<string[]>([]);
const { allControllers, allStashes, currentValidators, eraPoints, recentlyOnline } = props;
const [next, setNext] = useState<string[]>([]);
useEffect((): void => {
setNextSorted(
setNext(
isSubstrateV2
// this is a V2 node currentValidators is a list of stashes
? allStashes.filter((address): boolean => !currentValidators.includes(address))
@ -38,17 +37,19 @@ export default function Overview (props: Props): React.ReactElement<Props> {
<div className='staking--Overview'>
<Summary
allControllers={allControllers}
currentElected={currentElected}
currentValidators={currentValidators}
lastBlock={lastBlockNumber}
lastAuthor={lastBlockAuthor}
next={nextSorted}
next={next}
/>
<CurrentList
authorsMap={byAuthor}
currentElected={currentElected}
currentValidators={currentValidators}
eraPoints={eraPoints}
lastAuthor={lastBlockAuthor}
next={nextSorted}
next={next}
recentlyOnline={recentlyOnline}
/>
</div>

View File

@ -27,6 +27,7 @@ interface Props extends AppProps, ApiProps, I18nProps {
allAccounts?: SubjectInfo;
allStashesAndControllers?: [string[], string[]];
bestNumber?: BlockNumber;
currentElected?: string[];
currentValidators?: string[];
eraPoints?: EraPoints;
recentlyOnline?: DerivedHeartbeats;
@ -35,7 +36,7 @@ interface Props extends AppProps, ApiProps, I18nProps {
const EMPY_ACCOUNTS: string[] = [];
const EMPTY_ALL: [string[], string[]] = [EMPY_ACCOUNTS, EMPY_ACCOUNTS];
function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, className, currentValidators = EMPY_ACCOUNTS, basePath, eraPoints, recentlyOnline, t }: Props): React.ReactElement<Props> {
function App ({ allAccounts, allStashesAndControllers: [allStashes, allControllers] = EMPTY_ALL, className, currentElected, currentValidators, basePath, eraPoints, recentlyOnline, t }: Props): React.ReactElement<Props> {
const _renderComponent = (Component: React.ComponentType<ComponentProps>): () => React.ReactNode => {
// eslint-disable-next-line react/display-name
return (): React.ReactNode => {
@ -48,7 +49,8 @@ function App ({ allAccounts, allStashesAndControllers: [allStashes, allControlle
allAccounts={allAccounts}
allControllers={allControllers}
allStashes={allStashes}
currentValidators={currentValidators}
currentElected={currentElected || EMPY_ACCOUNTS}
currentValidators={currentValidators || EMPY_ACCOUNTS}
eraPoints={eraPoints}
recentlyOnline={recentlyOnline}
/>
@ -111,6 +113,11 @@ export default withMulti(
transform: (validators: AccountId[]): string[] =>
validators.map((accountId): string => accountId.toString())
}],
['query.staking.currentElected', {
propName: 'currentElected',
transform: (elected: AccountId[]): string[] =>
elected.map((accountId): string => accountId.toString())
}],
['query.staking.currentEra', { propName: 'currentEra' }],
['query.staking.currentEraPointsEarned', { paramName: 'currentEra', propName: 'eraPoints' }]
),

View File

@ -14,6 +14,7 @@ export interface ComponentProps {
allAccounts?: SubjectInfo;
allControllers: string[];
allStashes: string[];
currentElected: string[];
currentValidators: string[];
eraPoints?: EraPoints;
recentlyOnline?: DerivedHeartbeats;
@ -27,4 +28,4 @@ export interface CalculateBalanceProps {
export type AccountFilter = 'all' | 'controller' | 'session' | 'stash' | 'unbonded';
export type ValidatorFilter = 'all' | 'hasNominators' | 'noNominators' | 'hasWarnings' | 'noWarnings' | 'iNominated';
export type ValidatorFilter = 'all' | 'hasNominators' | 'noNominators' | 'hasWarnings' | 'noWarnings' | 'iNominated' | 'nextSet';

View File

@ -11,6 +11,7 @@ import { BareProps as Props } from '@polkadot/react-components/types';
import React, { useState } from 'react';
import store from 'store';
import styled from 'styled-components';
import { withCalls } from '@polkadot/react-api';
import GlobalStyle from '@polkadot/react-components/styles';
import Signer from '@polkadot/react-signer';
@ -27,6 +28,24 @@ interface SidebarState {
transition: SideBarTransition;
}
function Placeholder (): React.ReactElement {
return (
<div className='api-warm' />
);
}
const WarmUp = withCalls<{}>(
'derive.accounts.indexes',
'derive.balances.fees',
'query.session.validators'
// This are very ineffective queries that
// (a) adds load to the RPC node when activated globally
// (b) is used in additional information (next-up)
// 'derive.staking.all'
// 'derive.staking.controllers'
// 'query.staking.nominators'
)(Placeholder);
function Apps ({ className }: Props): React.ReactElement<Props> {
const [sidebar, setSidebar] = useState<SidebarState>({
isCollapsed: false,
@ -77,6 +96,7 @@ function Apps ({ className }: Props): React.ReactElement<Props> {
<ConnectingOverlay />
<AccountsOverlay />
</div>
<WarmUp />
</>
);
}

View File

@ -3,20 +3,19 @@
// of the Apache-2.0 license. See the LICENSE file for details.
import { I18nProps } from '@polkadot/react-components/types';
import { ApiProps } from '@polkadot/react-api/types';
import React, { useContext } from 'react';
import { withRouter, RouteComponentProps } from 'react-router';
import styled from 'styled-components';
import routing from '@polkadot/apps-routing';
import { withCalls, withMulti } from '@polkadot/react-api';
import { ApiContext } from '@polkadot/react-api';
import { StatusContext } from '@polkadot/react-components';
import Status from './Status';
import translate from '../translate';
import NotFound from './NotFound';
interface Props extends I18nProps, ApiProps, RouteComponentProps {}
interface Props extends I18nProps, RouteComponentProps {}
const unknown = {
display: {
@ -26,7 +25,8 @@ const unknown = {
name: ''
};
function Content ({ isApiConnected, isApiReady, className, location, t }: Props): React.ReactElement<Props> {
function Content ({ className, location, t }: Props): React.ReactElement<Props> {
const { isApiConnected, isApiReady } = useContext(ApiContext);
const { queueAction, stqueue, txqueue } = useContext(StatusContext);
const app = location.pathname.slice(1) || '';
const { Component, display: { needsApi }, name } = routing.routes.find((route): boolean =>
@ -57,7 +57,7 @@ function Content ({ isApiConnected, isApiReady, className, location, t }: Props)
}
// React-router needs to be first, otherwise we have blocked updates
export default withMulti(
export default translate(
withRouter(
styled(Content)`
background: #fafafa;
@ -79,18 +79,5 @@ export default withMulti(
padding: 1rem 0;
}
`
),
translate,
// These API queries are used in a number of places, warm them up
// to avoid constant un-/re-subscribe on these
withCalls<Props>(
'derive.accounts.indexes',
'derive.balances.fees',
'query.session.validators'
// This are very ineffective queries that
// (a) adds load to the RPC node when activated globally
// (b) is used in additional information (next-up)
// 'derive.staking.controllers'
// 'query.staking.nominators'
)
);

View File

@ -31,7 +31,7 @@
"homepage": "https://github.com/polkadot-js/ui/tree/master/packages/ui-reactive#readme",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@polkadot/api": "^0.96.0-beta.14",
"@polkadot/api": "^0.96.0-beta.15",
"@polkadot/extension-dapp": "^0.14.0-beta.1",
"edgeware-node-types": "^1.0.10",
"rxjs-compat": "^6.5.3"

View File

@ -0,0 +1,104 @@
// 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 React, { useState } from 'react';
import styled from 'styled-components';
import Tooltip from './Tooltip';
interface Props {
className?: string;
hover: React.ReactNode;
info: React.ReactNode;
isTooltip?: boolean;
type: 'online' | 'offline' | 'next';
}
let badgeId = 0;
function Badge ({ className, hover, info, isTooltip, type }: Props): React.ReactElement<Props> | null {
const [key] = useState(`${Date.now()}-${badgeId++}`);
const [isOpen, setIsOpen] = useState(false);
const _toggleOpen = (): void => setIsOpen(!isOpen);
return (
<div
className={`ui--Badge ${isOpen && 'expand'} ${isTooltip && 'tooltip'} ${type} ${className}`}
onClick={
isTooltip
? _toggleOpen
: undefined
}
data-for={`badge-status-${key}`}
data-tip={true}
data-tip-disable={!isTooltip}
>
<div className='badge'>
{info}
</div>
<div className='detail'>
{hover}
</div>
<Tooltip
trigger={`badge-status-${key}`}
text={hover}
/>
</div>
);
}
export default styled(Badge)`
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;
margin-bottom: 0.25rem;
padding: 0 4px;
text-align: center;
width: 22px;
&.next {
background: steelblue;
}
&.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

@ -3,156 +3,73 @@
// 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, { useEffect, useState } from 'react';
import styled from 'styled-components';
import { formatNumber } from '@polkadot/util';
import Badge from './Badge';
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;
isTooltip?: boolean;
value: DerivedStakingOnlineStatus;
}
function OnlineStatus ({ accountId, className, value, tooltip = false, t }: Props): React.ReactElement<Props> | null {
const [isOpen, setIsOpen] = useState(false);
const [version, setVersion] = useState<string | null>(null);
const _toggleOpen = (): void => setIsOpen(!isOpen);
function OnlineStatus ({ className, value, isTooltip = false, t }: Props): React.ReactElement<Props> | null {
const [{ hover, info, type }, setType] = useState<{ hover: React.ReactNode; info: React.ReactNode; type: 'online' | 'offline' | null }>({ hover: '', info: '', type: null });
useEffect((): void => {
const { online, offline } = value;
let hover: React.ReactNode = '';
let info: React.ReactNode = '';
let type: 'online' | 'offline' | null = null;
if (offline) {
setVersion('offline');
} else if (online && online.isOnline) {
setVersion('online');
} else {
setVersion(null);
}
}, [value]);
const count = offline.reduce((total, { count }): BN => total.add(count), new BN(0));
const blockNumbers = offline.map(({ blockNumber }): string => `#${formatNumber(blockNumber)}`);
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}}', {
info = count.toString();
hover = t('Reported offline {{count}} times, last at {{blockNumber}}', {
replace: {
count,
blockNumber: blockNumbers[blockNumbers.length - 1]
}
});
break;
type = 'offline';
} else if (online && online.isOnline) {
const blockNumber = value.online ? value.online.blockNumber : null;
case 'online':
classNames.push('online');
blockNumber = value.online ? value.online.blockNumber : null;
contents = <Icon name='check' />;
text = blockNumber
info = <Icon name='check' />;
hover = blockNumber
? t('Reported online at #{{blockNumber}}', {
replace: {
blockNumber: formatNumber(blockNumber)
}
})
: t('Reported online in the current session');
break;
type = 'online';
}
default:
return null;
setType({ hover, info, type });
}, [value]);
if (!type) {
return null;
}
return (
<div
className={classes(...classNames)}
{...(!tooltip ? { onClick: _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>
<Badge
className={`ui--OnlineStatus ${className}`}
hover={hover}
info={info}
isTooltip={isTooltip}
type={type}
/>
);
}
export default translate(
styled(OnlineStatus)`
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;
}
}
`
);
export default translate(OnlineStatus);

View File

@ -8,6 +8,7 @@ export { default as AddressInfo } from './AddressInfo';
export { default as AddressMini } from './AddressMini';
export { default as AddressRow } from './AddressRow';
export { default as Available } from './Available';
export { default as Badge } from './Badge';
export { default as Balance } from './Balance';
export { default as Bonded } from './Bonded';
export { default as Bubble } from './Bubble';

View File

@ -2016,45 +2016,45 @@
once "^1.4.0"
universal-user-agent "^4.0.0"
"@polkadot/api-contract@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.96.0-beta.14.tgz#26587385589cd5a351cfc4a5694ddde8a9033a26"
integrity sha512-78c1YR2YTqAden9APlu40noELrSR/dOHhGRFmk7zkoyeAaHgPH65An2lQBcEU4fzBUhjorWjb1Pd7IBbW/GK7Q==
"@polkadot/api-contract@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/api-contract/-/api-contract-0.96.0-beta.15.tgz#25c910de8535a52daf7a08dc8c19cb4c74985a21"
integrity sha512-QXQANhO2BrtORbQgIHEhEhM8JIMwo30Axhug5xKa9SQQ/FnHWt0dslU9qutGfqJngGFE0HSyn1WSrPoBXgZSzA==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/types" "^0.96.0-beta.14"
"@polkadot/types" "^0.96.0-beta.15"
"@polkadot/api-derive@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.96.0-beta.14.tgz#ae7faf61965f5934a683ecae23f46fb6243dc793"
integrity sha512-w92geU6JQDCoKJ/pPKIUitYNNcdgsDj2NLH5I1JBtMBhu6JGb5YZKDwyKH1Q4fmBk7gLD1ibyU/i1NiY945xEg==
"@polkadot/api-derive@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/api-derive/-/api-derive-0.96.0-beta.15.tgz#a0bb19445eccee7f480d87724e5b31d398c59bab"
integrity sha512-0Dg9QJUapk4GAJIHIYCw9hgpeZNBYiI797rtv1njWu6Pcpqv2TwrrHFiSZT6uC7Xz2r7k+prkjNhf42Q74T1Jg==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/api" "^0.96.0-beta.14"
"@polkadot/types" "^0.96.0-beta.14"
"@polkadot/api" "^0.96.0-beta.15"
"@polkadot/types" "^0.96.0-beta.15"
"@polkadot/api-metadata@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/api-metadata/-/api-metadata-0.96.0-beta.14.tgz#5eb4054b943e8af8ee2d39379b7695ac8301a107"
integrity sha512-g1tqyan014T9qZdLSi6lNeOozQ0gfJFS6SPx3TDqTbFm4ITVoviq+53n/+7VDf4HrRmC28Yp5Aku3pvUfPjSWA==
"@polkadot/api-metadata@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/api-metadata/-/api-metadata-0.96.0-beta.15.tgz#4d94e41e681f4bd68d4dab22bbbedbd89b23c29e"
integrity sha512-SFjoMRtQzxE35y39Rw0+ujvVif6gC015rKcjPsZbRTbbrleJsNNvwSm0OuqEUVRmnQShkafConM6AFv1Cbyigw==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/types" "^0.96.0-beta.14"
"@polkadot/types" "^0.96.0-beta.15"
"@polkadot/util" "^1.6.1"
"@polkadot/util-crypto" "^1.6.1"
"@polkadot/api@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.96.0-beta.14.tgz#e9979c874549a876b388dd476eccd1a4870f11db"
integrity sha512-WmQMwhwyF6ymhOwpyiymOAlLByb1rkew+OkCaZqF3N2sSy9E15XFJR5MjnrqN/Yz64GRqrCrFGKAgVl14zaxgA==
"@polkadot/api@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/api/-/api-0.96.0-beta.15.tgz#e12ac1ff2dd5136bbbeb8e7c9695c16ca965b31d"
integrity sha512-9D5je8yQxQl/tbNU5vdF4FLCFH5in0XDaxdmhOd2E8/HN/JyEhISL+t/S0x787VcoOcch6GI4P4Qpb0a3BXEDg==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/api-derive" "^0.96.0-beta.14"
"@polkadot/api-metadata" "^0.96.0-beta.14"
"@polkadot/api-derive" "^0.96.0-beta.15"
"@polkadot/api-metadata" "^0.96.0-beta.15"
"@polkadot/keyring" "^1.6.1"
"@polkadot/rpc-core" "^0.96.0-beta.14"
"@polkadot/rpc-provider" "^0.96.0-beta.14"
"@polkadot/types" "^0.96.0-beta.14"
"@polkadot/rpc-core" "^0.96.0-beta.15"
"@polkadot/rpc-provider" "^0.96.0-beta.15"
"@polkadot/types" "^0.96.0-beta.15"
"@polkadot/util-crypto" "^1.6.1"
"@polkadot/dev-react@^0.32.0-beta.12":
@ -2156,10 +2156,10 @@
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/jsonrpc@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.96.0-beta.14.tgz#c68db9f3cffec7dc0dc103c2f13ef91a35293dc1"
integrity sha512-pg9S+Pjl/IQAYanvz6hMEDUzujnqbcPzzhQIKIfY6dl1y1iQBWSqGqV/tKhWjK41VEqZYdmhG6R+kIL9yQUfLA==
"@polkadot/jsonrpc@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/jsonrpc/-/jsonrpc-0.96.0-beta.15.tgz#d885b1b17386a8e87dbc5951e8be88ca775a962f"
integrity sha512-db08i+S45Gy8c8apM2DtXZVN4bV3pWOmnoKxFxsW7fFatQ0lAeAe2xMdyMSdoU7UiU7OzMO/BIdwLkJe4o3Jnw==
dependencies:
"@babel/runtime" "^7.6.3"
@ -2197,25 +2197,25 @@
qrcode-generator "^1.4.4"
react-qr-reader "^2.2.1"
"@polkadot/rpc-core@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.96.0-beta.14.tgz#285dc0109f949df1f2a273b15818faf9dbf2cdec"
integrity sha512-K6EYCNMFdAtkAicMeN8/HMDIXWE9eELCzKX5LrdKVcJhnI8lAY2WL1fiWbkfYLmUyV1+30JYC2B6nFmnakO9Nw==
"@polkadot/rpc-core@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-core/-/rpc-core-0.96.0-beta.15.tgz#a1ca1fb0d37c68a0107313d9bfd67bf1c2e3ac01"
integrity sha512-aLRlfe1tOyMhJ5A2BCX82qHAJs4Nl/x8AIlvLs5Wme/Jl5eo+N3Y+R3p+uF99JIosrzJmdcdjBG8qGY3Cj4PvQ==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/jsonrpc" "^0.96.0-beta.14"
"@polkadot/rpc-provider" "^0.96.0-beta.14"
"@polkadot/types" "^0.96.0-beta.14"
"@polkadot/jsonrpc" "^0.96.0-beta.15"
"@polkadot/rpc-provider" "^0.96.0-beta.15"
"@polkadot/types" "^0.96.0-beta.15"
"@polkadot/util" "^1.6.1"
rxjs "^6.5.3"
"@polkadot/rpc-provider@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.96.0-beta.14.tgz#7565cf4fe2138bbe4e984fa0b88ed2925e497b7f"
integrity sha512-9t9xmj3Zy+0lOd4iZirkUsUB/5bDlHFTTAcuwgB2MtqLi9m63JJaXjcWWa1HaYmg9u+gycbwMOg7jFSlf6RjjQ==
"@polkadot/rpc-provider@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/rpc-provider/-/rpc-provider-0.96.0-beta.15.tgz#d540ed73dd2299eccd4fb53793fbd1addbe33890"
integrity sha512-A2QGImrRMRmZXZLgaNjqayIrerx8fi1+74KO4yx3wcMx7V7kEvZ/B7X00vEO4eg9ysRw/INcfZD67eWu0yjRbg==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/api-metadata" "^0.96.0-beta.14"
"@polkadot/api-metadata" "^0.96.0-beta.15"
"@polkadot/util" "^1.6.1"
"@polkadot/util-crypto" "^1.6.1"
"@types/nock" "^11.1.0"
@ -2230,10 +2230,10 @@
dependencies:
"@types/chrome" "^0.0.91"
"@polkadot/types@^0.96.0-beta.14":
version "0.96.0-beta.14"
resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.96.0-beta.14.tgz#88a3899d6f3a96eab4d7abc66cc903023ad0c88b"
integrity sha512-7icPtEkZXoukKqN2hTBEyZH9shwV7At2xVqLjEx8j4r6IP+gDL2UbvcQ2WevCsIeWfsnaN13pEMSbClEuWOuVQ==
"@polkadot/types@^0.96.0-beta.15":
version "0.96.0-beta.15"
resolved "https://registry.yarnpkg.com/@polkadot/types/-/types-0.96.0-beta.15.tgz#8b9acea0acb10e708f5ac8f5fa47450fb6e6ffb5"
integrity sha512-ufWNAkz7fy4HlsHzJuBkLbIHMXjZaskaGwoBAT0HOBJ7wI3NSz78ScF69beVY5KwG+9Qc0dSYvYWFDOmDJTJCQ==
dependencies:
"@babel/runtime" "^7.6.3"
"@polkadot/util" "^1.6.1"