Skip to content

Reduce error boundaries for connections, sources, destinations and onboarding, add retry button. #12848

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

Merged
merged 10 commits into from
May 24, 2022
Merged
98 changes: 80 additions & 18 deletions airbyte-webapp/src/components/ApiErrorBoundary/ApiErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,51 @@
import React from "react";
import { FormattedMessage } from "react-intl";
import { useQueryErrorResetBoundary } from "react-query";
import { useLocation } from "react-use";
import { LocationSensorState } from "react-use/lib/useLocation";
import styled from "styled-components";

import { Button } from "components/base/Button";

import { isVersionError } from "core/request/VersionError";
import { ErrorOccurredView } from "views/common/ErrorOccurredView";
import { ResourceNotFoundErrorBoundary } from "views/common/ResorceNotFoundErrorBoundary";
import { StartOverErrorView } from "views/common/StartOverErrorView";

type BoundaryState = { errorId?: string; message?: string };
const RetryContainer = styled.div`
margin-top: 20px;
display: flex;
justify-content: center;
`;

interface ApiErrorBoundaryState {
errorId?: string;
message?: string;
didRetry?: boolean;
}

enum ErrorId {
VersionMismatch = "version.mismatch",
ServerUnavailable = "server.unavailable",
UnknownError = "unknown",
}

class ApiErrorBoundary extends React.Component<unknown, BoundaryState> {
constructor(props: Record<string, unknown>) {
super(props);
this.state = {};
}
interface ApiErrorBoundaryProps {
hideHeader?: boolean;
}

interface ApiErrorBoundaryHookProps {
location: LocationSensorState;
onRetry?: () => void;
}

class ApiErrorBoundary extends React.Component<
ApiErrorBoundaryProps & ApiErrorBoundaryHookProps,
ApiErrorBoundaryState
> {
state: ApiErrorBoundaryState = {};

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

if (isNetworkBoundaryMessage || is502) {
return { errorId: ErrorId.ServerUnavailable };
return { errorId: ErrorId.ServerUnavailable, didRetry: false };
}

return { errorId: ErrorId.UnknownError };
return { errorId: ErrorId.UnknownError, didRetry: false };
}

componentDidUpdate(prevProps: ApiErrorBoundaryHookProps) {
const { location } = this.props;

if (location !== prevProps.location) {
this.setState({ errorId: undefined, didRetry: false });
}
}

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

render(): React.ReactNode {
if (this.state.errorId === ErrorId.VersionMismatch) {
return <ErrorOccurredView message={this.state.message} />;
const { errorId, didRetry, message } = this.state;
const { onRetry, hideHeader, children } = this.props;

if (errorId === ErrorId.VersionMismatch) {
return <ErrorOccurredView message={message} />;
}

if (this.state.errorId === ErrorId.ServerUnavailable) {
return <ErrorOccurredView message={<FormattedMessage id="webapp.cannotReachServer" />} />;
if (errorId === ErrorId.ServerUnavailable && !didRetry) {
return (
<ErrorOccurredView message={<FormattedMessage id="webapp.cannotReachServer" />} hideHeader={hideHeader}>
{onRetry && (
<RetryContainer>
<Button
onClick={() => {
this.setState({ didRetry: true, errorId: undefined });
onRetry?.();
}}
>
<FormattedMessage id="errorView.retry" />
</Button>
</RetryContainer>
)}
</ErrorOccurredView>
);
}

return !this.state.errorId ? (
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView />}>
{this.props.children}
return !errorId ? (
<ResourceNotFoundErrorBoundary errorComponent={<StartOverErrorView hideHeader={hideHeader} />}>
{children}
</ResourceNotFoundErrorBoundary>
) : (
<ErrorOccurredView message="Unknown error occurred" />
<ErrorOccurredView message={<FormattedMessage id="errorView.unknownError" />} hideHeader={hideHeader} />
);
}
}

export default ApiErrorBoundary;
const ApiErrorBoundaryWithHooks: React.FC<ApiErrorBoundaryProps> = ({ children, hideHeader }) => {
const { reset } = useQueryErrorResetBoundary();
const location = useLocation();

return (
<ApiErrorBoundary location={location} onRetry={reset} hideHeader={hideHeader}>
{children}
</ApiErrorBoundary>
);
};

export default ApiErrorBoundaryWithHooks;
12 changes: 8 additions & 4 deletions airbyte-webapp/src/components/BaseClearView/BaseClearView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,27 @@ const LogoImg = styled.img`
`;

const MainInfo = styled.div`
min-width: 550px;
display: flex;
align-items: center;
flex-direction: column;
`;

interface BaseClearViewProps {
onBackClick?: React.MouseEventHandler;
hideHeader?: boolean;
}

const BaseClearView: React.FC<BaseClearViewProps> = ({ children, onBackClick }) => {
const BaseClearView: React.FC<BaseClearViewProps> = ({ children, onBackClick, hideHeader }) => {
const { formatMessage } = useIntl();
return (
<Content>
<MainInfo>
<Link to=".." onClick={onBackClick}>
<LogoImg src="/logo.png" alt={formatMessage({ id: "ui.goBack" })} />
</Link>
{!hideHeader && (
<Link to=".." onClick={onBackClick}>
<LogoImg src="/logo.png" alt={formatMessage({ id: "ui.goBack" })} />
</Link>
)}
{children}
</MainInfo>
<Version />
Expand Down
2 changes: 2 additions & 0 deletions airbyte-webapp/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,9 @@
"docs.notFoundError": "We were not able to receive docs. Please click the link above to open docs on our website",
"errorView.notFound": "Resource not found.",
"errorView.notAuthorized": "You don’t have permission to access this page.",
"errorView.retry": "Retry",
"errorView.unknown": "Unknown",
"errorView.unknownError": "Unknown error occurred",

"ui.goBack": "Go back",
"ui.input.showPassword": "Show password",
Expand Down
41 changes: 22 additions & 19 deletions airbyte-webapp/src/packages/cloud/cloudRoutes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { Suspense, useMemo } from "react";
import { Navigate, Route, Routes, useLocation } from "react-router-dom";
import { useEffectOnce } from "react-use";

import ApiErrorBoundary from "components/ApiErrorBoundary";
import LoadingPage from "components/LoadingPage";

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

return (
<Routes>
<Route path={`${RoutePaths.Destination}/*`} element={<DestinationPage />} />
<Route path={`${RoutePaths.Source}/*`} element={<SourcesPage />} />
<Route path={`${RoutePaths.Connections}/*`} element={<ConnectionPage />} />
<Route path={`${RoutePaths.Settings}/*`} element={<CloudSettingsPage />} />
<Route path={CloudRoutes.Credits} element={<CreditsPage />} />

{workspace.displaySetupWizard && (
<Route
path={RoutePaths.Onboarding}
element={
<OnboardingServiceProvider>
<OnboardingPage />
</OnboardingServiceProvider>
}
/>
)}
<Route path="*" element={<Navigate to={mainNavigate} replace />} />
</Routes>
<ApiErrorBoundary>
<Routes>
<Route path={`${RoutePaths.Destination}/*`} element={<DestinationPage />} />
<Route path={`${RoutePaths.Source}/*`} element={<SourcesPage />} />
<Route path={`${RoutePaths.Connections}/*`} element={<ConnectionPage />} />
<Route path={`${RoutePaths.Settings}/*`} element={<CloudSettingsPage />} />
<Route path={CloudRoutes.Credits} element={<CreditsPage />} />

{workspace.displaySetupWizard && (
<Route
path={RoutePaths.Onboarding}
element={
<OnboardingServiceProvider>
<OnboardingPage />
</OnboardingServiceProvider>
}
/>
)}
<Route path="*" element={<Navigate to={mainNavigate} replace />} />
</Routes>
</ApiErrorBoundary>
);
};

Expand Down
4 changes: 3 additions & 1 deletion airbyte-webapp/src/packages/cloud/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,7 @@
"password.validation": "Your password is too weak",
"password.invalid": "Invalid password",

"verifyEmail.notification": "You successfully verified your email. Thank you."
"verifyEmail.notification": "You successfully verified your email. Thank you.",

"webapp.cannotReachServer": "Cannot reach server."
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { FormattedMessage } from "react-intl";
import { Route, Routes } from "react-router-dom";

import { DropDownRow, LoadingPage, PageTitle } from "components";
import ApiErrorBoundary from "components/ApiErrorBoundary";
import Breadcrumbs from "components/Breadcrumbs";
import { ItemTabs, StepsTypes, TableItemTitle } from "components/ConnectorBlocks";
import { ConnectorIcon } from "components/ConnectorIcon";
Expand Down Expand Up @@ -92,38 +93,40 @@ const DestinationItemPage: React.FC = () => {
/>

<Suspense fallback={<LoadingPage />}>
<Routes>
<Route
path="/settings"
element={
<DestinationSettings
currentDestination={destination}
connectionsWithDestination={connectionsWithDestination}
/>
}
/>
<Route
index
element={
<>
<TableItemTitle
type="source"
dropDownData={sourcesDropDownData}
onSelect={onSelect}
entityName={destination.name}
entity={destination.destinationName}
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
releaseStage={destinationDefinition.releaseStage}
<ApiErrorBoundary hideHeader>
<Routes>
<Route
path="/settings"
element={
<DestinationSettings
currentDestination={destination}
connectionsWithDestination={connectionsWithDestination}
/>
{connectionsWithDestination.length ? (
<DestinationConnectionTable connections={connectionsWithDestination} />
) : (
<Placeholder resource={ResourceTypes.Sources} />
)}
</>
}
/>
</Routes>
}
/>
<Route
index
element={
<>
<TableItemTitle
type="source"
dropDownData={sourcesDropDownData}
onSelect={onSelect}
entityName={destination.name}
entity={destination.destinationName}
entityIcon={destinationDefinition.icon ? getIcon(destinationDefinition.icon) : null}
releaseStage={destinationDefinition.releaseStage}
/>
{connectionsWithDestination.length ? (
<DestinationConnectionTable connections={connectionsWithDestination} />
) : (
<Placeholder resource={ResourceTypes.Sources} />
)}
</>
}
/>
</Routes>
</ApiErrorBoundary>
</Suspense>
</ConnectorDocumentationWrapper>
);
Expand Down
Loading