Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"lint": "eslint source --ext .js,.ts"
},
"dependencies": {
"axios": "1.13.2",
"axios": "1.13.6",
"cookie-parser": "1.4.7",
"cors": "2.8.5",
"csurf": "1.11.0",
Expand All @@ -23,7 +23,7 @@
"lnmessage": "0.2.9",
"protobufjs": "7.5.4",
"ts-node": "10.9.2",
"winston": "3.18.3",
"winston": "3.19.0",
"ws": "8.18.3"
},
"devDependencies": {
Expand Down
10 changes: 5 additions & 5 deletions apps/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"@fortawesome/free-solid-svg-icons": "7.1.0",
"@fortawesome/react-fontawesome": "3.1.0",
"@reduxjs/toolkit": "2.11.0",
"axios": "1.13.2",
"axios": "1.13.6",
"bootstrap": "5.3.8",
"copy-to-clipboard": "3.3.3",
"crypto-js": "4.2.0",
Expand All @@ -42,16 +42,16 @@
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
"@types/redux-mock-store": "1.5.0",
"axios-mock-adapter": "2.1.0",
"canvas": "3.2.0",
"react-scripts": "5.0.1",
"redux-mock-store": "1.5.5",
"@typescript-eslint/eslint-plugin": "8.48.0",
"@typescript-eslint/parser": "8.48.0",
"axios-mock-adapter": "2.1.0",
"canvas": "3.2.0",
"eslint": "9.39.1",
"eslint-plugin-jsx-a11y": "6.10.2",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "7.0.1",
"react-scripts": "5.0.1",
"redux-mock-store": "1.5.5",
"ts-jest": "29.4.5",
"typescript": "5.9.3"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback, useMemo } from 'react';
import './AccountEventsRoot.scss';
import { Card, Row, Col } from 'react-bootstrap';
import AccountEventsGraph from './AccountEventsGraph/AccountEventsGraph';
Expand All @@ -10,21 +10,72 @@ import { filterZeroActivityAccountEvents } from '../../../services/data-transfor
import { AccountEventsPeriod } from '../../../types/bookkeeper.type';
import { useSelector } from 'react-redux';
import { selectAccountEventPeriods } from '../../../store/bkprSelectors';
import { FilterMode } from '../../../utilities/constants';

const AccountEventsRoot = () => {
const navigate = useNavigate();
const accEvntPeriods = useSelector(selectAccountEventPeriods);
const [showZeroActivityPeriods, setShowZeroActivityPeriods] = useState<boolean>(false);
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
const [channelFilterMode, setChannelFilterMode] = useState<FilterMode>('include');
const [accountEventsData, setAccountEventsData] = useState<AccountEventsPeriod[]>(accEvntPeriods);
const multiSelectOptions = useMemo(() => {
const seen = new Set<string>();
const accounts: { name: string; dataKey: string }[] = [];
accEvntPeriods.forEach(period => {
period.accounts.forEach(account => {
const id = account.short_channel_id || account.account || account.remote_alias || '';
if (!seen.has(id)) {
seen.add(id);
accounts.push({ name: id, dataKey: id });
}
});
});
return accounts;
}, [accEvntPeriods]);

const handleShowZeroActivityChange = (show: boolean) => {
const applyFilters = useCallback((
periods: AccountEventsPeriod[],
showZero: boolean,
channelIds: string[],
filterMode: FilterMode,
): AccountEventsPeriod[] => {
const zeroFiltered = filterZeroActivityAccountEvents(periods, showZero);

if (!channelIds || channelIds.length === 0) {
return zeroFiltered;
}

return zeroFiltered.map(period => ({
...period,
accounts: period.accounts.filter(account => {
const id = account.short_channel_id;
const isMatch = id ? channelIds.includes(id) : false;
return filterMode === 'include' ? isMatch : !isMatch;
}),
}));
}, []);

const handleShowZeroActivityChange = useCallback((show: boolean) => {
setShowZeroActivityPeriods(show);
setAccountEventsData(filterZeroActivityAccountEvents((accEvntPeriods), show));
};
setAccountEventsData(applyFilters(accEvntPeriods, show, selectedChannelIds, channelFilterMode));
}, [accEvntPeriods, selectedChannelIds, channelFilterMode, applyFilters]);

const multiSelectChangeHandler = useCallback((selectedOptions: string[], filterMode: FilterMode) => {
setTimeout(() => {
setSelectedChannelIds(selectedOptions);
setChannelFilterMode(filterMode);
setAccountEventsData(
applyFilters(accEvntPeriods, showZeroActivityPeriods, selectedOptions, filterMode)
);
}, 0);
}, [accEvntPeriods, showZeroActivityPeriods, applyFilters]);

useEffect(() => {
setAccountEventsData(filterZeroActivityAccountEvents((accEvntPeriods), showZeroActivityPeriods));
}, [accEvntPeriods, showZeroActivityPeriods]);
setAccountEventsData(
applyFilters(accEvntPeriods, showZeroActivityPeriods, selectedChannelIds, channelFilterMode)
);
}, [accEvntPeriods]);

return (
<div className='account-events-container' data-testid='account-events-container'>
Expand All @@ -35,20 +86,24 @@ const AccountEventsRoot = () => {
<Col className='text-end'>
<span
className='span-close-svg'
onClick={() => {
navigate('..');
}}
onClick={() => navigate('..')}
>
<CloseSVG />
</span>
</Col>
</Row>
<DataFilterOptions filter='accountevents' onShowZeroActivityChange={handleShowZeroActivityChange} />
<DataFilterOptions
filter='accountevents'
onShowZeroActivityChange={handleShowZeroActivityChange}
multiSelectValues={multiSelectOptions}
multiSelectPlaceholder='Filter Channels'
multiSelectChangeHandler={multiSelectChangeHandler}
/>
</Card.Header>
<Card.Body className='pt-1 pb-3 d-flex flex-column align-items-center'>
<Col xs={12} className='account-events-graph-container'>
<AccountEventsGraph periods={accountEventsData} />
</Col>
<Col xs={12} className='account-events-graph-container'>
<AccountEventsGraph periods={accountEventsData} />
</Col>
<Col xs={12} className='account-events-table-container'>
<AccountEventsTable periods={accountEventsData} />
</Col>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import './AccountEventsTable.scss';
import { Button, OverlayTrigger, Table, Tooltip } from 'react-bootstrap';
import { formatCurrency } from '../../../../utilities/data-formatters';
import { TRANSITION_DURATION, Units } from '../../../../utilities/constants';
import { ChevronDown } from '../../../../svgs/ChevronDown';
import { motion, AnimatePresence } from 'framer-motion';
import PerfectScrollbar from 'react-perfect-scrollbar';
import { AccountEventsPeriod } from '../../../../types/bookkeeper.type';
import { useSelector } from 'react-redux';
import { selectUIConfigUnit } from '../../../../store/rootSelectors';
import { ChevronSVG } from '../../../../svgs/Chevron';

function AccountEventsTable({periods}: {periods: AccountEventsPeriod[]}) {
const [expandedRows, setExpandedRows] = useState<string[]>([]);
Expand Down Expand Up @@ -65,13 +65,8 @@ function AccountEventsTable({periods}: {periods: AccountEventsPeriod[]}) {
overlay={<Tooltip>{expandedRows.includes(period.period_key) ? 'Hide Details' : 'Show Details'}</Tooltip>}
>
<td>
<Button variant="link" onClick={() => toggleRow(period.period_key)}>
<motion.div
animate={{ rotate: expandedRows.includes(period.period_key) ? -180 : 0 }}
transition={{ duration: TRANSITION_DURATION }}
>
<ChevronDown width={16} height={10} />
</motion.div>
<Button className='py-0' variant="link" onClick={() => toggleRow(period.period_key)}>
<ChevronSVG open={expandedRows.includes(period.period_key)} width={'16'} height={'10'} />
</Button>
</td>
</OverlayTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,5 @@
import { memo, useMemo } from 'react';
import {
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
ComposedChart,
Legend,
Line
} from 'recharts';
import { Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ComposedChart, Legend, Line } from 'recharts';
import './SatsFlowGraph.scss';
import { Row, Col } from 'react-bootstrap';
import { Units } from '../../../../utilities/constants';
Expand All @@ -19,6 +9,15 @@ import { SatsFlowPeriod } from '../../../../types/bookkeeper.type';
import { useSelector } from 'react-redux';
import { selectUIConfigUnit } from '../../../../store/rootSelectors';

const ALL_EVENTS_VALUES = [
{ name: 'routed', dataKey: 'routed', fill: "rgba(201, 222, 83, 1)" },
{ name: 'invoice_fee', dataKey: 'invoice_fee', fill: "rgba(237, 88, 59, 1)" },
{ name: 'received_invoice', dataKey: 'received_invoice', fill: "rgba(121, 203, 96, 1)" },
{ name: 'paid_invoice', dataKey: 'paid_invoice', fill: "rgba(240, 147, 46, 1)" },
{ name: 'deposit', dataKey: 'deposit', fill: "rgba(0, 198, 160, 1)" },
{ name: 'onchain_fee', dataKey: 'onchain_fee', fill: "rgba(242, 207, 32, 1)" },
];

const SatsFlowGraphTooltip = ({ active, payload, label, unit, periods }: any) => {
if (active && payload && payload.length >= 0) {
const period = periods.find(d => d.period_key === label);
Expand All @@ -42,39 +41,38 @@ const SatsFlowGraphTooltip = ({ active, payload, label, unit, periods }: any) =>
return null;
}
})}
</div>
</div>
);
}
return null;
};

