forked from cerc-io/laconic-console
Error boundaries and laoding status.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"public_url": "/app",
|
||||
"server": "http://localhost",
|
||||
"publicUrl": "/app",
|
||||
"port": 4000,
|
||||
"path": "/graphql"
|
||||
"path": "/api"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
#
|
||||
# Copyright 2020 DxOS
|
||||
#
|
||||
|
||||
{
|
||||
status {
|
||||
timestamp
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@
|
||||
"apollo-boost": "^0.4.9",
|
||||
"debug": "^4.1.1",
|
||||
"graphql-tag": "^2.10.3",
|
||||
"lodash.defaultsdeep": "^4.6.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"source-map-support": "^0.5.12"
|
||||
@@ -52,6 +53,7 @@
|
||||
"dotenv-webpack": "^1.8.0",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^23.13.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-react": "^7.17.0",
|
||||
"html-webpack-plugin": "^4.3.0",
|
||||
"jest": "^24.8.0",
|
||||
@@ -66,5 +68,27 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "babel-eslint",
|
||||
"extends": [
|
||||
"plugin:jest/recommended",
|
||||
"semistandard",
|
||||
"standard-jsx"
|
||||
],
|
||||
"plugins": [
|
||||
"babel"
|
||||
],
|
||||
"rules": {
|
||||
"babel/semi": 1
|
||||
}
|
||||
},
|
||||
"semistandard": {
|
||||
"parser": "babel-eslint",
|
||||
"env": [
|
||||
"jest",
|
||||
"node",
|
||||
"browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// Copyright 2020 DxOS
|
||||
//
|
||||
|
||||
import React, { Component } from 'react';
|
||||
|
||||
/**
|
||||
* Root-level error boundary.
|
||||
* https://reactjs.org/docs/error-boundaries.html
|
||||
*
|
||||
* NOTE: Must currently be a Component.
|
||||
* https://reactjs.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes
|
||||
*/
|
||||
class ErrorBoundary extends Component {
|
||||
static getDerivedStateFromError (error) {
|
||||
return { error };
|
||||
}
|
||||
|
||||
state = {
|
||||
error: null
|
||||
};
|
||||
|
||||
componentDidCatch (error, errorInfo) {
|
||||
const { onError } = this.props;
|
||||
|
||||
// TODO(burdon): Show error indicator.
|
||||
// TODO(burdon): Logging service; output error file.
|
||||
onError(error);
|
||||
}
|
||||
|
||||
render () {
|
||||
const { children } = this.props;
|
||||
const { error } = this.state;
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<pre>{String(error)}</pre>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ErrorBoundary.defaultProps = {
|
||||
onError: console.warn
|
||||
};
|
||||
|
||||
export default ErrorBoundary;
|
||||
@@ -0,0 +1,44 @@
|
||||
//
|
||||
// Copyright 2020 DxOS
|
||||
//
|
||||
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useStatusReducer } from '../hooks';
|
||||
|
||||
// TODO(burdon): Factor out LoadingIndicator.
|
||||
const Layout = ({ children }) => {
|
||||
const [{ loading, error = '' }] = useStatusReducer();
|
||||
const [isLoading, setLoading] = useState(loading);
|
||||
|
||||
useEffect(() => {
|
||||
let t;
|
||||
if (loading) {
|
||||
setLoading(loading);
|
||||
t = setTimeout(() => {
|
||||
setLoading(false);
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(t);
|
||||
}, [loading]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
{error && (
|
||||
<span>{String(error)}</span>
|
||||
)}
|
||||
{isLoading && (
|
||||
<span>Loading</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright 2020 Wireline, Inc.
|
||||
//
|
||||
|
||||
import React, { useEffect, useReducer } from 'react';
|
||||
import defaultsDeep from 'lodash.defaultsdeep';
|
||||
|
||||
import ErrorBoundary from '../components/ErrorBoundary';
|
||||
|
||||
import { statusReducer, SET_STATUS } from '../hooks/status';
|
||||
|
||||
import { ConsoleContext } from '../hooks';
|
||||
|
||||
const defaultState = {};
|
||||
|
||||
/**
|
||||
* Actions reducer.
|
||||
* https://reactjs.org/docs/hooks-reference.html#usereducer
|
||||
* @param {Object} state
|
||||
* @param {string} action
|
||||
*/
|
||||
const appReducer = (state, action) => ({
|
||||
// TODO(burdon): Key shouldn't be same as action type.
|
||||
[SET_STATUS]: statusReducer(state[SET_STATUS], action)
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates the Console framework context, which provides the global UX state.
|
||||
* Wraps children with a React ErrorBoundary component, which catches runtime errors and enables reset.
|
||||
*
|
||||
* @param {function} children
|
||||
* @param {Object} [initialState]
|
||||
* @param {function} [errorHandler]
|
||||
* @returns {function}
|
||||
*/
|
||||
const ConsoleContextProvider = ({ children, initialState = {}, errorHandler }) => {
|
||||
const [state, dispatch] = useReducer(appReducer, defaultsDeep({}, initialState, defaultState));
|
||||
|
||||
const { errors: { exceptions = [] } = {} } = state[SET_STATUS] || {};
|
||||
|
||||
// Bind the error handler.
|
||||
if (errorHandler) {
|
||||
useEffect(() => {
|
||||
errorHandler.on('error', error => {
|
||||
dispatch({
|
||||
type: SET_STATUS,
|
||||
payload: {
|
||||
exceptions: [error, ...exceptions]
|
||||
}
|
||||
});
|
||||
});
|
||||
}, []);
|
||||
}
|
||||
|
||||
return (
|
||||
<ConsoleContext.Provider value={{ state, dispatch }}>
|
||||
<ErrorBoundary>
|
||||
{children}
|
||||
</ErrorBoundary>
|
||||
</ConsoleContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConsoleContextProvider;
|
||||
@@ -6,24 +6,30 @@ import { ApolloProvider } from '@apollo/react-hooks';
|
||||
import ApolloClient from 'apollo-boost';
|
||||
import React from 'react';
|
||||
|
||||
import Status from '../components/Status';
|
||||
import Status from './Status';
|
||||
|
||||
import config from '../../config.json';
|
||||
import Layout from '../components/Layout';
|
||||
import ConsoleContextProvider from './ConsoleContextProvider';
|
||||
|
||||
const { port, path } = config;
|
||||
const { server, port = 80, path } = config;
|
||||
|
||||
// TODO(burdon): Error handling for server errors.
|
||||
// TODO(burdon): Authentication:
|
||||
// https://www.apollographql.com/docs/react/networking/authentication/
|
||||
|
||||
const client = new ApolloClient({
|
||||
uri: `http://localhost:${port}${path}`
|
||||
uri: `${server}:${port}${path}`
|
||||
});
|
||||
|
||||
const Main = () => {
|
||||
return (
|
||||
<ApolloProvider client={client}>
|
||||
<Status />
|
||||
<ConsoleContextProvider>
|
||||
<Layout>
|
||||
<Status />
|
||||
</Layout>
|
||||
</ConsoleContextProvider>
|
||||
</ApolloProvider>
|
||||
);
|
||||
};
|
||||
|
||||
+5
-11
@@ -2,24 +2,18 @@
|
||||
// Copyright 2020 DxOS
|
||||
//
|
||||
|
||||
import debug from 'debug';
|
||||
import React from 'react';
|
||||
import { useQuery } from '@apollo/react-hooks';
|
||||
|
||||
import { useQueryStatusReducer } from '../hooks';
|
||||
|
||||
import QUERY_STATUS from '../../gql/status.graphql';
|
||||
|
||||
const log = debug('dxos:console:client:app');
|
||||
|
||||
const Status = () => {
|
||||
const { loading, error, data } = useQuery(QUERY_STATUS);
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
const data = useQueryStatusReducer(useQuery(QUERY_STATUS, { pollInterval: 5000 }));
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (error) {
|
||||
return <div>Error: ${error}</div>;
|
||||
}
|
||||
|
||||
log(JSON.stringify(data));
|
||||
|
||||
return (
|
||||
<pre>
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Copyright 2020 Wireline, Inc.
|
||||
//
|
||||
|
||||
import { createContext } from 'react';
|
||||
|
||||
/**
|
||||
* https://reactjs.org/docs/context.html#reactcreatecontext
|
||||
* @type {React.Context}
|
||||
*/
|
||||
export const ConsoleContext = createContext({});
|
||||
@@ -0,0 +1,6 @@
|
||||
//
|
||||
// Copyright 2020 Wireline, Inc.
|
||||
//
|
||||
|
||||
export * from './context';
|
||||
export * from './status';
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Copyright 2019 Wireline, Inc.
|
||||
//
|
||||
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { ConsoleContext } from './context';
|
||||
|
||||
export const SET_STATUS = 'errors';
|
||||
|
||||
export const useStatusReducer = () => {
|
||||
const { state, dispatch } = useContext(ConsoleContext);
|
||||
return [
|
||||
state[SET_STATUS] || {},
|
||||
value => dispatch({ type: SET_STATUS, payload: value || { exceptions: [] } })
|
||||
];
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle Apollo queries.
|
||||
*/
|
||||
export const useQueryStatusReducer = ({ loading, error, data }) => {
|
||||
const [, setStatus] = useStatusReducer();
|
||||
|
||||
if (loading) {
|
||||
setTimeout(() => setStatus({ loading }));
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setTimeout(() => setStatus({ error }));
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const statusReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case SET_STATUS:
|
||||
return {
|
||||
...state,
|
||||
...action.payload
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
@@ -2,4 +2,4 @@
|
||||
// Copyright 2020 DxOS
|
||||
//
|
||||
|
||||
export Main from './main';
|
||||
export * from './hooks';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"build": {
|
||||
"name": "@dxos/console-client",
|
||||
"buildDate": "2020-05-23T18:35:48.873Z",
|
||||
"buildDate": "2020-05-23T20:00:53.818Z",
|
||||
"version": "1.0.0-beta.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"port": 4000,
|
||||
"path": "/graphql"
|
||||
}
|
||||
@@ -48,5 +48,28 @@
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"parser": "babel-eslint",
|
||||
"extends": [
|
||||
"plugin:jest/recommended",
|
||||
"semistandard",
|
||||
"standard-jsx"
|
||||
],
|
||||
"plugins": [
|
||||
"babel",
|
||||
"node"
|
||||
],
|
||||
"rules": {
|
||||
"babel/semi": 1
|
||||
}
|
||||
},
|
||||
"semistandard": {
|
||||
"parser": "babel-eslint",
|
||||
"env": [
|
||||
"jest",
|
||||
"node",
|
||||
"browser"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
#
|
||||
# Copyright 2020 DxOS
|
||||
#
|
||||
|
||||
type Status {
|
||||
timestamp: String
|
||||
version: String
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
status {
|
||||
version
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,10 @@ import { ApolloServer, gql } from 'apollo-server-express';
|
||||
import { print } from 'graphql/language';
|
||||
|
||||
import QUERY_STATUS from '@dxos/console-client/gql/status.graphql';
|
||||
import clientConfig from '@dxos/console-client/config.json';
|
||||
import config from '@dxos/console-client/config.json';
|
||||
|
||||
import { resolvers } from './resolvers';
|
||||
|
||||
import config from '../config.json';
|
||||
|
||||
import SCHEMA from './gql/api.graphql';
|
||||
|
||||
const log = debug('dxos:console:server');
|
||||
@@ -44,11 +42,12 @@ const app = express();
|
||||
// React app
|
||||
//
|
||||
|
||||
const { public_url } = clientConfig;
|
||||
const { publicUrl } = config;
|
||||
|
||||
app.get(`${public_url}(/:filePath)?`, (req, res) => {
|
||||
app.get(`${publicUrl}(/:filePath)?`, (req, res) => {
|
||||
const { filePath = 'index.html' } = req.params;
|
||||
const file = path.join(__dirname + '../../../../node_modules/@dxos/console-client/dist/production', filePath);
|
||||
const file = path.join(__dirname, '../../../node_modules/@dxos/console-client/dist/production', filePath);
|
||||
console.log(__dirname, file);
|
||||
res.sendFile(file);
|
||||
});
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Copyright 2020 DxOS
|
||||
//
|
||||
|
||||
import debug from 'debug';
|
||||
// import debug from 'debug';
|
||||
|
||||
import { version } from '../package.json';
|
||||
|
||||
const log = debug('dxos:console:resolver');
|
||||
// const log = debug('dxos:console:resolver');
|
||||
|
||||
//
|
||||
// Resolver
|
||||
@@ -15,6 +15,7 @@ const log = debug('dxos:console:resolver');
|
||||
export const resolvers = {
|
||||
Query: {
|
||||
status: () => ({
|
||||
timestamp: new Date().toUTCString(),
|
||||
version
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user