Feat/231-explorer-infinite-loading-blocks (#306)
* Making a start with react-window-infinite-loader for the blocks infinite scrolling
* WIP block explorer infinte scroll
* WIP pairing
* pairing tidying
* Applied refetch url params more cleanly, moved useFetch to react-helpers lib
* Add notice of new blocks created since page load, some cleanup of blocks-infinite-list.tsx component
* Attempting a refresh of the 'new blocks' value in blocks-refetch.tsx
* Correctly updating state based on previous value
* Update libs/react-helpers/src/hooks/use-fetch.ts
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
* Update libs/react-helpers/src/hooks/use-fetch.ts
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
* Update apps/explorer/src/app/components/blocks/blocks-refetch.tsx
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
* Cleanup from convos and PR
* Prettier formatting
* struggling with websocket tests
* struggling with websocket tests
* Progress on websocket tests for blocks-refetch.tsx
* Tests for blocks-refetch
* Tests for blocks-infinite-list
* Scroll test for blocks-infinite-list
* Defined NOOP in blocks-infinite-list.tsx
* Separate web sockets for each test
* Separate web sockets for each test
* Tweaked e2e tests to account for blocks taking longer to load
* fix tests
* Removed nx knowledge of empty simple-trading-app-e2e for now
* mock at use fetch level instead of at fetch level
* fix edge cases and add further tests
* fix failing e2e tests
* rename test
* Update apps/explorer-e2e/src/support/pages/blocks-page.ts
* Update apps/explorer-e2e/src/support/pages/blocks-page.ts
* rename
* test: use explicit wait for rather than times
* style: remove console
* test: correct env file
* Revert "test: correct env file"
This reverts commit d01d3cfa5e
.
* think env var is incorrect
* correct env file
* fix flakiness
* add minor wait for test flakiness
* longer timeout
Co-authored-by: Dexter <dexter.edwards93@gmail.com>
Co-authored-by: Matthew Russell <mattrussell36@gmail.com>
This commit is contained in:
parent
107171d46a
commit
e0bcfe7bbe
@ -1,14 +1,16 @@
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
NX_VEGA_ENV = 'TESTNET'
|
||||
NX_VEGA_REST = 'https://lb.testnet.vega.xyz/datanode/rest'
|
||||
NX_CHAIN_EXPLORER_URL="https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL="https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL="wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL="https://lb.testnet.vega.xyz/query"
|
||||
NX_VEGA_ENV='TESTNET'
|
||||
NX_VEGA_REST='https://lb.testnet.vega.xyz/datanode/rest'
|
||||
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
NX_EXPLORER_GOVERNANCE = 1
|
||||
NX_EXPLORER_MARKETS = 1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS = 1
|
||||
NX_EXPLORER_PARTIES = 1
|
||||
NX_EXPLORER_VALIDATORS = 1
|
||||
CYPRESS_VEGA_TENDERMINT_URL='https://lb.testnet.vega.xyz/tm'
|
||||
|
||||
NX_EXPLORER_ASSETS=1
|
||||
NX_EXPLORER_GENESIS=1
|
||||
NX_EXPLORER_GOVERNANCE=1
|
||||
NX_EXPLORER_MARKETS=1
|
||||
NX_EXPLORER_NETWORK_PARAMETERS=1
|
||||
NX_EXPLORER_PARTIES=1
|
||||
NX_EXPLORER_VALIDATORS=1
|
||||
|
@ -19,6 +19,9 @@ export default class BasePage {
|
||||
}
|
||||
|
||||
navigateToBlocks() {
|
||||
const base = Cypress.env('VEGA_TENDERMINT_URL');
|
||||
const url = new URL('/tm/blockchain*', base).toString();
|
||||
cy.intercept(url).as('blockChain');
|
||||
cy.get(`a[href='${this.blocksUrl}']`).click();
|
||||
}
|
||||
|
||||
|
@ -15,7 +15,14 @@ export default class BlocksPage extends BasePage {
|
||||
jumpToBlockInput = 'block-input';
|
||||
jumpToBlockSubmit = 'go-submit';
|
||||
|
||||
private waitForBlocksResponse() {
|
||||
cy.wait('@blockChain');
|
||||
// eslint-disable-next-line cypress/no-unnecessary-waiting
|
||||
cy.wait(1000);
|
||||
}
|
||||
|
||||
validateBlocksPageDisplayed() {
|
||||
this.waitForBlocksResponse();
|
||||
cy.getByTestId(this.blockRow).should('have.length.above', 1);
|
||||
cy.getByTestId(this.blockHeight).first().should('not.be.empty');
|
||||
cy.getByTestId(this.numberOfTransactions).first().should('not.be.empty');
|
||||
@ -24,6 +31,7 @@ export default class BlocksPage extends BasePage {
|
||||
}
|
||||
|
||||
clickOnTopBlockHeight() {
|
||||
this.waitForBlocksResponse();
|
||||
cy.getByTestId(this.blockHeight).first().click();
|
||||
}
|
||||
|
||||
|
@ -18,6 +18,14 @@ NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
NX_CHAIN_EXPLORER_URL = "https://explorer.vega.trading/.netlify/functions/chain-explorer-api"
|
||||
NX_TENDERMINT_URL = "https://lb.testnet.vega.xyz/tm"
|
||||
NX_TENDERMINT_WEBSOCKET_URL = "wss://lb.testnet.vega.xyz/tm/websocket"
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
NX_VEGA_ENV = 'TESTNET'
|
||||
NX_VEGA_REST = 'https://lb.testnet.vega.xyz/datanode/rest'
|
||||
CYPRESS_VEGA_TENDERMINT_URL='https://lb.testnet.vega.xyz/tm'
|
||||
|
||||
# App flags
|
||||
NX_EXPLORER_ASSETS = 1
|
||||
NX_EXPLORER_GENESIS = 1
|
||||
|
@ -15,7 +15,7 @@ export const BlockData = ({ block, className }: BlockProps) => {
|
||||
return (
|
||||
<TableWithTbody
|
||||
aria-label={`Data for block ${block.header?.height}`}
|
||||
className={className}
|
||||
className={`${className} block-data-table`}
|
||||
>
|
||||
<TableRow data-testid="block-row" modifier="background">
|
||||
<TableCell
|
||||
|
@ -0,0 +1,166 @@
|
||||
import { BlocksInfiniteList } from './blocks-infinite-list';
|
||||
import { render, screen, fireEvent, act } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
|
||||
const generateBlocks = (number: number) => {
|
||||
return Array.from(Array(number)).map((_) => ({
|
||||
block_id: {
|
||||
hash: '1C4D61AD80C250713EB996FB19B4537283083FB1878A98DA620872EE17A2103A',
|
||||
parts: {
|
||||
total: 1,
|
||||
hash: '75A26A06BAA518A94B873229BF3A6DC5DD376A588C476B6416E181CF07F36FA1',
|
||||
},
|
||||
},
|
||||
block_size: '1965',
|
||||
header: {
|
||||
version: {
|
||||
block: '11',
|
||||
},
|
||||
chain_id: 'vega-mainnet-0006',
|
||||
height: '4993074',
|
||||
time: '2022-05-09T12:14:31.021164227Z',
|
||||
last_block_id: {
|
||||
hash: '3EA62A0EF7B668E1522EE06DC5395E2B8DE31B621C707B68D05F8AA66BD168EE',
|
||||
parts: {
|
||||
total: 1,
|
||||
hash: '5B3CD9883FFA5282FB46B0A2D7C587FDCEF2DC1660DF105B6D67649A21AEF3C7',
|
||||
},
|
||||
},
|
||||
last_commit_hash:
|
||||
'2CDC8B12074A5A4FD67B95CEB573BFC1E91A58D2E839E03B2B3FC94214100D04',
|
||||
data_hash:
|
||||
'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855',
|
||||
validators_hash:
|
||||
'C3F329FFD2DC0B5F51D6100669BAE705647D0ED016B82AFADE38AA1723ABE9D9',
|
||||
next_validators_hash:
|
||||
'C3F329FFD2DC0B5F51D6100669BAE705647D0ED016B82AFADE38AA1723ABE9D9',
|
||||
consensus_hash:
|
||||
'048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F',
|
||||
app_hash:
|
||||
'A7FFC6F8BF1ED76651C14756A061D662F580FF4DE43B49FA82D80A4B80F8434A3237DE294EFA38CE03133F411BFE0CEE5D6948CEA838B57C475143E2BF3A3D0CA1292C11CCDB876535C6699E8217E1A1294190D83E4233ECC490D32DF17A4116ADB0E5863D3F5309179F94FD63634116788C827BE5C6163F12DA68FAA77261CD',
|
||||
last_results_hash:
|
||||
'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855',
|
||||
evidence_hash:
|
||||
'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855',
|
||||
proposer_address: '53D53755DA3B815AB029C0062DD6A2B392E640DC',
|
||||
},
|
||||
num_txs: '0',
|
||||
}));
|
||||
};
|
||||
|
||||
describe('Blocks infinite list', () => {
|
||||
it('should display a "no items" message when no items provided', () => {
|
||||
render(
|
||||
<BlocksInfiniteList
|
||||
blocks={undefined}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={false}
|
||||
loadMoreBlocks={() => null}
|
||||
error={undefined}
|
||||
/>
|
||||
);
|
||||
expect(screen.getByText('No items')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('error is displayed at item level', () => {
|
||||
const blocks = generateBlocks(1);
|
||||
render(
|
||||
<BlocksInfiniteList
|
||||
blocks={blocks}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={false}
|
||||
loadMoreBlocks={() => 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 20 it initially
|
||||
// desires, all blocks will initially render
|
||||
const blocks = generateBlocks(10);
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlocksInfiniteList
|
||||
blocks={blocks}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={false}
|
||||
loadMoreBlocks={() => null}
|
||||
error={undefined}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('infinite-scroll-wrapper')
|
||||
.querySelectorAll('.block-data-table')
|
||||
).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 blocks = generateBlocks(15);
|
||||
const callback = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlocksInfiniteList
|
||||
blocks={blocks}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={true}
|
||||
loadMoreBlocks={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 blocks = generateBlocks(3);
|
||||
const callback = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlocksInfiniteList
|
||||
blocks={blocks}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={false}
|
||||
loadMoreBlocks={callback}
|
||||
error={undefined}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
expect(callback.mock.calls.length).toEqual(0);
|
||||
});
|
||||
|
||||
it('loads more items is called when scrolled', () => {
|
||||
const blocks = generateBlocks(20);
|
||||
const callback = jest.fn();
|
||||
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<BlocksInfiniteList
|
||||
blocks={blocks}
|
||||
areBlocksLoading={false}
|
||||
hasMoreBlocks={true}
|
||||
loadMoreBlocks={callback}
|
||||
error={undefined}
|
||||
/>
|
||||
</MemoryRouter>
|
||||
);
|
||||
|
||||
act(() => {
|
||||
fireEvent.scroll(screen.getByTestId('infinite-scroll-wrapper'), {
|
||||
target: { scrollY: 600 },
|
||||
});
|
||||
});
|
||||
|
||||
expect(callback.mock.calls.length).toEqual(1);
|
||||
});
|
||||
});
|
@ -0,0 +1,84 @@
|
||||
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 type { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
|
||||
import { BlockData } from './block-data';
|
||||
|
||||
interface BlocksInfiniteListProps {
|
||||
hasMoreBlocks: boolean;
|
||||
areBlocksLoading: boolean | undefined;
|
||||
blocks: BlockMeta[] | undefined;
|
||||
loadMoreBlocks: () => void;
|
||||
error: Error | undefined;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
interface ItemProps {
|
||||
index: number;
|
||||
style: React.CSSProperties;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
const NOOP = () => {};
|
||||
|
||||
export const BlocksInfiniteList = ({
|
||||
hasMoreBlocks,
|
||||
areBlocksLoading,
|
||||
blocks,
|
||||
loadMoreBlocks,
|
||||
error,
|
||||
className,
|
||||
}: BlocksInfiniteListProps) => {
|
||||
if (!blocks) {
|
||||
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 = hasMoreBlocks ? blocks.length + 1 : blocks.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 = areBlocksLoading ? NOOP : loadMoreBlocks;
|
||||
|
||||
// Every row is loaded except for our loading indicator row.
|
||||
const isItemLoaded = (index: number) =>
|
||||
!hasMoreBlocks || index < blocks.length;
|
||||
|
||||
const Item = ({ index, style }: ItemProps) => {
|
||||
let content;
|
||||
if (error) {
|
||||
content = t(`${error}`);
|
||||
} else if (!isItemLoaded(index)) {
|
||||
content = t('Loading...');
|
||||
} else {
|
||||
content = <BlockData block={blocks[index]} />;
|
||||
}
|
||||
|
||||
return <div style={style}>{content}</div>;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={className} data-testid="infinite-scroll-wrapper">
|
||||
<InfiniteLoader
|
||||
isItemLoaded={isItemLoaded}
|
||||
itemCount={itemCount}
|
||||
loadMoreItems={loadMoreItems}
|
||||
>
|
||||
{({ onItemsRendered, ref }) => (
|
||||
<List
|
||||
className="List"
|
||||
height={585}
|
||||
itemCount={itemCount}
|
||||
itemSize={30}
|
||||
onItemsRendered={onItemsRendered}
|
||||
ref={ref}
|
||||
width={'100%'}
|
||||
>
|
||||
{Item}
|
||||
</List>
|
||||
)}
|
||||
</InfiniteLoader>
|
||||
</div>
|
||||
);
|
||||
};
|
142
apps/explorer/src/app/components/blocks/blocks-refetch.spec.tsx
Normal file
142
apps/explorer/src/app/components/blocks/blocks-refetch.spec.tsx
Normal file
@ -0,0 +1,142 @@
|
||||
import WS from 'jest-websocket-mock';
|
||||
import useWebSocket from 'react-use-websocket';
|
||||
import {
|
||||
render,
|
||||
screen,
|
||||
fireEvent,
|
||||
act,
|
||||
waitFor,
|
||||
} from '@testing-library/react';
|
||||
import { TendermintWebsocketContext } from '../../contexts/websocket/tendermint-websocket-context';
|
||||
import { BlocksRefetch } from './blocks-refetch';
|
||||
|
||||
const BlocksRefetchInWebsocketProvider = ({
|
||||
callback,
|
||||
mocketLocation,
|
||||
}: {
|
||||
callback: () => null;
|
||||
mocketLocation: string;
|
||||
}) => {
|
||||
const contextShape = useWebSocket(mocketLocation);
|
||||
|
||||
return (
|
||||
<TendermintWebsocketContext.Provider value={{ ...contextShape }}>
|
||||
<BlocksRefetch refetch={callback} />
|
||||
</TendermintWebsocketContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('Blocks refetch', () => {
|
||||
it('should render inner components', async () => {
|
||||
const mocketLocation = 'wss:localhost:3002';
|
||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||
new WebSocket(mocketLocation);
|
||||
|
||||
render(
|
||||
<BlocksRefetchInWebsocketProvider
|
||||
callback={() => null}
|
||||
mocketLocation={mocketLocation}
|
||||
/>
|
||||
);
|
||||
await mocket.connected;
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('new blocks');
|
||||
expect(screen.getByTestId('refresh')).toBeInTheDocument();
|
||||
mocket.close();
|
||||
});
|
||||
|
||||
it('should initiate callback when the button is clicked', async () => {
|
||||
const mocketLocation = 'wss:localhost:3003';
|
||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||
new WebSocket(mocketLocation);
|
||||
|
||||
const callback = jest.fn();
|
||||
render(
|
||||
<BlocksRefetchInWebsocketProvider
|
||||
callback={callback}
|
||||
mocketLocation={mocketLocation}
|
||||
/>
|
||||
);
|
||||
await mocket.connected;
|
||||
const button = screen.getByTestId('refresh');
|
||||
|
||||
act(() => {
|
||||
fireEvent.click(button);
|
||||
});
|
||||
|
||||
expect(callback.mock.calls.length).toEqual(1);
|
||||
mocket.close();
|
||||
});
|
||||
|
||||
it('should show new blocks as websocket is correctly updated', async () => {
|
||||
const mocketLocation = 'wss:localhost:3004';
|
||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||
new WebSocket(mocketLocation);
|
||||
render(
|
||||
<BlocksRefetchInWebsocketProvider
|
||||
callback={() => null}
|
||||
mocketLocation={mocketLocation}
|
||||
/>
|
||||
);
|
||||
await mocket.connected;
|
||||
|
||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
||||
// @ts-ignore id on messages
|
||||
const id = mocket.messages[0].id;
|
||||
|
||||
const newBlockMessage = {
|
||||
id,
|
||||
result: {
|
||||
query: "tm.event = 'NewBlock'",
|
||||
},
|
||||
};
|
||||
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
||||
|
||||
act(() => {
|
||||
mocket.send(newBlockMessage);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('1 new blocks');
|
||||
|
||||
act(() => {
|
||||
mocket.send(newBlockMessage);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('2 new blocks');
|
||||
mocket.close();
|
||||
});
|
||||
|
||||
it('will not show new blocks if websocket has wrong ID', async () => {
|
||||
const mocketLocation = 'wss:localhost:3005';
|
||||
const mocket = new WS(mocketLocation, { jsonProtocol: true });
|
||||
new WebSocket(mocketLocation);
|
||||
|
||||
render(
|
||||
<BlocksRefetchInWebsocketProvider
|
||||
callback={() => null}
|
||||
mocketLocation={mocketLocation}
|
||||
/>
|
||||
);
|
||||
await mocket.connected;
|
||||
|
||||
// Ensuring we send an ID equal to the one the client subscribed with.
|
||||
await waitFor(() => expect(mocket.messages.length).toEqual(1));
|
||||
|
||||
const newBlockMessageBadId = {
|
||||
id: 'blahblahblah',
|
||||
result: {
|
||||
query: "tm.event = 'NewBlock'",
|
||||
},
|
||||
};
|
||||
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
||||
|
||||
act(() => {
|
||||
mocket.send(newBlockMessageBadId);
|
||||
});
|
||||
|
||||
expect(screen.getByTestId('new-blocks')).toHaveTextContent('0 new blocks');
|
||||
mocket.close();
|
||||
});
|
||||
});
|
@ -1,3 +1,5 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useTendermintWebsocket } from '../../hooks/use-tendermint-websocket';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
|
||||
@ -6,14 +8,35 @@ interface BlocksRefetchProps {
|
||||
}
|
||||
|
||||
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
|
||||
const [blocksToLoad, setBlocksToLoad] = useState<number>(0);
|
||||
|
||||
const { messages } = useTendermintWebsocket({
|
||||
query: "tm.event = 'NewBlock'",
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (messages.length > 0) {
|
||||
setBlocksToLoad((prev) => prev + 1);
|
||||
}
|
||||
}, [messages]);
|
||||
|
||||
const refresh = () => {
|
||||
refetch();
|
||||
setBlocksToLoad(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<Button
|
||||
onClick={() => refetch()}
|
||||
variant="inline-link"
|
||||
className="mb-28"
|
||||
data-testid="refresh"
|
||||
>
|
||||
{t('Refresh to see latest blocks')}
|
||||
</Button>
|
||||
<>
|
||||
<span data-testid="new-blocks">{blocksToLoad} new blocks -</span>
|
||||
|
||||
<Button
|
||||
onClick={refresh}
|
||||
variant="inline-link"
|
||||
className="mb-28"
|
||||
data-testid="refresh"
|
||||
>
|
||||
{t('refresh to see latest')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -20,7 +20,7 @@ export const Header = ({
|
||||
<header className="grid grid-rows-2 grid-cols-[1fr_auto] md:flex md:col-span-2 p-16 gap-12 border-b-1">
|
||||
<Link to={Routes.HOME}>
|
||||
<h1
|
||||
className="text-h3 font-alpha uppercase calt"
|
||||
className="text-h3 font-alpha uppercase calt mb-2"
|
||||
data-testid="explorer-header"
|
||||
>
|
||||
{t('Vega Explorer')}
|
||||
|
@ -1,4 +1,3 @@
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import type { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
|
||||
import { Routes } from '../../routes/route-names';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
@ -6,7 +5,7 @@ import { RenderFetched } from '../render-fetched';
|
||||
import { TruncatedLink } from '../truncate/truncated-link';
|
||||
import { TxOrderType } from './tx-order-type';
|
||||
import { Table, TableRow, TableCell } from '../table';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
interface TxsPerBlockProps {
|
||||
blockHeight: string | undefined;
|
||||
|
@ -1,97 +0,0 @@
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
|
||||
interface State<T> {
|
||||
data?: T;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
LOADING = 'LOADING',
|
||||
ERROR = 'ERROR',
|
||||
FETCHED = 'FETCHED',
|
||||
}
|
||||
|
||||
// discriminated union type
|
||||
type Action<T> =
|
||||
| { type: ActionType.LOADING }
|
||||
| { type: ActionType.FETCHED; payload: T }
|
||||
| { type: ActionType.ERROR; error: Error };
|
||||
|
||||
function useFetch<T>(
|
||||
url: string,
|
||||
options?: RequestInit
|
||||
): { state: State<T>; refetch: () => void } {
|
||||
// Used to prevent state update if the component is unmounted
|
||||
const cancelRequest = useRef<{ [key: string]: boolean }>({ [url]: false });
|
||||
|
||||
const initialState: State<T> = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
// Keep state logic separated
|
||||
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
|
||||
switch (action.type) {
|
||||
case ActionType.LOADING:
|
||||
return { ...initialState, loading: true };
|
||||
case ActionType.FETCHED:
|
||||
return { ...initialState, data: action.payload, loading: false };
|
||||
case ActionType.ERROR:
|
||||
return { ...initialState, error: action.error, loading: false };
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
const fetchCallback = useCallback(() => {
|
||||
if (!url) return;
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: ActionType.LOADING });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as T;
|
||||
if (data && 'error' in data) {
|
||||
// @ts-ignore - data.error
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (cancelRequest.current[url]) return;
|
||||
|
||||
dispatch({ type: ActionType.FETCHED, payload: data });
|
||||
} catch (error) {
|
||||
if (cancelRequest.current[url]) return;
|
||||
|
||||
dispatch({ type: ActionType.ERROR, error: error as Error });
|
||||
}
|
||||
};
|
||||
|
||||
void fetchData();
|
||||
|
||||
// Do nothing if the url is not given
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
const cancel = cancelRequest.current;
|
||||
cancel[url] = false;
|
||||
fetchCallback();
|
||||
// Use the cleanup function for avoiding a possibly...
|
||||
// ...state update after the component was unmounted
|
||||
return () => {
|
||||
cancel[url] = true;
|
||||
};
|
||||
}, [fetchCallback, url]);
|
||||
|
||||
return {
|
||||
state,
|
||||
refetch: fetchCallback,
|
||||
};
|
||||
}
|
||||
|
||||
export default useFetch;
|
@ -1,29 +1,120 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import type { TendermintBlockchainResponse } from '../tendermint-blockchain-response';
|
||||
import type {
|
||||
BlockMeta,
|
||||
TendermintBlockchainResponse,
|
||||
} from '../tendermint-blockchain-response';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { BlocksData, BlocksRefetch } from '../../../components/blocks';
|
||||
import { BlocksRefetch } from '../../../components/blocks';
|
||||
import { BlocksInfiniteList } from '../../../components/blocks/blocks-infinite-list';
|
||||
import { JumpToBlock } from '../../../components/jump-to-block';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
// This constant should only be changed if Tendermint API changes the max blocks returned
|
||||
const TM_BLOCKS_PER_REQUEST = 20;
|
||||
|
||||
interface BlocksStateProps {
|
||||
areBlocksLoading: boolean | undefined;
|
||||
blocksError: Error | undefined;
|
||||
blocksData: BlockMeta[];
|
||||
hasMoreBlocks: boolean;
|
||||
nextBlockHeightToLoad: number | undefined;
|
||||
}
|
||||
|
||||
const Blocks = () => {
|
||||
const [
|
||||
{
|
||||
areBlocksLoading,
|
||||
blocksError,
|
||||
blocksData,
|
||||
hasMoreBlocks,
|
||||
nextBlockHeightToLoad,
|
||||
},
|
||||
setBlocksState,
|
||||
] = useState<BlocksStateProps>({
|
||||
areBlocksLoading: false,
|
||||
blocksError: undefined,
|
||||
blocksData: [],
|
||||
hasMoreBlocks: true,
|
||||
nextBlockHeightToLoad: undefined,
|
||||
});
|
||||
|
||||
const {
|
||||
state: { data, error, loading },
|
||||
state: { error, loading },
|
||||
refetch,
|
||||
} = useFetch<TendermintBlockchainResponse>(
|
||||
`${DATA_SOURCES.tendermintUrl}/blockchain`
|
||||
`${DATA_SOURCES.tendermintUrl}/blockchain`,
|
||||
undefined,
|
||||
false
|
||||
);
|
||||
|
||||
const loadBlocks = useCallback(async () => {
|
||||
setBlocksState((prev) => ({
|
||||
...prev,
|
||||
areBlocksLoading: loading,
|
||||
}));
|
||||
|
||||
const maxHeight = Math.max(
|
||||
Number(nextBlockHeightToLoad),
|
||||
TM_BLOCKS_PER_REQUEST
|
||||
);
|
||||
|
||||
const minHeight =
|
||||
Number(nextBlockHeightToLoad) - TM_BLOCKS_PER_REQUEST > 1
|
||||
? Number(nextBlockHeightToLoad) - TM_BLOCKS_PER_REQUEST - 1
|
||||
: undefined;
|
||||
|
||||
const data = await refetch({
|
||||
maxHeight,
|
||||
minHeight,
|
||||
});
|
||||
|
||||
if (data) {
|
||||
const blockMetas = data.result.block_metas;
|
||||
const lastBlockHeightLoaded =
|
||||
blockMetas && blockMetas.length > 0
|
||||
? parseInt(blockMetas[blockMetas.length - 1].header.height)
|
||||
: undefined;
|
||||
|
||||
setBlocksState((prev) => ({
|
||||
...prev,
|
||||
nextBlockHeightToLoad: lastBlockHeightLoaded
|
||||
? lastBlockHeightLoaded - 1
|
||||
: undefined,
|
||||
hasMoreBlocks: !!lastBlockHeightLoaded && lastBlockHeightLoaded > 1,
|
||||
blocksData: [...prev.blocksData, ...blockMetas],
|
||||
}));
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setBlocksState((prev) => ({
|
||||
...prev,
|
||||
blocksError: error,
|
||||
}));
|
||||
}
|
||||
}, [error, loading, nextBlockHeightToLoad, refetch]);
|
||||
|
||||
const refreshBlocks = useCallback(async () => {
|
||||
setBlocksState((prev) => ({
|
||||
...prev,
|
||||
nextBlockHeightToLoad: undefined,
|
||||
hasMoreBlocks: true,
|
||||
blocksData: [],
|
||||
}));
|
||||
}, [setBlocksState]);
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle>{t('Blocks')}</RouteTitle>
|
||||
<RenderFetched error={error} loading={loading}>
|
||||
<>
|
||||
<BlocksRefetch refetch={refetch} />
|
||||
<BlocksData data={data} className="mb-28" />
|
||||
</>
|
||||
</RenderFetched>
|
||||
<BlocksRefetch refetch={refreshBlocks} />
|
||||
<BlocksInfiniteList
|
||||
hasMoreBlocks={hasMoreBlocks}
|
||||
areBlocksLoading={areBlocksLoading}
|
||||
blocks={blocksData}
|
||||
loadMoreBlocks={loadBlocks}
|
||||
error={blocksError}
|
||||
className="mb-28"
|
||||
/>
|
||||
<JumpToBlock />
|
||||
</section>
|
||||
);
|
||||
|
@ -2,10 +2,19 @@ import { Block } from './block';
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
||||
import { Routes as RouteNames } from '../../route-names';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
jest.mock('@vegaprotocol/react-helpers', () => {
|
||||
const original = jest.requireActual('@vegaprotocol/react-helpers');
|
||||
return {
|
||||
...original,
|
||||
useFetch: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const blockId = 1085890;
|
||||
|
||||
const createBlockResponse = () => {
|
||||
const createBlockResponse = (id: number = blockId) => {
|
||||
return {
|
||||
jsonrpc: '2.0',
|
||||
id: -1,
|
||||
@ -58,7 +67,7 @@ const createBlockResponse = () => {
|
||||
evidence: [],
|
||||
},
|
||||
last_commit: {
|
||||
height: blockId.toString(),
|
||||
height: id.toString(),
|
||||
round: 0,
|
||||
block_id: {
|
||||
hash: 'C50CA169545AC1280220433D7971C50D941F675E9B0FFF358ABE8F3A7F74AE0E',
|
||||
@ -110,9 +119,9 @@ const createBlockResponse = () => {
|
||||
};
|
||||
};
|
||||
|
||||
const renderComponent = () => {
|
||||
const renderComponent = (id: number = blockId) => {
|
||||
return (
|
||||
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${blockId}`]}>
|
||||
<MemoryRouter initialEntries={[`/${RouteNames.BLOCKS}/${id}`]}>
|
||||
<Routes>
|
||||
<Route path={`/${RouteNames.BLOCKS}/:block`} element={<Block />} />
|
||||
</Routes>
|
||||
@ -121,31 +130,41 @@ const renderComponent = () => {
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createBlockResponse()),
|
||||
})
|
||||
) as jest.Mock;
|
||||
jest.useFakeTimers().setSystemTime(1648123348642);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('Block', () => {
|
||||
it('renders error state if error is present', async () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: null, loading: false, error: 'asd' },
|
||||
});
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByText(`BLOCK ${blockId}`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Error retrieving data')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders loading state if present', async () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: null, loading: true, error: null },
|
||||
});
|
||||
render(renderComponent());
|
||||
|
||||
expect(screen.getByText(`BLOCK ${blockId}`)).toBeInTheDocument();
|
||||
expect(screen.getByText('Loading...')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render title, proposer address and time mined', async () => {
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(createBlockResponse()),
|
||||
})
|
||||
) as jest.Mock;
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: createBlockResponse(), loading: false, error: null },
|
||||
});
|
||||
render(renderComponent());
|
||||
await waitFor(() => screen.getByTestId('block-header'));
|
||||
|
||||
expect(screen.getByTestId('block-header')).toHaveTextContent(
|
||||
`BLOCK ${blockId}`
|
||||
);
|
||||
@ -160,6 +179,9 @@ describe('Block', () => {
|
||||
});
|
||||
|
||||
it('renders next and previous buttons', async () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: createBlockResponse(), loading: false, error: null },
|
||||
});
|
||||
render(renderComponent());
|
||||
await waitFor(() => screen.getByTestId('block-header'));
|
||||
|
||||
@ -172,4 +194,15 @@ describe('Block', () => {
|
||||
`/${RouteNames.BLOCKS}/${blockId + 1}`
|
||||
);
|
||||
});
|
||||
|
||||
it('disables previous button on block 1', async () => {
|
||||
(useFetch as jest.Mock).mockReturnValue({
|
||||
state: { data: createBlockResponse(1), loading: false, error: null },
|
||||
});
|
||||
render(renderComponent(1));
|
||||
await waitFor(() => screen.getByTestId('block-header'));
|
||||
expect(screen.getByTestId('previous-block-button')).toHaveAttribute(
|
||||
'disabled'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import type { TendermintBlocksResponse } from '../tendermint-blocks-response';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { SecondsAgo } from '../../../components/seconds-ago';
|
||||
@ -16,7 +15,7 @@ import { Button } from '@vegaprotocol/ui-toolkit';
|
||||
import { Routes } from '../../route-names';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { HighlightedLink } from '../../../components/highlighted-link';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const Block = () => {
|
||||
const { block } = useParams<{ block: string }>();
|
||||
@ -26,11 +25,6 @@ const Block = () => {
|
||||
`${DATA_SOURCES.tendermintUrl}/block?height=${block}`
|
||||
);
|
||||
|
||||
const header = blockData?.result.block.header;
|
||||
if (!header) {
|
||||
return <p>{t('Could not get block data')}</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<section>
|
||||
<RouteTitle data-testid="block-header">{t(`BLOCK ${block}`)}</RouteTitle>
|
||||
@ -42,6 +36,7 @@ const Block = () => {
|
||||
to={`/${Routes.BLOCKS}/${Number(block) - 1}`}
|
||||
>
|
||||
<Button
|
||||
data-testid="previous-block-button"
|
||||
className="w-full"
|
||||
disabled={Number(block) === 1}
|
||||
variant="secondary"
|
||||
@ -64,7 +59,7 @@ const Block = () => {
|
||||
<TableCell modifier="bordered">
|
||||
<HighlightedLink
|
||||
to={`/${Routes.VALIDATORS}`}
|
||||
text={header.proposer_address}
|
||||
text={blockData?.result.block.header.proposer_address}
|
||||
data-testid="block-validator"
|
||||
/>
|
||||
</TableCell>
|
||||
@ -72,7 +67,10 @@ const Block = () => {
|
||||
<TableRow modifier="bordered">
|
||||
<TableHeader scope="row">Time</TableHeader>
|
||||
<TableCell modifier="bordered">
|
||||
<SecondsAgo data-testid="block-time" date={header.time} />
|
||||
<SecondsAgo
|
||||
data-testid="block-time"
|
||||
date={blockData?.result.block.header.time}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableWithTbody>
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { RouteTitle } from '../../components/route-title';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import type { TendermintGenesisResponse } from './tendermint-genesis-response';
|
||||
|
||||
const Genesis = () => {
|
||||
|
@ -1,13 +1,12 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { gql } from '@apollo/client';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
import { SubHeading } from '../../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import type { TendermintSearchTransactionResponse } from '../tendermint-transaction-response';
|
||||
import type {
|
||||
PartyAssetsQuery,
|
||||
|
@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
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/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const PendingTxs = () => {
|
||||
const {
|
||||
|
@ -1,4 +1,3 @@
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import type { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
import { RouteTitle } from '../../../components/route-title';
|
||||
@ -6,7 +5,7 @@ import { BlocksRefetch } from '../../../components/blocks';
|
||||
import { RenderFetched } from '../../../components/render-fetched';
|
||||
import { BlockTxsData } from '../../../components/txs';
|
||||
import { JumpToBlock } from '../../../components/jump-to-block';
|
||||
import { t } from '@vegaprotocol/react-helpers';
|
||||
import { t, useFetch } from '@vegaprotocol/react-helpers';
|
||||
|
||||
const Txs = () => {
|
||||
const {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
|
||||
import type { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
|
||||
import { DATA_SOURCES } from '../../../config';
|
||||
|
@ -5,7 +5,7 @@ import { RouteTitle } from '../../components/route-title';
|
||||
import { SubHeading } from '../../components/sub-heading';
|
||||
import { SyntaxHighlighter } from '@vegaprotocol/ui-toolkit';
|
||||
import { DATA_SOURCES } from '../../config';
|
||||
import useFetch from '../../hooks/use-fetch';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import type { TendermintValidatorsResponse } from './tendermint-validator-response';
|
||||
import type { NodesQuery } from './__generated__/NodesQuery';
|
||||
|
||||
|
@ -10,7 +10,8 @@
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"lib": ["es5", "es6", "dom", "dom.iterable"]
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
|
26
apps/simple-trading-app-e2e/.env
Normal file
26
apps/simple-trading-app-e2e/.env
Normal file
@ -0,0 +1,26 @@
|
||||
# React Environment Variables
|
||||
|
||||
# https://facebook.github.io/create-react-app/docs/adding-custom-environment-variables#expanding-environment-variables-in-env
|
||||
|
||||
# Netlify Environment Variables
|
||||
|
||||
# https://www.netlify.com/docs/continuous-deployment/#environment-variables
|
||||
|
||||
NX_VERSION=$npm_package_version
|
||||
NX_REPOSITORY_URL=$REPOSITORY_URL
|
||||
NX_BRANCH=$BRANCH
|
||||
NX_PULL_REQUEST=$PULL_REQUEST
|
||||
NX_HEAD=$HEAD
|
||||
NX_COMMIT_REF=$COMMIT_REF
|
||||
NX_CONTEXT=$CONTEXT
|
||||
NX_REVIEW_ID=$REVIEW_ID
|
||||
NX_INCOMING_HOOK_TITLE=$INCOMING_HOOK_TITLE
|
||||
NX_INCOMING_HOOK_URL=$INCOMING_HOOK_URL
|
||||
NX_INCOMING_HOOK_BODY=$INCOMING_HOOK_BODY
|
||||
NX_URL=$URL
|
||||
NX_DEPLOY_URL=$DEPLOY_URL
|
||||
NX_DEPLOY_PRIME_URL=$DEPLOY_PRIME_URL
|
||||
|
||||
NX_VEGA_URL = "https://lb.testnet.vega.xyz/query"
|
||||
NX_VEGA_ENV = 'TESTNET'
|
||||
NX_VEGA_REST = 'https://lb.testnet.vega.xyz/datanode/rest'
|
7
apps/simple-trading-app-e2e/src/integration/app.test.ts
Normal file
7
apps/simple-trading-app-e2e/src/integration/app.test.ts
Normal file
@ -0,0 +1,7 @@
|
||||
describe('simple trading app', () => {
|
||||
beforeEach(() => cy.visit('/'));
|
||||
|
||||
it('render', () => {
|
||||
cy.get('#root').should('exist');
|
||||
});
|
||||
});
|
@ -1,95 +0,0 @@
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
|
||||
interface State<T> {
|
||||
data?: T;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
LOADING = 'LOADING',
|
||||
ERROR = 'ERROR',
|
||||
FETCHED = 'FETCHED',
|
||||
}
|
||||
|
||||
// discriminated union type
|
||||
type Action<T> =
|
||||
| { type: ActionType.LOADING }
|
||||
| { type: ActionType.FETCHED; payload: T }
|
||||
| { type: ActionType.ERROR; error: Error };
|
||||
|
||||
function useFetch<T = unknown>(
|
||||
url?: string,
|
||||
options?: RequestInit
|
||||
): { state: State<T>; refetch: () => void } {
|
||||
// Used to prevent state update if the component is unmounted
|
||||
const cancelRequest = useRef<boolean>(false);
|
||||
|
||||
const initialState: State<T> = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
// Keep state logic separated
|
||||
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
|
||||
switch (action.type) {
|
||||
case ActionType.LOADING:
|
||||
return { ...initialState, loading: true };
|
||||
case ActionType.FETCHED:
|
||||
return { ...initialState, data: action.payload, loading: false };
|
||||
case ActionType.ERROR:
|
||||
return { ...initialState, error: action.error, loading: false };
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
const fetchCallback = useCallback(() => {
|
||||
if (!url) return;
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: ActionType.LOADING });
|
||||
|
||||
try {
|
||||
const response = await fetch(url, options);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
const data = (await response.json()) as T;
|
||||
if ('error' in data) {
|
||||
// @ts-ignore - data.error
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (cancelRequest.current) return;
|
||||
|
||||
dispatch({ type: ActionType.FETCHED, payload: data });
|
||||
} catch (error) {
|
||||
if (cancelRequest.current) return;
|
||||
|
||||
dispatch({ type: ActionType.ERROR, error: error as Error });
|
||||
}
|
||||
};
|
||||
|
||||
void fetchData();
|
||||
|
||||
// Do nothing if the url is not given
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
|
||||
useEffect(() => {
|
||||
fetchCallback();
|
||||
// Use the cleanup function for avoiding a possibly...
|
||||
// ...state update after the component was unmounted
|
||||
return () => {
|
||||
cancelRequest.current = true;
|
||||
};
|
||||
}, [fetchCallback]);
|
||||
|
||||
return {
|
||||
state,
|
||||
refetch: fetchCallback,
|
||||
};
|
||||
}
|
||||
|
||||
export default useFetch;
|
@ -5,7 +5,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { SplashLoader } from '../../../components/splash-loader';
|
||||
import useFetch from '../../../hooks/use-fetch';
|
||||
import { useFetch } from '@vegaprotocol/react-helpers';
|
||||
import { getDataNodeUrl } from '../../../lib/get-data-node-url';
|
||||
import { Proposal } from '../components/proposal';
|
||||
import { PROPOSALS_FRAGMENT } from '../proposal-fragment';
|
||||
|
@ -3,7 +3,10 @@ declare global {
|
||||
namespace Cypress {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
interface Chainable<Subject> {
|
||||
getByTestId(selector: string): Chainable<JQuery<HTMLElement>>;
|
||||
getByTestId(
|
||||
selector: string,
|
||||
options?: Partial<Loggable & Timeoutable & Withinable & Shadow>
|
||||
): Chainable<JQuery<HTMLElement>>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,4 @@
|
||||
export * from './use-apply-grid-transaction';
|
||||
export * from './use-data-provider';
|
||||
export * from './use-theme-switcher';
|
||||
export * from './use-fetch';
|
||||
|
117
libs/react-helpers/src/hooks/use-fetch.ts
Normal file
117
libs/react-helpers/src/hooks/use-fetch.ts
Normal file
@ -0,0 +1,117 @@
|
||||
import { useCallback, useEffect, useReducer, useRef } from 'react';
|
||||
|
||||
interface State<T> {
|
||||
data?: T;
|
||||
error?: Error;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
enum ActionType {
|
||||
LOADING = 'LOADING',
|
||||
ERROR = 'ERROR',
|
||||
FETCHED = 'FETCHED',
|
||||
}
|
||||
|
||||
// discriminated union type
|
||||
type Action<T> =
|
||||
| { type: ActionType.LOADING }
|
||||
| { type: ActionType.FETCHED; payload: T }
|
||||
| { type: ActionType.ERROR; error: Error };
|
||||
|
||||
export const useFetch = <T>(
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
initialFetch = true
|
||||
): {
|
||||
state: State<T>;
|
||||
refetch: (
|
||||
params?: Record<string, string | number | null | undefined> | undefined
|
||||
) => Promise<T | undefined>;
|
||||
} => {
|
||||
// Used to prevent state update if the component is unmounted
|
||||
const cancelRequest = useRef<boolean>(false);
|
||||
|
||||
const initialState: State<T> = {
|
||||
error: undefined,
|
||||
data: undefined,
|
||||
loading: false,
|
||||
};
|
||||
|
||||
// Keep state logic separated
|
||||
const fetchReducer = (state: State<T>, action: Action<T>): State<T> => {
|
||||
switch (action.type) {
|
||||
case ActionType.LOADING:
|
||||
return { ...initialState, loading: true };
|
||||
case ActionType.FETCHED:
|
||||
return { ...initialState, data: action.payload, loading: false };
|
||||
case ActionType.ERROR:
|
||||
return { ...initialState, error: action.error, loading: false };
|
||||
}
|
||||
};
|
||||
|
||||
const [state, dispatch] = useReducer(fetchReducer, initialState);
|
||||
const fetchCallback = useCallback(
|
||||
async (params?: Record<string, string | null | undefined | number>) => {
|
||||
if (!url) return;
|
||||
|
||||
const fetchData = async () => {
|
||||
dispatch({ type: ActionType.LOADING });
|
||||
let data;
|
||||
try {
|
||||
const assembledUrl = new URL(url);
|
||||
if (params) {
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value) {
|
||||
assembledUrl.searchParams.set(key, value.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(assembledUrl.toString(), options);
|
||||
if (!response.ok) {
|
||||
throw new Error(response.statusText);
|
||||
}
|
||||
|
||||
data = (await response.json()) as T;
|
||||
if (data && 'error' in data) {
|
||||
// @ts-ignore - data.error
|
||||
throw new Error(data.error);
|
||||
}
|
||||
if (cancelRequest.current) return;
|
||||
|
||||
dispatch({ type: ActionType.FETCHED, payload: data });
|
||||
} catch (error) {
|
||||
if (cancelRequest.current) return;
|
||||
|
||||
dispatch({ type: ActionType.ERROR, error: error as Error });
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
return fetchData();
|
||||
},
|
||||
// Do nothing if the url is not given
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[url]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
cancelRequest.current = false;
|
||||
if (initialFetch) {
|
||||
fetchCallback();
|
||||
}
|
||||
}, [fetchCallback, initialFetch, url]);
|
||||
|
||||
useEffect(() => {
|
||||
// Use the cleanup function for avoiding a possibly...
|
||||
// ...state update after the component was unmounted
|
||||
return () => {
|
||||
cancelRequest.current = true;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
state,
|
||||
refetch: fetchCallback,
|
||||
};
|
||||
};
|
@ -64,6 +64,8 @@
|
||||
"react-syntax-highlighter": "^15.4.5",
|
||||
"react-use-websocket": "^3.0.0",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
"react-window": "^1.8.7",
|
||||
"react-window-infinite-loader": "^1.0.7",
|
||||
"recharts": "^2.1.2",
|
||||
"regenerator-runtime": "0.13.7",
|
||||
"sha3": "^2.1.4",
|
||||
@ -109,6 +111,8 @@
|
||||
"@types/react-dom": "17.0.9",
|
||||
"@types/react-router-dom": "5.3.1",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/react-window-infinite-loader": "^1.0.6",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.18.0",
|
||||
"@typescript-eslint/parser": "5.18.0",
|
||||
@ -130,6 +134,7 @@
|
||||
"husky": "^7.0.4",
|
||||
"jest": "27.2.3",
|
||||
"jest-canvas-mock": "^2.3.1",
|
||||
"jest-websocket-mock": "^2.3.0",
|
||||
"lint-staged": "^12.3.3",
|
||||
"nx": "13.10.1",
|
||||
"prettier": "^2.5.1",
|
||||
|
48
yarn.lock
48
yarn.lock
@ -5586,6 +5586,21 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-window-infinite-loader@^1.0.6":
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-window-infinite-loader/-/react-window-infinite-loader-1.0.6.tgz#d7b23b4afaa1e0e2050876b766c3ea19f748f549"
|
||||
integrity sha512-V8g8sBDLVeJJAfEENJS7VXZK+DRJ+jzPNtk8jpj2G+obhf+iqGNUDGwNWCbBhLiD+KpHhf3kWQlKBRi0tAeU4Q==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
"@types/react-window" "*"
|
||||
|
||||
"@types/react-window@*", "@types/react-window@^1.8.5":
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-window/-/react-window-1.8.5.tgz#285fcc5cea703eef78d90f499e1457e9b5c02fc1"
|
||||
integrity sha512-V9q3CvhC9Jk9bWBOysPGaWy/Z0lxYcTXLtLipkt2cnRj1JOSFNF7wqGpkScSXMgBwC+fnVRg/7shwgddBG5ICw==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@>=16.9.0":
|
||||
version "18.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.0.1.tgz#1b2e02fb7613212518733946e49fb963dfc66e19"
|
||||
@ -13873,7 +13888,7 @@ jest-config@^27.5.1:
|
||||
slash "^3.0.0"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
jest-diff@^27.0.0, jest-diff@^27.5.1:
|
||||
jest-diff@^27.0.0, jest-diff@^27.0.2, jest-diff@^27.5.1:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def"
|
||||
integrity sha512-m0NvkX55LDt9T4mctTEgnZk3fmEg3NRYutvMPWM/0iPnkFj2wIeF45O1718cMSOFO1vINkqmxqD8vE37uTEbqw==
|
||||
@ -14252,6 +14267,14 @@ jest-watcher@^27.5.1:
|
||||
jest-util "^27.5.1"
|
||||
string-length "^4.0.1"
|
||||
|
||||
jest-websocket-mock@^2.3.0:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.yarnpkg.com/jest-websocket-mock/-/jest-websocket-mock-2.3.0.tgz#317e7d7f8ba54ba632a7300777b02b7ebb606845"
|
||||
integrity sha512-kXhRRApRdT4hLG/4rhsfcR0Ke0OzqIsDj0P5t0dl5aiAftShSgoRqp/0pyjS5bh+b9GrIzmfkrV2cn9LxxvSvA==
|
||||
dependencies:
|
||||
jest-diff "^27.0.2"
|
||||
mock-socket "^9.1.0"
|
||||
|
||||
jest-worker@27.0.0-next.5:
|
||||
version "27.0.0-next.5"
|
||||
resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-27.0.0-next.5.tgz#5985ee29b12a4e191f4aae4bb73b97971d86ec28"
|
||||
@ -15167,6 +15190,11 @@ memfs@^3.1.2, memfs@^3.2.2, memfs@^3.4.1:
|
||||
dependencies:
|
||||
fs-monkey "1.0.3"
|
||||
|
||||
"memoize-one@>=3.1.1 <6":
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e"
|
||||
integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==
|
||||
|
||||
memoizerific@^1.11.3:
|
||||
version "1.11.3"
|
||||
resolved "https://registry.yarnpkg.com/memoizerific/-/memoizerific-1.11.3.tgz#7c87a4646444c32d75438570905f2dbd1b1a805a"
|
||||
@ -15431,6 +15459,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
|
||||
dependencies:
|
||||
minimist "^1.2.6"
|
||||
|
||||
mock-socket@^9.1.0:
|
||||
version "9.1.3"
|
||||
resolved "https://registry.yarnpkg.com/mock-socket/-/mock-socket-9.1.3.tgz#bcb106c6b345001fa7619466fcf2f8f5a156b10f"
|
||||
integrity sha512-uz8lx8c5wuJYJ21f5UtovqpV0+KJuVwE7cVOLNhrl2QW/CvmstOLRfjXnLSbfFHZtJtiaSGQu0oCJA8SmRcK6A==
|
||||
|
||||
module-deps@^6.2.3:
|
||||
version "6.2.3"
|
||||
resolved "https://registry.yarnpkg.com/module-deps/-/module-deps-6.2.3.tgz#15490bc02af4b56cf62299c7c17cba32d71a96ee"
|
||||
@ -17832,6 +17865,19 @@ react-virtualized-auto-sizer@^1.0.4, react-virtualized-auto-sizer@^1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.6.tgz#66c5b1c9278064c5ef1699ed40a29c11518f97ca"
|
||||
integrity sha512-7tQ0BmZqfVF6YYEWcIGuoR3OdYe8I/ZFbNclFlGOC3pMqunkYF/oL30NCjSGl9sMEb17AnzixDz98Kqc3N76HQ==
|
||||
|
||||
react-window-infinite-loader@^1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/react-window-infinite-loader/-/react-window-infinite-loader-1.0.7.tgz#958ef1a689d20dce122ef377583acd987760aee8"
|
||||
integrity sha512-wg3LWkUpG21lhv+cZvNy+p0+vtclZw+9nP2vO6T9PKT50EN1cUq37Dq6FzcM38h/c2domE0gsUhb6jHXtGogAA==
|
||||
|
||||
react-window@^1.8.7:
|
||||
version "1.8.7"
|
||||
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.7.tgz#5e9fd0d23f48f432d7022cdb327219353a15f0d4"
|
||||
integrity sha512-JHEZbPXBpKMmoNO1bNhoXOOLg/ujhL/BU4IqVU9r8eQPcy5KQnGHIHDRkJ0ns9IM5+Aq5LNwt3j8t3tIrePQzA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
memoize-one ">=3.1.1 <6"
|
||||
|
||||
react@17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
|
||||
|
Loading…
Reference in New Issue
Block a user