Refactor Assets Page <> Improve Non-Native Asset Balance Display (#11290)
* feat: shown assets in table format * feat: refactor Account and Asset components for improved rendering and styling * feat: add search functionality to asset balances display * feat: remove emptySpinner prop from Balances table
This commit is contained in:
parent
91e352848e
commit
11024f141a
@ -32,11 +32,11 @@ interface Props {
|
||||
siFormat: [number, string];
|
||||
}
|
||||
|
||||
function Account ({ account: { balance, isFrozen, reason, sufficient }, accountId, assetId, className, minBalance, siFormat }: Props): React.ReactElement<Props> {
|
||||
function Account ({ account: { balance, isFrozen, reason, sufficient }, accountId, assetId, minBalance, siFormat }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<tr className={className}>
|
||||
<>
|
||||
<td className='address'>
|
||||
<AddressSmall value={accountId} />
|
||||
</td>
|
||||
@ -62,7 +62,7 @@ function Account ({ account: { balance, isFrozen, reason, sufficient }, accountI
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
83
packages/page-assets/src/Balances/Asset.tsx
Normal file
83
packages/page-assets/src/Balances/Asset.tsx
Normal file
@ -0,0 +1,83 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { AssetInfoComplete } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
|
||||
import { styled } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
|
||||
import Account from './Account.js';
|
||||
import useBalances from './useBalances.js';
|
||||
|
||||
interface Props {
|
||||
asset: AssetInfoComplete,
|
||||
className?: string;
|
||||
searchValue: string;
|
||||
}
|
||||
|
||||
const Asset = ({ asset: { details, id, metadata }, className, searchValue }: Props) => {
|
||||
const balances = useBalances(id);
|
||||
|
||||
const siFormat = useMemo(
|
||||
(): [number, string] => metadata
|
||||
? [metadata.decimals.toNumber(), metadata.symbol.toUtf8().toUpperCase()]
|
||||
: [0, 'NONE'],
|
||||
[metadata]
|
||||
);
|
||||
|
||||
const shouldShowAsset = useMemo(() => metadata.name.toUtf8().toLowerCase().includes(searchValue) ||
|
||||
formatNumber(id).toString().replaceAll(',', '').includes(searchValue), [id, metadata.name, searchValue]);
|
||||
|
||||
if (!balances?.length || !shouldShowAsset) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return balances.map(({ account, accountId }, index) => {
|
||||
return (
|
||||
<StyledTr
|
||||
className={`isExpanded ${className}`}
|
||||
isFirstItem={index === 0}
|
||||
isLastItem={index === balances.length - 1}
|
||||
key={accountId}
|
||||
>
|
||||
<td className='all'>
|
||||
{index === 0 && <>{metadata.name.toUtf8()} ({formatNumber(id)})</>}
|
||||
</td>
|
||||
<Account
|
||||
account={account}
|
||||
accountId={accountId}
|
||||
assetId={id}
|
||||
key={accountId}
|
||||
minBalance={details.minBalance}
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
</StyledTr>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
const BASE_BORDER = 0.125;
|
||||
const BORDER_TOP = `${BASE_BORDER * 3}rem solid var(--bg-page)`;
|
||||
const BORDER_RADIUS = `${BASE_BORDER * 4}rem`;
|
||||
|
||||
const StyledTr = styled.tr<{isFirstItem: boolean; isLastItem: boolean}>`
|
||||
td {
|
||||
border-top: ${(props) => props.isFirstItem && BORDER_TOP};
|
||||
border-radius: 0rem !important;
|
||||
|
||||
&:first-child {
|
||||
padding-block: 1rem !important;
|
||||
border-top-left-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
border-bottom-left-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: ${(props) => props.isFirstItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
border-bottom-right-radius: ${(props) => props.isLastItem ? BORDER_RADIUS : '0rem'}!important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default Asset;
|
||||
@ -1,17 +1,14 @@
|
||||
// Copyright 2017-2025 @polkadot/app-assets authors & contributors
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import type { DropdownItemProps } from 'semantic-ui-react';
|
||||
import type { AssetInfo, AssetInfoComplete } from '@polkadot/react-hooks/types';
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import { Dropdown, styled, Table } from '@polkadot/react-components';
|
||||
import { formatNumber } from '@polkadot/util';
|
||||
import { Input, styled, Table } from '@polkadot/react-components';
|
||||
|
||||
import { useTranslation } from '../translate.js';
|
||||
import Account from './Account.js';
|
||||
import useBalances from './useBalances.js';
|
||||
import Asset from './Asset.js';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@ -20,97 +17,61 @@ interface Props {
|
||||
|
||||
function Balances ({ className, infos = [] }: Props): React.ReactElement<Props> {
|
||||
const { t } = useTranslation();
|
||||
const [selectedAssetValue, setSelectedAssetValue] = useState('0');
|
||||
const [info, setInfo] = useState<AssetInfoComplete | null>(null);
|
||||
const balances = useBalances(info?.id);
|
||||
const [{ searchValue }, onApplySearch] = useState({ searchValue: '' });
|
||||
|
||||
const headerRef = useRef<([React.ReactNode?, string?, number?] | false)[]>([
|
||||
[t('asset'), 'start'],
|
||||
[t('accounts'), 'start'],
|
||||
[t('frozen'), 'start'],
|
||||
[t('sufficient'), 'start'],
|
||||
[],
|
||||
[t('free balance'), 'start'],
|
||||
[]
|
||||
]);
|
||||
|
||||
const completeInfos = useMemo(
|
||||
const onChangeInput = useCallback((e: string) => {
|
||||
onApplySearch({ searchValue: e });
|
||||
}, []);
|
||||
|
||||
const completeAssets = useMemo(
|
||||
() => infos
|
||||
.filter((i): i is AssetInfoComplete => !!(i.details && i.metadata) && !i.details.supply.isZero())
|
||||
.sort((a, b) => a.id.cmp(b.id)),
|
||||
[infos]
|
||||
);
|
||||
|
||||
const assetOptions = useMemo(
|
||||
() => completeInfos.map(({ id, metadata }) => ({
|
||||
text: `${metadata.name.toUtf8()} (${formatNumber(id)})`,
|
||||
value: id.toString()
|
||||
})),
|
||||
[completeInfos]
|
||||
);
|
||||
|
||||
const siFormat = useMemo(
|
||||
(): [number, string] => info
|
||||
? [info.metadata.decimals.toNumber(), info.metadata.symbol.toUtf8().toUpperCase()]
|
||||
: [0, 'NONE'],
|
||||
[info]
|
||||
);
|
||||
|
||||
const onSearch = useCallback(
|
||||
(options: DropdownItemProps[], value: string): DropdownItemProps[] =>
|
||||
options.filter((options) => {
|
||||
const { text: optText, value: optValue } = options as { text: string, value: number };
|
||||
|
||||
return parseInt(value) === optValue || optText.includes(value);
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect((): void => {
|
||||
const info = completeInfos.find(({ id }) => id.toString() === selectedAssetValue);
|
||||
|
||||
// if no info found (usually happens on first load), select the first one automatically
|
||||
if (!info) {
|
||||
setInfo(completeInfos.at(0) ?? null);
|
||||
setSelectedAssetValue(completeInfos.at(0)?.id?.toString() ?? '0');
|
||||
} else {
|
||||
setInfo(info);
|
||||
}
|
||||
}, [completeInfos, selectedAssetValue]);
|
||||
|
||||
return (
|
||||
<StyledDiv className={className}>
|
||||
<Input
|
||||
aria-label={t('Search by asset id or name')}
|
||||
className='full isSmall'
|
||||
label={t('Search')}
|
||||
onChange={onChangeInput}
|
||||
placeholder={t('Search by asset id or name')}
|
||||
value={searchValue}
|
||||
/>
|
||||
<Table
|
||||
empty={info && balances && t('No accounts with balances found for the asset')}
|
||||
filter={assetOptions.length
|
||||
? (
|
||||
<Dropdown
|
||||
isFull
|
||||
label={t('the asset to query for balances')}
|
||||
onChange={setSelectedAssetValue}
|
||||
onSearch={onSearch}
|
||||
options={assetOptions}
|
||||
value={selectedAssetValue}
|
||||
/>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
empty={t('No accounts with balances found for the asset')}
|
||||
header={headerRef.current}
|
||||
>
|
||||
{info && balances?.map(({ account, accountId }) => (
|
||||
<Account
|
||||
account={account}
|
||||
accountId={accountId}
|
||||
assetId={info.id}
|
||||
key={accountId}
|
||||
minBalance={info.details.minBalance}
|
||||
siFormat={siFormat}
|
||||
/>
|
||||
))}
|
||||
{completeAssets.map((asset) => {
|
||||
return (
|
||||
<Asset
|
||||
asset={asset}
|
||||
key={asset.id.toString()}
|
||||
searchValue={searchValue.toLowerCase()}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Table>
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
|
||||
const StyledDiv = styled.div`
|
||||
input {
|
||||
max-width: 250px !important;
|
||||
}
|
||||
|
||||
table {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ function useAssetInfosImpl (ids?: BN[]): AssetInfo[] | undefined {
|
||||
const { api } = useApi();
|
||||
const { allAccounts } = useAccounts();
|
||||
|
||||
const isReady = useMemo(() => !!api.tx.assets?.setMetadata && !!api.tx.assets?.transferKeepAlive, [api.tx.assets?.setMetadata, api.tx.assets?.transferKeepAlive]);
|
||||
const isReady = useMemo(() => !!ids?.length && !!api.tx.assets?.setMetadata && !!api.tx.assets?.transferKeepAlive, [api.tx.assets?.setMetadata, api.tx.assets?.transferKeepAlive, ids?.length]);
|
||||
|
||||
const metadata = useCall<[[BN[]], PalletAssetsAssetMetadata[]]>(isReady && api.query.assets.metadata.multi, [ids], QUERY_OPTS);
|
||||
const details = useCall<[[BN[]], Option<PalletAssetsAssetDetails>[]]>(isReady && api.query.assets.asset.multi, [ids], QUERY_OPTS);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user