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.
This commit is contained in:
parent
41d8b7afe1
commit
8334bd274f
38 changed files with 1225 additions and 226 deletions
|
|
@ -7,8 +7,9 @@ const got = require('got')
|
||||||
const supportLogs = require('../support_logs')
|
const supportLogs = require('../support_logs')
|
||||||
const machineLoader = require('../machine-loader')
|
const machineLoader = require('../machine-loader')
|
||||||
const logs = require('../logs')
|
const logs = require('../logs')
|
||||||
const serverLogs = require('./server-logs')
|
const transactions = require('./transactions')
|
||||||
|
|
||||||
|
const serverLogs = require('./server-logs')
|
||||||
const supervisor = require('./supervisor')
|
const supervisor = require('./supervisor')
|
||||||
const funding = require('./funding')
|
const funding = require('./funding')
|
||||||
const config = require('./config')
|
const config = require('./config')
|
||||||
|
|
@ -80,6 +81,12 @@ app.get('/api/server_logs', (req, res, next) => {
|
||||||
.catch(next)
|
.catch(next)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.get('/api/txs', (req, res, next) => {
|
||||||
|
return transactions.batch()
|
||||||
|
.then(r => res.send(r))
|
||||||
|
.catch(next)
|
||||||
|
})
|
||||||
|
|
||||||
function dbNotify () {
|
function dbNotify () {
|
||||||
return got.post('http://localhost:3030/dbChange')
|
return got.post('http://localhost:3030/dbChange')
|
||||||
.catch(e => console.error('Error: lamassu-server not responding'))
|
.catch(e => console.error('Error: lamassu-server not responding'))
|
||||||
|
|
|
||||||
110
lib/new-admin/transactions.js
Normal file
110
lib/new-admin/transactions.js
Normal file
|
|
@ -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 }
|
||||||
48
new-lamassu-admin/package-lock.json
generated
48
new-lamassu-admin/package-lock.json
generated
|
|
@ -5762,7 +5762,6 @@
|
||||||
"version": "6.26.0",
|
"version": "6.26.0",
|
||||||
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
"resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz",
|
||||||
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
"integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"core-js": "^2.4.0",
|
"core-js": "^2.4.0",
|
||||||
"regenerator-runtime": "^0.11.0"
|
"regenerator-runtime": "^0.11.0"
|
||||||
|
|
@ -7533,7 +7532,6 @@
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.2.0.tgz",
|
||||||
"integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
|
"integrity": "sha512-eOZERzvCmxS8HWzugj4Uxl8OJxa7T2k1Gi0X5qavwydHIfuSHq2dTD09LOg/XyGq4Zpb5IsR/2OJ5lbOegz78w==",
|
||||||
"dev": true,
|
|
||||||
"requires": {
|
"requires": {
|
||||||
"toggle-selection": "^1.0.6"
|
"toggle-selection": "^1.0.6"
|
||||||
}
|
}
|
||||||
|
|
@ -7541,8 +7539,7 @@
|
||||||
"core-js": {
|
"core-js": {
|
||||||
"version": "2.6.5",
|
"version": "2.6.5",
|
||||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
|
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.5.tgz",
|
||||||
"integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A==",
|
"integrity": "sha512-klh/kDpwX8hryYL14M9w/xei6vrv6sE8gTHDG7/T/+SEovB/G4ejwcfE/CBzO6Edsu+OETZMZ3wcX/EjUkrl5A=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"core-js-compat": {
|
"core-js-compat": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.4",
|
||||||
|
|
@ -11018,9 +11015,9 @@
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"handlebars": {
|
"handlebars": {
|
||||||
"version": "4.4.3",
|
"version": "4.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.3.tgz",
|
||||||
"integrity": "sha512-B0W4A2U1ww3q7VVthTKfh+epHx+q4mCt6iK+zEAzbMBpWQAwxCeKxEGpj/1oQTpzPXDNSOG7hmG14TsISH50yw==",
|
"integrity": "sha512-3yPecJoJHK/4c6aZhSvxOyG4vJKDshV36VHp0iVCDVh7o9w2vwi3NSnL2MMPj3YdduqaBcu7cGbggJQM0br9xA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"neo-async": "^2.6.0",
|
"neo-async": "^2.6.0",
|
||||||
|
|
@ -17276,9 +17273,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react": {
|
"react": {
|
||||||
"version": "16.10.2",
|
"version": "16.12.0",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-16.10.2.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-16.12.0.tgz",
|
||||||
"integrity": "sha512-MFVIq0DpIhrHFyqLU0S3+4dIcBhhOvBE8bJ/5kHPVOVaGdo0KuiQzpcjCPsf585WvhypqtrMILyoE2th6dT+Lw==",
|
"integrity": "sha512-fglqy3k5E+81pA8s+7K0/T3DBCF0ZDOher1elBFzF7O6arXJgzyu/FW+COxFvAWXJoJN9KIZbT2LXlukwphYTA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
|
|
@ -17336,6 +17333,15 @@
|
||||||
"tinycolor2": "^1.4.1"
|
"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": {
|
"react-dev-utils": {
|
||||||
"version": "9.1.0",
|
"version": "9.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-9.1.0.tgz",
|
||||||
|
|
@ -17672,8 +17678,7 @@
|
||||||
"react-lifecycles-compat": {
|
"react-lifecycles-compat": {
|
||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
|
||||||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==",
|
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"react-popper": {
|
"react-popper": {
|
||||||
"version": "1.3.4",
|
"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": {
|
"reactcss": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/reactcss/-/reactcss-1.2.3.tgz",
|
||||||
|
|
@ -19556,8 +19574,7 @@
|
||||||
"regenerator-runtime": {
|
"regenerator-runtime": {
|
||||||
"version": "0.11.1",
|
"version": "0.11.1",
|
||||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz",
|
||||||
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==",
|
"integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"regenerator-transform": {
|
"regenerator-transform": {
|
||||||
"version": "0.14.1",
|
"version": "0.14.1",
|
||||||
|
|
@ -21866,8 +21883,7 @@
|
||||||
"toggle-selection": {
|
"toggle-selection": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz",
|
||||||
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI=",
|
"integrity": "sha1-bkWxJj8gF/oKzH2J14sVuL932jI="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"toidentifier": {
|
"toidentifier": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,11 @@
|
||||||
"lodash": "4.17.15",
|
"lodash": "4.17.15",
|
||||||
"moment": "2.24.0",
|
"moment": "2.24.0",
|
||||||
"qrcode.react": "0.9.3",
|
"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-dom": "^16.10.2",
|
||||||
"react-router-dom": "5.1.2",
|
"react-router-dom": "5.1.2",
|
||||||
|
"react-virtualized": "^9.21.2",
|
||||||
"yup": "0.27.0"
|
"yup": "0.27.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
import FileSaver from 'file-saver'
|
|
||||||
import classnames from 'classnames'
|
import classnames from 'classnames'
|
||||||
import { toInteger } from 'lodash/fp'
|
import { get, compose } from 'lodash/fp'
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
|
import FileSaver from 'file-saver'
|
||||||
import { makeStyles } from '@material-ui/core'
|
import { makeStyles } from '@material-ui/core'
|
||||||
|
|
||||||
import { ReactComponent as Arrow } from '../styling/icons/arrow/download_logs.svg'
|
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 { primaryColor, offColor, zircon } from '../styling/variables'
|
||||||
|
|
||||||
import { Link } from './buttons'
|
import { Link } from './buttons'
|
||||||
import { RadioGroup } from './inputs'
|
import { RadioGroup } from './inputs'
|
||||||
import Popover from './Popover'
|
import Popper from './Popper'
|
||||||
import DateRangePicker from './date-range-picker/DateRangePicker'
|
import DateRangePicker from './date-range-picker/DateRangePicker'
|
||||||
|
|
||||||
const { info1, label1, label2, h4 } = typographyStyles
|
const { info1, label1, label2, h4 } = typographyStyles
|
||||||
|
|
@ -121,58 +121,81 @@ const styles = {
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
const useStyles = makeStyles(styles)
|
||||||
|
|
||||||
const LogsDownloaderPopover = ({ id, open, anchorEl, onClose, logsResponse, ...props }) => {
|
const LogsDownloaderPopover = ({ id, name, open, anchorEl, getTimestamp, logs, title, ...props }) => {
|
||||||
const [radioButtons, setRadioButtons] = useState(0)
|
const radioButtonAll = 'all'
|
||||||
|
const radioButtonRange = 'range'
|
||||||
|
|
||||||
|
const [selectedRadio, setSelectedRadio] = useState(radioButtonAll)
|
||||||
const [range, setRange] = useState(null)
|
const [range, setRange] = useState(null)
|
||||||
|
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
|
|
||||||
const dateRangePickerClasses = {
|
const dateRangePickerClasses = {
|
||||||
[classes.dateRangePickerShowing]: radioButtons === 1,
|
[classes.dateRangePickerShowing]: selectedRadio === radioButtonRange,
|
||||||
[classes.dateRangePickerHidden]: radioButtons === 0
|
[classes.dateRangePickerHidden]: selectedRadio === radioButtonAll
|
||||||
}
|
|
||||||
|
|
||||||
const formatDateFile = date => {
|
|
||||||
return moment(date).format('YYYY-MM-DD_HH-mm')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRadioButtons = (event) => {
|
const handleRadioButtons = (event) => {
|
||||||
setRadioButtons(toInteger(event.target.value))
|
compose(setSelectedRadio, get('target.value'))
|
||||||
|
const radio = event.target.value
|
||||||
|
setSelectedRadio(radio)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRangeChange = (from, to) => {
|
const handleRangeChange = (from, to) => {
|
||||||
setRange({ 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 (
|
return (
|
||||||
<Popover
|
<Popper
|
||||||
id={id}
|
id={id}
|
||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
onClose={onClose}
|
placement='bottom'
|
||||||
anchorOrigin={{
|
|
||||||
vertical: 'bottom',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: 'top',
|
|
||||||
horizontal: 'center'
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={classes.popoverContent}>
|
<div className={classes.popoverContent}>
|
||||||
<div className={classes.popoverHeader}>
|
<div className={classes.popoverHeader}>
|
||||||
Download logs
|
{title}
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.radioButtonsContainer}>
|
<div className={classes.radioButtonsContainer}>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
name='logs-select'
|
name='logs-select'
|
||||||
value={radioButtons}
|
value={selectedRadio}
|
||||||
labels={['All logs', 'Date range']}
|
options={radioButtonOptions}
|
||||||
ariaLabel='logs-select'
|
ariaLabel='logs-select'
|
||||||
onChange={handleRadioButtons}
|
onChange={handleRadioButtons}
|
||||||
className={classes.radioButtons}
|
className={classes.radioButtons}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
{selectedRadio === radioButtonRange && (
|
||||||
<div className={classnames(dateRangePickerClasses)}>
|
<div className={classnames(dateRangePickerClasses)}>
|
||||||
<div className={classes.dateContainerWrapper}>
|
<div className={classes.dateContainerWrapper}>
|
||||||
{range && (
|
{range && (
|
||||||
|
|
@ -190,30 +213,17 @@ const LogsDownloaderPopover = ({ id, open, anchorEl, onClose, logsResponse, ...p
|
||||||
onRangeChange={handleRangeChange}
|
onRangeChange={handleRangeChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
<div className={classes.download}>
|
<div className={classes.download}>
|
||||||
<Link
|
<Link
|
||||||
color='primary'
|
color='primary'
|
||||||
onClick={() => {
|
onClick={() => downloadLogs(range, logs)}
|
||||||
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`)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
Download
|
Download
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Popover>
|
</Popper>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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 (
|
|
||||||
<MaterialPopover
|
|
||||||
classes={{
|
|
||||||
paper: classes.paper
|
|
||||||
}}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<div className={classes.arrow} />
|
|
||||||
</MaterialPopover>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Popover
|
|
||||||
107
new-lamassu-admin/src/components/Popper.js
Normal file
107
new-lamassu-admin/src/components/Popper.js
Normal file
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<MaterialPopper
|
||||||
|
disablePortal={false}
|
||||||
|
modifiers={modifiers}
|
||||||
|
className={classes.popover}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<Paper className={classes.root}>
|
||||||
|
<span className={classnames(arrowClasses)} ref={setArrowRef} />
|
||||||
|
{children}
|
||||||
|
</Paper>
|
||||||
|
</MaterialPopper>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Popover
|
||||||
|
|
@ -15,7 +15,10 @@ const styles = {
|
||||||
},
|
},
|
||||||
primary,
|
primary,
|
||||||
buttonIcon: {
|
buttonIcon: {
|
||||||
margin: 'auto'
|
margin: 'auto',
|
||||||
|
'& svg': {
|
||||||
|
overflow: 'visible'
|
||||||
|
}
|
||||||
},
|
},
|
||||||
buttonIconActive: {} // required to extend primary
|
buttonIconActive: {} // required to extend primary
|
||||||
}
|
}
|
||||||
|
|
|
||||||
113
new-lamassu-admin/src/components/buttons/IDButton.js
Normal file
113
new-lamassu-admin/src/components/buttons/IDButton.js
Normal file
|
|
@ -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 (
|
||||||
|
<>
|
||||||
|
<button aria-describedby={id} onClick={handleClick} className={classnames(classNames, className)} {...props}>
|
||||||
|
{Icon && !open && <div className={classnames(iconClassNames)}><Icon /></div>}
|
||||||
|
{InverseIcon && open &&
|
||||||
|
<div className={classnames(iconClassNames)}>
|
||||||
|
<InverseIcon />
|
||||||
|
</div>}
|
||||||
|
</button>
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
arrowSize={3}
|
||||||
|
placement='top'
|
||||||
|
>
|
||||||
|
<div className={classes.popoverContent}>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export default IDButton
|
||||||
|
|
@ -3,5 +3,6 @@ import Link from './Link'
|
||||||
import SimpleButton from './SimpleButton'
|
import SimpleButton from './SimpleButton'
|
||||||
import ActionButton from './ActionButton'
|
import ActionButton from './ActionButton'
|
||||||
import FeatureButton from './FeatureButton'
|
import FeatureButton from './FeatureButton'
|
||||||
|
import IDButton from './IDButton'
|
||||||
|
|
||||||
export { Button, Link, SimpleButton, ActionButton, FeatureButton }
|
export { Button, Link, SimpleButton, ActionButton, FeatureButton, IDButton }
|
||||||
|
|
|
||||||
|
|
@ -1,99 +1,122 @@
|
||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState } from 'react'
|
||||||
import { map, set } from 'lodash/fp'
|
|
||||||
import classnames from 'classnames'
|
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 { 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 ExpandClosedIcon } from '../../styling/icons/action/expand/closed.svg'
|
||||||
import { ReactComponent as ExpandOpenIcon } from '../../styling/icons/action/expand/open.svg'
|
import { ReactComponent as ExpandOpenIcon } from '../../styling/icons/action/expand/open.svg'
|
||||||
import { mainWidth } from '../../styling/variables'
|
import { mainWidth } from '../../styling/variables'
|
||||||
|
|
||||||
const styles = {
|
const styles = {
|
||||||
hideDetailsRow: {
|
|
||||||
display: 'none'
|
|
||||||
},
|
|
||||||
expandButton: {
|
expandButton: {
|
||||||
border: 'none',
|
border: 'none',
|
||||||
backgroundColor: 'transparent',
|
backgroundColor: 'transparent',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
padding: 4
|
padding: 4
|
||||||
|
},
|
||||||
|
row: {
|
||||||
|
borderRadius: 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const useStyles = makeStyles(styles)
|
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 classes = useStyles()
|
||||||
|
|
||||||
const detailsRowClasses = {
|
|
||||||
[classes.detailsRow]: true,
|
|
||||||
[classes.hideDetailsRow]: expanded
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tr className={classnames(className)} {...props}>
|
<Tr className={classnames(classes.row, className)} {...props}>
|
||||||
{columns.map((col, idx) => (
|
{columns.slice(0, -1).map((col, idx) => (
|
||||||
<Td key={uuidv1()} size={sizes[idx]} className={col.className} textAlign={col.textAlign}>{col.value}</Td>
|
<Td key={idx} size={col.size} className={col.className} textAlign={col.textAlign}>{col.value}</Td>
|
||||||
))}
|
))}
|
||||||
<Td size={sizes[sizes.length - 1]}>
|
<Td size={columns[columns.length - 1].size}>
|
||||||
<button onClick={() => expandRow(id)} className={classes.expandButton}>
|
<button onClick={() => expandRow(id)} className={classes.expandButton}>
|
||||||
{expanded && <ExpandOpenIcon />}
|
{expanded && <ExpandOpenIcon />}
|
||||||
{!expanded && <ExpandClosedIcon />}
|
{!expanded && <ExpandClosedIcon />}
|
||||||
</button></Td>
|
</button>
|
||||||
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
<Tr className={classnames(detailsRowClasses)}>
|
{expanded && (
|
||||||
|
<Tr className={classes.detailsRow}>
|
||||||
<Td size={mainWidth}>
|
<Td size={mainWidth}>
|
||||||
{details}
|
{details}
|
||||||
</Td>
|
</Td>
|
||||||
</Tr>
|
</Tr>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/* headers = [{ value, className, textAlign }]
|
/* rows = [{ columns = [{ name, value, className, textAlign, size }], details, className, error, errorMessage }]
|
||||||
* rows = [{ columns = [{ value, className, textAlign }], 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 ExpTable = ({ rows = [], className, ...props }) => {
|
||||||
const [rowStates, setRowStates] = useState(null)
|
const [expanded, setExpanded] = useState(null)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setRowStates(rows && rows.map((x) => { return { id: x.id, expanded: false } }))
|
|
||||||
}, [rows])
|
|
||||||
|
|
||||||
const expandRow = (id) => {
|
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 (
|
||||||
|
<CellMeasurer
|
||||||
|
cache={cache}
|
||||||
|
columnIndex={0}
|
||||||
|
key={key}
|
||||||
|
parent={parent}
|
||||||
|
rowIndex={index}
|
||||||
|
>
|
||||||
|
<div style={style}>
|
||||||
|
<ExpRow
|
||||||
|
id={index}
|
||||||
|
columns={rows[index].columns}
|
||||||
|
details={rows[index].details}
|
||||||
|
expanded={index === expanded}
|
||||||
|
className={rows[index].className}
|
||||||
|
expandRow={expandRow}
|
||||||
|
error={rows[index].error}
|
||||||
|
errorMessage={rows[index].errorMessage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</CellMeasurer>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Table className={classnames(className)}>
|
<>
|
||||||
|
<div>
|
||||||
<THead>
|
<THead>
|
||||||
{headers.map((header, idx) => (
|
{rows[0].columns.map((c, idx) => (
|
||||||
<Th key={uuidv1()} size={sizes[idx]} className={header.className} textAlign={header.textAlign}>{header.value}</Th>
|
<Th key={idx} size={c.size} className={c.className} textAlign={c.textAlign}>{c.name}</Th>
|
||||||
))}
|
))}
|
||||||
</THead>
|
</THead>
|
||||||
<TBody>
|
</div>
|
||||||
{rowStates && rowStates.map((r, idx) => {
|
<div style={{ flex: '1 1 auto' }}>
|
||||||
const row = rows[idx]
|
<AutoSizer disableWidth>
|
||||||
|
{({ height }) => (
|
||||||
return (
|
<List
|
||||||
<ExpRow
|
{...props}
|
||||||
key={uuidv1()}
|
height={height}
|
||||||
id={r.id}
|
width={mainWidth}
|
||||||
columns={row.columns}
|
rowCount={rows.length}
|
||||||
details={row.details}
|
rowHeight={cache.rowHeight}
|
||||||
sizes={sizes}
|
rowRenderer={rowRenderer}
|
||||||
expanded={r.expanded}
|
overscanRowCount={50}
|
||||||
className={row.className}
|
deferredMeasurementCache={cache}
|
||||||
expandRow={expandRow}
|
|
||||||
error={row.error}
|
|
||||||
errorMessage={row.errorMessage}
|
|
||||||
/>
|
/>
|
||||||
)
|
)}
|
||||||
})}
|
</AutoSizer>
|
||||||
</TBody>
|
</div>
|
||||||
</Table>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,13 +28,15 @@ const Label = withStyles({
|
||||||
}
|
}
|
||||||
})(props => <FormControlLabel {...props} />)
|
})(props => <FormControlLabel {...props} />)
|
||||||
|
|
||||||
const RadioGroup = ({ name, value, labels, ariaLabel, onChange, className, ...props }) => {
|
/* options = [{ label, value }]
|
||||||
|
*/
|
||||||
|
const RadioGroup = ({ name, value, options, ariaLabel, onChange, className, ...props }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{labels && (
|
{options && (
|
||||||
<MaterialRadioGroup aria-label={ariaLabel} name={name} value={value} onChange={onChange} className={classnames(className)}>
|
<MaterialRadioGroup aria-label={ariaLabel} name={name} value={value} onChange={onChange} className={classnames(className)}>
|
||||||
{labels.map((label, idx) => (
|
{options.map((options, idx) => (
|
||||||
<Label key={idx} value={idx} control={<GreenRadio />} label={label} />
|
<Label key={idx} value={options.value} control={<GreenRadio />} label={options.label} />
|
||||||
))}
|
))}
|
||||||
</MaterialRadioGroup>
|
</MaterialRadioGroup>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ export default {
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
width: WIDTH,
|
width: WIDTH,
|
||||||
zIndex: 1000,
|
zIndex: 2,
|
||||||
'& label': {
|
'& label': {
|
||||||
extend: label1,
|
extend: label1,
|
||||||
color: offColor,
|
color: offColor,
|
||||||
|
|
|
||||||
|
|
@ -6,14 +6,14 @@ import { spacer } from '../../styling/variables'
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
td: {
|
td: {
|
||||||
padding: `0 ${spacer * 3}px`
|
padding: [[0, spacer * 3]]
|
||||||
},
|
},
|
||||||
alignRight: {
|
alignRight: {
|
||||||
textAlign: 'right'
|
textAlign: 'right'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const TableCell = memo(({ rightAlign, className, children, ...props }) => {
|
const TableCell = memo(({ colspan, rightAlign, className, children, ...props }) => {
|
||||||
const classes = useStyles()
|
const classes = useStyles()
|
||||||
const styles = {
|
const styles = {
|
||||||
[classes.td]: true,
|
[classes.td]: true,
|
||||||
|
|
@ -21,7 +21,7 @@ const TableCell = memo(({ rightAlign, className, children, ...props }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<td className={classnames(styles, className)} {...props}>
|
<td colSpan={colspan} className={classnames(styles, className)} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -15,13 +15,21 @@ const useStyles = makeStyles({
|
||||||
textAlign: 'left',
|
textAlign: 'left',
|
||||||
color: white,
|
color: white,
|
||||||
padding: `0 ${spacer * 3}px`
|
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 classes = useStyles()
|
||||||
|
const styles = {
|
||||||
|
[classes.th]: true,
|
||||||
|
[classes.alignRight]: rightAlign
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<th {...props} className={classnames(classes.th, className)}>
|
<th {...props} className={classnames(styles, className)}>
|
||||||
{children}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from 'react'
|
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 moment from 'moment'
|
||||||
import useAxios from '@use-hooks/axios'
|
import useAxios from '@use-hooks/axios'
|
||||||
import { makeStyles } from '@material-ui/core'
|
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 { Table, TableHead, TableRow, TableHeader, TableBody, TableCell } from '../components/table'
|
||||||
import { Select } from '../components/inputs'
|
import { Select } from '../components/inputs'
|
||||||
import Uptime from '../components/Uptime'
|
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 Download } from '../styling/icons/button/download/zodiac.svg'
|
||||||
import { ReactComponent as DownloadActive } from '../styling/icons/button/download/white.svg'
|
import { ReactComponent as DownloadActive } from '../styling/icons/button/download/white.svg'
|
||||||
import { offColor } from '../styling/variables'
|
import { offColor } from '../styling/variables'
|
||||||
|
|
@ -108,11 +108,7 @@ const Logs = () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleOpenRangePicker = (event) => {
|
const handleOpenRangePicker = (event) => {
|
||||||
setAnchorEl(event.currentTarget)
|
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseRangePicker = () => {
|
|
||||||
setAnchorEl(null)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const open = Boolean(anchorEl)
|
const open = Boolean(anchorEl)
|
||||||
|
|
@ -133,11 +129,13 @@ const Logs = () => {
|
||||||
onClick={handleOpenRangePicker}
|
onClick={handleOpenRangePicker}
|
||||||
/>
|
/>
|
||||||
<LogsDowloaderPopover
|
<LogsDowloaderPopover
|
||||||
|
title='Download logs'
|
||||||
|
name='server-logs'
|
||||||
id={id}
|
id={id}
|
||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorEl}
|
anchorEl={anchorEl}
|
||||||
logsResponse={logsResponse}
|
logs={logsResponse.data.logs}
|
||||||
onClose={handleCloseRangePicker}
|
getTimestamp={(log) => log.timestamp}
|
||||||
/>
|
/>
|
||||||
<SimpleButton className={classes.button} disabled={loading} onClick={sendSnapshot}>
|
<SimpleButton className={classes.button} disabled={loading} onClick={sendSnapshot}>
|
||||||
Share with Lamassu
|
Share with Lamassu
|
||||||
|
|
|
||||||
68
new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js
Normal file
68
new-lamassu-admin/src/pages/Transactions/CopyToClipboard.js
Normal file
|
|
@ -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 (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
{children && (
|
||||||
|
<>
|
||||||
|
<div className={classnames(classes.address, className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className={classes.buttonWrapper}>
|
||||||
|
<ReactCopyToClipboard
|
||||||
|
text={replace(/\s/g, '')(children)}
|
||||||
|
>
|
||||||
|
<button aria-describedby={id} onClick={(event) => handleClick(event)}><CopyIcon /></button>
|
||||||
|
</ReactCopyToClipboard>
|
||||||
|
</div>
|
||||||
|
<Popover
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={handleClose}
|
||||||
|
arrowSize={3}
|
||||||
|
bgColor={comet}
|
||||||
|
placement='top'
|
||||||
|
>
|
||||||
|
<div className={classes.popoverContent}>
|
||||||
|
<div>Copied to clipboard!</div>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CopyToClipboard
|
||||||
199
new-lamassu-admin/src/pages/Transactions/DetailsCard.js
Normal file
199
new-lamassu-admin/src/pages/Transactions/DetailsCard.js
Normal file
|
|
@ -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 <div className={classes.label}>{children}</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<>
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<div className={classnames(classes.row)}>
|
||||||
|
<div className={classnames(classes.col, classes.col1)}> {/* Column 1 */}
|
||||||
|
<div className={classes.innerRow}>
|
||||||
|
<div>
|
||||||
|
<Label>Direction</Label>
|
||||||
|
<div>
|
||||||
|
<span className={classes.txIcon}>{tx.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}</span>
|
||||||
|
<span>{tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.availableIds}>
|
||||||
|
<Label>Available IDs</Label>
|
||||||
|
<div>
|
||||||
|
{tx.customerPhone && (
|
||||||
|
<IDButton
|
||||||
|
name='phone'
|
||||||
|
Icon={PhoneIdIcon}
|
||||||
|
InverseIcon={PhoneIdInverseIcon}
|
||||||
|
>
|
||||||
|
{tx.customerPhone}
|
||||||
|
</IDButton>
|
||||||
|
)}
|
||||||
|
{tx.customerIdCardPhotoPath && !tx.customerIdCardData && (
|
||||||
|
<IDButton
|
||||||
|
name='card'
|
||||||
|
Icon={CardIdIcon}
|
||||||
|
InverseIcon={CardIdInverseIcon}
|
||||||
|
>
|
||||||
|
<img src={tx.customerIdCardPhotoPath} />
|
||||||
|
</IDButton>
|
||||||
|
)}
|
||||||
|
{tx.customerIdCardData && (
|
||||||
|
<IDButton
|
||||||
|
name='card'
|
||||||
|
Icon={CardIdIcon}
|
||||||
|
InverseIcon={CardIdInverseIcon}
|
||||||
|
>
|
||||||
|
<div className={classes.idCardDataCard}>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Label>Name</Label>
|
||||||
|
<div>{customer.name}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Age</Label>
|
||||||
|
<div>{customer.age}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Country</Label>
|
||||||
|
<div>{customer.country}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<Label>ID number</Label>
|
||||||
|
<div>{customer.idCardNumber}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Gender</Label>
|
||||||
|
<div />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Expiration date</Label>
|
||||||
|
<div>{customer.idCardExpirationDate}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</IDButton>
|
||||||
|
)}
|
||||||
|
{tx.customerIdCameraPath && (
|
||||||
|
<IDButton
|
||||||
|
name='cam'
|
||||||
|
Icon={CamIdIcon}
|
||||||
|
InverseIcon={CamIdInverseIcon}
|
||||||
|
>
|
||||||
|
<img src={tx.customerIdCameraPath} />
|
||||||
|
</IDButton>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames(classes.col, classes.col2)}> {/* Column 2 */}
|
||||||
|
<div className={classes.innerRow}>
|
||||||
|
<div>
|
||||||
|
<Label>Exchange rate</Label>
|
||||||
|
<div>{`1 ${tx.cryptoCode} = ${Number(fiat / crypto).toFixed(3)} ${tx.fiatCode}`}</div>
|
||||||
|
</div>
|
||||||
|
<div className={classes.commissionWrapper}>
|
||||||
|
<Label>Commission</Label>
|
||||||
|
<div>{`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames(classes.col, classes.col3)}> {/* Column 3 */}
|
||||||
|
<div className={classnames(classes.innerRow)}>
|
||||||
|
<div style={{ height: 43.4 }}>
|
||||||
|
{/* Export to PDF */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames(classes.row)}>
|
||||||
|
<div className={classnames(classes.col, classes.col1)}> {/* Column 1 */}
|
||||||
|
<div className={classes.innerRow}>
|
||||||
|
<div>
|
||||||
|
<Label>BTC address</Label>
|
||||||
|
<div>
|
||||||
|
<CopyToClipboard className={classes.cryptoAddr}>
|
||||||
|
{formatAddress(addr)}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames(classes.col, classes.col2)}> {/* Column 2 */}
|
||||||
|
<div className={classes.innerRow}>
|
||||||
|
<div>
|
||||||
|
<Label>Transaction ID</Label>
|
||||||
|
<div>
|
||||||
|
<CopyToClipboard className={classes.txId}>
|
||||||
|
{txHash}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={classnames(classes.col, classes.col3)}> {/* Column 3 */}
|
||||||
|
<div className={classes.innerRow}>
|
||||||
|
<div>
|
||||||
|
<Label>Session ID</Label>
|
||||||
|
<CopyToClipboard className={classes.sessionId}>
|
||||||
|
{tx.id}
|
||||||
|
</CopyToClipboard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(DetailsRow, (prev, next) => prev.tx.id === next.tx.id)
|
||||||
162
new-lamassu-admin/src/pages/Transactions/Transactions.js
Normal file
162
new-lamassu-admin/src/pages/Transactions/Transactions.js
Normal file
|
|
@ -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' ? <TxOutIcon /> : <TxInIcon />,
|
||||||
|
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: (
|
||||||
|
<DetailsRow tx={tx} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleOpenRangePicker = (event) => {
|
||||||
|
setAnchorEl(anchorEl ? null : event.currentTarget)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCloseRangePicker = () => {
|
||||||
|
setAnchorEl(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
const open = Boolean(anchorEl)
|
||||||
|
const id = open ? 'date-range-popover' : undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className={classes.titleWrapper}>
|
||||||
|
<div className={classes.titleAndButtonsContainer}>
|
||||||
|
<Title>Transactions</Title>
|
||||||
|
{txResponse && (
|
||||||
|
<div className={classes.buttonsWrapper}>
|
||||||
|
<FeatureButton
|
||||||
|
Icon={Download}
|
||||||
|
InverseIcon={DownloadInverseIcon}
|
||||||
|
aria-describedby={id}
|
||||||
|
variant='contained'
|
||||||
|
onClick={handleOpenRangePicker}
|
||||||
|
/>
|
||||||
|
<LogsDowloaderPopover
|
||||||
|
title='Download logs'
|
||||||
|
name='transactions'
|
||||||
|
id={id}
|
||||||
|
open={open}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
logs={txResponse.data}
|
||||||
|
getTimestamp={(tx) => tx.created}
|
||||||
|
onClose={handleCloseRangePicker}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className={classes.headerLabels}>
|
||||||
|
<div><TxOutIcon /><span>Cash-out</span></div>
|
||||||
|
<div><TxInIcon /><span>Cash-in</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ExpTable rows={rows} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Transactions
|
||||||
160
new-lamassu-admin/src/pages/Transactions/Transactions.styles.js
Normal file
160
new-lamassu-admin/src/pages/Transactions/Transactions.styles.js
Normal file
|
|
@ -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 }
|
||||||
54
new-lamassu-admin/src/pages/Transactions/tx.js
Normal file
54
new-lamassu-admin/src/pages/Transactions/tx.js
Normal file
|
|
@ -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
|
||||||
|
|
@ -7,6 +7,7 @@ import Logs from '../pages/Logs'
|
||||||
import Locales from '../pages/Locales'
|
import Locales from '../pages/Locales'
|
||||||
import Funding from '../pages/Funding'
|
import Funding from '../pages/Funding'
|
||||||
import ServerLogs from '../pages/ServerLogs'
|
import ServerLogs from '../pages/ServerLogs'
|
||||||
|
import Transactions from '../pages/Transactions/Transactions'
|
||||||
|
|
||||||
const tree = [
|
const tree = [
|
||||||
{ key: 'transactions', label: 'Transactions', route: '/transactions' },
|
{ key: 'transactions', label: 'Transactions', route: '/transactions' },
|
||||||
|
|
@ -61,6 +62,7 @@ const Routes = () => (
|
||||||
<Route path='/maintenance/logs' component={Logs} />
|
<Route path='/maintenance/logs' component={Logs} />
|
||||||
<Route path='/maintenance/funding' component={Funding} />
|
<Route path='/maintenance/funding' component={Funding} />
|
||||||
<Route path='/maintenance/server-logs' component={ServerLogs} />
|
<Route path='/maintenance/server-logs' component={ServerLogs} />
|
||||||
|
<Route path='/transactions' component={Transactions} />
|
||||||
</Switch>
|
</Switch>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="16px" height="4px" viewBox="0 0 16 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="18px" height="6px" viewBox="0 0 18 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
<!-- Generator: Sketch 56.3 (81716) - https://sketch.com -->
|
||||||
<title>icon/actions/expand/closed</title>
|
<title>icon/action/expand/closed</title>
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<g id="icon/actions/expand/closed" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="show/closed-copy-3" stroke="#1B2559" stroke-width="1.5">
|
<g id="icon/action/expand/closed" transform="translate(1.000000, 1.000000)" stroke="#1B2559" stroke-width="1.5">
|
||||||
<circle id="Oval-4" cx="14" cy="2" r="2"></circle>
|
<circle id="Oval-4" cx="14" cy="2" r="2"></circle>
|
||||||
<circle id="Oval-4-Copy" cx="8" cy="2" r="2"></circle>
|
<circle id="Oval-4-Copy" cx="8" cy="2" r="2"></circle>
|
||||||
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 766 B |
|
|
@ -1,10 +1,10 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg width="16px" height="4px" viewBox="0 0 16 4" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
<svg width="18px" height="6px" viewBox="0 0 18 6" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
<!-- Generator: Sketch 56.3 (81716) - https://sketch.com -->
|
||||||
<title>icon/actions/expand/open</title>
|
<title>icon/action/expand/open</title>
|
||||||
<desc>Created with Sketch.</desc>
|
<desc>Created with Sketch.</desc>
|
||||||
<g id="icon/actions/expand/open" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||||
<g id="show/closed-copy-3" fill="#1B2559" stroke="#1B2559" stroke-width="1.5">
|
<g id="icon/action/expand/open" transform="translate(1.000000, 1.000000)" fill="#1B2559" stroke="#1B2559" stroke-width="1.5">
|
||||||
<circle id="Oval-4" cx="14" cy="2" r="2"></circle>
|
<circle id="Oval-4" cx="14" cy="2" r="2"></circle>
|
||||||
<circle id="Oval-4-Copy" cx="8" cy="2" r="2"></circle>
|
<circle id="Oval-4-Copy" cx="8" cy="2" r="2"></circle>
|
||||||
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 777 B |
Loading…
Add table
Add a link
Reference in a new issue