Combine status event types (#2478)

* Combine status event types

* Adjust some tables

* Adjust targets
This commit is contained in:
Jaco Greeff 2020-03-29 10:04:21 +02:00 committed by GitHub
parent 4f9794ed12
commit 3dfaf17ca8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 251 additions and 189 deletions

View File

@ -51,7 +51,7 @@ function Motion ({ className, isMember, members, motion: { hash, proposal, votes
</>
)}
</td>
<td className='number'>
<td className='address'>
{ayes.map((address, index): React.ReactNode => (
<AddressMini
key={`${index}:${address}`}
@ -60,7 +60,7 @@ function Motion ({ className, isMember, members, motion: { hash, proposal, votes
/>
))}
</td>
<td className='number'>
<td className='address'>
{nays.map((address, index): React.ReactNode => (
<AddressMini
key={`${index}:${address}`}
@ -78,10 +78,13 @@ function Motion ({ className, isMember, members, motion: { hash, proposal, votes
prime={prime}
proposal={proposal}
/>
</td>
<td className='mini'>
<LinkExternal
data={index}
hash={hash.toString()}
type='council'
withShort
/>
</td>
</tr>

View File

@ -51,9 +51,9 @@ function Proposals ({ className, motions, prime }: Props): React.ReactElement<Pr
<th colSpan={2}>&nbsp;</th>
<th>{t('threshold')}</th>
<th>{t('voting end')}</th>
<th>{t('aye')}</th>
<th>{t('nay')}</th>
<th>&nbsp;</th>
<th className='address'>{t('aye')}</th>
<th className='address'>{t('nay')}</th>
<th colSpan={2}>&nbsp;</th>
</Table.Head>
<Table.Body>
{motions?.map((motion: DeriveCollectiveProposal): React.ReactNode => (

View File

@ -32,7 +32,7 @@ function Validator ({ info: { accountId, bondOther, bondOwn, bondTotal, commissi
toggleFavorite={toggleFavorite}
/>
<td className='number'>{formatNumber(rankOverall)}</td>
<td className='address'>
<td className='address all'>
<AddressSmall value={accountId} />
</td>
<td className='number'>

View File

@ -41,7 +41,7 @@ function Proposal ({ className, imageHash, prime }: Props): React.ReactElement<P
<td className='number'>
{formatNumber(ayes.length)}/{formatNumber(threshold)}
</td>
<td className='number'>
<td className='address'>
{ayes.map((address, index): React.ReactNode => (
<AddressMini
key={`${index}:${address}`}
@ -50,7 +50,7 @@ function Proposal ({ className, imageHash, prime }: Props): React.ReactElement<P
/>
))}
</td>
<td className='number'>
<td className='address'>
{nays.map((address, index): React.ReactNode => (
<AddressMini
key={`${index}:${address}`}

View File

@ -33,10 +33,14 @@ function Propose ({ isMember, members }: Props): React.ReactElement<Props> {
[members]
);
const _onChangeExtrinsic = (method?: SubmittableExtrinsic<'promise'>): void =>
setProposal(() => method || null);
const _onChangeThreshold = (threshold?: BN): void =>
setThreshold([threshold || null, _hasThreshold(threshold)]);
const _onChangeExtrinsic = useCallback(
(method?: SubmittableExtrinsic<'promise'>): void => setProposal(() => method || null),
[]
);
const _onChangeThreshold = useCallback(
(threshold?: BN): void => setThreshold([threshold || null, _hasThreshold(threshold)]),
[_hasThreshold]
);
return (
<>

View File

@ -29,13 +29,13 @@ function Proposals ({ className, isMember, members, prime, proposals }: Props):
<Table>
<Table.Head>
<th colSpan={2}>&nbsp;</th>
<td>{t('threshold')}</td>
<td>{t('aye')}</td>
<td>{t('nay')}</td>
<th>{t('threshold')}</th>
<th className='address'>{t('aye')}</th>
<th className='address'>{t('nay')}</th>
<th>&nbsp;</th>
</Table.Head>
<Table.Body>
{proposals?.map((hash: Hash): React.ReactNode => (
{proposals.map((hash: Hash): React.ReactNode => (
<Proposal
imageHash={hash.toHex()}
key={hash.toHex()}

View File

@ -26,18 +26,18 @@ function ProposalDisplay ({ className, isMember, proposal: { council, id, propos
<td className='number'>
<h1>{formatNumber(id)}</h1>
</td>
<td className='address'>
<td className='address all'>
<AddressSmall value={proposal.proposer} />
</td>
<td className='number'>
<FormatBalance value={proposal.bond} />
</td>
<td className='address'>
<AddressMini value={proposal.beneficiary} />
</td>
<td>
<td className='number'>
<FormatBalance value={proposal.value} />
</td>
<td className='number'>
<FormatBalance value={proposal.bond} />
</td>
<td className='button'>
{withSend && (
<>
@ -52,9 +52,12 @@ function ProposalDisplay ({ className, isMember, proposal: { council, id, propos
/>
</>
)}
</td>
<td className='mini'>
<LinkExternal
data={id}
type='treasury'
withShort
/>
</td>
</tr>

View File

@ -48,10 +48,10 @@ function ProposalsBase ({ className, isApprovals, proposals }: Props): React.Rea
<Table>
<Table.Head>
<th colSpan={2}>&nbsp;</th>
<td>{t('bond')}</td>
<td className='address'>{t('beneficiary')}</td>
<td>{t('value')}</td>
<th>&nbsp;</th>
<th className='address'>{t('beneficiary')}</th>
<th>{t('payment')}</th>
<th>{t('bond')}</th>
<th colSpan={2}>&nbsp;</th>
</Table.Head>
<Table.Body>
{proposals.map((proposal): React.ReactNode => (

View File

@ -35,6 +35,10 @@ const Memo = React.memo(styled(ButtonGroup)`
margin-top: 0.75rem;
}
> .ui.buttons {
vertical-align: middle;
}
&.centered {
margin-bottom: 0.5rem;
text-align: center;

View File

@ -8,7 +8,7 @@ import { ITuple, SignerPayloadJSON } from '@polkadot/types/types';
import { BareProps } from '../types';
import { ActionStatus, PartialQueueTxExtrinsic, PartialQueueTxRpc, QueueStatus, QueueTx, QueueTxExtrinsic, QueueTxRpc, QueueTxStatus, SignerCallback } from './types';
import React, { useRef, useState } from 'react';
import React, { useCallback, useRef, useState } from 'react';
import { SubmittableResult } from '@polkadot/api';
import { registry } from '@polkadot/react-api';
import jsonrpc from '@polkadot/types/interfaces/jsonrpc';
@ -21,153 +21,195 @@ export interface Props extends BareProps {
children: React.ReactNode;
}
interface StatusCount {
count: number;
status: ActionStatus;
}
let nextId = 0;
const REMOVE_TIMEOUT = 7500;
const SUBMIT_RPC = jsonrpc.author.submitAndWatchExtrinsic;
function Queue ({ children }: Props): React.ReactElement<Props> {
const [_stqueue, _setStQueue] = useState<QueueStatus[]>([]);
const [_txqueue, _setTxQueue] = useState<QueueTx[]>([]);
const stRef = useRef(_stqueue);
const txRef = useRef(_txqueue);
function mergeStatus (status: ActionStatus[]): ActionStatus[] {
return status
.reduce((result: StatusCount[], status): StatusCount[] => {
const prev = result.find(({ status: prev }) => prev.action === status.action && prev.status === status.status);
const setStQueue = (st: QueueStatus[]): void => {
stRef.current = st;
_setStQueue(st);
};
const setTxQueue = (tx: QueueTx[]): void => {
txRef.current = tx;
_setTxQueue(tx);
};
if (prev) {
prev.count++;
} else {
result.push({ count: 1, status });
}
const addToTxQueue = (value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => {
const id = ++nextId;
const removeItem = (): void =>
setTxQueue([...txRef.current.map((item): QueueTx =>
item.id === id
? { ...item, status: 'completed' }
: item
)]);
return result;
}, [])
.map(({ count, status }): ActionStatus =>
count === 1
? status
: { ...status, action: `${status.action} (x${count})` }
);
}
setTxQueue([...txRef.current, {
...value,
id,
removeItem,
rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC,
status: 'queued'
}]);
};
function extractEvents (result?: SubmittableResult): ActionStatus[] {
return mergeStatus(
((result && result.events) || [])
// filter events handled globally, or those we are not interested in, these are
// handled by the global overview, so don't add them here
.filter((record): boolean => !!record.event && record.event.section !== 'democracy')
.map(({ event: { data, method, section } }): ActionStatus => {
if (section === 'system' && method === 'ExtrinsicFailed') {
const [dispatchError] = data as unknown as ITuple<[DispatchError]>;
let message = dispatchError.type;
const queueAction = (status: ActionStatus | ActionStatus[]): void => {
const _status = Array.isArray(status) ? status : [status];
if (dispatchError.isModule) {
try {
const mod = dispatchError.asModule;
const error = registry.findMetaError(new Uint8Array([mod.index.toNumber(), mod.error.toNumber()]));
if (_status.length) {
setStQueue([...stRef.current, ...(_status.map((item): QueueStatus => {
const id = ++nextId;
const removeItem = (): void =>
setStQueue([...stRef.current.filter((item): boolean => item.id !== id)]);
setTimeout(removeItem, REMOVE_TIMEOUT);
return {
...item,
id,
isCompleted: false,
removeItem
};
}))]);
}
};
const queueExtrinsic = ({ accountId, extrinsic, txFailedCb, txSuccessCb, txStartCb, txUpdateCb, isUnsigned }: PartialQueueTxExtrinsic): void =>
addToTxQueue({
accountId,
extrinsic,
isUnsigned,
txFailedCb,
txSuccessCb,
txStartCb,
txUpdateCb
});
const queuePayload = (payload: SignerPayloadJSON, signerCb: SignerCallback): void =>
addToTxQueue({
accountId: payload.address,
// this is not great, but the Extrinsic we don't need a submittable
extrinsic: createType(registry, 'Extrinsic',
{ method: createType(registry, 'Call', payload.method) },
{ version: payload.version }
) as unknown as SubmittableExtrinsic,
payload,
signerCb
});
const queueRpc = ({ accountId, rpc, values }: PartialQueueTxRpc): void =>
addToTxQueue({
accountId,
rpc,
values
});
const queueSetTxStatus = (id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => {
setTxQueue([...txRef.current.map((item): QueueTx =>
item.id === id
? {
...item,
error: error === undefined
? item.error
: error,
result: result === undefined
? item.result
: result,
status: item.status === 'completed'
? item.status
: status
}
: item
)]);
queueAction(
((result && result.events) || [])
// filter events handled globally, or those we are not interested in, these are
// handled by the global overview, so don't add them here
.filter((record): boolean => !!record.event && record.event.section !== 'democracy')
.map(({ event: { data, method, section } }): ActionStatus => {
if (section === 'system' && method === 'ExtrinsicFailed') {
const [dispatchError] = data as unknown as ITuple<[DispatchError]>;
let message = dispatchError.type;
if (dispatchError.isModule) {
try {
const mod = dispatchError.asModule;
const error = registry.findMetaError(new Uint8Array([mod.index.toNumber(), mod.error.toNumber()]));
message = `${error.section}.${error.name}`;
} catch (error) {
// swallow
}
message = `${error.section}.${error.name}`;
} catch (error) {
// swallow
}
return {
action: `${section}.${method}`,
status: 'error',
message
};
}
return {
action: `${section}.${method}`,
status: 'event',
message: 'extrinsic event'
status: 'error',
message
};
})
);
}
if (STATUS_COMPLETE.includes(status)) {
setTimeout((): void => {
const item = txRef.current.find((item): boolean => item.id === id);
return {
action: `${section}.${method}`,
status: 'event',
message: 'extrinsic event'
};
})
);
}
item && item.removeItem();
}, REMOVE_TIMEOUT);
}
};
function Queue ({ children }: Props): React.ReactElement<Props> {
const [stqueue, _setStQueue] = useState<QueueStatus[]>([]);
const [txqueue, _setTxQueue] = useState<QueueTx[]>([]);
const stRef = useRef(stqueue);
const txRef = useRef(txqueue);
const setStQueue = useCallback(
(st: QueueStatus[]): void => {
stRef.current = st;
_setStQueue(st);
},
[]
);
const setTxQueue = useCallback(
(tx: QueueTx[]): void => {
txRef.current = tx;
_setTxQueue(tx);
},
[]
);
const addToTxQueue = useCallback(
(value: QueueTxExtrinsic | QueueTxRpc | QueueTx): void => {
const id = ++nextId;
const removeItem = (): void =>
setTxQueue([...txRef.current.map((item): QueueTx =>
item.id === id
? { ...item, status: 'completed' }
: item
)]);
setTxQueue([...txRef.current, {
...value,
id,
removeItem,
rpc: (value as QueueTxRpc).rpc || SUBMIT_RPC,
status: 'queued'
}]);
},
[setTxQueue]
);
const queueAction = useCallback(
(_status: ActionStatus | ActionStatus[]): void => {
const status = Array.isArray(_status) ? _status : [_status];
status.length && setStQueue([
...stRef.current,
...(status.map((item): QueueStatus => {
const id = ++nextId;
const removeItem = (): void =>
setStQueue([...stRef.current.filter((item): boolean => item.id !== id)]);
setTimeout(removeItem, REMOVE_TIMEOUT);
return {
...item,
id,
isCompleted: false,
removeItem
};
}))
]);
},
[setStQueue]
);
const queueExtrinsic = useCallback(
(value: PartialQueueTxExtrinsic): void =>
addToTxQueue({ ...value }),
[addToTxQueue]
);
const queuePayload = useCallback(
(payload: SignerPayloadJSON, signerCb: SignerCallback): void =>
addToTxQueue({
accountId: payload.address,
// this is not great, but the Extrinsic we don't need a submittable
extrinsic: createType(registry, 'Extrinsic',
{ method: createType(registry, 'Call', payload.method) },
{ version: payload.version }
) as unknown as SubmittableExtrinsic,
payload,
signerCb
}),
[addToTxQueue]
);
const queueRpc = useCallback(
(value: PartialQueueTxRpc): void =>
addToTxQueue({ ...value }),
[addToTxQueue]
);
const queueSetTxStatus = useCallback(
(id: number, status: QueueTxStatus, result?: SubmittableResult, error?: Error): void => {
setTxQueue([...txRef.current.map((item): QueueTx =>
item.id === id
? {
...item,
error: error === undefined
? item.error
: error,
result: result === undefined
? item.result
: result,
status: item.status === 'completed'
? item.status
: status
}
: item
)]);
queueAction(
extractEvents(result)
);
if (STATUS_COMPLETE.includes(status)) {
setTimeout((): void => {
const item = txRef.current.find((item): boolean => item.id === id);
item && item.removeItem();
}, REMOVE_TIMEOUT);
}
},
[queueAction, setTxQueue]
);
return (
<QueueProvider value={{
@ -176,8 +218,8 @@ function Queue ({ children }: Props): React.ReactElement<Props> {
queuePayload,
queueRpc,
queueSetTxStatus,
stqueue: _stqueue,
txqueue: _txqueue
stqueue,
txqueue
}}>
{children}
</QueueProvider>

View File

@ -61,7 +61,7 @@ const Memo = React.memo(styled(Table)`
width: 100%;
thead tr {
background: #fafafa;
background: #f5f5f5;
}
tbody tr {
@ -130,14 +130,11 @@ const Memo = React.memo(styled(Table)`
}
&.address {
min-width: 11rem;
padding: 0.85rem 1rem;
> .ui--AddressMini,
> .ui--AddressMini.padded {
padding: 0;
margin-bottom: -0.85rem;
margin-top: -0.25rem;
label.ui--AddressMini-label {
margin-bottom: -0.3rem;
}
@ -169,9 +166,9 @@ const Memo = React.memo(styled(Table)`
}
th {
color: #999;
color: #888;
font-family: sans-serif;
font-weight: 500;
font-weight: 400;
text-align: right;
white-space: nowrap;

View File

@ -29,8 +29,7 @@ export default function useAccounts (): UseAccounts {
return (): void => {
setTimeout(() => subscription.unsubscribe(), 0);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [mountedRef]);
return state;
}

View File

@ -29,8 +29,7 @@ export default function useAccounts (): UseAccounts {
return (): void => {
setTimeout(() => subscription.unsubscribe(), 0);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
}, [mountedRef]);
return state;
}

View File

@ -14,17 +14,17 @@ interface Result {
export default function useMembers (collective: 'council' | 'technicalCommittee'): Result {
const { api } = useApi();
const { allAccounts } = useAccounts();
const { allAccounts, hasAccounts } = useAccounts();
const [state, setState] = useState<Result>({ isMember: false, members: [] });
const retrieved = (
collective === 'council'
// eslint-disable-next-line react-hooks/rules-of-hooks
? useCall<string[]>(api.query.electionsPhragmen?.members || api.query.elections.members, [], {
? useCall<string[]>(hasAccounts && (api.query.electionsPhragmen?.members || api.query.elections.members), [], {
transform: (accounts: [AccountId, Balance][]): string[] =>
accounts.map(([accountId]) => accountId.toString())
})
// eslint-disable-next-line react-hooks/rules-of-hooks
: useCall<string[]>(api.query.technicalCommittee.members, [], {
: useCall<string[]>(hasAccounts && api.query.technicalCommittee.members, [], {
transform: (accounts: AccountId[]): string[] =>
accounts.map((accountId) => accountId.toString())
})

View File

@ -33,15 +33,15 @@ function getStashes (allAccounts: string[], ownBonded: Option<AccountId>[], ownL
}
export default function useOwnStashes (): [string, IsInKeyring][] | undefined {
const { allAccounts } = useAccounts();
const { allAccounts, hasAccounts } = useAccounts();
const mountedRef = useIsMountedRef();
const { api } = useApi();
const ownBonded = useCall<Option<AccountId>[]>(api.query.staking?.bonded.multi as any, [allAccounts]);
const ownLedger = useCall<Option<StakingLedger>[]>(api.query.staking?.ledger.multi as any, [allAccounts]);
const ownBonded = useCall<Option<AccountId>[]>(hasAccounts && api.query.staking?.bonded.multi as any, [allAccounts]);
const ownLedger = useCall<Option<StakingLedger>[]>(hasAccounts && api.query.staking?.ledger.multi as any, [allAccounts]);
const [state, setState] = useState<[string, IsInKeyring][] | undefined>();
useEffect((): void => {
mountedRef.current && allAccounts && ownBonded && ownLedger && setState(
mountedRef.current && ownBonded && ownLedger && setState(
getStashes(allAccounts, ownBonded, ownLedger)
);
}, [allAccounts, mountedRef, ownBonded, ownLedger]);

View File

@ -18,8 +18,8 @@ interface State {
export default function useRegistrars (): State {
const { api } = useApi();
const { allAccounts } = useAccounts();
const query = useCall<Option<RegistrarInfo>[]>(api.query.identity?.registrars, []);
const { allAccounts, hasAccounts } = useAccounts();
const query = useCall<Option<RegistrarInfo>[]>(hasAccounts && api.query.identity?.registrars, []);
const [state, setState] = useState<State>({ isRegistrar: false, registrars: [] });
// determine if we have a registrar or not - registrars are allowed to approve

View File

@ -12,8 +12,8 @@ import useCall from './useCall';
export default function useSudo (): UseSudo {
const { api } = useApi();
const { allAccounts } = useAccounts();
const sudoKey = useCall<string>(api.query.sudo?.key, [], {
const { allAccounts, hasAccounts } = useAccounts();
const sudoKey = useCall<string>(hasAccounts && api.query.sudo?.key, [], {
transform: (k): string =>
k.toString()
});

View File

@ -192,7 +192,11 @@ class Signer extends React.PureComponent<Props, State> {
const { isExternal, isHardware, hardwareType } = extractExternal(currentItem.accountId);
return (
<Modal.Actions cancelLabel={signedTx ? t('Close') : undefined} withOr={!signedTx} onCancel={isQrVisible ? this.onCancelQr : signedTx ? this.onCancelSign : this.onCancel}>
<Modal.Actions
cancelLabel={signedTx ? t('Close') : undefined}
withOr={!signedTx}
onCancel={isQrVisible ? this.onCancelQr : signedTx ? this.onCancelSign : this.onCancel}
>
{!isRenderError && (!isQrVisible || !isQrScanning) && !signedTx && (
<>
{!currentItem.isUnsigned && this.renderSignToggle()}
@ -239,15 +243,22 @@ class Signer extends React.PureComponent<Props, State> {
tip={tip}
value={currentItem}
>
{isQrVisible ? (
<Qr address={qrAddress} isScanning={isQrScanning} onSignature={this.addQrSignature} payload={qrPayload} />
) : (
<>
{this.renderTip()}
{this.renderUnlock()}
{!isSubmit && this.renderSignFields()}
</>
)}
{isQrVisible
? (
<Qr
address={qrAddress}
isScanning={isQrScanning}
onSignature={this.addQrSignature}
payload={qrPayload}
/>
)
: (
<>
{this.renderTip()}
{this.renderUnlock()}
{!isSubmit && this.renderSignFields()}
</>
)}
</Transaction>
);
}