ghost-portal/packages/page-accounts/src/Accounts/index.tsx
Marcin Wachulski 466a1be22f
Account row collapsible row details (#5894)
* Retrofitting basic unit tests to Accounts

Extraction of useAccounts and useBalance hooks

* Retrofitting basic unit tests to Accounts

Extraction of useAccounts and useBalance hooks

* Fixing accounts unit tests - basic account balances

* Cleaning up after PR remarks

* Fixing arrangements of balances 1/3

Moving ArrangedAccount around

* Add useStakingInfo hook

* Adopt linter suggestions

* Fixing arrangements of balances 2/3

Linking arranged balances values to mocked balances calls.

* Fixing arrangements of balances 3/3

Formatting utils made consistent

* Details row odd-even + (un)hide

* Improving th/td/footer layout

* Add tags to details row

* Changing layout of account balance with details

* Add total/details balances unit tests

* Add tags unit tests

* Clean up of TODOs, adding comments

* Improve tags unit testing

* Alternate account row details coloring unit test

* Row details toggling unit tests

* Row details toggling unit tests - bad commit fix

* Style address balances (#5897)

* Fix jumping column problem

* Improve balances positioning

* Format sidebar

* Position header

* Extract common formatting of balances in tests.

Thx @vitaliyworks, great hint.

* Revert Sidebar styling

* Use rems

* Lint

* Use useToggle for expanding account row

* Add tags header

* Apply FormatBalance review comments

* Display parent account info above account name (#5947)

* Display parent address

* Improve layout

* Set keyring in tests

* Remove redundant code from components

* Use derivedAccount only in specific test

* Apply lint fixes

* Remove parent column from expanded row

* Apply review suggestions

* Improve styling in ParentAddress component

* Improve layout of parent address and add short address

* Apply review suggestions

* Use map instead of for loops

Co-authored-by: Marcin Wachulski <marcin@ethworks.io>
Co-authored-by: Vitaliy Mysak <vitaliy@ethworks.io>
Co-authored-by: MiZiet <55240109+MiZiet@users.noreply.github.com>
Co-authored-by: MiZiet <michalzietalak@o2.pl>
Co-authored-by: Ewa Kowalska <30404388+ekowalsk@users.noreply.github.com>
Co-authored-by: Ewa Kowalska <ewcia.kowalska@gmail.com>
2021-08-19 16:42:46 +02:00

261 lines
8.0 KiB
TypeScript

// Copyright 2017-2021 @polkadot/app-accounts authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type BN from 'bn.js';
import type { ActionStatus } from '@polkadot/react-components/Status/types';
import type { AccountId, ProxyDefinition, ProxyType, Voting } from '@polkadot/types/interfaces';
import type { AccountBalance, Delegation, SortedAccount } from '../types';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styled from 'styled-components';
import { Button, Input, Table } from '@polkadot/react-components';
import { useAccounts, useApi, useCall, useFavorites, useIpfs, useLedger, useLoadingDelay, useToggle } from '@polkadot/react-hooks';
import { BN_ZERO } from '@polkadot/util';
import CreateModal from '../modals/Create';
import ImportModal from '../modals/Import';
import Ledger from '../modals/Ledger';
import Multisig from '../modals/MultisigCreate';
import Proxy from '../modals/ProxiedAdd';
import Qr from '../modals/Qr';
import { useTranslation } from '../translate';
import { sortAccounts } from '../util';
import Account from './Account';
import BannerClaims from './BannerClaims';
import BannerExtension from './BannerExtension';
import Summary from './Summary';
interface Balances {
accounts: Record<string, AccountBalance>;
summary?: AccountBalance;
}
interface Sorted {
sortedAccounts: SortedAccount[];
sortedAddresses: string[];
}
interface Props {
className?: string;
onStatusChange: (status: ActionStatus) => void;
}
const STORE_FAVS = 'accounts:favorites';
function Overview ({ className = '', onStatusChange }: Props): React.ReactElement<Props> {
const { t } = useTranslation();
const { api } = useApi();
const { allAccounts, hasAccounts } = useAccounts();
const { isIpfs } = useIpfs();
const { isLedgerEnabled } = useLedger();
const [isCreateOpen, toggleCreate] = useToggle();
const [isImportOpen, toggleImport] = useToggle();
const [isLedgerOpen, toggleLedger] = useToggle();
const [isMultisigOpen, toggleMultisig] = useToggle();
const [isProxyOpen, toggleProxy] = useToggle();
const [isQrOpen, toggleQr] = useToggle();
const [favorites, toggleFavorite] = useFavorites(STORE_FAVS);
const [balances, setBalances] = useState<Balances>({ accounts: {} });
const [filterOn, setFilter] = useState<string>('');
const [sortedAccountsWithDelegation, setSortedAccountsWithDelegation] = useState<SortedAccount[] | undefined>();
const [{ sortedAccounts, sortedAddresses }, setSorted] = useState<Sorted>({ sortedAccounts: [], sortedAddresses: [] });
const delegations = useCall<Voting[]>(api.query.democracy?.votingOf?.multi, [sortedAddresses]);
const proxies = useCall<[ProxyDefinition[], BN][]>(api.query.proxy?.proxies.multi, [sortedAddresses], {
transform: (result: [([AccountId, ProxyType] | ProxyDefinition)[], BN][]): [ProxyDefinition[], BN][] =>
api.tx.proxy.addProxy.meta.args.length === 3
? result as [ProxyDefinition[], BN][]
: (result as [[AccountId, ProxyType][], BN][]).map(([arr, bn]): [ProxyDefinition[], BN] =>
[arr.map(([delegate, proxyType]): ProxyDefinition => api.createType('ProxyDefinition', { delegate, proxyType })), bn]
)
});
const isLoading = useLoadingDelay();
const headerRef = useRef([
[t('accounts'), 'start', 3],
[t('type')],
[t('transactions'), 'media--1500'],
[t('balances'), 'balances'],
[],
[undefined, 'media--1400'],
[]
]);
useEffect((): void => {
const sortedAccounts = sortAccounts(allAccounts, favorites);
const sortedAddresses = sortedAccounts.map((a) => a.account.address);
setSorted({ sortedAccounts, sortedAddresses });
}, [allAccounts, favorites]);
useEffect(() => {
setSortedAccountsWithDelegation(
sortedAccounts?.map((account, index) => {
let delegation: Delegation | undefined;
if (delegations && delegations[index]?.isDelegating) {
const { balance: amount, conviction, target } = delegations[index].asDelegating;
delegation = {
accountDelegated: target.toString(),
amount,
conviction
};
}
return ({
...account,
delegation
});
})
);
}, [api, delegations, sortedAccounts]);
const _setBalance = useCallback(
(account: string, balance: AccountBalance) =>
setBalances(({ accounts }: Balances): Balances => {
accounts[account] = balance;
const aggregate = (key: keyof AccountBalance) =>
Object.values(accounts).reduce((total: BN, value: AccountBalance) => total.add(value[key]), BN_ZERO);
return {
accounts,
summary: {
bonded: aggregate('bonded'),
locked: aggregate('locked'),
redeemable: aggregate('redeemable'),
total: aggregate('total'),
transferrable: aggregate('transferrable'),
unbonding: aggregate('unbonding')
}
};
}),
[]
);
const filter = useMemo(() => (
<div className='filter--tags'>
<Input
autoFocus
isFull
label={t<string>('filter by name or tags')}
onChange={setFilter}
value={filterOn}
/>
</div>
), [filterOn, t]);
return (
<div className={className}>
{isCreateOpen && (
<CreateModal
onClose={toggleCreate}
onStatusChange={onStatusChange}
/>
)}
{isImportOpen && (
<ImportModal
onClose={toggleImport}
onStatusChange={onStatusChange}
/>
)}
{isLedgerOpen && (
<Ledger onClose={toggleLedger} />
)}
{isMultisigOpen && (
<Multisig
onClose={toggleMultisig}
onStatusChange={onStatusChange}
/>
)}
{isProxyOpen && (
<Proxy
onClose={toggleProxy}
onStatusChange={onStatusChange}
/>
)}
{isQrOpen && (
<Qr
onClose={toggleQr}
onStatusChange={onStatusChange}
/>
)}
<Button.Group>
<Button
icon='plus'
isDisabled={isIpfs}
label={t<string>('Add account')}
onClick={toggleCreate}
/>
<Button
icon='sync'
isDisabled={isIpfs}
label={t<string>('Restore JSON')}
onClick={toggleImport}
/>
<Button
icon='qrcode'
label={t<string>('Add via Qr')}
onClick={toggleQr}
/>
{isLedgerEnabled && (
<>
<Button
icon='project-diagram'
label={t<string>('Add via Ledger')}
onClick={toggleLedger}
/>
</>
)}
<Button
icon='plus'
isDisabled={!(api.tx.multisig || api.tx.utility) || !hasAccounts}
label={t<string>('Multisig')}
onClick={toggleMultisig}
/>
<Button
icon='plus'
isDisabled={!api.tx.proxy || !hasAccounts}
label={t<string>('Proxied')}
onClick={toggleProxy}
/>
</Button.Group>
<BannerExtension />
<BannerClaims />
<Summary balance={balances.summary} />
<Table
empty={!isLoading && sortedAccountsWithDelegation && t<string>("You don't have any accounts. Some features are currently hidden and will only become available once you have accounts.")}
filter={filter}
header={headerRef.current}
>
{!isLoading && sortedAccountsWithDelegation?.map(({ account, delegation, isFavorite }, index): React.ReactNode => (
<Account
account={account}
delegation={delegation}
filter={filterOn}
isEven={!!(index % 2)}
isFavorite={isFavorite}
key={`${index}:${account.address}`}
proxy={proxies?.[index]}
setBalance={_setBalance}
toggleFavorite={toggleFavorite}
/>
))}
</Table>
</div>
);
}
export default React.memo(styled(Overview)`
.filter--tags {
.ui--Dropdown {
padding-left: 0;
label {
left: 1.55rem;
}
}
}
`);