const SatsFlowGraphLegend = (props: any) => {
const { payload } = props;
const SatsFlowGraphLegend = ({ eventValues }: { eventValues: typeof ALL_EVENTS_VALUES }) => {
return (
<Row className='gx-3 gy-1 justify-content-center align-items-center'>
{payload
.filter((entry: any) => entry.value !== 'net_inflow_msat')
.map((entry: any, index: number) => (
<Col key={`item-${index}`} xs='auto' className='col-sats-flow-lagend d-flex align-items-center'>
<div className='sats-flow-lagend-bullet' style={{ backgroundColor: entry.color }}/>
<span className='span-sats-flow-lagend'>{titleCase(entry.value.replace(/_/g, ' '))}</span>
</Col>
))
}
{eventValues.map((entry, index) => (
<Col key={`item-${index}`} xs='auto' className='col-sats-flow-lagend d-flex align-items-center'>
<div className='sats-flow-lagend-bullet' style={{ backgroundColor: entry.fill }} />
<span className='span-sats-flow-lagend'>{titleCase(entry.name.replace(/_/g, ' '))}</span>
</Col>
))}
</Row>
)
);
};

const CustomActiveDot = (props) => <circle cx={props.cx} cy={props.cy} r={4} fill="var(--bs-body-color)" />;

function SatsFlowGraph({periods}: {periods: SatsFlowPeriod[]}) {
function SatsFlowGraph({ periods }: { periods: SatsFlowPeriod[] }) {
const uiConfigUnit = useSelector(selectUIConfigUnit);
const barColors = ["rgba(0, 198, 160, 1)", "rgba(121, 203, 96, 1)", "rgba(201, 222, 83, 1)", "rgba(242, 207, 32, 1)", "rgba(240, 147, 46, 1)", "rgba(237, 88, 59, 1)"];

const data = useMemo(() => {
return transformSatsFlowGraphData(periods);
const eventValues = useMemo(() => {
const presentTags = new Set(
periods.flatMap(period => period.tag_groups.map(group => group.tag))
);
return ALL_EVENTS_VALUES.filter(bar => presentTags.has(bar.name));
}, [periods]);

const data = useMemo(() => transformSatsFlowGraphData(periods), [periods]);

return (
<div data-testid='sats-flow-graph' className='sats-flow-graph'>
<ResponsiveContainer width='100%'>
Expand All @@ -85,25 +83,22 @@ function SatsFlowGraph({periods}: {periods: SatsFlowPeriod[]}) {
>
<CartesianGrid strokeDasharray='3 3' />
<XAxis dataKey='name' />
<YAxis
<YAxis
tickFormatter={(value) => {
const formatted = formatCurrency(value, Units.MSATS, uiConfigUnit, false, 0, 'string');
return typeof formatted === 'string' ? formatted : String(formatted);
}}
/>
<Tooltip content={<SatsFlowGraphTooltip unit={uiConfigUnit} periods={periods} />} />
<Legend content={SatsFlowGraphLegend} />
<Bar name='routed' dataKey='routed' stackId='bar' fill={barColors[2]} />
<Bar name='invoice_fee' dataKey='invoice_fee' stackId='bar' fill={barColors[5]} />
<Bar name='received_invoice' dataKey='received_invoice' stackId='bar' fill={barColors[1]} />
<Bar name='paid_invoice' dataKey='paid_invoice' stackId='bar' fill={barColors[4]} />
<Bar name='deposit' dataKey='deposit' stackId='bar' fill={barColors[0]} />
<Bar name='onchain_fee' dataKey='onchain_fee' stackId='bar' fill={barColors[3]} />
<Legend content={<SatsFlowGraphLegend eventValues={eventValues} />} />
{eventValues.map((value: any) => (
<Bar name={value.name} key={value.name} dataKey={value.dataKey} stackId='bar' fill={value.fill} />
))}
<Line className='series-net-inflow' type='monotone' dataKey='net_inflow_msat' activeDot={<CustomActiveDot />} />
</ComposedChart>
</ResponsiveContainer>
</div>
);
};
}

export default memo(SatsFlowGraph);
Loading
Loading