feat: add initial channel list to Loop page

This commit is contained in:
jamaljsr 2020-04-25 03:34:25 -04:00
parent 492636c81f
commit 6ff4b753fb
13 changed files with 314 additions and 12 deletions

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.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>

View file

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

View 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} />;
};

View file

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

View 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

View file

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

View file

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

View 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;

View 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;

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

View file

@ -20,6 +20,7 @@ const theme = {
blue: '#252f4a',
darkBlue: '#212133',
gray: '#848a99',
darkGray: '#6b6969',
white: '#ffffff',
whitish: '#f5f5f5',
pink: '#f5406e',

View file

@ -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
//

View file

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