From 66ccc61f69c605569c263ec18b0292303d0614d8 Mon Sep 17 00:00:00 2001 From: jamaljsr Date: Wed, 22 Apr 2020 11:47:08 -0400 Subject: [PATCH] feat: add Loop History component, tests, and stories --- .vscode/launch.json | 4 +- app/.storybook/preview.tsx | 8 +++- app/src/__stories__/LoopHistory.stories.tsx | 27 +++++++++++++ app/src/__stories__/Tile.stories.tsx | 7 ++++ .../__tests__/components/common/Tile.spec.tsx | 38 ++++++++++++++++++ .../components/loop/LoopPage.spec.tsx | 23 +++++++++++ app/src/action/swap.ts | 2 +- app/src/assets/icons/arrow-right.svg | 3 ++ app/src/components/common/Tile.tsx | 26 ++++++++++-- app/src/components/common/grid.tsx | 10 +++-- app/src/components/common/icons.tsx | 1 + app/src/components/layout/Layout.tsx | 3 +- app/src/components/loop/LoopHistory.tsx | 40 +++++++++++++++++++ app/src/components/loop/LoopPage.tsx | 6 +++ app/src/i18n/locales/en-US.json | 6 +-- app/src/types/state.ts | 2 +- app/src/util/sampleData.ts | 6 +-- 17 files changed, 192 insertions(+), 20 deletions(-) create mode 100644 app/src/__stories__/LoopHistory.stories.tsx create mode 100644 app/src/__tests__/components/common/Tile.spec.tsx create mode 100644 app/src/assets/icons/arrow-right.svg create mode 100644 app/src/components/loop/LoopHistory.tsx diff --git a/.vscode/launch.json b/.vscode/launch.json index d2b1ac87..6e0f4715 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,9 +8,9 @@ "name": "Debug Tests", "type": "node", "request": "launch", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/react-scripts", + "runtimeExecutable": "${workspaceRoot}/app/node_modules/.bin/react-scripts", "args": ["test", "--runInBand", "--no-cache", "--watchAll=false"], - "cwd": "${workspaceRoot}", + "cwd": "${workspaceRoot}/app/", "protocol": "inspector", "console": "integratedTerminal", "internalConsoleOptions": "neverOpen", diff --git a/app/.storybook/preview.tsx b/app/.storybook/preview.tsx index a45b3b8b..a04fc342 100644 --- a/app/.storybook/preview.tsx +++ b/app/.storybook/preview.tsx @@ -32,6 +32,7 @@ const actions = createActions(store, grpc); // execute actions to populate the store data with the sample API responses actions.node.getBalances(); +actions.swap.listSwaps(); /** * add the mobx store to Storybook parameters so that stories can manipulate it @@ -44,10 +45,13 @@ addParameters({ store }); addDecorator((storyFn, ctx) => ( - + {/* modify the bg styles so it isn't too big in docs mode */} + {/* wrap the component in a centered div for small components */} {ctx.parameters.centered ? ( -
{storyFn()}
+
+ {storyFn()} +
) : ( storyFn() )} diff --git a/app/src/__stories__/LoopHistory.stories.tsx b/app/src/__stories__/LoopHistory.stories.tsx new file mode 100644 index 00000000..ff670647 --- /dev/null +++ b/app/src/__stories__/LoopHistory.stories.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { StoryContext } from '@storybook/addons'; +import { Store } from 'store'; +import Tile from 'components/common/Tile'; +import LoopHistory from 'components/loop/LoopHistory'; + +export default { + title: 'Loop History', + component: LoopHistory, + parameters: { centered: true }, +}; + +export const Default = (ctx: StoryContext) => { + // grab the store from the Storybook parameter defined in preview.tsx + const { swaps } = ctx.parameters.store as Store; + return ; +}; + +export const InsideTile = (ctx: StoryContext) => { + // grab the store from the Storybook parameter defined in preview.tsx + const { swaps } = ctx.parameters.store as Store; + return ( + + + + ); +}; diff --git a/app/src/__stories__/Tile.stories.tsx b/app/src/__stories__/Tile.stories.tsx index f56f6492..bbfc24c2 100644 --- a/app/src/__stories__/Tile.stories.tsx +++ b/app/src/__stories__/Tile.stories.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import { action } from '@storybook/addon-actions'; import Tile from 'components/common/Tile'; export default { @@ -15,6 +16,12 @@ export const WithChildren = () => ( Sample child content ); +export const WithArrowIcon = () => ( + action('ArrowIcon')}> + Sample Text + +); + export const InboundLiquidity = () => ( ); diff --git a/app/src/__tests__/components/common/Tile.spec.tsx b/app/src/__tests__/components/common/Tile.spec.tsx new file mode 100644 index 00000000..6b36dd7e --- /dev/null +++ b/app/src/__tests__/components/common/Tile.spec.tsx @@ -0,0 +1,38 @@ +import React, { ReactNode } from 'react'; +import { fireEvent } from '@testing-library/react'; +import { renderWithProviders } from 'util/tests'; +import Tile from 'components/common/Tile'; + +describe('Tile component', () => { + const handleArrowClick = jest.fn(); + + const render = (text?: string, children?: ReactNode) => { + const cmp = ( + + {children} + + ); + return renderWithProviders(cmp); + }; + + it('should display the title', () => { + const { getByText } = render(); + expect(getByText('Test Tile')).toBeInTheDocument(); + }); + + it('should display the text', () => { + const { getByText } = render('test text'); + expect(getByText('test text')).toBeInTheDocument(); + }); + + it('should display child components', () => { + const { getByText } = render(undefined, 'test child'); + expect(getByText('test child')).toBeInTheDocument(); + }); + + it('should handle the arrow click event', () => { + const { getByText } = render(); + fireEvent.click(getByText('arrow-right.svg')); + expect(handleArrowClick).toBeCalled(); + }); +}); diff --git a/app/src/__tests__/components/loop/LoopPage.spec.tsx b/app/src/__tests__/components/loop/LoopPage.spec.tsx index a8cabbf6..78f3e45f 100644 --- a/app/src/__tests__/components/loop/LoopPage.spec.tsx +++ b/app/src/__tests__/components/loop/LoopPage.spec.tsx @@ -11,4 +11,27 @@ describe('LoopPage component', () => { const { getByText } = render(); expect(getByText('Lightning Loop')).toBeInTheDocument(); }); + + it('should display the three tiles', () => { + const { getByText } = render(); + expect(getByText('Loop History')).toBeInTheDocument(); + expect(getByText('Total Inbound Liquidity')).toBeInTheDocument(); + expect(getByText('Total Outbound Liquidity')).toBeInTheDocument(); + }); + + it('should display the liquidity numbers', async () => { + const { findByText } = render(); + // these values are defined in sampleData.ts + expect(await findByText('4,501,409 SAT')).toBeInTheDocument(); + expect(await findByText('9,988,660 SAT')).toBeInTheDocument(); + }); + + it('should display the loop history records', async () => { + const { findByText } = render(); + // these values are defined in sampleData.ts + expect(await findByText('4/15/2020')).toBeInTheDocument(); + expect(await findByText('530,000 SAT')).toBeInTheDocument(); + expect(await findByText('4/14/2020')).toBeInTheDocument(); + expect(await findByText('525,000 SAT')).toBeInTheDocument(); + }); }); diff --git a/app/src/action/swap.ts b/app/src/action/swap.ts index 102f8b01..1a6b60fc 100644 --- a/app/src/action/swap.ts +++ b/app/src/action/swap.ts @@ -29,7 +29,7 @@ class SwapAction { .map(s => ({ id: s.id, type: this._typeToString(s.type), - amount: BigInt(s.amt), + amount: s.amt, createdOn: new Date(s.initiationTime / 1000 / 1000), status: this._stateToString(s.state), })); diff --git a/app/src/assets/icons/arrow-right.svg b/app/src/assets/icons/arrow-right.svg new file mode 100644 index 00000000..d199a0db --- /dev/null +++ b/app/src/assets/icons/arrow-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/components/common/Tile.tsx b/app/src/components/common/Tile.tsx index c715b21e..f3ffcd3a 100644 --- a/app/src/components/common/Tile.tsx +++ b/app/src/components/common/Tile.tsx @@ -1,14 +1,24 @@ import React from 'react'; import { styled } from 'components/theme'; +import { ArrowRight } from './icons'; import { Title } from './text'; const Styled = { TileWrap: styled.div` - min-height: 100px; + min-height: 105px; padding: 15px; background-color: ${props => props.theme.colors.tileBack}; border-radius: 4px; `, + Header: styled.div` + display: flex; + justify-content: space-between; + `, + ArrowIcon: styled(ArrowRight)` + width: 16px; + margin-top: -5px; + cursor: pointer; + `, Text: styled.div` font-size: ${props => props.theme.sizes.xl}; line-height: 37px; @@ -27,14 +37,22 @@ interface Props { * provided, then the `children` will be displayed instead */ text?: string; + /** + * optional click handler for the arrow which will not be + * visible if this prop is not defined + */ + onArrowClick?: () => void; } -const Tile: React.FC = ({ title, text, children }) => { - const { TileWrap, Text } = Styled; +const Tile: React.FC = ({ title, text, onArrowClick, children }) => { + const { TileWrap, Header, ArrowIcon, Text } = Styled; return ( - {title} +
+ {title} + {onArrowClick && } +
{text ? {text} : children}
); diff --git a/app/src/components/common/grid.tsx b/app/src/components/common/grid.tsx index 296b97fe..ec56ebad 100644 --- a/app/src/components/common/grid.tsx +++ b/app/src/components/common/grid.tsx @@ -9,6 +9,10 @@ export const Row: React.FC = ({ children }) =>
{children} = ({ cols, children }) => ( -
{children}
-); +export const Column: React.FC<{ + cols?: number; + className?: string; +}> = ({ cols, children, className }) => { + const cls = (className || '') + (cols ? ` col-${cols}` : ' col'); + return
{children}
; +}; diff --git a/app/src/components/common/icons.tsx b/app/src/components/common/icons.tsx index 10e928c0..97d15836 100644 --- a/app/src/components/common/icons.tsx +++ b/app/src/components/common/icons.tsx @@ -1,3 +1,4 @@ export { ReactComponent as Bolt } from 'assets/icons/bolt.svg'; export { ReactComponent as Bitcoin } from 'assets/icons/bitcoin.svg'; export { ReactComponent as Menu } from 'assets/icons/menu.svg'; +export { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg'; diff --git a/app/src/components/layout/Layout.tsx b/app/src/components/layout/Layout.tsx index 74a1dc80..6d7d2ebd 100644 --- a/app/src/components/layout/Layout.tsx +++ b/app/src/components/layout/Layout.tsx @@ -14,7 +14,8 @@ const Styled = { Container: styled.div` position: relative; min-height: 100vh; - width: 1440px; + max-width: 1440px; + width: 100%; margin: 0 auto; `, MenuIcon: styled(Menu)` diff --git a/app/src/components/loop/LoopHistory.tsx b/app/src/components/loop/LoopHistory.tsx new file mode 100644 index 00000000..9dcfa724 --- /dev/null +++ b/app/src/components/loop/LoopHistory.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { Swap } from 'types/state'; +import { Column, Row } from 'components/common/grid'; +import { SmallText } from 'components/common/text'; +import { styled } from 'components/theme'; + +const Styled = { + RightColumn: styled(Column)` + text-align: right; + `, + SmallText: styled(SmallText)` + line-height: 1; + `, +}; + +interface Props { + swaps: Swap[]; +} + +const LoopHistory: React.FC = ({ swaps }) => { + const recentSwaps = swaps.slice(0, 2); + + const { RightColumn, SmallText } = Styled; + return ( + <> + {recentSwaps.map(swap => ( + + + {swap.createdOn.toLocaleDateString()} + + + {`${swap.amount.toLocaleString()} SAT`} + + + ))} + + ); +}; + +export default LoopHistory; diff --git a/app/src/components/loop/LoopPage.tsx b/app/src/components/loop/LoopPage.tsx index 819e9112..6f0beb8f 100644 --- a/app/src/components/loop/LoopPage.tsx +++ b/app/src/components/loop/LoopPage.tsx @@ -6,6 +6,7 @@ import { Column, Row } from 'components/common/grid'; import { PageTitle } from 'components/common/text'; import Tile from 'components/common/Tile'; import { styled } from 'components/theme'; +import LoopHistory from './LoopHistory'; const Styled = { PageWrap: styled.div` @@ -36,6 +37,11 @@ const LoopPage: React.FC = () => { {l('pageTitle')} + + null}> + + + ({ - amt: 500000, - id: 'f4eb118383c2b09d8c7289ce21c25900cfb4545d46c47ed23a31ad2aa57ce835', + amt: 500000 + i * 5000, + id: `f4eb118383c2b09d8c7289ce21c25900cfb4545d46c47ed23a31ad2aa57ce83${i}`, idBytes: '9OsRg4PCsJ2MconOIcJZAM+0VF1GxH7SOjGtKqV86DU=', type: (i % 3) as any, state: i as any, - initiationTime: 1586390353623905000, + initiationTime: 1586390353623905000 + i * 100000000000000, lastUpdateTime: 1586398369729857000, htlcAddress: 'bcrt1qzu4077erkr78k52yuf2rwkk6ayr6m3wtazdfz2qqmd7taa5vvy9s5d75gd', costServer: 66,