diff --git a/apps/explorer/src/app/components/splash-loader/index.ts b/apps/explorer/src/app/components/splash-loader/index.ts deleted file mode 100644 index 05799d18e..000000000 --- a/apps/explorer/src/app/components/splash-loader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./splash-loader"; diff --git a/apps/explorer/src/app/components/splash-loader/splash-loader.scss b/apps/explorer/src/app/components/splash-loader/splash-loader.scss deleted file mode 100644 index 2dbfb3c06..000000000 --- a/apps/explorer/src/app/components/splash-loader/splash-loader.scss +++ /dev/null @@ -1,22 +0,0 @@ -@import "../../styles/colors"; - -.loading { - display: flex; - flex-direction: column; - align-items: center; - - &__animation { - display: flex; - flex-wrap: wrap; - width: 50px; - height: 50px; - margin-bottom: 20px; - - div { - width: 10px; - height: 10px; - background: white; - opacity: 0; - } - } -} diff --git a/apps/explorer/src/app/components/splash-loader/splash-loader.tsx b/apps/explorer/src/app/components/splash-loader/splash-loader.tsx deleted file mode 100644 index 3ffd85c61..000000000 --- a/apps/explorer/src/app/components/splash-loader/splash-loader.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import './splash-loader.scss'; - -import React from 'react'; - -export const SplashLoader = ({ text = 'Loading' }: { text?: string }) => { - const [, forceRender] = React.useState(false); - React.useEffect(() => { - const interval = setInterval(() => { - forceRender((x) => !x); - }, 100); - - return () => clearInterval(interval); - }, []); - - return ( -
-
- {new Array(25).fill(null).map((_, i) => { - return ( -
0.75 ? 1 : 0, - }} - /> - ); - })} -
-
{text}
-
- ); -}; diff --git a/apps/explorer/src/app/components/splash-screen/index.ts b/apps/explorer/src/app/components/splash-screen/index.ts deleted file mode 100644 index 8ec0f61c5..000000000 --- a/apps/explorer/src/app/components/splash-screen/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./splash-screen"; diff --git a/apps/explorer/src/app/components/splash-screen/splash-screen.scss b/apps/explorer/src/app/components/splash-screen/splash-screen.scss deleted file mode 100644 index 38da6eb3a..000000000 --- a/apps/explorer/src/app/components/splash-screen/splash-screen.scss +++ /dev/null @@ -1,12 +0,0 @@ -@import "../../styles/colors"; - -.splash-screen { - display: flex; - justify-content: center; - align-items: center; - text-align: center; - width: 100%; - height: 100%; - font-size: 20px; - color: $white; -} diff --git a/apps/explorer/src/app/components/splash-screen/splash-screen.tsx b/apps/explorer/src/app/components/splash-screen/splash-screen.tsx deleted file mode 100644 index 1d60c8edd..000000000 --- a/apps/explorer/src/app/components/splash-screen/splash-screen.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import './splash-screen.scss'; - -import React from 'react'; - -export const SplashScreen = ({ children }: { children: React.ReactNode }) => { - return
{children}
; -}; diff --git a/apps/explorer/src/app/contexts/websocket/tendermint-websocket-provider.tsx b/apps/explorer/src/app/contexts/websocket/tendermint-websocket-provider.tsx index 4dc267a23..dd25edd95 100644 --- a/apps/explorer/src/app/contexts/websocket/tendermint-websocket-provider.tsx +++ b/apps/explorer/src/app/contexts/websocket/tendermint-websocket-provider.tsx @@ -1,10 +1,9 @@ -import React, { useState } from "react"; -import useWebSocket from "react-use-websocket"; +import React, { useState } from 'react'; +import useWebSocket from 'react-use-websocket'; -import { SplashLoader } from "../../components/splash-loader"; -import { SplashScreen } from "../../components/splash-screen"; -import { DATA_SOURCES } from "../../config"; -import { TendermintWebsocketContext } from "./tendermint-websocket-context"; +import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; +import { DATA_SOURCES } from '../../config'; +import { TendermintWebsocketContext } from './tendermint-websocket-context'; /** * Provides a single, shared, websocket instance to the entire app to prevent recreation on every render @@ -19,9 +18,9 @@ export const TendermintWebsocketProvider = ({ if (!contextShape) { return ( - - - + + + ); } diff --git a/apps/explorer/src/app/hooks/use-fetch.tsx b/apps/explorer/src/app/hooks/use-fetch.tsx index 935fb18e6..94945ebab 100644 --- a/apps/explorer/src/app/hooks/use-fetch.tsx +++ b/apps/explorer/src/app/hooks/use-fetch.tsx @@ -18,12 +18,12 @@ type Action = | { type: ActionType.FETCHED; payload: T } | { type: ActionType.ERROR; error: Error }; -function useFetch( - url?: string, +function useFetch( + url: string, options?: RequestInit ): { state: State; refetch: () => void } { // Used to prevent state update if the component is unmounted - const cancelRequest = useRef(false); + const cancelRequest = useRef<{ [key: string]: boolean }>({ [url]: false }); const initialState: State = { error: undefined, @@ -61,11 +61,11 @@ function useFetch( // @ts-ignore - data.error throw new Error(data.error); } - if (cancelRequest.current) return; + if (cancelRequest.current[url]) return; dispatch({ type: ActionType.FETCHED, payload: data }); } catch (error) { - if (cancelRequest.current) return; + if (cancelRequest.current[url]) return; dispatch({ type: ActionType.ERROR, error: error as Error }); } @@ -78,13 +78,15 @@ function useFetch( }, [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 () => { - cancelRequest.current = true; + cancel[url] = true; }; - }, [fetchCallback]); + }, [fetchCallback, url]); return { state, diff --git a/apps/explorer/src/app/routes/blocks/id/block.spec.tsx b/apps/explorer/src/app/routes/blocks/id/block.spec.tsx new file mode 100644 index 000000000..5a1f358f1 --- /dev/null +++ b/apps/explorer/src/app/routes/blocks/id/block.spec.tsx @@ -0,0 +1,181 @@ +import { Block } from './block'; +import { + fireEvent, + render, + screen, + waitFor, + act, +} from '@testing-library/react'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { Routes as RouteNames } from '../../router-config'; + +const blockId = 1085890; + +const createBlockResponse = () => { + return { + jsonrpc: '2.0', + id: -1, + result: { + block_id: { + hash: '26D92E4B0C892AC6EF33A281185E612671976AD9C629F6C8182C1D92C2CE2F6F', + parts: { + total: 1, + hash: 'D18AEBADEAADA2C701FFFAD5A16EE8C493C5D1B54589FD1587EA5C61B7179AAC', + }, + }, + block: { + header: { + version: { + block: '11', + app: '1', + }, + chain_id: 'testnet-12cd7b', + height: '1085891', + time: '2022-03-24T11:03:40.014303953Z', + last_block_id: { + hash: 'C50CA169545AC1280220433D7971C50D941F675E9B0FFF358ABE8F3A7F74AE0E', + parts: { + total: 1, + hash: '86974C6359B39084235EE31C1389DEA052E01E552CD1D113B3222A63A8DF390C', + }, + }, + last_commit_hash: + 'D8FBE7DEB393D740B22EF8E91DA426494E2535902A6FB89B1D754F0DAF74DB37', + data_hash: + 'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', + validators_hash: + '2BC96D9FD4A7663A270909F7E604C24E1F8C87605F913F6DA55AF2DDE023BAC9', + next_validators_hash: + '2BC96D9FD4A7663A270909F7E604C24E1F8C87605F913F6DA55AF2DDE023BAC9', + consensus_hash: + '048091BC7DDC283F77BFBF91D73C44DA58C3DF8A9CBC867405D8B7F3DAADA22F', + app_hash: + 'B04B71A61C9970A132631FFBA50E36B9C5A8A490983E803F6295133C255D3FCE', + last_results_hash: + 'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', + evidence_hash: + 'E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855', + proposer_address: '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21', + }, + data: { + txs: [], + }, + evidence: { + evidence: [], + }, + last_commit: { + height: blockId.toString(), + round: 0, + block_id: { + hash: 'C50CA169545AC1280220433D7971C50D941F675E9B0FFF358ABE8F3A7F74AE0E', + parts: { + total: 1, + hash: '86974C6359B39084235EE31C1389DEA052E01E552CD1D113B3222A63A8DF390C', + }, + }, + signatures: [ + { + block_id_flag: 2, + validator_address: '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21', + timestamp: '2022-03-24T11:03:40.026173466Z', + signature: + '/BbNDfNflmhL5eNmpijxjjuLV8WJ1SkoesIThcpvxSjUhf+8tjZ+mIUkXig7xD5JB/7X23l6eEsbrBLxG6ppBA==', + }, + { + block_id_flag: 2, + validator_address: '31D6EBD2A8E40524142613A241CA1D2056159EF4', + timestamp: '2022-03-24T11:03:40.014303953Z', + signature: + 'zJ717hzAyUN0qdfjtXHHQP05oKeGPSL5HOZ8syU6M0kj3C5fuP+IG6PdVHj26ZKthTyRhEyHcMBJ/FHu2s5MBw==', + }, + { + block_id_flag: 2, + validator_address: '6DB7E2A705ABF86C6B4A4817E778669D45421166', + timestamp: '2022-03-24T11:03:39.991116117Z', + signature: + 'lRwyqUnIBqyyL9XHfgTdfABVT3B3T9aIb7HP656TcqOf1d1hmnZ8oZGXeKc5SNpssJSlHl9V/F9k2LZtHChKBg==', + }, + { + block_id_flag: 2, + validator_address: 'A5429AF24A820AFD9C3D21507C8642F27F5DD308', + timestamp: '2022-03-24T11:03:39.988302733Z', + signature: + 'ARjFOJger/wlBwMap3DaMhYKe9ywkQg/rxVCLZ0MMwdhAkviC8gvZRwoDajbKuYgbsgG1MwsGk/mEib5O5cBBA==', + }, + { + block_id_flag: 2, + validator_address: 'AE5B9A8193AEFC405C159C930ED2BBF40A806785', + timestamp: '2022-03-24T11:03:40.020448546Z', + signature: + 'o2z4gdBiNUskFQ4m/yb+uM0/jaOf1p6jpGlKoEhebn2ExreaayN/JJR8F98uWk1M4S0zK9trI9oWDgwmxo5CAg==', + }, + ], + }, + }, + }, + }; +}; + +const renderComponent = () => { + return ( + + + } /> + + + ); +}; + +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(); +}); + +describe('Block', () => { + 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; + render(renderComponent()); + await waitFor(() => screen.getByTestId('block-header')); + + expect(screen.getByTestId('block-header')).toHaveTextContent( + `BLOCK ${blockId}` + ); + const proposer = screen.getByTestId('block-validator'); + expect(proposer).toHaveTextContent( + '1C9B6E2708F8217F8D5BFC8D8734ED9A5BC19B21' + ); + expect(proposer).toHaveAttribute('href', `/${RouteNames.VALIDATORS}`); + expect(screen.getByTestId('block-time')).toHaveTextContent( + '3528 seconds ago' + ); + }); + + it('renders next and previous buttons', async () => { + render(renderComponent()); + await waitFor(() => screen.getByTestId('block-header')); + + expect(screen.getByTestId('previous-block')).toHaveAttribute( + 'href', + `/${RouteNames.BLOCKS}/${blockId - 1}` + ); + expect(screen.getByTestId('next-block')).toHaveAttribute( + 'href', + `/${RouteNames.BLOCKS}/${blockId + 1}` + ); + }); +}); diff --git a/apps/explorer/src/app/routes/blocks/id/block.tsx b/apps/explorer/src/app/routes/blocks/id/block.tsx new file mode 100644 index 000000000..0cddc8a28 --- /dev/null +++ b/apps/explorer/src/app/routes/blocks/id/block.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import { Link, useParams } from 'react-router-dom'; +import { DATA_SOURCES } from '../../../config'; +import useFetch from '../../../hooks/use-fetch'; +import { TendermintBlocksResponse } from '../tendermint-blocks-response'; +import { RouteTitle } from '../../../components/route-title'; +import { SecondsAgo } from '../../../components/seconds-ago'; +import { + Table, + TableRow, + TableHeader, + TableCell, +} from '../../../components/table'; +import { TxsPerBlock } from '../../../components/txs/txs-per-block'; +import { Button } from '@vegaprotocol/ui-toolkit'; +import { Routes } from '../../router-config'; +import { RenderFetched } from '../../../components/render-fetched'; + +const Block = () => { + const { block } = useParams<{ block: string }>(); + const { + state: { data: blockData, loading, error }, + } = useFetch( + `${DATA_SOURCES.tendermintUrl}/block?height=${block}` + ); + + const header = blockData?.result.block.header; + if (!header) { + return

Could not get block data

; + } + + return ( +
+ BLOCK {block} + + <> +
+ + + + + + +
+ + + Mined by + + + {header.proposer_address} + + + + + Time + + + + +
+ {blockData && blockData.result.block.data.txs.length > 0 ? ( + + ) : null} + +
+
+ ); +}; + +export { Block }; diff --git a/apps/explorer/src/app/routes/blocks/id/index.tsx b/apps/explorer/src/app/routes/blocks/id/index.tsx index 8a335394e..d814a5a7c 100644 --- a/apps/explorer/src/app/routes/blocks/id/index.tsx +++ b/apps/explorer/src/app/routes/blocks/id/index.tsx @@ -1,60 +1 @@ -import React from 'react'; -import { Link, useParams } from 'react-router-dom'; -import { DATA_SOURCES } from '../../../config'; -import useFetch from '../../../hooks/use-fetch'; -import { TendermintBlocksResponse } from '../tendermint-blocks-response'; -import { RouteTitle } from '../../../components/route-title'; -import { SecondsAgo } from '../../../components/seconds-ago'; -import { - Table, - TableRow, - TableHeader, - TableCell, -} from '../../../components/table'; -import { TxsPerBlock } from '../../../components/txs/txs-per-block'; - -const Block = () => { - const { block } = useParams<{ block: string }>(); - const { - state: { data: blockData }, - } = useFetch( - `${DATA_SOURCES.tendermintUrl}/block?height=${block}` - ); - - const header = blockData?.result.block.header; - - if (!header) { - return <>Could not get block data; - } - - return ( -
- BLOCK {block} - - - Mined by - - - {header.proposer_address} - - - - - Time - - - - -
- {blockData?.result.block.data.txs.length > 0 && ( - - )} -
- ); -}; - -export { Block }; +export * from './block'; diff --git a/apps/explorer/src/app/routes/index.tsx b/apps/explorer/src/app/routes/index.tsx index 350b6e59e..edbcf7df0 100644 --- a/apps/explorer/src/app/routes/index.tsx +++ b/apps/explorer/src/app/routes/index.tsx @@ -2,9 +2,8 @@ import React from 'react'; import { useRoutes } from 'react-router-dom'; import { RouteErrorBoundary } from '../components/router-error-boundary'; -import { SplashLoader } from '../components/splash-loader'; -import { SplashScreen } from '../components/splash-screen'; import routerConfig from './router-config'; +import { Loader, Splash } from '@vegaprotocol/ui-toolkit'; export interface RouteChildProps { name: string; @@ -14,9 +13,9 @@ export const AppRouter = () => { const routes = useRoutes(routerConfig); const splashLoading = ( - - - + + + ); return ( diff --git a/apps/explorer/src/app/routes/parties/home/index.tsx b/apps/explorer/src/app/routes/parties/home/index.tsx index a1f7ee6f1..327a00757 100644 --- a/apps/explorer/src/app/routes/parties/home/index.tsx +++ b/apps/explorer/src/app/routes/parties/home/index.tsx @@ -7,23 +7,19 @@ import { Routes } from '../../router-config'; export const JumpToParty = () => { const navigate = useNavigate(); - const handleSubmit = React.useCallback( - () => (e: React.SyntheticEvent) => { - e.preventDefault(); + const handleSubmit = (e: React.SyntheticEvent) => { + e.preventDefault(); - const target = e.target as typeof e.target & { - partyId: { value: number }; - }; + const target = e.target as typeof e.target & { + partyId: { value: number }; + }; - const partyId = target.partyId.value; - - if (partyId) { - navigate(`/${Routes.PARTIES}/${partyId}`); - } - }, - [navigate] - ); + const partyId = target.partyId.value; + if (partyId) { + navigate(`/${Routes.PARTIES}/${partyId}`); + } + }; return (