Browse Source

Staking updates (#1815)

* Staking updates

* Bump API

* So currentElected first in next up

* Linting

* Remove unused state entries
Jaco Greeff 5 years ago
parent
commit
df095c2829

+ 3 - 3
package.json

@@ -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",

+ 1 - 1
packages/app-accounts/src/modals/Create.tsx

@@ -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 => {

+ 3 - 4
packages/app-accounts/src/modals/Qr.tsx

@@ -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({

+ 1 - 1
packages/app-address-book/src/modals/Create.tsx

@@ -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) {

+ 1 - 1
packages/app-contracts/package.json

@@ -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"
   }
 }

+ 25 - 46
packages/app-staking/src/Actions/Account/index.tsx

@@ -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;

+ 61 - 32
packages/app-staking/src/Overview/Address.tsx

@@ -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}

+ 10 - 6
packages/app-staking/src/Overview/CurrentList.tsx

@@ -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>

+ 7 - 6
packages/app-staking/src/Overview/index.tsx

@@ -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>

+ 9 - 2
packages/app-staking/src/index.tsx

@@ -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' }]
   ),

+ 2 - 1
packages/app-staking/src/types.ts

@@ -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';

+ 20 - 0
packages/apps/src/Apps.tsx

@@ -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 />
     </>
   );
 }

+ 5 - 18
packages/apps/src/Content/index.tsx

@@ -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'
   )
 );

+ 1 - 1
packages/react-api/package.json

@@ -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"

+ 104 - 0
packages/react-components/src/Badge.tsx

@@ -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;
+    }
+  }
+`;

+ 31 - 114
packages/react-components/src/OnlineStatus.tsx

@@ -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 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)}`);
+      const count = offline.reduce((total, { count }): BN => total.add(count), new BN(0));
+      const 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';
+    }
+
+    setType({ hover, info, type });
+  }, [value]);
 
-    default:
-      return null;
+  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);

+ 1 - 0
packages/react-components/src/index.tsx

@@ -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';

+ 45 - 45
yarn.lock

@@ -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"