mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
Merge pull request #17 from lightninglabs/feat/loop-page
Add Loop page with the top tiles
This commit is contained in:
commit
492636c81f
29 changed files with 458 additions and 151 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",
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { createActions } from '../src/action';
|
|||
import { Background } from '../src/components/common/base';
|
||||
import { ThemeProvider } from '../src/components/theme';
|
||||
import { Store, StoreProvider } from '../src/store';
|
||||
import { sampleApiResponses } from '../src/util/sampleData';
|
||||
import { sampleApiResponses } from '../src/util/tests/sampleData';
|
||||
|
||||
/**
|
||||
* Create a store with dummy data to use for stories
|
||||
|
|
@ -17,15 +17,14 @@ const store = new Store();
|
|||
/**
|
||||
* Create dummy actions to use for stories
|
||||
*/
|
||||
|
||||
// mock the GRPC client to return sample data instead of making an actual request
|
||||
const grpc = {
|
||||
request: (methodDescriptor: any) => {
|
||||
const endpoint = `${methodDescriptor.service.serviceName}.${methodDescriptor.methodName}`;
|
||||
const data = sampleApiResponses[endpoint] || {};
|
||||
// the calling function expects the return value to have a `toObject` function
|
||||
const response: any = {
|
||||
toObject: () => data,
|
||||
};
|
||||
const response: any = { toObject: () => data };
|
||||
return Promise.resolve(response);
|
||||
},
|
||||
};
|
||||
|
|
@ -33,16 +32,30 @@ 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
|
||||
*/
|
||||
addParameters({ store });
|
||||
|
||||
/**
|
||||
* decorator function to wrap all stories with the necessary providers
|
||||
*/
|
||||
addDecorator(storyFn => (
|
||||
addDecorator((storyFn, ctx) => (
|
||||
<StoreProvider store={store} actions={actions}>
|
||||
<ThemeProvider>
|
||||
<Background>{storyFn()}</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', padding: '100px 0' }}>
|
||||
{storyFn()}
|
||||
</div>
|
||||
) : (
|
||||
storyFn()
|
||||
)}
|
||||
</Background>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
));
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
@import '../node_modules/bootstrap/scss/buttons';
|
||||
@import '../node_modules/bootstrap/scss/tables';
|
||||
@import '../node_modules/bootstrap/scss/nav';
|
||||
@import '../node_modules/bootstrap/scss/grid';
|
||||
|
||||
@font-face {
|
||||
font-family: 'OpenSans Light';
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from 'react';
|
|||
import './App.scss';
|
||||
import { Store, StoreProvider } from 'store';
|
||||
import { Layout } from 'components/layout';
|
||||
import SamplePage from 'components/pages/SamplePage';
|
||||
import LoopPage from 'components/loop/LoopPage';
|
||||
import { ThemeProvider } from 'components/theme';
|
||||
|
||||
const App = () => {
|
||||
|
|
@ -11,7 +11,7 @@ const App = () => {
|
|||
<StoreProvider store={store}>
|
||||
<ThemeProvider>
|
||||
<Layout>
|
||||
<SamplePage />
|
||||
<LoopPage />
|
||||
</Layout>
|
||||
</ThemeProvider>
|
||||
</StoreProvider>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { ProtobufMessage } from '@improbable-eng/grpc-web/dist/typings/message';
|
||||
import { UnaryMethodDefinition } from '@improbable-eng/grpc-web/dist/typings/service';
|
||||
import { UnaryRpcOptions } from '@improbable-eng/grpc-web/dist/typings/unary';
|
||||
import { sampleApiResponses } from 'util/sampleData';
|
||||
import { sampleApiResponses } from 'util/tests/sampleData';
|
||||
|
||||
// mock grpc module
|
||||
export const grpc = {
|
||||
|
|
|
|||
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>
|
||||
);
|
||||
};
|
||||
9
app/src/__stories__/LoopPage.stories.tsx
Normal file
9
app/src/__stories__/LoopPage.stories.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react';
|
||||
import LoopPage from 'components/loop/LoopPage';
|
||||
|
||||
export default {
|
||||
title: 'Loop Page',
|
||||
component: LoopPage,
|
||||
};
|
||||
|
||||
export const Default = () => <LoopPage />;
|
||||
|
|
@ -1,9 +1,31 @@
|
|||
import React from 'react';
|
||||
import React, { useEffect } from 'react';
|
||||
import { StoryContext } from '@storybook/addons';
|
||||
import { Store } from 'store';
|
||||
import NodeStatus from 'components/NodeStatus';
|
||||
|
||||
export default {
|
||||
title: 'Node Status',
|
||||
component: NodeStatus,
|
||||
parameters: { centered: true },
|
||||
};
|
||||
|
||||
export const Default = () => <NodeStatus />;
|
||||
export const Default = (ctx: StoryContext) => {
|
||||
useEffect(() => {
|
||||
// grab the store from the Storybook parameter defined in preview.tsx
|
||||
const store = ctx.parameters.store as Store;
|
||||
const { channelBalance, walletBalance } = store.balances || {
|
||||
channelBalance: 0,
|
||||
walletBalance: 0,
|
||||
};
|
||||
store.balances = { channelBalance: 0, walletBalance: 0 };
|
||||
|
||||
// change back to sample data when the component is unmounted
|
||||
return () => {
|
||||
store.balances = { channelBalance, walletBalance };
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <NodeStatus />;
|
||||
};
|
||||
|
||||
export const WithBalances = () => <NodeStatus />;
|
||||
|
|
|
|||
27
app/src/__stories__/Tile.stories.tsx
Normal file
27
app/src/__stories__/Tile.stories.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import Tile from 'components/common/Tile';
|
||||
|
||||
export default {
|
||||
title: 'Tile',
|
||||
component: Tile,
|
||||
parameters: { centered: true },
|
||||
};
|
||||
|
||||
export const Empty = () => <Tile title="Empty Tile" />;
|
||||
|
||||
export const WithText = () => <Tile title="Tile With Text" text="Sample Text" />;
|
||||
|
||||
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" />
|
||||
);
|
||||
|
|
@ -4,6 +4,6 @@ import App from '../App';
|
|||
|
||||
it('renders the App', () => {
|
||||
const { getByText } = render(<App />);
|
||||
const linkElement = getByText('Node Info');
|
||||
const linkElement = getByText('Node Status');
|
||||
expect(linkElement).toBeInTheDocument();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { lndChannelBalance, lndWalletBalance } from 'util/sampleData';
|
||||
import { lndChannelBalance, lndWalletBalance } from 'util/tests/sampleData';
|
||||
import NodeAction from 'action/node';
|
||||
import { GrpcClient, LndApi } from 'api';
|
||||
import { Store } from 'store';
|
||||
|
|
|
|||
32
app/src/__tests__/action/store.spec.ts
Normal file
32
app/src/__tests__/action/store.spec.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { lndListChannels } from 'util/tests/sampleData';
|
||||
import { createActions, StoreActions } from 'action';
|
||||
import { Store } from 'store';
|
||||
|
||||
describe('SwapAction', () => {
|
||||
let store: Store;
|
||||
let actions: StoreActions;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new Store();
|
||||
actions = createActions(store);
|
||||
});
|
||||
|
||||
it('should compute inbound liquidity', async () => {
|
||||
const inbound = lndListChannels.channelsList.reduce(
|
||||
(sum, chan) => sum + chan.remoteBalance,
|
||||
0,
|
||||
);
|
||||
|
||||
await actions.channel.getChannels();
|
||||
expect(store.totalInbound).toBe(inbound);
|
||||
});
|
||||
|
||||
it('should compute outbound liquidity', async () => {
|
||||
const outbound = lndListChannels.channelsList.reduce(
|
||||
(sum, chan) => sum + chan.localBalance,
|
||||
0,
|
||||
);
|
||||
await actions.channel.getChannels();
|
||||
expect(store.totalOutbound).toBe(outbound);
|
||||
});
|
||||
});
|
||||
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();
|
||||
});
|
||||
});
|
||||
43
app/src/__tests__/components/loop/LoopPage.spec.tsx
Normal file
43
app/src/__tests__/components/loop/LoopPage.spec.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { SwapStatus } from 'types/generated/loop_pb';
|
||||
import { renderWithProviders } from 'util/tests';
|
||||
import { loopListSwaps } from 'util/tests/sampleData';
|
||||
import LoopPage from 'components/loop/LoopPage';
|
||||
|
||||
describe('LoopPage component', () => {
|
||||
const render = () => {
|
||||
return renderWithProviders(<LoopPage />);
|
||||
};
|
||||
|
||||
it('should display the page title', () => {
|
||||
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();
|
||||
// convert from numeric timestamp to string (1586390353623905000 -> '4/15/2020')
|
||||
const formatDate = (s: SwapStatus.AsObject) =>
|
||||
new Date(s.initiationTime / 1000 / 1000).toLocaleDateString();
|
||||
const [swap1, swap2] = loopListSwaps.swapsList;
|
||||
|
||||
expect(await findByText(formatDate(swap1))).toBeInTheDocument();
|
||||
expect(await findByText('530,000 SAT')).toBeInTheDocument();
|
||||
expect(await findByText(formatDate(swap2))).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 |
61
app/src/components/common/Tile.tsx
Normal file
61
app/src/components/common/Tile.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { styled } from 'components/theme';
|
||||
import { ArrowRight } from './icons';
|
||||
import { Title } from './text';
|
||||
|
||||
const Styled = {
|
||||
TileWrap: styled.div`
|
||||
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;
|
||||
letter-spacing: 0.43px;
|
||||
margin-top: 10px;
|
||||
`,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
/**
|
||||
* the title to display in the tile
|
||||
*/
|
||||
title: string;
|
||||
/**
|
||||
* optional text to display in the tile. if this is not
|
||||
* 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, onArrowClick, children }) => {
|
||||
const { TileWrap, Header, ArrowIcon, Text } = Styled;
|
||||
|
||||
return (
|
||||
<TileWrap>
|
||||
<Header>
|
||||
<Title>{title}</Title>
|
||||
{onArrowClick && <ArrowIcon title="arrow-right" onClick={onArrowClick} />}
|
||||
</Header>
|
||||
{text ? <Text>{text}</Text> : children}
|
||||
</TileWrap>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tile;
|
||||
18
app/src/components/common/grid.tsx
Normal file
18
app/src/components/common/grid.tsx
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import React from 'react';
|
||||
|
||||
/**
|
||||
* This component represents a Row in the bootstrap Grid layout
|
||||
*/
|
||||
export const Row: React.FC = ({ children }) => <div className="row">{children}</div>;
|
||||
|
||||
/**
|
||||
* A column in the bootstrap Grid layout
|
||||
* @param cols the number of columns wide (optional)
|
||||
*/
|
||||
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';
|
||||
|
|
|
|||
|
|
@ -30,3 +30,12 @@ export const XLargeText = styled.span<BlockProps>`
|
|||
font-size: ${props => props.theme.sizes.xl};
|
||||
letter-spacing: 0.43px;
|
||||
`;
|
||||
|
||||
export const PageTitle = styled.h2`
|
||||
font-family: ${props => props.theme.fonts.light};
|
||||
font-size: ${props => props.theme.sizes.l};
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2.7px;
|
||||
line-height: 30px;
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
63
app/src/components/loop/LoopPage.tsx
Normal file
63
app/src/components/loop/LoopPage.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { usePrefixedTranslation } from 'hooks';
|
||||
import { useActions, useStore } from 'store';
|
||||
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`
|
||||
padding: 40px 50px;
|
||||
`,
|
||||
TileSection: styled.section`
|
||||
margin-top: 90px;
|
||||
`,
|
||||
};
|
||||
|
||||
const LoopPage: React.FC = () => {
|
||||
const store = useStore();
|
||||
const { node, channel, swap } = useActions();
|
||||
const { l } = usePrefixedTranslation('cmps.loop.LoopPage');
|
||||
|
||||
useEffect(() => {
|
||||
// fetch RPC data when the component mounts if there is no
|
||||
if (store.channels.length === 0) {
|
||||
channel.getChannels();
|
||||
node.getBalances();
|
||||
swap.listSwaps();
|
||||
}
|
||||
}, [store, node, channel, swap]);
|
||||
|
||||
const { PageWrap, TileSection } = Styled;
|
||||
return (
|
||||
<PageWrap>
|
||||
<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')}
|
||||
text={`${store.totalInbound.toLocaleString()} SAT`}
|
||||
/>
|
||||
</Column>
|
||||
<Column cols={4}>
|
||||
<Tile
|
||||
title={l('outbound')}
|
||||
text={`${store.totalOutbound.toLocaleString()} SAT`}
|
||||
/>
|
||||
</Column>
|
||||
</Row>
|
||||
</TileSection>
|
||||
</PageWrap>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(LoopPage);
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { usePrefixedTranslation } from 'hooks';
|
||||
import { useActions, useStore } from 'store/provider';
|
||||
|
||||
const SamplePage: React.FC = () => {
|
||||
const store = useStore();
|
||||
const { node, channel, swap } = useActions();
|
||||
const { l } = usePrefixedTranslation('App');
|
||||
useEffect(() => {
|
||||
// fetch node info when the component is mounted
|
||||
const fetchInfo = async () => {
|
||||
try {
|
||||
await node.getInfo();
|
||||
await node.getBalances();
|
||||
} catch (error) {
|
||||
console.log('Failed to fetch node info', error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchInfo();
|
||||
}, [node]);
|
||||
|
||||
const balances = store.balances || { channelBalance: 0, walletBalance: 0 };
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="text-center">
|
||||
<h3>Lightning Loop</h3>
|
||||
</header>
|
||||
<section className="mt-4">
|
||||
<h1>{l('App.nodeInfo')}</h1>
|
||||
{store.info && (
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{l('pubkey')}</th>
|
||||
<td>{store.info.identityPubkey}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('alias')}</th>
|
||||
<td>{store.info.alias}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('version')}</th>
|
||||
<td>{store.info.version}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('numChannels')}</th>
|
||||
<td>{store.info.numActiveChannels}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>{l('balances')}</th>
|
||||
<td>{`${balances.channelBalance} in channels, ${balances.walletBalance} in wallet`}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
</section>
|
||||
<section className="mt-4">
|
||||
<h2>
|
||||
{store.channels.length} Channels
|
||||
<button
|
||||
className="btn btn-outline-light float-right"
|
||||
onClick={channel.getChannels}
|
||||
>
|
||||
Fetch
|
||||
</button>
|
||||
</h2>
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Can Receive</td>
|
||||
<td>Can Send</td>
|
||||
<td>In Fee %</td>
|
||||
<td>Up time %</td>
|
||||
<td>Volume (24h)</td>
|
||||
<td>Peer/Alias</td>
|
||||
<td>Capacity</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.channels.map(c => (
|
||||
<tr key={c.chanId}>
|
||||
<td>{c.remoteBalance}</td>
|
||||
<td>{c.localBalance}</td>
|
||||
<td></td>
|
||||
<td>{c.uptime}</td>
|
||||
<td></td>
|
||||
<td>{c.remotePubkey.substring(0, 12)}</td>
|
||||
<td>{c.capacity}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
<section className="mt-4">
|
||||
<h2>
|
||||
{store.swaps.length} Swaps
|
||||
<button className="btn btn-outline-light float-right" onClick={swap.listSwaps}>
|
||||
Fetch
|
||||
</button>
|
||||
</h2>
|
||||
<table className="table" style={{ color: '#fff' }}>
|
||||
<thead>
|
||||
<tr>
|
||||
<td>Date</td>
|
||||
<td>Type</td>
|
||||
<td>Amount</td>
|
||||
<td>Status</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{store.swaps.map(s => (
|
||||
<tr key={s.id}>
|
||||
<td>{s.createdOn.toString()}</td>
|
||||
<td>{s.type}</td>
|
||||
<td>{s.amount.toString()}</td>
|
||||
<td>{s.status}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default observer(SamplePage);
|
||||
|
|
@ -23,6 +23,7 @@ const theme = {
|
|||
white: '#ffffff',
|
||||
whitish: '#f5f5f5',
|
||||
pink: '#f5406e',
|
||||
tileBack: 'rgba(245,245,245,0.04)',
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +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",
|
||||
"App.nodeInfo": "Node Info",
|
||||
"App.pubkey": "Pubkey",
|
||||
"App.alias": "Alias",
|
||||
|
|
|
|||
|
|
@ -1,17 +1,40 @@
|
|||
import { observable } from 'mobx';
|
||||
import { computed, observable } from 'mobx';
|
||||
import { Channel, NodeBalances, NodeInfo, Swap } from 'types/state';
|
||||
|
||||
/**
|
||||
* The store used to manage global app state
|
||||
*/
|
||||
export class Store {
|
||||
//
|
||||
// App state
|
||||
//
|
||||
@observable sidebarCollapsed = false;
|
||||
|
||||
//
|
||||
// API data
|
||||
//
|
||||
@observable info?: NodeInfo = undefined;
|
||||
@observable balances?: NodeBalances = undefined;
|
||||
@observable channels: Channel[] = [];
|
||||
@observable swaps: Swap[] = [];
|
||||
|
||||
//
|
||||
// computed data
|
||||
//
|
||||
|
||||
/**
|
||||
* the sum of remote balance of all channels
|
||||
*/
|
||||
@computed get totalInbound() {
|
||||
return this.channels.reduce((sum, chan) => sum + chan.remoteBalance, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* the sum of local balance of all channels
|
||||
*/
|
||||
@computed get totalOutbound() {
|
||||
return this.channels.reduce((sum, chan) => sum + chan.localBalance, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// re-export from provider
|
||||
|
|
|
|||
|
|
@ -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