feat(error-handling): introduce unified and configurable error handling (#7761)

Refs #7778
This commit is contained in:
Vladimir Gorej
2022-01-24 16:12:13 +01:00
committed by GitHub
parent 4f2287fe53
commit 8b1c4a7c1a
19 changed files with 629 additions and 295 deletions

View File

@@ -31,7 +31,7 @@ const HighlightCode = ({value, fileName, className, downloadable, getConfigs, ca
}
const handlePreventYScrollingBeyondElement = (e) => {
const { target, deltaY } = e
const { target, deltaY } = e
const { scrollHeight: contentHeight, offsetHeight: visibleHeight, scrollTop } = target
const scrollOffset = visibleHeight + scrollTop
const isElementScrollable = contentHeight > visibleHeight

View File

@@ -28,7 +28,6 @@ export default class BaseLayout extends React.Component {
const SchemesContainer = getComponent("SchemesContainer", true)
const AuthorizeBtnContainer = getComponent("AuthorizeBtnContainer", true)
const FilterContainer = getComponent("FilterContainer", true)
const ErrorBoundary = getComponent("ErrorBoundary", true)
let isSwagger2 = specSelectors.isSwagger2()
let isOAS3 = specSelectors.isOAS3()
@@ -87,40 +86,38 @@ export default class BaseLayout extends React.Component {
return (
<div className='swagger-ui'>
<ErrorBoundary targetName="BaseLayout">
<SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
<Errors/>
<Row className="information-container">
<Col mobile={12}>
<InfoContainer/>
</Col>
</Row>
<SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
<Errors/>
<Row className="information-container">
<Col mobile={12}>
<InfoContainer/>
</Col>
</Row>
{hasServers || hasSchemes || hasSecurityDefinitions ? (
<div className="scheme-container">
<Col className="schemes wrapper" mobile={12}>
{hasServers ? (<ServersContainer />) : null}
{hasSchemes ? (<SchemesContainer />) : null}
{hasSecurityDefinitions ? (<AuthorizeBtnContainer />) : null}
</Col>
</div>
) : null}
<FilterContainer/>
<Row>
<Col mobile={12} desktop={12} >
<Operations/>
{hasServers || hasSchemes || hasSecurityDefinitions ? (
<div className="scheme-container">
<Col className="schemes wrapper" mobile={12}>
{hasServers ? (<ServersContainer />) : null}
{hasSchemes ? (<SchemesContainer />) : null}
{hasSecurityDefinitions ? (<AuthorizeBtnContainer />) : null}
</Col>
</Row>
<Row>
<Col mobile={12} desktop={12} >
<Models/>
</Col>
</Row>
</VersionPragmaFilter>
</ErrorBoundary>
</div>
) : null}
<FilterContainer/>
<Row>
<Col mobile={12} desktop={12} >
<Operations/>
</Col>
</Row>
<Row>
<Col mobile={12} desktop={12} >
<Models/>
</Col>
</Row>
</VersionPragmaFilter>
</div>
)
}

View File

@@ -1,4 +1,5 @@
import { pascalCaseFilename } from "core/utils"
import SafeRender from "core/plugins/safe-render"
const request = require.context(".", true, /\.jsx?$/)
@@ -18,4 +19,6 @@ request.keys().forEach( function( key ){
allPlugins[pascalCaseFilename(key)] = mod.default ? mod.default : mod
})
allPlugins.SafeRender = SafeRender
export default allPlugins

View File

@@ -88,7 +88,7 @@ const RequestBody = ({
const sampleForMediaType = rawExamplesOfMediaType?.map((container, key) => {
const val = container?.get("value", null)
if(val) {
container = container.set("value", getDefaultRequestBodyValue(
container = container.set("value", getDefaultRequestBodyValue(
requestBody,
contentType,
key,

View File

@@ -1,27 +1,28 @@
import PropTypes from "prop-types"
import React, { Component } from "react"
import { componentDidCatch } from "../fn"
import Fallback from "./fallback"
export class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
constructor(...args) {
super(...args)
this.state = { hasError: false, error: null }
}
componentDidCatch(error, errorInfo) {
console.error(error, errorInfo) // eslint-disable-line no-console
this.props.fn.componentDidCatch(error, errorInfo)
}
render() {
const { getComponent, targetName, children } = this.props
const FallbackComponent = getComponent("Fallback")
if (this.state.hasError) {
const FallbackComponent = getComponent("Fallback")
return <FallbackComponent name={targetName} />
}
@@ -31,6 +32,7 @@ export class ErrorBoundary extends Component {
ErrorBoundary.propTypes = {
targetName: PropTypes.string,
getComponent: PropTypes.func,
fn: PropTypes.object,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
@@ -39,6 +41,9 @@ ErrorBoundary.propTypes = {
ErrorBoundary.defaultProps = {
targetName: "this component",
getComponent: () => Fallback,
fn: {
componentDidCatch,
},
children: null,
}

View File

@@ -0,0 +1,32 @@
import React, { Component } from "react"
export const componentDidCatch = console.error
const isClassComponent = component => component.prototype && component.prototype.isReactComponent
export const withErrorBoundary = (getSystem) => (WrappedComponent) => {
const { getComponent, fn } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary")
const targetName = fn.getDisplayName(WrappedComponent)
class WithErrorBoundary extends Component {
render() {
return (
<ErrorBoundary targetName={targetName} getComponent={getComponent} fn={fn}>
<WrappedComponent {...this.props} {...this.context} />
</ErrorBoundary>
)
}
}
WithErrorBoundary.displayName = `WithErrorBoundary(${targetName})`
if (isClassComponent(WrappedComponent)) {
/**
* We need to handle case of class components defining a `mapStateToProps` public method.
* Components with `mapStateToProps` public method cannot be wrapped.
*/
WithErrorBoundary.prototype.mapStateToProps = WrappedComponent.prototype.mapStateToProps
}
return WithErrorBoundary
}

View File

@@ -0,0 +1,42 @@
import zipObject from "lodash/zipObject"
import ErrorBoundary from "./components/error-boundary"
import Fallback from "./components/fallback"
import { componentDidCatch, withErrorBoundary } from "./fn"
const safeRenderPlugin = ({componentList = [], fullOverride = false} = {}) => ({ getSystem }) => {
const defaultComponentList = [
"App",
"BaseLayout",
"VersionPragmaFilter",
"InfoContainer",
"ServersContainer",
"SchemesContainer",
"AuthorizeBtnContainer",
"FilterContainer",
"Operations",
"OperationContainer",
"parameters",
"responses",
"OperationServers",
"Models",
"ModelWrapper",
]
const mergedComponentList = fullOverride ? componentList : [...defaultComponentList, ...componentList]
const wrapFactory = (Original, { fn }) => fn.withErrorBoundary(Original)
const wrapComponents = zipObject(mergedComponentList, Array(mergedComponentList.length).fill(wrapFactory))
return {
fn: {
componentDidCatch,
withErrorBoundary: withErrorBoundary(getSystem),
},
components: {
ErrorBoundary,
Fallback,
},
wrapComponents,
}
}
export default safeRenderPlugin

View File

@@ -0,0 +1 @@
export const getDisplayName = (WrappedComponent) => WrappedComponent.displayName || WrappedComponent.name || "Component"

View File

@@ -1,26 +1,23 @@
import * as rootInjects from "./root-injects"
import { memoize } from "core/utils"
import ErrorBoundary from "./error-boundary"
import Fallback from "./fallback"
export default function({getComponents, getStore, getSystem}) {
let { getComponent, render, makeMappedContainer } = rootInjects
import { getComponent, render, withMappedContainer } from "./root-injects"
import { getDisplayName } from "./fn"
const viewPlugin = ({getComponents, getStore, getSystem}) => {
// getComponent should be passed into makeMappedContainer, _already_ memoized... otherwise we have a big performance hit ( think, really big )
const memGetComponent = memoize(getComponent.bind(null, getSystem, getStore, getComponents))
const memMakeMappedContainer = memoize(makeMappedContainer.bind(null, getSystem, getStore, memGetComponent, getComponents))
const memGetComponent = memoize(getComponent(getSystem, getStore, getComponents))
const memMakeMappedContainer = memoize(withMappedContainer(getSystem, getStore, memGetComponent))
return {
rootInjects: {
getComponent: memGetComponent,
makeMappedContainer: memMakeMappedContainer,
render: render.bind(null, getSystem, getStore, getComponent, getComponents),
render: render(getSystem, getStore, getComponent, getComponents),
},
components: {
ErrorBoundary,
Fallback,
fn: {
getDisplayName,
},
}
}
export default viewPlugin

View File

@@ -1,55 +1,67 @@
import React, { Component } from "react"
import ReactDOM from "react-dom"
import { compose } from "redux"
import { connect, Provider } from "react-redux"
import omit from "lodash/omit"
import identity from "lodash/identity"
const SystemWrapper = (getSystem, ComponentToWrap ) => class extends Component {
render() {
return <ComponentToWrap {...getSystem()} {...this.props} {...this.context} />
const withSystem = (getSystem) => (WrappedComponent) => {
const { fn } = getSystem()
class WithSystem extends Component {
render() {
return <WrappedComponent {...getSystem()} {...this.props} {...this.context} />
}
}
WithSystem.displayName = `WithSystem(${fn.getDisplayName(WrappedComponent)})`
return WithSystem
}
const RootWrapper = (getSystem, reduxStore, ComponentToWrap) => class extends Component {
render() {
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary", true)
const withRoot = (getSystem, reduxStore) => (WrappedComponent) => {
const { fn } = getSystem()
return (
<Provider store={reduxStore}>
<ErrorBoundary targetName={ComponentToWrap?.name}>
<ComponentToWrap {...this.props} {...this.context} />
</ErrorBoundary>
</Provider>
)
class WithRoot extends Component {
render() {
return (
<Provider store={reduxStore}>
<WrappedComponent {...this.props} {...this.context} />
</Provider>
)
}
}
WithRoot.displayName = `WithRoot(${fn.getDisplayName(WrappedComponent)})`
return WithRoot
}
const makeContainer = (getSystem, component, reduxStore) => {
const mapStateToProps = function(state, ownProps) {
const propsForContainerComponent = Object.assign({}, ownProps, getSystem())
const ori = component.prototype.mapStateToProps || (state => { return {state} })
return ori(state, propsForContainerComponent)
const withConnect = (getSystem, WrappedComponent, reduxStore) => {
const mapStateToProps = (state, ownProps) => {
const props = {...ownProps, ...getSystem()}
const customMapStateToProps = WrappedComponent.prototype?.mapStateToProps || (state => ({state}))
return customMapStateToProps(state, props)
}
let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore)
let connected = connect( mapStateToProps )(wrappedWithSystem)
if(reduxStore)
return RootWrapper(getSystem, reduxStore, connected)
return connected
return compose(
reduxStore ? withRoot(getSystem, reduxStore) : identity,
connect(mapStateToProps),
withSystem(getSystem),
)(WrappedComponent)
}
const handleProps = (getSystem, mapping, props, oldProps) => {
for (let prop in mapping) {
let fn = mapping[prop]
if(typeof fn === "function")
for (const prop in mapping) {
const fn = mapping[prop]
if (typeof fn === "function") {
fn(props[prop], oldProps[prop], getSystem())
}
}
}
export const makeMappedContainer = (getSystem, getStore, memGetComponent, getComponents, componentName, mapping) => {
return class extends Component {
export const withMappedContainer = (getSystem, getStore, memGetComponent) => (componentName, mapping) => {
const { fn } = getSystem()
const WrappedComponent = memGetComponent(componentName, "root")
class WithMappedContainer extends Component {
constructor(props, context) {
super(props, context)
handleProps(getSystem, mapping, props, {})
@@ -60,84 +72,44 @@ export const makeMappedContainer = (getSystem, getStore, memGetComponent, getCom
}
render() {
let cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
let Comp = memGetComponent(componentName, "root")
return <Comp {...cleanProps}/>
const cleanProps = omit(this.props, mapping ? Object.keys(mapping) : [])
return <WrappedComponent {...cleanProps} />
}
}
WithMappedContainer.displayName = `WithMappedContainer(${fn.getDisplayName(WrappedComponent)})`
return WithMappedContainer
}
export const render = (getSystem, getStore, getComponent, getComponents, domNode) => {
let App = getComponent(getSystem, getStore, getComponents, "App", "root")
export const render = (getSystem, getStore, getComponent, getComponents) => (domNode) => {
const App = getComponent(getSystem, getStore, getComponents)("App", "root")
ReactDOM.render(<App/>, domNode)
}
/**
* Creates a class component from a stateless one and wrap it with Error Boundary
* to handle errors coming from a stateless component.
*/
const createClass = (getSystem, OriginalComponent) => class extends Component {
render() {
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary")
export const getComponent = (getSystem, getStore, getComponents) => (componentName, container, config = {}) => {
return (
<ErrorBoundary targetName={OriginalComponent?.name} getComponent={getComponent}>
<OriginalComponent {...this.props} />
</ErrorBoundary>
)
}
}
const wrapRender = (getSystem, component) => {
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
const target = isStateless(component) ? createClass(getSystem, component) : component
const { render: oriRender} = target.prototype
/**
* This render method override handles errors that are throw in render method
* of class components.
*/
target.prototype.render = function render(...args) {
try {
return oriRender.apply(this, args)
} catch (error) {
const { getComponent } = getSystem()
const Fallback = getComponent("Fallback")
console.error(error) // eslint-disable-line no-console
return <Fallback name={target.name} />
}
}
return target
}
export const getComponent = (getSystem, getStore, getComponents, componentName, container, config = {}) => {
if(typeof componentName !== "string")
if (typeof componentName !== "string")
throw new TypeError("Need a string, to fetch a component. Was given a " + typeof componentName)
// getComponent has a config object as a third, optional parameter
// using the config object requires the presence of the second parameter, container
// e.g. getComponent("JsonSchema_string_whatever", false, { failSilently: true })
let component = getComponents(componentName)
const component = getComponents(componentName)
if(!component) {
if (!component) {
if (!config.failSilently) {
getSystem().log.warn("Could not find component:", componentName)
}
return null
}
if(!container)
return wrapRender(getSystem, component)
if(!container) {
return component
}
if(container === "root")
return makeContainer(getSystem, component, getStore())
if(container === "root") {
return withConnect(getSystem, component, getStore())
}
// container == truthy
return makeContainer(getSystem, wrapRender(getSystem, component))
return withConnect(getSystem, component)
}

View File

@@ -13,6 +13,7 @@ import configsPlugin from "core/plugins/configs"
import deepLinkingPlugin from "core/plugins/deep-linking"
import filter from "core/plugins/filter"
import onComplete from "core/plugins/on-complete"
import safeRender from "core/plugins/safe-render"
import OperationContainer from "core/containers/OperationContainer"
@@ -193,6 +194,7 @@ export default function() {
deepLinkingPlugin,
filter,
onComplete,
requestSnippets
requestSnippets,
safeRender(),
]
}