Merge pull request #58 from vegaprotocol/feat/41-blocks-styling

Feat/41 blocks styling
This commit is contained in:
Sam Keen 2022-03-15 13:27:13 +00:00 committed by GitHub
commit c2e4f1d007
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 465 additions and 140 deletions

View File

@ -0,0 +1,60 @@
import React from 'react';
import { BlockMeta } from '../../routes/blocks/tendermint-blockchain-response';
import { Routes } from '../../routes/router-config';
import { Link } from 'react-router-dom';
import { SecondsAgo } from '../seconds-ago';
import { Table, TableRow, TableCell } from '../table';
interface BlockProps {
block: BlockMeta;
className?: string;
}
export const BlockData = ({ block, className }: BlockProps) => {
return (
<Table
aria-label={`Data for block ${block.header?.height}`}
className={className}
>
<TableRow data-testid="block-row" modifier="background">
<TableCell
data-testid="block-height"
className="pl-4 py-2"
aria-label="Block height"
>
<Link
to={`/${Routes.BLOCKS}/${block.header?.height}`}
className="text-vega-yellow"
>
{block.header?.height}
</Link>
</TableCell>
<TableCell
data-testid="num-txs"
className="px-8 text-center"
aria-label="Number of transactions"
>
{block.num_txs === '1'
? '1 transaction'
: `${block.num_txs} transactions`}
</TableCell>
<TableCell
data-testid="validator-link"
className="px-8 text-center"
aria-label="Validator"
>
<Link to={`/${Routes.VALIDATORS}`}>
{block.header.proposer_address}
</Link>
</TableCell>
<TableCell
data-testid="block-time"
className="text-center pr-28 text-neutral-300"
aria-label="Block genesis"
>
<SecondsAgo date={block.header?.time} />
</TableCell>
</TableRow>
</Table>
);
};

View File

@ -0,0 +1,29 @@
import React from 'react';
import { TendermintBlockchainResponse } from '../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from './block-data';
interface BlocksProps {
data: TendermintBlockchainResponse | undefined;
className?: string;
}
export const BlocksData = ({ data, className }: BlocksProps) => {
if (!data?.result) {
return <div className={className}>Awaiting block data</div>;
}
return (
<ul
aria-label={`Showing ${data.result?.block_metas.length} most recently loaded blocks`}
className={className}
>
{data.result?.block_metas?.map((block, index) => {
return (
<li key={index} data-testid="block-row">
<BlockData block={block} />
</li>
);
})}
</ul>
);
};

View File

@ -0,0 +1,15 @@
interface BlocksRefetchProps {
refetch: () => void;
}
export const BlocksRefetch = ({ refetch }: BlocksRefetchProps) => {
return (
<button
onClick={() => refetch()}
className="underline mb-28"
data-testid="refresh"
>
Refresh to see latest blocks
</button>
);
};

View File

@ -1,53 +0,0 @@
import React from 'react';
import { TendermintBlockchainResponse } from '../../../routes/blocks/tendermint-blockchain-response';
import { Link } from 'react-router-dom';
import { SecondsAgo } from '../../seconds-ago';
import { TxsPerBlock } from '../../txs/txs-per-block';
import { Table } from '../../table';
interface BlocksProps {
data: TendermintBlockchainResponse | undefined;
showTransactions?: boolean;
}
export const BlocksTable = ({ data, showTransactions }: BlocksProps) => {
if (!data?.result) {
return <>No block data</>;
}
return (
<Table>
{data.result?.block_metas?.map((block, index) => {
return (
<React.Fragment key={index}>
<tr data-testid="block-row">
<td data-testid="block-height">
<Link to={`/blocks/${block.header?.height}`}>
{block.header?.height}
</Link>
</td>
<td data-testid="num-txs">
{block.num_txs === '1'
? '1 transaction'
: `${block.num_txs} transactions`}
</td>
<td data-testid="validator-link">
<Link to={`/validators/${block.header?.proposer_address}`}>
{block.header.proposer_address}
</Link>
</td>
<td data-testid="block-time">
<SecondsAgo date={block.header?.time} />
</td>
</tr>
{showTransactions && (
<tr>
<TxsPerBlock blockHeight={block.header?.height} />
</tr>
)}
</React.Fragment>
);
})}
</Table>
);
};

View File

@ -1 +1,3 @@
export { BlocksTable } from './home/blocks-table'; export { BlocksData } from './blocks-data';
export { BlockData } from './block-data';
export { BlocksRefetch } from './blocks-refetch';

View File

@ -20,8 +20,24 @@ export const JumpToBlock = () => {
return ( return (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<input type={'tel'} name={'blockNumber'} placeholder={'Block number'} /> <label
<input type={'submit'} value={'Go'} /> htmlFor="block-input"
className="block uppercase text-h5 font-bold"
>
Jump to block
</label>
<input
id="block-input"
type="tel"
name={'blockNumber'}
placeholder={'Block number'}
className="bg-white-25 border-white border px-8 py-4 placeholder-white-60"
/>
<input
className="border-white border px-28 py-4 cursor-pointer"
type={'submit'}
value={'Go'}
/>
</form> </form>
); );
}; };

View File

@ -0,0 +1,26 @@
import React from 'react';
import { StatusMessage } from '../status-message';
interface RenderFetchedProps {
children: React.ReactElement;
error: Error | undefined;
loading: boolean | undefined;
className?: string;
}
export const RenderFetched = ({
error,
loading,
children,
className,
}: RenderFetchedProps) => {
if (loading) {
return <StatusMessage className={className}>Loading...</StatusMessage>;
}
if (error) {
return <StatusMessage className={className}>Error: {error}</StatusMessage>;
}
return children;
};

View File

@ -0,0 +1,19 @@
import classnames from 'classnames';
import React from 'react';
interface RouteTitleProps {
children: React.ReactNode;
className?: string;
}
export const RouteTitle = ({ children, className }: RouteTitleProps) => {
const classes = classnames(
'font-alpha',
'text-h3',
'uppercase',
'mt-12',
'mb-28',
className
);
return <h1 className={classes}>{children}</h1>;
};

View File

@ -0,0 +1,12 @@
import classnames from 'classnames';
import React from 'react';
interface StatusMessageProps {
children: React.ReactNode;
className?: string;
}
export const StatusMessage = ({ children, className }: StatusMessageProps) => {
const classes = classnames('font-alpha text-h4 mb-28', className);
return <h3 className={classes}>{children}</h3>;
};

View File

@ -1,13 +1,84 @@
import React from 'react'; import React, { ThHTMLAttributes } from 'react';
import classnames from 'classnames';
interface TableProps { interface TableProps extends ThHTMLAttributes<HTMLTableElement> {
children: React.ReactNode; children: React.ReactNode;
className?: string;
} }
export const Table = ({ children }: TableProps) => { interface TableHeaderProps
extends ThHTMLAttributes<HTMLTableHeaderCellElement> {
children: React.ReactNode;
className?: string;
}
interface TableRowProps extends ThHTMLAttributes<HTMLTableRowElement> {
children: React.ReactNode;
className?: string;
modifier?: 'bordered' | 'background';
}
interface TableCellProps extends ThHTMLAttributes<HTMLTableCellElement> {
children: React.ReactNode;
className?: string;
modifier?: 'bordered' | 'background';
}
export const Table = ({ children, className, ...props }: TableProps) => {
const classes = classnames(className, 'overflow-x-auto whitespace-nowrap');
return ( return (
<table> <div className={classes}>
<tbody>{children}</tbody> <table className="w-full" {...props}>
</table> <tbody>{children}</tbody>
</table>
</div>
);
};
export const TableHeader = ({
children,
className,
...props
}: TableHeaderProps) => {
const cellClasses = classnames(className, {
'text-left font-normal': props?.scope === 'row',
});
return (
<th className={cellClasses} {...props}>
{children}
</th>
);
};
export const TableRow = ({
children,
className,
modifier,
...props
}: TableRowProps) => {
const cellClasses = classnames(className, {
'border-b border-white-40': modifier === 'bordered',
'bg-white-25 border-b-4 border-b-black': modifier === 'background',
});
return (
<tr className={cellClasses} {...props}>
{children}
</tr>
);
};
export const TableCell = ({
children,
className,
modifier,
...props
}: TableCellProps) => {
const cellClasses = classnames(className, {
'py-4': modifier === 'bordered',
});
return (
<td className={cellClasses} {...props}>
{children}
</td>
); );
}; };

