feat: add Loop History component, tests, and stories

This commit is contained in:
jamaljsr 2020-04-22 11:47:08 -04:00
parent 4cfb5c141c
commit 66ccc61f69
17 changed files with 192 additions and 20 deletions

4
.vscode/launch.json vendored
View file

@ -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",

View file

@ -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) => (
<StoreProvider store={store} actions={actions}>
<ThemeProvider>
<Background>
{/* modify the bg styles so it isn't too big in docs mode */}
<Background style={{ minHeight: 'inherit', height: '100%' }}>
{/* wrap the component in a centered div for small components */}
{ctx.parameters.centered ? (
<div style={{ width: 300, margin: 'auto', paddingTop: 100 }}>{storyFn()}</div>
<div style={{ width: 300, margin: 'auto', padding: '100px 0' }}>
{storyFn()}
</div>
) : (
storyFn()
)}

View file

@ -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 <LoopHistory swaps={swaps} />;
};
export const InsideTile = (ctx: StoryContext) => {
// grab the store from the Storybook parameter defined in preview.tsx
const { swaps } = ctx.parameters.store as Store;
return (
<Tile title="Loop History">
<LoopHistory swaps={swaps} />
</Tile>
);
};

View file

@ -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 = () => (
<Tile title="Tile With Children">Sample child content</Tile>
);
export const WithArrowIcon = () => (
<Tile title="Tile With Arrow" onArrowClick={() => action('ArrowIcon')}>
Sample Text
</Tile>
);
export const InboundLiquidity = () => (
<Tile title="Inbound Liquidity" text="123,456,789 SAT" />
);

View file

@ -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 = (
<Tile title="Test Tile" text={text} onArrowClick={handleArrowClick}>
{children}
</Tile>
);
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();
});
});

View file

@ -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();
});
});

View file

@ -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),
}));

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-chevron-right">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>

After

Width:  |  Height:  |  Size: 274 B

View file

@ -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<Props> = ({ title, text, children }) => {
const { TileWrap, Text } = Styled;
const Tile: React.FC<Props> = ({ title, text, onArrowClick, children }) => {
const { TileWrap, Header, ArrowIcon, Text } = Styled;
return (
<TileWrap>
<Title>{title}</Title>
<Header>
<Title>{title}</Title>
{onArrowClick && <ArrowIcon title="arrow-right" onClick={onArrowClick} />}
</Header>
{text ? <Text>{text}</Text> : children}
</TileWrap>
);

View file

@ -9,6 +9,10 @@ export const Row: React.FC = ({ children }) => <div className="row">{children}</
* A column in the bootstrap Grid layout
* @param cols the number of columns wide (optional)
*/
export const Column: React.FC<{ cols?: number }> = ({ cols, children }) => (
<div className={cols ? `col-${cols}` : 'col'}>{children}</div>
);
export const Column: React.FC<{
cols?: number;
className?: string;
}> = ({ cols, children, className }) => {
const cls = (className || '') + (cols ? ` col-${cols}` : ' col');
return <div className={cls.trim()}>{children}</div>;
};

View file

@ -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';

View file

@ -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)`

View file

@ -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<Props> = ({ swaps }) => {
const recentSwaps = swaps.slice(0, 2);
const { RightColumn, SmallText } = Styled;
return (
<>
{recentSwaps.map(swap => (
<Row key={swap.id}>
<Column cols={6}>
<SmallText>{swap.createdOn.toLocaleDateString()}</SmallText>
</Column>
<RightColumn cols={6}>
<SmallText>{`${swap.amount.toLocaleString()} SAT`}</SmallText>
</RightColumn>
</Row>
))}
</>
);
};
export default LoopHistory;

View file

@ -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 = () => {
<PageTitle>{l('pageTitle')}</PageTitle>
<TileSection>
<Row>
<Column>
<Tile title={l('history')} onArrowClick={() => null}>
<LoopHistory swaps={store.swaps} />
</Tile>
</Column>
<Column cols={4}>
<Tile
title={l('inbound')}

View file

@ -1,8 +1,8 @@
{
"cmps.loop.LoopPage.pageTitle": "Lightning Loop",
"cmps.loop.LoopPage.history": "Loop history",
"cmps.loop.LoopPage.inbound": "Total inbound Liquidity",
"cmps.loop.LoopPage.outbound": "Total outbound Liquidity",
"cmps.loop.LoopPage.history": "Loop History",
"cmps.loop.LoopPage.inbound": "Total Inbound Liquidity",
"cmps.loop.LoopPage.outbound": "Total Outbound Liquidity",
"App.nodeInfo": "Node Info",
"App.pubkey": "Pubkey",
"App.alias": "Alias",

View file

@ -25,7 +25,7 @@ export interface Channel {
export interface Swap {
id: string;
type: string;
amount: BigInt;
amount: number;
createdOn: Date;
status: string;
}

View file

@ -92,12 +92,12 @@ export const lndListChannels: LND.ListChannelsResponse.AsObject = {
export const loopListSwaps: LOOP.ListSwapsResponse.AsObject = {
swapsList: [...Array(7)].map((x, i) => ({
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,