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.
|
|
@ -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'))
|
||||
|
|
|
|||
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
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<Popover
|
||||
<Popper
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={onClose}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'center'
|
||||
}}
|
||||
placement='bottom'
|
||||
>
|
||||
<div className={classes.popoverContent}>
|
||||
<div className={classes.popoverHeader}>
|
||||
Download logs
|
||||
{title}
|
||||
</div>
|
||||
<div className={classes.radioButtonsContainer}>
|
||||
<RadioGroup
|
||||
name='logs-select'
|
||||
value={radioButtons}
|
||||
labels={['All logs', 'Date range']}
|
||||
value={selectedRadio}
|
||||
options={radioButtonOptions}
|
||||
ariaLabel='logs-select'
|
||||
onChange={handleRadioButtons}
|
||||
className={classes.radioButtons}
|
||||
/>
|
||||
</div>
|
||||
<div className={classnames(dateRangePickerClasses)}>
|
||||
<div className={classes.dateContainerWrapper}>
|
||||
{range && (
|
||||
<>
|
||||
<DateContainer date={range.from}>From</DateContainer>
|
||||
<div className={classes.arrowContainer}>
|
||||
<Arrow className={classes.arrow} />
|
||||
</div>
|
||||
<DateContainer date={range.to}>To</DateContainer>
|
||||
</>
|
||||
)}
|
||||
{selectedRadio === radioButtonRange && (
|
||||
<div className={classnames(dateRangePickerClasses)}>
|
||||
<div className={classes.dateContainerWrapper}>
|
||||
{range && (
|
||||
<>
|
||||
<DateContainer date={range.from}>From</DateContainer>
|
||||
<div className={classes.arrowContainer}>
|
||||
<Arrow className={classes.arrow} />
|
||||
</div>
|
||||
<DateContainer date={range.to}>To</DateContainer>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<DateRangePicker
|
||||
maxDate={moment()}
|
||||
onRangeChange={handleRangeChange}
|
||||
/>
|
||||
</div>
|
||||
<DateRangePicker
|
||||
maxDate={moment()}
|
||||
onRangeChange={handleRangeChange}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.download}>
|
||||
<Link
|
||||
color='primary'
|
||||
onClick={() => {
|
||||
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
|
||||
</Link>
|
||||
</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
|
|
@ -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,
|
||||
buttonIcon: {
|
||||
margin: 'auto'
|
||||
margin: 'auto',
|
||||
'& svg': {
|
||||
overflow: 'visible'
|
||||
}
|
||||
},
|
||||
buttonIconActive: {} // required to extend primary
|
||||
}
|
||||
|
|
|
|||
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 ActionButton from './ActionButton'
|
||||
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 { 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 (
|
||||
<>
|
||||
<Tr className={classnames(className)} {...props}>
|
||||
{columns.map((col, idx) => (
|
||||
<Td key={uuidv1()} size={sizes[idx]} className={col.className} textAlign={col.textAlign}>{col.value}</Td>
|
||||
<Tr className={classnames(classes.row, className)} {...props}>
|
||||
{columns.slice(0, -1).map((col, idx) => (
|
||||
<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}>
|
||||
{expanded && <ExpandOpenIcon />}
|
||||
{!expanded && <ExpandClosedIcon />}
|
||||
</button></Td>
|
||||
</Tr>
|
||||
<Tr className={classnames(detailsRowClasses)}>
|
||||
<Td size={mainWidth}>
|
||||
{details}
|
||||
</button>
|
||||
</Td>
|
||||
</Tr>
|
||||
{expanded && (
|
||||
<Tr className={classes.detailsRow}>
|
||||
<Td size={mainWidth}>
|
||||
{details}
|
||||
</Td>
|
||||
</Tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/* 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 (
|
||||
<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 (
|
||||
<Table className={classnames(className)}>
|
||||
<THead>
|
||||
{headers.map((header, idx) => (
|
||||
<Th key={uuidv1()} size={sizes[idx]} className={header.className} textAlign={header.textAlign}>{header.value}</Th>
|
||||
))}
|
||||
</THead>
|
||||
<TBody>
|
||||
{rowStates && rowStates.map((r, idx) => {
|
||||
const row = rows[idx]
|
||||
|
||||
return (
|
||||
<ExpRow
|
||||
key={uuidv1()}
|
||||
id={r.id}
|
||||
columns={row.columns}
|
||||
details={row.details}
|
||||
sizes={sizes}
|
||||
expanded={r.expanded}
|
||||
className={row.className}
|
||||
expandRow={expandRow}
|
||||
error={row.error}
|
||||
errorMessage={row.errorMessage}
|
||||
<>
|
||||
<div>
|
||||
<THead>
|
||||
{rows[0].columns.map((c, idx) => (
|
||||
<Th key={idx} size={c.size} className={c.className} textAlign={c.textAlign}>{c.name}</Th>
|
||||
))}
|
||||
</THead>
|
||||
</div>
|
||||
<div style={{ flex: '1 1 auto' }}>
|
||||
<AutoSizer disableWidth>
|
||||
{({ height }) => (
|
||||
<List
|
||||
{...props}
|
||||
height={height}
|
||||
width={mainWidth}
|
||||
rowCount={rows.length}
|
||||
rowHeight={cache.rowHeight}
|
||||
rowRenderer={rowRenderer}
|
||||
overscanRowCount={50}
|
||||
deferredMeasurementCache={cache}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</TBody>
|
||||
</Table>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,13 +28,15 @@ const Label = withStyles({
|
|||
}
|
||||
})(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 (
|
||||
<>
|
||||
{labels && (
|
||||
{options && (
|
||||
<MaterialRadioGroup aria-label={ariaLabel} name={name} value={value} onChange={onChange} className={classnames(className)}>
|
||||
{labels.map((label, idx) => (
|
||||
<Label key={idx} value={idx} control={<GreenRadio />} label={label} />
|
||||
{options.map((options, idx) => (
|
||||
<Label key={idx} value={options.value} control={<GreenRadio />} label={options.label} />
|
||||
))}
|
||||
</MaterialRadioGroup>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ export default {
|
|||
},
|
||||
select: {
|
||||
width: WIDTH,
|
||||
zIndex: 1000,
|
||||
zIndex: 2,
|
||||
'& label': {
|
||||
extend: label1,
|
||||
color: offColor,
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<td className={classnames(styles, className)} {...props}>
|
||||
<td colSpan={colspan} className={classnames(styles, className)} {...props}>
|
||||
{children}
|
||||
</td>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<th {...props} className={classnames(classes.th, className)}>
|
||||
<th {...props} className={classnames(styles, className)}>
|
||||
{children}
|
||||
</th>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
<LogsDowloaderPopover
|
||||
title='Download logs'
|
||||
name='server-logs'
|
||||
id={id}
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
logsResponse={logsResponse}
|
||||
onClose={handleCloseRangePicker}
|
||||
logs={logsResponse.data.logs}
|
||||
getTimestamp={(log) => log.timestamp}
|
||||
/>
|
||||
<SimpleButton className={classes.button} disabled={loading} onClick={sendSnapshot}>
|
||||
Share with Lamassu
|
||||
|
|
|
|||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 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 = () => (
|
|||
<Route path='/maintenance/logs' component={Logs} />
|
||||
<Route path='/maintenance/funding' component={Funding} />
|
||||
<Route path='/maintenance/server-logs' component={ServerLogs} />
|
||||
<Route path='/transactions' component={Transactions} />
|
||||
</Switch>
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,4 @@
|
|||
<line x1="11.7857143" y1="7.2" x2="18.8571429" y2="7.2" id="Stroke-4" stroke="#5F668A" stroke-width="1.6"></line>
|
||||
<polygon id="Stroke-5" stroke="#5F668A" stroke-width="1.6" points="3.14285714 11.2 8.64285714 11.2 8.64285714 4 3.14285714 4"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 975 B |
|
|
@ -9,4 +9,4 @@
|
|||
<line x1="11.7857143" y1="7.2" x2="18.8571429" y2="7.2" id="Stroke-4" stroke="#FF584A" stroke-width="1.6"></line>
|
||||
<polygon id="Stroke-5" stroke="#FF584A" stroke-width="1.6" points="3.14285714 11.2 8.64285714 11.2 8.64285714 4 3.14285714 4"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 977 B |
|
|
@ -9,4 +9,4 @@
|
|||
<line x1="11.7857143" y1="7.2" x2="18.8571429" y2="7.2" id="Stroke-4" stroke="#FFFFFF" stroke-width="1.6"></line>
|
||||
<polygon id="Stroke-5" stroke="#FFFFFF" stroke-width="1.6" points="3.14285714 11.2 8.64285714 11.2 8.64285714 4 3.14285714 4"></polygon>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 974 B After Width: | Height: | Size: 975 B |
|
|
@ -13,4 +13,4 @@
|
|||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 984 B After Width: | Height: | Size: 985 B |
|
|
@ -6,4 +6,4 @@
|
|||
<g id="icon/ID/phone/comet" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.47150618,12.52898 C9.939556,15.9970298 13.7804112,16.1146315 15.4756355,15.9586292 C16.0220434,15.9090285 16.5308507,15.6578249 16.9188563,15.2698193 L19.0004862,13.1881894 L17.0220577,11.210561 L15.0436293,10.5505516 L13.7244104,11.8697705 C13.7244104,11.8697705 12.4059914,13.1881894 9.10914407,9.89054208 C5.81229671,6.59449473 7.13071565,5.27527578 7.13071565,5.27527578 L8.4499346,3.95605683 L7.78992512,1.97842842 L5.81229671,0 L3.73066681,2.0816299 C3.34186123,2.46963548 3.09145763,2.97844279 3.04105691,3.52485063 C2.88585468,5.22007499 3.00345637,9.06013015 6.47150618,12.52898 Z" id="Stroke-1-Copy" stroke="#5F668A" stroke-width="1.6"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -6,4 +6,4 @@
|
|||
<g id="icon/ID/phone/tomato" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.47150618,12.52898 C9.939556,15.9970298 13.7804112,16.1146315 15.4756355,15.9586292 C16.0220434,15.9090285 16.5308507,15.6578249 16.9188563,15.2698193 L19.0004862,13.1881894 L17.0220577,11.210561 L15.0436293,10.5505516 L13.7244104,11.8697705 C13.7244104,11.8697705 12.4059914,13.1881894 9.10914407,9.89054208 C5.81229671,6.59449473 7.13071565,5.27527578 7.13071565,5.27527578 L8.4499346,3.95605683 L7.78992512,1.97842842 L5.81229671,0 L3.73066681,2.0816299 C3.34186123,2.46963548 3.09145763,2.97844279 3.04105691,3.52485063 C2.88585468,5.22007499 3.00345637,9.06013015 6.47150618,12.52898 Z" id="Stroke-1-Copy" stroke="#FF584A" stroke-width="1.6"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -6,4 +6,4 @@
|
|||
<g id="icon/ID/phone/white" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.47150618,12.52898 C9.939556,15.9970298 13.7804112,16.1146315 15.4756355,15.9586292 C16.0220434,15.9090285 16.5308507,15.6578249 16.9188563,15.2698193 L19.0004862,13.1881894 L17.0220577,11.210561 L15.0436293,10.5505516 L13.7244104,11.8697705 C13.7244104,11.8697705 12.4059914,13.1881894 9.10914407,9.89054208 C5.81229671,6.59449473 7.13071565,5.27527578 7.13071565,5.27527578 L8.4499346,3.95605683 L7.78992512,1.97842842 L5.81229671,0 L3.73066681,2.0816299 C3.34186123,2.46963548 3.09145763,2.97844279 3.04105691,3.52485063 C2.88585468,5.22007499 3.00345637,9.06013015 6.47150618,12.52898 Z" id="Stroke-1-Copy" stroke="#FFFFFF" stroke-width="1.6"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -6,4 +6,4 @@
|
|||
<g id="icon/ID/phone/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.47150618,12.52898 C9.939556,15.9970298 13.7804112,16.1146315 15.4756355,15.9586292 C16.0220434,15.9090285 16.5308507,15.6578249 16.9188563,15.2698193 L19.0004862,13.1881894 L17.0220577,11.210561 L15.0436293,10.5505516 L13.7244104,11.8697705 C13.7244104,11.8697705 12.4059914,13.1881894 9.10914407,9.89054208 C5.81229671,6.59449473 7.13071565,5.27527578 7.13071565,5.27527578 L8.4499346,3.95605683 L7.78992512,1.97842842 L5.81229671,0 L3.73066681,2.0816299 C3.34186123,2.46963548 3.09145763,2.97844279 3.04105691,3.52485063 C2.88585468,5.22007499 3.00345637,9.06013015 6.47150618,12.52898 Z" id="Stroke-1-Copy" stroke="#1B2559" stroke-width="1.6"></path>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
|
@ -1,13 +1,13 @@
|
|||
<?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">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>icon/actions/expand/closed</title>
|
||||
<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 56.3 (81716) - https://sketch.com -->
|
||||
<title>icon/action/expand/closed</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="icon/actions/expand/closed" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="show/closed-copy-3" stroke="#1B2559" stroke-width="1.5">
|
||||
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<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-Copy" cx="8" cy="2" r="2"></circle>
|
||||
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 733 B After Width: | Height: | Size: 766 B |
|
|
@ -1,13 +1,13 @@
|
|||
<?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">
|
||||
<!-- Generator: Sketch 60.1 (88133) - https://sketch.com -->
|
||||
<title>icon/actions/expand/open</title>
|
||||
<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 56.3 (81716) - https://sketch.com -->
|
||||
<title>icon/action/expand/open</title>
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="icon/actions/expand/open" 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="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<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-Copy" cx="8" cy="2" r="2"></circle>
|
||||
<circle id="Oval-4-Copy-2" cx="2" cy="2" r="2"></circle>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 777 B |
|
|
@ -7,4 +7,4 @@
|
|||
<circle id="Oval" stroke="#FFFFFF" cx="6" cy="6" r="6"></circle>
|
||||
<polyline id="Stroke-13" stroke="#FFFFFF" stroke-linecap="round" stroke-linejoin="round" points="4 6.66666667 5 8 8 4"></polyline>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 665 B After Width: | Height: | Size: 666 B |
|
|
@ -9,4 +9,4 @@
|
|||
</g>
|
||||
<polyline id="Stroke-13" stroke="#1B2559" stroke-linecap="round" stroke-linejoin="round" points="4 6.66666667 5 8 8 4"></polyline>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 709 B After Width: | Height: | Size: 710 B |
|
|
@ -9,4 +9,4 @@
|
|||
<line x1="0" y1="0" x2="10" y2="10" id="Stroke-3"></line>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 731 B After Width: | Height: | Size: 732 B |
|
|
@ -9,4 +9,4 @@
|
|||
<line x1="0" y1="0" x2="12" y2="12" id="Stroke-3"></line>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 710 B After Width: | Height: | Size: 711 B |
|
|
@ -9,4 +9,4 @@
|
|||
<polyline id="Stroke-3" points="4.2002 9.601 3.0002 10.8 4.2002 12"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 905 B After Width: | Height: | Size: 906 B |
|
|
@ -9,4 +9,4 @@
|
|||
<polyline id="Stroke-3" points="4.2002 9.601 3.0002 10.8 4.2002 12"></polyline>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 907 B After Width: | Height: | Size: 908 B |