View File

@ -0,0 +1,54 @@
import * as React from 'react';
const ELLIPSIS = '\u2026';
interface TruncateInlineProps {
text: string | null;
className?: string;
children?: (truncatedText: string) => React.ReactElement;
startChars?: number; // number chars to show before ellipsis
endChars?: number; // number of chars to show after ellipsis
}
/**
* Truncates a string of text from the center showing a specified number
* of characters from the start and end. Optionally takes a children as
* a render props so truncated text can be used inside other elements such
* as links
*/
export function TruncateInline({
text,
className,
children,
startChars,
endChars,
}: TruncateInlineProps) {
if (text === null) {
return <span data-testid="empty-truncation" />;
}
const truncatedText = truncateByChars(text, startChars, endChars);
const wrapperProps = {
title: text,
className,
};
if (children !== undefined) {
return <span {...wrapperProps}>{children(truncatedText)}</span>;
} else {
return <span {...wrapperProps}>{truncatedText}</span>;
}
}
export function truncateByChars(s: string, startChars = 6, endChars = 6) {
// if the text is shorted than the total number of chars to show
// no truncation is needed. Plus one is to account for the ellipsis
if (s.length <= startChars + endChars + 1) {
return s;
}
const start = s.slice(0, startChars);
const end = s.slice(-endChars);
return start + ELLIPSIS + end;
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import { TendermintBlockchainResponse } from '../../../routes/blocks/tendermint-blockchain-response';
import { BlockData } from '../../blocks';
import { TxsPerBlock } from '../txs-per-block';
interface TxsProps {
data: TendermintBlockchainResponse | undefined;
className?: string;
}
export const BlockTxsData = ({ data, className }: TxsProps) => {
if (!data?.result) {
// Data for the block has already been fetched at this point, so no errors
// or loading to deal with. This is specifically the case
// where the data object is not undefined, but lacks a result.
return <div className={className}>No data</div>;
}
return (
<ul
aria-label={`Showing ${data.result?.block_metas.length} most recently loaded blocks and transactions`}
className={className}
>
{data.result?.block_metas?.map((block, index) => {
return (
<li key={index} data-testid="block-row">
<BlockData block={block} className="mb-12" />
<TxsPerBlock blockHeight={block.header.height} />
</li>
);
})}
</ul>
);
};

View File

@ -1,7 +1,7 @@
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Routes } from '../../../routes/router-config'; import { Routes } from '../../../routes/router-config';
import { Result } from '../../../routes/txs/tendermint-transaction-response.d'; import { Result } from '../../../routes/txs/tendermint-transaction-response.d';
import { Table } from '../../table'; import { Table, TableRow, TableCell } from '../../table';
interface TxDetailsProps { interface TxDetailsProps {
txData: Result | undefined; txData: Result | undefined;
@ -15,35 +15,37 @@ export const TxDetails = ({ txData, pubKey }: TxDetailsProps) => {
return ( return (
<Table> <Table>
<tr> <TableRow>
<td>Hash</td> <TableCell>Hash</TableCell>
<td data-testid="hash">{txData.hash}</td> <TableCell data-testid="hash">{txData.hash}</TableCell>
</tr> </TableRow>
{pubKey ? ( {pubKey ? (
<tr> <TableRow>
<td>Submitted by</td> <td>Submitted by</td>
<td data-testid="submitted-by"> <td data-testid="submitted-by">
<Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link> <Link to={`/${Routes.PARTIES}/${pubKey}`}>{pubKey}</Link>
</td> </td>
</tr> </TableRow>
) : ( ) : (
<tr> <TableRow>
<td>Submitted by</td> <td>Submitted by</td>
<td>Awaiting decoded transaction data</td> <td>Awaiting decoded transaction data</td>
</tr> </TableRow>
)} )}
{txData.height ? ( {txData.height ? (
<tr> <TableRow>
<td>Block</td> <td>Block</td>
<td data-testid="block"> <td data-testid="block">
<Link to={`/blocks/${txData.height}`}>{txData.height}</Link> <Link to={`/${Routes.BLOCKS}/${txData.height}`}>
{txData.height}
</Link>
</td> </td>
</tr> </TableRow>
) : null} ) : null}
<tr> <TableRow>
<td>Encoded tnx</td> <td>Encoded tnx</td>
<td data-testid="encoded-tnx">{txData.tx}</td> <td data-testid="encoded-tnx">{txData.tx}</td>
</tr> </TableRow>
</Table> </Table>
); );
}; };

View File

@ -1,3 +1,4 @@
export { TxDetails } from './id/tx-details'; export { TxDetails } from './id/tx-details';
export { TxContent } from './id/tx-content'; export { TxContent } from './id/tx-content';
export { TxList } from './home/tx-list'; export { TxList } from './pending/tx-list';
export { BlockTxsData } from './home/block-txs-data';

View File

@ -1,15 +1,20 @@
import useFetch from '../../../hooks/use-fetch'; import useFetch from '../../../hooks/use-fetch';
import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response'; import { ChainExplorerTxResponse } from '../../../routes/types/chain-explorer-response';
import { Routes } from '../../../routes/router-config';
import { DATA_SOURCES } from '../../../config'; import { DATA_SOURCES } from '../../../config';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { RenderFetched } from '../../render-fetched';
import { TruncateInline } from '../../truncate/truncate';
interface TxsPerBlockProps { interface TxsPerBlockProps {
blockHeight: string | undefined; blockHeight: string | undefined;
} }
const truncateLength = 14;
export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => { export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
const { const {
state: { data: decodedBlockData }, state: { data: decodedBlockData, loading, error },
} = useFetch<ChainExplorerTxResponse[]>(DATA_SOURCES.chainExplorerUrl, { } = useFetch<ChainExplorerTxResponse[]>(DATA_SOURCES.chainExplorerUrl, {
method: 'POST', method: 'POST',
mode: 'cors', mode: 'cors',
@ -24,28 +29,46 @@ export const TxsPerBlock = ({ blockHeight }: TxsPerBlockProps) => {
}); });
return ( return (
<table> <RenderFetched error={error} loading={loading} className="text-body-large">
<thead> <div className="overflow-x-auto whitespace-nowrap mb-28">
<tr> <table className="w-full">
<td>Transaction</td> <thead>
<td>From</td> <tr className="font-mono">
<td>Type</td> <td>Transaction</td>
</tr> <td>From</td>
</thead> <td>Type</td>
<tbody> </tr>
{decodedBlockData && </thead>
decodedBlockData.map(({ TxHash, PubKey, Type }, index) => { <tbody>
return ( {decodedBlockData &&
<tr key={index}> decodedBlockData.map(({ TxHash, PubKey, Type }) => {
<td> return (
<Link to={`/txs/${TxHash}`}>{TxHash}</Link> <tr key={TxHash}>
</td> <td>
<td>{PubKey}</td> <Link to={`/${Routes.TX}/${TxHash}`}>
<td>{Type}</td> <TruncateInline
</tr> text={TxHash}
); startChars={truncateLength}
})} endChars={truncateLength}
</tbody> className="text-vega-yellow font-mono"
</table> />
</Link>
</td>
<td>
<TruncateInline
text={PubKey}
startChars={truncateLength}
endChars={truncateLength}
className="font-mono"
/>
</td>
<td>{Type}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</RenderFetched>
); );
}; };

View File

@ -1,29 +1,30 @@
import { DATA_SOURCES } from '../../../config'; import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch'; import useFetch from '../../../hooks/use-fetch';
import { TendermintBlockchainResponse } from '../tendermint-blockchain-response'; import { TendermintBlockchainResponse } from '../tendermint-blockchain-response';
import { BlocksTable } from '../../../components/blocks'; import { RouteTitle } from '../../../components/route-title';
import { RenderFetched } from '../../../components/render-fetched';
import { BlocksData, BlocksRefetch } from '../../../components/blocks';
import { JumpToBlock } from '../../../components/jump-to-block'; import { JumpToBlock } from '../../../components/jump-to-block';
const Blocks = () => { const Blocks = () => {
const { const {
state: { data }, state: { data, error, loading },
refetch, refetch,
} = useFetch<TendermintBlockchainResponse>( } = useFetch<TendermintBlockchainResponse>(
`${DATA_SOURCES.tendermintUrl}/blockchain` `${DATA_SOURCES.tendermintUrl}/blockchain`
); );
return ( return (
<> <section>
<section> <RouteTitle>Blocks</RouteTitle>
<h1>Blocks</h1> <RenderFetched error={error} loading={loading}>
<button data-testid="refresh" onClick={() => refetch()}> <>
Refresh to see latest blocks <BlocksRefetch refetch={refetch} />
</button> <BlocksData data={data} className="mb-28" />
<BlocksTable data={data} /> </>
</section> </RenderFetched>
<JumpToBlock /> <JumpToBlock />
</> </section>
); );
}; };

View File

@ -3,9 +3,15 @@ import { Link, useParams } from 'react-router-dom';
import { DATA_SOURCES } from '../../../config'; import { DATA_SOURCES } from '../../../config';
import useFetch from '../../../hooks/use-fetch'; import useFetch from '../../../hooks/use-fetch';
import { TendermintBlocksResponse } from '../tendermint-blocks-response'; import { TendermintBlocksResponse } from '../tendermint-blocks-response';
import { RouteTitle } from '../../../components/route-title';
import { TxsPerBlock } from '../../../components/txs/txs-per-block'; import { TxsPerBlock } from '../../../components/txs/txs-per-block';
import { SecondsAgo } from '../../../components/seconds-ago'; import { SecondsAgo } from '../../../components/seconds-ago';
import { Table } from '../../../components/table'; import {
Table,
TableRow,
TableHeader,
TableCell,
} from '../../../components/table';
const Block = () => { const Block = () => {
const { block } = useParams<{ block: string }>(); const { block } = useParams<{ block: string }>();
@ -23,24 +29,29 @@ const Block = () => {
return ( return (
<section> <section>
<h1>BLOCK {block}</h1> <RouteTitle>BLOCK {block}</RouteTitle>
<Table> <Table className="mb-28">
<tr> <TableRow modifier="bordered">
<td>Mined by</td> <TableHeader scope="row">Mined by</TableHeader>
<td> <TableCell modifier="bordered">
<Link to={`/validators/${header.proposer_address}`}> <Link
className="text-vega-yellow"
to={"/validators"}
>
{header.proposer_address} {header.proposer_address}
</Link> </Link>
</td> </TableCell>
</tr> </TableRow>
<tr> <TableRow modifier="bordered">
<td>Time</td> <TableHeader scope="row">Time</TableHeader>
<td> <TableCell modifier="bordered">
<SecondsAgo date={header.time} /> <SecondsAgo date={header.time} />
</td> </TableCell>
</tr> </TableRow>
</Table> </Table>
<TxsPerBlock blockHeight={block} /> {blockData?.result.block.data.txs.length > 0 && (
<TxsPerBlock blockHeight={block} />
)}
</section> </section>
); );
}; };

View File

@ -1,29 +1,31 @@
import useFetch from '../../../hooks/use-fetch'; import useFetch from '../../../hooks/use-fetch';
import { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response'; import { TendermintBlockchainResponse } from '../../blocks/tendermint-blockchain-response';
import { DATA_SOURCES } from '../../../config'; import { DATA_SOURCES } from '../../../config';
import { BlocksTable } from '../../../components/blocks'; import { RouteTitle } from '../../../components/route-title';
import { BlocksRefetch } from '../../../components/blocks';
import { RenderFetched } from '../../../components/render-fetched';
import { BlockTxsData } from '../../../components/txs';
import { JumpToBlock } from '../../../components/jump-to-block'; import { JumpToBlock } from '../../../components/jump-to-block';
const Txs = () => { const Txs = () => {
const { const {
state: { data }, state: { data, error, loading },
refetch, refetch,
} = useFetch<TendermintBlockchainResponse>( } = useFetch<TendermintBlockchainResponse>(
`${DATA_SOURCES.tendermintUrl}/blockchain` `${DATA_SOURCES.tendermintUrl}/blockchain`
); );
return ( return (
<> <section>
<section> <RouteTitle>Transactions</RouteTitle>
<h1>Transactions</h1> <RenderFetched error={error} loading={loading}>
<button data-testid="refresh" onClick={() => refetch()}> <>
Refresh to see latest blocks <BlocksRefetch refetch={refetch} />
</button> <BlockTxsData data={data} />
<BlocksTable data={data} showTransactions={true} /> </>
</section> </RenderFetched>
<JumpToBlock /> <JumpToBlock />
</> </section>
); );
}; };