fix: added timestamp parameters for a date range on the gql queries for

machineLogs, serverLogs and transactions

feat: added optional limit and offset variables for the logs queries,
for filtering and pagination

feat: adapted the LogsDownloaderPopper to download the logs by whats set
on the filters

fix: improved code readability

fix: avoid errors when the range option is selected and no range is
actually selected
This commit is contained in:
Liordino Neto 2020-07-16 21:50:38 -03:00 committed by Josh Harvey
parent 37ea3a04c3
commit f641e605a4
7 changed files with 109 additions and 68 deletions

View file

@ -81,28 +81,31 @@ function getUnlimitedMachineLogs (deviceId, until = new Date().toISOString()) {
})) }))
} }
function getMachineLogs (deviceId, until = new Date().toISOString()) { function getMachineLogs (deviceId, until = new Date().toISOString(), limit = null, offset = 0) {
const sql = `select id, log_level, timestamp, message from logs const sql = `select id, log_level, timestamp, message from logs
where device_id=$1 where device_id=$1
and timestamp <= $3 and timestamp <= $2
order by timestamp desc, serial desc order by timestamp desc, serial desc
limit $2` limit $3
offset $4`
return Promise.all([db.any(sql, [ deviceId, NUM_RESULTS, until ]), getMachineName(deviceId)]) return Promise.all([db.any(sql, [ deviceId, until, limit, offset ]), getMachineName(deviceId)])
.then(([logs, machineName]) => ({ .then(([logs, machineName]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs), logs: _.map(_.mapKeys(_.camelCase), logs),
currentMachine: {deviceId, name: machineName} currentMachine: {deviceId, name: machineName}
})) }))
} }
function simpleGetMachineLogs (deviceId, until = new Date().toISOString()) { function simpleGetMachineLogs (deviceId, from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
const sql = `select id, log_level, timestamp, message from logs const sql = `select id, log_level, timestamp, message from logs
where device_id=$1 where device_id=$1
and timestamp >= $2
and timestamp <= $3 and timestamp <= $3
order by timestamp desc, serial desc order by timestamp desc, serial desc
limit $2` limit $4
offset $5`
return db.any(sql, [ deviceId, NUM_RESULTS, until ]) return db.any(sql, [ deviceId, from, until, limit, offset ])
.then(_.map(_.mapKeys(_.camelCase))) .then(_.map(_.mapKeys(_.camelCase)))
} }

View file

@ -195,12 +195,12 @@ const typeDefs = gql`
machines: [Machine] machines: [Machine]
customers: [Customer] customers: [Customer]
customer(customerId: ID!): Customer customer(customerId: ID!): Customer
machineLogs(deviceId: ID!): [MachineLog] machineLogs(deviceId: ID!, from: Date, until: Date, limit: Int, offset: Int): [MachineLog]
funding: [CoinFunds] funding: [CoinFunds]
serverVersion: String! serverVersion: String!
uptime: [ProcessStatus] uptime: [ProcessStatus]
serverLogs: [ServerLog] serverLogs(from: Date, until: Date, limit: Int, offset: Int): [ServerLog]
transactions: [Transaction] transactions(from: Date, until: Date, limit: Int, offset: Int): [Transaction]
accounts: JSONObject accounts: JSONObject
config: JSONObject config: JSONObject
} }
@ -250,11 +250,14 @@ const resolvers = {
customers: () => customers.getCustomersList(), customers: () => customers.getCustomersList(),
customer: (...[, { customerId }]) => customers.getCustomerById(customerId), customer: (...[, { customerId }]) => customers.getCustomerById(customerId),
funding: () => funding.getFunding(), funding: () => funding.getFunding(),
machineLogs: (...[, { deviceId }]) => logs.simpleGetMachineLogs(deviceId), machineLogs: (...[, { deviceId, from, until, limit, offset }]) =>
logs.simpleGetMachineLogs(deviceId, from, until, limit, offset),
serverVersion: () => serverVersion, serverVersion: () => serverVersion,
uptime: () => supervisor.getAllProcessInfo(), uptime: () => supervisor.getAllProcessInfo(),
serverLogs: () => serverLogs.getServerLogs(), serverLogs: (...[, { from, until, limit, offset }]) =>
transactions: () => transactions.batch(), serverLogs.getServerLogs(from, until, limit, offset),
transactions: (...[, { from, until, limit, offset }]) =>
transactions.batch(from, until, limit, offset),
config: () => settingsLoader.getConfig(), config: () => settingsLoader.getConfig(),
accounts: () => settingsLoader.getAccounts() accounts: () => settingsLoader.getAccounts()
}, },

View file

@ -5,12 +5,14 @@ const db = require('../db')
const NUM_RESULTS = 500 const NUM_RESULTS = 500
function getServerLogs (until = new Date().toISOString()) { function getServerLogs (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
const sql = `select id, log_level, timestamp, message from server_logs const sql = `select id, log_level, timestamp, message from server_logs
where timestamp >= $1 and timestamp <= $2
order by timestamp desc order by timestamp desc
limit $1` limit $3
offset $4`
return db.any(sql, [ NUM_RESULTS ]) return db.any(sql, [ from, until, limit, offset ])
.then(_.map(_.mapKeys(_.camelCase))) .then(_.map(_.mapKeys(_.camelCase)))
} }

View file

@ -23,9 +23,8 @@ function addNames (txs) {
const camelize = _.mapKeys(_.camelCase) const camelize = _.mapKeys(_.camelCase)
function batch () { function batch (from = new Date(0).toISOString(), until = new Date().toISOString(), limit = null, offset = 0) {
const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), const packager = _.flow(_.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
_.take(NUM_RESULTS), _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class, txs.*, const cashInSql = `select 'cashIn' as tx_class, txs.*,
c.phone as customer_phone, c.phone as customer_phone,
@ -38,7 +37,8 @@ function batch () {
((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired ((not txs.send_confirmed) and (txs.created <= now() - interval $1)) as expired
from cash_in_txs as txs from cash_in_txs as txs
left outer join customers c on txs.customer_id = c.id left outer join customers c on txs.customer_id = c.id
order by created desc limit $2` where txs.created >= $2 and txs.created <= $3
order by created desc limit $4 offset $5`
const cashOutSql = `select 'cashOut' as tx_class, const cashOutSql = `select 'cashOut' as tx_class,
txs.*, txs.*,
@ -50,14 +50,18 @@ function batch () {
c.name as customer_name, c.name as customer_name,
c.front_camera_path as customer_front_camera_path, c.front_camera_path as customer_front_camera_path,
c.id_card_photo_path as customer_id_card_photo_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 (extract(epoch from (now() - greatest(txs.created, txs.confirmed_at))) * 1000) >= $1 as expired
from cash_out_txs txs from cash_out_txs txs
inner join cash_out_actions actions on txs.id = actions.tx_id inner join cash_out_actions actions on txs.id = actions.tx_id
and actions.action = 'provisionAddress' and actions.action = 'provisionAddress'
left outer join customers c on txs.customer_id = c.id left outer join customers c on txs.customer_id = c.id
order by created desc limit $1` where txs.created >= $2 and txs.created <= $3
order by created desc limit $4 offset $5`
return Promise.all([db.any(cashInSql, [cashInTx.PENDING_INTERVAL, NUM_RESULTS]), db.any(cashOutSql, [NUM_RESULTS, REDEEMABLE_AGE])]) return Promise.all([
db.any(cashInSql, [cashInTx.PENDING_INTERVAL, from, until, limit, offset]),
db.any(cashOutSql, [REDEEMABLE_AGE, from, until, limit, offset])
])
.then(packager) .then(packager)
} }
@ -65,8 +69,7 @@ function getCustomerTransactions (customerId) {
const packager = _.flow(it => { const packager = _.flow(it => {
console.log() console.log()
return it return it
}, _.flatten, _.orderBy(_.property('created'), ['desc']), }, _.flatten, _.orderBy(_.property('created'), ['desc']), _.map(camelize), addNames)
_.take(NUM_RESULTS), _.map(camelize), addNames)
const cashInSql = `select 'cashIn' as tx_class, txs.*, const cashInSql = `select 'cashIn' as tx_class, txs.*,
c.phone as customer_phone, c.phone as customer_phone,

View file

@ -1,3 +1,4 @@
import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles } from '@material-ui/core' import { makeStyles } from '@material-ui/core'
import classnames from 'classnames' import classnames from 'classnames'
import FileSaver from 'file-saver' import FileSaver from 'file-saver'
@ -128,10 +129,13 @@ const useStyles = makeStyles(styles)
const ALL = 'all' const ALL = 'all'
const RANGE = 'range' const RANGE = 'range'
const LogsDownloaderPopover = ({ name, getTimestamp, logs, title }) => { const LogsDownloaderPopover = ({ name, query, args, title, getLogs }) => {
const [selectedRadio, setSelectedRadio] = useState(ALL) const [selectedRadio, setSelectedRadio] = useState(ALL)
const [range, setRange] = useState({ from: null, to: null }) const [range, setRange] = useState({ from: null, until: null })
const [anchorEl, setAnchorEl] = useState(null) const [anchorEl, setAnchorEl] = useState(null)
const [fetchLogs] = useLazyQuery(query, {
onCompleted: data => createLogsFile(getLogs(data), range)
})
const classes = useStyles() const classes = useStyles()
@ -143,49 +147,55 @@ const LogsDownloaderPopover = ({ name, getTimestamp, logs, title }) => {
const handleRadioButtons = evt => { const handleRadioButtons = evt => {
const selectedRadio = R.path(['target', 'value'])(evt) const selectedRadio = R.path(['target', 'value'])(evt)
setSelectedRadio(selectedRadio) setSelectedRadio(selectedRadio)
if (selectedRadio === ALL) setRange({ from: null, to: null }) if (selectedRadio === ALL) setRange({ from: null, until: null })
} }
const handleRangeChange = useCallback( const handleRangeChange = useCallback(
(from, to) => { (from, until) => {
setRange({ from, to }) setRange({ from, until })
}, },
[setRange] [setRange]
) )
const downloadLogs = (range, logs) => { const downloadLogs = (range, args, fetchLogs) => {
if (!range) return if (selectedRadio === ALL) {
fetchLogs({
variables: {
...args
}
})
}
if (range.from && !range.to) range.to = moment() if (!range || !range.from) return
if (range.from && !range.until) range.until = moment()
if (selectedRadio === RANGE) {
fetchLogs({
variables: {
...args,
from: range.from,
until: range.until
}
})
}
}
const createLogsFile = (logs, range) => {
const formatDateFile = date => { const formatDateFile = date => {
return moment(date).format('YYYY-MM-DD_HH-mm') return moment(date).format('YYYY-MM-DD_HH-mm')
} }
if (selectedRadio === ALL) { const text = logs.map(it => JSON.stringify(it)).join('\n')
const text = logs.map(it => JSON.stringify(it)).join('\n') const blob = new window.Blob([text], {
const blob = new window.Blob([text], { type: 'text/plain;charset=utf-8'
type: 'text/plain;charset=utf-8' })
})
FileSaver.saveAs(blob, `${formatDateFile(new Date())}_${name}`)
return
}
if (selectedRadio === RANGE) { FileSaver.saveAs(
const text = logs blob,
.filter(log => selectedRadio === ALL
moment(getTimestamp(log)).isBetween(range.from, range.to, 'day', '[]') ? `${formatDateFile(new Date())}_${name}`
) : `${formatDateFile(range.from)}_${formatDateFile(range.until)}_${name}`
.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 handleOpenRangePicker = event => { const handleOpenRangePicker = event => {
@ -231,7 +241,7 @@ const LogsDownloaderPopover = ({ name, getTimestamp, logs, title }) => {
<div className={classes.arrowContainer}> <div className={classes.arrowContainer}>
<Arrow className={classes.arrow} /> <Arrow className={classes.arrow} />
</div> </div>
<DateContainer date={range.to}>To</DateContainer> <DateContainer date={range.until}>To</DateContainer>
</> </>
)} )}
</div> </div>
@ -242,7 +252,9 @@ const LogsDownloaderPopover = ({ name, getTimestamp, logs, title }) => {
</div> </div>
)} )}
<div className={classes.download}> <div className={classes.download}>
<Link color="primary" onClick={() => downloadLogs(range, logs)}> <Link
color="primary"
onClick={() => downloadLogs(range, args, fetchLogs)}>
Download Download
</Link> </Link>
</div> </div>

View file

@ -34,9 +34,16 @@ const GET_MACHINES = gql`
} }
` `
const NUM_LOG_RESULTS = 1000
const GET_MACHINE_LOGS = gql` const GET_MACHINE_LOGS = gql`
query MachineLogs($deviceId: ID!) { query MachineLogs($deviceId: ID!, $limit: Int, $from: Date, $until: Date) {
machineLogs(deviceId: $deviceId) { machineLogs(
deviceId: $deviceId
limit: $limit
from: $from
until: $until
) {
logLevel logLevel
id id
timestamp timestamp
@ -65,7 +72,11 @@ const Logs = () => {
const deviceId = selected?.deviceId const deviceId = selected?.deviceId
const { data: machineResponse } = useQuery(GET_MACHINES) const { data: machineResponse } = useQuery(GET_MACHINES, {
variables: {
limit: NUM_LOG_RESULTS
}
})
const [sendSnapshot, { loading }] = useMutation(SUPPORT_LOGS, { const [sendSnapshot, { loading }] = useMutation(SUPPORT_LOGS, {
variables: { deviceId }, variables: { deviceId },
@ -97,8 +108,9 @@ const Logs = () => {
<LogsDowloaderPopover <LogsDowloaderPopover
title="Download logs" title="Download logs"
name="machine-logs" name="machine-logs"
logs={logsResponse.machineLogs} query={GET_MACHINE_LOGS}
getTimestamp={log => log.timestamp} args={{ deviceId }}
getLogs={logs => R.path(['machineLogs'])(logs)}
/> />
<SimpleButton <SimpleButton
className={classes.shareButton} className={classes.shareButton}

View file

@ -18,9 +18,11 @@ import { mainStyles } from './Transactions.styles'
const useStyles = makeStyles(mainStyles) const useStyles = makeStyles(mainStyles)
const NUM_LOG_RESULTS = 1000
const GET_TRANSACTIONS = gql` const GET_TRANSACTIONS = gql`
{ query transactions($limit: Int, $from: Date, $until: Date) {
transactions { transactions(limit: $limit, from: $from, until: $until) {
id id
txClass txClass
txHash txHash
@ -52,7 +54,11 @@ const GET_TRANSACTIONS = gql`
const Transactions = () => { const Transactions = () => {
const classes = useStyles() const classes = useStyles()
const { data: txResponse } = useQuery(GET_TRANSACTIONS) const { data: txResponse } = useQuery(GET_TRANSACTIONS, {
variables: {
limit: NUM_LOG_RESULTS
}
})
const formatCustomerName = customer => { const formatCustomerName = customer => {
const { firstName, lastName } = customer const { firstName, lastName } = customer
@ -136,8 +142,8 @@ const Transactions = () => {
<LogsDowloaderPopover <LogsDowloaderPopover
title="Download logs" title="Download logs"
name="transactions" name="transactions"
logs={txResponse.transactions} query={GET_TRANSACTIONS}
getTimestamp={tx => tx.created} getLogs={logs => R.path(['transactions'])(logs)}
/> />
</div> </div>
)} )}