feat: channel ui manager

This commit is contained in:
AP 2020-05-24 00:58:19 +02:00
parent 8099a3919e
commit 35cf8db27f
12 changed files with 344 additions and 112 deletions

View file

@ -41,7 +41,7 @@ const Wrapper: React.FC = ({ children }) => {
return <>{children}</>;
}
if (hasAccount === 'false') {
return <LoadingCard noCard={true} />;
return <LoadingCard loadingHeight={'50vh'} noCard={true} />;
}
return <GridWrapper>{children}</GridWrapper>;
};
@ -100,19 +100,18 @@ const App = ({
App.getInitialProps = async props => {
const cookieParam = getUrlParam(props.router?.query?.token);
const cookies = parseCookies(props.ctx.req);
const defaultState = {};
if (!cookies?.config) {
return { initialConfig: {}, ...defaultState };
return { initialConfig: {} };
}
try {
const config = JSON.parse(cookies.config);
return {
initialConfig: { ...config, ...defaultState },
initialConfig: config,
cookieParam,
};
} catch (error) {
return { initialConfig: {}, cookieParam, ...defaultState };
return { initialConfig: {}, cookieParam };
}
};

View file

@ -84,10 +84,9 @@ const BalanceView = () => {
};
const renderChannels = (isOutgoing?: boolean) => {
const channels = sortBy(data.getChannels, [
(channel: any) =>
getPercent(channel.remote_balance, channel.local_balance),
]);
const channels = sortBy(data.getChannels, channel =>
getPercent(channel.remote_balance, channel.local_balance)
);
const finalChannels = isOutgoing ? channels : channels.reverse();

View file

@ -2,6 +2,9 @@ import React, { useState, useEffect } from 'react';
import { useAccountState } from 'src/context/AccountContext';
import { useGetChannelAmountInfoQuery } from 'src/graphql/queries/__generated__/getNodeInfo.generated';
import styled from 'styled-components';
import { Settings } from 'react-feather';
import { IconCursor } from 'src/views/channels/channels/Channel.style';
import { ChannelManage } from 'src/views/channels/channels/ChannelManage';
import { Channels } from '../src/views/channels/channels/Channels';
import { PendingChannels } from '../src/views/channels/pendingChannels/PendingChannels';
import { ClosedChannels } from '../src/views/channels/closedChannels/ClosedChannels';
@ -28,6 +31,7 @@ const ButtonRow = styled.div`
`;
const ChannelView = () => {
const [isOpen, isOpenSet] = useState<boolean>(false);
const [view, setView] = useState<number>(1);
const [amounts, setAmounts] = useState({
active: 0,
@ -94,8 +98,14 @@ const ChannelView = () => {
<SmallButton onClick={() => setView(3)}>
{`Closed (${amounts.closed})`}
</SmallButton>
{view === 1 && (
<IconCursor>
<Settings size={16} onClick={() => isOpenSet(p => !p)} />
</IconCursor>
)}
</ButtonRow>
</ChannelsCardTitle>
{view === 1 && isOpen && <ChannelManage />}
{getView()}
</CardWithTitle>
);

View file

@ -27,6 +27,8 @@ export const ProgressBar = styled.div<ProgressBar>`
return chartColors.green;
case 3:
return chartColors.orange;
case 4:
return progressBackground;
default:
return chartColors.purple;
}

View file

@ -81,13 +81,18 @@ interface SubCardProps {
padding?: string;
withMargin?: string;
noCard?: boolean;
noBackground?: boolean;
}
export const SubCard = styled.div<SubCardProps>`
margin: ${({ withMargin }) => (withMargin ? withMargin : '0 0 10px 0')};
padding: ${({ padding }) => (padding ? padding : '16px')};
background: ${subCardColor};
border: 1px solid ${cardBorderColor};
${({ noBackground }) =>
!noBackground &&
css`
background: ${subCardColor};
border: 1px solid ${cardBorderColor};
`}
border-left: ${({ color }) => (color ? `2px solid ${color}` : '')};
&:hover {

View file

@ -5,6 +5,11 @@ import Cookies from 'js-cookie';
const themeTypes = ['dark', 'light'];
const currencyTypes = ['sat', 'btc', 'EUR', 'USD'];
export type channelBarStyleTypes = 'normal' | 'compact' | 'ultracompact';
export type channelBarTypeTypes = 'balance' | 'details' | 'partner';
export type channelSortTypes = 'none' | 'local' | 'balance';
export type sortDirectionTypes = 'increase' | 'decrease';
type State = {
currency: string;
theme: string;
@ -17,7 +22,10 @@ type State = {
hideNonVerified: boolean;
maxFee: number;
chatPollingSpeed: number;
channelBarType: 'normal' | 'partner';
channelBarStyle: channelBarStyleTypes;
channelBarType: channelBarTypeTypes;
channelSort: channelSortTypes;
sortDirection: sortDirectionTypes;
};
type ConfigInitProps = {
@ -37,7 +45,10 @@ type ActionType = {
hideNonVerified?: boolean;
maxFee?: number;
chatPollingSpeed?: number;
channelBarType?: 'normal' | 'partner';
channelBarStyle?: channelBarStyleTypes;
channelBarType?: channelBarTypeTypes;
channelSort?: channelSortTypes;
sortDirection?: sortDirectionTypes;
};
type Dispatch = (action: ActionType) => void;
@ -65,7 +76,10 @@ const initialState: State = {
hideNonVerified: false,
maxFee: 20,
chatPollingSpeed: 1000,
channelBarType: 'normal',
channelBarStyle: 'normal',
channelBarType: 'balance',
channelSort: 'none',
sortDirection: 'decrease',
};
const stateReducer = (state: State, action: ActionType): State => {

View file

@ -34,10 +34,11 @@ export const ChannelColumnSection = styled.div`
export const ChannelLineSection = styled.div`
display: flex;
align-items: center;
max-width: 300px;
flex-direction: column;
width: 180px;
@media (${mediaWidths.mobile}) {
align-items: center;
max-width: unset;
justify-content: center;
width: 100%;

View file

@ -62,7 +62,7 @@ export const BalanceCard = ({
} = channel;
const { alias } = partner_node_info;
const balancedness = getPercent(remote_balance, local_balance) / 100;
const balancedness = getPercent(local_balance, remote_balance) / 100;
const formatBalance = numeral(balancedness).format('0,0.00');
const props = withColor ? { color: themeColors.blue3 } : {};
@ -72,9 +72,12 @@ export const BalanceCard = ({
<MainInfo onClick={() => callback && callback()}>
<ResponsiveLine withWrap={true}>
<ChannelLineSection>
{alias && alias !== ''
? `${alias} - ${id}`
: `${partner_public_key?.substring(0, 6)} - ${id}`}
<div>
{alias && alias !== ''
? alias
: partner_public_key?.substring(0, 6)}
</div>
<DarkSubTitle>{id}</DarkSubTitle>
</ChannelLineSection>
<ChannelColumnSection>
<SingleLine>

View file

@ -0,0 +1,55 @@
import styled from 'styled-components';
import { mediaWidths } from 'src/styles/Themes';
export const ChannelIconPadding = styled.div`
display: flex;
margin-left: 8px;
`;
export const ChannelStatsColumn = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
`;
export const ChannelStatsLine = styled.div`
width: 100%;
display: flex;
`;
export const ChannelBarSide = styled.div`
width: 50%;
display: flex;
flex-direction: column;
cursor: pointer;
@media (${mediaWidths.mobile}) {
width: 100%;
}
`;
export const ChannelNodeTitle = styled.div`
font-size: 16px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (${mediaWidths.mobile}) {
text-align: center;
margin-bottom: 8px;
}
`;
export const ChannelSingleLine = styled.div`
display: flex;
align-items: center;
`;
export const IconCursor = styled.div`
display: flex;
align-items: center;
cursor: pointer;
margin-left: 8px;
`;

View file

@ -1,8 +1,6 @@
import React, { useState } from 'react';
import ReactTooltip from 'react-tooltip';
import styled from 'styled-components';
import { ArrowDown, ArrowUp, EyeOff } from 'react-feather';
import { mediaWidths } from 'src/styles/Themes';
import { ChannelType } from 'src/graphql/types';
import { getPercent, formatSeconds } from '../../../utils/helpers';
import {
@ -18,10 +16,7 @@ import {
ResponsiveLine,
DarkSubTitle,
} from '../../../components/generic/Styled';
import {
useConfigState,
useConfigDispatch,
} from '../../../context/ConfigContext';
import { useConfigState } from '../../../context/ConfigContext';
import {
getStatusDot,
getTooltipType,
@ -37,47 +32,14 @@ import { AdminSwitch } from '../../../components/adminSwitch/AdminSwitch';
import { ColorButton } from '../../../components/buttons/colorButton/ColorButton';
import { getPrice } from '../../../components/price/Price';
import { usePriceState } from '../../../context/PriceContext';
const IconPadding = styled.div`
display: flex;
flex-direction: column;
margin-left: 8px;
@media (${mediaWidths.mobile}) {
display: none;
}
`;
const StatsColumn = styled.div`
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-end;
`;
const BarSide = styled.div`
width: 50%;
display: flex;
flex-direction: column;
cursor: pointer;
@media (${mediaWidths.mobile}) {
width: 100%;
}
`;
const NodeTitle = styled.div`
font-size: 16px;
font-weight: 700;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@media (${mediaWidths.mobile}) {
text-align: center;
margin-bottom: 8px;
}
`;
import {
ChannelNodeTitle,
ChannelBarSide,
ChannelIconPadding,
ChannelStatsColumn,
ChannelSingleLine,
ChannelStatsLine,
} from './Channel.style';
const getSymbol = (status: boolean) => {
return status ? <ArrowDown size={14} /> : <ArrowUp size={14} />;
@ -115,8 +77,7 @@ export const ChannelCard = ({
biggestBaseFee,
biggestRateFee,
}: ChannelCardProps) => {
const { channelBarType } = useConfigState();
const dispatch = useConfigDispatch();
const { channelBarType, channelBarStyle } = useConfigState();
const [modalOpen, setModalOpen] = useState(false);
const { theme, currency, displayValues } = useConfigState();
@ -180,13 +141,6 @@ export const ChannelCard = ({
}
};
const handleBarClick = () => {
dispatch({
type: 'change',
channelBarType: channelBarType === 'normal' ? 'partner' : 'normal',
});
};
const renderPartner = () =>
alias ? (
<>
@ -196,8 +150,8 @@ export const ChannelCard = ({
'Last Update:',
`${getDateDif(updated_at)} ago (${getFormatDate(updated_at)})`
)}
{renderLine('Base Fee:', `${base_fee} mSats`)}
{renderLine('Fee Rate:', `${fee_rate} sats/MSats`)}
{renderLine('Base Fee:', `${base_fee} msats`)}
{renderLine('Fee Rate:', `${fee_rate} sats/Msats`)}
{renderLine('CTLV Delta:', cltv_delta)}
</>
) : (
@ -253,7 +207,7 @@ export const ChannelCard = ({
switch (channelBarType) {
case 'partner':
return (
<>
<ChannelStatsColumn>
<ProgressBar
order={0}
percent={getBar(Number(partnerNodeCapacity), biggestPartner)}
@ -264,16 +218,35 @@ export const ChannelCard = ({
/>
<ProgressBar order={1} percent={getBar(base_fee, biggestBaseFee)} />
<ProgressBar order={2} percent={getBar(fee_rate, biggestRateFee)} />
</>
</ChannelStatsColumn>
);
default:
case 'details':
return (
<>
<ChannelStatsColumn>
<ProgressBar order={0} percent={getBar(local_balance, biggest)} />
<ProgressBar order={1} percent={getBar(remote_balance, biggest)} />
<ProgressBar order={2} percent={getBar(received, biggest)} />
<ProgressBar order={3} percent={getBar(sent, biggest)} />
</>
</ChannelStatsColumn>
);
default:
return (
<ChannelStatsColumn>
<ChannelStatsLine>
<ProgressBar
order={1}
percent={getPercent(local_balance, remote_balance)}
/>
<ProgressBar
order={4}
percent={getPercent(remote_balance, local_balance)}
/>
</ChannelStatsLine>
<ChannelStatsLine>
<ProgressBar order={2} percent={getPercent(received, sent)} />
<ProgressBar order={4} percent={getPercent(sent, received)} />
</ChannelStatsLine>
</ChannelStatsColumn>
);
}
};
@ -285,10 +258,11 @@ export const ChannelCard = ({
<>
<div>{`Partner Capacity: ${nodeCapacity}`}</div>
<div>{`Partner Channels: ${channel_count}`}</div>
<div>{`Partner Base Fee: ${base_fee} mSats`}</div>
<div>{`Partner Fee Rate: ${fee_rate} sats/MSats`}</div>
<div>{`Partner Base Fee: ${base_fee} msats`}</div>
<div>{`Partner Fee Rate: ${fee_rate} sats/Msats`}</div>
</>
);
case 'details':
default:
return (
<>
@ -301,34 +275,52 @@ export const ChannelCard = ({
}
};
const getSubCardProps = () => {
switch (channelBarStyle) {
case 'ultracompact':
return {
withMargin: '0 0 4px 0',
padding: index === indexOpen ? '0 0 16px' : '2px 0',
noBackground: true,
};
case 'compact':
return {
withMargin: '0 0 4px 0',
padding: index === indexOpen ? '4px 8px 16px' : '4px 8px',
};
default:
return {};
}
};
return (
<SubCard key={`${index}-${id}`} noCard={true}>
<StatusLine>
{getStatusDot(is_active, 'active')}
{getStatusDot(is_opening, 'opening')}
{getStatusDot(is_closing, 'closing')}
</StatusLine>
<ResponsiveLine>
<NodeTitle style={{ flexGrow: 2 }}>
<MainInfo onClick={() => handleClick()}>
<SubCard key={`${index}-${id}`} noCard={true} {...getSubCardProps()}>
<MainInfo onClick={() => handleClick()}>
{channelBarStyle === 'normal' && (
<StatusLine>
{getStatusDot(is_active, 'active')}
{getStatusDot(is_opening, 'opening')}
{getStatusDot(is_closing, 'closing')}
</StatusLine>
)}
<ResponsiveLine>
<ChannelNodeTitle style={{ flexGrow: 2 }}>
{alias || partner_public_key?.substring(0, 6)}
<DarkSubTitle>{formatBalance}</DarkSubTitle>
</MainInfo>
</NodeTitle>
<BarSide>
<StatsColumn
data-tip
data-for={`node_balance_tip_${index}`}
onClick={handleBarClick}
>
{channelBarStyle !== 'ultracompact' && (
<ChannelSingleLine>
<DarkSubTitle>{formatBalance}</DarkSubTitle>
<ChannelIconPadding>
{getPrivate(is_private)}
{getSymbol(is_partner_initiated)}
</ChannelIconPadding>
</ChannelSingleLine>
)}
</ChannelNodeTitle>
<ChannelBarSide data-tip data-for={`node_balance_tip_${index}`}>
{renderBars()}
</StatsColumn>
</BarSide>
<IconPadding>
{getPrivate(is_private)}
{getSymbol(is_partner_initiated)}
</IconPadding>
</ResponsiveLine>
</ChannelBarSide>
</ResponsiveLine>
</MainInfo>
{index === indexOpen && renderDetails()}
<ReactTooltip
id={`node_balance_tip_${index}`}

View file

@ -0,0 +1,131 @@
import * as React from 'react';
import {
SingleButton,
MultiButton,
} from 'src/components/buttons/multiButton/MultiButton';
import {
useConfigState,
useConfigDispatch,
channelBarStyleTypes,
channelBarTypeTypes,
channelSortTypes,
sortDirectionTypes,
} from 'src/context/ConfigContext';
import { Card, SingleLine, Sub4Title } from 'src/components/generic/Styled';
import styled from 'styled-components';
const MarginLine = styled(SingleLine)`
margin: 8px 0;
`;
export const ChannelManage = () => {
const {
channelBarType,
channelBarStyle,
channelSort,
sortDirection,
} = useConfigState();
const dispatch = useConfigDispatch();
const changeStyle = (style: channelBarStyleTypes) =>
dispatch({ type: 'change', channelBarStyle: style });
const changeType = (type: channelBarTypeTypes) =>
dispatch({ type: 'change', channelBarType: type });
const changeSort = (type: channelSortTypes) =>
dispatch({ type: 'change', channelSort: type });
const changeDirection = (type: sortDirectionTypes) =>
dispatch({ type: 'change', sortDirection: type });
return (
<Card>
<MarginLine>
<Sub4Title>Card Type</Sub4Title>
<MultiButton>
<SingleButton
selected={channelBarStyle === 'normal'}
onClick={() => changeStyle('normal')}
>
Normal
</SingleButton>
<SingleButton
selected={channelBarStyle === 'compact'}
onClick={() => changeStyle('compact')}
>
Compact
</SingleButton>
<SingleButton
selected={channelBarStyle === 'ultracompact'}
onClick={() => changeStyle('ultracompact')}
>
Ultra-Compact
</SingleButton>
</MultiButton>
</MarginLine>
<MarginLine>
<Sub4Title>Bar Types</Sub4Title>
<MultiButton>
<SingleButton
selected={channelBarType === 'balance'}
onClick={() => changeType('balance')}
>
Balance
</SingleButton>
<SingleButton
selected={channelBarType === 'details'}
onClick={() => changeType('details')}
>
Proportional
</SingleButton>
<SingleButton
selected={channelBarType === 'partner'}
onClick={() => changeType('partner')}
>
Partner
</SingleButton>
</MultiButton>
</MarginLine>
<MarginLine>
<Sub4Title>Sort</Sub4Title>
<MultiButton>
<SingleButton
selected={channelSort === 'none'}
onClick={() => changeSort('none')}
>
None
</SingleButton>
<SingleButton
selected={channelSort === 'local'}
onClick={() => changeSort('local')}
>
Local
</SingleButton>
<SingleButton
selected={channelSort === 'balance'}
onClick={() => changeSort('balance')}
>
Balance
</SingleButton>
</MultiButton>
</MarginLine>
{channelSort !== 'none' && (
<MarginLine>
<Sub4Title>Direction</Sub4Title>
<MultiButton>
<SingleButton
selected={sortDirection === 'increase'}
onClick={() => changeDirection('increase')}
>
Increasing
</SingleButton>
<SingleButton
selected={sortDirection === 'decrease'}
onClick={() => changeDirection('decrease')}
>
Decreasing
</SingleButton>
</MultiButton>
</MarginLine>
)}
</Card>
);
};

View file

@ -2,12 +2,16 @@ import React, { useState } from 'react';
import { toast } from 'react-toastify';
import { useAccountState } from 'src/context/AccountContext';
import { useGetChannelsQuery } from 'src/graphql/queries/__generated__/getChannels.generated';
import { useConfigState } from 'src/context/ConfigContext';
import { sortBy } from 'underscore';
import { getPercent } from 'src/utils/helpers';
import { Card } from '../../../components/generic/Styled';
import { getErrorContent } from '../../../utils/error';
import { LoadingCard } from '../../../components/loading/LoadingCard';
import { ChannelCard } from './ChannelCard';
export const Channels = () => {
const { sortDirection, channelSort } = useConfigState();
const [indexOpen, setIndexOpen] = useState(0);
const { auth } = useAccountState();
@ -62,9 +66,26 @@ export const Channels = () => {
}
}
const getChannels = () => {
switch (channelSort) {
case 'local': {
const newArray = sortBy(data.getChannels, 'local_balance');
return sortDirection === 'increase' ? newArray : newArray.reverse();
}
case 'balance': {
const newArray = sortBy(data.getChannels, channel =>
getPercent(channel.local_balance, channel.remote_balance)
);
return sortDirection === 'increase' ? newArray : newArray.reverse();
}
default:
return data.getChannels;
}
};
return (
<Card mobileCardPadding={'0'} mobileNoBackground={true}>
{data.getChannels.map((channel, index: number) => (
{getChannels().map((channel, index: number) => (
<ChannelCard
channelInfo={channel}
index={index + 1}
@ -74,7 +95,7 @@ export const Channels = () => {
biggest={biggest}
biggestPartner={biggestPartner}
mostChannels={mostChannels}
biggestBaseFee={biggestBaseFee * 2}
biggestBaseFee={biggestBaseFee}
biggestRateFee={biggestRateFee}
/>
))}