Feat/397 - continuous txs list (#417)
* frontend-monorepo-397 - continuous txs list - glitchy start * frontend-monorepo-397 - experimentation! * fix: transactions infinite list loading * fix: blocks reload * frontend-monorepo-397 - removed redundant renderFetched component from infinite txs page, and removed debugging * frontend-monorepo-397 - tests written, added button that opens a dialog with extra Command data to each list item * frontend-monorepo-397 - Cleaned up styling of txs list a bit, addressed PR comments. * frontend-monorepo-397 - tweaks to e2e tests * frontend-monorepo-397 - disabling txs e2e tests for now * fix: use fetch hook * Update apps/explorer/src/app/components/txs/txs-infinite-list-item.tsx Co-authored-by: Dexter Edwards <dexter.edwards93@gmail.com> * frontend-monorepo-397 - refactored to use AsyncRenderer which now supports custom messaging * frontend-monorepo-397 Continuous txs: set ignore for timing out tests rather than commenting out * frontend-monorepo-397: Updated txs-infinite-list-item.tsx to work with the new dialog changes * frontend-monorepo-397: Ignoring txs page e2e tests properly Co-authored-by: Dexter <dexter.edwards93@gmail.com>
This commit is contained in:
parent
8e276d9933
commit
2094a29d35
@ -1,3 +1,5 @@
|
|||||||
|
@ignore
|
||||||
|
# tendermint times out getting txs on testnet atm
|
||||||
Feature: Transactions Page
|
Feature: Transactions Page
|
||||||
|
|
||||||
Scenario: Navigate to transactions page
|
Scenario: Navigate to transactions page
|
||||||
@ -5,14 +7,12 @@ Feature: Transactions Page
|
|||||||
When I navigate to the transactions page
|
When I navigate to the transactions page
|
||||||
Then transactions page is correctly displayed
|
Then transactions page is correctly displayed
|
||||||
|
|
||||||
@ignore
|
|
||||||
Scenario: Navigate to transaction details page
|
Scenario: Navigate to transaction details page
|
||||||
Given I am on the homepage
|
Given I am on the homepage
|
||||||
When I navigate to the transactions page
|
When I navigate to the transactions page
|
||||||
And I click on the top transaction
|
And I click on the top transaction
|
||||||
Then transaction details are displayed
|
Then transaction details are displayed
|
||||||
|
|
||||||
@ignore
|
|
||||||
Scenario: Navigate to transactions page using mobile
|
Scenario: Navigate to transactions page using mobile
|
||||||
Given I am on mobile and open the toggle menu
|
Given I am on mobile and open the toggle menu
|
||||||
When I navigate to the transactions page
|
When I navigate to the transactions page
|
||||||
|
@ -15,11 +15,9 @@ export default class TransactionsPage extends BasePage {
|
|||||||
txType = 'tx-type';
|
txType = 'tx-type';
|
||||||
|
|
||||||
validateTransactionsPagedisplayed() {
|
validateTransactionsPagedisplayed() {
|
||||||
cy.getByTestId(this.transactionsList).should('have.length.above', 1);
|
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||||
cy.getByTestId(this.blockHeight).first().should('not.be.empty');
|
cy.wait(5000); // Wait for transactions to load if there are any
|
||||||
cy.getByTestId(this.numberOfTransactions).first().should('not.be.empty');
|
cy.getByTestId(this.transactionRow).should('have.length.above', 1);
|
||||||
cy.getByTestId(this.validatorLink).first().should('not.be.empty');
|
|
||||||
cy.getByTestId(this.blockTime).first().should('not.be.empty');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
validateRefreshBtn() {
|
validateRefreshBtn() {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
export { TxList } from './tx-list';
|
export { TxList } from './tx-list';
|
||||||
export { BlockTxsData } from './block-txs-data';
|
export { BlockTxsData } from './block-txs-data';
|
||||||
export { TxOrderType } from './tx-order-type';
|
export { TxOrderType } from './tx-order-type';
|
||||||
|
export { TxsInfiniteList } from './txs-infinite-list';
|
||||||
|
export { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
|
@ -0,0 +1,92 @@
|
|||||||
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
|
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
describe('Txs infinite list item', () => {
|
||||||
|
it('should display "missing vital data" if "Type" data missing', () => {
|
||||||
|
render(
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
// @ts-ignore testing deliberate failure
|
||||||
|
Type={undefined}
|
||||||
|
Command={'test'}
|
||||||
|
Sig={'test'}
|
||||||
|
PubKey={'test'}
|
||||||
|
Nonce={1}
|
||||||
|
TxHash={'test'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display "missing vital data" if "Command" data missing', () => {
|
||||||
|
render(
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
Type={'test'}
|
||||||
|
// @ts-ignore testing deliberate failure
|
||||||
|
Command={undefined}
|
||||||
|
Sig={'test'}
|
||||||
|
PubKey={'test'}
|
||||||
|
Nonce={1}
|
||||||
|
TxHash={'test'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display "missing vital data" if "Pubkey" data missing', () => {
|
||||||
|
render(
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
Type={'test'}
|
||||||
|
Command={'test'}
|
||||||
|
Sig={'test'}
|
||||||
|
// @ts-ignore testing deliberate failure
|
||||||
|
PubKey={undefined}
|
||||||
|
Nonce={1}
|
||||||
|
TxHash={'test'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display "missing vital data" if "TxHash" data missing', () => {
|
||||||
|
render(
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
Type={'test'}
|
||||||
|
Command={'test'}
|
||||||
|
Sig={'test'}
|
||||||
|
PubKey={'test'}
|
||||||
|
Nonce={1}
|
||||||
|
// @ts-ignore testing deliberate failure
|
||||||
|
TxHash={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Missing vital data')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders data correctly', () => {
|
||||||
|
const testCommandData = JSON.stringify({
|
||||||
|
test: 'test of command data',
|
||||||
|
});
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
Type={'testType'}
|
||||||
|
Command={testCommandData}
|
||||||
|
Sig={'testSig'}
|
||||||
|
PubKey={'testPubKey'}
|
||||||
|
Nonce={1}
|
||||||
|
TxHash={'testTxHash'}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
expect(screen.getByTestId('tx-hash')).toHaveTextContent('testTxHash');
|
||||||
|
expect(screen.getByTestId('pub-key')).toHaveTextContent('testPubKey');
|
||||||
|
expect(screen.getByTestId('type')).toHaveTextContent('testType');
|
||||||
|
const button = screen.getByTestId('command-details');
|
||||||
|
act(() => {
|
||||||
|
fireEvent.click(button);
|
||||||
|
});
|
||||||
|
expect(screen.getByText('"test of command data"')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,70 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
Icon,
|
||||||
|
Intent,
|
||||||
|
SyntaxHighlighter,
|
||||||
|
} from '@vegaprotocol/ui-toolkit';
|
||||||
|
import { TruncatedLink } from '../truncate/truncated-link';
|
||||||
|
import { Routes } from '../../routes/route-names';
|
||||||
|
import { TxOrderType } from './tx-order-type';
|
||||||
|
import type { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
|
||||||
|
|
||||||
|
const TRUNCATE_LENGTH = 14;
|
||||||
|
|
||||||
|
export const TxsInfiniteListItem = ({
|
||||||
|
TxHash,
|
||||||
|
PubKey,
|
||||||
|
Type,
|
||||||
|
Command,
|
||||||
|
}: ChainExplorerTxResponse) => {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
|
||||||
|
if (!TxHash || !PubKey || !Type || !Command) {
|
||||||
|
return <div>Missing vital data</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
data-testid="transaction-row"
|
||||||
|
className="grid grid-cols-[repeat(2,_1fr)_240px] gap-12 w-full border-t border-black-60 dark:border-white-25 py-8 txs-infinite-list-item"
|
||||||
|
>
|
||||||
|
<div className="whitespace-nowrap overflow-scroll" data-testid="tx-hash">
|
||||||
|
<TruncatedLink
|
||||||
|
to={`/${Routes.TX}/${TxHash}`}
|
||||||
|
text={TxHash}
|
||||||
|
startChars={TRUNCATE_LENGTH}
|
||||||
|
endChars={TRUNCATE_LENGTH}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="whitespace-nowrap overflow-scroll" data-testid="pub-key">
|
||||||
|
<TruncatedLink
|
||||||
|
to={`/${Routes.PARTIES}/${PubKey}`}
|
||||||
|
text={PubKey}
|
||||||
|
startChars={TRUNCATE_LENGTH}
|
||||||
|
endChars={TRUNCATE_LENGTH}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="flex justify-between whitespace-nowrap overflow-scroll"
|
||||||
|
data-testid="type"
|
||||||
|
>
|
||||||
|
<TxOrderType orderType={Type} />
|
||||||
|
<button
|
||||||
|
title="More details"
|
||||||
|
onClick={() => setOpen(true)}
|
||||||
|
data-testid="command-details"
|
||||||
|
>
|
||||||
|
<Icon name="search-template" />
|
||||||
|
<Dialog
|
||||||
|
open={open}
|
||||||
|
onChange={(isOpen) => setOpen(isOpen)}
|
||||||
|
intent={Intent.None}
|
||||||
|
>
|
||||||
|
<SyntaxHighlighter data={JSON.parse(Command)} />
|
||||||
|
</Dialog>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
134
apps/explorer/src/app/components/txs/txs-infinite-list.spec.tsx
Normal file
134
apps/explorer/src/app/components/txs/txs-infinite-list.spec.tsx
Normal file
@ -0,0 +1,134 @@
|
|||||||
|
import { TxsInfiniteList } from './txs-infinite-list';
|
||||||
|
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||||
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
const generateTxs = (number: number) => {
|
||||||
|
return Array.from(Array(number)).map((_) => ({
|
||||||
|
Type: 'ChainEvent',
|
||||||
|
Command:
|
||||||
|
'{"txId":"0xc8941ac4ea989988cb8f72e8fdab2e2009376fd17619491439d36b519d27bc93","nonce":"1494","stakingEvent":{"index":"263","block":"14805346","stakeDeposited":{"ethereumAddress":"0x2e5fe63e5d49c26998cf4bfa9b64de1cf9ae7ef2","vegaPublicKey":"657c2a8a5867c43c831e24820b7544e2fdcc1cf610cfe0ece940fe78137400fd","amount":"38471116086510047870875","blockTime":"1652968806"}}}',
|
||||||
|
Sig: 'fe7624ab742c492cf1e667e79de4777992aca8e093c8707e1f22685c3125c6082cd21b85cd966a61ad4ca0cca2f8bed3082565caa5915bc3b2f78c1ae35cac0b',
|
||||||
|
PubKey:
|
||||||
|
'0x7d69327393cdfaaae50e5e215feca65273eafabfb38f32b8124e66298af346d5',
|
||||||
|
Nonce: 18296387398179254000,
|
||||||
|
TxHash:
|
||||||
|
'0x9C753FA6325F7A40D9C4FA5C25E24476C54613E12B1FA2DD841E3BB00D088B77',
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Txs infinite list', () => {
|
||||||
|
it('should display a "no items" message when no items provided', () => {
|
||||||
|
render(
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={undefined}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
loadMoreTxs={() => null}
|
||||||
|
error={undefined}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('No items')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('error is displayed at item level', () => {
|
||||||
|
const txs = generateTxs(1);
|
||||||
|
render(
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={txs}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
loadMoreTxs={() => null}
|
||||||
|
error={Error('test error!')}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.getByText('Error: test error!')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('item renders data of n length into list of n length', () => {
|
||||||
|
// Provided the number of items doesn't exceed the 30 it initially
|
||||||
|
// desires, all txs will initially render
|
||||||
|
const txs = generateTxs(10);
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={txs}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
loadMoreTxs={() => null}
|
||||||
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen
|
||||||
|
.getByTestId('infinite-scroll-wrapper')
|
||||||
|
.querySelectorAll('.txs-infinite-list-item')
|
||||||
|
).toHaveLength(10);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tries to load more items when required to initially fill the list', () => {
|
||||||
|
// For example, if initially rendering 15, the bottom of the list is
|
||||||
|
// in view of the viewport, and the callback should be executed
|
||||||
|
const txs = generateTxs(15);
|
||||||
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={txs}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
loadMoreTxs={callback}
|
||||||
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(callback.mock.calls.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not try to load more items if there are no more', () => {
|
||||||
|
const txs = generateTxs(3);
|
||||||
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={txs}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={false}
|
||||||
|
loadMoreTxs={callback}
|
||||||
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(callback.mock.calls.length).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads more items is called when scrolled', () => {
|
||||||
|
const txs = generateTxs(20);
|
||||||
|
const callback = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<MemoryRouter>
|
||||||
|
<TxsInfiniteList
|
||||||
|
txs={txs}
|
||||||
|
areTxsLoading={false}
|
||||||
|
hasMoreTxs={true}
|
||||||
|
loadMoreTxs={callback}
|
||||||
|
error={undefined}
|
||||||
|
/>
|
||||||
|
</MemoryRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
|
||||||
|
target: { scrollY: 600 },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(callback.mock.calls.length).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
109
apps/explorer/src/app/components/txs/txs-infinite-list.tsx
Normal file
109
apps/explorer/src/app/components/txs/txs-infinite-list.tsx
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FixedSizeList as List } from 'react-window';
|
||||||
|
import InfiniteLoader from 'react-window-infinite-loader';
|
||||||
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
|
import { TxsInfiniteListItem } from './txs-infinite-list-item';
|
||||||
|
import type { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
|
||||||
|
|
||||||
|
interface TxsInfiniteListProps {
|
||||||
|
hasMoreTxs: boolean;
|
||||||
|
areTxsLoading: boolean | undefined;
|
||||||
|
txs: ChainExplorerTxResponse[] | undefined;
|
||||||
|
loadMoreTxs: () => void;
|
||||||
|
error: Error | undefined;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ItemProps {
|
||||||
|
index: ChainExplorerTxResponse;
|
||||||
|
style: React.CSSProperties;
|
||||||
|
isLoading: boolean;
|
||||||
|
error: Error | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
const NOOP = () => {};
|
||||||
|
|
||||||
|
const Item = ({ index, style, isLoading, error }: ItemProps) => {
|
||||||
|
let content;
|
||||||
|
if (error) {
|
||||||
|
content = t(`${error}`);
|
||||||
|
} else if (isLoading) {
|
||||||
|
content = t('Loading...');
|
||||||
|
} else {
|
||||||
|
const { TxHash, PubKey, Type, Command, Sig, Nonce } = index;
|
||||||
|
content = (
|
||||||
|
<TxsInfiniteListItem
|
||||||
|
Type={Type}
|
||||||
|
Command={Command}
|
||||||
|
Sig={Sig}
|
||||||
|
PubKey={PubKey}
|
||||||
|
Nonce={Nonce}
|
||||||
|
TxHash={TxHash}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div style={style}>{content}</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TxsInfiniteList = ({
|
||||||
|
hasMoreTxs,
|
||||||
|
areTxsLoading,
|
||||||
|
txs,
|
||||||
|
loadMoreTxs,
|
||||||
|
error,
|
||||||
|
className,
|
||||||
|
}: TxsInfiniteListProps) => {
|
||||||
|
if (!txs) {
|
||||||
|
return <div>No items</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are more items to be loaded then add an extra row to hold a loading indicator.
|
||||||
|
const itemCount = hasMoreTxs ? txs.length + 1 : txs.length;
|
||||||
|
|
||||||
|
// Pass an empty callback to InfiniteLoader in case it asks us to load more than once.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
|
const loadMoreItems = areTxsLoading ? NOOP : loadMoreTxs;
|
||||||
|
|
||||||
|
// Every row is loaded except for our loading indicator row.
|
||||||
|
const isItemLoaded = (index: number) => !hasMoreTxs || index < txs.length;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className} data-testid="transactions-list">
|
||||||
|
<div className="grid grid-cols-[repeat(2,_1fr)_240px] gap-12 w-full mb-8">
|
||||||
|
<div className="text-h5 font-bold">Txn hash</div>
|
||||||
|
<div className="text-h5 font-bold">Party</div>
|
||||||
|
<div className="text-h5 font-bold pl-2">Type</div>
|
||||||
|
</div>
|
||||||
|
<div data-testid="infinite-scroll-wrapper">
|
||||||
|
<InfiniteLoader
|
||||||
|
isItemLoaded={isItemLoaded}
|
||||||
|
itemCount={itemCount}
|
||||||
|
loadMoreItems={loadMoreItems}
|
||||||
|
>
|
||||||
|
{({ onItemsRendered, ref }) => (
|
||||||
|
<List
|
||||||
|
className="List"
|
||||||
|
height={595}
|
||||||
|
itemCount={itemCount}
|
||||||
|
itemSize={41}
|
||||||
|
onItemsRendered={onItemsRendered}
|
||||||
|
ref={ref}
|
||||||
|
width={'100%'}
|
||||||
|
>
|
||||||
|
{({ index, style }) => (
|
||||||
|
<Item
|
||||||
|
index={txs[index]}
|
||||||
|
style={style}
|
||||||
|
isLoading={!isItemLoaded(index)}
|
||||||
|
error={error}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</List>
|
||||||
|
)}
|
||||||
|
</InfiniteLoader>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -12,7 +12,7 @@ import Genesis from './genesis';
|
|||||||
import { Block } from './blocks/id';
|
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 { Txs as TxHome } from './txs/home';
|
import { TxsHome } from './txs/home';
|
||||||
import { PendingTxs } from './pending';
|
import { PendingTxs } from './pending';
|
||||||
import flags from '../config/flags';
|
import flags from '../config/flags';
|
||||||
import { t } from '@vegaprotocol/react-helpers';
|
import { t } from '@vegaprotocol/react-helpers';
|
||||||
@ -129,7 +129,7 @@ const routerConfig = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: true,
|
index: true,
|
||||||
element: <TxHome />,
|
element: <TxsHome />,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
@ -1,32 +1,122 @@
|
|||||||
import type { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
|
|
||||||
import { DATA_SOURCES } from '../../../config';
|
import { DATA_SOURCES } from '../../../config';
|
||||||
|
import { useCallback, useState, useMemo } from 'react';
|
||||||
|
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||||
import { RouteTitle } from '../../../components/route-title';
|
import { RouteTitle } from '../../../components/route-title';
|
||||||
import { BlocksRefetch } from '../../../components/blocks';
|
import { BlocksRefetch } from '../../../components/blocks';
|
||||||
import { RenderFetched } from '../../../components/render-fetched';
|
|
||||||
import { BlockTxsData } from '../../../components/txs';
|
|
||||||
import { JumpToBlock } from '../../../components/jump-to-block';
|
import { JumpToBlock } from '../../../components/jump-to-block';
|
||||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
import { TxsInfiniteList } from '../../../components/txs';
|
||||||
|
import { AsyncRenderer } from '@vegaprotocol/ui-toolkit';
|
||||||
|
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
|
||||||
|
import type { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
|
||||||
|
|
||||||
const Txs = () => {
|
interface TxsProps {
|
||||||
const {
|
latestBlockHeight: string;
|
||||||
state: { data, error, loading },
|
}
|
||||||
refetch,
|
|
||||||
} = useFetch<TendermintBlockchainResponse>(
|
interface TxsStateProps {
|
||||||
`${DATA_SOURCES.tendermintUrl}/blockchain`
|
txsData: ChainExplorerTxResponse[];
|
||||||
|
hasMoreTxs: boolean;
|
||||||
|
nextPage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Txs = ({ latestBlockHeight }: TxsProps) => {
|
||||||
|
const [{ txsData, hasMoreTxs, nextPage }, setTxsState] =
|
||||||
|
useState<TxsStateProps>({
|
||||||
|
txsData: [],
|
||||||
|
hasMoreTxs: true,
|
||||||
|
nextPage: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
const reusedBodyParams = useMemo(
|
||||||
|
() => ({
|
||||||
|
node_url: DATA_SOURCES.tendermintUrl,
|
||||||
|
transaction_height: parseInt(latestBlockHeight),
|
||||||
|
page_size: 30,
|
||||||
|
}),
|
||||||
|
[latestBlockHeight]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
state: { error, loading },
|
||||||
|
refetch,
|
||||||
|
} = useFetch(
|
||||||
|
DATA_SOURCES.chainExplorerUrl,
|
||||||
|
{
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(reusedBodyParams),
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadTxs = useCallback(async () => {
|
||||||
|
const data = await refetch(
|
||||||
|
undefined,
|
||||||
|
JSON.stringify({
|
||||||
|
...reusedBodyParams,
|
||||||
|
page_number: nextPage,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
setTxsState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
nextPage: prev.nextPage + 1,
|
||||||
|
hasMoreTxs: true,
|
||||||
|
txsData: [...prev.txsData, ...(data as ChainExplorerTxResponse[])],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}, [nextPage, refetch, reusedBodyParams]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<RouteTitle>{t('Transactions')}</RouteTitle>
|
<RouteTitle>{t('Transactions')}</RouteTitle>
|
||||||
<RenderFetched error={error} loading={loading}>
|
<BlocksRefetch
|
||||||
<>
|
refetch={() =>
|
||||||
<BlocksRefetch refetch={refetch} />
|
refetch(
|
||||||
<BlockTxsData data={data} />
|
undefined,
|
||||||
</>
|
JSON.stringify({
|
||||||
</RenderFetched>
|
...reusedBodyParams,
|
||||||
|
page_number: 1,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<TxsInfiniteList
|
||||||
|
hasMoreTxs={hasMoreTxs}
|
||||||
|
areTxsLoading={loading}
|
||||||
|
txs={txsData}
|
||||||
|
loadMoreTxs={loadTxs}
|
||||||
|
error={error}
|
||||||
|
className="mb-28"
|
||||||
|
/>
|
||||||
<JumpToBlock />
|
<JumpToBlock />
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { Txs };
|
const Wrapper = () => {
|
||||||
|
const {
|
||||||
|
state: { data, error, loading },
|
||||||
|
} = useFetch<TendermintBlockchainResponse>(
|
||||||
|
`${DATA_SOURCES.tendermintUrl}/blockchain`
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncRenderer
|
||||||
|
loading={!!loading}
|
||||||
|
loadingMessage={t('Getting latest block height...')}
|
||||||
|
error={error}
|
||||||
|
data={data}
|
||||||
|
noDataMessage={t('Could not get latest block height')}
|
||||||
|
render={(data) => (
|
||||||
|
<Txs
|
||||||
|
latestBlockHeight={
|
||||||
|
data?.result?.block_metas?.[0]?.header?.height || ''
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { Wrapper as TxsHome };
|
||||||
|
@ -25,7 +25,8 @@ export const useFetch = <T>(
|
|||||||
): {
|
): {
|
||||||
state: State<T>;
|
state: State<T>;
|
||||||
refetch: (
|
refetch: (
|
||||||
params?: Record<string, string | number | null | undefined> | undefined
|
params?: Record<string, string | number | null | undefined> | undefined,
|
||||||
|
body?: BodyInit
|
||||||
) => Promise<T | undefined>;
|
) => Promise<T | undefined>;
|
||||||
} => {
|
} => {
|
||||||
// Used to prevent state update if the component is unmounted
|
// Used to prevent state update if the component is unmounted
|
||||||
@ -51,7 +52,10 @@ export const useFetch = <T>(
|
|||||||
|
|
||||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||||
const fetchCallback = useCallback(
|
const fetchCallback = useCallback(
|
||||||
async (params?: Record<string, string | null | undefined | number>) => {
|
async (
|
||||||
|
params?: Record<string, string | null | undefined | number>,
|
||||||
|
body?: BodyInit
|
||||||
|
) => {
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@ -67,7 +71,10 @@ export const useFetch = <T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(assembledUrl.toString(), options);
|
const response = await fetch(assembledUrl.toString(), {
|
||||||
|
...options,
|
||||||
|
body: body ? body : options?.body,
|
||||||
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(response.statusText);
|
throw new Error(response.statusText);
|
||||||
}
|
}
|
||||||
|
@ -4,29 +4,41 @@ import { t } from '@vegaprotocol/react-helpers';
|
|||||||
|
|
||||||
interface AsyncRendererProps<T> {
|
interface AsyncRendererProps<T> {
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
loadingMessage?: string;
|
||||||
error: Error | undefined | null;
|
error: Error | undefined | null;
|
||||||
|
errorMessage?: string;
|
||||||
data: T | undefined;
|
data: T | undefined;
|
||||||
|
noDataMessage?: string;
|
||||||
children?: ReactNode | null;
|
children?: ReactNode | null;
|
||||||
render?: (data: T) => ReactNode;
|
render?: (data: T) => ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AsyncRenderer<T = object>({
|
export function AsyncRenderer<T = object>({
|
||||||
loading,
|
loading,
|
||||||
|
loadingMessage,
|
||||||
error,
|
error,
|
||||||
|
errorMessage,
|
||||||
data,
|
data,
|
||||||
|
noDataMessage,
|
||||||
children,
|
children,
|
||||||
render,
|
render,
|
||||||
}: AsyncRendererProps<T>) {
|
}: AsyncRendererProps<T>) {
|
||||||
if (error) {
|
if (error) {
|
||||||
return <Splash>{t(`Something went wrong: ${error.message}`)}</Splash>;
|
return (
|
||||||
|
<Splash>
|
||||||
|
{errorMessage
|
||||||
|
? errorMessage
|
||||||
|
: t(`Something went wrong: ${error.message}`)}
|
||||||
|
</Splash>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return <Splash>{t('Loading...')}</Splash>;
|
return <Splash>{loadingMessage ? loadingMessage : t('Loading...')}</Splash>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return <Splash>{t('No data')}</Splash>;
|
return <Splash>{noDataMessage ? noDataMessage : t('No data')}</Splash>;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react/jsx-no-useless-fragment
|
// eslint-disable-next-line react/jsx-no-useless-fragment
|
||||||
return <>{render ? render(data) : children}</>;
|
return <>{render ? render(data) : children}</>;
|
||||||
|
Loading…
Reference in New Issue
Block a user