Skip to content
Snippets Groups Projects
Unverified Commit e3fef011 authored by Alexander Polyankin's avatar Alexander Polyankin Committed by GitHub
Browse files

Fix global error page (#25749)

parent a136ddc4
Branches
Tags
No related merge requests found
import React, { ErrorInfo, ReactNode, useRef, useState } from "react";
import { t } from "ttag";
import React, { ErrorInfo, ReactNode, useState } from "react";
import { connect } from "react-redux";
import { Location } from "history";
import AppErrorCard from "metabase/components/AppErrorCard/AppErrorCard";
import ScrollToTop from "metabase/hoc/ScrollToTop";
import {
Archived,
......@@ -20,6 +17,7 @@ import {
getIsAppBarVisible,
getIsNavBarVisible,
} from "metabase/selectors/app";
import { setErrorPage } from "metabase/redux/app";
import { useOnMount } from "metabase/hooks/use-on-mount";
import { initializeIframeResizer } from "metabase/lib/dom";
......@@ -57,12 +55,16 @@ interface AppStateProps {
isNavBarVisible: boolean;
}
interface AppDispatchProps {
onError: (error: unknown) => void;
}
interface AppRouterOwnProps {
location: Location;
children: ReactNode;
}
type AppProps = AppStateProps & AppRouterOwnProps;
type AppProps = AppStateProps & AppDispatchProps & AppRouterOwnProps;
const mapStateToProps = (
state: State,
......@@ -74,13 +76,15 @@ const mapStateToProps = (
isNavBarVisible: getIsNavBarVisible(state, props),
});
const mapDispatchToProps: AppDispatchProps = {
onError: setErrorPage,
};
class ErrorBoundary extends React.Component<{
onError: (errorInfo: ErrorInfo & Error) => void;
countError: () => void;
onError: (errorInfo: ErrorInfo) => void;
}> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError({ ...error, componentStack: errorInfo.componentStack });
this.props.countError();
this.props.onError(errorInfo);
}
render() {
......@@ -88,57 +92,36 @@ class ErrorBoundary extends React.Component<{
}
}
const MAX_ERRORS_ALLOWED = 3;
function App({
errorPage,
isAdminApp,
isAppBarVisible,
isNavBarVisible,
children,
onError,
}: AppProps) {
const [viewportElement, setViewportElement] = useState<HTMLElement | null>();
const [errorInfo, setErrorInfo] = useState<(ErrorInfo & Error) | null>(null);
const [errorCount, setErrorCount] = useState(0);
const countError = () => setErrorCount(prev => prev + 1);
useOnMount(() => {
initializeIframeResizer();
});
return (
<ErrorBoundary onError={setErrorInfo} countError={countError}>
<ErrorBoundary onError={onError}>
<ScrollToTop>
<AppContainer className="spread">
<AppBanner />
{isAppBarVisible && <AppBar isNavBarVisible={isNavBarVisible} />}
<AppContentContainer isAdminApp={isAdminApp}>
{isNavBarVisible && <Navbar />}
{errorCount < MAX_ERRORS_ALLOWED ? (
<AppContent ref={setViewportElement}>
<ContentViewportContext.Provider
value={viewportElement ?? null}
>
{errorPage ? getErrorComponent(errorPage) : children}
</ContentViewportContext.Provider>
</AppContent>
) : (
getErrorComponent({
status: 500,
data: {
error_code: "looping error",
message:
(errorInfo?.message ?? "") +
" " +
(errorInfo?.componentStack ?? ""),
},
})
)}
<AppContent ref={setViewportElement}>
<ContentViewportContext.Provider value={viewportElement ?? null}>
{errorPage ? getErrorComponent(errorPage) : children}
</ContentViewportContext.Provider>
</AppContent>
<UndoListing />
<StatusListing />
</AppContentContainer>
<AppErrorCard errorInfo={errorInfo} />
</AppContainer>
</ScrollToTop>
</ErrorBoundary>
......@@ -147,4 +130,5 @@ function App({
export default connect<AppStateProps, unknown, AppRouterOwnProps, State>(
mapStateToProps,
mapDispatchToProps,
)(App);
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import { t } from "ttag";
import { isCypressActive, isProduction } from "metabase/env";
import BodyComponent from "metabase/components/BodyComponent";
import Icon from "metabase/components/Icon";
import Button from "metabase/core/components/Button";
import { FullscreenCard, FixedCard } from "./AppErrorCard.styled";
const CardComponent = isCypressActive ? FullscreenCard : FixedCard;
const isInEnvWhereErrorShouldBeShown = !isProduction || isCypressActive;
export default BodyComponent(AppErrorCard);
AppErrorCard.propTypes = {
errorInfo: PropTypes.shape({
componentStack: PropTypes.string,
}),
};
function AppErrorCard({ errorInfo }) {
const [hasNewError, setHasNewError] = useState(false);
useEffect(() => {
if (errorInfo) {
setHasNewError(true);
}
}, [errorInfo]);
const showError = hasNewError && isInEnvWhereErrorShouldBeShown;
return showError ? (
<CardComponent>
<div className="flex justify-between align-center mb1">
<div className="text-error flex align-center">
<Icon name="info_outline" mr={1} size="20" />
<h2>{t`Something went wrong`}</h2>
</div>
<Button
onlyIcon
icon="close"
className="pl1"
onClick={() => setHasNewError(false)}
/>
</div>
<pre>{errorInfo.componentStack}</pre>
</CardComponent>
) : null;
}
import styled from "@emotion/styled";
import Card from "metabase/components/Card";
export const FullscreenCard = styled(Card)`
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100vh;
padding: 1rem;
z-index: 5;
overflow: auto;
`;
export const FixedCard = styled(Card)`
position: fixed;
right: 0;
bottom: 0;
width: 350px;
height: 500px;
margin: 1rem;
padding: 1rem;
z-index: 5;
overflow: auto;
`;
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment