Skip to content

Commit 16c503d

Browse files
authored
Merge pull request #42 from lightninglabs/feat/csv-export
Implement CSV export for channels and swaps
2 parents 08bb168 + 51f76c7 commit 16c503d

File tree

16 files changed

+206
-6
lines changed

16 files changed

+206
-6
lines changed

app/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@
2121
"@emotion/core": "10.0.28",
2222
"@emotion/styled": "10.0.27",
2323
"@improbable-eng/grpc-web": "0.12.0",
24+
"@types/file-saver": "2.0.1",
2425
"@types/react-virtualized": "^9.21.9",
2526
"debug": "4.1.1",
2627
"emotion-theming": "10.0.27",
28+
"file-saver": "2.0.2",
2729
"i18next": "19.4.4",
2830
"i18next-browser-languagedetector": "4.1.1",
2931
"lottie-web": "5.6.8",
@@ -74,6 +76,7 @@
7476
]
7577
},
7678
"jest": {
79+
"globalSetup": "./src/setupTestsGlobal.ts",
7780
"collectCoverageFrom": [
7881
"src/**/*.{js,jsx,ts,tsx}",
7982
"!src/**/*.d.ts",

app/src/__mocks__/file-saver.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const saveAs = jest.fn();
2+
3+
export default { saveAs };

app/src/__tests__/components/history/HistoryPage.spec.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React from 'react';
22
import { fireEvent } from '@testing-library/react';
3+
import { saveAs } from 'file-saver';
34
import { renderWithProviders } from 'util/tests';
45
import { createStore, Store } from 'store';
56
import HistoryPage from 'components/history/HistoryPage';
@@ -50,4 +51,10 @@ describe('HistoryPage', () => {
5051
fireEvent.click(getByText('arrow-left.svg'));
5152
expect(store.uiStore.page).toEqual('loop');
5253
});
54+
55+
it('should export channels', () => {
56+
const { getByText } = render();
57+
fireEvent.click(getByText('download.svg'));
58+
expect(saveAs).toBeCalledWith(expect.any(Blob), 'swaps.csv');
59+
});
5360
});

app/src/__tests__/components/loop/LoopPage.spec.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React from 'react';
22
import { SwapStatus } from 'types/generated/loop_pb';
33
import { grpc } from '@improbable-eng/grpc-web';
44
import { fireEvent, waitFor } from '@testing-library/react';
5+
import { saveAs } from 'file-saver';
56
import { formatSats } from 'util/formatters';
67
import { renderWithProviders } from 'util/tests';
78
import { loopListSwaps } from 'util/tests/sampleData';
@@ -57,6 +58,17 @@ describe('LoopPage component', () => {
5758
expect(await findByText('525,000 sats')).toBeInTheDocument();
5859
});
5960

61+
it('should display the export icon', () => {
62+
const { getByText } = render();
63+
expect(getByText('download.svg')).toBeInTheDocument();
64+
});
65+
66+
it('should export channels', () => {
67+
const { getByText } = render();
68+
fireEvent.click(getByText('download.svg'));
69+
expect(saveAs).toBeCalledWith(expect.any(Blob), 'channels.csv');
70+
});
71+
6072
describe('Swap Process', () => {
6173
it('should display actions bar when Loop button is clicked', () => {
6274
const { getByText } = render();

app/src/__tests__/timezone.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { loopListSwaps } from 'util/tests/sampleData';
2+
import { Swap } from 'store/models';
3+
4+
/**
5+
* These test just ensure that the test runner is executing with
6+
* the system time zone set to UTC. This prevents tests from passing
7+
* on one machine and failing on another due to different time zones
8+
*
9+
* The `process.env.TZ` value is set to UTC in the jest global
10+
* config file setupTestsGlobal.ts
11+
*/
12+
describe('Timezone', () => {
13+
it('should always run unit tests in UTC', () => {
14+
expect(new Date().getTimezoneOffset()).toBe(0);
15+
});
16+
17+
it('should format the swap timestamps correctly', () => {
18+
const swap = new Swap(loopListSwaps.swapsList[0]);
19+
expect(swap.createdOnLabel).toEqual('4/8/2020 11:59:13 PM');
20+
expect(swap.updatedOnLabel).toEqual('4/9/2020 2:12:49 AM');
21+
});
22+
});

app/src/__tests__/util/csv.spec.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { saveAs } from 'file-saver';
2+
import CsvExporter from 'util/csv';
3+
import { loopListSwaps } from 'util/tests/sampleData';
4+
import { Swap } from 'store/models';
5+
6+
describe('csv Util', () => {
7+
const csv = new CsvExporter();
8+
const swaps = [new Swap(loopListSwaps.swapsList[0])];
9+
10+
it('should export using the .csv extension', () => {
11+
csv.export('swaps', Swap.csvColumns, swaps);
12+
expect(saveAs).toBeCalledWith(expect.any(Blob), 'swaps.csv');
13+
});
14+
15+
it('should convert swap data to the correct string', () => {
16+
const actual = csv.convert(Swap.csvColumns, swaps);
17+
const expected = [
18+
'"Swap ID","Type","Amount","Status","Created On","Updated On"',
19+
'"f4eb118383c2b09d8c7289ce21c25900cfb4545d46c47ed23a31ad2aa57ce830","Loop Out","500000","Failed","4/8/2020 11:59:13 PM","4/9/2020 2:12:49 AM"',
20+
].join('\n');
21+
expect(actual).toEqual(expected);
22+
});
23+
});

app/src/components/history/HistoryPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const Styled = {
1414

1515
const HistoryPage: React.FC = () => {
1616
const { l } = usePrefixedTranslation('cmps.history.HistoryPage');
17-
const { uiStore } = useStore();
17+
const { uiStore, swapStore } = useStore();
1818

1919
const { Wrapper } = Styled;
2020
return (
@@ -23,7 +23,7 @@ const HistoryPage: React.FC = () => {
2323
title={l('pageTitle')}
2424
backText={l('backText')}
2525
onBackClick={uiStore.goToLoop}
26-
onExportClick={() => alert('TODO: Export CSV of Swaps')}
26+
onExportClick={swapStore.exportSwaps}
2727
/>
2828
<HistoryList />
2929
</Wrapper>

app/src/components/loop/LoopPage.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const Styled = {
1818

1919
const LoopPage: React.FC = () => {
2020
const { l } = usePrefixedTranslation('cmps.loop.LoopPage');
21-
const { uiStore, buildSwapStore } = useStore();
21+
const { uiStore, buildSwapStore, channelStore } = useStore();
2222

2323
const { PageWrap } = Styled;
2424
return (
@@ -32,7 +32,7 @@ const LoopPage: React.FC = () => {
3232
<PageHeader
3333
title={l('pageTitle')}
3434
onHistoryClick={uiStore.goToHistory}
35-
onExportClick={() => alert('TODO: Export CSV of Channels')}
35+
onExportClick={channelStore.exportChannels}
3636
/>
3737
<LoopTiles />
3838
<LoopActions />

app/src/setupTestsGlobal.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export default () => {
2+
process.env.TZ = 'UTC';
3+
};

app/src/store/models/channel.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { action, computed, observable } from 'mobx';
22
import * as LND from 'types/generated/lnd_pb';
33
import { getBalanceStatus } from 'util/balances';
44
import { BalanceMode, BalanceModes } from 'util/constants';
5+
import { CsvColumns } from 'util/csv';
56
import { Store } from 'store/store';
67

78
export default class Channel {
@@ -80,4 +81,19 @@ export default class Channel {
8081
this.uptime = lndChannel.uptime;
8182
this.lifetime = lndChannel.lifetime;
8283
}
84+
85+
/**
86+
* Specifies which properties of this class should be exported to CSV
87+
* @param key must match the name of a property on this class
88+
* @param value the user-friendly name displayed in the CSV header
89+
*/
90+
static csvColumns: CsvColumns = {
91+
chanId: 'Channel ID',
92+
remotePubkey: 'Remote Pubkey',
93+
capacity: 'Capacity',
94+
localBalance: 'Local Balance',
95+
remoteBalance: 'Remote Balance',
96+
active: 'Active',
97+
uptimePercent: 'Uptime Percent',
98+
};
8399
}

0 commit comments

Comments
 (0)