From c05feb25c58587a768b1283769f5fd5bad18c6bb Mon Sep 17 00:00:00 2001 From: cristhianzl Date: Thu, 21 Mar 2024 22:40:00 -0300 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20(App.tsx):=20Import=20the=20`useNav?= =?UTF-8?q?igate`=20hook=20from=20`react-router-dom`=20to=20enable=20progr?= =?UTF-8?q?ammatic=20navigation=20within=20the=20app.=20=F0=9F=93=9D=20(Ap?= =?UTF-8?q?p.tsx):=20Add=20comments=20to=20explain=20the=20purpose=20of=20?= =?UTF-8?q?the=20`isLoadingHealth`=20state=20variable=20and=20the=20`check?= =?UTF-8?q?ApplicationHealth`=20function.=20=F0=9F=93=9D=20(App.tsx):=20Ad?= =?UTF-8?q?d=20comments=20to=20explain=20the=20purpose=20of=20the=20`onHea?= =?UTF-8?q?lthCheck`=20function.=20=F0=9F=90=9B=20(App.tsx):=20Fix=20a=20b?= =?UTF-8?q?ug=20where=20the=20`checkApplicationHealth`=20function=20was=20?= =?UTF-8?q?not=20being=20called=20when=20the=20component=20mounts.=20?= =?UTF-8?q?=F0=9F=90=9B=20(App.tsx):=20Fix=20a=20bug=20where=20the=20`onHe?= =?UTF-8?q?althCheck`=20function=20was=20not=20being=20called=20when=20the?= =?UTF-8?q?=20health=20check=20was=20successful.=20=F0=9F=93=9D=20(App.tsx?= =?UTF-8?q?):=20Add=20comments=20to=20explain=20the=20purpose=20of=20the?= =?UTF-8?q?=20`checkApplicationHealth`=20function=20and=20the=20`onHealthC?= =?UTF-8?q?heck`=20function.=20=E2=9C=A8=20(fetchErrorComponent/index.tsx)?= =?UTF-8?q?:=20Import=20the=20`BaseModal`=20component=20from=20the=20`moda?= =?UTF-8?q?ls/baseModal`=20module=20to=20display=20the=20fetch=20error=20c?= =?UTF-8?q?omponent=20in=20a=20modal.=20=E2=9C=A8=20(fetchErrorComponent/i?= =?UTF-8?q?ndex.tsx):=20Import=20the=20`Button`=20component=20from=20the?= =?UTF-8?q?=20`ui/button`=20module=20to=20display=20a=20retry=20button=20i?= =?UTF-8?q?n=20the=20fetch=20error=20component.=20=E2=9C=A8=20(fetchErrorC?= =?UTF-8?q?omponent/index.tsx):=20Add=20a=20retry=20button=20to=20the=20fe?= =?UTF-8?q?tch=20error=20component=20to=20allow=20the=20user=20to=20retry?= =?UTF-8?q?=20the=20failed=20request.=20=E2=9C=A8=20(ui/dialog-with-no-clo?= =?UTF-8?q?se.tsx):=20Create=20a=20new=20file=20`ui/dialog-with-no-close.t?= =?UTF-8?q?sx`=20to=20define=20a=20custom=20dialog=20component=20without?= =?UTF-8?q?=20a=20close=20button.=20=E2=9C=A8=20(ui/dialog-with-no-close.t?= =?UTF-8?q?sx):=20Define=20the=20`Dialog`,=20`DialogTrigger`,=20`DialogPor?= =?UTF-8?q?tal`,=20`DialogOverlay`,=20`DialogContent`,=20`DialogHeader`,?= =?UTF-8?q?=20`DialogFooter`,=20`DialogTitle`,=20and=20`DialogDescription`?= =?UTF-8?q?=20components=20for=20the=20custom=20dialog=20component.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 📝 (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 --- src/frontend/src/App.tsx | 68 +++++++--- .../components/fetchErrorComponent/index.tsx | 47 ++++++- .../components/ui/dialog-with-no-close.tsx | 119 ++++++++++++++++++ src/frontend/src/modals/baseModal/index.tsx | 53 ++++++-- src/frontend/src/stores/flowsManagerStore.ts | 1 + src/frontend/src/stores/typesStore.ts | 4 - src/frontend/src/types/components/index.ts | 3 + 7 files changed, 258 insertions(+), 37 deletions(-) create mode 100644 src/frontend/src/components/ui/dialog-with-no-close.tsx diff --git a/src/frontend/src/App.tsx b/src/frontend/src/App.tsx index be45e2eabf..fe4b9628d6 100644 --- a/src/frontend/src/App.tsx +++ b/src/frontend/src/App.tsx @@ -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"; @@ -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(); @@ -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); @@ -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
@@ -86,20 +115,29 @@ export default function App() { }} FallbackComponent={CrashErrorComponent} > - {fetchError ? ( - - ) : isLoading ? ( -
- -
- ) : ( - <> - - - )} + <> + { + { + checkApplicationHealth(); + }} + isLoadingHealth={isLoadingHealth} + > + } + + {isLoading ? ( +
+ +
+ ) : ( + <> + + + )} +
diff --git a/src/frontend/src/components/fetchErrorComponent/index.tsx b/src/frontend/src/components/fetchErrorComponent/index.tsx index 6004d9dfca..956de62700 100644 --- a/src/frontend/src/components/fetchErrorComponent/index.tsx +++ b/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 ( -
- -

- {message} - {description} -
+ <> + + +
+ +

+ {message} + + {description} + +
+
+ + +
+ +
+
+
+ ); } diff --git a/src/frontend/src/components/ui/dialog-with-no-close.tsx b/src/frontend/src/components/ui/dialog-with-no-close.tsx new file mode 100644 index 0000000000..929172e008 --- /dev/null +++ b/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) => ( + +
+ {children} +
+
+); +DialogPortal.displayName = DialogPrimitive.Portal.displayName; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +}; diff --git a/src/frontend/src/modals/baseModal/index.tsx b/src/frontend/src/modals/baseModal/index.tsx index 695d4a72e4..ba8f70fb2d 100644 --- a/src/frontend/src/modals/baseModal/index.tsx +++ b/src/frontend/src/modals/baseModal/index.tsx @@ -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"; type ContentProps = { children: ReactNode }; @@ -75,6 +81,7 @@ interface BaseModalProps { disable?: boolean; onChangeOpenModal?: (open?: boolean) => void; + type?: "modal" | "dialog"; } function BaseModal({ open, @@ -82,6 +89,7 @@ function BaseModal({ children, size = "large", onChangeOpenModal, + type = "dialog", }: BaseModalProps) { const headerChild = React.Children.toArray(children).find( (child) => (child as React.ReactElement).type === Header @@ -155,18 +163,39 @@ function BaseModal({ //UPDATE COLORS AND STYLE CLASSSES return ( - - {triggerChild} - -
- {headerChild} -
-
{ContentChild}
- {ContentFooter && ( -
{ContentFooter}
- )} -
-
+ <> + {type === "modal" ? ( + + {triggerChild} + +
+ {headerChild} +
+
+ {ContentChild} +
+ {ContentFooter && ( +
{ContentFooter}
+ )} +
+
+ ) : ( + + {triggerChild} + +
+ {headerChild} +
+
+ {ContentChild} +
+ {ContentFooter && ( +
{ContentFooter}
+ )} +
+
+ )} + ); } diff --git a/src/frontend/src/stores/flowsManagerStore.ts b/src/frontend/src/stores/flowsManagerStore.ts index 2c9fca428f..37851c849d 100644 --- a/src/frontend/src/stores/flowsManagerStore.ts +++ b/src/frontend/src/stores/flowsManagerStore.ts @@ -85,6 +85,7 @@ const useFlowsManagerStore = create((set, get) => ({ } }) .catch((e) => { + set({ isLoading: false }); useAlertStore.getState().setErrorData({ title: "Could not load flows from database", }); diff --git a/src/frontend/src/stores/typesStore.ts b/src/frontend/src/stores/typesStore.ts index 557a3a30d6..808a5d6469 100644 --- a/src/frontend/src/stores/typesStore.ts +++ b/src/frontend/src/stores/typesStore.ts @@ -27,10 +27,6 @@ export const useTypesStore = create((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(); diff --git a/src/frontend/src/types/components/index.ts b/src/frontend/src/types/components/index.ts index ab32dab625..4bdf56b320 100644 --- a/src/frontend/src/types/components/index.ts +++ b/src/frontend/src/types/components/index.ts @@ -665,6 +665,9 @@ export type ApiKey = { export type fetchErrorComponentType = { message: string; description: string; + openModal?: boolean; + setRetry: () => void; + isLoadingHealth: boolean; }; export type dropdownButtonPropsType = {