feat: created the search component
style: added spec styles fix: fixed font color on search input style: added box-shadow to the search component feat: added local search functionality to the search component feat: integrated search component into the transactions page feat: allow multiple filter selection on the search component fix: let the user select only one filter for each type feat: added chips for the selected filters on the transactions page feat: added the remove function on the filter chips style: styled items according to spec refactor: simplified search component (moved logic to the outside) feat: added transaction filters to the gql query feat: added a 'clear all filters' button feat: added a filters query feat: added a gql query for the transaction filters fix: fixed the transactions gql query so it haves the same options as the transaction filters feat: added a 'loading' feature to the search box (shown while loading the filters) fix: fetch transactions and filters separately in the transactions page fix: style export fix: packages fix: transaction conflicts
This commit is contained in:
parent
468f2cb28b
commit
852bf7b089
14 changed files with 1343 additions and 888 deletions
|
|
@ -16,7 +16,16 @@ const cashInLow = require('./cash-in-low')
|
|||
const PENDING_INTERVAL = '60 minutes'
|
||||
const MAX_PENDING = 10
|
||||
|
||||
module.exports = { post, monitorPending, cancel, PENDING_INTERVAL }
|
||||
const TRANSACTION_STATES = `
|
||||
case
|
||||
when operator_completed then 'Cancelled'
|
||||
when error is not null then 'Error'
|
||||
when send_confirmed then 'Sent'
|
||||
when ((not send_confirmed) and (created <= now() - interval '${PENDING_INTERVAL}')) then 'Expired'
|
||||
else 'Pending'
|
||||
end`
|
||||
|
||||
module.exports = {post, monitorPending, cancel, PENDING_INTERVAL, TRANSACTION_STATES}
|
||||
|
||||
function post (machineTx, pi) {
|
||||
return cashInAtomic.atomic(machineTx, pi)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,15 @@ const BN = require('../bn')
|
|||
|
||||
const REDEEMABLE_AGE = T.day
|
||||
|
||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE }
|
||||
const CASH_OUT_TRANSACTION_STATES = `
|
||||
case
|
||||
when error is not null then 'Error'
|
||||
when dispense then 'Success'
|
||||
when (extract(epoch from (now() - greatest(created, confirmed_at))) * 1000) >= ${REDEEMABLE_AGE} then 'Expired'
|
||||
else 'Pending'
|
||||
end`
|
||||
|
||||
module.exports = { redeemableTxs, toObj, toDb, REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES }
|
||||
|
||||
const mapValuesWithKey = _.mapValues.convert({cap: false})
|
||||
|
||||
|
|
|
|||
30
lib/new-admin/filters.js
Normal file
30
lib/new-admin/filters.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const db = require('../db')
|
||||
const cashInTx = require('../cash-in/cash-in-tx')
|
||||
const { CASH_OUT_TRANSACTION_STATES } = require('../cash-out/cash-out-helper')
|
||||
|
||||
function transaction() {
|
||||
const sql = `select distinct * from (
|
||||
select 'type' as type, 'Cash In' as value union
|
||||
select 'type' as type, 'Cash Out' as value union
|
||||
select 'machine' as type, name as value from devices d inner join cash_in_txs t on d.device_id = t.device_id union
|
||||
select 'machine' as type, name as value from devices d inner join cash_out_txs t on d.device_id = t.device_id union
|
||||
select 'customer' as type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') as value
|
||||
from customers c inner join cash_in_txs t on c.id = t.customer_id
|
||||
where c.id_card_data::json->>'firstName' is not null or c.id_card_data::json->>'lastName' is not null union
|
||||
select 'customer' as type, concat(id_card_data::json->>'firstName', ' ', id_card_data::json->>'lastName') as value
|
||||
from customers c inner join cash_out_txs t on c.id = t.customer_id
|
||||
where c.id_card_data::json->>'firstName' is not null or c.id_card_data::json->>'lastName' is not null union
|
||||
select 'fiat' as type, fiat_code as value from cash_in_txs union
|
||||
select 'fiat' as type, fiat_code as value from cash_out_txs union
|
||||
select 'crypto' as type, crypto_code as value from cash_in_txs union
|
||||
select 'crypto' as type, crypto_code as value from cash_out_txs union
|
||||
select 'address' as type, to_address as value from cash_in_txs union
|
||||
select 'address' as type, to_address as value from cash_in_txs union
|
||||
select 'status' as type, ${cashInTx.TRANSACTION_STATES} as value from cash_in_txs union
|
||||
select 'status' as type, ${CASH_OUT_TRANSACTION_STATES} as value from cash_out_txs
|
||||
) f`
|
||||
|
||||
return db.any(sql)
|
||||
}
|
||||
|
||||
module.exports = { transaction }
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
const DataLoader = require('dataloader')
|
||||
const { parseAsync } = require('json2csv')
|
||||
|
||||
const filters = require('../../filters')
|
||||
const transactions = require('../../services/transactions')
|
||||
const anonymous = require('../../../constants').anonymousCustomer
|
||||
|
||||
|
|
@ -25,14 +26,16 @@ const resolvers = {
|
|||
isAnonymous: parent => (parent.customerId === anonymous.uuid)
|
||||
},
|
||||
Query: {
|
||||
transactions: (...[, { from, until, limit, offset, deviceId }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId),
|
||||
transactionsCsv: (...[, { from, until, limit, offset }]) =>
|
||||
transactions.batch(from, until, limit, offset).then(data => parseAsync(data, { fields: txLogFields })),
|
||||
transactions: (...[, { from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||
transactions.batch(from, until, limit, offset, deviceId, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status),
|
||||
transactionsCsv: (...[, { from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status }]) =>
|
||||
transactions.batch(from, until, limit, offset, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status)
|
||||
.then(data => parseAsync(data, {fields: tx_logFields})),
|
||||
transactionCsv: (...[, { id, txClass }]) =>
|
||||
transactions.getTx(id, txClass).then(parseAsync),
|
||||
txAssociatedDataCsv: (...[, { id, txClass }]) =>
|
||||
transactions.getTxAssociatedData(id, txClass).then(parseAsync)
|
||||
transactions.getTxAssociatedData(id, txClass).then(parseAsync),
|
||||
transactionFilters: () => filters.transaction()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,11 +45,17 @@ const typeDef = gql`
|
|||
discount: Int
|
||||
}
|
||||
|
||||
type Filter {
|
||||
type: String
|
||||
value: String
|
||||
}
|
||||
|
||||
type Query {
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int): String @auth
|
||||
transactions(from: Date, until: Date, limit: Int, offset: Int, deviceId: ID, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): [Transaction] @auth
|
||||
transactionsCsv(from: Date, until: Date, limit: Int, offset: Int, txClass: String, machineName: String, customerName: String, fiatCode: String, cryptoCode: String, toAddress: String, status: String): String @auth
|
||||
transactionCsv(id: ID, txClass: String): String @auth
|
||||
txAssociatedDataCsv(id: ID, txClass: String): String @auth
|
||||
transactionFilters: [Filter] @auth
|
||||
}
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ 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 { REDEEMABLE_AGE, CASH_OUT_TRANSACTION_STATES } = require('../../cash-out/cash-out-helper')
|
||||
|
||||
const NUM_RESULTS = 1000
|
||||
|
||||
|
|
@ -24,7 +24,20 @@ function addNames (txs) {
|
|||
|
||||
const camelize = _.mapKeys(_.camelCase)
|
||||
|
||||
function batch (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0, id = null) {
|
||||
function batch (
|
||||
from = new Date(0).toISOString(),
|
||||
until = new Date().toISOString(),
|
||||
limit = null,
|
||||
offset = 0,
|
||||
id = null,
|
||||
txClass = null,
|
||||
machineName = null,
|
||||
customerName = null,
|
||||
fiatCode = null,
|
||||
cryptoCode = null,
|
||||
toAddress = null,
|
||||
status = null
|
||||
) {
|
||||
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
|
||||
|
||||
const cashInSql = `select 'cashIn' as tx_class, txs.*,
|
||||
|
|
@ -32,15 +45,23 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
|||
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,
|
||||
concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') 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
|
||||
from (select *, ${cashInTx.TRANSACTION_STATES} as txStatus from cash_in_txs) as txs
|
||||
left outer join customers c on txs.customer_id = c.id
|
||||
inner join devices d on txs.device_id = d.device_id
|
||||
where txs.created >= $2 and txs.created <= $3 ${
|
||||
id !== null ? `and txs.device_id = $6` : ``
|
||||
}
|
||||
and ($7 is null or $7 = 'Cash In')
|
||||
and ($8 is null or d.name = $8)
|
||||
and ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||
and ($10 is null or txs.fiat_code = $10)
|
||||
and ($11 is null or txs.crypto_code = $11)
|
||||
and ($12 is null or txs.to_address = $12)
|
||||
and ($13 is null or txs.txStatus = $13)
|
||||
order by created desc limit $4 offset $5`
|
||||
|
||||
const cashOutSql = `select 'cashOut' as tx_class,
|
||||
|
|
@ -50,22 +71,30 @@ function batch (from = new Date(0).toISOString(), until = new Date().toISOString
|
|||
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,
|
||||
concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') 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) >= $1 as expired
|
||||
from cash_out_txs txs
|
||||
from (select *, ${CASH_OUT_TRANSACTION_STATES} as txStatus 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
|
||||
inner join devices d on txs.device_id = d.device_id
|
||||
where txs.created >= $2 and txs.created <= $3 ${
|
||||
id !== null ? `and txs.device_id = $6` : ``
|
||||
}
|
||||
and ($7 is null or $7 = 'Cash Out')
|
||||
and ($8 is null or d.name = $8)
|
||||
and ($9 is null or concat(c.id_card_data::json->>'firstName', ' ', c.id_card_data::json->>'lastName') = $9)
|
||||
and ($10 is null or txs.fiat_code = $10)
|
||||
and ($11 is null or txs.crypto_code = $11)
|
||||
and ($12 is null or txs.to_address = $12)
|
||||
and ($13 is null or txs.txStatus = $13)
|
||||
order by created desc limit $4 offset $5`
|
||||
|
||||
return Promise.all([
|
||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id]),
|
||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id])
|
||||
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status]),
|
||||
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset, id, txClass, machineName, customerName, fiatCode, cryptoCode, toAddress, status])
|
||||
])
|
||||
.then(packager)
|
||||
}
|
||||
|
|
|
|||
719
new-lamassu-admin/package-lock.json
generated
719
new-lamassu-admin/package-lock.json
generated
File diff suppressed because it is too large
Load diff
91
new-lamassu-admin/src/components/SearchBox.js
Normal file
91
new-lamassu-admin/src/components/SearchBox.js
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import InputBase from '@material-ui/core/InputBase'
|
||||
import Paper from '@material-ui/core/Paper'
|
||||
import { makeStyles } from '@material-ui/core/styles'
|
||||
import MAutocomplete from '@material-ui/lab/Autocomplete'
|
||||
import classnames from 'classnames'
|
||||
import React, { memo, useState, useEffect } from 'react'
|
||||
|
||||
import { P } from 'src/components/typography'
|
||||
import { ReactComponent as SearchIcon } from 'src/styling/icons/circle buttons/search/zodiac.svg'
|
||||
|
||||
import styles from './SearchBox.styles'
|
||||
|
||||
const useStyles = makeStyles(styles)
|
||||
|
||||
const SearchBox = memo(
|
||||
({
|
||||
loading = false,
|
||||
filters = [],
|
||||
options = [],
|
||||
inputPlaceholder = '',
|
||||
size,
|
||||
onChange,
|
||||
...props
|
||||
}) => {
|
||||
const classes = useStyles({ size })
|
||||
|
||||
const [popupOpen, setPopupOpen] = useState(false)
|
||||
|
||||
const inputClasses = {
|
||||
[classes.input]: true,
|
||||
[classes.inputWithPopup]: popupOpen
|
||||
}
|
||||
|
||||
const innerOnChange = filters => onChange(filters)
|
||||
|
||||
// eslint-disable-next-line
|
||||
useEffect(() => innerOnChange(filters), [filters])
|
||||
|
||||
return (
|
||||
<MAutocomplete
|
||||
loading={loading}
|
||||
classes={{ option: classes.autocomplete }}
|
||||
value={filters}
|
||||
options={options}
|
||||
getOptionLabel={it => it.value}
|
||||
renderOption={it => (
|
||||
<div className={classes.item}>
|
||||
<P className={classes.itemLabel}>{it.value}</P>
|
||||
<P className={classes.itemType}>{it.type}</P>
|
||||
</div>
|
||||
)}
|
||||
autoHighlight
|
||||
disableClearable
|
||||
clearOnEscape
|
||||
multiple
|
||||
filterSelectedOptions
|
||||
getOptionSelected={(option, value) => option.type === value.type}
|
||||
PaperComponent={({ children }) => (
|
||||
<Paper elevation={0} className={classes.popup}>
|
||||
<div className={classes.separator} />
|
||||
{children}
|
||||
</Paper>
|
||||
)}
|
||||
renderInput={params => {
|
||||
return (
|
||||
<InputBase
|
||||
ref={params.InputProps.ref}
|
||||
{...params}
|
||||
className={classnames(inputClasses)}
|
||||
startAdornment={<SearchIcon className={classes.iconButton} />}
|
||||
placeholder={inputPlaceholder}
|
||||
inputProps={{
|
||||
className: classes.bold,
|
||||
classes: {
|
||||
root: classes.size
|
||||
},
|
||||
...params.inputProps
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
onOpen={() => setPopupOpen(true)}
|
||||
onClose={() => setPopupOpen(false)}
|
||||
onChange={(_, filters) => innerOnChange(filters)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
export default SearchBox
|
||||
78
new-lamassu-admin/src/components/SearchBox.styles.js
Normal file
78
new-lamassu-admin/src/components/SearchBox.styles.js
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
import baseButtonStyles from 'src/components/buttons/BaseButton.styles'
|
||||
import { bySize, bold } from 'src/styling/helpers'
|
||||
import { zircon, comet, primaryColor } from 'src/styling/variables'
|
||||
|
||||
const { baseButton } = baseButtonStyles
|
||||
|
||||
const searchBoxBorderRadius = baseButton.height / 2
|
||||
const searchBoxHeight = 32
|
||||
const popupBorderRadiusFocus = baseButton.height / 4
|
||||
|
||||
const hoverColor = 'rgba(0, 0, 0, 0.08)'
|
||||
const boxShadow = `0 4px 4px 0 ${hoverColor}`
|
||||
|
||||
const styles = {
|
||||
size: ({ size }) => ({
|
||||
marginTop: size === 'lg' ? 0 : 2,
|
||||
...bySize(size)
|
||||
}),
|
||||
bold,
|
||||
autocomplete: {
|
||||
'&[data-focus="true"]': {
|
||||
backgroundColor: hoverColor
|
||||
}
|
||||
},
|
||||
popup: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
borderRadius: [[0, 0, popupBorderRadiusFocus, popupBorderRadiusFocus]],
|
||||
backgroundColor: zircon,
|
||||
boxShadow
|
||||
},
|
||||
separator: {
|
||||
width: '88%',
|
||||
height: 1,
|
||||
margin: '0 auto',
|
||||
border: 'solid 0.5px',
|
||||
borderColor: comet
|
||||
},
|
||||
item: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
width: '100%',
|
||||
height: 36,
|
||||
alignItems: 'center'
|
||||
},
|
||||
itemLabel: {
|
||||
margin: [0],
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
},
|
||||
itemType: {
|
||||
marginLeft: 'auto',
|
||||
fontSize: 12,
|
||||
color: comet,
|
||||
margin: [0]
|
||||
},
|
||||
input: {
|
||||
display: 'flex',
|
||||
flex: 1,
|
||||
width: 273,
|
||||
padding: [[8, 12]],
|
||||
alignItems: 'center',
|
||||
height: searchBoxHeight,
|
||||
borderRadius: searchBoxBorderRadius,
|
||||
backgroundColor: zircon,
|
||||
color: primaryColor
|
||||
},
|
||||
inputWithPopup: {
|
||||
borderRadius: [[popupBorderRadiusFocus, popupBorderRadiusFocus, 0, 0]],
|
||||
boxShadow
|
||||
},
|
||||
iconButton: {
|
||||
marginRight: 12
|
||||
}
|
||||
}
|
||||
|
||||
export default styles
|
||||
|
|
@ -4,22 +4,27 @@ import BigNumber from 'bignumber.js'
|
|||
import gql from 'graphql-tag'
|
||||
import { utils as coinUtils } from 'lamassu-coins'
|
||||
import * as R from 'ramda'
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { useHistory } from 'react-router-dom'
|
||||
|
||||
import Chip from 'src/components/Chip'
|
||||
import LogsDowloaderPopover from 'src/components/LogsDownloaderPopper'
|
||||
import SearchBox from 'src/components/SearchBox'
|
||||
import Title from 'src/components/Title'
|
||||
import DataTable from 'src/components/tables/DataTable'
|
||||
import { P } from 'src/components/typography'
|
||||
import { ReactComponent as CloseIcon } from 'src/styling/icons/action/close/zodiac.svg'
|
||||
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
|
||||
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
|
||||
import { ReactComponent as CustomerLinkIcon } from 'src/styling/icons/month arrows/right.svg'
|
||||
import { formatDate } from 'src/utils/timezones'
|
||||
|
||||
import DetailsRow from './DetailsCard'
|
||||
import { mainStyles } from './Transactions.styles'
|
||||
import { getStatus } from './helper'
|
||||
import { mainStyles, chipStyles } from './Transactions.styles'
|
||||
import { getStatus /*, getStatusProperties */ } from './helper'
|
||||
|
||||
const useStyles = makeStyles(mainStyles)
|
||||
const useChipStyles = makeStyles(chipStyles)
|
||||
|
||||
const NUM_LOG_RESULTS = 1000
|
||||
|
||||
|
|
@ -35,9 +40,40 @@ const GET_TRANSACTIONS_CSV = gql`
|
|||
}
|
||||
`
|
||||
|
||||
const GET_TRANSACTION_FILTERS = gql`
|
||||
query filters {
|
||||
transactionFilters {
|
||||
type
|
||||
value
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const GET_TRANSACTIONS = gql`
|
||||
query transactions($limit: Int, $from: DateTime, $until: DateTime) {
|
||||
transactions(limit: $limit, from: $from, until: $until) {
|
||||
query transactions(
|
||||
$limit: Int
|
||||
$from: DateTime
|
||||
$until: DateTime
|
||||
$txClass: String
|
||||
$machineName: String
|
||||
$customerName: String
|
||||
$fiatCode: String
|
||||
$cryptoCode: String
|
||||
$toAddress: String
|
||||
$status: String
|
||||
) {
|
||||
transactions(
|
||||
limit: $limit
|
||||
from: $from
|
||||
until: $until
|
||||
txClass: $txClass
|
||||
machineName: $machineName
|
||||
customerName: $customerName
|
||||
fiatCode: $fiatCode
|
||||
cryptoCode: $cryptoCode
|
||||
toAddress: $toAddress
|
||||
status: $status
|
||||
) {
|
||||
id
|
||||
txClass
|
||||
txHash
|
||||
|
|
@ -72,12 +108,23 @@ const GET_TRANSACTIONS = gql`
|
|||
const Transactions = () => {
|
||||
const classes = useStyles()
|
||||
const history = useHistory()
|
||||
const { data: txResponse, loading } = useQuery(GET_TRANSACTIONS, {
|
||||
variables: {
|
||||
limit: NUM_LOG_RESULTS
|
||||
},
|
||||
const chipClasses = useChipStyles()
|
||||
|
||||
const [filters, setFilters] = useState([])
|
||||
const { data: filtersResponse, loading: loadingFilters } = useQuery(
|
||||
GET_TRANSACTION_FILTERS
|
||||
)
|
||||
const [filteredTransactions, setFilteredTransactions] = useState([])
|
||||
const [variables, setVariables] = useState({ limit: NUM_LOG_RESULTS })
|
||||
const { data: txResponse, loading: loadingTransactions, refetch } = useQuery(
|
||||
GET_TRANSACTIONS,
|
||||
{
|
||||
variables,
|
||||
onCompleted: data =>
|
||||
setFilteredTransactions(R.path(['transactions'])(data)),
|
||||
pollInterval: 10000
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
const { data: configResponse, configLoading } = useQuery(GET_DATA)
|
||||
const timezone = R.path(['config', 'locale_timezone'], configResponse)
|
||||
|
|
@ -167,11 +214,51 @@ const Transactions = () => {
|
|||
}
|
||||
]
|
||||
|
||||
const onFilterChange = filters => {
|
||||
const filtersObject = R.compose(
|
||||
R.mergeAll,
|
||||
R.map(f => ({
|
||||
[f.type]: f.value
|
||||
}))
|
||||
)(filters)
|
||||
|
||||
setFilters(filters)
|
||||
|
||||
setVariables({
|
||||
limit: NUM_LOG_RESULTS,
|
||||
txClass: filtersObject.type,
|
||||
machineName: filtersObject.machine,
|
||||
customerName: filtersObject.customer,
|
||||
fiatCode: filtersObject.fiat,
|
||||
cryptoCode: filtersObject.crypto,
|
||||
toAddress: filtersObject.address,
|
||||
status: filtersObject.status
|
||||
})
|
||||
|
||||
refetch && refetch()
|
||||
}
|
||||
|
||||
const onFilterDelete = filter =>
|
||||
setFilters(
|
||||
R.filter(f => !R.whereEq(R.pick(['type', 'value'], f), filter))(filters)
|
||||
)
|
||||
|
||||
const filterOptions = R.path(['transactionFilters'])(filtersResponse)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classes.titleWrapper}>
|
||||
<div className={classes.titleAndButtonsContainer}>
|
||||
<Title>Transactions</Title>
|
||||
<div className={classes.buttonsWrapper}>
|
||||
<SearchBox
|
||||
loading={loadingFilters}
|
||||
filters={filters}
|
||||
options={filterOptions}
|
||||
inputPlaceholder={'Search Transactions'}
|
||||
onChange={onFilterChange}
|
||||
/>
|
||||
</div>
|
||||
{txResponse && (
|
||||
<div className={classes.buttonsWrapper}>
|
||||
<LogsDowloaderPopover
|
||||
|
|
@ -194,11 +281,33 @@ const Transactions = () => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{filters.length > 0 && (
|
||||
<>
|
||||
<P className={classes.text}>{'Filters:'}</P>
|
||||
<div>
|
||||
{filters.map((f, idx) => (
|
||||
<Chip
|
||||
key={idx}
|
||||
classes={chipClasses}
|
||||
label={`${f.type}: ${f.value}`}
|
||||
onDelete={() => onFilterDelete(f)}
|
||||
deleteIcon={<CloseIcon className={classes.button} />}
|
||||
/>
|
||||
))}
|
||||
<Chip
|
||||
classes={chipClasses}
|
||||
label={`Delete filters`}
|
||||
onDelete={() => setFilters([])}
|
||||
deleteIcon={<CloseIcon className={classes.button} />}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<DataTable
|
||||
loading={loading && configLoading}
|
||||
loading={loadingTransactions && configLoading}
|
||||
emptyText="No transactions so far"
|
||||
elements={elements}
|
||||
data={R.path(['transactions'])(txResponse)}
|
||||
data={filteredTransactions}
|
||||
Details={DetailsRow}
|
||||
expandable
|
||||
rowSize="sm"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,15 @@
|
|||
import typographyStyles from 'src/components/typography/styles'
|
||||
import baseStyles from 'src/pages/Logs.styles'
|
||||
import { offColor, white } from 'src/styling/variables'
|
||||
import {
|
||||
offColor,
|
||||
white,
|
||||
primaryColor,
|
||||
zircon,
|
||||
smallestFontSize,
|
||||
inputFontFamily,
|
||||
inputFontWeight,
|
||||
spacer
|
||||
} from 'src/styling/variables'
|
||||
|
||||
const { label1, mono, p } = typographyStyles
|
||||
const { titleWrapper, titleAndButtonsContainer, buttonsWrapper } = baseStyles
|
||||
|
|
@ -64,6 +73,10 @@ const mainStyles = {
|
|||
titleWrapper,
|
||||
titleAndButtonsContainer,
|
||||
buttonsWrapper,
|
||||
text: {
|
||||
marginTop: 0,
|
||||
marginBottom: 0
|
||||
},
|
||||
headerLabels: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
|
|
@ -102,7 +115,35 @@ const mainStyles = {
|
|||
marginLeft: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5
|
||||
},
|
||||
button: {
|
||||
width: 8,
|
||||
height: 8,
|
||||
marginLeft: 8
|
||||
}
|
||||
}
|
||||
|
||||
export { cpcStyles, detailsRowStyles, labelStyles, mainStyles }
|
||||
const chipStyles = {
|
||||
root: {
|
||||
borderRadius: spacer / 2,
|
||||
marginTop: spacer / 2,
|
||||
marginRight: spacer / 4,
|
||||
marginBottom: spacer / 2,
|
||||
marginLeft: spacer / 4,
|
||||
height: spacer * 3,
|
||||
backgroundColor: zircon,
|
||||
'&:hover, &:focus, &:active': {
|
||||
backgroundColor: zircon
|
||||
}
|
||||
},
|
||||
label: {
|
||||
fontSize: smallestFontSize,
|
||||
fontWeight: inputFontWeight,
|
||||
fontFamily: inputFontFamily,
|
||||
paddingRight: spacer / 2,
|
||||
paddingLeft: spacer / 2,
|
||||
color: primaryColor
|
||||
}
|
||||
}
|
||||
|
||||
export { cpcStyles, detailsRowStyles, labelStyles, mainStyles, chipStyles }
|
||||
|
|
|
|||
|
|
@ -24,4 +24,12 @@ const getStatusDetails = it => {
|
|||
return it.hasError ? it.hasError : null
|
||||
}
|
||||
|
||||
export { getStatus, getStatusDetails }
|
||||
const getStatusProperties = status => ({
|
||||
hasError: status === 'Error' || null,
|
||||
dispense: status === 'Success' || null,
|
||||
expired: status === 'Expired' || null,
|
||||
operatorCompleted: status === 'Cancelled' || null,
|
||||
sendConfirmed: status === 'Sent' || null
|
||||
})
|
||||
|
||||
export { getStatus, getStatusProperties, getStatusDetails }
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="20px" height="20px" viewBox="0 0 20 20" 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 -->
|
||||
<desc>Created with Sketch.</desc>
|
||||
<g id="icon/sf-small/search/zodiac" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M15.8635238,8.17028571 C15.8635238,12.4198095 12.4187619,15.8645714 8.1692381,15.8645714 C3.92066667,15.8645714 0.475904762,12.4198095 0.475904762,8.17028571 C0.475904762,3.9207619 3.92066667,0.476 8.1692381,0.476 C12.4187619,0.476 15.8635238,3.9207619 15.8635238,8.17028571 Z" id="Stroke-1" stroke="#1B2559" stroke-width="2"></path>
|
||||
<line x1="13.7035238" y1="13.7046667" x2="19.4844762" y2="19.485619" id="Stroke-3" stroke="#1B2559" stroke-width="2" stroke-linecap="round"></line>
|
||||
<title>icon/search/dark02</title>
|
||||
<g id="icon/search/dark02" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<g id="Group" transform="translate(1.000000, 1.000000)" stroke="#1B2559" stroke-width="2">
|
||||
<path d="M14.2771714,7.35325714 C14.2771714,11.1778286 11.1768857,14.2781143 7.35231429,14.2781143 C3.5286,14.2781143 0.428314286,11.1778286 0.428314286,7.35325714 C0.428314286,3.52868571 3.5286,0.4284 7.35231429,0.4284 C11.1768857,0.4284 14.2771714,3.52868571 14.2771714,7.35325714 Z" id="Stroke-1"></path>
|
||||
<line x1="12.3331714" y1="12.3342" x2="17.5360286" y2="17.5370571" id="Stroke-3" stroke-linecap="round"></line>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 913 B After Width: | Height: | Size: 888 B |
1007
package-lock.json
generated
1007
package-lock.json
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue