Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.

Commit 5ad8d67

Browse files
edmunditoJordan Scott
authored and
Jordan Scott
committed
Reduce error boundaries for connections, sources, destinations and onboarding, add retry button. (airbytehq#12848)
* Add retry logic to ApiErrorBoundary component and move boundary one level deeper * Simplify cannot reach message error message on cloud. * Update ApiErrorBoundary to use Button component, add strings to en.json * Add clearOnLocationChange flag to ApiErrorBoundary to clear the error when navigating to another page * Add error boundaries to DestinationItemPage and SourceItemPage * Update ApiErrorBoundary to include ability to reset enabled by optional prop withRetry * Remove location and retry options from ApiErrorBoundary * Add ApiErrorBoundary to onboarding page * Show titles on most onboarding steps even in error * Add the ability to hide the header for ApiErrorBoundaries and BaseClearView * Remove retry logic from state change in ApiErrorBoundary
1 parent e77d4b6 commit 5ad8d67

File tree

14 files changed

+275
-220
lines changed

14 files changed

+275
-220
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,51 @@
11
import React from "react";
22
import { FormattedMessage } from "react-intl";
3+
import { useQueryErrorResetBoundary } from "react-query";
4+
import { useLocation } from "react-use";
5+
import { LocationSensorState } from "react-use/lib/useLocation";
6+
import styled from "styled-components";
7+
8+
import { Button } from "components/base/Button";
39

410
import { isVersionError } from "core/request/VersionError";
511
import { ErrorOccurredView } from "views/common/ErrorOccurredView";
612
import { ResourceNotFoundErrorBoundary } from "views/common/ResorceNotFoundErrorBoundary";
713
import { StartOverErrorView } from "views/common/StartOverErrorView";
814

9-
type BoundaryState = { errorId?: string; message?: string };
15+
const RetryContainer = styled.div`
16+
margin-top: 20px;
17+
display: flex;
18+
justify-content: center;
19+
`;
20+
21+
interface ApiErrorBoundaryState {
22+
errorId?: string;
23+
message?: string;
24+
didRetry?: boolean;
25+
}
1026

1127
enum ErrorId {
1228
VersionMismatch = "version.mismatch",
1329
ServerUnavailable = "server.unavailable",
1430
UnknownError = "unknown",
1531
}
1632

17-
class ApiErrorBoundary extends React.Component<unknown, BoundaryState> {
18-
constructor(props: Record<string, unknown>) {
19-
super(props);
20-
this.state = {};
21-
}
33+
interface ApiErrorBoundaryProps {
34+
hideHeader?: boolean;
35+
}
36+
37+
interface ApiErrorBoundaryHookProps {
38+
location: LocationSensorState;
39+
onRetry?: () => void;
40+
}
41+
42+
class ApiErrorBoundary extends React.Component<
43+
ApiErrorBoundaryProps & ApiErrorBoundaryHookProps,
44+
ApiErrorBoundaryState
45+
> {
46+
state: ApiErrorBoundaryState = {};
2247

23-
static getDerivedStateFromError(error: { message: string; status?: number; __type?: string }): BoundaryState {
48+
static getDerivedStateFromError(error: { message: string; status?: number; __type?: string }): ApiErrorBoundaryState {
2449
// Update state so the next render will show the fallback UI.
2550
if (isVersionError(error)) {
2651
return { errorId: ErrorId.VersionMismatch, message: error.message };
@@ -30,32 +55,69 @@ class ApiErrorBoundary extends React.Component<unknown, BoundaryState> {
3055
const is502 = error.status === 502;
3156

3257
if (isNetworkBoundaryMessage || is502) {
33-
return { errorId: ErrorId.ServerUnavailable };
58+
return { errorId: ErrorId.ServerUnavailable, didRetry: false };
3459
}
3560

36-
return { errorId: ErrorId.UnknownError };
61+
return { errorId: ErrorId.UnknownError, didRetry: false };
62+
}
63+
64+
componentDidUpdate(prevProps: ApiErrorBoundaryHookProps) {
65+
const { location } = this.props;
66+
67+
if (location !== prevProps.location) {
68+
this.setState({ errorId: undefined, didRetry: false });
69+
}
3770
}
3871

3972
// eslint-disable-next-line @typescript-eslint/no-empty-function
4073
componentDidCatch(): void {}
4174

4275
render(): React.ReactNode {
43-
if (this.state.errorId === ErrorId.VersionMismatch) {
44-
return <ErrorOccurredView message={this.state.message} />;
76+
const { errorId, didRetry, message } = this.state;
77+
const { onRetry, hideHeader, children } = this.props;
78+
79+
if (errorId === ErrorId.VersionMismatch) {
80+
return <ErrorOccurredView message={message} />;
4581
}
4682

47-
if (this.state.errorId === ErrorId.ServerUnavailable) {
48-
return <ErrorOccurredView message={<FormattedMessage id="webapp.cannotReachServer" />} />;
83+
if (errorId === ErrorId.ServerUnavailable && !didRetry) {
84+
return (
85+
<ErrorOccurredView message={<FormattedMessage id="webapp.cannotReachServer" />} hideHeader={hideHeader}>
86+
{onRetry && (
87+
<RetryContainer>
88+
<Button
89+
onClick={() => {
90+
this.setState({ didRetry: true, errorId: undefined });
91+
onRetry?.();
92+
}}
93+
>
94+
<FormattedMessage id="errorView.retry" />
95+
</Button>
96+
</RetryContainer>
97+
)}
98+
</ErrorOccurredView>
99+
);
49100
}
50101

51-
return !this.state.errorId ? (
52-
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView />}>
53-
{this.props.children}
102+
return !errorId ? (
103+
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView hideHeader={hideHeader} />}>
104+
{children}
54105
</ResourceNotFoundErrorBoundary>
55106
) : (
56-
<ErrorOccurredView message="Unknown error occurred" />
107+
<ErrorOccurredView message={<FormattedMessage id="errorView.unknownError" />} hideHeader={hideHeader} />
57108
);
58109
}
59110
}
60111

61-
export default ApiErrorBoundary;
112+
const ApiErrorBoundaryWithHooks: React.FC<ApiErrorBoundaryProps> = ({ children, hideHeader }) => {
113+
const { reset } = useQueryErrorResetBoundary();
114+
const location = useLocation();
115+
116+
return (
117+
<ApiErrorBoundary location={location} onRetry={reset} hideHeader={hideHeader}>
118+
{children}
119+
</ApiErrorBoundary>
120+
);
121+
};
122+
123+
export default ApiErrorBoundaryWithHooks;

airbyte-webapp/src/components/BaseClearView/BaseClearView.tsx

+8-4
Original file line numberDiff line numberDiff line change
@@ -22,23 +22,27 @@ const LogoImg = styled.img`
2222
`;
2323

2424
const MainInfo = styled.div`
25+
min-width: 550px;
2526
display: flex;
2627
align-items: center;
2728
flex-direction: column;
2829
`;
2930

3031
interface BaseClearViewProps {
3132
onBackClick?: React.MouseEventHandler;
33+
hideHeader?: boolean;
3234
}
3335

34-
const BaseClearView: React.FC<BaseClearViewProps> = ({ children, onBackClick }) => {
36+
const BaseClearView: React.FC<BaseClearViewProps> = ({ children, onBackClick, hideHeader }) => {
3537
const { formatMessage } = useIntl();
3638
return (
3739
<Content>
3840
<MainInfo>
39-
<Link to=".." onClick={onBackClick}>
40-
<LogoImg src="/logo.png" alt={formatMessage({ id: "ui.goBack" })} />
41-
</Link>
41+
{!hideHeader && (
42+
<Link to=".." onClick={onBackClick}>
43+
<LogoImg src="/logo.png" alt={formatMessage({ id: "ui.goBack" })} />
44+
</Link>
45+
)}
4246
{children}
4347
</MainInfo>
4448
<Version />

airbyte-webapp/src/locales/en.json

+2
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,9 @@
494494
"docs.notFoundError": "We were not able to receive docs. Please click the link above to open docs on our website",
495495
"errorView.notFound": "Resource not found.",
496496
"errorView.notAuthorized": "You don’t have permission to access this page.",
497+
"errorView.retry": "Retry",
497498
"errorView.unknown": "Unknown",
499+
"errorView.unknownError": "Unknown error occurred",
498500

499501
"ui.goBack": "Go back",
500502
"ui.input.showPassword": "Show password",

airbyte-webapp/src/packages/cloud/cloudRoutes.tsx

+22-19
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import React, { Suspense, useMemo } from "react";
22
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
33
import { useEffectOnce } from "react-use";
44

5+
import ApiErrorBoundary from "components/ApiErrorBoundary";
56
import LoadingPage from "components/LoadingPage";
67

78
import { useAnalyticsIdentifyUser, useAnalyticsRegisterValues } from "hooks/services/Analytics/useAnalyticsService";
@@ -81,25 +82,27 @@ const MainRoutes: React.FC = () => {
8182
useFeatureRegisterValues(features);
8283

8384
return (
84-
<Routes>
85-
<Route path={`${RoutePaths.Destination}/*`} element={<DestinationPage />} />
86-
<Route path={`${RoutePaths.Source}/*`} element={<SourcesPage />} />
87-
<Route path={`${RoutePaths.Connections}/*`} element={<ConnectionPage />} />
88-
<Route path={`${RoutePaths.Settings}/*`} element={<CloudSettingsPage />} />
89-
<Route path={CloudRoutes.Credits} element={<CreditsPage />} />
90-
91-
{workspace.displaySetupWizard && (
92-
<Route
93-
path={RoutePaths.Onboarding}
94-
element={
95-
<OnboardingServiceProvider>
96-
<OnboardingPage />
97-
</OnboardingServiceProvider>
98-
}
99-
/>
100-
)}
101-
<Route path="*" element={<Navigate to={mainNavigate} replace />} />
102-
</Routes>
85+
<ApiErrorBoundary>
86+
<Routes>
87+
<Route path={`${RoutePaths.Destination}/*`} element={<DestinationPage />} />
88+
<Route path={`${RoutePaths.Source}/*`} element={<SourcesPage />} />
89+
<Route path={`${RoutePaths.Connections}/*`} element={<ConnectionPage />} />
90+
<Route path={`${RoutePaths.Settings}/*`} element={<CloudSettingsPage />} />
91+
<Route path={CloudRoutes.Credits} element={<CreditsPage />} />
92+
93+
{workspace.displaySetupWizard && (
94+
<Route
95+
path={RoutePaths.Onboarding}
96+
element={
97+
<OnboardingServiceProvider>
98+
<OnboardingPage />
99+
</OnboardingServiceProvider>
100+
}
101+
/>
102+
)}
103+
<Route path="*" element={<Navigate to={mainNavigate} replace />} />
104+
</Routes>
105+
</ApiErrorBoundary>
103106
);
104107
};
105108

airbyte-webapp/src/packages/cloud/locales/en.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -118,5 +118,7 @@
118118
"password.validation": "Your password is too weak",
119119
"password.invalid": "Invalid password",
120120

121-
"verifyEmail.notification": "You successfully verified your email. Thank you."
121+
"verifyEmail.notification": "You successfully verified your email. Thank you.",
122+
123+
"webapp.cannotReachServer": "Cannot reach server."
122124
}

airbyte-webapp/src/pages/DestinationPage/pages/DestinationItemPage/DestinationItemPage.tsx

+34-31
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl";
33
import { Route, Routes } from "react-router-dom";
44

55
import { DropDownRow, LoadingPage, PageTitle } from "components";
6+
import ApiErrorBoundary from "components/ApiErrorBoundary";
67
import Breadcrumbs from "components/Breadcrumbs";
78
import { ItemTabs, StepsTypes, TableItemTitle } from "components/ConnectorBlocks";
89
import { ConnectorIcon } from "components/ConnectorIcon";
@@ -92,38 +93,40 @@ const DestinationItemPage: React.FC = () => {
9293
/>
9394

9495
<Suspense fallback={<LoadingPage />}>
95-
<Routes>
96-
<Route
97-
path="/settings"
98-
element={
99-
<DestinationSettings
100-
currentDestination={destination}
101-
connectionsWithDestination={connectionsWithDestination}
102-
/>
103-
}
104-
/>
105-
<Route
106-
index
107-
element={
108-
<>
109-
<TableItemTitle
110-
type="source"
111-
dropDownData={sourcesDropDownData}
112-
onSelect={onSelect}
113-
entityName={destination.name}
114-
entity={destination.destinationName}
115-
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
116-
releaseStage={destinationDefinition.releaseStage}
96+
<ApiErrorBoundary hideHeader>
97+
<Routes>
98+
<Route
99+
path="/settings"
100+
element={
101+
<DestinationSettings
102+
currentDestination={destination}
103+
connectionsWithDestination={connectionsWithDestination}
117104
/>
118-
{connectionsWithDestination.length ? (
119-
<DestinationConnectionTable connections={connectionsWithDestination} />
120-
) : (
121-
<Placeholder resource={ResourceTypes.Sources} />
122-
)}
123-
</>
124-
}
125-
/>
126-
</Routes>
105+
}
106+
/>
107+
<Route
108+
index
109+
element={
110+
<>
111+
<TableItemTitle
112+
type="source"
113+
dropDownData={sourcesDropDownData}
114+
onSelect={onSelect}
115+
entityName={destination.name}
116+
entity={destination.destinationName}
117+
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
118+
releaseStage={destinationDefinition.releaseStage}
119+
/>
120+
{connectionsWithDestination.length ? (
121+
<DestinationConnectionTable connections={connectionsWithDestination} />
122+
) : (
123+
<Placeholder resource={ResourceTypes.Sources} />
124+
)}
125+
</>
126+
}
127+
/>
128+
</Routes>
129+
</ApiErrorBoundary>
127130
</Suspense>
128131
</ConnectorDocumentationWrapper>
129132
);

0 commit comments

Comments
 (0)