Skip to content

Commit 0ce1b45

Browse files
committed
csv: implement CSV export of channels and swaps
1 parent 08bb168 commit 0ce1b45

File tree

10 files changed

+135
-6
lines changed

10 files changed

+135
-6
lines changed

app/package.json

Lines changed: 2 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",

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/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
}

app/src/store/models/swap.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { action, computed, observable } from 'mobx';
22
import { now } from 'mobx-utils';
33
import * as LOOP from 'types/generated/loop_pb';
4+
import { CsvColumns } from 'util/csv';
45
import { ellipseInside } from 'util/strings';
56

67
export default class Swap {
@@ -108,4 +109,18 @@ export default class Swap {
108109
this.lastUpdateTime = loopSwap.lastUpdateTime;
109110
this.state = loopSwap.state;
110111
}
112+
113+
/**
114+
* Specifies which properties of this class should be exported to CSV
115+
* @param key must match the name of a property on this class
116+
* @param value the user-friendly name displayed in the CSV header
117+
*/
118+
static csvColumns: CsvColumns = {
119+
id: 'Swap ID',
120+
typeName: 'Type',
121+
amount: 'Amount',
122+
stateLabel: 'Status',
123+
createdOnLabel: 'Created On',
124+
updatedOnLabel: 'Updated On',
125+
};
111126
}

app/src/store/store.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { observable } from 'mobx';
22
import { IS_DEV, IS_TEST } from 'config';
33
import AppStorage from 'util/appStorage';
4+
import CsvExporter from 'util/csv';
45
import { actionLog, Logger } from 'util/log';
56
import { GrpcClient, LndApi, LoopApi } from 'api';
67
import {
@@ -39,6 +40,9 @@ export class Store {
3940
/** the wrapper class around persistent storage */
4041
storage: AppStorage<PersistentSettings>;
4142

43+
/** the class to use for exporting lists of models to CSV */
44+
csv: CsvExporter;
45+
4246
// a flag to indicate when the store has completed all of its
4347
// API requests requested during initialization
4448
@observable initialized = false;
@@ -47,11 +51,13 @@ export class Store {
4751
lnd: LndApi,
4852
loop: LoopApi,
4953
storage: AppStorage<PersistentSettings>,
54+
csv: CsvExporter,
5055
log: Logger,
5156
) {
5257
this.api = { lnd, loop };
53-
this.log = log;
5458
this.storage = storage;
59+
this.csv = csv;
60+
this.log = log;
5561
}
5662

5763
/**
@@ -69,6 +75,7 @@ export class Store {
6975
/**
7076
* Creates an initialized Store instance with the dependencies injected
7177
* @param grpcClient an alternate GrpcClient to use instead of the default
78+
* @param appStorage an alternate AppStorage to use instead of the default
7279
*/
7380
export const createStore = (
7481
grpcClient?: GrpcClient,
@@ -78,8 +85,9 @@ export const createStore = (
7885
const storage = appStorage || new AppStorage();
7986
const lndApi = new LndApi(grpc);
8087
const loopApi = new LoopApi(grpc);
88+
const csv = new CsvExporter();
8189

82-
const store = new Store(lndApi, loopApi, storage, actionLog);
90+
const store = new Store(lndApi, loopApi, storage, csv, actionLog);
8391
// initialize the store immediately to fetch API data, except when running unit tests
8492
if (!IS_TEST) store.init();
8593

app/src/store/stores/channelStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,11 @@ export default class ChannelStore {
7474
this._store.log.info('updated channelStore.channels', toJS(this.channels));
7575
});
7676
}
77+
78+
/** exports the sorted list of channels to CSV file */
79+
@action.bound
80+
exportChannels() {
81+
this._store.log.info('exporting Channels to a CSV file');
82+
this._store.csv.export('channels', Channel.csvColumns, toJS(this.sortedChannels));
83+
}
7784
}

app/src/store/stores/swapStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,4 +123,11 @@ export default class SwapStore {
123123
this._store.log.info('polling was already stopped');
124124
}
125125
}
126+
127+
/** exports the sorted list of swaps to CSV file */
128+
@action.bound
129+
exportSwaps() {
130+
this._store.log.info('exporting Swaps to a CSV file');
131+
this._store.csv.export('swaps', Swap.csvColumns, toJS(this.sortedSwaps));
132+
}
126133
}

app/src/util/csv.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { saveAs } from 'file-saver';
2+
3+
const ROW_SEPARATOR = '\n';
4+
const COL_SEPARATOR = ',';
5+
6+
/**
7+
* A mapping of object property names to CSV file header names
8+
* @param key the property name used to pluck a value from each object
9+
* @param value the header text to display in the first row of this column
10+
*/
11+
export type CsvColumns = Record<string, string>;
12+
13+
export default class CsvExporter {
14+
/**
15+
* Exports data to a CSV file and prompts the user to download via the browser
16+
* @param fileName the file name without the `csv` extension
17+
* @param columns the columns containing keys to pluck off of each object
18+
* @param data an array of objects containing the data
19+
*/
20+
export(fileName: string, columns: CsvColumns, data: any[]) {
21+
const content = this.convert(columns, data);
22+
const blob = new Blob([content], { type: 'text/csv;charset=utf-8' });
23+
saveAs(blob, `${fileName}.csv`);
24+
}
25+
26+
/**
27+
* Converts and array of data objects into a CSV formatted string using
28+
* the columns mapping to specify which properties to include
29+
* @param columns the columns containing keys to pluck off of each object
30+
* @param data an array of objects containing the data
31+
*/
32+
convert(columns: CsvColumns, data: any[]) {
33+
// an array of rows in the CSV file
34+
const rows: string[] = [];
35+
36+
// add the header row
37+
rows.push(Object.values(columns).map(this.wrap).join(','));
38+
39+
// add each row of data
40+
data.forEach(record => {
41+
// convert each object of data into an array of the values
42+
const values = Object.keys(columns).reduce((cols, dataKey) => {
43+
const value = record[dataKey];
44+
cols.push(this.wrap(value ? value : ''));
45+
return cols;
46+
}, [] as string[]);
47+
48+
// convert the values to string and add to the content array
49+
rows.push(values.join(COL_SEPARATOR));
50+
});
51+
52+
// convert the rows into a string
53+
return rows.join(ROW_SEPARATOR);
54+
}
55+
56+
/**
57+
* Wraps a value in double quotes. If the value contains a
58+
* separator character, it would break the CSV structure
59+
* @param value the value to wrap
60+
*/
61+
wrap(value: string) {
62+
return `"${value}"`;
63+
}
64+
}

app/yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2226,6 +2226,11 @@
22262226
resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7"
22272227
integrity sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==
22282228

2229+
"@types/file-saver@2.0.1":
2230+
version "2.0.1"
2231+
resolved "https://registry.yarnpkg.com/@types/file-saver/-/file-saver-2.0.1.tgz#e18eb8b069e442f7b956d313f4fadd3ef887354e"
2232+
integrity sha512-g1QUuhYVVAamfCifK7oB7G3aIl4BbOyzDOqVyUfEr4tfBKrXfeH+M+Tg7HKCXSrbzxYdhyCP7z9WbKo0R2hBCw==
2233+
22292234
"@types/glob@^7.1.1":
22302235
version "7.1.1"
22312236
resolved "https://registry.yarnpkg.com/@types/glob/-/glob-7.1.1.tgz#aa59a1c6e3fbc421e07ccd31a944c30eba521575"
@@ -6459,6 +6464,11 @@ file-loader@4.3.0, file-loader@^4.2.0:
64596464
loader-utils "^1.2.3"
64606465
schema-utils "^2.5.0"
64616466

6467+
file-saver@2.0.2:
6468+
version "2.0.2"
6469+
resolved "https://registry.yarnpkg.com/file-saver/-/file-saver-2.0.2.tgz#06d6e728a9ea2df2cce2f8d9e84dfcdc338ec17a"
6470+
integrity sha512-Wz3c3XQ5xroCxd1G8b7yL0Ehkf0TC9oYC6buPFkNnU9EnaPlifeAFCyCh+iewXTyFRcg0a6j3J7FmJsIhlhBdw==
6471+
64626472
file-system-cache@^1.0.5:
64636473
version "1.0.5"
64646474
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"

0 commit comments

Comments
 (0)