Created unstyled block explorer blocks page, with new refresh method from useFetch, and a 'navigate to' component

This commit is contained in:
sam-keen 2022-03-02 10:26:41 +00:00 committed by Dexter Edwards
parent e467344a4d
commit f88661e422
17 changed files with 299 additions and 181 deletions

View File

@ -0,0 +1,47 @@
import { TendermintBlockchainResponse } from '../../../routes/blocks/tendermint-blockchain-response';
import { Link } from 'react-router-dom';
import { SecondsAgo } from '../../seconds-ago';
interface BlocksProps {
data: TendermintBlockchainResponse | undefined;
showExpanded?: boolean;
}
export const BlocksTable = ({ data }: BlocksProps) => {
if (!data?.result) {
return <>Awaiting block data</>;
}
return (
<table>
<tbody>
{data.result?.block_metas?.map((block) => {
return (
<tr>
<td>
<Link to={`/blocks/${block.header?.height}`}>
{block.header?.height}
</Link>
</td>
<td>
{block.num_txs === '1'
? '1 transaction'
: `${block.num_txs} transactions`}
</td>
{block.header?.proposer_address && (
<td>
<Link to={`/validators/${block.header.proposer_address}`}>
{block.header.proposer_address}
</Link>
</td>
)}
<td>
<SecondsAgo date={block.header?.time} />
</td>
</tr>
);
})}
</tbody>
</table>
);
};

View File

@ -0,0 +1 @@
export { BlocksTable } from './home/blocksTable';

View File

@ -0,0 +1,27 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
export const JumpToBlock = () => {
const navigate = useNavigate();
const handleSubmit = (e: React.SyntheticEvent) => {
e.preventDefault();
const target = e.target as typeof e.target & {
blockNumber: { value: number };
};
const blockNumber = target.blockNumber.value;
if (blockNumber) {
navigate(`/blocks/${blockNumber}`);
}
};
return (
<form onSubmit={handleSubmit}>
<input type={'tel'} name={'blockNumber'} placeholder={'Block number'} />
<input type={'submit'} value={'Go'} />
</form>
);
};

View File

@ -0,0 +1,28 @@
import { useEffect, useState } from 'react';
interface SecondsAgoProps {
date: string;
}
export const SecondsAgo = ({ date }: SecondsAgoProps) => {
const [now, setNow] = useState(Date.now());
useEffect(() => {
const int = setInterval(() => {
setNow(Date.now());
}, 500);
return () => clearInterval(int);
}, [setNow]);
if (!date) {
return <>Date unknown</>;
}
const timeAgoInSeconds = Math.floor((now - new Date(date).getTime()) / 1000);
return (
<div>
{timeAgoInSeconds === 1 ? '1 second' : `${timeAgoInSeconds} seconds`} ago
</div>
);
};

View File

@ -1,7 +1,13 @@
import { TendermintUnconfirmedTransactionsResponse } from '../../../routes/txs/tendermint-unconfirmed-transactions-response.d';
interface TxsProps {
data: string | undefined;
data: TendermintUnconfirmedTransactionsResponse | undefined;
}
export const Txs = ({ data }: TxsProps) => {
return <>Blah</>;
export const TxList = ({ data }: TxsProps) => {
if (!data) {
return <div>Awaiting transactions</div>;
}
return <div>{JSON.stringify(data, null, ' ')}</div>;
};

View File

@ -0,0 +1,37 @@
import { Codeblock } from '../../codeblock';
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
interface TxContentProps {
data: ChainExplorerTxResponse | undefined;
}
export const TxContent = ({ data }: TxContentProps) => {
if (!data?.Command) {
return <>Awaiting decoded transaction data</>;
}
const Command = JSON.parse(data.Command);
const displayCode = `{
"market": "${Command.marketId}",
"type": "${Command.type}",
"side": "${Command.side}",
"size": "${Command.size}",
}`;
return (
<>
<table>
<tbody>
<tr>
<td>Type</td>
<td>{data.Type}</td>
</tr>
</tbody>
</table>
<h3>Decoded transaction content</h3>
<Codeblock code={displayCode} language={'javascript'} />
</>
);
};

View File

@ -0,0 +1,48 @@
import { Link } from 'react-router-dom';
import { Routes } from '../../../routes/router-config';
import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
interface TxDetailsProps {
txData: Result | undefined;
pubKey: string | undefined;
}
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
if (!txData) {
return <>Awaiting Tendermint transaction details</>;
}
return (
<table>
<tbody>
<tr>
<td>Hash</td>
<td>{txData.hash}</td>
</tr>
{pubKey ? (
<tr>
<td>Submitted by</td>
<td>
<Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link>
</td>
</tr>
) : (
<tr>
<td>Submitted by</td>
<td>Awaiting decoded transaction data</td>
</tr>
)}
{txData.height ? (
<tr>
<td>Block</td>
<td>{txData.height}</td>
</tr>
) : null}
<tr>
<td>Encoded tnx</td>
<td>{txData.tx}</td>
</tr>
</tbody>
</table>
);
};

View File

@ -1,85 +1,3 @@
import { Link } from 'react-router-dom';
import { Routes } from '../../routes/router-config';
import { Codeblock } from '../codeblock';
import { ChainExplorerTxResponse } from '../../routes/types/chain-explorer-response';
import { Result } from '../../routes/txs/tendermint-transaction-response.d';
interface TxDetailsProps {
txData: Result | undefined;
pubKey: string | undefined;
}
interface TxContentProps {
data: ChainExplorerTxResponse | undefined;
}
export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
if (!txData) {
return <>Awaiting Tendermint transaction details</>;
}
return (
<table>
<tbody>
<tr>
<td>Hash</td>
<td>{txData.hash}</td>
</tr>
{pubKey ? (
<tr>
<td>Submitted by</td>
<td>
<Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link>
</td>
</tr>
) : (
<tr>
<td>Submitted by</td>
<td>Awaiting decoded transaction data</td>
</tr>
)}
{txData.height ? (
<tr>
<td>Block</td>
<td>{txData.height}</td>
</tr>
) : null}
<tr>
<td>Encoded tnx</td>
<td>{txData.tx}</td>
</tr>
</tbody>
</table>
);
};
export const TxContent = ({ data }: TxContentProps) => {
if (!data?.Command) {
return <>Awaiting decoded transaction data</>;
}
const Command = JSON.parse(data.Command);
const displayCode = `{
"market": "${Command.marketId}",
"type": "${Command.type}",
"side": "${Command.side}",
"size": "${Command.size}",
}`;
return (
<>
<table>
<tbody>
<tr>
<td>Type</td>
<td>{data.Type}</td>
</tr>
</tbody>
</table>
<h3>Decoded transaction content</h3>
<Codeblock code={displayCode} language={'javascript'} />
</>
);
};
export { TxDetails } from './id/txDetails';
export { TxContent } from './id/txContent';
export { TxList } from './home/txList';

View File

