feat: progress on tx list

This commit is contained in:
Edd
2023-07-04 11:19:28 +01:00
parent 67f2a7f478
commit f3f662cc2f
13 changed files with 202 additions and 106 deletions
+1
View File
@@ -14,6 +14,7 @@ import { RouterProvider } from 'react-router-dom';
import { router } from './routes/router-config';
import { t } from '@vegaprotocol/i18n';
import { Suspense } from 'react';
import { useExplorerNodeNamesLazyQuery } from './routes/validators/__generated__/NodeNames';
const splashLoading = (
<Splash>
@@ -5,18 +5,38 @@ import type { ComponentProps } from 'react';
import Hash from '../hash';
import { t } from '@vegaprotocol/i18n';
import { isValidPartyId } from '../../../routes/parties/id/components/party-id-error';
import { truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { Icon, truncateMiddle } from '@vegaprotocol/ui-toolkit';
import { useExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
import type { ExplorerNodeNamesQuery } from '../../../routes/validators/__generated__/NodeNames';
export const SPECIAL_CASE_NETWORK_ID =
'0000000000000000000000000000000000000000000000000000000000000000';
export const SPECIAL_CASE_NETWORK = 'network';
export function getNameForParty(id: string, data?: ExplorerNodeNamesQuery) {
if (!data || data?.nodesConnection?.edges?.length === 0) {
return id;
}
const validator = data.nodesConnection.edges?.find((e) => {
return e?.node.pubkey === id;
});
if (validator) {
return validator.node.name;
}
return id;
}
export type PartyLinkProps = Partial<ComponentProps<typeof Link>> & {
id: string;
truncate?: boolean;
};
const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
const { data } = useExplorerNodeNamesQuery();
// Some transactions will involve the 'network' party, which is alias for '000...000'
// The party page does not handle this nicely, so in this case we render the word 'Network'
if (id === SPECIAL_CASE_NETWORK || id === SPECIAL_CASE_NETWORK_ID) {
@@ -37,14 +57,20 @@ const PartyLink = ({ id, truncate = false, ...props }: PartyLinkProps) => {
);
}
const name = getNameForParty(id, data);
const useName = name !== id;
return (
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
<Hash text={truncate ? truncateMiddle(id) : id} />
</Link>
<span className="whitespace-nowrap">
{useName && <Icon size={4} name="cube" className="mr-2" />}
<Link
className="underline font-mono"
{...props}
to={`/${Routes.PARTIES}/${id}`}
>
{useName ? name : <Hash text={truncate ? truncateMiddle(id) : id} />}
</Link>
</span>
);
};
@@ -1,4 +1,4 @@
import { Icon } from '@vegaprotocol/ui-toolkit';
import { Icon, Tooltip } from '@vegaprotocol/ui-toolkit';
// https://github.com/vegaprotocol/vega/blob/develop/core/blockchain/response.go
export const ErrorCodes = new Map([
@@ -17,6 +17,8 @@ interface ChainResponseCodeProps {
code: number;
hideLabel?: boolean;
error?: string;
hideIfOk?: boolean;
small?: boolean;
}
/**
@@ -28,14 +30,21 @@ export const ChainResponseCode = ({
code,
hideLabel = false,
error,
hideIfOk = false,
small = false,
}: ChainResponseCodeProps) => {
if (hideIfOk && code === 0) {
return null;
}
const isSuccess = successCodes.has(code);
const size = small ? 3 : 4;
const successColour =
code === 71 ? 'fill-vega-orange' : 'fill-vega-green-600';
code === 71 ? '!fill-vega-orange' : '!fill-vega-green-600';
const icon = isSuccess ? (
<Icon name="tick-circle" className={successColour} />
<Icon size={size} name="tick-circle" className={`${successColour}`} />
) : (
<Icon name="cross" className="fill-vega-pink-600" />
<Icon size={size} name="cross" className="!fill-vega-pink-500" />
);
const label = ErrorCodes.get(code) || 'Unknown response code';
@@ -44,18 +53,28 @@ export const ChainResponseCode = ({
error && error.length > 100 ? error.replace(/,/g, ',\r\n') : error;
return (
<div title={`Response code: ${code} - ${label}`} className=" inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
role="img"
>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
{!hideLabel && !!displayError ? (
<span className="ml-1 whitespace-pre">&mdash;&nbsp;{displayError}</span>
) : null}
</div>
<Tooltip
description={
<span>
Response code: {code} - {label}
</span>
}
>
<div className="mt-[-1px] inline-block">
<span
className="mr-2"
aria-label={isSuccess ? 'Success' : 'Warning'}
role="img"
>
{icon}
</span>
{hideLabel ? null : <span>{label}</span>}
{!hideLabel && !!displayError ? (
<span className="ml-1 whitespace-pre">
&mdash;&nbsp;{displayError}
</span>
) : null}
</div>
</Tooltip>
);
};
@@ -36,9 +36,11 @@ const displayString: StringMap = {
UndelegateSubmission: 'Undelegation',
KeyRotateSubmission: 'Key Rotation',
StateVariableProposal: 'State Variable',
'State Variable Proposal': 'State Variable',
Transfer: 'Transfer',
CancelTransfer: 'Cancel Transfer',
ValidatorHeartbeat: 'Heartbeat',
'Validator Heartbeat': 'Heartbeat',
'Batch Market Instructions': 'Batch',
};
@@ -172,7 +174,7 @@ export const TxOrderType = ({ orderType, command }: TxOrderTypeProps) => {
return (
<div
data-testid="tx-type"
className={`text-sm rounded-md leading-none px-2 py-2 inline-block ${colours}`}
className={`text-sm rounded-md leading-tight px-2 inline-block whitespace-nowrap ${colours}`}
>
{type}
</div>
@@ -5,6 +5,7 @@ import type { BlockExplorerTransactionResult } from '../../routes/types/block-ex
import { toHex } from '../search/detect-search';
import { ChainResponseCode } from './details/chain-response-code/chain-reponse.code';
import isNumber from 'lodash/isNumber';
import { PartyLink } from '../links';
const TRUNCATE_LENGTH = 10;
@@ -30,15 +31,15 @@ export const TxsInfiniteListItem = ({
return (
<tr
data-testid="transaction-row"
className="flex items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item grid grid-cols-10 py-[2px]"
className="text-left items-center h-full border-t border-neutral-600 dark:border-neutral-800 txs-infinite-list-item py-[2px]"
>
<td
className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="tx-hash"
>
<span className="md:hidden uppercase text-vega-dark-300">
ID:&nbsp;
</span>
<td className="text-sm leading-none font-mono" data-testid="tx-hash">
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} hideIfOk={true} />
) : (
code
)}
<TruncatedLink
to={`/${Routes.TX}/${toHex(hash)}`}
text={hash}
@@ -46,30 +47,13 @@ export const TxsInfiniteListItem = ({
endChars={0}
/>
</td>
<td
className="text-sm col-span-10 md:col-span-3 leading-none"
data-testid="pub-key"
>
<span className="md:hidden uppercase text-vega-dark-300">
By:&nbsp;
</span>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={TRUNCATE_LENGTH}
endChars={TRUNCATE_LENGTH}
/>
<td className="text-sm leading-none" data-testid="pub-key">
<PartyLink truncate={true} id={submitter} />
</td>
<td className="text-sm col-span-5 md:col-span-2 leading-none flex items-center">
<td className="text-sm leading-none">
<TxOrderType orderType={type} command={command} />
</td>
<td
className="text-sm col-span-3 md:col-span-1 leading-none flex items-center"
data-testid="tx-block"
>
<span className="md:hidden uppercase text-vega-dark-300">
Block:&nbsp;
</span>
<td className="text-sm items-center font-mono" data-testid="tx-block">
<TruncatedLink
to={`/${Routes.BLOCKS}/${block}`}
text={block}
@@ -77,19 +61,6 @@ export const TxsInfiniteListItem = ({
endChars={TRUNCATE_LENGTH}
/>
</td>
<td
className="text-sm col-span-2 md:col-span-1 leading-none flex items-center"
data-testid="tx-success"
>
<span className="md:hidden uppercase text-vega-dark-300">
Success&nbsp;
</span>
{isNumber(code) ? (
<ChainResponseCode code={code} hideLabel={true} />
) : (
code
)}
</td>
</tr>
);
};
@@ -54,15 +54,14 @@ export const TxsInfiniteList = ({
return (
<table className={className} data-testid="transactions-list">
<thead>
<tr className="lg:grid grid-cols-10 w-full mb-3 hidden text-vega-dark-300 uppercase">
<th className="col-span-3">
<span className="hidden xl:inline">{t('Transaction')} &nbsp;</span>
<tr className="w-full mb-3 text-vega-dark-300 uppercase text-left">
<th>
<span className="hidden xl:inline">{t('Txn')} &nbsp;</span>
<span>ID</span>
</th>
<th className="col-span-3">{t('Submitted By')}</th>
<th className="col-span-2">{t('Type')}</th>
<th className="col-span-1">{t('Block')}</th>
<th className="col-span-1">{t('Success')}</th>
<th className="text-left">{t('From')}</th>
<th>{t('Type')}</th>
<th>{t('Block')}</th>
</tr>
</thead>
<tbody>
@@ -10,6 +10,7 @@ import { ChainResponseCode } from './details/chain-response-code/chain-reponse.c
import { getTxsDataUrl } from '../../hooks/use-txs-data';
import { AsyncRenderer, Loader } from '@vegaprotocol/ui-toolkit';
import EmptyList from '../empty-list/empty-list';
import { PartyLink } from '../links';
interface TxsPerBlockProps {
blockHeight: string;
@@ -62,12 +63,7 @@ export const TxsPerBlock = ({ blockHeight, txCount }: TxsPerBlockProps) => {
modifier="bordered"
className="pr-12 font-mono"
>
<TruncatedLink
to={`/${Routes.PARTIES}/${submitter}`}
text={submitter}
startChars={truncateLength}
endChars={truncateLength}
/>
<PartyLink id={submitter} truncate={true} />
</TableCell>
<TableCell modifier="bordered">
<TxOrderType orderType={type} command={command} />
+2 -16
View File
@@ -66,29 +66,15 @@ export const useTxsData = ({ limit, filters }: IUseTxsData) => {
useEffect(() => {
if (data && isNumber(data?.transactions?.length)) {
setTxsState((prev) => {
const cursor =
data.transactions[data.transactions.length - 1]?.cursor || '';
const previousCursor = prev.cursor;
return {
...prev,
cursor: data.transactions.at(-1)?.cursor || '',
txsData: data.transactions,
hasMoreTxs: data.transactions.length > 0,
previousCursor,
cursor,
};
});
}
}, [cursor, setTxsState, data]);
useEffect(() => {
l('', '', 'Initial reset');
setTxsState((prev) => ({
txsData: [],
hasMoreTxs: false,
cursor: '',
previousCursor: '',
}));
}, [filters, limit, refetch]);
const nextPage = useCallback(() => {
return refetch({
limit,
+3 -1
View File
@@ -21,6 +21,7 @@ import {
import { Footer } from '../components/footer/footer';
import { Header } from '../components/header';
import { Routes } from './route-names';
import { useExplorerNodeNamesLazyQuery } from './validators/__generated__/NodeNames';
const DialogsContainer = () => {
const { isOpen, id, trigger, asJson, setOpen } = useAssetDetailsDialogStore();
@@ -39,6 +40,7 @@ export const Layout = () => {
const isHome = Boolean(useMatch(Routes.HOME));
const { ANNOUNCEMENTS_CONFIG_URL } = useEnvironment();
const fixedWidthClasses = 'w-full max-w-[1500px] mx-auto';
const _ = useExplorerNodeNamesLazyQuery();
return (
<>
@@ -49,7 +51,7 @@ export const Layout = () => {
'grid grid-rows-[auto_1fr_auto] grid-cols-1',
'border-vega-light-200 dark:border-vega-dark-200',
'antialiased text-black dark:text-white',
'overflow-hidden relative'
'relative'
)}
>
<div>
@@ -8,12 +8,19 @@ import { useTxsData } from '../../../hooks/use-txs-data';
import { TxsInfiniteList } from '../../../components/txs';
import { PageHeader } from '../../../components/page-header';
import { useDocumentTitle } from '../../../hooks/use-document-title';
import { Icon, Intent, Notification, Splash } from '@vegaprotocol/ui-toolkit';
import {
Button,
Icon,
Intent,
Notification,
Splash,
} from '@vegaprotocol/ui-toolkit';
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';
import { useDataProvider } from '@vegaprotocol/data-provider';
import { TxsFilter } from '../../../components/txs/tx-filter';
const Party = () => {
const { party } = useParams<{ party: string }>();
@@ -25,10 +32,11 @@ const Party = () => {
const { isMobile } = useScreenDimensions();
const visibleChars = useMemo(() => (isMobile ? 10 : 14), [isMobile]);
const filters = `filters[tx.submitter]=${partyId}`;
const { hasMoreTxs, nextPage, error, txsData, loading } = useTxsData({
limit: 10,
filters,
});
const { hasMoreTxs, nextPage, previousPage, error, txsData, loading } =
useTxsData({
limit: 25,
filters,
});
const variables = useMemo(() => ({ partyId }), [partyId]);
const {
@@ -81,6 +89,26 @@ const Party = () => {
</div>
<SubHeading>{t('Transactions')}</SubHeading>
<menu className="mb-2">
<div className="right">
<Button
size="xs"
onClick={() => {
previousPage();
}}
>
Previous
</Button>
<Button
size="xs"
onClick={() => {
nextPage();
}}
>
Next
</Button>
</div>
</menu>
{!error && txsData ? (
<TxsInfiniteList
hasMoreTxs={hasMoreTxs}
@@ -88,7 +116,7 @@ const Party = () => {
txs={txsData}
loadMoreTxs={nextPage}
error={error}
className="mb-28"
className="mb-28 w-full"
/>
) : (
<Splash>
@@ -9,7 +9,7 @@ import { useState } from 'react';
import { AllFilterOptions, TxsFilter } from '../../../components/txs/tx-filter';
import { Button } from '@vegaprotocol/ui-toolkit';
const BE_TXS_PER_REQUEST = 15;
const BE_TXS_PER_REQUEST = 25;
export const TxsList = () => {
useDocumentTitle(['Transactions']);
@@ -73,7 +73,7 @@ export const TxsListFiltered = () => {
txs={txsData}
loadMoreTxs={nextPage}
error={error}
className="mb-28"
className="mb-28 w-full min-w-[400px]"
/>
</>
);
@@ -0,0 +1,13 @@
query ExplorerNodeNames {
nodesConnection {
edges {
node {
id
name
pubkey
tmPubkey
ethereumAddress
}
}
}
}
@@ -0,0 +1,53 @@
import * as Types from '@vegaprotocol/types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
const defaultOptions = {} as const;
export type ExplorerNodeNamesQueryVariables = Types.Exact<{ [key: string]: never; }>;
export type ExplorerNodeNamesQuery = { __typename?: 'Query', nodesConnection: { __typename?: 'NodesConnection', edges?: Array<{ __typename?: 'NodeEdge', node: { __typename?: 'Node', id: string, name: string, pubkey: string, tmPubkey: string, ethereumAddress: string } } | null> | null } };
export const ExplorerNodeNamesDocument = gql`
query ExplorerNodeNames {
nodesConnection {
edges {
node {
id
name
pubkey
tmPubkey
ethereumAddress
}
}
}
}
`;
/**
* __useExplorerNodeNamesQuery__
*
* To run a query within a React component, call `useExplorerNodeNamesQuery` and pass it any options that fit your needs.
* When your component renders, `useExplorerNodeNamesQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useExplorerNodeNamesQuery({
* variables: {
* },
* });
*/
export function useExplorerNodeNamesQuery(baseOptions?: Apollo.QueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
}
export function useExplorerNodeNamesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>(ExplorerNodeNamesDocument, options);
}
export type ExplorerNodeNamesQueryHookResult = ReturnType<typeof useExplorerNodeNamesQuery>;
export type ExplorerNodeNamesLazyQueryHookResult = ReturnType<typeof useExplorerNodeNamesLazyQuery>;
export type ExplorerNodeNamesQueryResult = Apollo.QueryResult<ExplorerNodeNamesQuery, ExplorerNodeNamesQueryVariables>;