Skip to content

Commit

Permalink
Add a new handle when the backend application is offline (#1550)
Browse files Browse the repository at this point in the history
✨ (App.tsx): Import the `useNavigate` hook from `react-router-dom` to enable programmatic navigation within the app.
📝 (App.tsx): Add comments to explain the purpose of the `isLoadingHealth` state variable and the `checkApplicationHealth` function.
📝 (App.tsx): Add comments to explain the purpose of the `onHealthCheck` function.
🐛 (App.tsx): Fix a bug where the `checkApplicationHealth` function was not being called when the component mounts.
🐛 (App.tsx): Fix a bug where the `onHealthCheck` function was not being called when the health check was successful.
📝 (App.tsx): Add comments to explain the purpose of the `checkApplicationHealth` function and the `onHealthCheck` function.
✨ (fetchErrorComponent/index.tsx): Import the `BaseModal` component from the `modals/baseModal` module to display the fetch error component in a modal.
✨ (fetchErrorComponent/index.tsx): Import the `Button` component from the `ui/button` module to display a retry button in the fetch error component.
✨ (fetchErrorComponent/index.tsx): Add a retry button to the fetch error component to allow the user to retry the failed request.
✨ (ui/dialog-with-no-close.tsx): Create a new file `ui/dialog-with-no-close.tsx` to define a custom dialog component without a close button.
✨ (ui/dialog-with-no-close.tsx): Define the `Dialog`, `DialogTrigger`, `DialogPortal`, `DialogOverlay`, `DialogContent`, `DialogHeader`, `DialogFooter`, `DialogTitle`, and `DialogDescription` components for the custom dialog component.

📝 (baseModal/index.tsx): add support for a new type prop to switch between modal and dialog mode
🐛 (baseModal/index.tsx): fix typo in import statement for Modal and ModalContent components
♻️ (baseModal/index.tsx): refactor BaseModal component to conditionally render either Modal or Dialog based on the type prop
🐛 (flowsManagerStore.ts): fix issue where isLoading state was not being set to false after catching an error
🐛 (typesStore.ts): fix issue where error alert was not being shown when fetching types failed
🐛 (typesStore.ts): remove unnecessary error alert when fetching types failed
✨ (components/index.ts): add new properties to fetchErrorComponentType to support opening a modal, retrying, and showing loading state
  • Loading branch information
Cristhianzl committed Mar 22, 2024
1 parent fd7a0fc commit 496c2aa
Show file tree
Hide file tree
Showing 7 changed files with 262 additions and 41 deletions.
68 changes: 53 additions & 15 deletions src/frontend/src/App.tsx
Expand Up @@ -3,6 +3,7 @@ import "reactflow/dist/style.css";
import "./App.css";

import { ErrorBoundary } from "react-error-boundary";
import { useNavigate } from "react-router-dom";
import ErrorAlert from "./alerts/error";
import NoticeAlert from "./alerts/notice";
import SuccessAlert from "./alerts/success";
Expand Down Expand Up @@ -43,6 +44,9 @@ export default function App() {
const refreshVersion = useDarkStore((state) => state.refreshVersion);
const refreshStars = useDarkStore((state) => state.refreshStars);
const checkHasStore = useStoreStore((state) => state.checkHasStore);
const navigate = useNavigate();

const [isLoadingHealth, setIsLoadingHealth] = useState(false);

useEffect(() => {
refreshStars();
Expand All @@ -60,11 +64,12 @@ export default function App() {
}, [isAuthenticated]);

useEffect(() => {
checkApplicationHealth();
// Timer to call getHealth every 5 seconds
const timer = setInterval(() => {
getHealth()
.then(() => {
if (fetchError) setFetchError(false);
onHealthCheck();
})
.catch(() => {
setFetchError(true);
Expand All @@ -77,6 +82,30 @@ export default function App() {
};
}, []);

const checkApplicationHealth = () => {
setIsLoadingHealth(true);
getHealth()
.then(() => {
onHealthCheck();
})
.catch(() => {
setFetchError(true);
});

setTimeout(() => {
setIsLoadingHealth(false);
}, 2000);
};

const onHealthCheck = () => {
setFetchError(false);
//This condition is necessary to avoid infinite loop on starter page when the application is not healthy
if (isLoading === true && window.location.pathname === "/") {
navigate("/flows");
window.location.reload();
}
};

return (
//need parent component with width and height
<div className="flex h-full flex-col">
Expand All @@ -86,20 +115,29 @@ export default function App() {
}}
FallbackComponent={CrashErrorComponent}
>
{fetchError ? (
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
></FetchErrorComponent>
) : isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
<>
{
<FetchErrorComponent
description={FETCH_ERROR_DESCRIPION}
message={FETCH_ERROR_MESSAGE}
openModal={fetchError}
setRetry={() => {
checkApplicationHealth();
}}
isLoadingHealth={isLoadingHealth}
></FetchErrorComponent>
}

{isLoading ? (
<div className="loading-page-panel">
<LoadingComponent remSize={50} />
</div>
) : (
<>
<Router />
</>
)}
</>
</ErrorBoundary>
<div></div>
<div className="app-div">
Expand Down
47 changes: 41 additions & 6 deletions src/frontend/src/components/fetchErrorComponent/index.tsx
@@ -1,16 +1,51 @@
import BaseModal from "../../modals/baseModal";
import { fetchErrorComponentType } from "../../types/components";
import IconComponent from "../genericIconComponent";
import { Button } from "../ui/button";

export default function FetchErrorComponent({
message,
description,
openModal,
setRetry,
isLoadingHealth,
}: fetchErrorComponentType) {
return (
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">{description}</span>
</div>
<>
<BaseModal size="small-h-full" open={openModal} type="modal">
<BaseModal.Content>
<div role="status" className="m-auto flex flex-col items-center">
<IconComponent
className={`h-16 w-16`}
name="Unplug"
></IconComponent>
<br></br>
<span className="text-lg text-almost-medium-blue">{message}</span>
<span className="text-lg text-almost-medium-blue">
{description}
</span>
</div>
</BaseModal.Content>

<BaseModal.Footer>
<div className="m-auto">
<Button
disabled={isLoadingHealth}
onClick={() => {
setRetry();
}}
>
{isLoadingHealth ? (
<div>
<IconComponent name={"Loader2"} className={"animate-spin"} />
</div>
) : (
"Retry"
)}
</Button>
</div>
</BaseModal.Footer>
</BaseModal>
</>
);
}
119 changes: 119 additions & 0 deletions src/frontend/src/components/ui/dialog-with-no-close.tsx
@@ -0,0 +1,119 @@
import * as DialogPrimitive from "@radix-ui/react-dialog";
import * as React from "react";
import { cn } from "../../utils/utils";

const Dialog = DialogPrimitive.Root;

const DialogTrigger = DialogPrimitive.Trigger;

const DialogPortal = ({
children,
...props
}: DialogPrimitive.DialogPortalProps) => (
<DialogPrimitive.Portal {...props}>
<div className="nopan nodelete nodrag noundo nocopy fixed inset-0 z-50 flex items-start justify-center sm:items-center">
{children}
</div>
</DialogPrimitive.Portal>
);
DialogPortal.displayName = DialogPrimitive.Portal.displayName;

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"nopan nodelete nodrag noundo nocopy fixed inset-0 bottom-0 left-0 right-0 top-0 z-50 overflow-auto bg-blur-shared backdrop-blur-sm data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
));
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName;

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 flex w-full max-w-lg flex-col gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] sm:rounded-lg md:w-full",
className
)}
{...props}
>
{children}
</DialogPrimitive.Content>
</DialogPortal>
));
DialogContent.displayName = DialogPrimitive.Content.displayName;

const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
);
DialogHeader.displayName = "DialogHeader";

const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
);
DialogFooter.displayName = "DialogFooter";

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
));
DialogTitle.displayName = DialogPrimitive.Title.displayName;

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
));
DialogDescription.displayName = DialogPrimitive.Description.displayName;

export {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
};
61 changes: 45 additions & 16 deletions src/frontend/src/modals/baseModal/index.tsx
Expand Up @@ -9,6 +9,12 @@ import {
DialogTitle,
DialogTrigger,
} from "../../components/ui/dialog";

import {
Dialog as Modal,
DialogContent as ModalContent,
} from "../../components/ui/dialog-with-no-close";

import { modalHeaderType } from "../../types/components";
import { cn } from "../../utils/utils";

Expand Down Expand Up @@ -76,13 +82,15 @@ interface BaseModalProps {

disable?: boolean;
onChangeOpenModal?: (open?: boolean) => void;
type?: "modal" | "dialog";
}
function BaseModal({
open,
setOpen,
children,
size = "large",
onChangeOpenModal,
type = "dialog",
}: BaseModalProps) {
const headerChild = React.Children.toArray(children).find(
(child) => (child as React.ReactElement).type === Header
Expand Down Expand Up @@ -156,22 +164,43 @@ function BaseModal({

//UPDATE COLORS AND STYLE CLASSSES
return (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</DialogContent>
</Dialog>
<>
{type === "modal" ? (
<Modal open={open} onOpenChange={setOpen}>
{triggerChild}
<ModalContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</ModalContent>
</Modal>
) : (
<Dialog open={open} onOpenChange={setOpen}>
{triggerChild}
<DialogContent className={cn(minWidth, "duration-300")}>
<div className="truncate-doubleline word-break-break-word">
{headerChild}
</div>
<div
className={`flex flex-col ${height!} w-full transition-all duration-300`}
>
{ContentChild}
</div>
{ContentFooter && (
<div className="flex flex-row-reverse">{ContentFooter}</div>
)}
</DialogContent>
</Dialog>
)}
</>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/frontend/src/stores/flowsManagerStore.ts
Expand Up @@ -88,6 +88,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
}
})
.catch((e) => {
set({ isLoading: false });
useAlertStore.getState().setErrorData({
title: "Could not load flows from database",
});
Expand Down
4 changes: 0 additions & 4 deletions src/frontend/src/stores/typesStore.ts
Expand Up @@ -27,10 +27,6 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
resolve();
})
.catch((error) => {
useAlertStore.getState().setErrorData({
title: "An error has occurred while fetching types.",
list: ["Please refresh the page."],
});
console.error("An error has occurred while fetching types.");
console.log(error);
reject();
Expand Down

0 comments on commit 496c2aa

Please sign in to comment.