Skip to content

Commit 6f76934

Browse files
evansmjShahanaFarooqui
authored andcommitted
BKPR Account Events
1 parent 4541352 commit 6f76934

File tree

10 files changed

+620
-25
lines changed

10 files changed

+620
-25
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
@import '../../../../styles/constants.scss';
2+
3+
.bkpr-tooltip {
4+
border: 1px solid $light-dark;
5+
color: $dark;
6+
background-color: $white;
7+
border-radius: 4px;
8+
box-shadow: 0px 4px 8px 0px rgba($gray-400, 0.16);
9+
}
10+
11+
.account-events-graph {
12+
width: 100%;
13+
height: 100%;
14+
}
15+
16+
@include color-mode(dark) {
17+
.bkpr-tooltip {
18+
border: 1px solid $card-bg-dark;
19+
color: $light-dark;
20+
background-color: $tooltip-bg-dark;
21+
}
22+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { screen } from '@testing-library/react';
2+
import { renderWithProviders } from '../../../../utilities/test-utilities/mockStore';
3+
import { mockBKPRAccountEvents, mockAppStore } from '../../../../utilities/test-utilities/mockData';
4+
import AccountEventsGraph from './AccountEventsGraph';
5+
6+
describe('Account Events Graph component', () => {
7+
beforeEach(() => {
8+
class ResizeObserverMock {
9+
private callback: ResizeObserverCallback;
10+
constructor(callback: ResizeObserverCallback) {
11+
this.callback = callback;
12+
}
13+
observe = (target: Element) => {
14+
// Simulate dimensions
15+
this.callback(
16+
[
17+
{
18+
target,
19+
contentRect: {
20+
width: 500,
21+
height: 400,
22+
top: 0,
23+
left: 0,
24+
bottom: 400,
25+
right: 500,
26+
x: 0,
27+
y: 0,
28+
toJSON: () => {},
29+
},
30+
} as ResizeObserverEntry,
31+
],
32+
this
33+
);
34+
};
35+
unobserve = jest.fn();
36+
disconnect = jest.fn();
37+
}
38+
global.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver;
39+
Object.defineProperty(HTMLElement.prototype, 'clientWidth', { configurable: true, value: 500 });
40+
Object.defineProperty(HTMLElement.prototype, 'clientHeight', { configurable: true, value: 400 });
41+
});
42+
43+
it('should render the graph container', async () => {
44+
await renderWithProviders(
45+
<AccountEventsGraph periods={mockBKPRAccountEvents.periods} />,
46+
{ preloadedState: mockAppStore, initialRoute: ['/bookkeeper/accountevents'] }
47+
);
48+
expect(screen.getByTestId('account-events-graph')).toBeInTheDocument();
49+
});
50+
51+
it('should render the correct number of bars based on account names', async () => {
52+
await renderWithProviders( <AccountEventsGraph periods={mockBKPRAccountEvents.periods} />,
53+
{ preloadedState: mockAppStore, initialRoute: ['/bookkeeper/accountevents'] }
54+
);
55+
const svg = screen.getByTestId('account-events-graph').querySelector('svg');
56+
const bars = svg?.querySelectorAll('rect');
57+
expect(bars?.length).toBeGreaterThan(0);
58+
});
59+
60+
it('should render XAxis and YAxis labels formatted correctly', async () => {
61+
await renderWithProviders(
62+
<AccountEventsGraph periods={mockBKPRAccountEvents.periods} />,
63+
{ preloadedState: mockAppStore, initialRoute: ['/bookkeeper/accountevents'] }
64+
);
65+
const rechartsWrapper = screen.getByTestId('account-events-graph');
66+
const svg = rechartsWrapper.querySelector('svg');
67+
const ticksGroup = svg?.querySelector('g.recharts-cartesian-axis-ticks');
68+
expect(ticksGroup).toBeInTheDocument();
69+
const tickTexts = ticksGroup?.querySelectorAll('text');
70+
expect(tickTexts?.length).toBeGreaterThan(0);
71+
const tickValues = Array.from(tickTexts || []).map(tick => tick.textContent);
72+
expect(tickValues).toContain(mockBKPRAccountEvents.periods[0].period_key);
73+
});
74+
75+
});
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { memo, useMemo } from 'react';
2+
import './AccountEventsGraph.scss';
3+
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer } from 'recharts';
4+
import { getBarColors, Units } from '../../../../utilities/constants';
5+
import { formatCurrency } from '../../../../utilities/data-formatters';
6+
import { transformAccountEventsGraphData } from '../../../../services/data-transform.service';
7+
import { AccountEventsPeriod } from '../../../../types/bookkeeper.type';
8+
import { useSelector } from 'react-redux';
9+
import { selectUIConfigUnit } from '../../../../store/rootSelectors';
10+
11+
const AccountEventsGraphTooltip = ({ active, payload, unit }: { active?: boolean; payload?: any[], unit?: Units }) => {
12+
if (active && payload && payload.length) {
13+
const total = payload.reduce((sum, entry) => sum + (entry.value || 0), 0);
14+
return (
15+
<div className='bkpr-tooltip p-3'>
16+
<p><strong>Period:</strong>{payload[0].payload.period_key}</p>
17+
<p><strong>Total:</strong>{formatCurrency(total, Units.MSATS, unit, false, 0, 'string')}</p>
18+
<hr />
19+
{payload.map((entry, index) => (
20+
<p className='ps-4' key={index} style={{ color: entry.color }}>
21+
{entry.name}: {formatCurrency(entry.value, Units.MSATS, unit, false, 0, 'string')}
22+
</p>
23+
))}
24+
</div>
25+
);
26+
}
27+
return null;
28+
};
29+
30+
const AccountEventsGraph = ({periods}: {periods: AccountEventsPeriod[]}) => {
31+
const uiConfigUnit = useSelector(selectUIConfigUnit);
32+
const { chartData, accountNames, barColors } = useMemo(() => {
33+
const data = transformAccountEventsGraphData(periods)
34+
const names = Array.from(new Set(periods.flatMap((period) => period.accounts.map((account) => account.short_channel_id))));
35+
return { chartData: data, accountNames: names, barColors: getBarColors(names.length) };
36+
}, [periods]);
37+
38+
return (
39+
<div data-testid='account-events-graph' className='account-events-graph'>
40+
<ResponsiveContainer width='100%'>
41+
<BarChart
42+
data={chartData}
43+
margin={{
44+
top: 20,
45+
right: 0,
46+
left: 30,
47+
bottom: 20,
48+
}}
49+
>
50+
<CartesianGrid strokeDasharray='3 3' />
51+
<XAxis dataKey='period_key' />
52+
<YAxis
53+
tickFormatter={(value) => {
54+
const formatted = formatCurrency(value, Units.MSATS, uiConfigUnit, false, 0, 'string');
55+
return typeof formatted === 'string' ? formatted : String(formatted);
56+
}}
57+
/>
58+
<Tooltip content={<AccountEventsGraphTooltip unit={uiConfigUnit} />} />
59+
<Legend />
60+
{accountNames.map((account, index) => (
61+
<Bar
62+
key={index}
63+
dataKey={account || ''}
64+
stackId='a'
65+
fill={barColors[index]}
66+
/>
67+
))}
68+
</BarChart>
69+
</ResponsiveContainer>
70+
</div>
71+
);
72+
};
73+
74+
export default memo(AccountEventsGraph);
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@import '../../../styles/constants.scss';
2+
3+
.account-events-container {
4+
height: 85vh;
5+
max-height: 85vh;
6+
}
7+
8+
.account-events-graph-container {
9+
height: 40vh;
10+
max-height: 40vh;
11+
}
12+
13+
.account-events-table-container {
14+
height: 25vh;
15+
max-height: 25vh;
16+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { screen } from '@testing-library/react';
2+
import { mockAppStore } from '../../../utilities/test-utilities/mockData';
3+
import { spyOnBKPRGetAccountEvents, spyOnBKPRGetSatsFlow, spyOnBKPRGetVolume } from '../../../utilities/test-utilities/mockService';
4+
import { renderWithProviders } from '../../../utilities/test-utilities/mockStore';
5+
import AccountEventsRoot from './AccountEventsRoot';
6+
7+
describe('Account Events component', () => {
8+
beforeEach(() => {
9+
global.ResizeObserver = jest.fn().mockImplementation(() => ({
10+
observe: jest.fn(),
11+
unobserve: jest.fn(),
12+
disconnect: jest.fn(),
13+
}));
14+
});
15+
16+
it('should be in the document', async () => {
17+
spyOnBKPRGetAccountEvents();
18+
spyOnBKPRGetSatsFlow();
19+
spyOnBKPRGetVolume();
20+
await renderWithProviders(<AccountEventsRoot />, { preloadedState: mockAppStore, initialRoute: ['/bookkeeper/accountevents'] });
21+
expect(screen.getByTestId('account-events-container')).not.toBeEmptyDOMElement();
22+
});
23+
});
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { useState, useEffect } from 'react';
2+
import './AccountEventsRoot.scss';
3+
import { Card, Row, Col } from 'react-bootstrap';
4+
import AccountEventsGraph from './AccountEventsGraph/AccountEventsGraph';
5+
import AccountEventsTable from './AccountEventsTable/AccountEventsTable';
6+
import { CloseSVG } from '../../../svgs/Close';
7+
import { useNavigate } from 'react-router-dom';
8+
import DataFilterOptions from '../../shared/DataFilterOptions/DataFilterOptions';
9+
import { filterZeroActivityAccountEvents } from '../../../services/data-transform.service';
10+
import { AccountEventsPeriod } from '../../../types/bookkeeper.type';
11+
import { useSelector } from 'react-redux';
12+
import { selectAccountEventPeriods } from '../../../store/bkprSelectors';
13+
14+
const AccountEventsRoot = () => {
15+
const navigate = useNavigate();
16+
const accEvntPeriods = useSelector(selectAccountEventPeriods);
17+
const [showZeroActivityPeriods, setShowZeroActivityPeriods] = useState<boolean>(false);
18+
const [accountEventsData, setAccountEventsData] = useState<AccountEventsPeriod[]>(accEvntPeriods);
19+
20+
const handleShowZeroActivityChange = (show: boolean) => {
21+
setShowZeroActivityPeriods(show);
22+
setAccountEventsData(filterZeroActivityAccountEvents((accEvntPeriods), show));
23+
};
24+
25+
useEffect(() => {
26+
setAccountEventsData(filterZeroActivityAccountEvents((accEvntPeriods), showZeroActivityPeriods));
27+
}, [accEvntPeriods, showZeroActivityPeriods]);
28+
29+
return (
30+
<div className='account-events-container' data-testid='account-events-container'>
31+
<Card className='p-3 pb-4'>
32+
<Card.Header className='fs-5 fw-bold text-dark'>
33+
<Row className='d-flex justify-content-between align-items-center'>
34+
<Col xs={9} className='fs-4 fw-bold'>Account Events</Col>
35+
<Col className='text-end'>
36+
<span
37+
className='span-close-svg'
38+
onClick={() => {
39+
navigate('..');
40+
}}
41+
>
42+
<CloseSVG />
43+
</span>
44+
</Col>
45+
</Row>
46+
<DataFilterOptions filter='accountevents' onShowZeroActivityChange={handleShowZeroActivityChange} />
47+
</Card.Header>
48+
<Card.Body className='pt-1 pb-3 d-flex flex-column align-items-center'>
49+
<Col xs={12} className='account-events-graph-container'>
50+
<AccountEventsGraph periods={accountEventsData} />
51+
</Col>
52+
<Col xs={12} className='account-events-table-container'>
53+
<AccountEventsTable periods={accountEventsData} />
54+
</Col>
55+
</Card.Body>
56+
</Card>
57+
</div>
58+
);
59+
};
60+
61+
export default AccountEventsRoot;

0 commit comments

Comments
 (0)