Skip to content
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
66 changes: 66 additions & 0 deletions src/components/Configure/ErrorBoundary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/* eslint-disable react/prop-types */
import React, { Component, ReactNode } from "react";

import { ComponentContainerError } from "./ComponentContainer";

interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}

interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}

/**
* React Error Boundary component that catches JavaScript errors anywhere in the child component tree
* and displays a fallback UI instead of crashing the whole application.
*
* This is especially important for the InstallIntegration component to prevent errors in hooks
* or rendering from crashing the parent application.
*/
export class InstallIntegrationErrorBoundary extends Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, error: null };
}

static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI
return { hasError: true, error };
}

componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void {
// Log error to console for debugging
console.error("InstallIntegration Error Boundary caught an error:", error);
console.error("[Error Info]:", errorInfo);

// Call optional error callback
this.props.onError?.(error, errorInfo);
}

render(): ReactNode {
if (this.state.hasError) {
// Use custom fallback if provided, otherwise use default
if (this.props.fallback) {
return this.props.fallback;
}

return (
<ComponentContainerError
message={
this.state.error?.message ||
"Something went wrong. Please try again later."
}
/>
);
}

return this.props.children;
}
}
26 changes: 17 additions & 9 deletions src/components/Configure/InstallIntegration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
ComponentContainerLoading,
} from "./ComponentContainer";
import { InstallationContent } from "./content/InstallationContent";
import { InstallIntegrationErrorBoundary } from "./ErrorBoundary";
import { ConditionalHasConfigurationLayout } from "./layout/ConditionalHasConfigurationLayout/ConditionalHasConfigurationLayout";
import { ProtectedConnectionLayout } from "./layout/ProtectedConnectionLayout";
import { ObjectManagementNav } from "./nav/ObjectManagementNav";
Expand Down Expand Up @@ -165,15 +166,22 @@ export function InstallIntegration({
};

return (
// eventually will use the headless providers for integration, consumer, and group etc
<InstallationProvider
integration={integration}
consumerRef={consumerRef}
consumerName={consumerName}
groupRef={groupRef}
groupName={groupName}
// catch errors in the InstallIntegrationContent component
<InstallIntegrationErrorBoundary
fallback={
<ComponentContainerError message="Something went wrong, couldn't find integration information" />
}
>
<InstallIntegrationContent {...props} />
</InstallationProvider>
{/* eventually will use the headless providers for integration, consumer, and group etc */}
<InstallationProvider
integration={integration}
consumerRef={consumerRef}
consumerName={consumerName}
groupRef={groupRef}
groupName={groupName}
>
<InstallIntegrationContent {...props} />
</InstallationProvider>
</InstallIntegrationErrorBoundary>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,10 @@ export function ConfigureInstallationBase({
const isStateNew = isModified || isCreateMode || isSelectedReadObjectComplete;

// if the selected read object has an error in the manifest, it should not be saved
const isSelectedReadObjectError = !!hydratedRevision?.content?.read?.objects?.find(
(obj) => obj.objectName === selectedObjectName,
)?.error;
const isSelectedReadObjectError =
!!hydratedRevision?.content?.read?.objects?.find(
(obj) => obj.objectName === selectedObjectName,
)?.error;

// should the save button be disabled?
const isDisabled =
Expand Down
1 change: 1 addition & 0 deletions src/components/Configure/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { InstallIntegration } from "./InstallIntegration";
export { InstallIntegrationErrorBoundary } from "./ErrorBoundary";
4 changes: 3 additions & 1 deletion src/headless/installation/useCreateInstallation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export function useCreateInstallation() {
onSettled?: () => void;
}) => {
if (installation) {
const error = new Error("Installation already created. Try updating instead.");
const error = new Error(
"Installation already created. Try updating instead.",
);
onError?.(error);
onSettled?.();
return;
Expand Down