diff --git a/packages/page-assets/src/Balances/Balance.tsx b/packages/page-assets/src/Balances/Balance.tsx new file mode 100644 index 0000000000..c4308d2f49 --- /dev/null +++ b/packages/page-assets/src/Balances/Balance.tsx @@ -0,0 +1,44 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetBalance } from '@polkadot/types/interfaces'; + +import React from 'react'; + +import { AddressSmall } from '@polkadot/react-components'; +import { FormatBalance } from '@polkadot/react-query'; + +import { useTranslation } from '../translate'; + +interface Props { + accountId: string; + balance: AssetBalance; + className?: string; + siFormat: [number, string]; +} + +function Balance ({ accountId, balance: { balance, isFrozen, isSufficient }, className, siFormat }: Props): React.ReactElement { + const { t } = useTranslation(); + + return ( + + + + + + {isFrozen.isTrue ? t('Yes') : t('No')} + + + {isSufficient.isTrue ? t('Yes') : t('No')} + + + + + + ); +} + +export default React.memo(Balance); diff --git a/packages/page-assets/src/Balances/index.tsx b/packages/page-assets/src/Balances/index.tsx new file mode 100644 index 0000000000..1acad60682 --- /dev/null +++ b/packages/page-assets/src/Balances/index.tsx @@ -0,0 +1,94 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetInfo, AssetInfoComplete } from '../types'; + +import React, { useEffect, useMemo, useRef, useState } from 'react'; + +import { Dropdown, Table } from '@polkadot/react-components'; +import { formatNumber } from '@polkadot/util'; + +import { useTranslation } from '../translate'; +import Balance from './Balance'; +import useBalances from './useBalances'; + +interface Props { + className?: string; + infos?: AssetInfo[]; +} + +function Balances ({ className, infos = [] }: Props): React.ReactElement { + const { t } = useTranslation(); + const [infoIndex, setInfoIndex] = useState(0); + const [info, setInfo] = useState(null); + const balances = useBalances(info); + + const headerRef = useRef([ + [t('accounts'), 'start'], + [t('frozen'), 'start'], + [t('sufficient'), 'start'], + [] + ]); + + const completeInfos = 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 }, index) => ({ + text: `${metadata.name.toUtf8()} (${formatNumber(id)})`, + value: index + })), + [completeInfos] + ); + + const siFormat = useMemo( + (): [number, string] => info + ? [info.metadata.decimals.toNumber(), info.metadata.symbol.toUtf8().toUpperCase()] + : [0, 'NONE'], + [info] + ); + + useEffect((): void => { + setInfo(() => + infoIndex >= 0 && infoIndex < completeInfos.length + ? completeInfos[infoIndex] + : null + ); + }, [completeInfos, infoIndex]); + + return ( +
+ ('No accounts with balances found for the asset')} + filter={assetOptions.length + ? ( + ('the asset to query for balances')} + onChange={setInfoIndex} + options={assetOptions} + value={infoIndex} + /> + ) + : undefined + } + header={headerRef.current} + > + {info && balances?.map(({ accountId, balance }) => ( + + ))} +
+
+ ); +} + +export default React.memo(Balances); diff --git a/packages/page-assets/src/Balances/useBalances.ts b/packages/page-assets/src/Balances/useBalances.ts new file mode 100644 index 0000000000..6dc00929fe --- /dev/null +++ b/packages/page-assets/src/Balances/useBalances.ts @@ -0,0 +1,51 @@ +// Copyright 2017-2021 @polkadot/app-assets authors & contributors +// SPDX-License-Identifier: Apache-2.0 + +import type { AssetBalance, AssetId } from '@polkadot/types/interfaces'; +import type { AssetInfoComplete } from '../types'; + +import { useEffect, useState } from 'react'; + +import { useAccounts, useApi, useCall, useIsMountedRef } from '@polkadot/react-hooks'; + +interface Result { + accountId: string; + balance: AssetBalance; +} + +const queryOptions = { + transform: ([[params], results]: [[[AssetId, string][]], AssetBalance[]]): Result[] => + results + .filter(({ balance }) => !balance.isZero()) + .map((balance, index) => ({ + accountId: params[index][1], + balance + })), + withParamsTransform: true +}; + +export default function useBalances (info?: AssetInfoComplete | null): Result[] | null { + const { api } = useApi(); + const { allAccounts } = useAccounts(); + const mountedRef = useIsMountedRef(); + const [state, setState] = useState(null); + const query = useCall(info && api.query.assets.account.multi, info && [allAccounts.map((a) => [info.id, a])], queryOptions); + + useEffect((): void => { + if (mountedRef.current) { + setState(null); + + if (info) { + // dummy + } + } + }, [info, mountedRef]); + + useEffect((): void => { + if (mountedRef.current && query) { + setState(query); + } + }, [query, mountedRef]); + + return state; +} diff --git a/packages/page-assets/src/Overview/Asset.tsx b/packages/page-assets/src/Overview/Asset.tsx index d0f94a19a4..bab6606a46 100644 --- a/packages/page-assets/src/Overview/Asset.tsx +++ b/packages/page-assets/src/Overview/Asset.tsx @@ -1,7 +1,7 @@ // Copyright 2017-2021 @polkadot/app-assets authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { AssetInfo } from './types'; +import type { AssetInfo } from '../types'; import React, { useMemo } from 'react'; diff --git a/packages/page-assets/src/Overview/Assets.tsx b/packages/page-assets/src/Overview/Assets.tsx index b9f581a211..6684456468 100644 --- a/packages/page-assets/src/Overview/Assets.tsx +++ b/packages/page-assets/src/Overview/Assets.tsx @@ -1,7 +1,7 @@ // Copyright 2017-2021 @polkadot/app-assets authors & contributors // SPDX-License-Identifier: Apache-2.0 -import type { AssetId } from '@polkadot/types/interfaces'; +import type { AssetInfo } from '../types'; import React, { useRef } from 'react'; @@ -9,16 +9,14 @@ import { Table } from '@polkadot/react-components'; import { useTranslation } from '../translate'; import Asset from './Asset'; -import useAssetInfos from './useAssetInfos'; interface Props { - assetIds?: AssetId[]; className?: string; + infos?: AssetInfo[]; } -function Assets ({ assetIds, className }: Props): React.ReactElement { +function Assets ({ className, infos }: Props): React.ReactElement { const { t } = useTranslation(); - const assetInfos = useAssetInfos(assetIds); const headerRef = useRef([ [t('assets'), 'start', 2], @@ -33,10 +31,10 @@ function Assets ({ assetIds, className }: Props): React.ReactElement { return ( ('No assets found')} + empty={infos && t('No assets found')} header={headerRef.current} > - {assetInfos?.map((info) => ( + {infos?.map((info) => ( { - const assetIds = useAssetIds(); - +function Overview ({ className, ids, infos }: Props): React.ReactElement { return (
- + - + - +
); } diff --git a/packages/page-assets/src/index.tsx b/packages/page-assets/src/index.tsx index 7a0122c06d..eba5085a6d 100644 --- a/packages/page-assets/src/index.tsx +++ b/packages/page-assets/src/index.tsx @@ -1,13 +1,17 @@ // Copyright 2017-2021 @polkadot/app-assets authors & contributors // SPDX-License-Identifier: Apache-2.0 -import React, { useRef } from 'react'; +import React, { useMemo, useRef } from 'react'; import { Route, Switch } from 'react-router'; import { Tabs } from '@polkadot/react-components'; +import { useAccounts } from '@polkadot/react-hooks'; +import Balances from './Balances'; import Overview from './Overview'; import { useTranslation } from './translate'; +import useAssetIds from './useAssetIds'; +import useAssetInfos from './useAssetInfos'; interface Props { basePath: string; @@ -16,24 +20,43 @@ interface Props { function GiltApp ({ basePath, className }: Props): React.ReactElement { const { t } = useTranslation(); + const { hasAccounts } = useAccounts(); + const ids = useAssetIds(); + const infos = useAssetInfos(ids); const tabsRef = useRef([ { isRoot: true, name: 'overview', text: t('Overview') + }, + { + name: 'balances', + text: t('Balances') } ]); + const hidden = useMemo( + () => (hasAccounts && infos?.length) ? [] : ['balances'], + [hasAccounts, infos] + ); + return (
diff --git a/packages/page-assets/src/Overview/types.ts b/packages/page-assets/src/types.ts similarity index 79% rename from packages/page-assets/src/Overview/types.ts rename to packages/page-assets/src/types.ts index efe42aa4e2..e76ac48d3d 100644 --- a/packages/page-assets/src/Overview/types.ts +++ b/packages/page-assets/src/types.ts @@ -13,3 +13,8 @@ export interface AssetInfo { key: string; metadata: AssetMetadata | null; } + +export interface AssetInfoComplete extends AssetInfo { + details: AssetDetails; + metadata: AssetMetadata; +} diff --git a/packages/page-assets/src/Overview/useAssetIds.ts b/packages/page-assets/src/useAssetIds.ts similarity index 100% rename from packages/page-assets/src/Overview/useAssetIds.ts rename to packages/page-assets/src/useAssetIds.ts diff --git a/packages/page-assets/src/Overview/useAssetInfos.ts b/packages/page-assets/src/useAssetInfos.ts similarity index 100% rename from packages/page-assets/src/Overview/useAssetInfos.ts rename to packages/page-assets/src/useAssetInfos.ts diff --git a/packages/react-components/src/Table/index.tsx b/packages/react-components/src/Table/index.tsx index c89a297177..d201794298 100644 --- a/packages/react-components/src/Table/index.tsx +++ b/packages/react-components/src/Table/index.tsx @@ -68,7 +68,7 @@ export default React.memo(styled(Table)` table { border-spacing: 0; max-width: 100%; - overflow: hidden; + overflow-x: hidden; position: relative; width: 100%; z-index: 1; diff --git a/packages/react-hooks/src/useCall.ts b/packages/react-hooks/src/useCall.ts index 70f0d74872..09179d6476 100644 --- a/packages/react-hooks/src/useCall.ts +++ b/packages/react-hooks/src/useCall.ts @@ -99,7 +99,7 @@ function subscribe (mountedRef: MountedRef, tracker: TrackerRef, fn: TrackFn // - returns a promise with an unsubscribe function // - has a callback to set the value // FIXME The typings here need some serious TLC -export function useCall (fn: TrackFn | undefined | null | false, params?: CallParams, options?: CallOptions): T | undefined { +export function useCall (fn: TrackFn | undefined | null | false, params?: CallParams | null, options?: CallOptions): T | undefined { const mountedRef = useIsMountedRef(); const tracker = useRef({ isActive: false, serialized: null, subscriber: null }); const [value, setValue] = useState((options || {}).defaultValue);