@ -1,4 +1,4 @@
import { useEffect, useReducer, useRef } from "react";
import { useCallback, useEffect, useReducer, useRef } from 'react';
interface State<T> {
data?: T;
@ -7,9 +7,9 @@ interface State<T> {
}
enum ActionType {
LOADING = "LOADING",
ERROR = "ERROR",
FETCHED = "FETCHED",
LOADING = 'LOADING',
ERROR = 'ERROR',
FETCHED = 'FETCHED',
}
// discriminated union type
@ -18,7 +18,10 @@ type Action<T> =
| { type: ActionType.FETCHED; payload: T }
| { type: ActionType.ERROR; error: Error };
function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
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);
@ -41,9 +44,7 @@ function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
};
const [state, dispatch] = useReducer(fetchReducer, initialState);
useEffect(() => {
// Do nothing if the url is not given
const fetchCallback = useCallback(() => {
if (!url) return;
const fetchData = async () => {
@ -56,8 +57,8 @@ function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
}
const data = (await response.json()) as T;
if ("error" in data) {
// @ts-ignore
if ('error' in data) {
// @ts-ignore - data.error
throw new Error(data.error);
}
if (cancelRequest.current) return;
@ -72,15 +73,23 @@ function useFetch<T = unknown>(url?: string, options?: RequestInit): State<T> {
void fetchData();
// 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;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [url]);
// Do nothing if the url is not given
}, [fetchCallback]);
return state;
return {
state,
refetch: fetchCallback,
};
}
export default useFetch;

View File

@ -1,40 +1,27 @@
import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
import { NewBlockMessage } from '../tendermint-new-block';
import { TendermintBlockchainResponse } from '../tendermint-blockchain-response';
import { useTendermintWebsocket } from '../../../hooks/use-tendermint-websocket';
const MAX_BLOCKS = 10;
import { BlocksTable } from '../../../components/blocks';
import { JumpToBlock } from '../../../components/jump-to-block';
const Blocks = () => {
const { messages: blocks } = useTendermintWebsocket<NewBlockMessage>(
{
query: "tm.event = 'NewBlock'",
},
MAX_BLOCKS
);
const { data } = useFetch<TendermintBlockchainResponse>(
const {
state: { data },
refetch,
} = useFetch<TendermintBlockchainResponse>(
`${DATA_SOURCES.tendermintUrl}/blockchain`
);
return (
<section>
<h1>Blocks</h1>
<h2>Blocks from blockchain</h2>
{`${DATA_SOURCES.tendermintUrl}/blockchain`}
<br />
<div>Height: {data?.result?.last_height || 0}</div>
<br />
<div>
<br />
<pre>{JSON.stringify(data, null, ' ')}</pre>
</div>
<h2>Blocks streamed in</h2>
<div>
<br />
<pre>{JSON.stringify(blocks, null, ' ')}</pre>
</div>
</section>
<>
<section>
<h1>Blocks</h1>
<button onClick={() => refetch()}>Refresh to see latest blocks</button>
<BlocksTable data={data} />
</section>
<JumpToBlock />
</>
);
};

View File

@ -7,22 +7,24 @@ import { TendermintBlocksResponse } from '../tendermint-blocks-response';
const Block = () => {
const { block } = useParams<{ block: string }>();
const { data: decodedBlockData } = useFetch<ChainExplorerTxResponse[]>(
DATA_SOURCES.chainExplorerUrl,
{
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
block_height: parseInt(block!),
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
}
);
const {
state: { data: decodedBlockData },
} = useFetch<ChainExplorerTxResponse[]>(DATA_SOURCES.chainExplorerUrl, {
method: 'POST',
mode: 'cors',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
block_height: parseInt(block!),
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
});
const { data: blockData } = useFetch<TendermintBlocksResponse>(
const {
state: { data: blockData },
} = useFetch<TendermintBlocksResponse>(
`${DATA_SOURCES.tendermintUrl}/block?height=${block}`
);

View File

@ -4,7 +4,9 @@ import useFetch from '../../hooks/use-fetch';
import { TendermintGenesisResponse } from './tendermint-genesis-response';
const Genesis = () => {
const { data: genesis } = useFetch<TendermintGenesisResponse>(
const {
state: { data: genesis },
} = useFetch<TendermintGenesisResponse>(
`${DATA_SOURCES.tendermintUrl}/genesis`
);
if (!genesis?.result.genesis) return null;

View File

@ -3,7 +3,7 @@
// @generated
// This file was automatically generated and should not be edited.
import { AccountType } from './../../../../../__generated__/globalTypes';
import { AccountType } from '../../../../../__generated__/globalTypes';
// ====================================================
// GraphQL query operation: PartyAssetsQuery
@ -123,5 +123,5 @@ export interface PartyAssetsQuery {
}
export interface PartyAssetsQueryVariables {
partyId: string;
partyId: string | undefined;
}

View File

@ -47,7 +47,9 @@ const PARTY_ASSETS_QUERY = gql`
const Party = () => {
const { party } = useParams<{ party: string }>();
const { data: partyData } = useFetch<TendermintSearchTransactionResponse>(
const {
state: { data: partyData },
} = useFetch<TendermintSearchTransactionResponse>(
`${DATA_SOURCES.tendermintWebsocketUrl}/tx_search?query="tx.submitter=%27${party}%27"`
);
@ -56,7 +58,7 @@ const Party = () => {
{
// Don't cache data for this query, party information can move quite quickly
fetchPolicy: 'network-only',
variables: { partyId: party!.replace('0x', '') },
variables: { partyId: party?.replace('0x', '') },
skip: !party,
}
);

View File

@ -1,25 +1,26 @@
import React from "react";
import { DATA_SOURCES } from "../../../config";
import useFetch from "../../../hooks/use-fetch";
import { TendermintUnconfirmedTransactionsResponse } from "../tendermint-unconfirmed-transactions-response.d";
import React from 'react';
import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
import { TendermintUnconfirmedTransactionsResponse } from '../tendermint-unconfirmed-transactions-response.d';
import { TxList } from '../../../components/txs';
const Txs = () => {
const { data: unconfirmedTransactions } =
useFetch<TendermintUnconfirmedTransactionsResponse>(
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
);
const {
state: { data: unconfirmedTransactions },
} = useFetch<TendermintUnconfirmedTransactionsResponse>(
`${DATA_SOURCES.tendermintUrl}/unconfirmed_txs`
);
return (
<section>
<h1>Tx</h1>
<h2>Unconfirmed transactions</h2>
<h1>Unconfirmed transactions</h1>
https://lb.testnet.vega.xyz/tm/unconfirmed_txs
<br />
<div>Number: {unconfirmedTransactions?.result?.n_txs || 0}</div>
<br />
<div>
<br />
{JSON.stringify(unconfirmedTransactions, null, " ")}
<TxList data={unconfirmedTransactions} />
</div>
</section>
);

View File

@ -4,23 +4,24 @@ import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch';
import { ChainExplorerTxResponse } from '../../types/chain-explorer-response';
import { TendermintTransactionResponse } from '../tendermint-transaction-response.d';
import { TxDetails, TxContent } from '../../../components/transaction';
import { TxDetails, TxContent } from '../../../components/txs';
const Tx = () => {
const { txHash } = useParams<{ txHash: string }>();
const { data: transactionData } = useFetch<TendermintTransactionResponse>(
const {
state: { data: transactionData },
} = useFetch<TendermintTransactionResponse>(
`${DATA_SOURCES.tendermintUrl}/tx?hash=${txHash}`
);
const { data: decodedData } = useFetch<ChainExplorerTxResponse>(
DATA_SOURCES.chainExplorerUrl,
{
method: 'POST',
body: JSON.stringify({
tx_hash: txHash,
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
}
);
const {
state: { data: decodedData },
} = useFetch<ChainExplorerTxResponse>(DATA_SOURCES.chainExplorerUrl, {
method: 'POST',
body: JSON.stringify({
tx_hash: txHash,
node_url: `${DATA_SOURCES.tendermintUrl}/`,
}),
});
return (
<section>

View File

@ -1,9 +1,9 @@
import { gql, useQuery } from "@apollo/client";
import React from "react";
import { DATA_SOURCES } from "../../config";
import useFetch from "../../hooks/use-fetch";
import { TendermintValidatorsResponse } from "./tendermint-validator-response";
import { NodesQuery } from "./__generated__/NodesQuery";
import { gql, useQuery } from '@apollo/client';
import React from 'react';
import { DATA_SOURCES } from '../../config';
import useFetch from '../../hooks/use-fetch';
import { TendermintValidatorsResponse } from './tendermint-validator-response';
import { NodesQuery } from './__generated__/NodesQuery';
const NODES_QUERY = gql`
query NodesQuery {
@ -34,7 +34,9 @@ const NODES_QUERY = gql`
`;
const Validators = () => {
const { data: validators } = useFetch<TendermintValidatorsResponse>(
const {
state: { data: validators },
} = useFetch<TendermintValidatorsResponse>(
`${DATA_SOURCES.tendermintUrl}/validators`
);
const { data } = useQuery<NodesQuery>(NODES_QUERY);
@ -43,9 +45,9 @@ const Validators = () => {
<section>
<h1>Validators</h1>
<h2>Tendermint data</h2>
<pre>{JSON.stringify(validators, null, " ")}</pre>
<pre>{JSON.stringify(validators, null, ' ')}</pre>
<h2>Vega data</h2>
<pre>{JSON.stringify(data, null, " ")}</pre>
<pre>{JSON.stringify(data, null, ' ')}</pre>
</section>
);
};