mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
feat: add Loop History component, tests, and stories
This commit is contained in:
parent
4cfb5c141c
commit
66ccc61f69
17 changed files with 192 additions and 20 deletions
4
.vscode/launch.json
vendored
4
.vscode/launch.json
vendored
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
)}
|
||||
|
|
|
|||
27
app/src/__stories__/LoopHistory.stories.tsx
Normal file
27
app/src/__stories__/LoopHistory.stories.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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" />
|
||||
);
|
||||
|
|
|
|||
38
app/src/__tests__/components/common/Tile.spec.tsx
Normal file
38
app/src/__tests__/components/common/Tile.spec.tsx
Normal 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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}));
|
||||
|
|
|
|||
3
app/src/assets/icons/arrow-right.svg
Normal file
3
app/src/assets/icons/arrow-right.svg
Normal 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 |
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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)`
|
||||
|
|
|
|||
40
app/src/components/loop/LoopHistory.tsx
Normal file
40
app/src/components/loop/LoopHistory.tsx
Normal 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;
|
||||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ export interface Channel {
|
|||
export interface Swap {
|
||||
id: string;
|
||||
type: string;
|
||||
amount: BigInt;
|
||||
amount: number;
|
||||
createdOn: Date;
|
||||
status: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue