Resolve hook problems.

Fixed logging.
This commit is contained in:
RB 2020-05-26 22:35:55 -04:00
parent bc177cc1a1
commit 38efa6a030
14 changed files with 143 additions and 102 deletions

View File

@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- [x] Fixed hook that causes 100x rerendering.
- [x] Logging.
- [x] Error handling.
- [x] Table sorting. - [x] Table sorting.
- [x] Version check. - [x] Version check.
- [x] Fix JsonTree (yarn link). - [x] Fix JsonTree (yarn link).
@ -23,9 +26,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [x] Monorepo for client/server. - [x] Monorepo for client/server.
- [x] Basic React/Apollo component. - [x] Basic React/Apollo component.
## Tasks ### Next
### POC
- [ ] Webpack and dynamic config - [ ] Webpack and dynamic config
- [ ] Complete WNS functionality - [ ] Complete WNS functionality
@ -38,8 +39,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- [ ] Signal - [ ] Signal
- [ ] Metadata - [ ] Metadata
### Next
- [ ] https://github.com/standard/standardx (JSX) - [ ] https://github.com/standard/standardx (JSX)
- [ ] Client/server API abstraction (error handler, etc.) - [ ] Client/server API abstraction (error handler, etc.)
- [ ] Port dashboard API calls (resolve config first). - [ ] Port dashboard API calls (resolve config first).

View File

@ -30,8 +30,9 @@
"@apollo/react-components": "^3.1.5", "@apollo/react-components": "^3.1.5",
"@apollo/react-hooks": "^3.1.5", "@apollo/react-hooks": "^3.1.5",
"@babel/runtime": "^7.8.7", "@babel/runtime": "^7.8.7",
"@dxos/debug": "^1.0.0-beta.20",
"@dxos/gem-core": "^1.0.0-beta.11", "@dxos/gem-core": "^1.0.0-beta.11",
"@dxos/react-ux": "^1.0.0-beta.20", "@dxos/react-ux": "^1.0.0-beta.37",
"@material-ui/core": "^4.10.0", "@material-ui/core": "^4.10.0",
"@material-ui/icons": "^4.9.1", "@material-ui/icons": "^4.9.1",
"@material-ui/lab": "^4.0.0-alpha.54", "@material-ui/lab": "^4.0.0-alpha.54",

View File

@ -2,7 +2,7 @@
// Copyright 2020 DxOS.org // Copyright 2020 DxOS.org
// //
import React from 'react'; import React, { useEffect, useState } from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
import Alert from '@material-ui/lab/Alert'; import Alert from '@material-ui/lab/Alert';
import AlertTitle from '@material-ui/lab/AlertTitle'; import AlertTitle from '@material-ui/lab/AlertTitle';
@ -10,30 +10,37 @@ import Snackbar from '@material-ui/core/Snackbar';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({
root: { root: {
marginBottom: 60 marginBottom: 48
},
alert: {
minWidth: 400
} }
})); }));
const Error = ({ message, ...rest }) => { const Error = ({ error, ...rest }) => {
const classes = useStyles(); const classes = useStyles();
if (!message) { const [message, setMessage] = useState(error);
return null;
}
const messages = Array.isArray(message) ? message : [message]; useEffect(() => {
setMessage(error ? error.message || String(error) : null);
}, [error]);
const handleClose = () => {
setMessage(null);
};
return ( return (
<Snackbar <Snackbar
className={classes.root} classes={{ root: classes.root }}
open={Boolean(message)} open={Boolean(message)}
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }} anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
TransitionProps={{ exit: false }} TransitionProps={{ exit: false }}
autoHideDuration={1000}
> >
<Alert severity="error" {...rest}> <Alert classes={{ root: classes.alert }} severity="error" {...rest} onClose={handleClose}>
<AlertTitle>Error</AlertTitle> <AlertTitle>Error</AlertTitle>
{messages.map((message, i) => ( <div>{message}</div>
<div key={i}>{message}</div>
))}
</Alert> </Alert>
</Snackbar> </Snackbar>
); );

View File

@ -4,31 +4,28 @@
import clsx from 'clsx'; import clsx from 'clsx';
import moment from 'moment'; import moment from 'moment';
import React, { Fragment } from 'react'; import React from 'react';
import { makeStyles } from '@material-ui/core/styles'; import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({ const useStyles = makeStyles(theme => ({
root: { root: {
display: 'flex', display: 'flex',
flex: 1, flex: 1,
flexDirection: 'column',
overflow: 'hidden' overflow: 'hidden'
}, },
container: { log: {
display: 'flex', display: 'flex',
// Pin to bottom (render in time order). // Pin to bottom (render in time order).
flexDirection: 'column-reverse', flexDirection: 'column-reverse',
flex: 1, overflow: 'scroll',
overflowX: 'scroll',
overflowY: 'scroll'
},
log: {
padding: theme.spacing(1), padding: theme.spacing(1),
'& div': {
fontSize: 16, fontSize: 16,
fontFamily: 'monospace', fontFamily: 'monospace',
whiteSpace: 'nowrap' whiteSpace: 'nowrap'
}
}, },
level: { level: {
@ -50,7 +47,7 @@ const useStyles = makeStyles(theme => ({
} }
})); }));
const Log = ({ log = [], onClear }) => { const Log = ({ log = [] }) => {
const classes = useStyles(); const classes = useStyles();
const levels = { const levels = {
@ -91,11 +88,11 @@ const Log = ({ log = [], onClear }) => {
const pkg = levels[level] ? '' : `[${level}]: `; const pkg = levels[level] ? '' : `[${level}]: `;
message = ( message = (
<Fragment> <>
<span className={classes.ts}>{datetime}</span> <span className={classes.ts}>{datetime}</span>
<span className={clsx(classes.level, className)}>{label || level}</span> <span className={clsx(classes.level, className)}>{label || level}</span>
<span>{pkg}{text}</span> <span>{pkg}{text}</span>
</Fragment> </>
); );
return true; return true;
@ -111,10 +108,8 @@ const Log = ({ log = [], onClear }) => {
return ( return (
<div className={classes.root}> <div className={classes.root}>
<div className={classes.container}>
<div className={classes.log}> <div className={classes.log}>
{log.reverse().map((line, i) => <Line key={i} message={line} />)} {log.map((line, i) => <Line key={i} message={line} />)}
</div>
</div> </div>
</div> </div>
); );

View File

@ -2,7 +2,7 @@
// Copyright 2020 DxOS.org.org // Copyright 2020 DxOS.org.org
// //
import React, { Fragment } from 'react'; import React from 'react';
import Link from '@material-ui/core/Link'; import Link from '@material-ui/core/Link';
import { getServiceUrl } from '../util/config'; import { getServiceUrl } from '../util/config';
@ -14,7 +14,6 @@ import { getServiceUrl } from '../util/config';
* @param {string} pkg * @param {string} pkg
*/ */
const PackageLink = ({ config, type, pkg }) => { const PackageLink = ({ config, type, pkg }) => {
// TODO(burdon): Pass in expected arg types. // TODO(burdon): Pass in expected arg types.
if (typeof pkg === 'string') { if (typeof pkg === 'string') {
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${pkg}` }); const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${pkg}` });
@ -25,28 +24,26 @@ const PackageLink = ({ config, type, pkg }) => {
switch (type) { switch (type) {
case 'wrn:bot': { case 'wrn:bot': {
const packageLinks = []; const packageLinks = [];
Object.keys(pkg).forEach(platform => { Object.keys(pkg).forEach((platform, i) => {
Object.keys(pkg[platform]).forEach(arch => { Object.keys(pkg[platform]).forEach(arch => {
const cid = pkg[platform][arch]; const cid = pkg[platform][arch];
const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` }); const ipfsUrl = getServiceUrl(config, 'ipfs.gateway', { path: `${cid}` });
packageLinks.push( packageLinks.push(
<Fragment>
<Link <Link
key={cid} key={`${cid}`}
href={ipfsUrl} href={ipfsUrl}
title={cid} title={cid}
target="ipfs" target="ipfs"
> >
{platform}/{arch}: {cid} {platform}/{arch}: {cid}
</Link> </Link>
</Fragment>
); );
}); });
}); });
return ( return (
<Fragment>{packageLinks}</Fragment> <>{packageLinks}</>
); );
} }
} }

View File

@ -7,7 +7,7 @@ import defaultsDeep from 'lodash.defaultsdeep';
import ErrorBoundary from '../components/ErrorBoundary'; import ErrorBoundary from '../components/ErrorBoundary';
import { ConsoleContext, statusReducer, SET_STATUS } from '../hooks'; import { ConsoleContext, statusReducer, STATUS, SET_STATUS } from '../hooks';
const defaultState = {}; const defaultState = {};
@ -18,8 +18,7 @@ const defaultState = {};
* @param {string} action * @param {string} action
*/ */
const appReducer = (state, action) => ({ const appReducer = (state, action) => ({
// TODO(burdon): Key shouldn't be same as action type. [STATUS]: statusReducer(state[STATUS], action)
[SET_STATUS]: statusReducer(state[SET_STATUS], action)
}); });
/** /**
@ -36,8 +35,6 @@ const appReducer = (state, action) => ({
const ConsoleContextProvider = ({ children, config, modules, initialState = {}, errorHandler }) => { const ConsoleContextProvider = ({ children, config, modules, initialState = {}, errorHandler }) => {
const [state, dispatch] = useReducer(appReducer, defaultsDeep({}, initialState, defaultState)); const [state, dispatch] = useReducer(appReducer, defaultsDeep({}, initialState, defaultState));
const { errors: { exceptions = [] } = {} } = state[SET_STATUS] || {};
// Bind the error handler. // Bind the error handler.
if (errorHandler) { if (errorHandler) {
useEffect(() => { useEffect(() => {
@ -45,7 +42,7 @@ const ConsoleContextProvider = ({ children, config, modules, initialState = {},
dispatch({ dispatch({
type: SET_STATUS, type: SET_STATUS,
payload: { payload: {
exceptions: [error, ...exceptions] error
} }
}); });
}); });

View File

@ -9,6 +9,8 @@ import { ApolloProvider } from '@apollo/react-hooks';
import { ThemeProvider } from '@material-ui/core/styles'; import { ThemeProvider } from '@material-ui/core/styles';
import CssBaseline from '@material-ui/core/CssBaseline'; import CssBaseline from '@material-ui/core/CssBaseline';
import { ErrorHandler } from '@dxos/debug';
import config from '../../config.yml'; import config from '../../config.yml';
import { build } from '../../version.json'; import { build } from '../../version.json';
@ -33,13 +35,16 @@ Object.assign(config, { build });
debug.enable(config.system.debug); debug.enable(config.system.debug);
// Global error handler.
const errorHandler = new ErrorHandler();
/** /**
* Root application. * Root application.
*/ */
const Main = () => { const Main = () => {
return ( return (
<ApolloProvider client={clientFactory(config)}> <ApolloProvider client={clientFactory(config)}>
<ConsoleContextProvider config={config} modules={modules}> <ConsoleContextProvider config={config} modules={modules} errorHandler={errorHandler}>
<ThemeProvider theme={createTheme(config.app.theme)}> <ThemeProvider theme={createTheme(config.app.theme)}>
<CssBaseline /> <CssBaseline />
<HashRouter> <HashRouter>

View File

@ -18,6 +18,8 @@ import red from '@material-ui/core/colors/red';
import { ConsoleContext, useStatusReducer } from '../hooks'; import { ConsoleContext, useStatusReducer } from '../hooks';
import Error from '../components/Error';
import VersionCheck from './VersionCheck'; import VersionCheck from './VersionCheck';
const useStyles = makeStyles((theme) => ({ const useStyles = makeStyles((theme) => ({
@ -30,17 +32,15 @@ const useStyles = makeStyles((theme) => ({
color: grey[400] color: grey[400]
}, },
left: { left: {
display: 'flex',
width: 160 width: 160
}, },
right: { right: {
display: 'flex',
width: 160, width: 160,
textAlign: 'right' justifyContent: 'flex-end'
}, },
center: { center: {
flex: 1,
textAlign: 'center'
},
info: {
display: 'flex', display: 'flex',
fontFamily: 'monospace', fontFamily: 'monospace',
fontSize: 'large', fontSize: 'large',
@ -90,7 +90,7 @@ const StatusBar = () => {
const StatusIcon = ({ error }) => { const StatusIcon = ({ error }) => {
if (error) { if (error) {
return ( return (
<ErrorIcon className={clsx(classes.icon, classes.error)} title={String(error)} /> <ErrorIcon className={clsx(classes.icon, classes.error)} />
); );
} else { } else {
return ( return (
@ -100,6 +100,7 @@ const StatusBar = () => {
}; };
return ( return (
<>
<Toolbar className={classes.root}> <Toolbar className={classes.root}>
<div className={classes.left}> <div className={classes.left}>
<Link className={classes.link} href={config.app.website} rel="noreferrer" target="_blank"> <Link className={classes.link} href={config.app.website} rel="noreferrer" target="_blank">
@ -107,7 +108,7 @@ const StatusBar = () => {
</Link> </Link>
</div> </div>
<div className={classes.info}> <div className={classes.center}>
<div>{name} ({version})</div> <div>{name} ({version})</div>
<div>{moment(buildDate).format('L')}</div> <div>{moment(buildDate).format('L')}</div>
<VersionCheck /> <VersionCheck />
@ -118,6 +119,9 @@ const StatusBar = () => {
<StatusIcon error={error} /> <StatusIcon error={error} />
</div> </div>
</Toolbar> </Toolbar>
<Error error={error} />
</>
); );
}; };

View File

@ -20,8 +20,8 @@ import WNSLog from './WNSLog';
import WNSRecords, { WNSRecordType } from './WNSRecords'; import WNSRecords, { WNSRecordType } from './WNSRecords';
import WNSStatus from './WNSStatus'; import WNSStatus from './WNSStatus';
const TAB_RECORDS = 'explorer'; const TAB_RECORDS = 'records';
const TAB_STATUS = 'records'; const TAB_STATUS = 'status';
const TAB_LOG = 'log'; const TAB_LOG = 'log';
const useStyles = makeStyles(() => ({ const useStyles = makeStyles(() => ({

View File

@ -2,20 +2,21 @@
// Copyright 2019 DxOS // Copyright 2019 DxOS
// //
import { useContext } from 'react'; import { useContext, useEffect } from 'react';
import { ConsoleContext } from './context'; import { ConsoleContext } from './context';
export const SET_STATUS = 'errors'; export const STATUS = 'xxx';
export const SET_STATUS = 'set.status';
/** /**
* * Dispatcher for app status.
*/ */
export const useStatusReducer = () => { export const useStatusReducer = () => {
const { state, dispatch } = useContext(ConsoleContext); const { state, dispatch } = useContext(ConsoleContext);
return [ return [
state[SET_STATUS] || {}, state[STATUS] || {},
value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } }) value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } })
]; ];
}; };
@ -26,6 +27,7 @@ export const useStatusReducer = () => {
export const useQueryStatusReducer = ({ loading, error, data }) => { export const useQueryStatusReducer = ({ loading, error, data }) => {
const [, setStatus] = useStatusReducer(); const [, setStatus] = useStatusReducer();
useEffect(() => {
if (loading) { if (loading) {
setTimeout(() => setStatus({ loading })); setTimeout(() => setStatus({ loading }));
} }
@ -33,6 +35,7 @@ export const useQueryStatusReducer = ({ loading, error, data }) => {
if (error) { if (error) {
setTimeout(() => setStatus({ error })); setTimeout(() => setStatus({ error }));
} }
}, [loading, error]);
return data; return data;
}; };

View File

@ -12,6 +12,8 @@ const log = debug('dxos:console:client:resolvers');
const timestamp = () => new Date().toUTCString(); const timestamp = () => new Date().toUTCString();
const MAX_LOG_LENGTH = 200;
/** /**
* Resolvers * Resolvers
* https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers * https://www.apollographql.com/docs/tutorial/local-state/#local-resolvers
@ -21,6 +23,11 @@ export const createResolvers = config => {
const endpoint = getServiceUrl(config, 'wns.server', { absolute: true }); const endpoint = getServiceUrl(config, 'wns.server', { absolute: true });
const registry = new Registry(endpoint); const registry = new Registry(endpoint);
// TODO(burdon): Errors swallowed!
// Oldest to latest.
let cachedLog = [];
return { return {
Query: { Query: {
wns_status: async () => { wns_status: async () => {
@ -52,13 +59,29 @@ export const createResolvers = config => {
wns_log: async () => { wns_log: async () => {
log('WNS log...'); log('WNS log...');
// TODO(burdon): Cache and merge previous state.
const data = await registry.getLogs(); const data = await registry.getLogs();
// TODO(burdon): Bug returns blank line at end.
const filtered = data.map(line => line).filter(Boolean);
// Cache and merge previous state.
let i = filtered.findIndex(line => line === cachedLog[cachedLog.length - 1]);
if (i === -1) {
cachedLog = filtered;
} else {
i++;
for (; i < filtered.length - 1; i++) {
cachedLog.push(filtered[i]);
}
// Trim.
cachedLog.splice(0, cachedLog.length - MAX_LOG_LENGTH);
}
return { return {
__typename: 'JSONLog', __typename: 'JSONLog',
timestamp: timestamp(), timestamp: timestamp(),
log: data log: [...cachedLog].reverse()
}; };
} }
} }

View File

@ -1,7 +1,7 @@
{ {
"build": { "build": {
"name": "@dxos/console-client", "name": "@dxos/console-client",
"buildDate": "2020-05-26T17:38:45.688Z", "buildDate": "2020-05-27T01:16:27.565Z",
"version": "1.0.0-beta.0" "version": "1.0.0-beta.0"
} }
} }

View File

@ -15,6 +15,10 @@ const timestamp = () => new Date().toUTCString();
* @param config * @param config
*/ */
export const createResolvers = config => ({ export const createResolvers = config => ({
// TODO(burdon): Auth mutations.
// https://www.apollographql.com/docs/apollo-server/data/errors/#codes
Mutation: { Mutation: {
// //
// WNS // WNS

View File

@ -1179,6 +1179,13 @@
dependencies: dependencies:
debug "^4.1.1" debug "^4.1.1"
"@dxos/debug@^1.0.0-beta.37":
version "1.0.0-beta.37"
resolved "https://registry.yarnpkg.com/@dxos/debug/-/debug-1.0.0-beta.37.tgz#0fc3a1d9d0b2616c5c22da6fcea7c369610ec282"
integrity sha512-65jOaqMrjv1Comepqq2U0k9olpAiKtk6pVEnhZHb+FutPMnjIkA+TPedNfTpxWQEs3MpG0IVyj64JugGl2KhQA==
dependencies:
debug "^4.1.1"
"@dxos/gem-core@^1.0.0-beta.11": "@dxos/gem-core@^1.0.0-beta.11":
version "1.0.0-beta.11" version "1.0.0-beta.11"
resolved "https://registry.yarnpkg.com/@dxos/gem-core/-/gem-core-1.0.0-beta.11.tgz#b4b8e82b3cfe7a9dd06fac8596ccf04fa8028e21" resolved "https://registry.yarnpkg.com/@dxos/gem-core/-/gem-core-1.0.0-beta.11.tgz#b4b8e82b3cfe7a9dd06fac8596ccf04fa8028e21"
@ -1194,17 +1201,16 @@
react-dom "^16.13.1" react-dom "^16.13.1"
react-resize-aware "^3.0.0" react-resize-aware "^3.0.0"
"@dxos/react-ux@^1.0.0-beta.20": "@dxos/react-ux@^1.0.0-beta.37":
version "1.0.0-beta.20" version "1.0.0-beta.37"
resolved "https://registry.yarnpkg.com/@dxos/react-ux/-/react-ux-1.0.0-beta.20.tgz#0f0801ae2ddb9089428034cc4b466ee98206a180" resolved "https://registry.yarnpkg.com/@dxos/react-ux/-/react-ux-1.0.0-beta.37.tgz#4a6003ce52f0afa156e0af457162d2d14695f153"
integrity sha512-larSB5cNCbyvP55/qan9uzioGXrcbtvrmy+fBRx7eLlsFs/eTgD/zrNMSVtq5mvMAn/oKKCOX1wipxm4gK6fdA== integrity sha512-QuRIAc4xY57uHRqRnXwGZEW5VD6NVmpIQWRB1T2XsmRdyIzwWDHgK6pBzd7zeNQI/6nqzHr0fkeRY0Lsqou8Cw==
dependencies: dependencies:
"@dxos/crypto" "^1.0.0-beta.1" "@dxos/crypto" "^1.0.0-beta.1"
"@dxos/debug" "^1.0.0-beta.20" "@dxos/debug" "^1.0.0-beta.37"
"@material-ui/core" "^4.9.0" "@material-ui/core" "^4.9.0"
"@material-ui/icons" "^4.5.1" "@material-ui/icons" "^4.5.1"
"@material-ui/lab" "^4.0.0-alpha.42" "@material-ui/lab" "^4.0.0-alpha.42"
"@material-ui/styles" "^4.9.0"
clsx "^1.0.4" clsx "^1.0.4"
lodash.isplainobject "^4.0.6" lodash.isplainobject "^4.0.6"
uuid "^3.3.3" uuid "^3.3.3"
@ -2189,7 +2195,7 @@
prop-types "^15.7.2" prop-types "^15.7.2"
react-is "^16.8.0" react-is "^16.8.0"
"@material-ui/styles@^4.10.0", "@material-ui/styles@^4.9.0": "@material-ui/styles@^4.10.0":
version "4.10.0" version "4.10.0"
resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071" resolved "https://registry.yarnpkg.com/@material-ui/styles/-/styles-4.10.0.tgz#2406dc23aa358217aa8cc772e6237bd7f0544071"
integrity sha512-XPwiVTpd3rlnbfrgtEJ1eJJdFCXZkHxy8TrdieaTvwxNYj42VnnCyFzxYeNW9Lhj4V1oD8YtQ6S5Gie7bZDf7Q== integrity sha512-XPwiVTpd3rlnbfrgtEJ1eJJdFCXZkHxy8TrdieaTvwxNYj42VnnCyFzxYeNW9Lhj4V1oD8YtQ6S5Gie7bZDf7Q==