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 (