refactor: ♻️ forward channels

This commit is contained in:
Anthony Potdevin 2020-11-15 11:55:34 +01:00
parent 72860f334e
commit 05fd6b2573
No known key found for this signature in database
GPG key ID: 4403F1DFBE779457
10 changed files with 144 additions and 334 deletions

View file

@ -1,10 +1,8 @@
import { getForwardChannelsReport } from './resolvers/getForwardChannelsReport';
import { getInOut } from './resolvers/getInOut'; import { getInOut } from './resolvers/getInOut';
import { getChannelReport } from './resolvers/getChannelReport'; import { getChannelReport } from './resolvers/getChannelReport';
export const widgetResolvers = { export const widgetResolvers = {
Query: { Query: {
getForwardChannelsReport,
getInOut, getInOut,
getChannelReport, getChannelReport,
}, },

View file

@ -1,157 +0,0 @@
import { getForwards, getWalletInfo, getClosedChannels } from 'ln-service';
import { subHours, subDays } from 'date-fns';
import { sortBy } from 'underscore';
import { ContextType } from 'server/types/apiTypes';
import { getNodeFromChannel } from 'server/helpers/getNodeFromChannel';
import { requestLimiter } from 'server/helpers/rateLimiter';
import { to } from 'server/helpers/async';
import {
GetForwardsType,
GetWalletInfoType,
GetClosedChannelsType,
} from 'server/types/ln-service.types';
import { countArray, countRoutes } from './helpers';
export const getForwardChannelsReport = async (
_: undefined,
params: any,
context: ContextType
) => {
await requestLimiter(context.ip, 'forwardChannels');
const { lnd } = context;
let startDate = new Date();
const endDate = new Date();
if (params.time === 'week') {
startDate = subDays(endDate, 7);
} else if (params.time === 'month') {
startDate = subDays(endDate, 30);
} else if (params.time === 'quarter_year') {
startDate = subDays(endDate, 90);
} else if (params.time === 'half_year') {
startDate = subDays(endDate, 180);
} else if (params.time === 'year') {
startDate = subDays(endDate, 360);
} else {
startDate = subHours(endDate, 24);
}
const closedChannels = await to<GetClosedChannelsType>(
getClosedChannels({
lnd,
})
);
const getRouteAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAliasIn = await getNodeFromChannel(
channel.in,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.in)
);
const nodeAliasOut = await getNodeFromChannel(
channel.out,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.out)
);
return {
aliasIn: nodeAliasIn.alias,
colorIn: nodeAliasIn.color,
aliasOut: nodeAliasOut.alias,
colorOut: nodeAliasOut.color,
...channel,
};
})
);
const getAlias = (array: any[], publicKey: string) =>
Promise.all(
array.map(async channel => {
const nodeAlias = await getNodeFromChannel(
channel.name,
publicKey,
lnd,
closedChannels?.channels.find(c => c.id === channel.name)
);
return {
alias: nodeAlias.alias,
color: nodeAlias.color,
...channel,
};
})
);
const forwardsList = await to<GetForwardsType>(
getForwards({
lnd,
after: startDate,
before: endDate,
})
);
const walletInfo = await to<GetWalletInfoType>(
getWalletInfo({
lnd,
})
);
let forwards = forwardsList.forwards;
let next = forwardsList.next;
let finishedFetching = false;
if (!next || !forwards || forwards.length <= 0) {
finishedFetching = true;
}
while (!finishedFetching) {
if (next) {
const moreForwards = await to<GetForwardsType>(
getForwards({ lnd, token: next })
);
forwards = [...forwards, ...moreForwards.forwards];
if (moreForwards.next) {
next = moreForwards.next;
} else {
finishedFetching = true;
}
} else {
finishedFetching = true;
}
}
if (params.type === 'route') {
const mapped = forwards.map(forward => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,
};
});
const grouped = countRoutes(mapped);
const routeAlias = await getRouteAlias(grouped, walletInfo.public_key);
const sortedRoute = sortBy(routeAlias, params.order).reverse().slice(0, 10);
return JSON.stringify(sortedRoute);
}
if (params.type === 'incoming') {
const incomingCount = countArray(forwards, true);
const incomingAlias = await getAlias(incomingCount, walletInfo.public_key);
const sortedInCount = sortBy(incomingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedInCount);
}
const outgoingCount = countArray(forwards, false);
const outgoingAlias = await getAlias(outgoingCount, walletInfo.public_key);
const sortedOutCount = sortBy(outgoingAlias, params.order)
.reverse()
.slice(0, 10);
return JSON.stringify(sortedOutCount);
};

