Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: error boundaries #7671

Merged
merged 4 commits into from
Nov 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/jest/jest.unit.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
'<rootDir>/test/unit/components/online-validator-badge.jsx',
'<rootDir>/test/unit/components/live-response.jsx',
],
silent: true, // set to `false` to allow console.* calls to be printed
transformIgnorePatterns: [
'/node_modules/(?!(react-syntax-highlighter)/)'
]
Expand Down
10 changes: 6 additions & 4 deletions src/core/components/layouts/base.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ 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()

Expand All @@ -36,7 +37,7 @@ export default class BaseLayout extends React.Component {
const loadingStatus = specSelectors.loadingStatus()

let loadingMessage = null

if(loadingStatus === "loading") {
loadingMessage = <div className="info">
<div className="loading-container">
Expand Down Expand Up @@ -85,8 +86,8 @@ export default class BaseLayout extends React.Component {
const hasSecurityDefinitions = !!specSelectors.securityDefinitions()

return (

<div className='swagger-ui'>
<ErrorBoundary targetName="BaseLayout">
<SvgAssets />
<VersionPragmaFilter isSwagger2={isSwagger2} isOAS3={isOAS3} alsoShow={<Errors/>}>
<Errors/>
Expand Down Expand Up @@ -119,7 +120,8 @@ export default class BaseLayout extends React.Component {
</Col>
</Row>
</VersionPragmaFilter>
</div>
)
</ErrorBoundary>
</div>
)
}
}
45 changes: 45 additions & 0 deletions src/core/plugins/view/error-boundary.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import PropTypes from "prop-types"
import React, { Component } from "react"

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 }
}

componentDidCatch(error, errorInfo) {
console.error(error, errorInfo) // eslint-disable-line no-console
}

render() {
const { getComponent, targetName, children } = this.props
const FallbackComponent = getComponent("Fallback")

if (this.state.hasError) {
return <FallbackComponent name={targetName} />
}

return children
}
}
ErrorBoundary.propTypes = {
targetName: PropTypes.string,
getComponent: PropTypes.func,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
])
}
ErrorBoundary.defaultProps = {
targetName: "this component",
getComponent: () => Fallback,
children: null,
}

export default ErrorBoundary
13 changes: 13 additions & 0 deletions src/core/plugins/view/fallback.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import React from "react"
import PropTypes from "prop-types"

const Fallback = ({ name }) => (
<div className="fallback">
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
</div>
)
Fallback.propTypes = {
name: PropTypes.string.isRequired,
}

export default Fallback
9 changes: 8 additions & 1 deletion src/core/plugins/view/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
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
Expand All @@ -14,6 +17,10 @@ export default function({getComponents, getStore, getSystem}) {
getComponent: memGetComponent,
makeMappedContainer: memMakeMappedContainer,
render: render.bind(null, getSystem, getStore, getComponent, getComponents),
}
},
components: {
ErrorBoundary,
Fallback,
},
}
}
86 changes: 30 additions & 56 deletions src/core/plugins/view/root-injects.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import React, { Component } from "react"
import PropTypes from "prop-types"
import ReactDOM from "react-dom"
import { connect, Provider } from "react-redux"
import omit from "lodash/omit"

const SystemWrapper = (getSystem, ComponentToWrap ) => class extends Component {
render() {
return <ComponentToWrap {...getSystem() } {...this.props} {...this.context} />
return <ComponentToWrap {...getSystem()} {...this.props} {...this.context} />
}
}

const RootWrapper = (reduxStore, ComponentToWrap) => class extends Component {
const RootWrapper = (getSystem, reduxStore, ComponentToWrap) => class extends Component {
render() {
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary", true)

return (
<Provider store={reduxStore}>
<ComponentToWrap {...this.props} {...this.context} />
<ErrorBoundary targetName={ComponentToWrap?.name}>
<ComponentToWrap {...this.props} {...this.context} />
</ErrorBoundary>
</Provider>
)
}
Expand All @@ -30,7 +34,7 @@ const makeContainer = (getSystem, component, reduxStore) => {
let wrappedWithSystem = SystemWrapper(getSystem, component, reduxStore)
let connected = connect( mapStateToProps )(wrappedWithSystem)
if(reduxStore)
return RootWrapper(reduxStore, connected)
return RootWrapper(getSystem, reduxStore, connected)
return connected
}

Expand Down Expand Up @@ -66,73 +70,43 @@ export const makeMappedContainer = (getSystem, getStore, memGetComponent, getCom
}

export const render = (getSystem, getStore, getComponent, getComponents, domNode) => {
let App = (getComponent(getSystem, getStore, getComponents, "App", "root"))
ReactDOM.render(( <App/> ), domNode)
let App = getComponent(getSystem, getStore, getComponents, "App", "root")
ReactDOM.render(<App/>, domNode)
}

class ErrorBoundary extends Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}

static getDerivedStateFromError(error) {
return { hasError: true, error }
}

componentDidCatch(error, errorInfo) {
console.error(error, errorInfo) // eslint-disable-line no-console
}

/**
* 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() {
if (this.state.hasError) {
return <Fallback name={this.props.targetName} />
}
const { getComponent } = getSystem()
const ErrorBoundary = getComponent("ErrorBoundary")

return this.props.children
}
}
ErrorBoundary.propTypes = {
targetName: PropTypes.string,
children: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.node),
PropTypes.node,
])
}
ErrorBoundary.defaultProps = {
targetName: "this component",
children: null,
}

const Fallback = ({ name }) => (
<div className="fallback">
😱 <i>Could not render { name === "t" ? "this component" : name }, see the console.</i>
</div>
)
Fallback.propTypes = {
name: PropTypes.string.isRequired,
}

// Render try/catch wrapper
const createClass = OriginalComponent => class extends Component {
render() {
return (
<ErrorBoundary targetName={OriginalComponent?.name}>
<ErrorBoundary targetName={OriginalComponent?.name} getComponent={getComponent}>
<OriginalComponent {...this.props} />
</ErrorBoundary>
)
}
}

const wrapRender = (component) => {
const wrapRender = (getSystem, component) => {
const isStateless = component => !(component.prototype && component.prototype.isReactComponent)
const target = isStateless(component) ? createClass(component) : component
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} />
}
Expand All @@ -159,11 +133,11 @@ export const getComponent = (getSystem, getStore, getComponents, componentName,
}

if(!container)
return wrapRender(component)
return wrapRender(getSystem, component)

if(container === "root")
return makeContainer(getSystem, component, getStore())

// container == truthy
return makeContainer(getSystem, wrapRender(component))
return makeContainer(getSystem, wrapRender(getSystem, component))
}
32 changes: 16 additions & 16 deletions src/standalone/layout.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@


import React from "react"
import PropTypes from "prop-types"

Expand All @@ -16,27 +14,29 @@ export default class StandaloneLayout extends React.Component {
}

render() {
let { getComponent } = this.props

let Container = getComponent("Container")
let Row = getComponent("Row")
let Col = getComponent("Col")

const { getComponent } = this.props
const Container = getComponent("Container")
const Row = getComponent("Row")
const Col = getComponent("Col")
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'>
{Topbar ? <Topbar /> : null}
<BaseLayout />
<Row>
<Col>
<OnlineValidatorBadge />
</Col>
</Row>
<ErrorBoundary targetName="Topbar">
{Topbar ? <Topbar /> : null}
</ErrorBoundary>
<BaseLayout />
<ErrorBoundary targetName="OnlineValidatorBadge">
<Row>
<Col>
<OnlineValidatorBadge />
</Col>
</Row>
</ErrorBoundary>
</Container>
)
}
Expand Down
Loading