Skip to content
Snippets Groups Projects
Unverified Commit f24cd6bc authored by Ryan Laurie's avatar Ryan Laurie Committed by GitHub
Browse files

Add error boundary to dashcards and visualizations (#27936)

* add error boundary to dashcards

* fix type error

* update error catching behavior and allow override component

* update default error component

* add error boundary to visualization component
parent 45a1d369
Branches
Tags
No related merge requests found
import React, { ErrorInfo, ReactNode, useState } from "react";
import React, { ReactNode, useState } from "react";
import { connect } from "react-redux";
import { Location } from "history";
......@@ -30,6 +30,7 @@ import { ContentViewportContext } from "metabase/core/context/ContentViewportCon
import { AppErrorDescriptor, State } from "metabase-types/store";
import { AppContainer, AppContent, AppContentContainer } from "./App.styled";
import ErrorBoundary from "./ErrorBoundary";
const getErrorComponent = ({ status, data, context }: AppErrorDescriptor) => {
if (status === 403 || data?.error_code === "unauthorized") {
......@@ -80,18 +81,6 @@ const mapDispatchToProps: AppDispatchProps = {
onError: setErrorPage,
};
class ErrorBoundary extends React.Component<{
onError: (errorInfo: ErrorInfo) => void;
}> {
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
this.props.onError(errorInfo);
}
render() {
return this.props.children;
}
}
function App({
errorPage,
isAdminApp,
......
import React, { ErrorInfo } from "react";
import { SmallGenericError } from "metabase/containers/ErrorPages";
export default class ErrorBoundary extends React.Component<
{
onError?: (errorInfo: ErrorInfo) => void;
errorComponent?: Element;
},
{
hasError: boolean;
errorDetails: string;
}
> {
constructor(props: any) {
super(props);
this.state = {
hasError: false,
errorDetails: "",
};
}
static getDerivedStateFromError() {
// Update state so the next render will show the fallback UI.
return { hasError: true };
}
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
// if we don't provide a specific onError action, the component will display a generic error message
if (this.props.onError) {
this.props.onError(errorInfo);
} else {
this.setState({
hasError: true,
errorDetails: errorInfo.componentStack,
});
}
}
render() {
if (this.state.hasError && !this.props.onError) {
const ErrorComponent = this.props.errorComponent;
if (!ErrorComponent) {
return <SmallGenericError />;
}
return ErrorComponent;
}
return this.props.children;
}
}
/* eslint-disable react/prop-types */
import React from "react";
import { t } from "ttag";
import { color } from "metabase/lib/colors";
import Icon from "metabase/components/Icon";
import EmptyState from "metabase/components/EmptyState";
......@@ -54,3 +55,14 @@ export const Archived = ({ entityName, linkTo }) => (
/>
</ErrorPageRoot>
);
export const SmallGenericError = ({ message = t`Something's gone wrong` }) => (
<ErrorPageRoot>
<Icon
name="warning"
size={32}
color={color("text-light")}
tooltip={message}
/>
</ErrorPageRoot>
);
......@@ -20,6 +20,8 @@ import { isVirtualDashCard } from "metabase/dashboard/utils";
import { isActionCard } from "metabase/actions/utils";
import ErrorBoundary from "metabase/ErrorBoundary";
import type {
Card,
CardId,
......@@ -311,46 +313,48 @@ function DashCard({
]);
return (
<DashCardRoot
className="Card rounded flex flex-column hover-parent hover--visibility"
hasHiddenBackground={hasHiddenBackground}
isNightMode={isNightMode}
isUsuallySlow={isSlow === "usually-slow"}
ref={cardRootRef}
>
{renderDashCardActions()}
<DashCardVisualization
dashboard={dashboard}
dashcard={dashcard}
series={series}
parameterValues={parameterValues}
parameterValuesBySlug={parameterValuesBySlug}
metadata={metadata}
mode={mode}
gridSize={gridSize}
gridItemWidth={gridItemWidth}
totalNumGridCols={totalNumGridCols}
headerIcon={headerIcon}
expectedDuration={expectedDuration}
error={error}
isAction={isAction}
isEmbed={isEmbed}
isEditing={isEditing}
isEditingDashCardClickBehavior={isEditingDashCardClickBehavior}
isEditingDashboardLayout={isEditingDashboardLayout}
isEditingParameter={isEditingParameter}
isClickBehaviorSidebarOpen={isClickBehaviorSidebarOpen}
isSlow={isSlow}
isPreviewing={isPreviewingCard}
isFullscreen={isFullscreen}
<ErrorBoundary>
<DashCardRoot
className="Card rounded flex flex-column hover-parent hover--visibility"
hasHiddenBackground={hasHiddenBackground}
isNightMode={isNightMode}
isMobile={isMobile}
showClickBehaviorSidebar={showClickBehaviorSidebar}
onUpdateVisualizationSettings={onUpdateVisualizationSettings}
onChangeCardAndRun={changeCardAndRunHandler}
onChangeLocation={onChangeLocation}
/>
</DashCardRoot>
isUsuallySlow={isSlow === "usually-slow"}
ref={cardRootRef}
>
{renderDashCardActions()}
<DashCardVisualization
dashboard={dashboard}
dashcard={dashcard}
series={series}
parameterValues={parameterValues}
parameterValuesBySlug={parameterValuesBySlug}
metadata={metadata}
mode={mode}
gridSize={gridSize}
gridItemWidth={gridItemWidth}
totalNumGridCols={totalNumGridCols}
headerIcon={headerIcon}
expectedDuration={expectedDuration}
error={error}
isAction={isAction}
isEmbed={isEmbed}
isEditing={isEditing}
isEditingDashCardClickBehavior={isEditingDashCardClickBehavior}
isEditingDashboardLayout={isEditingDashboardLayout}
isEditingParameter={isEditingParameter}
isClickBehaviorSidebarOpen={isClickBehaviorSidebarOpen}
isSlow={isSlow}
isPreviewing={isPreviewingCard}
isFullscreen={isFullscreen}
isNightMode={isNightMode}
isMobile={isMobile}
showClickBehaviorSidebar={showClickBehaviorSidebar}
onUpdateVisualizationSettings={onUpdateVisualizationSettings}
onChangeCardAndRun={changeCardAndRunHandler}
onChangeLocation={onChangeLocation}
/>
</DashCardRoot>
</ErrorBoundary>
);
}
......
......@@ -30,6 +30,7 @@ import { isSameSeries } from "metabase/visualizations/lib/utils";
import { getMode } from "metabase/modes/lib/modes";
import { getFont } from "metabase/styled-components/selectors";
import ErrorBoundary from "metabase/ErrorBoundary";
import Question from "metabase-lib/Question";
import Mode from "metabase-lib/Mode";
import { datasetContainsNoResults } from "metabase-lib/queries/utils/dataset";
......@@ -448,74 +449,76 @@ class Visualization extends React.PureComponent {
(replacementContent && (dashcard.size_y !== 1 || isMobile));
return (
<VisualizationRoot className={className} style={style}>
{!!hasHeader && (
<VisualizationHeader>
<ChartCaption
<ErrorBoundary>
<VisualizationRoot className={className} style={style}>
{!!hasHeader && (
<VisualizationHeader>
<ChartCaption
series={series}
settings={settings}
icon={headerIcon}
actionButtons={extra}
onChangeCardAndRun={
this.props.onChangeCardAndRun && !replacementContent
? this.handleOnChangeCardAndRun
: null
}
/>
</VisualizationHeader>
)}
{replacementContent ? (
replacementContent
) : isDashboard && noResults ? (
<NoResultsView isSmall={small} />
) : error ? (
<ErrorView
error={error}
icon={errorIcon}
isSmall={small}
isDashboard={isDashboard}
/>
) : loading ? (
<LoadingView expectedDuration={expectedDuration} isSlow={isSlow} />
) : (
<CardVisualization
{...this.props}
// NOTE: CardVisualization class used to target ExplicitSize HOC
className="CardVisualization flex-full flex-basis-none"
isPlaceholder={isPlaceholder}
series={series}
settings={settings}
icon={headerIcon}
actionButtons={extra}
card={series[0].card} // convenience for single-series visualizations
data={series[0].data} // convenience for single-series visualizations
hovered={hovered}
clicked={clicked}
headerIcon={hasHeader ? null : headerIcon}
onHoverChange={this.handleHoverChange}
onVisualizationClick={this.handleVisualizationClick}
visualizationIsClickable={this.visualizationIsClickable}
onRenderError={this.onRenderError}
onRender={this.onRender}
onActionDismissal={this.hideActions}
gridSize={gridSize}
onChangeCardAndRun={
this.props.onChangeCardAndRun && !replacementContent
this.props.onChangeCardAndRun
? this.handleOnChangeCardAndRun
: null
}
/>
</VisualizationHeader>
)}
{replacementContent ? (
replacementContent
) : isDashboard && noResults ? (
<NoResultsView isSmall={small} />
) : error ? (
<ErrorView
error={error}
icon={errorIcon}
isSmall={small}
isDashboard={isDashboard}
/>
) : loading ? (
<LoadingView expectedDuration={expectedDuration} isSlow={isSlow} />
) : (
<CardVisualization
{...this.props}
// NOTE: CardVisualization class used to target ExplicitSize HOC
className="CardVisualization flex-full flex-basis-none"
isPlaceholder={isPlaceholder}
series={series}
settings={settings}
card={series[0].card} // convenience for single-series visualizations
data={series[0].data} // convenience for single-series visualizations
hovered={hovered}
clicked={clicked}
headerIcon={hasHeader ? null : headerIcon}
onHoverChange={this.handleHoverChange}
onVisualizationClick={this.handleVisualizationClick}
visualizationIsClickable={this.visualizationIsClickable}
onRenderError={this.onRenderError}
onRender={this.onRender}
onActionDismissal={this.hideActions}
gridSize={gridSize}
onChangeCardAndRun={
this.props.onChangeCardAndRun
? this.handleOnChangeCardAndRun
: null
}
/>
)}
<ChartTooltip series={series} hovered={hovered} settings={settings} />
{this.props.onChangeCardAndRun && (
<ChartClickActions
clicked={clicked}
clickActions={clickActions}
onChangeCardAndRun={this.handleOnChangeCardAndRun}
onClose={this.hideActions}
series={series}
onUpdateVisualizationSettings={onUpdateVisualizationSettings}
/>
)}
</VisualizationRoot>
)}
<ChartTooltip series={series} hovered={hovered} settings={settings} />
{this.props.onChangeCardAndRun && (
<ChartClickActions
clicked={clicked}
clickActions={clickActions}
onChangeCardAndRun={this.handleOnChangeCardAndRun}
onClose={this.hideActions}
series={series}
onUpdateVisualizationSettings={onUpdateVisualizationSettings}
/>
)}
</VisualizationRoot>
</ErrorBoundary>
);
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment