fix(explorer): improve party page loading (#3274)

This commit is contained in:
Edd 2023-05-09 09:43:12 +01:00 committed by GitHub
parent 72d93bb568
commit 928b899c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 468 additions and 296 deletions

View File

@ -6,6 +6,7 @@ export type AssetBalanceProps = {
assetId: string; assetId: string;
price: string; price: string;
showAssetLink?: boolean; showAssetLink?: boolean;
showAssetSymbol?: boolean;
}; };
/** /**
@ -16,18 +17,21 @@ const AssetBalance = ({
assetId, assetId,
price, price,
showAssetLink = true, showAssetLink = true,
showAssetSymbol = false,
}: AssetBalanceProps) => { }: AssetBalanceProps) => {
const { data: asset } = useAssetDataProvider(assetId); const { data: asset, loading } = useAssetDataProvider(assetId);
const label = const label =
asset && asset.decimals !loading && asset && asset.decimals
? addDecimalsFormatNumber(price, asset.decimals) ? addDecimalsFormatNumber(price, asset.decimals)
: price; : price;
return ( return (
<div className="inline-block"> <div className="inline-block">
<span>{label}</span>{' '} <span>{label}</span>{' '}
{showAssetLink && asset?.id ? <AssetLink assetId={assetId} /> : null} {showAssetLink && asset?.id ? (
<AssetLink showAssetSymbol={showAssetSymbol} assetId={assetId} />
) : null}
</div> </div>
); );
}; };

View File

@ -13,11 +13,17 @@ const DEFAULT_DECIMALS = 18;
* the governance asset first, which is set by a network parameter * the governance asset first, which is set by a network parameter
*/ */
const GovernanceAssetBalance = ({ price }: GovernanceAssetBalanceProps) => { const GovernanceAssetBalance = ({ price }: GovernanceAssetBalanceProps) => {
const { data } = useExplorerGovernanceAssetQuery(); const { data, loading } = useExplorerGovernanceAssetQuery();
if (data && data.networkParameter?.value) { if (!loading && data && data.networkParameter?.value) {
const governanceAssetId = data.networkParameter.value; const governanceAssetId = data.networkParameter.value;
return <AssetBalance price={price} assetId={governanceAssetId} />; return (
<AssetBalance
price={price}
showAssetSymbol={true}
assetId={governanceAssetId}
/>
);
} else { } else {
return ( return (
<div className="inline-block"> <div className="inline-block">

View File

@ -20,7 +20,7 @@ export const PageHeader = ({
copy = false, copy = false,
className, className,
}: PageHeaderProps) => { }: PageHeaderProps) => {
const titleClasses = 'text-4xl xl:text-5xl uppercase font-alpha calt'; const titleClasses = 'text-xl uppercase font-alpha calt';
return ( return (
<header className={className}> <header className={className}>
<span className={`${titleClasses} block`}>{prefix}</span> <span className={`${titleClasses} block`}>{prefix}</span>

View File

@ -30,7 +30,7 @@ export const BundleExists = ({
// Note if this is wrong, the wrong decoder will be used which will give incorrect data // Note if this is wrong, the wrong decoder will be used which will give incorrect data
return ( return (
<div className="w-auto max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5"> <div className="w-auto h-10 max-w-lg border-2 border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<IconForBundleStatus status={status} /> <IconForBundleStatus status={status} />
<h1 className="text-xl pb-1"> <h1 className="text-xl pb-1">
{status === 'STATUS_ENABLED' {status === 'STATUS_ENABLED'

View File

@ -1,4 +1,3 @@
export { TxList } from './tx-list';
export { TxOrderType } from './tx-order-type'; export { TxOrderType } from './tx-order-type';
export { TxsInfiniteList } from './txs-infinite-list'; export { TxsInfiniteList } from './txs-infinite-list';
export { TxsInfiniteListItem } from './txs-infinite-list-item'; export { TxsInfiniteListItem } from './txs-infinite-list-item';

View File

@ -1,14 +0,0 @@
import { t } from '@vegaprotocol/i18n';
import type { TendermintUnconfirmedTransactionsResponse } from '../../routes/txs/tendermint-unconfirmed-transactions-response.d';
interface TxsProps {
data: TendermintUnconfirmedTransactionsResponse | undefined;
}
export const TxList = ({ data }: TxsProps) => {
if (!data) {
return <div>{t('Awaiting transactions')}</div>;
}
return <div>{JSON.stringify(data, null, ' ')}</div>;
};

View File

@ -7,7 +7,7 @@ import { toHex } from '../search/detect-search';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code'; import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import isNumber from 'lodash/isNumber'; import isNumber from 'lodash/isNumber';
const TRUNCATE_LENGTH = 5; const TRUNCATE_LENGTH = 10;
export const TxsInfiniteListItem = ({ export const TxsInfiniteListItem = ({
hash, hash,
@ -34,10 +34,10 @@ export const TxsInfiniteListItem = ({
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2" className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-2"
> >
<div <div
className="text-sm col-span-10 xl:col-span-3 leading-none" className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="tx-hash" data-testid="tx-hash"
> >
<span className="xl:hidden uppercase text-vega-dark-300"> <span className="md:hidden uppercase text-vega-dark-300">
ID:&nbsp; ID:&nbsp;
</span> </span>
<TruncatedLink <TruncatedLink
@ -48,10 +48,10 @@ export const TxsInfiniteListItem = ({
/> />
</div> </div>
<div <div
className="text-sm col-span-10 xl:col-span-3 leading-none" className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="pub-key" data-testid="pub-key"
> >
<span className="xl:hidden uppercase text-vega-dark-300"> <span className="md:hidden uppercase text-vega-dark-300">
By:&nbsp; By:&nbsp;
</span> </span>
<TruncatedLink <TruncatedLink
@ -61,14 +61,14 @@ export const TxsInfiniteListItem = ({
endChars={TRUNCATE_LENGTH} endChars={TRUNCATE_LENGTH}
/> />
</div> </div>
<div className="text-sm col-span-5 xl:col-span-2 leading-none flex items-center"> <div className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
<TxOrderType orderType={type} command={command} /> <TxOrderType orderType={type} command={command} />
</div> </div>
<div <div
className="text-sm col-span-3 xl:col-span-1 leading-none flex items-center" className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
data-testid="tx-block" data-testid="tx-block"
> >
<span className="xl:hidden uppercase text-vega-dark-300"> <span className="md:hidden uppercase text-vega-dark-300">
Block:&nbsp; Block:&nbsp;
</span> </span>
<TruncatedLink <TruncatedLink
@ -79,10 +79,10 @@ export const TxsInfiniteListItem = ({
/> />
</div> </div>
<div <div
className="text-sm col-span-2 xl:col-span-1 leading-none flex items-center" className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
data-testid="tx-success" data-testid="tx-success"
> >
<span className="xl:hidden uppercase text-vega-dark-300"> <span className="md:hidden uppercase text-vega-dark-300">
Success:&nbsp; Success:&nbsp;
</span> </span>
{isNumber(code) ? ( {isNumber(code) ? (

View File

@ -64,9 +64,7 @@ describe('Txs infinite list', () => {
error={Error('test error!')} error={Error('test error!')}
/> />
); );
expect( expect(screen.getByText('Cannot fetch transaction')).toBeInTheDocument();
screen.getByText('Cannot fetch transaction: Error: test error!')
).toBeInTheDocument();
}); });
it('item renders data of n length into list of n length', () => { it('item renders data of n length into list of n length', () => {

View File

@ -30,7 +30,7 @@ const NOOP = () => {};
const Item = ({ index, style, isLoading, error }: ItemProps) => { const Item = ({ index, style, isLoading, error }: ItemProps) => {
let content; let content;
if (error) { if (error) {
content = t(`Cannot fetch transaction: ${error}`); content = t(`Cannot fetch transaction`);
} else if (isLoading) { } else if (isLoading) {
content = <Loader />; content = <Loader />;
} else { } else {
@ -68,7 +68,7 @@ export const TxsInfiniteList = ({
className, className,
}: TxsInfiniteListProps) => { }: TxsInfiniteListProps) => {
const { screenSize } = useScreenDimensions(); const { screenSize } = useScreenDimensions();
const isStacked = ['xs', 'sm', 'md', 'lg'].includes(screenSize); const isStacked = ['xs', 'sm'].includes(screenSize);
if (!txs) { if (!txs) {
if (!areTxsLoading) { if (!areTxsLoading) {
@ -95,15 +95,15 @@ export const TxsInfiniteList = ({
return ( return (
<div className={className} data-testid="transactions-list"> <div className={className} data-testid="transactions-list">
<div className="xl:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase"> <div className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<div className="col-span-3"> <div className="col-span-3">
<span className="hidden xl:inline">Transaction &nbsp;</span> <span className="hidden xl:inline">{t('Transaction')} &nbsp;</span>
<span>ID</span> <span>ID</span>
</div> </div>
<div className="col-span-3">Submitted By</div> <div className="col-span-3">{t('Submitted By')}</div>
<div className="col-span-2">Type</div> <div className="col-span-2">{t('Type')}</div>
<div className="col-span-1">Block</div> <div className="col-span-1">{t('Block')}</div>
<div className="col-span-1">Success</div> <div className="col-span-1">{t('Success')}</div>
</div> </div>
<div data-testid="infinite-scroll-wrapper"> <div data-testid="infinite-scroll-wrapper">
<InfiniteLoader <InfiniteLoader

View File

@ -1,5 +1,4 @@
import { Routes } from '../../routes/route-names'; import { Routes } from '../../routes/route-names';
import { RenderFetched } from '../render-fetched';
import { TruncatedLink } from '../truncate/truncated-link'; import { TruncatedLink } from '../truncate/truncated-link';
import { TxOrderType } from './tx-order-type'; import { TxOrderType } from './tx-order-type';
import { Table, TableRow, TableCell } from '../table'; import { Table, TableRow, TableCell } from '../table';
@ -9,7 +8,7 @@ import type { BlockExplorerTransactions } from '../../routes/types/block-explore
import isNumber from 'lodash/isNumber'; import isNumber from 'lodash/isNumber';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code'; import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import { getTxsDataUrl } from '../../hooks/use-txs-data'; import { getTxsDataUrl } from '../../hooks/use-txs-data';
import { Loader } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list'; import EmptyList from '../empty-list/empty-list';
interface TxsPerBlockProps { interface TxsPerBlockProps {
@ -27,7 +26,7 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
} = useFetch<BlockExplorerTransactions>(url); } = useFetch<BlockExplorerTransactions>(url);
return ( return (
<RenderFetched error={error} loading={loading} className="text-body-large"> <AsyncRenderer data={data} error={error} loading={!!loading}>
{data && data.transactions.length > 0 ? ( {data && data.transactions.length > 0 ? (
<div className="overflow-x-auto whitespace-nowrap mb-28"> <div className="overflow-x-auto whitespace-nowrap mb-28">
<Table> <Table>
@ -95,6 +94,6 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
label={t('0 transactions')} label={t('0 transactions')}
/> />
)} )}
</RenderFetched> </AsyncRenderer>
); );
}; };

View File

@ -144,12 +144,12 @@ afterEach(() => {
describe('Block', () => { describe('Block', () => {
it('renders error state if error is present', async () => { it('renders error state if error is present', async () => {
(useFetch as jest.Mock).mockReturnValue({ (useFetch as jest.Mock).mockReturnValue({
state: { data: null, loading: false, error: 'asd' }, state: { data: null, loading: false, error: new Error('asd') },
}); });
render(renderComponent()); render(renderComponent());
expect(screen.getByText(`BLOCK ${blockId}`)).toBeInTheDocument(); expect(screen.getByText(`BLOCK ${blockId}`)).toBeInTheDocument();
expect(screen.getByText('Error retrieving data')).toBeInTheDocument(); expect(screen.getByText('Something went wrong: asd')).toBeInTheDocument();
}); });
it('renders loading state if present', async () => { it('renders loading state if present', async () => {

View File

@ -1,4 +1,3 @@
import React from 'react';
import { Link, useParams } from 'react-router-dom'; import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config'; import { DATA_SOURCES } from '../../../config';
import { getDateTimeFormat } from '@vegaprotocol/utils'; import { getDateTimeFormat } from '@vegaprotocol/utils';
@ -12,9 +11,8 @@ import {
TableCell, TableCell,
} from '../../../components/table'; } from '../../../components/table';
import { TxsPerBlock } from '../../../components/txs/txs-per-block'; import { TxsPerBlock } from '../../../components/txs/txs-per-block';
import { Button } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, Button } from '@vegaprotocol/ui-toolkit';
import { Routes } from '../../route-names'; import { Routes } from '../../route-names';
import { RenderFetched } from '../../../components/render-fetched';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers'; import { useFetch } from '@vegaprotocol/react-helpers';
import { NodeLink } from '../../../components/links'; import { NodeLink } from '../../../components/links';
@ -34,7 +32,7 @@ const Block = () => {
return ( return (
<section> <section>
<RouteTitle data-testid="block-header">{t(`BLOCK ${block}`)}</RouteTitle> <RouteTitle data-testid="block-header">{t(`BLOCK ${block}`)}</RouteTitle>
<RenderFetched error={error} loading={loading}> <AsyncRenderer data={blockData} error={error} loading={!!loading}>
<> <>
<div className="grid grid-cols-2 gap-2 mb-8"> <div className="grid grid-cols-2 gap-2 mb-8">
<Link <Link
@ -123,7 +121,7 @@ const Block = () => {
</> </>
)} )}
</> </>
</RenderFetched> </AsyncRenderer>
</section> </section>
); );
}; };

View File

@ -1,7 +1,7 @@
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers'; import { useFetch } from '@vegaprotocol/react-helpers';
import { RouteTitle } from '../../components/route-title'; import { RouteTitle } from '../../components/route-title';
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { DATA_SOURCES } from '../../config'; import { DATA_SOURCES } from '../../config';
import type { TendermintGenesisResponse } from './tendermint-genesis-response'; import type { TendermintGenesisResponse } from './tendermint-genesis-response';
import { useDocumentTitle } from '../../hooks/use-document-title'; import { useDocumentTitle } from '../../hooks/use-document-title';
@ -10,21 +10,26 @@ const Genesis = () => {
useDocumentTitle(['Genesis']); useDocumentTitle(['Genesis']);
const { const {
state: { data: genesis, loading }, state: { data, loading, error },
} = useFetch<TendermintGenesisResponse>( } = useFetch<TendermintGenesisResponse>(
`${DATA_SOURCES.tendermintUrl}/genesis` `${DATA_SOURCES.tendermintUrl}/genesis`
); );
if (!genesis?.result.genesis) {
if (loading) {
return <Loader />;
}
return null;
}
return ( return (
<section> <>
<RouteTitle data-testid="genesis-header">{t('Genesis')}</RouteTitle> <RouteTitle data-testid="genesis-header">{t('Genesis')}</RouteTitle>
<SyntaxHighlighter data={genesis?.result.genesis} /> <AsyncRenderer
</section> data={data}
error={error}
loading={!!loading}
loadingMessage={t('Loading genesis information...')}
errorMessage={t('Could not fetch genesis data')}
>
<section>
<SyntaxHighlighter data={data?.result.genesis} />
</section>
</AsyncRenderer>
</>
); );
}; };

View File

@ -49,6 +49,7 @@ export const MarketPage = () => {
/> />
<AsyncRenderer <AsyncRenderer
noDataMessage={t('This chain has no markets')} noDataMessage={t('This chain has no markets')}
errorMessage={t('Could not fetch market') + ' ' + marketId}
data={data} data={data}
loading={loading} loading={loading}
error={error} error={error}

View File

@ -1,4 +1,4 @@
import { Loader, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import { RouteTitle } from '../../../components/route-title'; import { RouteTitle } from '../../../components/route-title';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles'; import { useExplorerOracleSpecsQuery } from '../__generated__/Oracles';
@ -8,7 +8,7 @@ import { useScrollToLocation } from '../../../hooks/scroll-to-location';
import filter from 'recursive-key-filter'; import filter from 'recursive-key-filter';
const Oracles = () => { const Oracles = () => {
const { data, loading } = useExplorerOracleSpecsQuery(); const { data, loading, error } = useExplorerOracleSpecsQuery();
useDocumentTitle(['Oracles']); useDocumentTitle(['Oracles']);
useScrollToLocation(); useScrollToLocation();
@ -16,28 +16,40 @@ const Oracles = () => {
return ( return (
<section> <section>
<RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle> <RouteTitle data-testid="oracle-specs-heading">{t('Oracles')}</RouteTitle>
{loading ? <Loader /> : null} <AsyncRenderer
{data?.oracleSpecsConnection?.edges data={data}
? data.oracleSpecsConnection.edges.map((o) => { loading={loading}
const id = o?.node.dataSourceSpec.spec.id; error={error}
if (!id) { loadingMessage={t('Loading oracle data...')}
return null; errorMessage={t('Oracle data could not be loaded')}
} noDataMessage={t('No oracles found')}
return ( noDataCondition={(data) =>
<div id={id} key={id} className="mb-10"> !data?.oracleSpecsConnection?.edges ||
<OracleDetails data.oracleSpecsConnection.edges?.length === 0
id={id} }
dataSource={o?.node} >
showBroadcasts={false} {data?.oracleSpecsConnection?.edges
/> ? data.oracleSpecsConnection.edges.map((o) => {
<details> const id = o?.node.dataSourceSpec.spec.id;
<summary className="pointer">JSON</summary> if (!id) {
<SyntaxHighlighter data={filter(o, ['__typename'])} /> return null;
</details> }
</div> return (
); <div id={id} key={id} className="mb-10">
}) <OracleDetails
: null} id={id}
dataSource={o?.node}
showBroadcasts={false}
/>
<details>
<summary className="pointer">JSON</summary>
<SyntaxHighlighter data={filter(o, ['__typename'])} />
</details>
</div>
);
})
: null}
</AsyncRenderer>
</section> </section>
); );
}; };

View File

@ -1,12 +1,11 @@
import { RouteTitle } from '../../../components/route-title'; import { RouteTitle } from '../../../components/route-title';
import { RenderFetched } from '../../../components/render-fetched';
import { truncateByChars } from '@vegaprotocol/utils'; import { truncateByChars } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useDocumentTitle } from '../../../hooks/use-document-title';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useExplorerOracleSpecByIdQuery } from '../__generated__/Oracles'; import { useExplorerOracleSpecByIdQuery } from '../__generated__/Oracles';
import { OracleDetails } from '../components/oracle'; import { OracleDetails } from '../components/oracle';
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit'; import { AsyncRenderer, SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
import filter from 'recursive-key-filter'; import filter from 'recursive-key-filter';
import { TruncateInline } from '../../../components/truncate/truncate'; import { TruncateInline } from '../../../components/truncate/truncate';
@ -27,7 +26,14 @@ export const Oracle = () => {
{t(`Oracle `)} {t(`Oracle `)}
<TruncateInline startChars={5} endChars={5} text={id || '1'} /> <TruncateInline startChars={5} endChars={5} text={id || '1'} />
</RouteTitle> </RouteTitle>
<RenderFetched error={error} loading={loading}> <AsyncRenderer
data={data}
error={error}
loading={loading}
noDataCondition={(data) => !data?.oracleSpec}
errorMessage={t('Could not load oracle data')}
loadingMessage={t('Loading oracle data...')}
>
{data?.oracleSpec ? ( {data?.oracleSpec ? (
<div id={id} key={id} className="mb-10"> <div id={id} key={id} className="mb-10">
<OracleDetails <OracleDetails
@ -44,7 +50,7 @@ export const Oracle = () => {
) : ( ) : (
<span></span> <span></span>
)} )}
</RenderFetched> </AsyncRenderer>
</section> </section>
); );
}; };

View File

@ -48,6 +48,13 @@ query ExplorerPartyAssets($partyId: ID!) {
} }
stakingSummary { stakingSummary {
currentStakeAvailable currentStakeAvailable
linkings(pagination: { first: 100 }) {
edges {
node {
amount
}
}
}
} }
accountsConnection { accountsConnection {
edges { edges {

View File

@ -10,7 +10,7 @@ export type ExplorerPartyAssetsQueryVariables = Types.Exact<{
}>; }>;
export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null }; export type ExplorerPartyAssetsQuery = { __typename?: 'Query', partiesConnection?: { __typename?: 'PartyConnection', edges: Array<{ __typename?: 'PartyEdge', node: { __typename?: 'Party', id: string, delegationsConnection?: { __typename?: 'DelegationsConnection', edges?: Array<{ __typename?: 'DelegationEdge', node: { __typename?: 'Delegation', amount: string, epoch: number, node: { __typename?: 'Node', id: string, name: string } } } | null> | null } | null, stakingSummary: { __typename?: 'StakingSummary', currentStakeAvailable: string, linkings: { __typename?: 'StakesConnection', edges?: Array<{ __typename?: 'StakeLinkingEdge', node: { __typename?: 'StakeLinking', amount: string } } | null> | null } }, accountsConnection?: { __typename?: 'AccountsConnection', edges?: Array<{ __typename?: 'AccountEdge', node: { __typename?: 'AccountBalance', type: Types.AccountType, balance: string, asset: { __typename?: 'Asset', name: string, id: string, decimals: number, symbol: string, source: { __typename: 'BuiltinAsset' } | { __typename: 'ERC20', contractAddress: string } }, market?: { __typename?: 'Market', id: string, decimalPlaces: number, tradableInstrument: { __typename?: 'TradableInstrument', instrument: { __typename?: 'Instrument', name: string, product: { __typename?: 'Future', quoteName: string } } } } | null } } | null> | null } | null } }> } | null };
export const ExplorerPartyAssetsAccountsFragmentDoc = gql` export const ExplorerPartyAssetsAccountsFragmentDoc = gql`
fragment ExplorerPartyAssetsAccounts on AccountBalance { fragment ExplorerPartyAssetsAccounts on AccountBalance {
@ -64,6 +64,13 @@ export const ExplorerPartyAssetsDocument = gql`
} }
stakingSummary { stakingSummary {
currentStakeAvailable currentStakeAvailable
linkings(pagination: {first: 100}) {
edges {
node {
amount
}
}
}
} }
accountsConnection { accountsConnection {
edges { edges {

View File

@ -0,0 +1,24 @@
import { t } from '@vegaprotocol/i18n';
import { useParams } from 'react-router-dom';
import { toNonHex } from '../../../../components/search/detect-search';
import { PageHeader } from '../../../../components/page-header';
import { useDocumentTitle } from '../../../../hooks/use-document-title';
import { PartyAccounts } from '../components/party-accounts';
const PartyAccountsByAsset = () => {
const { party } = useParams<{ party: string }>();
useDocumentTitle(['Public keys', party || '-']);
const partyId = toNonHex(party ? party : '');
return (
<section>
<PageHeader title={t('Balances by asset')} />
<PartyAccounts partyId={partyId} />
</section>
);
};
export { PartyAccountsByAsset };

View File

@ -1,32 +1,9 @@
import { t } from '@vegaprotocol/i18n'; import { AccountManager } from '@vegaprotocol/accounts';
import get from 'lodash/get'; import { useCallback } from 'react';
import AssetBalance from '../../../../components/asset-balance/asset-balance'; import { useAssetDetailsDialogStore } from '@vegaprotocol/assets';
import { AssetLink, MarketLink } from '../../../../components/links';
import { Table, TableRow } from '../../../../components/table';
import type * as Schema from '@vegaprotocol/types';
import type { ExplorerPartyAssetsAccountsFragment } from '../__generated__/Party-assets';
const accountTypeString: Record<Schema.AccountType, string> = {
ACCOUNT_TYPE_BOND: t('Bond'),
ACCOUNT_TYPE_EXTERNAL: t('External'),
ACCOUNT_TYPE_FEES_INFRASTRUCTURE: t('Fees (Infrastructure)'),
ACCOUNT_TYPE_FEES_LIQUIDITY: t('Fees (Liquidity)'),
ACCOUNT_TYPE_FEES_MAKER: t('Fees (Maker)'),
ACCOUNT_TYPE_GENERAL: t('General'),
ACCOUNT_TYPE_GLOBAL_INSURANCE: t('Global Insurance Pool'),
ACCOUNT_TYPE_GLOBAL_REWARD: t('Global Reward Pool'),
ACCOUNT_TYPE_INSURANCE: t('Insurance'),
ACCOUNT_TYPE_MARGIN: t('Margin'),
ACCOUNT_TYPE_PENDING_TRANSFERS: t('Pending Transfers'),
ACCOUNT_TYPE_REWARD_LP_RECEIVED_FEES: t('Reward - LP Fees received'),
ACCOUNT_TYPE_REWARD_MAKER_PAID_FEES: t('Reward - Maker fees paid'),
ACCOUNT_TYPE_REWARD_MAKER_RECEIVED_FEES: t('Reward - Maker fees received'),
ACCOUNT_TYPE_REWARD_MARKET_PROPOSERS: t('Reward - Market proposers'),
ACCOUNT_TYPE_SETTLEMENT: t('Settlement'),
};
interface PartyAccountsProps { interface PartyAccountsProps {
accounts: ExplorerPartyAssetsAccountsFragment[]; partyId: string;
} }
/** /**
@ -34,49 +11,22 @@ interface PartyAccountsProps {
* probably do with sorting by asset, and then within asset, by type with general * probably do with sorting by asset, and then within asset, by type with general
* appearing first and... tbd * appearing first and... tbd
*/ */
export const PartyAccounts = ({ accounts }: PartyAccountsProps) => { export const PartyAccounts = ({ partyId }: PartyAccountsProps) => {
return ( const { open: openAssetDetailsDialog } = useAssetDetailsDialogStore();
<Table className="max-w-5xl min-w-fit"> const onClickAsset = useCallback(
<thead> (assetId?: string) => {
<TableRow modifier="bordered" className="font-mono"> assetId && openAssetDetailsDialog(assetId);
<td>{t('Type')}</td> },
<td>{t('Market')}</td> [openAssetDetailsDialog]
<td className="text-right pr-2">{t('Balance')}</td> );
<td>{t('Asset')}</td>
</TableRow>
</thead>
<tbody>
{accounts.map((account) => {
const m = get(account, 'market.tradableInstrument.instrument.name');
return ( return (
<TableRow <div className="block min-h-44 h-60 4 w-full border-red-800 relative">
key={`pa-${account.asset.id}-${account.type}`} <AccountManager
title={account.asset.name} partyId={partyId}
id={`${accountTypeString[account.type]} ${m ? ` - ${m}` : ''}`} onClickAsset={onClickAsset}
> isReadOnly={true}
<td className="text-md">{accountTypeString[account.type]}</td> />
<td className="text-md"> </div>
{account.market?.id ? (
<MarketLink id={account.market?.id} />
) : (
<p>-</p>
)}
</td>
<td className="text-md text-right pr-2">
<AssetBalance
assetId={account.asset.id}
price={account.balance}
showAssetLink={false}
/>
</td>
<td className="text-md">
<AssetLink assetId={account.asset.id} asDialog={true} />
</td>
</TableRow>
);
})}
</tbody>
</Table>
); );
}; };

View File

@ -0,0 +1,63 @@
import { t } from '@vegaprotocol/i18n';
import { useNavigate } from 'react-router-dom';
import { Routes } from '../../../../routes/route-names';
import { Button, Icon, Loader } from '@vegaprotocol/ui-toolkit';
import { PartyBlock } from './party-block';
import type { AccountFields } from '@vegaprotocol/accounts';
export interface PartyBlockAccountProps {
partyId: string;
accountData: AccountFields[] | null;
accountLoading: boolean;
accountError?: Error;
}
/**
* Displays an overview of a party's assets. This uses existing data
* providers to structure the details by asset, rather than looking at
* it by account. The assumption is that this is a more natural way to
* get an idea of the assets and activity of a party.
*/
export const PartyBlockAccounts = ({
partyId,
accountData,
accountLoading,
accountError,
}: PartyBlockAccountProps) => {
const navigate = useNavigate();
const shouldShowActionButton =
accountData && accountData.length > 0 && !accountLoading && !accountError;
const action = shouldShowActionButton ? (
<Button
size="sm"
onClick={() => navigate(`/${Routes.PARTIES}/${partyId}/assets`)}
>
{t('Show all')}
</Button>
) : null;
return (
<PartyBlock title={t('Assets')} action={action}>
{accountData && accountData.length > 0 ? (
<p>
{accountData.length} {t('assets, including')}{' '}
{accountData
.map((a) => a.asset.symbol)
.slice(0, 3)
.join(', ')}
</p>
) : accountLoading && !accountError ? (
<Loader size="small" />
) : accountData && accountData.length === 0 ? (
<p>{t('No accounts found')}</p>
) : (
<p>
<Icon className="mr-1" name="error" />
<span className="text-sm">{t('Could not load assets')}</span>
</p>
)}
</PartyBlock>
);
};

View File

@ -0,0 +1,83 @@
import { t } from '@vegaprotocol/i18n';
import { useExplorerPartyAssetsQuery } from '../__generated__/Party-assets';
import GovernanceAssetBalance from '../../../../components/asset-balance/governance-asset-balance';
import {
Icon,
KeyValueTable,
KeyValueTableRow,
Loader,
} from '@vegaprotocol/ui-toolkit';
import { PartyBlock } from './party-block';
import BigNumber from 'bignumber.js';
export interface PartyBlockStakeProps {
partyId: string;
accountLoading: boolean;
accountError?: Error;
}
/**
* Displays an overview of a single party's staking balance, importantly maintaining'
* the same height before and after the details are loaded in.
*
* Unlike PartyBlockAccounts there is not action button in the title of this block as
* there is no page for it to link to currently. That's a future task.
*/
export const PartyBlockStake = ({
partyId,
accountLoading,
accountError,
}: PartyBlockStakeProps) => {
const partyRes = useExplorerPartyAssetsQuery({
// Don't cache data for this query, party information can move quite quickly
fetchPolicy: 'network-only',
variables: { partyId: partyId },
skip: !partyId,
});
const p = partyRes.data?.partiesConnection?.edges[0].node;
const linkedLength = p?.stakingSummary?.linkings?.edges?.length;
const linkedStake =
linkedLength && linkedLength > 0
? p?.stakingSummary?.linkings?.edges
?.reduce((total, e) => {
return new BigNumber(total).plus(
new BigNumber(e?.node.amount || 0)
);
}, new BigNumber(0))
.toString()
: '0';
return (
<PartyBlock title={t('Staking')}>
{p?.stakingSummary.currentStakeAvailable ? (
<KeyValueTable>
<KeyValueTableRow noBorder={true}>
<div>{t('Available stake')}</div>
<div>
<GovernanceAssetBalance
price={p.stakingSummary.currentStakeAvailable}
/>
</div>
</KeyValueTableRow>
<KeyValueTableRow noBorder={true}>
<div>{t('Active stake')}</div>
<div>
<GovernanceAssetBalance price={linkedStake || '0'} />
</div>
</KeyValueTableRow>
</KeyValueTable>
) : accountLoading && !accountError ? (
<Loader size="small" />
) : !accountError ? (
<p>{t('No staking balance')}</p>
) : (
<p>
<Icon className="mr-1" name="error" />
<span className="text-sm">{t('Could not load stake details')}</span>
</p>
)}
</PartyBlock>
);
};

View File

@ -0,0 +1,24 @@
import type { ReactNode } from 'react';
export interface PartyBlockProps {
children: ReactNode;
title: string;
action?: ReactNode;
}
export function PartyBlock({ children, title, action }: PartyBlockProps) {
return (
<div className="border-2 min-h-[138px] border-solid border-vega-light-100 dark:border-vega-dark-200 p-5 mt-5">
<div
className="flex flex-col md:flex-row gap-1 justify-between content-start mb-2"
data-testid="page-title"
>
<h3 className="font-semibold text-lg">{title}</h3>
{action ? action : null}
</div>
{children}
</div>
);
}

View File

@ -1,24 +1,28 @@
import { getNodes } from '@vegaprotocol/utils';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { useScreenDimensions } from '@vegaprotocol/react-helpers'; import {
useDataProvider,
useScreenDimensions,
} from '@vegaprotocol/react-helpers';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useParams } from 'react-router-dom'; import { useNavigate, useParams } from 'react-router-dom';
import { SubHeading } from '../../../components/sub-heading'; import { SubHeading } from '../../../components/sub-heading';
import { Panel } from '../../../components/panel';
import { toNonHex } from '../../../components/search/detect-search'; import { toNonHex } from '../../../components/search/detect-search';
import { useTxsData } from '../../../hooks/use-txs-data'; import { useTxsData } from '../../../hooks/use-txs-data';
import { TxsInfiniteList } from '../../../components/txs'; import { TxsInfiniteList } from '../../../components/txs';
import { PageHeader } from '../../../components/page-header'; import { PageHeader } from '../../../components/page-header';
import { useExplorerPartyAssetsQuery } from './__generated__/Party-assets';
import type { ExplorerPartyAssetsAccountsFragment } from './__generated__/Party-assets';
import { useDocumentTitle } from '../../../hooks/use-document-title'; import { useDocumentTitle } from '../../../hooks/use-document-title';
import GovernanceAssetBalance from '../../../components/asset-balance/governance-asset-balance'; import { Icon, Intent, Notification, Splash } from '@vegaprotocol/ui-toolkit';
import { PartyAccounts } from './components/party-accounts'; import { aggregatedAccountsDataProvider } from '@vegaprotocol/accounts';
import { PartyBlockStake } from './components/party-block-stake';
import { PartyBlockAccounts } from './components/party-block-accounts';
import { isValidPartyId } from './components/party-id-error';
const Party = () => { const Party = () => {
const { party } = useParams<{ party: string }>(); const { party } = useParams<{ party: string }>();
useDocumentTitle(['Public keys', party || '-']); useDocumentTitle(['Public keys', party || '-']);
const navigate = useNavigate();
const partyId = toNonHex(party ? party : ''); const partyId = toNonHex(party ? party : '');
const { isMobile } = useScreenDimensions(); const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]); const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
@ -28,71 +32,72 @@ const Party = () => {
filters, filters,
}); });
const partyRes = useExplorerPartyAssetsQuery({ const variables = useMemo(() => ({ partyId }), [partyId]);
// Don't cache data for this query, party information can move quite quickly const {
fetchPolicy: 'network-only', data: AccountData,
variables: { partyId: partyId }, loading: AccountLoading,
skip: !party, error: AccountError,
} = useDataProvider({
dataProvider: aggregatedAccountsDataProvider,
variables,
}); });
const p = partyRes.data?.partiesConnection?.edges[0].node; if (!isValidPartyId(partyId)) {
return (
const header = p?.id ? ( <div className="max-w-sm mx-auto">
<PageHeader <Notification
title={p.id} message={t('Invalid party ID')}
copy intent={Intent.Danger}
truncateStart={visibleChars} buttonProps={{
truncateEnd={visibleChars} text: t('Go back'),
/> action: () => navigate(-1),
) : ( className: 'py-1',
<Panel> size: 'sm',
<p>No data found for public key {party}</p> }}
</Panel> />
); </div>
);
const staking = ( }
<section>
{p?.stakingSummary?.currentStakeAvailable ? (
<div className="mt-4 leading-3">
<strong className="font-semibold">{t('Staking Balance: ')}</strong>
<GovernanceAssetBalance
price={p.stakingSummary.currentStakeAvailable}
/>
</div>
) : null}
</section>
);
const accounts = getNodes<ExplorerPartyAssetsAccountsFragment>(
p?.accountsConnection
);
return ( return (
<section> <section>
<h1 <PageHeader
className="font-alpha calt uppercase font-xl mb-4 text-vega-dark-100 dark:text-vega-light-100" title={partyId}
data-testid="parties-header" copy
> truncateStart={visibleChars}
{t('Public key')} truncateEnd={visibleChars}
</h1> />
{partyRes.data ? (
<>
{header}
<SubHeading>{t('Asset data')}</SubHeading>
{accounts ? <PartyAccounts accounts={accounts} /> : null}
{staking}
<SubHeading>{t('Transactions')}</SubHeading> <div className="grid md:grid-flow-col grid-flow-row md:space-x-4 grid-cols-1 md:grid-cols-2 w-full">
<TxsInfiniteList <PartyBlockAccounts
hasMoreTxs={hasMoreTxs} accountError={AccountError}
areTxsLoading={loading} accountLoading={AccountLoading}
txs={txsData} accountData={AccountData}
loadMoreTxs={loadTxs} partyId={partyId}
error={error} />
className="mb-28" <PartyBlockStake
/> accountError={AccountError}
</> accountLoading={AccountLoading}
) : null} partyId={partyId}
/>
</div>
<SubHeading>{t('Transactions')}</SubHeading>
{!error && txsData ? (
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
areTxsLoading={loading}
txs={txsData}
loadMoreTxs={loadTxs}
error={error}
className="mb-28"
/>
) : (
<Splash>
<Icon name="error" className="mr-1" />
&nbsp;{t('Could not load transaction list for party')}
</Splash>
)}
</section> </section>
); );
}; };

View File

@ -0,0 +1,7 @@
import { Outlet } from 'react-router-dom';
const PartiesSubPage = () => {
return <Outlet />;
};
export default PartiesSubPage;

View File

@ -1,35 +0,0 @@
import React from 'react';
import { DATA_SOURCES } from '../../config';
import type { TendermintUnconfirmedTransactionsResponse } from '../txs/tendermint-unconfirmed-transactions-response.d';
import { TxList } from '../../components/txs';
import { RouteTitle } from '../../components/route-title';
import { t } from '@vegaprotocol/i18n';
import { useFetch } from '@vegaprotocol/react-helpers';
import { useDocumentTitle } from '../../hooks/use-document-title';
const PendingTxs = () => {
const {
state: { data: unconfirmedTransactions },
} = useFetch<TendermintUnconfirmedTransactionsResponse>(
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
);
useDocumentTitle(['Pending transactions']);
return (
<section>
<RouteTitle data-testid="unconfirmed-transactions-header">
{t('Unconfirmed transactions')}
</RouteTitle>
<br />
<div>{t(`Number: ${unconfirmedTransactions?.result?.n_txs || 0}`)}</div>
<br />
<div>
<br />
<TxList data={unconfirmedTransactions} />
</div>
</section>
);
};
export { PendingTxs };

View File

@ -14,7 +14,6 @@ import { Block } from './blocks/id';
import { Blocks } from './blocks/home'; import { Blocks } from './blocks/home';
import { Tx } from './txs/id'; import { Tx } from './txs/id';
import { TxsList } from './txs/home'; import { TxsList } from './txs/home';
import { PendingTxs } from './pending';
import flags from '../config/flags'; import flags from '../config/flags';
import { t } from '@vegaprotocol/i18n'; import { t } from '@vegaprotocol/i18n';
import { Routes } from './route-names'; import { Routes } from './route-names';
@ -29,6 +28,7 @@ import compact from 'lodash/compact';
import { AssetLink, MarketLink } from '../components/links'; import { AssetLink, MarketLink } from '../components/links';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit'; import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { remove0x } from '@vegaprotocol/utils'; import { remove0x } from '@vegaprotocol/utils';
import { PartyAccountsByAsset } from './parties/id/accounts';
export type Navigable = { export type Navigable = {
path: string; path: string;
@ -75,14 +75,43 @@ const partiesRoutes: Route[] = flags.parties
}, },
{ {
path: ':party', path: ':party',
element: <PartySingle />, element: <Party />,
handle: {
breadcrumb: (params: Params<string>) => ( children: [
<Link to={linkTo(Routes.PARTIES, params.party)}> {
{truncateMiddle(params.party as string)} index: true,
</Link> element: <PartySingle />,
), handle: {
}, breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
},
{
path: 'assets',
element: <Party />,
handle: {
breadcrumb: (params: Params<string>) => (
<Link to={linkTo(Routes.PARTIES, params.party)}>
{truncateMiddle(params.party as string)}
</Link>
),
},
children: [
{
index: true,
element: <PartyAccountsByAsset />,
handle: {
breadcrumb: () => {
return t('Assets');
},
},
},
],
},
],
}, },
], ],
}, },
@ -237,17 +266,6 @@ export const routerConfig: Route[] = [
breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>, breadcrumb: () => <Link to={Routes.TX}>{t('Transactions')}</Link>,
}, },
children: [ children: [
{
path: 'pending',
element: <PendingTxs />,
handle: {
breadcrumb: () => (
<Link to={linkTo(Routes.TX, 'pending')}>
{t('Pending transactions')}
</Link>
),
},
},
{ {
path: ':txHash', path: ':txHash',
element: <Tx />, element: <Tx />,

View File

@ -10,7 +10,7 @@ interface TxDetailsProps {
export const txDetailsTruncateLength = 30; export const txDetailsTruncateLength = 30;
export const TxDetails = ({ txData, pubKey, className }: TxDetailsProps) => { export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
if (!txData) { if (!txData) {
return <>{t('Awaiting Block Explorer transaction details')}</>; return <>{t('Awaiting Block Explorer transaction details')}</>;
} }

View File

@ -28,6 +28,7 @@ import {
} from '@vegaprotocol/environment'; } from '@vegaprotocol/environment';
import classNames from 'classnames'; import classNames from 'classnames';
import { NodeStatus, NodeStatusMapping } from '@vegaprotocol/types'; import { NodeStatus, NodeStatusMapping } from '@vegaprotocol/types';
import { PartyLink } from '../../components/links';
type RateProps = { type RateProps = {
value: BigNumber | number | undefined; value: BigNumber | number | undefined;
@ -191,7 +192,9 @@ export const ValidatorsPage = () => {
<KeyValueTable> <KeyValueTable>
<KeyValueTableRow> <KeyValueTableRow>
<div>{t('ID')}</div> <div>{t('ID')}</div>
<div className="break-all text-xs">{v.id}</div> <div className="break-all text-xs font-mono">
{v.id}
</div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
<div>{t('Status')}</div> <div>{t('Status')}</div>
@ -218,12 +221,14 @@ export const ValidatorsPage = () => {
</div> </div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
<div>{t('Public key')}</div> <div>{t('Key')}</div>
<div className="break-all text-xs">{v.pubkey}</div> <div className="break-all text-xs">
<PartyLink id={v.pubkey} />
</div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
<div>{t('Ethereum address')}</div> <div>{t('Ethereum address')}</div>
<div className="break-all text-xs"> <div className="break-all text-xs font-mono">
<EtherscanLink address={v.ethereumAddress} />{' '} <EtherscanLink address={v.ethereumAddress} />{' '}
<CopyWithTooltip text={v.ethereumAddress}> <CopyWithTooltip text={v.ethereumAddress}>
<button title={t('Copy address to clipboard')}> <button title={t('Copy address to clipboard')}>
@ -234,7 +239,9 @@ export const ValidatorsPage = () => {
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>
<div>{t('Tendermint public key')}</div> <div>{t('Tendermint public key')}</div>
<div className="break-all text-xs">{v.tmPubkey}</div> <div className="break-all text-xs font-mono">
{v.tmPubkey}
</div>
</KeyValueTableRow> </KeyValueTableRow>
<KeyValueTableRow> <KeyValueTableRow>

View File

@ -50,9 +50,7 @@ export const RichSelect = forwardRef<
const setWidth = () => { const setWidth = () => {
if (contentRef.current) { if (contentRef.current) {
contentRef.current.style.width = containerRef.current contentRef.current.style.width = containerRef.current ? `450px` : 'auto';
? `${containerRef.current.getBoundingClientRect().width}px`
: 'auto';
} }
}; };