Skip to content

Commit

Permalink
Block chain depth indicators + mempool UI (#44)
Browse files Browse the repository at this point in the history
* Display last portion of ellipsized text and migrate Block.tsx to atomic CSS

* Fix issue with scrolling selected block into view

* Fix issue with scrolling selected block into view (take 2)

* Add depth link to the top of the block chain

* Add depth bottom link

* Style Mempool UI

* Wire mempool data from RPC

* Improve loading experience of block list, block details, and mempool info

* Lint

* Rename block fixtures according to their height

* Fix SearchBox.test.ts

* Add tests for BlockList

* Lint
  • Loading branch information
msafi authored May 8, 2020
1 parent 362b52e commit 6fd55f4
Show file tree
Hide file tree
Showing 39 changed files with 1,475 additions and 594 deletions.
9 changes: 9 additions & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,15 @@ module.exports = {
"react/jsx-curly-newline": "off",
"react/jsx-wrap-multilines": "off",
"react/jsx-indent": "off",

/**
* I found it very difficult to style a button as an anchor link. It's much
* easier to use an `a` element with an `onClick` handler. Does using
* `role="button"` on the element solve the accessibility problem?
*
* I'd like to hear from someone who knows about this stuff.
*/
"jsx-a11y/anchor-is-valid": "off",
},
settings: {
react: {
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"Executables",
"Mempool",
"SCROLLABLE",
"Satoshi",
"bitcoind",
"clsx",
"formik",
Expand Down
1 change: 1 addition & 0 deletions src/main/mainRpcClient/isRpcMethodAllowed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export const isRpcMethodAllowed = (methodName: RpcRequest["method"]) => {
"getblock",
"getblockhash",
"getrawtransaction",
"getmempoolinfo",
"uptime",
];

Expand Down
2 changes: 1 addition & 1 deletion src/renderer/App/AppBar/AppBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const AppBar: React.FC = () => {
elevation={1}
color="inherit"
className={a(
"borderWidth2",
"borderWidth2px",
"borderTopStyleSolid",
"borderColorSecondaryMain",
)}
Expand Down
21 changes: 17 additions & 4 deletions src/renderer/App/AppBar/SearchBox/SearchBox.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
import { InputBase } from "@material-ui/core";
import { Search } from "@material-ui/icons";
import React from "react";
import React, { useEffect, useRef } from "react";
import { useDispatch, useSelector } from "react-redux";
import * as actions from "_r/redux/actions";
import { useAtomicCss } from "_r/useAtomicCss";
import { testIds } from "_tu/testIds";
import { useSearchBoxStyles } from "./SearchBoxStyles";
import { useSearchHandlers } from "./SearchBoxHooks";
import { useSearchBoxStyles } from "./SearchBoxStyles";

export const SearchBox: React.FC = () => {
const classNames = useSearchBoxStyles();
const a = useAtomicCss();
const { onKeyUp, onChange } = useSearchHandlers();
const inputRef = useRef<HTMLInputElement>(null);
const focusRequested = useSelector(state => state.requestSearchBoxFocus);
const dispatch = useDispatch();

useEffect(() => {
if (focusRequested) {
inputRef.current?.focus();
dispatch(actions.requestSearchBoxFocus(null));
}
});

return (
<div
className={a(
"positionRelative",
"borderRadiusShape",
"backgroundColorBlackFade01",
"hoverBackgroundColorBlackFade012",
"backgroundColorBlack10%Opaque",
"hoverBackgroundColorBlack12%Opaque",
"marginRight02",
"marginLeft10",
"widthAuto",
Expand All @@ -42,6 +54,7 @@ export const SearchBox: React.FC = () => {
"data-testid": testIds.searchInputField,
}}
onChange={onChange}
inputRef={inputRef}
onKeyUp={onKeyUp}
type="search"
placeholder="Search block height or block hash..."
Expand Down
33 changes: 21 additions & 12 deletions src/renderer/App/AppBar/SearchBox/SearchBoxStyles.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
import { makeStyles } from "@material-ui/core";
import { getAtomicCssAndStyleGroups } from "_r/useAtomicCss";

export const useSearchBoxStyles = makeStyles(theme => ({
inputRoot: {
color: "inherit",
},
inputInput: {
padding: theme.spacing(3, 3, 3, 11),
transition: theme.transitions.create("width"),
width: 300,
export const useSearchBoxStyles = makeStyles(theme => {
const { atomicCss } = getAtomicCssAndStyleGroups(theme);

"&:focus": {
width: 600,
return {
inputRoot: {
color: "inherit",
},
},
}));

inputInput: {
padding: theme.spacing(3, 3, 3, 11),
transition: theme.transitions.create("width"),
width: 300,
"&:focus": {
width: 600,
...atomicCss.borderWidth1px,
...atomicCss.borderRadius4px,
...atomicCss.borderColorSecondaryMain,
...atomicCss.borderStyleSolid,
},
},
};
});
49 changes: 23 additions & 26 deletions src/renderer/App/AppBar/SearchBox/tests/SearchBox.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { screen } from "@testing-library/dom";
import { fireEvent } from "@testing-library/react";
import { initializeElectronCode } from "_tu/initializeElectronCode";
import { findByTestId } from "_tu/findByTestId";
import * as blockFixtures from "_tu/fixtures/blockFixtures";
import { initializeElectronCode } from "_tu/initializeElectronCode";
import { renderAppWithStore } from "_tu/renderAppWithStore";
import { waitWithTime } from "_tu/smallUtils";
import { startMockRpcServer } from "_tu/startMockRpcServer";
import { testIds } from "_tu/testIds";

describe("SearchBox", () => {
describe("Search flow", () => {
Expand All @@ -26,7 +27,7 @@ describe("SearchBox", () => {
* We will start by searching for a block by height
*/
fireEvent.change(await findByTestId("searchInputField"), {
target: { value: blockFixtures.blockFixture2.height },
target: { value: blockFixtures.blockFixture19.height },
});

fireEvent.keyUp(await findByTestId("searchInputField"), { keyCode: 13 });
Expand All @@ -35,35 +36,33 @@ describe("SearchBox", () => {
* `h1` is showing the block height of `blockFixture2` because we searched
* for it
*/
expect(
await screen.findByText(
`#${blockFixtures.blockFixture2.height.toLocaleString()}`,
{ selector: "h3" },
await waitWithTime(async () =>
expect(await findByTestId(testIds.blockDetailsH1)).toHaveTextContent(
`#${blockFixtures.blockFixture19.height.toLocaleString()}`,
),
).toBeVisible();
);
});

test("searching by hash", async () => {
/**
* We can now try searching for blockFixture3 by hash
*/
fireEvent.change(await findByTestId("searchInputField"), {
target: { value: blockFixtures.blockFixture3.hash },
target: { value: blockFixtures.blockFixture18.hash },
});

fireEvent.keyUp(await findByTestId("searchInputField"), { keyCode: 13 });

expect(
await screen.findByText(
`#${blockFixtures.blockFixture3.height.toLocaleString()}`,
{ selector: "h3" },
await waitWithTime(async () =>
expect(await findByTestId(testIds.blockDetailsH1)).toHaveTextContent(
`#${blockFixtures.blockFixture18.height.toLocaleString()}`,
),
).toBeVisible();
);
});

test("searching by transaction", async () => {
fireEvent.change(await findByTestId("searchInputField"), {
target: { value: blockFixtures.blockFixture3.tx[2] },
target: { value: blockFixtures.blockFixture18.tx[2] },
});

fireEvent.keyUp(await findByTestId("searchInputField"), { keyCode: 13 });
Expand All @@ -73,7 +72,7 @@ describe("SearchBox", () => {

test("it does not do anything if we modify the search field but try to submit with a key other than enter, like shift", async () => {
fireEvent.change(await findByTestId("searchInputField"), {
target: { value: blockFixtures.blockFixture2.hash },
target: { value: blockFixtures.blockFixture19.hash },
});

fireEvent.keyUp(await findByTestId("searchInputField"), {
Expand All @@ -85,12 +84,11 @@ describe("SearchBox", () => {
* the previous test is still showing. Pressing shift didn't
* trigger the search.
*/
expect(
await screen.findByText(
`#${blockFixtures.blockFixture3.height.toLocaleString()}`,
{ selector: "h3" },
await waitWithTime(async () =>
expect(await findByTestId(testIds.blockDetailsH1)).toHaveTextContent(
`#${blockFixtures.blockFixture18.height.toLocaleString()}`,
),
).toBeVisible();
);
});

test("it does not do anything when the search string does not return a block", async () => {
Expand All @@ -103,12 +101,11 @@ describe("SearchBox", () => {
/**
* Same block is still displayed. Search didn't cause a change.
*/
expect(
await screen.findByText(
`#${blockFixtures.blockFixture3.height.toLocaleString()}`,
{ selector: "h3" },
await waitWithTime(async () =>
expect(await findByTestId(testIds.blockDetailsH1)).toHaveTextContent(
`#${blockFixtures.blockFixture18.height.toLocaleString()}`,
),
).toBeVisible();
);
});
});
});
56 changes: 18 additions & 38 deletions src/renderer/App/Explorer/BlockDetails/BlockDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
} from "@material-ui/icons";
import React, { useEffect, useState, ReactElement } from "react";
import { useSelector } from "react-redux";
import { Link, Route, Switch, useRouteMatch } from "react-router-dom";
import {
Link,
Route,
Switch,
useRouteMatch,
useParams,
} from "react-router-dom";
import AutoSizer from "react-virtualized-auto-sizer";
import { FixedSizeList } from "react-window";
import { useLoadingAwareTypography } from "_r/hooks";
Expand All @@ -28,8 +34,10 @@ import {
} from "_r/utils/smallUtils";
import { withDelay } from "_r/utils/withDelay";
import { Block as TBlock } from "_t/RpcResponses";
import { testIds } from "_tu/testIds";
import { TransactionDetails } from "./TransactionDetails/TransactionDetails";
import { OtherDetails } from "./OtherDetails";
import { dummyBlockData } from "../common/dummyBlockData";

const blockDataFormatters = {
mediantime: secondsTimestampToFormattedDate,
Expand All @@ -51,61 +59,29 @@ const excludedBlockData = [
"size",
];

const dummyBlockData: TBlock = {
bits: "1a0155de",
chainwork: "000000000000000000000000000000000000000000000140bf0116a01add88d5",
confirmations: 1,
difficulty: 12563071.03178775,
hash: "0000x000000000e28bb262d7a2306c3efa3cda42c2fc27cf135a4154a02fb0cc",
height: 1664631,
mediantime: 1580599789,
merkleroot:
"52281b4d8b53c2cd52cc4ce547fcabd993064e0aa56ff66e534569cf1bc17068",
nTx: 58,
nonce: 3247988372,
previousblockhash:
"000000000000014da63868cd0618f76cd7b46aee4baec51e5f1b7b5c21a74540",
size: 17246,
strippedsize: 10550,
time: 1580602067,
tx: [
"5e97b31f2905baf0bf400fe94e7d8b42be9ff8e47ddf4c8c52dcdf0fc33dad5a",
"d00ba21708b82e51e54f7cd2e88a4d8deed59ce4c5a685dd45642cb84185f194",
"b756675416b56a6ffe5b3773fed0bf48315e060fdfff2807ed8196c7e3133c17",
"7c46362774e072a841387845f2eecb25e5557d0d1e0f56d850de22342b17d90c",
"bd739b5b65ee7c092dcabec0ce26e609eff53aa461f83103c990f0c29374ffe3",
"dcadb8076cb4c5dff331505106936ae8404c34f0e6fd972ab7e892b6d100893e",
"235811d0d8e07c68ee759e0000e98c505471681da5eac5cccab9efa6aa32e03c",
"4773cf0218e31a0cf344920aa70b509efb18a5affff53796496ff99ee839743f",
"57fe8cca77bbe99d0c0d4c752fabea22b1e4f779a4934bdd3ca51dd83c6c754a",
],
version: 549453824,
versionHex: "20c00000",
weight: 48896,
};

export const BLOCK_DETAILS_PADDING = 6;

export const BlockDetails = () => {
const { url, path } = useRouteMatch();
const a = useAtomicCss();
const [blockData, setBlockData] = useState<TBlock>(dummyBlockData);
const [isLoading, setIsLoading] = useState(true);
const [isWaitingForData, setIsWaitingForData] = useState(true);
const theme = useTheme();
const selectedExplorerBlock = useSelector(s => s.selectedExplorerBlock);
const { blockHeightAsId } = useParams();

useEffect(() => {
let isMounted = true;

const requestData = async () => {
setBlockData(dummyBlockData);
setIsLoading(true);
setIsWaitingForData(true);

const blockData_ = await withDelay(selectedExplorerBlock);

if (blockData_ && isMounted) {
setBlockData(blockData_);
setIsLoading(false);
setIsWaitingForData(false);
}
};

Expand All @@ -116,6 +92,8 @@ export const BlockDetails = () => {
};
}, [selectedExplorerBlock]);

const isLoading =
isWaitingForData || Number(blockHeightAsId) !== blockData.height;
const Typography = useLoadingAwareTypography(isLoading);
const transactionCellHeight = 42;
const transactionListMaxHeight = theme.spacing(80);
Expand All @@ -127,6 +105,7 @@ export const BlockDetails = () => {
const heading = (
<>
<Typography
data-testid={testIds.blockDetailsH1}
variant="h1"
className={a("fontWeight500", "fontStyleItalic")}
>
Expand Down Expand Up @@ -279,7 +258,7 @@ export const BlockDetails = () => {
const transactionDetailsContainer = (
<div
className={a(
"borderWidth4",
"borderWidth4px",
"borderColorDividerFade06",
"borderLeftStyleSolid",
"marginTop02",
Expand Down Expand Up @@ -313,6 +292,7 @@ export const BlockDetails = () => {

return (
<div
data-testid={isLoading ? "" : testIds.blockDetails}
className={a(
"overflowScroll",
`padding${BLOCK_DETAILS_PADDING}` as "padding6",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ export const TransactionDetails: React.FC<{
a(
"alignItemsCenter",
"borderColorDivider",
"borderWidth1",
"borderWidth1px",
"displayFlex",
"flex1",
"padding2",
Expand Down Expand Up @@ -130,6 +130,7 @@ export const TransactionDetails: React.FC<{
"overflowXHidden",
"whiteSpaceNoWrap",
"textOverflowEllipsis",
"directionRtl",
)}
>
{cellChildren}
Expand Down
Loading

0 comments on commit 6fd55f4

Please sign in to comment.