feat: add new server log page

This commit is contained in:
Luis Félix 2019-11-12 11:15:00 +00:00
parent fc1951c4b2
commit 703c5d7c91
38 changed files with 2844 additions and 29 deletions

View file

@ -1,6 +1,9 @@
const Pgp = require('pg-promise')
const uuid = require('uuid')
const _ = require('lodash/fp')
const psqlUrl = require('../lib/options').postgresql
const logger = require('./logger')
const eventBus = require('./event-bus')
const pgp = Pgp({
pgNative: true,
@ -15,4 +18,15 @@ const pgp = Pgp({
})
const db = pgp(psqlUrl)
eventBus.subscribe('log', args => {
const { level, message, meta } = args
const sql = `insert into server_logs
(id, device_id, message, log_level, meta) values ($1, $2, $3, $4, $5) returning *`
db.one(sql, [uuid.v4(), '', message, level, meta])
.then(_.mapKeys(_.camelCase))
})
module.exports = db

29
lib/event-bus.js Normal file
View file

@ -0,0 +1,29 @@
// Adapted from https://medium.com/@soffritti.pierfrancesco/create-a-simple-event-bus-in-javascript-8aa0370b3969
const uuid = require('uuid')
const _ = require('lodash/fp')
const subscriptions = {}
function subscribe (eventType, callback) {
const id = uuid.v1()
if (!subscriptions[eventType]) subscriptions[eventType] = {}
subscriptions[eventType][id] = callback
return {
unsubscribe: () => {
delete subscriptions[eventType][id]
if (_.keys(subscriptions[eventType]).length === 0) delete subscriptions[eventType]
}
}
}
function publish (eventType, arg) {
if (!subscriptions[eventType]) return
_.keys(subscriptions[eventType]).forEach(key => subscriptions[eventType][key](arg))
}
module.exports = { subscribe, publish }

View file

@ -1,11 +1,15 @@
const winston = require('winston')
const Postgres = require('./pg-transport')
const options = require('./options')
const _ = require('lodash/fp')
const logger = new winston.Logger({
level: options.logLevel,
transports: [
new (winston.transports.Console)({ timestamp: true, colorize: true })
new (winston.transports.Console)({ timestamp: true, colorize: true }),
new Postgres({
connectionString: options.postgresql,
tableName: 'server_logs'
})
],
rewriters: [
(...[,, meta]) => meta instanceof Error ? { message: meta.message, stack: meta.stack } : meta

View file

@ -7,7 +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 supervisor = require('./supervisor')
const funding = require('./funding')
const config = require('./config')
@ -56,6 +58,28 @@ app.post('/api/support_logs', (req, res, next) => {
.catch(next)
})
app.get('/api/version', (req, res, next) => {
res.send(require('../../package.json').version)
})
app.get('/api/uptimes', (req, res, next) => {
return supervisor.getAllProcessInfo()
.then(r => res.send(r))
.catch(next)
})
app.post('/api/server_support_logs', (req, res, next) => {
return serverLogs.insert()
.then(r => res.send(r))
.catch(next)
})
app.get('/api/server_logs', (req, res, next) => {
return serverLogs.getServerLogs()
.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'))

View file

@ -0,0 +1,26 @@
const _ = require('lodash/fp')
const uuid = require('uuid')
const db = require('../db')
const NUM_RESULTS = 500
function getServerLogs (until = new Date().toISOString()) {
const sql = `select id, log_level, timestamp, message from server_logs
order by timestamp desc
limit $1`
return Promise.all([db.any(sql, [ NUM_RESULTS ])])
.then(([logs]) => ({
logs: _.map(_.mapKeys(_.camelCase), logs)
}))
}
function insert () {
const sql = `insert into server_support_logs
(id) values ($1) returning *`
return db.one(sql, [uuid.v4()])
.then(_.mapKeys(_.camelCase))
}
module.exports = { getServerLogs, insert }

View file

@ -0,0 +1,58 @@
const xmlrpc = require('xmlrpc')
const logger = require('../logger')
const { promisify } = require('util')
function getAllProcessInfo () {
const convertStates = (state) => {
// From http://supervisord.org/subprocess.html#process-states
switch (state) {
case 'STOPPED':
return 'STOPPED'
case 'STARTING':
return 'RUNNING'
case 'RUNNING':
return 'RUNNING'
case 'BACKOFF':
return 'FATAL'
case 'STOPPING':
return 'STOPPED'
case 'EXITED':
return 'STOPPED'
case 'UNKNOWN':
return 'FATAL'
default:
logger.error(`Supervisord returned an unsupported state: ${state}`)
return 'FATAL'
}
}
const client = xmlrpc.createClient({
host: 'localhost',
port: '9001',
path: '/RPC2'
})
client.methodCall[promisify.custom] = (method, params) => {
return new Promise((resolve, reject) => client.methodCall(method, params, (err, value) => {
if (err) reject(err)
else resolve(value)
}))
}
return promisify(client.methodCall)('supervisor.getAllProcessInfo', [])
.then((value) => {
return value.map(process => (
{
name: process.name,
state: convertStates(process.statename),
uptime: (process.statename === 'RUNNING') ? new Date(process.now) - new Date(process.start) : 0
}
))
})
.catch((error) => {
if (error.code === 'ECONNREFUSED') logger.error('Failed to connect to supervisord HTTP server.')
else logger.error(error)
})
}
module.exports = { getAllProcessInfo }

39
lib/pg-transport.js Normal file
View file

@ -0,0 +1,39 @@
const Transport = require('winston-transport')
const eventBus = require('./event-bus')
//
// Inherit from `winston-transport` so you can take advantage
// of the base functionality and `.exceptions.handle()`.
//
module.exports = class CustomTransport extends Transport {
constructor (opts) {
super(opts)
//
// Consume any custom options here. e.g.:
// - Connection information for databases
// - Authentication information for APIs (e.g. loggly, papertrail,
// logentries, etc.).
//
this.tableName = opts.tableName || 'winston_logs'
if (!opts.connectionString) {
throw new Error('You have to define connectionString')
}
this.connectionString = opts.connectionString
}
log (level, message, meta, callback) {
if (!callback) callback = () => {}
setImmediate(() => {
this.emit('logged', level, message, meta)
})
// Perform the writing to the remote service
eventBus.publish('log', { level, message, meta })
callback()
}
}

View file

@ -2,6 +2,14 @@ const db = require('./db')
exports.up = function (next) {
var sqls = [
'create table server_logs ( ' +
'id uuid PRIMARY KEY, ' +
'device_id text, ' +
'log_level text, ' +
'timestamp timestamptz DEFAULT now(), ' +
'message text, ' +
'meta json)',
'CREATE TABLE IF NOT EXISTS user_config ( ' +
'id serial PRIMARY KEY, ' +
'type text NOT NULL, ' +

View file

@ -0,0 +1,15 @@
var db = require('./db')
exports.up = function (next) {
const sql =
[`create table server_support_logs (
id uuid PRIMARY KEY,
timestamp timestamptz not null default now() )`
]
db.multi(sql, next)
}
exports.down = function (next) {
next()
}

View file

@ -0,0 +1,63 @@
import React from 'react'
import { floor, lowerCase, startCase } from 'lodash/fp'
import { makeStyles } from '@material-ui/core'
import classnames from 'classnames'
import { spring3, spring2, mistyRose, tomato, zircon, comet, white, fontSecondary } from '../styling/variables'
import typographyStyles from './typography/styles'
const { label } = typographyStyles
const styles = {
uptimeContainer: {
display: 'inline-block',
minWidth: 120,
margin: '0 20px 0 20px'
},
name: {
paddingLeft: 8,
color: comet
},
uptime: {
extend: label,
textAlign: 'center',
padding: 4
},
running: {
backgroundColor: spring3,
color: spring2
},
notRunning: {
backgroundColor: mistyRose,
color: tomato
}
}
const useStyles = makeStyles(styles)
const Uptime = ({ process, ...props }) => {
const classes = useStyles()
const uptimeClassNames = {
[classes.uptime]: true,
[classes.running]: process.state === 'RUNNING',
[classes.notRunning]: process.state !== 'RUNNING'
}
const uptime = (time) => {
if (time < 60) return `${time}s`
if (time < 3600) return `${floor(time / 60, 0)}m`
if (time < 86400) return `${floor(time / 60 / 60, 0)}h`
return `${floor(time / 60 / 60 / 24, 0)}d`
}
return (
<div className={classes.uptimeContainer}>
<div className={classes.name}>{lowerCase(process.name)}</div>
<div className={classnames(uptimeClassNames)}>
{process.state === 'RUNNING' ? `Running for ${uptime(process.uptime)}` : startCase(lowerCase(process.state))}
</div>
</div>
)
}
export default Uptime

View file

@ -21,7 +21,7 @@ const ActionButton = memo(({ className, Icon, InverseIcon, color, children, ...p
<div className={classnames(classes.actionButtonIcon, classes.actionButtonIconActive)}>
<InverseIcon />
</div>}
<div>{children}</div>
{children && <div>{children}</div>}
</button>
)
})

View file

@ -0,0 +1,70 @@
import {
white,
fontColor,
subheaderColor,
subheaderDarkColor,
offColor,
offDarkColor
} from '../../styling/variables'
const colors = (color1, color2, color3) => {
return {
backgroundColor: color1,
'&:hover': {
backgroundColor: color2
},
'&:active': {
backgroundColor: color3
}
}
}
const buttonHeight = 45
export default {
baseButton: {
extend: colors(subheaderColor, subheaderDarkColor, offColor),
cursor: 'pointer',
border: 'none',
outline: 0,
height: buttonHeight,
color: fontColor,
'&:active': {
color: white
}
},
primary: {
extend: colors(subheaderColor, subheaderDarkColor, offColor),
'&:active': {
color: white,
'& $buttonIcon': {
display: 'none'
},
'& $buttonIconActive': {
display: 'block'
}
},
'& $buttonIconActive': {
display: 'none'
}
},
secondary: {
extend: colors(offColor, offDarkColor, white),
color: white,
'&:active': {
color: fontColor,
'& $buttonIcon': {
display: 'flex'
},
'& $buttonIconActive': {
display: 'none'
}
},
'& $buttonIcon': {
display: 'none'
},
'& $buttonIconActive': {
display: 'flex'
}
}
}

View file

@ -0,0 +1,50 @@
import React, { memo } from 'react'
import classnames from 'classnames'
import { makeStyles } from '@material-ui/core/styles'
import baseButtonStyles from './BaseButton.styles'
const { baseButton, primary } = baseButtonStyles
const svgSize = 25
const styles = {
featureButton: {
extend: baseButton,
width: baseButton.height,
borderRadius: baseButton.height / 2,
display: 'flex'
},
primary,
buttonIcon: {
margin: 'auto',
'& svg': {
width: svgSize,
height: svgSize
}
},
buttonIconActive: {} // required to extend primary
}
const useStyles = makeStyles(styles)
const FeatureButton = memo(({ className, Icon, InverseIcon, ...props }) => {
const classes = useStyles()
const classNames = {
[classes.featureButton]: true,
[classes.primary]: true
}
return (
<button className={classnames(classNames, className)} {...props}>
{Icon && <div className={classes.buttonIcon}><Icon /></div>}
{InverseIcon &&
<div className={classnames(classes.buttonIcon, classes.buttonIconActive)}>
<InverseIcon />
</div>}
</button>
)
})
export default FeatureButton

View file

@ -1,9 +1,27 @@
import React, { memo } from 'react'
import classnames from 'classnames'
import baseButtonStyles from './BaseButton.styles'
import { makeStyles } from '@material-ui/core/styles'
const { baseButton } = baseButtonStyles
const styles = {
button: {
extend: baseButton,
borderRadius: baseButton.height / 2,
outline: 0,
padding: '0 20px'
}
}
const useStyles = makeStyles(styles)
const SimpleButton = memo(({ className, children, color, size, ...props }) => {
const classes = useStyles()
return (
<button className={classnames('simple-button', className)} {...props}>
<button className={classnames(classes.button, className)} {...props}>
{children}
</button>
)

View file

@ -2,5 +2,6 @@ import Button from './Button'
import Link from './Link'
import SimpleButton from './SimpleButton'
import ActionButton from './ActionButton'
import FeatureButton from './FeatureButton'
export { Button, Link, SimpleButton, ActionButton }
export { Button, Link, SimpleButton, ActionButton, FeatureButton }

View file

@ -0,0 +1,60 @@
import React, { useState } from 'react'
import {
Td,
Tr,
THead,
TBody,
Table
} from './Table'
import { Link } from '../../components/buttons'
const EditableRow = ({ elements, cancel, save }) => {
const [editing, setEditing] = useState(false)
const innerCancel = () => {
setEditing(false)
cancel()
}
return (
<Tr>
{elements.map(({ size, edit, view }, idx) => (
<Td key={idx} size={size}>{editing ? edit : view}</Td>
))}
<Td>
{editing ? (
<>
<Link style={{ marginRight: '20px' }} color='secondary' onClick={innerCancel}>
Cancel
</Link>
<Link color='primary' onClick={save}>
Save
</Link>
</>
) : (
<Link color='primary' onClick={() => setEditing(true)}>
Edit
</Link>
)}
</Td>
</Tr>
)
}
const EditableTable = ({ elements = [], data = [], cancel, save }) => {
return (
<Table>
<THead>
{elements.map(({ size, header }, idx) => (
<Td header key={idx} size={size}>{header}</Td>
))}
</THead>
<TBody>
{data.map((it, idx) => <EditableRow key={idx} elements={elements} cancel={cancel} save={save} />)}
</TBody>
</Table>
)
}
export default EditableTable

View file

@ -0,0 +1,36 @@
import {
tableHeaderColor,
tableHeaderHeight,
spacer,
white
} from '../styling/variables'
import typographyStyles from './typography/styles'
const { label2 } = typographyStyles
export default {
tableBody: {
borderSpacing: '0 4px'
},
header: {
extend: label2,
backgroundColor: tableHeaderColor,
height: tableHeaderHeight,
textAlign: 'left',
color: white,
// display: 'flex'
display: 'table-row'
},
td: {
padding: `0 ${spacer * 3}px`
},
tdHeader: {
verticalAlign: 'middle',
display: 'table-cell',
padding: `0 ${spacer * 3}px`
},
summary: {
cursor: 'auto'
}
}

View file

@ -0,0 +1,43 @@
import React, { memo } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Checkbox from '@material-ui/core/Checkbox'
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'
import CheckBoxIcon from '@material-ui/icons/CheckBox'
import { secondaryColor } from '../../styling/variables'
const useStyles = makeStyles({
root: {
color: secondaryColor,
'&$checked': {
color: secondaryColor
}
},
checked: {}
})
const CheckboxInput = memo(({ label, ...props }) => {
const classes = useStyles()
const { name, onChange, value } = props.field
const { values, touched, errors } = props.form
return (
<Checkbox
id={name}
classes={{
root: classes.root,
checked: classes.checked
}}
onChange={onChange}
value={value}
checked={value}
icon={<CheckBoxOutlineBlankIcon style={{ marginLeft: 2, fontSize: 16 }} />}
checkedIcon={<CheckBoxIcon style={{ fontSize: 20 }} />}
disableRipple
{...props}
/>
)
})
export default CheckboxInput

View file

@ -0,0 +1,14 @@
import React from 'react'
function Radio ({ label, ...props }) {
return (
<>
<label>
<input type='radio' className='with-gap' name='gruop1' />
<span>{label || ''}</span>
</label>
</>
)
}
export default Radio

View file

@ -0,0 +1,60 @@
import React from 'react'
import { useSelect } from 'downshift'
import { startCase } from 'lodash/fp'
import classnames from 'classnames'
import { ReactComponent as Arrowdown } from '../../styling/icons/action/arrow/regular.svg'
import styles from './Select.styles'
import { makeStyles } from '@material-ui/core/styles'
const useStyles = makeStyles(styles)
function Select ({ label, items, ...props }) {
const classes = useStyles()
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getItemProps
} = useSelect({
items,
selectedItem: props.selectedItem,
onSelectedItemChange: item => {
props.onSelectedItemChange(item.selectedItem)
}
})
const selectClassNames = {
[classes.select]: true,
[classes.selectFiltered]: selectedItem !== props.default,
[classes.open]: isOpen
}
return (
<div className={classnames(selectClassNames)}>
<label {...getLabelProps()}>{startCase(label)}</label>
<button
{...getToggleButtonProps()}
>
{startCase(selectedItem)} <Arrowdown />
</button>
<ul {...getMenuProps()}>
{isOpen &&
items.map((item, index) => (
<li
key={`${item}${index}`}
{...getItemProps({ item, index })}
>
{startCase(item)}
</li>
))}
</ul>
</div>
)
}
export default Select

View file

@ -0,0 +1,83 @@
import { zircon, comet, white, fontSecondary } from '../../styling/variables'
import typographyStyles from '../typography/styles'
const { select, regularLabel, label, label2 } = typographyStyles
const WIDTH = 152
export default {
select: {
width: WIDTH,
zIndex: 1000,
'& label': {
extend: regularLabel,
color: comet,
paddingLeft: 10
},
'& button': {
extend: select,
position: 'relative',
border: 0,
backgroundColor: zircon,
width: WIDTH,
padding: '6px 0 6px 12px',
borderRadius: 20,
lineHeight: '1.14',
textAlign: 'left',
color: comet,
cursor: 'pointer',
outline: '0 none'
},
'& ul': {
maxHeight: '200px',
width: WIDTH,
overflowY: 'auto',
position: 'absolute',
margin: 0,
borderTop: 0,
padding: 0,
borderRadius: '0 0 16px 16px',
backgroundColor: zircon,
outline: '0 none',
'& li': {
listStyleType: 'none',
padding: '6px 0 6px 12px',
cursor: 'pointer'
},
'& li:hover': {
backgroundColor: comet,
color: white
}
},
'& svg': {
position: 'absolute',
top: 12,
right: 14,
fill: comet
}
},
selectFiltered: {
'& button': {
backgroundColor: comet,
color: white
},
'& ul': {
'& li': {
backgroundColor: comet,
color: white
},
'& li:hover': {
backgroundColor: zircon,
color: comet
}
},
'& svg': {
fill: `${white} !important`
}
},
open: {
'& button': {
borderRadius: '16px 16px 0 0'
}
}
}

View file

@ -0,0 +1,73 @@
import React, { memo } from 'react'
import { makeStyles } from '@material-ui/core/styles'
import Switch from '@material-ui/core/Switch'
import { secondaryColor, offColor, disabledColor, disabledColor2 } from '../../styling/variables'
const useStyles = makeStyles(theme => ({
root: {
width: 32,
height: 20,
padding: 0,
margin: theme.spacing(1)
},
switchBase: {
padding: 2,
'&$disabled': {
color: disabledColor2,
'& + $track': {
backgroundColor: disabledColor,
opacity: 1
}
},
'&$checked': {
color: theme.palette.common.white,
'& + $track': {
backgroundColor: secondaryColor,
opacity: 1,
border: 'none'
}
},
'&$focusVisible $thumb': {
border: '6px solid #fff'
}
},
thumb: {
width: 16,
height: 16
},
track: {
borderRadius: 17,
border: 'none',
backgroundColor: offColor,
opacity: 1,
transition: theme.transitions.create(['background-color', 'border'])
},
disabled: {
},
checked: {
},
focusVisible: {
}
}))
const SwitchInput = memo(({ ...props }) => {
const classes = useStyles()
return (
<Switch
focusVisibleClassName={classes.focusVisible}
disableRipple
classes={{
root: classes.root,
switchBase: classes.switchBase,
thumb: classes.thumb,
track: classes.track,
checked: classes.checked,
disabled: classes.disabled
}}
{...props}
/>
)
})
export default SwitchInput

View file

@ -0,0 +1,51 @@
import React, { memo } from 'react'
import TextField from '@material-ui/core/TextField'
import InputAdornment from '@material-ui/core/InputAdornment'
import { makeStyles } from '@material-ui/core/styles'
import { fontColor, inputFontSize, inputFontSizeLg, inputFontWeight } from '../../styling/variables'
const useStyles = makeStyles({
inputRoot: {
fontSize: inputFontSize,
color: fontColor,
fontWeight: inputFontWeight
},
inputRootLg: {
fontSize: inputFontSizeLg,
color: fontColor,
fontWeight: inputFontWeight
},
labelRoot: {
color: fontColor
}
})
const TextInput = memo(({ suffix, large, ...props }) => {
const { name, onChange, onBlur, value } = props.field
const { touched, errors } = props.form
const classes = useStyles()
return (
<TextField
id={name}
onChange={onChange}
onBlur={onBlur}
error={!!(touched[name] && errors[name])}
value={value}
classes={{ root: classes.root }}
InputProps={{
className: large ? classes.inputRootLg : classes.inputRoot,
endAdornment: suffix ? (
<InputAdornment className={classes.inputRoot} disableTypography position='end'>
{suffix}
</InputAdornment>
) : null
}}
InputLabelProps={{ className: classes.labelRoot }}
{...props}
/>
)
})
export default TextInput

View file

@ -1,8 +1,9 @@
import Autocomplete from './autocomplete/Autocomplete'
import AutocompleteMultiple from './autocomplete/AutocompleteMultiple'
import Checkbox from './base/Checkbox'
import Radio from './base/Radio'
import TextInput from './base/TextInput'
import Switch from './base/Switch'
import Checkbox from './Checkbox'
import Radio from './Radio'
import TextInput from './TextInput'
import Switch from './Switch'
import Select from './Select'
export { Autocomplete, AutocompleteMultiple, TextInput, Radio, Checkbox, Switch }
export { Autocomplete, AutocompleteMultiple, TextInput, Radio, Checkbox, Switch, Select }

View file

@ -105,6 +105,11 @@ export default {
fontFamily: fontSecondary,
fontWeight: 700
},
select: {
fontSize: fontSize3,
fontFamily: fontSecondary,
fontWeight: 500
},
inline: {
display: 'inline'
}

View file

@ -0,0 +1,60 @@
$spacer: 8;
$subheader-color: white;
$placeholder-color: white;
.wrapper {
display: flex;
flex-direction: row;
height: 100%;
}
.main {
display: flex;
flex: 1;
}
.firstSide {
margin: 0 ($spacer * 8px) 0 ($spacer * 6px);
}
.secondSide {
margin-top: -49px;
}
.coinTotal {
margin: ($spacer * 1.5px) 0;
}
.noMargin {
margin: 0px;
}
.leftSpacer {
margin-left: $spacer * 1px;
}
.topSpacer {
margin-top: $spacer * 5px;
}
.addressWrapper {
display: flex;
flex-direction: column;
flex: 1;
background-color: $subheader-color;
}
.address {
width: 375px;
margin: ($spacer * 1.5px) ($spacer * 3px);
}
.total {
margin-top: auto;
text-align: right;
margin-right: 20px;
}
.totalTitle {
color: $placeholder-color;
}

View file

@ -0,0 +1,12 @@
export default {
titleAndButtonsContainer: {
display: 'flex'
},
buttonsWrapper: {
display: 'flex',
marginLeft: 10,
'& > *': {
margin: 'auto 10px'
}
}
}

View file

@ -0,0 +1,52 @@
.titleWrapper {
display: flex;
justify-content: space-between;
align-items: center;
flex-direction: row;
}
.wrapper {
display: flex;
flex-direction: row;
height: 100%;
}
.tableWrapper {
flex: 1;
margin-left: 40px;
display: block;
overflow-x: auto;
width: 100%;
max-width: 78%;
max-height: 70vh;
}
.table {
white-space: nowrap;
display: block;
& th {
position: sticky;
top: 0;
}
}
.dateColumn {
min-width: 160px;
}
.levelColumn {
min-width: 100px;
}
.fillColumn {
width: 100%;
}
.button {
margin: 8px;
}
.buttonsWrapper {
display: flex;
align-items: center;
}

View file

@ -0,0 +1,200 @@
import React, { useState } from 'react'
import FileSaver from 'file-saver'
import { concat, uniq } from 'lodash/fp'
import moment from 'moment'
import useAxios from '@use-hooks/axios'
import Title from '../components/Title'
import { Info3 } from '../components/typography'
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 { ReactComponent as Download } from '../styling/icons/button/download/zodiac.svg'
import { ReactComponent as DownloadActive } from '../styling/icons/button/download/white.svg'
import { makeStyles } from '@material-ui/core'
import typographyStyles from '../components/typography/styles'
import { comet } from '../styling/variables'
import styles from './Logs.styles'
import logPageHeaderStyles from './LogPageHeader.styles'
const { regularLabel } = typographyStyles
const { tableWrapper } = styles
const { titleAndButtonsContainer, buttonsWrapper } = logPageHeaderStyles
styles.titleWrapper = {
display: 'flex',
justifyContent: 'space-between'
}
styles.serverTableWrapper = {
extend: tableWrapper,
maxWidth: '100%',
marginLeft: 0
}
styles.serverVersion = {
extend: regularLabel,
color: comet,
margin: 'auto 0 auto 0'
}
styles.headerLine2 = {
height: 60,
display: 'flex',
justifyContent: 'space-between',
marginBottom: 24
}
styles.uptimeContainer = {
margin: 'auto 0 auto 0'
}
styles.titleAndButtonsContainer = titleAndButtonsContainer
styles.buttonsWrapper = buttonsWrapper
const useStyles = makeStyles(styles)
const SHOW_ALL = 'Show all'
const formatDate = date => {
return moment(date).format('YYYY-MM-DD HH:mm')
}
const Logs = () => {
const [saveMessage, setSaveMessage] = useState(null)
const [logLevel, setLogLevel] = useState(SHOW_ALL)
const [version, setVersion] = useState(null)
const [processStates, setProcessStates] = useState(null)
const classes = useStyles()
useAxios({
url: 'http://localhost:8070/api/version',
method: 'GET',
trigger: [],
customHandler: (err, res) => {
if (err) return
if (res) {
setVersion(res.data)
}
}
})
useAxios({
url: 'http://localhost:8070/api/uptimes',
method: 'GET',
trigger: [],
customHandler: (err, res) => {
if (err) return
if (res) {
setProcessStates(res.data)
}
}
})
const { response: logsResponse } = useAxios({
url: 'http://localhost:8070/api/server_logs/',
method: 'GET',
trigger: [],
customHandler: () => {
setSaveMessage('')
}
})
const { loading, reFetch: sendSnapshot } = useAxios({
url: 'http://localhost:8070/api/server_support_logs',
method: 'POST',
customHandler: (err, res) => {
if (err) {
setSaveMessage('Failure saving snapshot')
throw err
}
setSaveMessage('✓ Saved latest snapshot')
}
})
const handleLogLevelChange = (item) => setLogLevel(item)
const formatDateFile = date => {
return moment(date).format('YYYY-MM-DD_HH-mm')
}
return (
<>
<div className={classes.titleWrapper}>
<div className={classes.titleAndButtonsContainer}>
<Title>Server</Title>
{logsResponse && (
<div className={classes.buttonsWrapper}>
<FeatureButton
Icon={Download}
InverseIcon={DownloadActive}
onClick={() => {
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`)
}}
/>
<SimpleButton className={classes.button} disabled={loading} onClick={sendSnapshot}>
Share with Lamassu
</SimpleButton>
<Info3>{saveMessage}</Info3>
</div>
)}
</div>
<div className={classes.serverVersion}>
{version && (
<span>Server version: v{version}</span>
)}
</div>
</div>
<div className={classes.headerLine2}>
{logsResponse && (
<Select
onSelectedItemChange={handleLogLevelChange}
label='Level'
items={concat([SHOW_ALL], uniq(logsResponse.data.logs.map(log => log.logLevel)))}
default={SHOW_ALL}
selectedItem={logLevel}
/>
)}
<div className={classes.uptimeContainer}>
{processStates &&
processStates.map((process, idx) => (
<Uptime key={idx} process={process} />
))}
</div>
</div>
<div className={classes.wrapper}>
<div className={classes.serverTableWrapper}>
<Table className={classes.table}>
<TableHead>
<TableRow header>
<TableHeader className={classes.dateColumn}>Date</TableHeader>
<TableHeader className={classes.levelColumn}>Level</TableHeader>
<TableHeader className={classes.fillColumn} />
</TableRow>
</TableHead>
<TableBody>
{logsResponse &&
logsResponse.data.logs.filter(log => logLevel === SHOW_ALL || log.logLevel === logLevel).map((log, idx) => (
<TableRow key={idx} size='sm'>
<TableCell>{formatDate(log.timestamp)}</TableCell>
<TableCell>{log.logLevel}</TableCell>
<TableCell>{log.message}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
</>
)
}
export default Logs

View file

@ -6,6 +6,7 @@ import Commissions from '../pages/Commissions'
import Logs from '../pages/Logs'
import Locales from '../pages/Locales'
import Funding from '../pages/Funding'
import ServerLogs from '../pages/ServerLogs'
const tree = [
{ key: 'transactions', label: 'Transactions', route: '/transactions' },
@ -17,7 +18,8 @@ const tree = [
route: '/maintenance',
children: [
{ key: 'logs', label: 'Logs', route: '/maintenance/logs' },
{ key: 'fuding', label: 'Funding', route: '/maintenance/funding' }
{ key: 'fuding', label: 'Funding', route: '/maintenance/funding' },
{ key: 'server-logs', label: 'Server', route: '/maintenance/server-logs' }
]
},
{
@ -58,6 +60,7 @@ const Routes = () => (
<Route path='/settings/locale' component={Locales} />
<Route path='/maintenance/logs' component={Logs} />
<Route path='/maintenance/funding' component={Funding} />
<Route path='/maintenance/server-logs' component={ServerLogs} />
</Switch>
)

View file

@ -38,6 +38,9 @@ export default {
a:active,
a:hover`]: {
outline: '0 none'
},
'button::-moz-focus-inner': {
border: 0
}
}
}

View file

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="13px" height="8px" viewBox="0 0 13 8" 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/arrow/regular</title>
<desc>Created with Sketch.</desc>
<defs>
<path d="M5.3501239,7.53208616 L0.473798314,2.73082122 C-0.158421727,2.1051411 -0.158421727,1.0952488 0.476737158,0.466675069 C1.11220338,-0.155816755 2.1378971,-0.155816755 2.77494316,0.468226909 L6.49990857,4.13723769 L10.2264532,0.466675069 C10.8619195,-0.155816755 11.8876132,-0.155816755 12.5260183,0.469568675 C13.1582383,1.0952488 13.1582383,2.1051411 12.5245507,2.73226987 L7.64673876,7.53497972 C7.33802629,7.83583835 6.92590837,8 6.49990828,8 C6.0739082,8 5.66179027,7.83583835 5.3501239,7.53208616 Z" id="path-1"></path>
</defs>
<g id="Styleguide" stroke="none" stroke-width="1" fill-rule="evenodd">
<g id="icon/action/arrow/regular">
<mask id="mask-2" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<use id="Mask" fill-rule="nonzero" xlink:href="#path-1"></use>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="13px" viewBox="0 0 14 13" 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/button/download/white</title>
<desc>Created with Sketch.</desc>
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="icon/button/download/white" transform="translate(1.000000, 0.000000)" stroke="#FFFFFF">
<g id="icon/sf-small/wizzard">
<polyline id="Path-3" points="3.6 5.4 6 7.8 8.4 5.4"></polyline>
<path d="M6,0.5 L6,7.4" id="Path-4"></path>
<path d="M0,10 L0,10 C0,10.9942 0.8058,11.8 1.8,11.8 L10.2,11.8 C11.1942,11.8 12,10.9942 12,10" id="Stroke-1"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 933 B

View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="14px" height="13px" viewBox="0 0 14 13" 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/button/download/zodiac</title>
<desc>Created with Sketch.</desc>
<g id="Styleguide" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round">
<g id="icon/button/download/zodiac" transform="translate(1.000000, 0.000000)" stroke="#1B2559">
<g id="icon/sf-small/wizzard">
<polyline id="Path-3" points="3.6 5.4 6 7.8 8.4 5.4"></polyline>
<path d="M6,0.5 L6,7.4" id="Path-4"></path>
<path d="M0,10 L0,10 C0,10.9942 0.8058,11.8 1.8,11.8 L10.2,11.8 C11.1942,11.8 12,10.9942 12,10" id="Stroke-1"></path>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 935 B

View file

@ -111,6 +111,11 @@ export {
white,
zircon,
zircon2,
comet,
spring2,
spring3,
tomato,
mistyRose,
primaryColor,
secondaryColor,

1606
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -61,8 +61,10 @@
"uuid": "^3.1.0",
"web3": "^0.20.6",
"winston": "^2.4.2",
"winston-transport": "^4.3.0",
"ws": "^3.1.0",
"xml-stream": "^0.4.5"
"xml-stream": "^0.4.5",
"xmlrpc": "^1.3.2"
},
"repository": {
"type": "git",

14
shell.nix Normal file
View file

@ -0,0 +1,14 @@
with import <nixpkgs> {};
stdenv.mkDerivation {
name = "node";
buildInputs = [
nodejs-8_x
python2Full
openssl_1_0_2
postgresql_9_6
];
shellHook = ''
export PATH="$HOME/.local:$PWD/node_modules/.bin/:$PATH"
'';
}