View file

@ -1,5 +1,3 @@
import { reduce, groupBy } from 'underscore';
import { ForwardType } from 'server/types/ln-service.types';
import { InOutProps, InOutListProps } from './interface'; import { InOutProps, InOutListProps } from './interface';
export const reduceInOutArray = (list: InOutListProps) => { export const reduceInOutArray = (list: InOutListProps) => {
@ -7,8 +5,7 @@ export const reduceInOutArray = (list: InOutListProps) => {
for (const key in list) { for (const key in list) {
if (Object.prototype.hasOwnProperty.call(list, key)) { if (Object.prototype.hasOwnProperty.call(list, key)) {
const element: InOutProps[] = list[key]; const element: InOutProps[] = list[key];
const reducedArray: InOutProps = reduce( const reducedArray: InOutProps = element.reduce(
element,
(a, b) => ({ (a, b) => ({
tokens: a.tokens + b.tokens, tokens: a.tokens + b.tokens,
}), }),
@ -23,62 +20,3 @@ export const reduceInOutArray = (list: InOutListProps) => {
} }
return reducedOrder; return reducedOrder;
}; };
export const countArray = (list: ForwardType[], type: boolean) => {
const inOrOut = type ? 'incoming_channel' : 'outgoing_channel';
const grouped = groupBy(list, inOrOut);
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
name: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
export const countRoutes = (list: ForwardType[]) => {
const grouped = groupBy(list, 'route');
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
route: key,
in: element[0].incoming_channel,
out: element[0].outgoing_channel,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};

View file

@ -1,51 +0,0 @@
/* eslint-disable */
import * as Types from '../../types';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type GetForwardChannelsReportQueryVariables = Types.Exact<{
time?: Types.Maybe<Types.Scalars['String']>;
order?: Types.Maybe<Types.Scalars['String']>;
type?: Types.Maybe<Types.Scalars['String']>;
}>;
export type GetForwardChannelsReportQuery = (
{ __typename?: 'Query' }
& Pick<Types.Query, 'getForwardChannelsReport'>
);
export const GetForwardChannelsReportDocument = gql`
query GetForwardChannelsReport($time: String, $order: String, $type: String) {
getForwardChannelsReport(time: $time, order: $order, type: $type)
}
`;
/**
* __useGetForwardChannelsReportQuery__
*
* To run a query within a React component, call `useGetForwardChannelsReportQuery` and pass it any options that fit your needs.
* When your component renders, `useGetForwardChannelsReportQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetForwardChannelsReportQuery({
* variables: {
* time: // value for 'time'
* order: // value for 'order'
* type: // value for 'type'
* },
* });
*/
export function useGetForwardChannelsReportQuery(baseOptions?: Apollo.QueryHookOptions<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>) {
return Apollo.useQuery<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>(GetForwardChannelsReportDocument, baseOptions);
}
export function useGetForwardChannelsReportLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>) {
return Apollo.useLazyQuery<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>(GetForwardChannelsReportDocument, baseOptions);
}
export type GetForwardChannelsReportQueryHookResult = ReturnType<typeof useGetForwardChannelsReportQuery>;
export type GetForwardChannelsReportLazyQueryHookResult = ReturnType<typeof useGetForwardChannelsReportLazyQuery>;
export type GetForwardChannelsReportQueryResult = Apollo.QueryResult<GetForwardChannelsReportQuery, GetForwardChannelsReportQueryVariables>;

View file

@ -1,7 +0,0 @@
import { gql } from '@apollo/client';
export const GET_FORWARD_CHANNELS_REPORT = gql`
query GetForwardChannelsReport($time: String, $order: String, $type: String) {
getForwardChannelsReport(time: $time, order: $order, type: $type)
}
`;

View file

@ -7,28 +7,23 @@ import { ResponsiveSingle } from 'src/components/generic/Styled';
import { ReportType, ReportDuration, FlowReportType } from './ForwardReport'; import { ReportType, ReportDuration, FlowReportType } from './ForwardReport';
interface ButtonProps { interface ButtonProps {
isTime: ReportDuration; days: number;
isType: ReportType; order: ReportType;
setDays: (days: number) => void; setDays: (days: number) => void;
setIsTime: (text: ReportDuration) => void; setOrder: (text: ReportType) => void;
setIsType: (text: ReportType) => void;
} }
export const ButtonRow: React.FC<ButtonProps> = ({ export const ButtonRow: React.FC<ButtonProps> = ({
isTime, days,
setIsTime,
setDays, setDays,
isType, order,
setIsType, setOrder,
}) => { }) => {
const timeButton = (time: ReportDuration, title: string, days: number) => ( const timeButton = (title: string, buttonDays: number) => (
<SingleButton <SingleButton
withPadding={'4px 8px'} withPadding={'4px 8px'}
onClick={() => { onClick={() => setDays(buttonDays)}
setIsTime(time); selected={days === buttonDays}
setDays(days);
}}
selected={isTime === time}
> >
{title} {title}
</SingleButton> </SingleButton>
@ -37,8 +32,8 @@ export const ButtonRow: React.FC<ButtonProps> = ({
const typeButton = (type: ReportType, title: string) => ( const typeButton = (type: ReportType, title: string) => (
<SingleButton <SingleButton
withPadding={'4px 8px'} withPadding={'4px 8px'}
onClick={() => setIsType(type)} onClick={() => setOrder(type)}
selected={isType === type} selected={order === type}
> >
{title} {title}
</SingleButton> </SingleButton>
@ -47,12 +42,12 @@ export const ButtonRow: React.FC<ButtonProps> = ({
return ( return (
<ResponsiveSingle> <ResponsiveSingle>
<MultiButton> <MultiButton>
{timeButton('day', '1D', 1)} {timeButton('1D', 1)}
{timeButton('week', '1W', 7)} {timeButton('1W', 7)}
{timeButton('month', '1M', 30)} {timeButton('1M', 30)}
{timeButton('quarter_year', '3M', 90)} {timeButton('3M', 90)}
{timeButton('half_year', '6M', 180)} {timeButton('6M', 180)}
{timeButton('year', '1Y', 360)} {timeButton('1Y', 360)}
</MultiButton> </MultiButton>
<MultiButton> <MultiButton>
{typeButton('amount', 'Amount')} {typeButton('amount', 'Amount')}

View file

@ -2,11 +2,12 @@ import React, { useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { GitCommit, ArrowDown, ArrowUp } from 'react-feather'; import { GitCommit, ArrowDown, ArrowUp } from 'react-feather';
import styled from 'styled-components'; import styled from 'styled-components';
import { useGetForwardChannelsReportQuery } from 'src/graphql/queries/__generated__/getForwardChannelsReport.generated';
import { import {
MultiButton, MultiButton,
SingleButton, SingleButton,
} from 'src/components/buttons/multiButton/MultiButton'; } from 'src/components/buttons/multiButton/MultiButton';
import { useGetForwardsPastDaysQuery } from 'src/graphql/queries/__generated__/getForwardsPastDays.generated';
import { Forward } from 'src/graphql/types';
import { getErrorContent } from '../../../../utils/error'; import { getErrorContent } from '../../../../utils/error';
import { import {
DarkSubTitle, DarkSubTitle,
@ -16,7 +17,8 @@ import { LoadingCard } from '../../../../components/loading/LoadingCard';
import { getPrice } from '../../../../components/price/Price'; import { getPrice } from '../../../../components/price/Price';
import { useConfigState } from '../../../../context/ConfigContext'; import { useConfigState } from '../../../../context/ConfigContext';
import { usePriceState } from '../../../../context/PriceContext'; import { usePriceState } from '../../../../context/PriceContext';
import { ReportType, ReportDuration } from './ForwardReport'; import { ReportType } from './ForwardReport';
import { orderForwardChannels } from './helpers';
import { CardContent } from '.'; import { CardContent } from '.';
const ChannelRow = styled.div` const ChannelRow = styled.div`
@ -39,11 +41,12 @@ const LastTableLine = styled(TableLine)`
`; `;
type Props = { type Props = {
isTime: ReportDuration; days: number;
isType: ReportType; order: ReportType;
}; };
type ParsedRouteType = { type ParsedRouteType = {
route: string;
aliasIn: string; aliasIn: string;
aliasOut: string; aliasOut: string;
fee: number; fee: number;
@ -59,16 +62,16 @@ type ParsedChannelType = {
amount: number; amount: number;
}; };
export const ForwardChannelsReport = ({ isTime, isType }: Props) => { export const ForwardChannelsReport = ({ days, order }: Props) => {
const [type, setType] = useState<'route' | 'incoming' | 'outgoing'>('route'); const [type, setType] = useState<'route' | 'incoming' | 'outgoing'>('route');
const { currency, displayValues } = useConfigState(); const { currency, displayValues } = useConfigState();
const priceContext = usePriceState(); const priceContext = usePriceState();
const format = getPrice(currency, displayValues, priceContext); const format = getPrice(currency, displayValues, priceContext);
const { data, loading } = useGetForwardChannelsReportQuery({ const { data, loading } = useGetForwardsPastDaysQuery({
ssr: false, ssr: false,
variables: { time: isTime, order: isType, type }, variables: { days },
onError: error => toast.error(getErrorContent(error)), onError: error => toast.error(getErrorContent(error)),
}); });
@ -76,14 +79,15 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
return <LoadingCard noCard={true} title={'Forward Report'} />; return <LoadingCard noCard={true} title={'Forward Report'} />;
} }
// TODO: JSON.parse is really bad... Absolutely no type safety at all const forwardArray = orderForwardChannels(
const parsed: (ParsedChannelType | ParsedRouteType)[] = JSON.parse( type,
data.getForwardChannelsReport || '[]' order,
data.getForwardsPastDays as Forward[]
); );
const getFormatString = (amount: number | string) => { const getFormatString = (amount: number | string) => {
if (typeof amount === 'string') return amount; if (typeof amount === 'string') return amount;
if (isType !== 'amount') { if (order !== 'amount') {
return format({ amount }); return format({ amount });
} }
return amount; return amount;
@ -94,7 +98,7 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
<ChannelRow key={index}> <ChannelRow key={index}>
<TableLine>{channel.aliasIn}</TableLine> <TableLine>{channel.aliasIn}</TableLine>
<TableLine>{channel.aliasOut}</TableLine> <TableLine>{channel.aliasOut}</TableLine>
<LastTableLine>{getFormatString(channel[isType])}</LastTableLine> <LastTableLine>{getFormatString(channel[order])}</LastTableLine>
</ChannelRow> </ChannelRow>
)); ));
@ -115,7 +119,7 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
<ChannelRow key={index}> <ChannelRow key={index}>
<TableLine>{`${channel.alias}`}</TableLine> <TableLine>{`${channel.alias}`}</TableLine>
<DarkSubTitle>{`${channel.name}`}</DarkSubTitle> <DarkSubTitle>{`${channel.name}`}</DarkSubTitle>
<LastTableLine>{getFormatString(channel[isType])}</LastTableLine> <LastTableLine>{getFormatString(channel[order])}</LastTableLine>
</ChannelRow> </ChannelRow>
)); ));
@ -184,14 +188,14 @@ export const ForwardChannelsReport = ({ isTime, isType }: Props) => {
} }
}; };
if (parsed.length <= 0) { if (forwardArray.length <= 0) {
return null; return null;
} }
return ( return (
<CardContent> <CardContent>
{renderTitle()} {renderTitle()}
{renderContent(parsed)} {renderContent(forwardArray)}
</CardContent> </CardContent>
); );
}; };

View file

@ -36,10 +36,10 @@ export type FlowReportType = 'tokens' | 'amount';
interface Props { interface Props {
days: number; days: number;
isType: ReportType; order: ReportType;
} }
export const ForwardReport = ({ days, isType }: Props) => { export const ForwardReport = ({ days, order }: Props) => {
const { theme, currency, displayValues } = useConfigState(); const { theme, currency, displayValues } = useConfigState();
const priceContext = usePriceState(); const priceContext = usePriceState();
const format = getPrice(currency, displayValues, priceContext); const format = getPrice(currency, displayValues, priceContext);
@ -72,7 +72,7 @@ export const ForwardReport = ({ days, isType }: Props) => {
} }
const getLabelString = (value: number) => { const getLabelString = (value: number) => {
if (isType === 'amount') { if (order === 'amount') {
return numeral(value).format('0,0'); return numeral(value).format('0,0');
} }
return format({ amount: value }); return format({ amount: value });
@ -84,7 +84,7 @@ export const ForwardReport = ({ days, isType }: Props) => {
); );
const total = getLabelString( const total = getLabelString(
reduced.map(x => x[isType]).reduce((a, c) => a + c, 0) reduced.map(x => x[order]).reduce((a, c) => a + c, 0)
); );
const renderContent = () => { const renderContent = () => {
@ -101,14 +101,14 @@ export const ForwardReport = ({ days, isType }: Props) => {
height={110} height={110}
padding={{ padding={{
top: 20, top: 20,
left: isType === 'tokens' ? 80 : 50, left: order === 'tokens' ? 80 : 50,
right: 50, right: 50,
bottom: 10, bottom: 10,
}} }}
containerComponent={ containerComponent={
<VictoryVoronoiContainer <VictoryVoronoiContainer
voronoiDimension="x" voronoiDimension="x"
labels={({ datum }) => `${getLabelString(datum[isType])}`} labels={({ datum }) => `${getLabelString(datum[order])}`}
labelComponent={<VictoryTooltip orientation={'bottom'} />} labelComponent={<VictoryTooltip orientation={'bottom'} />}
/> />
} }
@ -131,13 +131,13 @@ export const ForwardReport = ({ days, isType }: Props) => {
axis: { stroke: 'transparent' }, axis: { stroke: 'transparent' },
}} }}
tickFormat={a => tickFormat={a =>
isType === 'tokens' ? format({ amount: a, breakNumber: true }) : a order === 'tokens' ? format({ amount: a, breakNumber: true }) : a
} }
/> />
<VictoryBar <VictoryBar
data={reduced} data={reduced}
x="period" x="period"
y={isType} y={order}
style={{ style={{
data: { data: {
fill: chartBarColor[theme], fill: chartBarColor[theme],

View file

@ -1,5 +1,6 @@
import { differenceInCalendarDays, differenceInHours, subDays } from 'date-fns'; import { differenceInCalendarDays, differenceInHours, subDays } from 'date-fns';
import groupBy from 'lodash.groupby'; import groupBy from 'lodash.groupby';
import sortBy from 'lodash.sortby';
import { Forward } from 'src/graphql/types'; import { Forward } from 'src/graphql/types';
type ListProps = { type ListProps = {
@ -58,3 +59,94 @@ export const orderAndReducedArray = (time: number, forwardArray: Forward[]) => {
return reducedOrderedDay; return reducedOrderedDay;
}; };
const countRoutes = (list: Forward[]) => {
const grouped = groupBy(list, 'route');
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
channelInfo.push({
aliasIn: element[0].incoming_node?.alias || 'Unknown',
aliasOut: element[0].outgoing_node?.alias || 'Unknown',
route: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
const countArray = (list: Forward[], type: boolean) => {
const inOrOut = type ? 'incoming_channel' : 'outgoing_channel';
const grouped = groupBy(list, inOrOut);
const channelInfo = [];
for (const key in grouped) {
if (Object.prototype.hasOwnProperty.call(grouped, key)) {
const element = grouped[key];
const fee = element
.map(forward => forward.fee)
.reduce((p: number, c: number) => p + c);
const tokens = element
.map(forward => forward.tokens)
.reduce((p: number, c: number) => p + c);
const alias = type
? element[0].incoming_node?.alias || 'Unknown'
: element[0].outgoing_node?.alias || 'Unknown';
channelInfo.push({
alias,
name: key,
amount: element.length,
fee,
tokens,
});
}
}
return channelInfo;
};
export const orderForwardChannels = (
type: string,
order: string,
forwardArray: Forward[]
) => {
if (type === 'route') {
const mapped = forwardArray.map(forward => {
return {
route: `${forward.incoming_channel} - ${forward.outgoing_channel}`,
...forward,
};
});
const grouped = countRoutes(mapped);
const sortedRoute = sortBy(grouped, order).reverse().slice(0, 10);
return sortedRoute;
}
if (type === 'incoming') {
const incomingCount = countArray(forwardArray, true);
const sortedInCount = sortBy(incomingCount, order).reverse().slice(0, 10);
return sortedInCount;
}
const outgoingCount = countArray(forwardArray, false);
const sortedOutCount = sortBy(outgoingCount, order).reverse().slice(0, 10);
return sortedOutCount;
};

View file

@ -8,7 +8,7 @@ import {
Separation, Separation,
} from '../../../../components/generic/Styled'; } from '../../../../components/generic/Styled';
import { mediaWidths } from '../../../../styles/Themes'; import { mediaWidths } from '../../../../styles/Themes';
import { ForwardReport, ReportDuration, ReportType } from './ForwardReport'; import { ForwardReport, ReportType } from './ForwardReport';
import { ForwardChannelsReport } from './ForwardChannelReport'; import { ForwardChannelsReport } from './ForwardChannelReport';
import { ButtonRow } from './Buttons'; import { ButtonRow } from './Buttons';
@ -24,9 +24,8 @@ export const CardContent = styled.div`
`; `;
export const ForwardBox = () => { export const ForwardBox = () => {
const [isTime, setIsTime] = useState<ReportDuration>('month');
const [days, setDays] = useState<number>(30); const [days, setDays] = useState<number>(30);
const [isType, setIsType] = useState<ReportType>('amount'); const [order, setOrder] = useState<ReportType>('amount');
return ( return (
<CardWithTitle> <CardWithTitle>
@ -35,15 +34,14 @@ export const ForwardBox = () => {
</CardTitle> </CardTitle>
<Card mobileCardPadding={'8px'}> <Card mobileCardPadding={'8px'}>
<ButtonRow <ButtonRow
isTime={isTime} days={days}
isType={isType} order={order}
setDays={setDays} setDays={setDays}
setIsTime={setIsTime} setOrder={setOrder}
setIsType={setIsType}
/> />
<ForwardReport days={days} isType={isType} /> <ForwardReport days={days} order={order} />
<Separation /> <Separation />
<ForwardChannelsReport isTime={isTime} isType={isType} /> <ForwardChannelsReport days={days} order={order} />
</Card> </Card>
</CardWithTitle> </CardWithTitle>
); );