mirror of
https://github.com/lightninglabs/lightning-terminal.git
synced 2026-08-13 12:33:36 +02:00
feat: add initial channel list to Loop page
This commit is contained in:
parent
492636c81f
commit
6ff4b753fb
13 changed files with 314 additions and 12 deletions
|
|
@ -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.channel.getChannels();
|
||||
actions.swap.listSwaps();
|
||||
|
||||
/**
|
||||
|
|
@ -47,12 +48,25 @@ addDecorator((storyFn, ctx) => (
|
|||
<ThemeProvider>
|
||||
{/* 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 ? (
|
||||
// wrap the component in a centered div for small components
|
||||
<div style={{ width: 300, margin: 'auto', padding: '100px 0' }}>
|
||||
{storyFn()}
|
||||
</div>
|
||||
) : ctx.parameters.contained ? (
|
||||
// or wrap in a full width container for larger components
|
||||
<div
|
||||
style={{
|
||||
width: '98%',
|
||||
maxWidth: '1440px',
|
||||
margin: 'auto',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{storyFn()}
|
||||
</div>
|
||||
) : (
|
||||
// or don't wrap for the layout
|
||||
storyFn()
|
||||
)}
|
||||
</Background>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@emotion/core": "10.0.28",
|
||||
"@emotion/styled": "10.0.27",
|
||||
"@improbable-eng/grpc-web": "0.12.0",
|
||||
"@types/react-virtualized": "^9.21.9",
|
||||
"debug": "4.1.1",
|
||||
"emotion-theming": "10.0.27",
|
||||
"i18next": "19.4.1",
|
||||
|
|
@ -30,7 +31,8 @@
|
|||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-i18next": "11.3.4",
|
||||
"react-scripts": "3.4.1"
|
||||
"react-scripts": "3.4.1",
|
||||
"react-virtualized": "^9.21.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "^5.3.18",
|
||||
|
|
|
|||
54
app/src/__stories__/ChannelList.stories.tsx
Normal file
54
app/src/__stories__/ChannelList.stories.tsx
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import { toJS } from 'mobx';
|
||||
import { StoryContext } from '@storybook/addons';
|
||||
import { Store } from 'store';
|
||||
import ChannelList from 'components/loop/ChannelList';
|
||||
|
||||
export default {
|
||||
title: 'Channel List',
|
||||
component: ChannelList,
|
||||
parameters: { contained: true },
|
||||
};
|
||||
|
||||
export const NoChannels = (ctx: StoryContext) => {
|
||||
// grab the store from the Storybook parameter defined in preview.tsx
|
||||
const store = ctx.parameters.store as Store;
|
||||
|
||||
useEffect(() => {
|
||||
// convert the store state to pure JS so it can be reverted on unmount
|
||||
const channels = toJS(store.channels);
|
||||
store.channels = [];
|
||||
|
||||
// change back to sample data when the component is unmounted
|
||||
return () => {
|
||||
store.channels = channels;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <ChannelList channels={store.channels} />;
|
||||
};
|
||||
|
||||
export const FewChannels = (ctx: StoryContext) => {
|
||||
// grab the store from the Storybook parameter defined in preview.tsx
|
||||
const store = ctx.parameters.store as Store;
|
||||
|
||||
useEffect(() => {
|
||||
// convert the store state to pure JS so it can be reverted on unmount
|
||||
const channels = toJS(store.channels);
|
||||
store.channels = channels.slice(0, 5);
|
||||
|
||||
// change back to sample data when the component is unmounted
|
||||
return () => {
|
||||
store.channels = channels;
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <ChannelList channels={store.channels} />;
|
||||
};
|
||||
|
||||
export const ManyChannels = (ctx: StoryContext) => {
|
||||
// grab the store from the Storybook parameter defined in preview.tsx
|
||||
const store = ctx.parameters.store as Store;
|
||||
|
||||
return <ChannelList channels={store.channels} />;
|
||||
};
|
||||
|
|
@ -28,7 +28,7 @@ class ChannelAction {
|
|||
capacity: c.capacity,
|
||||
localBalance: c.localBalance,
|
||||
remoteBalance: c.remoteBalance,
|
||||
uptime: c.uptime,
|
||||
uptime: Math.floor((c.uptime * 100) / c.lifetime),
|
||||
active: c.active,
|
||||
}));
|
||||
log.info('updated store.channels', toJS(this._store.channels));
|
||||
|
|
|
|||
10
app/src/assets/icons/dot.svg
Normal file
10
app/src/assets/icons/dot.svg
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="10px" height="11px" viewBox="0 0 10 11" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g transform="translate(-335.000000, -1965.000000)" fill="currentColor">
|
||||
<g transform="translate(335.188657, 1965.641229)">
|
||||
<circle cx="4.72550184" cy="4.86069343" r="4.72526812"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 525 B |
|
|
@ -1,9 +1,21 @@
|
|||
import React from 'react';
|
||||
import React, { HTMLAttributes } from 'react';
|
||||
|
||||
/**
|
||||
* This component represents a Row in the bootstrap Grid layout
|
||||
*/
|
||||
export const Row: React.FC = ({ children }) => <div className="row">{children}</div>;
|
||||
export const Row: React.FC<HTMLAttributes<HTMLDivElement>> = ({
|
||||
children,
|
||||
className,
|
||||
...rest
|
||||
}) => {
|
||||
const cn: string[] = ['row'];
|
||||
className && cn.push(className);
|
||||
return (
|
||||
<div className={cn.join(' ')} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* A column in the bootstrap Grid layout
|
||||
|
|
@ -11,8 +23,12 @@ export const Row: React.FC = ({ children }) => <div className="row">{children}</
|
|||
*/
|
||||
export const Column: React.FC<{
|
||||
cols?: number;
|
||||
right?: boolean;
|
||||
className?: string;
|
||||
}> = ({ cols, children, className }) => {
|
||||
const cls = (className || '') + (cols ? ` col-${cols}` : ' col');
|
||||
return <div className={cls.trim()}>{children}</div>;
|
||||
}> = ({ cols, right, children, className }) => {
|
||||
const cn: string[] = [];
|
||||
cn.push(cols ? `col-${cols}` : 'col');
|
||||
className && cn.push(className);
|
||||
right && cn.push('text-right');
|
||||
return <div className={cn.join(' ')}>{children}</div>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg';
|
||||
export { ReactComponent as Bolt } from 'assets/icons/bolt.svg';
|
||||
export { ReactComponent as Bitcoin } from 'assets/icons/bitcoin.svg';
|
||||
export { ReactComponent as Dot } from 'assets/icons/dot.svg';
|
||||
export { ReactComponent as Menu } from 'assets/icons/menu.svg';
|
||||
export { ReactComponent as ArrowRight } from 'assets/icons/arrow-right.svg';
|
||||
|
|
|
|||
61
app/src/components/loop/ChannelList.tsx
Normal file
61
app/src/components/loop/ChannelList.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import React from 'react';
|
||||
import { AutoSizer, List, WindowScroller } from 'react-virtualized';
|
||||
import { Channel } from 'types/state';
|
||||
import styled from '@emotion/styled';
|
||||
import ChannelRow, { ChannelRowHeader, ROW_HEIGHT } from './ChannelRow';
|
||||
|
||||
const Styled = {
|
||||
Wrapper: styled.section`
|
||||
margin: 50px 0;
|
||||
`,
|
||||
ListContainer: styled.div`
|
||||
/**
|
||||
* the virtualized list doesn't play nice with the bootstrap row -15px
|
||||
* margin. We need to manually offset the container and remove the
|
||||
* padding from the last column to get the alignment correct
|
||||
*/
|
||||
margin-right: -15px;
|
||||
|
||||
.col:last-child {
|
||||
padding-right: 0;
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
channels: Channel[];
|
||||
}
|
||||
|
||||
const ChannelList: React.FC<Props> = ({ channels }) => {
|
||||
const { Wrapper, ListContainer } = Styled;
|
||||
return (
|
||||
<Wrapper>
|
||||
<ChannelRowHeader />
|
||||
<ListContainer>
|
||||
<AutoSizer disableHeight>
|
||||
{({ width }) => (
|
||||
<WindowScroller>
|
||||
{({ height, isScrolling, onChildScroll, scrollTop }) => (
|
||||
<List
|
||||
autoHeight
|
||||
height={height}
|
||||
isScrolling={isScrolling}
|
||||
onScroll={onChildScroll}
|
||||
rowCount={channels.length}
|
||||
rowHeight={ROW_HEIGHT}
|
||||
rowRenderer={({ index, key, style }) => (
|
||||
<ChannelRow key={key} channel={channels[index]} style={style} />
|
||||
)}
|
||||
scrollTop={scrollTop}
|
||||
width={width}
|
||||
/>
|
||||
)}
|
||||
</WindowScroller>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</ListContainer>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelList;
|
||||
89
app/src/components/loop/ChannelRow.tsx
Normal file
89
app/src/components/loop/ChannelRow.tsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import React, { CSSProperties } from 'react';
|
||||
import { Channel } from 'types/state';
|
||||
import { Column, Row } from 'components/common/grid';
|
||||
import { Dot } from 'components/common/icons';
|
||||
import { Title } from 'components/common/text';
|
||||
import { styled } from 'components/theme';
|
||||
|
||||
/**
|
||||
* the virtualized list requires each row to have a specified
|
||||
* height. Defining a const here because it is used in multiple places
|
||||
*/
|
||||
export const ROW_HEIGHT = 60;
|
||||
|
||||
const Styled = {
|
||||
Row: styled(Row)`
|
||||
border-bottom: 0.5px solid ${props => props.theme.colors.darkGray};
|
||||
|
||||
&:last-child {
|
||||
border-bottom-width: 0;
|
||||
}
|
||||
`,
|
||||
Column: styled(Column)<{ last?: boolean }>`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
line-height: ${ROW_HEIGHT}px;
|
||||
`,
|
||||
StatusIcon: styled.span`
|
||||
float: left;
|
||||
margin-top: -1px;
|
||||
margin-left: 15px;
|
||||
color: ${props => props.theme.colors.pink};
|
||||
`,
|
||||
Balance: styled.div`
|
||||
margin-top: ${ROW_HEIGHT / 2 - 2}px;
|
||||
height: 4px;
|
||||
width: 100%;
|
||||
background-color: ${props => props.theme.colors.pink};
|
||||
border-radius: 2px;
|
||||
`,
|
||||
};
|
||||
|
||||
interface Props {
|
||||
channel: Channel;
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
export const ChannelRowHeader: React.FC = () => (
|
||||
<Row>
|
||||
<Column right>
|
||||
<Title>Can Receive</Title>
|
||||
</Column>
|
||||
<Column cols={3}></Column>
|
||||
<Column>
|
||||
<Title>Can Send</Title>
|
||||
</Column>
|
||||
<Column>
|
||||
<Title>Up Time %</Title>
|
||||
</Column>
|
||||
<Column>
|
||||
<Title>Peer/Alias</Title>
|
||||
</Column>
|
||||
<Column right>
|
||||
<Title>Capacity</Title>
|
||||
</Column>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const ChannelRow: React.FC<Props> = ({ channel, style }) => {
|
||||
const { Row, Column, StatusIcon, Balance } = Styled;
|
||||
return (
|
||||
<Row style={style}>
|
||||
<Column right>
|
||||
<StatusIcon>
|
||||
<Dot />
|
||||
</StatusIcon>
|
||||
{channel.localBalance.toLocaleString()}
|
||||
</Column>
|
||||
<Column cols={3}>
|
||||
<Balance />
|
||||
</Column>
|
||||
<Column>{channel.remoteBalance.toLocaleString()}</Column>
|
||||
<Column>{channel.uptime}</Column>
|
||||
<Column>{channel.remotePubkey}</Column>
|
||||
<Column right>{channel.capacity.toLocaleString()}</Column>
|
||||
</Row>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChannelRow;
|
||||
|
|
@ -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 ChannelList from './ChannelList';
|
||||
import LoopHistory from './LoopHistory';
|
||||
|
||||
const Styled = {
|
||||
|
|
@ -56,6 +57,7 @@ const LoopPage: React.FC = () => {
|
|||
</Column>
|
||||
</Row>
|
||||
</TileSection>
|
||||
<ChannelList channels={store.channels} />
|
||||
</PageWrap>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const theme = {
|
|||
blue: '#252f4a',
|
||||
darkBlue: '#212133',
|
||||
gray: '#848a99',
|
||||
darkGray: '#6b6969',
|
||||
white: '#ffffff',
|
||||
whitish: '#f5f5f5',
|
||||
pink: '#f5406e',
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export const lndWalletBalance: LND.WalletBalanceResponse.AsObject = {
|
|||
unconfirmedBalance: 0,
|
||||
};
|
||||
|
||||
export const lndListChannels: LND.ListChannelsResponse.AsObject = {
|
||||
export const lndListChannelsOne: LND.ListChannelsResponse.AsObject = {
|
||||
channelsList: [
|
||||
{
|
||||
active: true,
|
||||
|
|
@ -86,6 +86,25 @@ export const lndListChannels: LND.ListChannelsResponse.AsObject = {
|
|||
],
|
||||
};
|
||||
|
||||
export const lndListChannels: LND.ListChannelsResponse.AsObject = {
|
||||
channelsList: [...Array(500)].map((_, i) => {
|
||||
const c = lndListChannelsOne.channelsList[0];
|
||||
// pick a random capacity between 0.5 and 1 BTC
|
||||
const cap = Math.floor(Math.random() * 50000000) + 50000000;
|
||||
// pick a local balance that is at least 100K sats
|
||||
const local = Math.max(100000, Math.floor(Math.random() * cap - 100000));
|
||||
return {
|
||||
...c,
|
||||
chanId: `${i}${c.chanId}`,
|
||||
remotePubkey: `${i}${c.remotePubkey}`,
|
||||
localBalance: local,
|
||||
remoteBalance: cap - local,
|
||||
capacity: cap,
|
||||
uptime: Math.floor(Math.random() * (c.lifetime / 2)) + c.lifetime / 2,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
//
|
||||
// Loop API Responses
|
||||
//
|
||||
|
|
|
|||
|
|
@ -2313,6 +2313,14 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-virtualized@^9.21.9":
|
||||
version "9.21.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-virtualized/-/react-virtualized-9.21.9.tgz#10b57fa5979d008bd38633ad62a7461e14327cc0"
|
||||
integrity sha512-W7zTK290hACRANzYllz6hJJkeceYWyofxe5HeZt9gtONErB+y6f1UZNrhWe2wx1vuWQbdyZMKFRv9/nAmiXHew==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*", "@types/react@^16.9.0":
|
||||
version "16.9.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.32.tgz#f6368625b224604148d1ddf5920e4fefbd98d383"
|
||||
|
|
@ -4517,6 +4525,11 @@ clone@^2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
|
||||
integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
|
||||
|
||||
clsx@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.0.tgz#62937c6adfea771247c34b54d320fb99624f5702"
|
||||
integrity sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==
|
||||
|
||||
co@^4.6.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
|
|
@ -5178,7 +5191,7 @@ cssstyle@^1.0.0, cssstyle@^1.1.1:
|
|||
dependencies:
|
||||
cssom "0.3.x"
|
||||
|
||||
csstype@^2.2.0, csstype@^2.5.7:
|
||||
csstype@^2.2.0, csstype@^2.5.7, csstype@^2.6.7:
|
||||
version "2.6.10"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.10.tgz#e63af50e66d7c266edb6b32909cfd0aabe03928b"
|
||||
integrity sha512-D34BqZU4cIlMCY93rZHbrq9pjTAQJ3U8S8rfBqjwHxkGPThWFjzZDQpgMJY0QViLxth6ZKYiwFBo14RdN44U/w==
|
||||
|
|
@ -5479,6 +5492,14 @@ dom-converter@^0.2:
|
|||
dependencies:
|
||||
utila "~0.4"
|
||||
|
||||
dom-helpers@^5.0.0:
|
||||
version "5.1.4"
|
||||
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.1.4.tgz#4609680ab5c79a45f2531441f1949b79d6587f4b"
|
||||
integrity sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.8.7"
|
||||
csstype "^2.6.7"
|
||||
|
||||
dom-serializer@0:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51"
|
||||
|
|
@ -9008,7 +9029,7 @@ longest@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097"
|
||||
integrity sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.3.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
|
|
@ -11832,6 +11853,18 @@ react-textarea-autosize@^7.1.0:
|
|||
"@babel/runtime" "^7.1.2"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-virtualized@^9.21.2:
|
||||
version "9.21.2"
|
||||
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.21.2.tgz#02e6df65c1e020c8dbf574ec4ce971652afca84e"
|
||||
integrity sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==
|
||||
dependencies:
|
||||
babel-runtime "^6.26.0"
|
||||
clsx "^1.0.1"
|
||||
dom-helpers "^5.0.0"
|
||||
loose-envify "^1.3.0"
|
||||
prop-types "^15.6.0"
|
||||
react-lifecycles-compat "^3.0.4"
|
||||
|
||||
react@^16.13.1, react@^16.8.3:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue