feat(error-handling): introduce unified and configurable error handling (#7761)
Refs #7778
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
32
src/core/plugins/safe-render/fn.jsx
Normal file
32
src/core/plugins/safe-render/fn.jsx
Normal 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
|
||||
}
|
||||
|
||||
42
src/core/plugins/safe-render/index.js
Normal file
42
src/core/plugins/safe-render/index.js
Normal 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
|
||||
1
src/core/plugins/view/fn.js
Normal file
1
src/core/plugins/view/fn.js
Normal file
@@ -0,0 +1 @@
|
||||
export const getDisplayName = (WrappedComponent) => WrappedComponent.displayName || WrappedComponent.name || "Component"
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import StandaloneLayout from "./layout"
|
||||
import TopbarPlugin from "plugins/topbar"
|
||||
import ConfigsPlugin from "corePlugins/configs"
|
||||
import SafeRenderPlugin from "core/plugins/safe-render"
|
||||
|
||||
// the Standalone preset
|
||||
|
||||
@@ -11,5 +12,13 @@ export default [
|
||||
return {
|
||||
components: { StandaloneLayout }
|
||||
}
|
||||
}
|
||||
},
|
||||
SafeRenderPlugin({
|
||||
fullOverride: true,
|
||||
componentList: [
|
||||
"Topbar",
|
||||
"StandaloneLayout",
|
||||
"onlineValidatorBadge"
|
||||
]
|
||||
})
|
||||
]
|
||||
|
||||
@@ -21,22 +21,16 @@ export default class StandaloneLayout extends React.Component {
|
||||
const Topbar = getComponent("Topbar", true)
|
||||
const BaseLayout = getComponent("BaseLayout", true)
|
||||
const OnlineValidatorBadge = getComponent("onlineValidatorBadge", true)
|
||||
const ErrorBoundary = getComponent("ErrorBoundary", true)
|
||||
|
||||
|
||||
return (
|
||||
<Container className='swagger-ui'>
|
||||
<ErrorBoundary targetName="Topbar">
|
||||
{Topbar ? <Topbar /> : null}
|
||||
</ErrorBoundary>
|
||||
{Topbar ? <Topbar /> : null}
|
||||
<BaseLayout />
|
||||
<ErrorBoundary targetName="OnlineValidatorBadge">
|
||||
<Row>
|
||||
<Col>
|
||||
<OnlineValidatorBadge />
|
||||
</Col>
|
||||
</Row>
|
||||
</ErrorBoundary>
|
||||
<Row>
|
||||
<Col>
|
||||
<OnlineValidatorBadge />
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user