From 8334bd274f94d21d78bd91953e3f660ff545af36 Mon Sep 17 00:00:00 2001 From: Rafael Taranto Date: Thu, 12 Dec 2019 13:55:52 +0000 Subject: [PATCH] feat: transactions page (#342) * feat: transactions page * fix: remove unused txHash function * refactor: rewrite transactions sql queries * fix: use left instead of inner join on txs * fix: change expandable table logic * fix: add other coins * refactor: move log download function to component * refactor: use name values in RadioGroup * fix: assorted fixes * feat: virtualize expandable table * fix: clean up imports * fix: remove border radius * fix: move formatting out of CopyToClipboard And use CSS instead of JS to format. * fix: remove customer's last name formatting This was using lodash's string case functions, which produce unwanted results if, for instance, a user has a double-barrel last name. --- lib/new-admin/admin-server.js | 9 +- lib/new-admin/transactions.js | 110 ++++++++++ new-lamassu-admin/package-lock.json | 48 +++-- new-lamassu-admin/package.json | 4 +- ...aderPopover.js => LogsDownloaderPopper.js} | 126 ++++++----- new-lamassu-admin/src/components/Popover.js | 46 ---- new-lamassu-admin/src/components/Popper.js | 107 ++++++++++ .../src/components/buttons/FeatureButton.js | 5 +- .../src/components/buttons/IDButton.js | 113 ++++++++++ .../src/components/buttons/index.js | 3 +- .../components/expandable-table/ExpTable.js | 137 +++++++----- .../src/components/inputs/base/RadioGroup.js | 10 +- .../components/inputs/base/Select.styles.js | 2 +- .../src/components/table/TableCell.js | 6 +- .../src/components/table/TableHeader.js | 12 +- new-lamassu-admin/src/pages/ServerLogs.js | 16 +- .../src/pages/Transactions/CopyToClipboard.js | 68 ++++++ .../src/pages/Transactions/DetailsCard.js | 199 ++++++++++++++++++ .../src/pages/Transactions/Transactions.js | 162 ++++++++++++++ .../pages/Transactions/Transactions.styles.js | 160 ++++++++++++++ .../src/pages/Transactions/tx.js | 54 +++++ new-lamassu-admin/src/routing/routes.js | 2 + .../src/styling/icons/ID/card/comet.svg | 2 +- .../src/styling/icons/ID/card/tomato.svg | 2 +- .../src/styling/icons/ID/card/white.svg | 2 +- .../src/styling/icons/ID/card/zodiac.svg | 2 +- .../src/styling/icons/ID/phone/comet.svg | 2 +- .../src/styling/icons/ID/phone/tomato.svg | 2 +- .../src/styling/icons/ID/phone/white.svg | 2 +- .../src/styling/icons/ID/phone/zodiac.svg | 2 +- .../styling/icons/action/expand/closed.svg | 12 +- .../src/styling/icons/action/expand/open.svg | 12 +- .../styling/icons/button/authorize/white.svg | 2 +- .../styling/icons/button/authorize/zodiac.svg | 2 +- .../src/styling/icons/button/cancel/white.svg | 2 +- .../styling/icons/button/cancel/zodiac.svg | 2 +- .../src/styling/icons/button/retry/white.svg | 2 +- .../src/styling/icons/button/retry/zodiac.svg | 2 +- 38 files changed, 1225 insertions(+), 226 deletions(-) create mode 100644 lib/new-admin/transactions.js rename new-lamassu-admin/src/components/{LogsDownloaderPopover.js => LogsDownloaderPopper.js} (58%) delete mode 100644 new-lamassu-admin/src/components/Popover.js create mode 100644 new-lamassu-admin/src/components/Popper.js create mode 100644 new-lamassu-admin/src/components/buttons/IDButton.js create mode 100644 new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js create mode 100644 new-lamassu-admin/src/pages/Transactions/DetailsCard.js create mode 100644 new-lamassu-admin/src/pages/Transactions/Transactions.js create mode 100644 new-lamassu-admin/src/pages/Transactions/Transactions.styles.js create mode 100644 new-lamassu-admin/src/pages/Transactions/tx.js diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index beb5aa50..a65de16a 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -7,8 +7,9 @@ const got = require('got') const supportLogs = require('../support_logs') const machineLoader = require('../machine-loader') const logs = require('../logs') -const serverLogs = require('./server-logs') +const transactions = require('./transactions') +const serverLogs = require('./server-logs') const supervisor = require('./supervisor') const funding = require('./funding') const config = require('./config') @@ -80,6 +81,12 @@ app.get('/api/server_logs', (req, res, next) => { .catch(next) }) +app.get('/api/txs', (req, res, next) => { + return transactions.batch() + .then(r => res.send(r)) + .catch(next) +}) + function dbNotify () { return got.post('http://localhost:3030/dbChange') .catch(e => console.error('Error: lamassu-server not responding')) diff --git a/lib/new-admin/transactions.js b/lib/new-admin/transactions.js new file mode 100644 index 00000000..2ad38e12 --- /dev/null +++ b/lib/new-admin/transactions.js @@ -0,0 +1,110 @@ +const _ = require('lodash/fp') + +const db = require('../db') +const machineLoader = require('../machine-loader') +const tx = require('../tx') +const cashInTx = require('../cash-in/cash-in-tx') +const { REDEEMABLE_AGE } = require('../cash-out/cash-out-helper') + +const NUM_RESULTS = 1000 + +function addNames (txs) { + return machineLoader.getMachineNames() + .then(machines => { + const addName = tx => { + const machine = _.find(['deviceId', tx.deviceId], machines) + const name = machine ? machine.name : 'Unpaired' + return _.set('machineName', name, tx) + } + + return _.map(addName, txs) + }) +} + +const camelize = _.mapKeys(_.camelCase) + +function batch () { + const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), + _.take(NUM_RESULTS), _.map(camelize), addNames) + + const cashInSql = `select 'cashIn' as tx_class, txs.*, + c.phone as customer_phone, + c.id_card_data_number as customer_id_card_data_number, + c.id_card_data_expiration as customer_id_card_data_expiration, + c.id_card_data as customer_id_card_data, + c.name as customer_name, + c.front_camera_path as customer_front_camera_path, + c.id_card_photo_path as customer_id_card_photo_path, + ((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired + from cash_in_txs as txs + left outer join customers c on txs.customer_id = c.id + order by created desc limit $2` + + const cashOutSql = `select 'cashOut' as tx_class, + txs.*, + actions.tx_hash, + c.phone as customer_phone, + c.id_card_data_number as customer_id_card_data_number, + c.id_card_data_expiration as customer_id_card_data_expiration, + c.id_card_data as customer_id_card_data, + c.name as customer_name, + c.front_camera_path as customer_front_camera_path, + c.id_card_photo_path as customer_id_card_photo_path, + (extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $2 as expired + from cash_out_txs txs + inner join cash_out_actions actions on txs.id = actions.tx_id + and actions.action = 'provisionAddress' + left outer join customers c on txs.customer_id = c.id + order by created desc limit $1` + + return Promise.all([db.any(cashInSql, [cashInTx.PENDING_INTERVAL, NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS, REDEEMABLE_AGE])]) + .then(packager) +} + +function single (txId) { + const packager = _.flow(_.compact, _.map(camelize), addNames) + + const cashInSql = `select 'cashIn' as tx_class, txs.*, + c.phone as customer_phone, + c.id_card_data_number as customer_id_card_data_number, + c.id_card_data_expiration as customer_id_card_data_expiration, + c.id_card_data as customer_id_card_data, + c.name as customer_name, + c.front_camera_path as customer_front_camera_path, + c.id_card_photo_path as customer_id_card_photo_path, + ((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired + from cash_in_txs as txs + left outer join customers c on txs.customer_id = c.id + where id=$2` + + const cashOutSql = `select 'cashOut' as tx_class, + txs.*, + actions.tx_hash, + c.phone as customer_phone, + c.id_card_data_number as customer_id_card_data_number, + c.id_card_data_expiration as customer_id_card_data_expiration, + c.id_card_data as customer_id_card_data, + c.name as customer_name, + c.front_camera_path as customer_front_camera_path, + c.id_card_photo_path as customer_id_card_photo_path, + (extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $2 as expired + from cash_out_txs txs + inner join cash_out_actions actions on txs.id = actions.tx_id + and actions.action = 'provisionAddress' + left outer join customers c on txs.customer_id = c.id + where id=$1` + + return Promise.all([ + db.oneOrNone(cashInSql, [cashInTx.PENDING_INTERVAL, txId]), + db.oneOrNone(cashOutSql, [txId, REDEEMABLE_AGE]) + ]) + .then(packager) + .then(_.head) +} + +function cancel (txId) { + return tx.cancel(txId) + .then(() => single(txId)) +} + +module.exports = { batch, single, cancel } diff --git a/new-lamassu-admin/package-lock.json b/new-lamassu-admin/package-lock.json index 9807e7b0..7cc3fb8d 100644 --- a/new-lamassu-admin/package-lock.json +++ b/new-lamassu-admin/package-lock.json @@ -5762,7 +5762,6 @@ "version": "6.26.0", "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", - "dev": true, "requires": { "core-js": "^2.4.0", "regenerator-runtime": "^0.11.0" @@ -7533,7 +7532,6 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz", "integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==", - "dev": true, "requires": { "toggle-selection": "^1.0.6" } @@ -7541,8 +7539,7 @@ "core-js": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz", - "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==", - "dev": true + "integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==" }, "core-js-compat": { "version": "3.1.4", @@ -11018,9 +11015,9 @@ "dev": true }, "handlebars": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz", - "integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz", + "integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -17276,9 +17273,9 @@ } }, "react": { - "version": "16.10.2", - "resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz", - "integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==", + "version": "16.12.0", + "resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz", + "integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==", "requires": { "loose-envify": "^1.1.0", "object-assign": "^4.1.1", @@ -17336,6 +17333,15 @@ "tinycolor2": "^1.4.1" } }, + "react-copy-to-clipboard": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.2.tgz", + "integrity": "sha512-/2t5mLMMPuN5GmdXo6TebFa8IoFxZ+KTDDqYhcDm0PhkgEzSxVvIX26G20s1EB02A4h2UZgwtfymZ3lGJm0OLg==", + "requires": { + "copy-to-clipboard": "^3", + "prop-types": "^15.5.8" + } + }, "react-dev-utils": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz", @@ -17672,8 +17678,7 @@ "react-lifecycles-compat": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", - "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==", - "dev": true + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, "react-popper": { "version": "1.3.4", @@ -19128,6 +19133,19 @@ } } }, + "react-virtualized": { + "version": "9.21.2", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz", + "integrity": "sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==", + "requires": { + "babel-runtime": "^6.26.0", + "clsx": "^1.0.1", + "dom-helpers": "^5.0.0", + "loose-envify": "^1.3.0", + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" + } + }, "reactcss": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz", @@ -19556,8 +19574,7 @@ "regenerator-runtime": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", - "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==", - "dev": true + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" }, "regenerator-transform": { "version": "0.14.1", @@ -21866,8 +21883,7 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=", - "dev": true + "integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=" }, "toidentifier": { "version": "1.0.0", diff --git a/new-lamassu-admin/package.json b/new-lamassu-admin/package.json index 813a6ba2..30b45a8c 100644 --- a/new-lamassu-admin/package.json +++ b/new-lamassu-admin/package.json @@ -16,9 +16,11 @@ "lodash": "4.17.15", "moment": "2.24.0", "qrcode.react": "0.9.3", - "react": "^16.10.2", + "react": "^16.12.0", + "react-copy-to-clipboard": "^5.0.2", "react-dom": "^16.10.2", "react-router-dom": "5.1.2", + "react-virtualized": "^9.21.2", "yup": "0.27.0" }, "devDependencies": { diff --git a/new-lamassu-admin/src/components/LogsDownloaderPopover.js b/new-lamassu-admin/src/components/LogsDownloaderPopper.js similarity index 58% rename from new-lamassu-admin/src/components/LogsDownloaderPopover.js rename to new-lamassu-admin/src/components/LogsDownloaderPopper.js index 10ca75dc..f60cc6d5 100644 --- a/new-lamassu-admin/src/components/LogsDownloaderPopover.js +++ b/new-lamassu-admin/src/components/LogsDownloaderPopper.js @@ -1,17 +1,17 @@ import React, { useState } from 'react' -import FileSaver from 'file-saver' import classnames from 'classnames' -import { toInteger } from 'lodash/fp' +import { get, compose } from 'lodash/fp' import moment from 'moment' +import FileSaver from 'file-saver' import { makeStyles } from '@material-ui/core' import { ReactComponent as Arrow } from '../styling/icons/arrow/download_logs.svg' -import typographyStyles from '../components/typography/styles' +import typographyStyles from './typography/styles' import { primaryColor, offColor, zircon } from '../styling/variables' import { Link } from './buttons' import { RadioGroup } from './inputs' -import Popover from './Popover' +import Popper from './Popper' import DateRangePicker from './date-range-picker/DateRangePicker' const { info1, label1, label2, h4 } = typographyStyles @@ -121,99 +121,109 @@ const styles = { const useStyles = makeStyles(styles) -const LogsDownloaderPopover = ({ id, open, anchorEl, onClose, logsResponse, ...props }) => { - const [radioButtons, setRadioButtons] = useState(0) +const LogsDownloaderPopover = ({ id, name, open, anchorEl, getTimestamp, logs, title, ...props }) => { + const radioButtonAll = 'all' + const radioButtonRange = 'range' + + const [selectedRadio, setSelectedRadio] = useState(radioButtonAll) const [range, setRange] = useState(null) const classes = useStyles() const dateRangePickerClasses = { - [classes.dateRangePickerShowing]: radioButtons === 1, - [classes.dateRangePickerHidden]: radioButtons === 0 - } - - const formatDateFile = date => { - return moment(date).format('YYYY-MM-DD_HH-mm') + [classes.dateRangePickerShowing]: selectedRadio === radioButtonRange, + [classes.dateRangePickerHidden]: selectedRadio === radioButtonAll } const handleRadioButtons = (event) => { - setRadioButtons(toInteger(event.target.value)) + compose(setSelectedRadio, get('target.value')) + const radio = event.target.value + setSelectedRadio(radio) } const handleRangeChange = (from, to) => { setRange({ from, to }) } + const downloadLogs = (range, logs) => { + if (!range) return + + if (range.from && !range.to) range.to = moment() + + const formatDateFile = date => { + return moment(date).format('YYYY-MM-DD_HH-mm') + } + + if (selectedRadio === radioButtonAll) { + const text = logs.map(it => JSON.stringify(it)).join('\n') + const blob = new window.Blob([text], { + type: 'text/plain;charset=utf-8' + }) + FileSaver.saveAs(blob, `${formatDateFile(new Date())}_${name}`) + return + } + + if (selectedRadio === radioButtonRange) { + const text = logs.filter((log) => moment(getTimestamp(log)).isBetween(range.from, range.to, 'day', '[]')).map(it => JSON.stringify(it)).join('\n') + const blob = new window.Blob([text], { + type: 'text/plain;charset=utf-8' + }) + FileSaver.saveAs(blob, `${formatDateFile(range.from)}_${formatDateFile(range.to)}_${name}`) + } + } + + const radioButtonOptions = [{ label: 'All logs', value: radioButtonAll }, { label: 'Date range', value: radioButtonRange }] + return ( -
- Download logs + {title}
-
-
- {range && ( - <> - From -
- -
- To - - )} + {selectedRadio === radioButtonRange && ( +
+
+ {range && ( + <> + From +
+ +
+ To + + )} +
+
- -
+ )}
{ - if (radioButtons === 0) { - const text = logsResponse.data.logs.map(it => JSON.stringify(it)).join('\n') - const blob = new window.Blob([text], { - type: 'text/plain;charset=utf-8' - }) - FileSaver.saveAs(blob, `${formatDateFile(new Date())}_server`) - } else if (radioButtons === 1 && range.from && range.to) { - const text = logsResponse.data.logs.filter((log) => moment(log.timestamp).isBetween(range.from, range.to, 'day', '[]')).map(it => JSON.stringify(it)).join('\n') - const blob = new window.Blob([text], { - type: 'text/plain;charset=utf-8' - }) - FileSaver.saveAs(blob, `${formatDateFile(range.from)}_${formatDateFile(range.to)}_server`) - } - }} + onClick={() => downloadLogs(range, logs)} > Download
- + ) } diff --git a/new-lamassu-admin/src/components/Popover.js b/new-lamassu-admin/src/components/Popover.js deleted file mode 100644 index 85607b8c..00000000 --- a/new-lamassu-admin/src/components/Popover.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react' -import { makeStyles, Popover as MaterialPopover } from '@material-ui/core' - -const arrowHeight = 10 - -const styles = { - arrow: { - width: 0, - height: 0, - position: 'absolute', - borderStyle: 'solid', - margin: 5, - borderWidth: [[0, 15, arrowHeight, 15]], - borderLeftColor: 'transparent', - borderRightColor: 'transparent', - borderTopColor: 'transparent', - top: arrowHeight * -1, - left: 116, - marginTop: 0, - marginBottom: 0, - borderColor: '#ffffff' - }, - paper: { - overflow: 'visible' - } -} - -const useStyles = makeStyles(styles) - -const Popover = ({ children, ...props }) => { - const classes = useStyles() - - return ( - - {children} -
- - ) -} - -export default Popover diff --git a/new-lamassu-admin/src/components/Popper.js b/new-lamassu-admin/src/components/Popper.js new file mode 100644 index 00000000..9c7b8556 --- /dev/null +++ b/new-lamassu-admin/src/components/Popper.js @@ -0,0 +1,107 @@ +import React, { useState } from 'react' +import classnames from 'classnames' +import { merge } from 'lodash/fp' +import { makeStyles, Popper as MaterialPopper, Paper } from '@material-ui/core' + +import { white } from '../styling/variables' + +const Popover = ({ children, bgColor = white, arrowSize = 7, ...props }) => { + const [arrowRef, setArrowRef] = useState(null) + + const styles = { + popover: { + zIndex: 1000, + backgroundColor: bgColor, + borderRadius: 4 + }, + arrow: { + position: 'absolute', + fontSize: arrowSize, + width: '3em', + height: '3em' + }, + arrowBottom: { + top: 0, + width: 0, + height: 0, + borderLeft: [['2em', 'solid', 'transparent']], + borderRight: [['2em', 'solid', 'transparent']], + borderBottom: [['2em', 'solid', bgColor]], + marginTop: '-1.9em' + }, + arrowTop: { + bottom: 0, + width: 0, + height: 0, + borderLeft: [['2em', 'solid', 'transparent']], + borderRight: [['2em', 'solid', 'transparent']], + borderTop: [['2em', 'solid', bgColor]], + marginBottom: '-1.9em' + }, + arrowRight: { + left: 0, + width: 0, + height: 0, + borderTop: [['2em', 'solid', 'transparent']], + borderBottom: [['2em', 'solid', 'transparent']], + borderRight: [['2em', 'solid', bgColor]], + marginLeft: '-1.9em' + }, + arrowLeft: { + right: 0, + width: 0, + height: 0, + borderTop: [['2em', 'solid', 'transparent']], + borderBottom: [['2em', 'solid', 'transparent']], + borderLeft: [['2em', 'solid', bgColor]], + marginRight: '-1.9em' + }, + root: { + backgroundColor: bgColor + } + } + + const useStyles = makeStyles(styles) + + const classes = useStyles() + + const arrowClasses = { + [classes.arrow]: true, + [classes.arrowBottom]: props.placement === 'bottom', + [classes.arrowTop]: props.placement === 'top', + [classes.arrowRight]: props.placement === 'right', + [classes.arrowLeft]: props.placement === 'left' + } + + const modifiers = merge(props.modifiers, { + flip: { + enabled: false + }, + preventOverflow: { + enabled: true, + boundariesElement: 'scrollParent' + }, + arrow: { + enabled: true, + element: arrowRef + } + }) + + return ( + <> + + + + {children} + + + + ) +} + +export default Popover diff --git a/new-lamassu-admin/src/components/buttons/FeatureButton.js b/new-lamassu-admin/src/components/buttons/FeatureButton.js index 3b03b16a..65bd9025 100644 --- a/new-lamassu-admin/src/components/buttons/FeatureButton.js +++ b/new-lamassu-admin/src/components/buttons/FeatureButton.js @@ -15,7 +15,10 @@ const styles = { }, primary, buttonIcon: { - margin: 'auto' + margin: 'auto', + '& svg': { + overflow: 'visible' + } }, buttonIconActive: {} // required to extend primary } diff --git a/new-lamassu-admin/src/components/buttons/IDButton.js b/new-lamassu-admin/src/components/buttons/IDButton.js new file mode 100644 index 00000000..21b6c3c9 --- /dev/null +++ b/new-lamassu-admin/src/components/buttons/IDButton.js @@ -0,0 +1,113 @@ +import React, { useState, memo } from 'react' +import classnames from 'classnames' +import { makeStyles } from '@material-ui/core/styles' + +import Popover from '../Popper' +import { subheaderColor, subheaderDarkColor, offColor } from '../../styling/variables' +import typographyStyles from '../typography/styles' + +const { info2 } = typographyStyles + +const colors = (color1, color2, color3) => { + return { + backgroundColor: color1, + '&:hover': { + backgroundColor: color2 + }, + '&:active': { + backgroundColor: color3 + } + } +} + +const styles = { + idButton: { + width: 34, + height: 28, + display: 'flex', + borderRadius: 4, + padding: 0, + border: 'none', + cursor: 'pointer' + }, + buttonIcon: { + margin: 'auto', + lineHeight: 1, + '& svg': { + overflow: 'visible' + } + }, + closed: { + extend: colors(subheaderColor, subheaderDarkColor, offColor) + }, + open: { + extend: colors(offColor, offColor, offColor) + }, + popoverContent: { + extend: info2, + padding: 8, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + borderRadius: 4, + '& img': { + maxHeight: 145 + } + } +} + +const useStyles = makeStyles(styles) + +const IDButton = memo(({ name, className, Icon, InverseIcon, popoverWidth = 152, children, ...props }) => { + const [anchorEl, setAnchorEl] = useState(null) + + const classes = useStyles() + + const open = Boolean(anchorEl) + const id = open ? `simple-popper-${name}` : undefined + + const classNames = { + [classes.idButton]: true, + [classes.primary]: true, + [classes.open]: open, + [classes.closed]: !open + } + + const iconClassNames = { + [classes.buttonIcon]: true + } + + const handleClick = event => { + setAnchorEl(anchorEl ? null : event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + return ( + <> + + +
+
{children}
+
+
+ + ) +}) + +export default IDButton diff --git a/new-lamassu-admin/src/components/buttons/index.js b/new-lamassu-admin/src/components/buttons/index.js index 7d446cb0..3598effc 100644 --- a/new-lamassu-admin/src/components/buttons/index.js +++ b/new-lamassu-admin/src/components/buttons/index.js @@ -3,5 +3,6 @@ import Link from './Link' import SimpleButton from './SimpleButton' import ActionButton from './ActionButton' import FeatureButton from './FeatureButton' +import IDButton from './IDButton' -export { Button, Link, SimpleButton, ActionButton, FeatureButton } +export { Button, Link, SimpleButton, ActionButton, FeatureButton, IDButton } diff --git a/new-lamassu-admin/src/components/expandable-table/ExpTable.js b/new-lamassu-admin/src/components/expandable-table/ExpTable.js index 81987adb..4b475e5f 100644 --- a/new-lamassu-admin/src/components/expandable-table/ExpTable.js +++ b/new-lamassu-admin/src/components/expandable-table/ExpTable.js @@ -1,99 +1,122 @@ -import React, { useState, useEffect } from 'react' -import { map, set } from 'lodash/fp' +import React, { useState } from 'react' import classnames from 'classnames' -import uuidv1 from 'uuid/v1' +import { AutoSizer, List, CellMeasurer, CellMeasurerCache } from 'react-virtualized' import { makeStyles } from '@material-ui/core/styles' -import { Table, THead, Tr, TBody, Td, Th } from '../fake-table/Table' +import { THead, Tr, Td, Th } from '../fake-table/Table' import { ReactComponent as ExpandClosedIcon } from '../../styling/icons/action/expand/closed.svg' import { ReactComponent as ExpandOpenIcon } from '../../styling/icons/action/expand/open.svg' import { mainWidth } from '../../styling/variables' const styles = { - hideDetailsRow: { - display: 'none' - }, expandButton: { border: 'none', backgroundColor: 'transparent', cursor: 'pointer', padding: 4 + }, + row: { + borderRadius: 0 } } const useStyles = makeStyles(styles) -const ExpRow = ({ id, columns, details, sizes, expanded, className, expandRow, ...props }) => { +const ExpRow = ({ id, columns, details, expanded, className, expandRow, ...props }) => { const classes = useStyles() - const detailsRowClasses = { - [classes.detailsRow]: true, - [classes.hideDetailsRow]: expanded - } - return ( <> - - {columns.map((col, idx) => ( - {col.value} + + {columns.slice(0, -1).map((col, idx) => ( + {col.value} ))} - + - - - - {details} + + {expanded && ( + + + {details} + + + )} ) } -/* headers = [{ value, className, textAlign }] - * rows = [{ columns = [{ value, className, textAlign }], details, className, error, errorMessage }] +/* rows = [{ columns = [{ name, value, className, textAlign, size }], details, className, error, errorMessage }] + * Don't forget to include the size of the last (expand button) column! */ -const ExpTable = ({ headers = [], rows = [], sizes = [], className, ...props }) => { - const [rowStates, setRowStates] = useState(null) - - useEffect(() => { - setRowStates(rows && rows.map((x) => { return { id: x.id, expanded: false } })) - }, [rows]) +const ExpTable = ({ rows = [], className, ...props }) => { + const [expanded, setExpanded] = useState(null) const expandRow = (id) => { - setRowStates(map(r => set('expanded', r.id === id ? !r.expanded : false, r))) + setExpanded(id === expanded ? null : id) + } + + if (!rows) return null + + const cache = new CellMeasurerCache({ + defaultHeight: 62, + fixedWidth: true + }) + + function rowRenderer ({ index, isScrolling, key, parent, style }) { + return ( + +
+ +
+
+ ) } return ( - - - {headers.map((header, idx) => ( - - ))} - - - {rowStates && rowStates.map((r, idx) => { - const row = rows[idx] - - return ( - +
+
+ {rows[0].columns.map((c, idx) => ( + + ))} + + +
+ + {({ height }) => ( + - ) - })} - -
{header.value}
{c.name}
+ )} + +
+ ) } diff --git a/new-lamassu-admin/src/components/inputs/base/RadioGroup.js b/new-lamassu-admin/src/components/inputs/base/RadioGroup.js index 34c692aa..17b7ca51 100644 --- a/new-lamassu-admin/src/components/inputs/base/RadioGroup.js +++ b/new-lamassu-admin/src/components/inputs/base/RadioGroup.js @@ -28,13 +28,15 @@ const Label = withStyles({ } })(props => ) -const RadioGroup = ({ name, value, labels, ariaLabel, onChange, className, ...props }) => { +/* options = [{ label, value }] + */ +const RadioGroup = ({ name, value, options, ariaLabel, onChange, className, ...props }) => { return ( <> - {labels && ( + {options && ( - {labels.map((label, idx) => ( - )} diff --git a/new-lamassu-admin/src/components/inputs/base/Select.styles.js b/new-lamassu-admin/src/components/inputs/base/Select.styles.js index 2d16783a..bc9c8dde 100644 --- a/new-lamassu-admin/src/components/inputs/base/Select.styles.js +++ b/new-lamassu-admin/src/components/inputs/base/Select.styles.js @@ -14,7 +14,7 @@ export default { }, select: { width: WIDTH, - zIndex: 1000, + zIndex: 2, '& label': { extend: label1, color: offColor, diff --git a/new-lamassu-admin/src/components/table/TableCell.js b/new-lamassu-admin/src/components/table/TableCell.js index d8f627d7..256c430e 100644 --- a/new-lamassu-admin/src/components/table/TableCell.js +++ b/new-lamassu-admin/src/components/table/TableCell.js @@ -6,14 +6,14 @@ import { spacer } from '../../styling/variables' const useStyles = makeStyles({ td: { - padding: `0 ${spacer * 3}px` + padding: [[0, spacer * 3]] }, alignRight: { textAlign: 'right' } }) -const TableCell = memo(({ rightAlign, className, children, ...props }) => { +const TableCell = memo(({ colspan, rightAlign, className, children, ...props }) => { const classes = useStyles() const styles = { [classes.td]: true, @@ -21,7 +21,7 @@ const TableCell = memo(({ rightAlign, className, children, ...props }) => { } return ( - + {children} ) diff --git a/new-lamassu-admin/src/components/table/TableHeader.js b/new-lamassu-admin/src/components/table/TableHeader.js index 425b7fd4..08fec13c 100644 --- a/new-lamassu-admin/src/components/table/TableHeader.js +++ b/new-lamassu-admin/src/components/table/TableHeader.js @@ -15,13 +15,21 @@ const useStyles = makeStyles({ textAlign: 'left', color: white, padding: `0 ${spacer * 3}px` + }, + alignRight: { + textAlign: 'right' } }) -const TableHeaderCell = memo(({ children, className, ...props }) => { +const TableHeaderCell = memo(({ rightAlign, children, className, ...props }) => { const classes = useStyles() + const styles = { + [classes.th]: true, + [classes.alignRight]: rightAlign + } + return ( - + {children} ) diff --git a/new-lamassu-admin/src/pages/ServerLogs.js b/new-lamassu-admin/src/pages/ServerLogs.js index c8fcfcea..2363b7ce 100644 --- a/new-lamassu-admin/src/pages/ServerLogs.js +++ b/new-lamassu-admin/src/pages/ServerLogs.js @@ -1,5 +1,5 @@ import React, { useState } from 'react' -import { concat, uniq, merge } from 'lodash/fp' +import { concat, uniq, merge, find } from 'lodash/fp' import moment from 'moment' import useAxios from '@use-hooks/axios' import { makeStyles } from '@material-ui/core' @@ -10,7 +10,7 @@ import { FeatureButton, SimpleButton } from '../components/buttons' import { Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '../components/table' import { Select } from '../components/inputs' import Uptime from '../components/Uptime' -import LogsDowloaderPopover from '../components/LogsDownloaderPopover' +import LogsDowloaderPopover from '../components/LogsDownloaderPopper' import { ReactComponent as Download } from '../styling/icons/button/download/zodiac.svg' import { ReactComponent as DownloadActive } from '../styling/icons/button/download/white.svg' import { offColor } from '../styling/variables' @@ -108,11 +108,7 @@ const Logs = () => { }) const handleOpenRangePicker = (event) => { - setAnchorEl(event.currentTarget) - } - - const handleCloseRangePicker = () => { - setAnchorEl(null) + setAnchorEl(anchorEl ? null : event.currentTarget) } const open = Boolean(anchorEl) @@ -133,11 +129,13 @@ const Logs = () => { onClick={handleOpenRangePicker} /> log.timestamp} /> Share with Lamassu diff --git a/new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js b/new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js new file mode 100644 index 00000000..80416f97 --- /dev/null +++ b/new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js @@ -0,0 +1,68 @@ +import React, { useState, useEffect } from 'react' +import classnames from 'classnames' +import { CopyToClipboard as ReactCopyToClipboard } from 'react-copy-to-clipboard' +import { replace } from 'lodash/fp' +import { makeStyles } from '@material-ui/core/styles' + +import { cpcStyles } from './Transactions.styles' + +import Popover from '../../components/Popper' +import { ReactComponent as CopyIcon } from '../../styling/icons/action/copy/copy.svg' +import { comet } from '../../styling/variables' + +const CopyToClipboard = ({ className, children, ...props }) => { + const [anchorEl, setAnchorEl] = useState(null) + + useEffect(() => { + if (anchorEl) setTimeout(() => setAnchorEl(null), 3000) + }, [anchorEl]) + + const useStyles = makeStyles(cpcStyles) + + const classes = useStyles() + + const handleClick = event => { + setAnchorEl(anchorEl ? null : event.currentTarget) + } + + const handleClose = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + const id = open ? 'simple-popper' : undefined + + return ( +
+ {children && ( + <> +
+ {children} +
+
+ + + +
+ +
+
Copied to clipboard!
+
+
+ + )} +
+ ) +} + +export default CopyToClipboard diff --git a/new-lamassu-admin/src/pages/Transactions/DetailsCard.js b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js new file mode 100644 index 00000000..51820c50 --- /dev/null +++ b/new-lamassu-admin/src/pages/Transactions/DetailsCard.js @@ -0,0 +1,199 @@ +import React, { memo } from 'react' +import classnames from 'classnames' +import moment from 'moment' +import BigNumber from 'bignumber.js' +import { startCase, lowerCase } from 'lodash/fp' +import { makeStyles } from '@material-ui/core/styles' + +import { detailsRowStyles, labelStyles } from './Transactions.styles' +import CopyToClipboard from './CopyToClipboard' +import toUnit from './tx' + +import { IDButton } from '../../components/buttons' +import { ReactComponent as TxInIcon } from '../../styling/icons/direction/cash-in.svg' +import { ReactComponent as TxOutIcon } from '../../styling/icons/direction/cash-out.svg' +import { ReactComponent as CamIdIcon } from '../../styling/icons/ID/photo/zodiac.svg' +import { ReactComponent as CamIdInverseIcon } from '../../styling/icons/ID/photo/white.svg' +import { ReactComponent as CardIdIcon } from '../../styling/icons/ID/card/zodiac.svg' +import { ReactComponent as CardIdInverseIcon } from '../../styling/icons/ID/card/white.svg' +import { ReactComponent as PhoneIdIcon } from '../../styling/icons/ID/phone/zodiac.svg' +import { ReactComponent as PhoneIdInverseIcon } from '../../styling/icons/ID/phone/white.svg' + +const Label = ({ children }) => { + const useStyles = makeStyles(labelStyles) + + const classes = useStyles() + + return
{children}
+} + +const DetailsRow = ({ tx, ...props }) => { + const useStyles = makeStyles(detailsRowStyles) + + const classes = useStyles() + + const addr = tx.toAddress + const txHash = tx.txHash + const fiat = Number.parseFloat(tx.fiat) + const crypto = toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toFormat(5) + const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2) + const commission = tx.txClass === 'cashOut' ? fiat * commissionPercentage : fiat * commissionPercentage + Number.parseFloat(tx.fee) + const customer = tx.customerIdCardData && { + name: startCase(lowerCase(`${tx.customerIdCardData.firstName} ${tx.customerIdCardData.lastName}`)), + age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'), + country: tx.customerIdCardData.country, + idCardNumber: tx.customerIdCardData.documentNumber, + idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format('DD-MM-YYYY') + } + + const formatAddress = (address = '') => { + return address.replace(/(.{5})/g, '$1 ') + } + + return ( + <> +
+
+
{/* Column 1 */} +
+
+ +
+ {tx.txClass === 'cashOut' ? : } + {tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'} +
+
+
+ +
+ {tx.customerPhone && ( + + {tx.customerPhone} + + )} + {tx.customerIdCardPhotoPath && !tx.customerIdCardData && ( + + + + )} + {tx.customerIdCardData && ( + +
+
+
+ +
{customer.name}
+
+
+ +
{customer.age}
+
+
+ +
{customer.country}
+
+
+
+
+ +
{customer.idCardNumber}
+
+
+ +
+
+
+ +
{customer.idCardExpirationDate}
+
+
+
+ + )} + {tx.customerIdCameraPath && ( + + + + )} +
+
+
+
+
{/* Column 2 */} +
+
+ +
{`1 ${tx.cryptoCode} = ${Number(fiat / crypto).toFixed(3)} ${tx.fiatCode}`}
+
+
+ +
{`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}
+
+
+
+
{/* Column 3 */} +
+
+ {/* Export to PDF */} +
+
+
+
+
+
{/* Column 1 */} +
+
+ +
+ + {formatAddress(addr)} + +
+
+
+
+
{/* Column 2 */} +
+
+ +
+ + {txHash} + +
+
+
+
+
{/* Column 3 */} +
+
+ + + {tx.id} + +
+
+
+
+
+ + ) +} + +export default memo(DetailsRow, (prev, next) => prev.tx.id === next.tx.id) diff --git a/new-lamassu-admin/src/pages/Transactions/Transactions.js b/new-lamassu-admin/src/pages/Transactions/Transactions.js new file mode 100644 index 00000000..c3810da4 --- /dev/null +++ b/new-lamassu-admin/src/pages/Transactions/Transactions.js @@ -0,0 +1,162 @@ +import React, { useState } from 'react' +import moment from 'moment' +import BigNumber from 'bignumber.js' +import { upperCase } from 'lodash/fp' +import { makeStyles } from '@material-ui/core/styles' +import useAxios from '@use-hooks/axios' + +import { mainStyles } from './Transactions.styles' +import DetailsRow from './DetailsCard' +import toUnit from './tx' + +import Title from '../../components/Title' +import ExpTable from '../../components/expandable-table/ExpTable' +import LogsDowloaderPopover from '../../components/LogsDownloaderPopper' +import { FeatureButton } from '../../components/buttons' +import { ReactComponent as TxInIcon } from '../../styling/icons/direction/cash-in.svg' +import { ReactComponent as TxOutIcon } from '../../styling/icons/direction/cash-out.svg' +import { ReactComponent as Download } from '../../styling/icons/button/download/zodiac.svg' +import { ReactComponent as DownloadInverseIcon } from '../../styling/icons/button/download/white.svg' + +const Transactions = () => { + const [anchorEl, setAnchorEl] = useState(null) + + const useStyles = makeStyles(mainStyles) + + const classes = useStyles() + + const { response: txResponse } = useAxios({ + url: 'http://localhost:8070/api/txs/', + method: 'GET', + trigger: [] + }) + + const formatCustomerName = (customer) => { + const { firstName, lastName } = customer + + return `${upperCase(firstName.slice(0, 1))}. ${lastName}` + } + + const getCustomerDisplayName = (tx) => { + if (tx.customerName) return tx.customerName + if (tx.customerIdCardData) return formatCustomerName(tx.customerIdCardData) + return tx.customerPhone + } + + const rows = txResponse && txResponse.data.map(tx => { + const customerName = getCustomerDisplayName(tx) + + return { + id: tx.id, + columns: [ + { + name: '', + value: tx.txClass === 'cashOut' ? : , + size: 62 + }, + { + name: 'Machine', + value: tx.machineName, + className: classes.overflowTd, + size: 180 + }, + { + name: 'Customer', + value: customerName, + className: classes.overflowTd, + size: 162 + }, + { + name: 'Cash', + value: `${Number.parseFloat(tx.fiat)} ${tx.fiatCode}`, + textAlign: 'right', + size: 110 + }, + { + name: 'Crypto', + value: `${toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode).toFormat(5)} ${tx.cryptoCode}`, + textAlign: 'right', + size: 141 + }, + { + name: 'Address', + value: tx.toAddress, + className: classes.overflowTd, + size: 136 + }, + { + name: 'Date (UTC)', + value: moment.utc(tx.created).format('YYYY-MM-D'), + textAlign: 'right', + size: 124 + }, + { + name: 'Time (UTC)', + value: moment.utc(tx.created).format('HH:mm:ss'), + textAlign: 'right', + size: 124 + }, + { + name: '', // Trade + value: '', + size: 90 + }, + { + size: 71 + } + ], + details: ( + + ) + } + }) + + const handleOpenRangePicker = (event) => { + setAnchorEl(anchorEl ? null : event.currentTarget) + } + + const handleCloseRangePicker = () => { + setAnchorEl(null) + } + + const open = Boolean(anchorEl) + const id = open ? 'date-range-popover' : undefined + + return ( + <> +
+
+ Transactions + {txResponse && ( +
+ + tx.created} + onClose={handleCloseRangePicker} + /> +
+ )} +
+
+
Cash-out
+
Cash-in
+
+
+ + + ) +} + +export default Transactions diff --git a/new-lamassu-admin/src/pages/Transactions/Transactions.styles.js b/new-lamassu-admin/src/pages/Transactions/Transactions.styles.js new file mode 100644 index 00000000..66e0e99a --- /dev/null +++ b/new-lamassu-admin/src/pages/Transactions/Transactions.styles.js @@ -0,0 +1,160 @@ +import baseStyles from '../Logs.styles' + +import { offColor, white } from '../../styling/variables' +import typographyStyles from '../../components/typography/styles' + +const { label1, mono, p } = typographyStyles +const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles + +const cpcStyles = { + wrapper: { + extend: mono, + display: 'flex', + alignItems: 'center', + height: 32 + }, + address: { + lineBreak: 'anywhere' + }, + buttonWrapper: { + '& button': { + border: 'none', + backgroundColor: 'transparent', + cursor: 'pointer' + } + }, + popoverContent: { + extend: label1, + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + color: white, + borderRadius: 4, + padding: [[5, 9]] + } +} + +const detailsRowStyles = { + wrapper: { + display: 'flex', + flexDirection: 'column' + }, + col: { + display: 'flex', + flexDirection: 'column' + }, + col1: { + width: 413 + }, + col2: { + width: 506 + }, + col3: { + width: 233 + }, + innerRow: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between' + }, + row: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + margin: [[25, 0]] + }, + mono: { + extend: mono + }, + txIcon: { + marginRight: 10 + }, + availableIds: { + width: 110, + marginRight: 61, + '& > div': { + display: 'flex', + flexDirection: 'row', + '& button': { + '&:first-child': { + marginRight: 4 + }, + '&:last-child': { + marginLeft: 4 + }, + '&:only-child': { + margin: 0 + }, + '&:nth-child(2):last-child': { + margin: 0 + } + } + } + }, + commissionWrapper: { + width: 110, + marginRight: 155 + }, + idCardDataCard: { + extend: p, + display: 'flex', + padding: [[11, 8]], + '& > div': { + display: 'flex', + flexDirection: 'column', + '& > div': { + width: 144, + height: 37, + marginBottom: 15, + '&:last-child': { + marginBottom: 0 + } + } + } + }, + cryptoAddr: { + width: 252 + }, + txId: { + width: 346 + }, + sessionId: { + width: 184 + } +} + +const labelStyles = { + label: { + extend: label1, + color: offColor, + marginBottom: 4 + } +} + +const mainStyles = { + titleWrapper, + titleAndButtonsContainer, + buttonsWrapper, + headerLabels: { + display: 'flex', + flexDirection: 'row', + '& div': { + display: 'flex', + alignItems: 'center' + }, + '& > div:first-child': { + marginRight: 24 + }, + '& span': { + extend: label1, + marginLeft: 6 + } + }, + overflowTd: { + overflow: 'hidden', + whiteSpace: 'nowrap', + textOverflow: 'ellipsis' + } +} + +export { cpcStyles, detailsRowStyles, labelStyles, mainStyles } diff --git a/new-lamassu-admin/src/pages/Transactions/tx.js b/new-lamassu-admin/src/pages/Transactions/tx.js new file mode 100644 index 00000000..bfd8473d --- /dev/null +++ b/new-lamassu-admin/src/pages/Transactions/tx.js @@ -0,0 +1,54 @@ +import { find } from 'lodash/fp' + +const CRYPTO_CURRENCIES = [ + { + cryptoCode: 'BTC', + display: 'Bitcoin', + code: 'bitcoin', + unitScale: 8 + }, + { + cryptoCode: 'ETH', + display: 'Ethereum', + code: 'ethereum', + unitScale: 18 + }, + { + cryptoCode: 'LTC', + display: 'Litecoin', + code: 'litecoin', + unitScale: 8 + }, + { + cryptoCode: 'DASH', + display: 'Dash', + code: 'dash', + unitScale: 8 + }, + { + cryptoCode: 'ZEC', + display: 'Zcash', + code: 'zcash', + unitScale: 8 + }, + { + cryptoCode: 'BCH', + display: 'Bitcoin Cash', + code: 'bitcoincash', + unitScale: 8 + } +] + +function getCryptoCurrency (cryptoCode) { + const cryptoCurrency = find(['cryptoCode', cryptoCode], CRYPTO_CURRENCIES) + if (!cryptoCurrency) throw new Error(`Unsupported crypto: ${cryptoCode}`) + return cryptoCurrency +} + +function toUnit (cryptoAtoms, cryptoCode) { + const cryptoRec = getCryptoCurrency(cryptoCode) + const unitScale = cryptoRec.unitScale + return cryptoAtoms.shiftedBy(-unitScale) +} + +export default toUnit diff --git a/new-lamassu-admin/src/routing/routes.js b/new-lamassu-admin/src/routing/routes.js index b35fd6d7..ff726baa 100644 --- a/new-lamassu-admin/src/routing/routes.js +++ b/new-lamassu-admin/src/routing/routes.js @@ -7,6 +7,7 @@ import Logs from '../pages/Logs' import Locales from '../pages/Locales' import Funding from '../pages/Funding' import ServerLogs from '../pages/ServerLogs' +import Transactions from '../pages/Transactions/Transactions' const tree = [ { key: 'transactions', label: 'Transactions', route: '/transactions' }, @@ -61,6 +62,7 @@ const Routes = () => ( + ) diff --git a/new-lamassu-admin/src/styling/icons/ID/card/comet.svg b/new-lamassu-admin/src/styling/icons/ID/card/comet.svg index 0e31a623..2eb5e448 100644 --- a/new-lamassu-admin/src/styling/icons/ID/card/comet.svg +++ b/new-lamassu-admin/src/styling/icons/ID/card/comet.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/card/tomato.svg b/new-lamassu-admin/src/styling/icons/ID/card/tomato.svg index bffe900a..8d19b415 100644 --- a/new-lamassu-admin/src/styling/icons/ID/card/tomato.svg +++ b/new-lamassu-admin/src/styling/icons/ID/card/tomato.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/card/white.svg b/new-lamassu-admin/src/styling/icons/ID/card/white.svg index c8688e28..8c0e3f14 100644 --- a/new-lamassu-admin/src/styling/icons/ID/card/white.svg +++ b/new-lamassu-admin/src/styling/icons/ID/card/white.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/card/zodiac.svg b/new-lamassu-admin/src/styling/icons/ID/card/zodiac.svg index afd7c287..733ef463 100644 --- a/new-lamassu-admin/src/styling/icons/ID/card/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/ID/card/zodiac.svg @@ -13,4 +13,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/phone/comet.svg b/new-lamassu-admin/src/styling/icons/ID/phone/comet.svg index 06dd9df4..cb049e57 100644 --- a/new-lamassu-admin/src/styling/icons/ID/phone/comet.svg +++ b/new-lamassu-admin/src/styling/icons/ID/phone/comet.svg @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/phone/tomato.svg b/new-lamassu-admin/src/styling/icons/ID/phone/tomato.svg index f9f0163f..7978f7f2 100644 --- a/new-lamassu-admin/src/styling/icons/ID/phone/tomato.svg +++ b/new-lamassu-admin/src/styling/icons/ID/phone/tomato.svg @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/phone/white.svg b/new-lamassu-admin/src/styling/icons/ID/phone/white.svg index e5e76873..908351d6 100644 --- a/new-lamassu-admin/src/styling/icons/ID/phone/white.svg +++ b/new-lamassu-admin/src/styling/icons/ID/phone/white.svg @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/ID/phone/zodiac.svg b/new-lamassu-admin/src/styling/icons/ID/phone/zodiac.svg index b2da6741..372ec697 100644 --- a/new-lamassu-admin/src/styling/icons/ID/phone/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/ID/phone/zodiac.svg @@ -6,4 +6,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/action/expand/closed.svg b/new-lamassu-admin/src/styling/icons/action/expand/closed.svg index 597cb878..5036a066 100644 --- a/new-lamassu-admin/src/styling/icons/action/expand/closed.svg +++ b/new-lamassu-admin/src/styling/icons/action/expand/closed.svg @@ -1,13 +1,13 @@ - - - icon/actions/expand/closed + + + icon/action/expand/closed Created with Sketch. - - + + - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/action/expand/open.svg b/new-lamassu-admin/src/styling/icons/action/expand/open.svg index ae8601d5..cb7388ac 100644 --- a/new-lamassu-admin/src/styling/icons/action/expand/open.svg +++ b/new-lamassu-admin/src/styling/icons/action/expand/open.svg @@ -1,13 +1,13 @@ - - - icon/actions/expand/open + + + icon/action/expand/open Created with Sketch. - - + + - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/authorize/white.svg b/new-lamassu-admin/src/styling/icons/button/authorize/white.svg index 2ec79f66..a4276171 100644 --- a/new-lamassu-admin/src/styling/icons/button/authorize/white.svg +++ b/new-lamassu-admin/src/styling/icons/button/authorize/white.svg @@ -7,4 +7,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/authorize/zodiac.svg b/new-lamassu-admin/src/styling/icons/button/authorize/zodiac.svg index 3086f7c0..1e99eb49 100644 --- a/new-lamassu-admin/src/styling/icons/button/authorize/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/button/authorize/zodiac.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/cancel/white.svg b/new-lamassu-admin/src/styling/icons/button/cancel/white.svg index 5b7df3ed..f412286b 100644 --- a/new-lamassu-admin/src/styling/icons/button/cancel/white.svg +++ b/new-lamassu-admin/src/styling/icons/button/cancel/white.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/cancel/zodiac.svg b/new-lamassu-admin/src/styling/icons/button/cancel/zodiac.svg index 628e16ff..44b89506 100644 --- a/new-lamassu-admin/src/styling/icons/button/cancel/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/button/cancel/zodiac.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/retry/white.svg b/new-lamassu-admin/src/styling/icons/button/retry/white.svg index d247726d..9e2f4887 100644 --- a/new-lamassu-admin/src/styling/icons/button/retry/white.svg +++ b/new-lamassu-admin/src/styling/icons/button/retry/white.svg @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/new-lamassu-admin/src/styling/icons/button/retry/zodiac.svg b/new-lamassu-admin/src/styling/icons/button/retry/zodiac.svg index b843afbc..72ee5fb5 100644 --- a/new-lamassu-admin/src/styling/icons/button/retry/zodiac.svg +++ b/new-lamassu-admin/src/styling/icons/button/retry/zodiac.svg @@ -9,4 +9,4 @@ - \ No newline at end of file +