From 703c5d7c91b22be44a5684d6fada6dded23137ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luis=20F=C3=A9lix?= Date: Tue, 12 Nov 2019 11:15:00 +0000 Subject: [PATCH] feat: add new server log page --- lib/db.js | 14 + lib/event-bus.js | 29 + lib/logger.js | 8 +- lib/new-admin/admin-server.js | 24 + lib/new-admin/server-logs.js | 26 + lib/new-admin/supervisor.js | 58 + lib/pg-transport.js | 39 + migrations/001-initial.js | 8 + .../1572524820075-server-support-logs.js | 15 + new-lamassu-admin/src/components/Uptime.js | 63 + .../src/components/buttons/ActionButton.js | 2 +- .../components/buttons/BaseButton.styles.js | 70 + .../src/components/buttons/FeatureButton.js | 50 + .../src/components/buttons/SimpleButton.js | 20 +- .../src/components/buttons/index.js | 3 +- .../components/fake-table/EditableTable.js | 60 + .../src/components/fake-table/Table.styles.js | 36 + .../src/components/inputs/Checkbox.js | 43 + .../src/components/inputs/Radio.js | 14 + .../src/components/inputs/Select.js | 60 + .../src/components/inputs/Select.styles.js | 83 + .../src/components/inputs/Switch.js | 73 + .../src/components/inputs/TextInput.js | 51 + .../src/components/inputs/index.js | 11 +- .../src/components/typography/styles.js | 5 + .../src/pages/Funding.module.scss | 60 + .../src/pages/LogPageHeader.styles.js | 12 + new-lamassu-admin/src/pages/Logs.module.scss | 52 + new-lamassu-admin/src/pages/ServerLogs.js | 200 ++ new-lamassu-admin/src/routing/routes.js | 5 +- new-lamassu-admin/src/styling/global/index.js | 3 + .../styling/icons/action/arrow/regular.svg | 17 + .../styling/icons/button/download/white.svg | 15 + .../styling/icons/button/download/zodiac.svg | 15 + new-lamassu-admin/src/styling/variables.js | 5 + package-lock.json | 1606 ++++++++++++++++- package.json | 4 +- shell.nix | 14 + 38 files changed, 2844 insertions(+), 29 deletions(-) create mode 100644 lib/event-bus.js create mode 100644 lib/new-admin/server-logs.js create mode 100644 lib/new-admin/supervisor.js create mode 100644 lib/pg-transport.js create mode 100644 migrations/1572524820075-server-support-logs.js create mode 100644 new-lamassu-admin/src/components/Uptime.js create mode 100644 new-lamassu-admin/src/components/buttons/BaseButton.styles.js create mode 100644 new-lamassu-admin/src/components/buttons/FeatureButton.js create mode 100644 new-lamassu-admin/src/components/fake-table/EditableTable.js create mode 100644 new-lamassu-admin/src/components/fake-table/Table.styles.js create mode 100644 new-lamassu-admin/src/components/inputs/Checkbox.js create mode 100644 new-lamassu-admin/src/components/inputs/Radio.js create mode 100644 new-lamassu-admin/src/components/inputs/Select.js create mode 100644 new-lamassu-admin/src/components/inputs/Select.styles.js create mode 100644 new-lamassu-admin/src/components/inputs/Switch.js create mode 100644 new-lamassu-admin/src/components/inputs/TextInput.js create mode 100644 new-lamassu-admin/src/pages/Funding.module.scss create mode 100644 new-lamassu-admin/src/pages/LogPageHeader.styles.js create mode 100644 new-lamassu-admin/src/pages/Logs.module.scss create mode 100644 new-lamassu-admin/src/pages/ServerLogs.js create mode 100644 new-lamassu-admin/src/styling/icons/action/arrow/regular.svg create mode 100644 new-lamassu-admin/src/styling/icons/button/download/white.svg create mode 100644 new-lamassu-admin/src/styling/icons/button/download/zodiac.svg create mode 100644 shell.nix diff --git a/lib/db.js b/lib/db.js index bed67ddf..4a190214 100644 --- a/lib/db.js +++ b/lib/db.js @@ -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 diff --git a/lib/event-bus.js b/lib/event-bus.js new file mode 100644 index 00000000..0c481327 --- /dev/null +++ b/lib/event-bus.js @@ -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 } diff --git a/lib/logger.js b/lib/logger.js index 0531717b..d8c1a56e 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -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 diff --git a/lib/new-admin/admin-server.js b/lib/new-admin/admin-server.js index ff112fb9..beb5aa50 100644 --- a/lib/new-admin/admin-server.js +++ b/lib/new-admin/admin-server.js @@ -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')) diff --git a/lib/new-admin/server-logs.js b/lib/new-admin/server-logs.js new file mode 100644 index 00000000..9a25412b --- /dev/null +++ b/lib/new-admin/server-logs.js @@ -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 } diff --git a/lib/new-admin/supervisor.js b/lib/new-admin/supervisor.js new file mode 100644 index 00000000..4fb806c4 --- /dev/null +++ b/lib/new-admin/supervisor.js @@ -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 } diff --git a/lib/pg-transport.js b/lib/pg-transport.js new file mode 100644 index 00000000..65f39494 --- /dev/null +++ b/lib/pg-transport.js @@ -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() + } +} diff --git a/migrations/001-initial.js b/migrations/001-initial.js index c4ac59ce..f49b331b 100644 --- a/migrations/001-initial.js +++ b/migrations/001-initial.js @@ -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, ' + diff --git a/migrations/1572524820075-server-support-logs.js b/migrations/1572524820075-server-support-logs.js new file mode 100644 index 00000000..6edaadff --- /dev/null +++ b/migrations/1572524820075-server-support-logs.js @@ -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() +} diff --git a/new-lamassu-admin/src/components/Uptime.js b/new-lamassu-admin/src/components/Uptime.js new file mode 100644 index 00000000..aaa23f8f --- /dev/null +++ b/new-lamassu-admin/src/components/Uptime.js @@ -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 ( +
+
{lowerCase(process.name)}
+
+ {process.state === 'RUNNING' ? `Running for ${uptime(process.uptime)}` : startCase(lowerCase(process.state))} +
+
+ ) +} + +export default Uptime diff --git a/new-lamassu-admin/src/components/buttons/ActionButton.js b/new-lamassu-admin/src/components/buttons/ActionButton.js index 14f4f5c5..0f4caf6b 100644 --- a/new-lamassu-admin/src/components/buttons/ActionButton.js +++ b/new-lamassu-admin/src/components/buttons/ActionButton.js @@ -21,7 +21,7 @@ const ActionButton = memo(({ className, Icon, InverseIcon, color, children, ...p
} -
{children}
+ {children &&
{children}
} ) }) diff --git a/new-lamassu-admin/src/components/buttons/BaseButton.styles.js b/new-lamassu-admin/src/components/buttons/BaseButton.styles.js new file mode 100644 index 00000000..4630fd77 --- /dev/null +++ b/new-lamassu-admin/src/components/buttons/BaseButton.styles.js @@ -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' + } + } +} diff --git a/new-lamassu-admin/src/components/buttons/FeatureButton.js b/new-lamassu-admin/src/components/buttons/FeatureButton.js new file mode 100644 index 00000000..9d46ce43 --- /dev/null +++ b/new-lamassu-admin/src/components/buttons/FeatureButton.js @@ -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 ( + + ) +}) + +export default FeatureButton diff --git a/new-lamassu-admin/src/components/buttons/SimpleButton.js b/new-lamassu-admin/src/components/buttons/SimpleButton.js index 7b3f0bad..bc7e037d 100644 --- a/new-lamassu-admin/src/components/buttons/SimpleButton.js +++ b/new-lamassu-admin/src/components/buttons/SimpleButton.js @@ -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 ( - ) diff --git a/new-lamassu-admin/src/components/buttons/index.js b/new-lamassu-admin/src/components/buttons/index.js index 1da9ee24..7d446cb0 100644 --- a/new-lamassu-admin/src/components/buttons/index.js +++ b/new-lamassu-admin/src/components/buttons/index.js @@ -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 } diff --git a/new-lamassu-admin/src/components/fake-table/EditableTable.js b/new-lamassu-admin/src/components/fake-table/EditableTable.js new file mode 100644 index 00000000..6b38d3d1 --- /dev/null +++ b/new-lamassu-admin/src/components/fake-table/EditableTable.js @@ -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 ( + + {elements.map(({ size, edit, view }, idx) => ( + {editing ? edit : view} + ))} + + {editing ? ( + <> + + Cancel + + + Save + + + ) : ( + setEditing(true)}> + Edit + + )} + + + ) +} + +const EditableTable = ({ elements = [], data = [], cancel, save }) => { + return ( + + + {elements.map(({ size, header }, idx) => ( + + ))} + + + {data.map((it, idx) => )} + +
{header}
+ ) +} + +export default EditableTable diff --git a/new-lamassu-admin/src/components/fake-table/Table.styles.js b/new-lamassu-admin/src/components/fake-table/Table.styles.js new file mode 100644 index 00000000..dc26d465 --- /dev/null +++ b/new-lamassu-admin/src/components/fake-table/Table.styles.js @@ -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' + } +} diff --git a/new-lamassu-admin/src/components/inputs/Checkbox.js b/new-lamassu-admin/src/components/inputs/Checkbox.js new file mode 100644 index 00000000..1fe9e747 --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/Checkbox.js @@ -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 ( + } + checkedIcon={} + disableRipple + {...props} + /> + ) +}) + +export default CheckboxInput diff --git a/new-lamassu-admin/src/components/inputs/Radio.js b/new-lamassu-admin/src/components/inputs/Radio.js new file mode 100644 index 00000000..39b1c2be --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/Radio.js @@ -0,0 +1,14 @@ +import React from 'react' + +function Radio ({ label, ...props }) { + return ( + <> + + + ) +} + +export default Radio diff --git a/new-lamassu-admin/src/components/inputs/Select.js b/new-lamassu-admin/src/components/inputs/Select.js new file mode 100644 index 00000000..a528eb18 --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/Select.js @@ -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 ( +
+ + +
    + {isOpen && + items.map((item, index) => ( +
  • + {startCase(item)} +
  • + ))} +
+
+ ) +} + +export default Select diff --git a/new-lamassu-admin/src/components/inputs/Select.styles.js b/new-lamassu-admin/src/components/inputs/Select.styles.js new file mode 100644 index 00000000..2b8f28e4 --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/Select.styles.js @@ -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' + } + } +} diff --git a/new-lamassu-admin/src/components/inputs/Switch.js b/new-lamassu-admin/src/components/inputs/Switch.js new file mode 100644 index 00000000..0ff25bfc --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/Switch.js @@ -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 ( + + ) +}) + +export default SwitchInput diff --git a/new-lamassu-admin/src/components/inputs/TextInput.js b/new-lamassu-admin/src/components/inputs/TextInput.js new file mode 100644 index 00000000..2e5c5310 --- /dev/null +++ b/new-lamassu-admin/src/components/inputs/TextInput.js @@ -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 ( + + {suffix} + + ) : null + }} + InputLabelProps={{ className: classes.labelRoot }} + {...props} + /> + ) +}) + +export default TextInput diff --git a/new-lamassu-admin/src/components/inputs/index.js b/new-lamassu-admin/src/components/inputs/index.js index 5d0c5a51..6bae3bda 100644 --- a/new-lamassu-admin/src/components/inputs/index.js +++ b/new-lamassu-admin/src/components/inputs/index.js @@ -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 } diff --git a/new-lamassu-admin/src/components/typography/styles.js b/new-lamassu-admin/src/components/typography/styles.js index e314f606..30e262d0 100644 --- a/new-lamassu-admin/src/components/typography/styles.js +++ b/new-lamassu-admin/src/components/typography/styles.js @@ -105,6 +105,11 @@ export default { fontFamily: fontSecondary, fontWeight: 700 }, + select: { + fontSize: fontSize3, + fontFamily: fontSecondary, + fontWeight: 500 + }, inline: { display: 'inline' } diff --git a/new-lamassu-admin/src/pages/Funding.module.scss b/new-lamassu-admin/src/pages/Funding.module.scss new file mode 100644 index 00000000..a64f686f --- /dev/null +++ b/new-lamassu-admin/src/pages/Funding.module.scss @@ -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; +} diff --git a/new-lamassu-admin/src/pages/LogPageHeader.styles.js b/new-lamassu-admin/src/pages/LogPageHeader.styles.js new file mode 100644 index 00000000..166b83ff --- /dev/null +++ b/new-lamassu-admin/src/pages/LogPageHeader.styles.js @@ -0,0 +1,12 @@ +export default { + titleAndButtonsContainer: { + display: 'flex' + }, + buttonsWrapper: { + display: 'flex', + marginLeft: 10, + '& > *': { + margin: 'auto 10px' + } + } +} diff --git a/new-lamassu-admin/src/pages/Logs.module.scss b/new-lamassu-admin/src/pages/Logs.module.scss new file mode 100644 index 00000000..c134602e --- /dev/null +++ b/new-lamassu-admin/src/pages/Logs.module.scss @@ -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; +} diff --git a/new-lamassu-admin/src/pages/ServerLogs.js b/new-lamassu-admin/src/pages/ServerLogs.js new file mode 100644 index 00000000..968c275c --- /dev/null +++ b/new-lamassu-admin/src/pages/ServerLogs.js @@ -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 ( + <> +
+
+ Server + {logsResponse && ( +
+ { + 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`) + }} + /> + + Share with Lamassu + + {saveMessage} +
+ )} +
+
+ {version && ( + Server version: v{version} + )} +
+
+
+ {logsResponse && ( +