Add a new handle when the backend application is offline (#1550)
✨ (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
This commit is contained in:
parent
fd7a0fcbff
commit
496c2aae3e
7 changed files with 262 additions and 41 deletions
|
|
@ -3,6 +3,7 @@ import "reactflow/dist/style.css";
|
||||||
import "./App.css";
|
import "./App.css";
|
||||||
|
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
import { ErrorBoundary } from "react-error-boundary";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import ErrorAlert from "./alerts/error";
|
import ErrorAlert from "./alerts/error";
|
||||||
import NoticeAlert from "./alerts/notice";
|
import NoticeAlert from "./alerts/notice";
|
||||||
import SuccessAlert from "./alerts/success";
|
import SuccessAlert from "./alerts/success";
|
||||||
|
|
@ -43,6 +44,9 @@ export default function App() {
|
||||||
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
const refreshVersion = useDarkStore((state) => state.refreshVersion);
|
||||||
const refreshStars = useDarkStore((state) => state.refreshStars);
|
const refreshStars = useDarkStore((state) => state.refreshStars);
|
||||||
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
const checkHasStore = useStoreStore((state) => state.checkHasStore);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const [isLoadingHealth, setIsLoadingHealth] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshStars();
|
refreshStars();
|
||||||
|
|
@ -60,11 +64,12 @@ export default function App() {
|
||||||
}, [isAuthenticated]);
|
}, [isAuthenticated]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
checkApplicationHealth();
|
||||||
// Timer to call getHealth every 5 seconds
|
// Timer to call getHealth every 5 seconds
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
getHealth()
|
getHealth()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
if (fetchError) setFetchError(false);
|
onHealthCheck();
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setFetchError(true);
|
setFetchError(true);
|
||||||
|
|
@ -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 (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
<div className="flex h-full flex-col">
|
<div className="flex h-full flex-col">
|
||||||
|
|
@ -86,20 +115,29 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
{fetchError ? (
|
<>
|
||||||
<FetchErrorComponent
|
{
|
||||||
description={FETCH_ERROR_DESCRIPION}
|
<FetchErrorComponent
|
||||||
message={FETCH_ERROR_MESSAGE}
|
description={FETCH_ERROR_DESCRIPION}
|
||||||
></FetchErrorComponent>
|
message={FETCH_ERROR_MESSAGE}
|
||||||
) : isLoading ? (
|
openModal={fetchError}
|
||||||
<div className="loading-page-panel">
|
setRetry={() => {
|
||||||
<LoadingComponent remSize={50} />
|
checkApplicationHealth();
|
||||||
</div>
|
}}
|
||||||
) : (
|
isLoadingHealth={isLoadingHealth}
|
||||||
<>
|
></FetchErrorComponent>
|
||||||
<Router />
|
}
|
||||||
</>
|
|
||||||
)}
|
{isLoading ? (
|
||||||
|
<div className="loading-page-panel">
|
||||||
|
<LoadingComponent remSize={50} />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Router />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="app-div">
|
<div className="app-div">
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,51 @@
|
||||||
|
import BaseModal from "../../modals/baseModal";
|
||||||
import { fetchErrorComponentType } from "../../types/components";
|
import { fetchErrorComponentType } from "../../types/components";
|
||||||
import IconComponent from "../genericIconComponent";
|
import IconComponent from "../genericIconComponent";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
export default function FetchErrorComponent({
|
export default function FetchErrorComponent({
|
||||||
message,
|
message,
|
||||||
description,
|
description,
|
||||||
|
openModal,
|
||||||
|
setRetry,
|
||||||
|
isLoadingHealth,
|
||||||
}: fetchErrorComponentType) {
|
}: fetchErrorComponentType) {
|
||||||
return (
|
return (
|
||||||
<div role="status" className="m-auto flex flex-col items-center">
|
<>
|
||||||
<IconComponent className={`h-16 w-16`} name="Unplug"></IconComponent>
|
<BaseModal size="small-h-full" open={openModal} type="modal">
|
||||||
<br></br>
|
<BaseModal.Content>
|
||||||
<span className="text-lg text-almost-medium-blue">{message}</span>
|
<div role="status" className="m-auto flex flex-col items-center">
|
||||||
<span className="text-lg text-almost-medium-blue">{description}</span>
|
<IconComponent
|
||||||
</div>
|
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
src/frontend/src/components/ui/dialog-with-no-close.tsx
Normal file
119
src/frontend/src/components/ui/dialog-with-no-close.tsx
Normal file
|
|
@ -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,
|
||||||
|
};
|
||||||
|
|
@ -9,6 +9,12 @@ import {
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "../../components/ui/dialog";
|
} from "../../components/ui/dialog";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Dialog as Modal,
|
||||||
|
DialogContent as ModalContent,
|
||||||
|
} from "../../components/ui/dialog-with-no-close";
|
||||||
|
|
||||||
import { modalHeaderType } from "../../types/components";
|
import { modalHeaderType } from "../../types/components";
|
||||||
import { cn } from "../../utils/utils";
|
import { cn } from "../../utils/utils";
|
||||||
|
|
||||||
|
|
@ -76,6 +82,7 @@ interface BaseModalProps {
|
||||||
|
|
||||||
disable?: boolean;
|
disable?: boolean;
|
||||||
onChangeOpenModal?: (open?: boolean) => void;
|
onChangeOpenModal?: (open?: boolean) => void;
|
||||||
|
type?: "modal" | "dialog";
|
||||||
}
|
}
|
||||||
function BaseModal({
|
function BaseModal({
|
||||||
open,
|
open,
|
||||||
|
|
@ -83,6 +90,7 @@ function BaseModal({
|
||||||
children,
|
children,
|
||||||
size = "large",
|
size = "large",
|
||||||
onChangeOpenModal,
|
onChangeOpenModal,
|
||||||
|
type = "dialog",
|
||||||
}: BaseModalProps) {
|
}: BaseModalProps) {
|
||||||
const headerChild = React.Children.toArray(children).find(
|
const headerChild = React.Children.toArray(children).find(
|
||||||
(child) => (child as React.ReactElement).type === Header
|
(child) => (child as React.ReactElement).type === Header
|
||||||
|
|
@ -156,22 +164,43 @@ function BaseModal({
|
||||||
|
|
||||||
//UPDATE COLORS AND STYLE CLASSSES
|
//UPDATE COLORS AND STYLE CLASSSES
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<>
|
||||||
{triggerChild}
|
{type === "modal" ? (
|
||||||
<DialogContent className={cn(minWidth, "duration-300")}>
|
<Modal open={open} onOpenChange={setOpen}>
|
||||||
<div className="truncate-doubleline word-break-break-word">
|
{triggerChild}
|
||||||
{headerChild}
|
<ModalContent className={cn(minWidth, "duration-300")}>
|
||||||
</div>
|
<div className="truncate-doubleline word-break-break-word">
|
||||||
<div
|
{headerChild}
|
||||||
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
</div>
|
||||||
>
|
<div
|
||||||
{ContentChild}
|
className={`flex flex-col ${height!} w-full transition-all duration-300`}
|
||||||
</div>
|
>
|
||||||
{ContentFooter && (
|
{ContentChild}
|
||||||
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
</div>
|
||||||
)}
|
{ContentFooter && (
|
||||||
</DialogContent>
|
<div className="flex flex-row-reverse">{ContentFooter}</div>
|
||||||
</Dialog>
|
)}
|
||||||
|
</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>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ const useFlowsManagerStore = create<FlowsManagerStoreType>((set, get) => ({
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
|
set({ isLoading: false });
|
||||||
useAlertStore.getState().setErrorData({
|
useAlertStore.getState().setErrorData({
|
||||||
title: "Could not load flows from database",
|
title: "Could not load flows from database",
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,6 @@ export const useTypesStore = create<TypesStoreType>((set, get) => ({
|
||||||
resolve();
|
resolve();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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.error("An error has occurred while fetching types.");
|
||||||
console.log(error);
|
console.log(error);
|
||||||
reject();
|
reject();
|
||||||
|
|
|
||||||
|
|
@ -665,6 +665,9 @@ export type ApiKey = {
|
||||||
export type fetchErrorComponentType = {
|
export type fetchErrorComponentType = {
|
||||||
message: string;
|
message: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
openModal?: boolean;
|
||||||
|
setRetry: () => void;
|
||||||
|
isLoadingHealth: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type dropdownButtonPropsType = {
|
export type dropdownButtonPropsType = {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue