From 398773ea083eb715225673c0184a3b98884aaaa2 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Fri, 14 Jun 2024 01:12:28 +0200 Subject: [PATCH 1/8] notifying frontend about backend error looks better --- reflex/components/core/banner.py | 14 ++++++++++++++ reflex/components/core/banner.pyi | 6 +++--- reflex/components/sonner/toast.py | 30 ++++++++++++++++++++++++++++-- reflex/components/sonner/toast.pyi | 21 ++++++++++++++------- reflex/config.py | 3 +++ reflex/state.py | 29 ++++++++++++++++++++++++----- 6 files changed, 86 insertions(+), 17 deletions(-) diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index b634ab75a8e..c6b46696c67 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -153,6 +153,20 @@ def add_hooks(self) -> list[str | Var]: hook, ] + @classmethod + def create(cls, *children, **props) -> Component: + """Create a connection toaster component. + + Args: + *children: The children of the component. + **props: The properties of the component. + + Returns: + The connection toaster component. + """ + Toaster.is_used = True + return super().create(*children, **props) + class ConnectionBanner(Component): """A connection banner component.""" diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi index c957bab932f..e39efd63637 100644 --- a/reflex/components/core/banner.pyi +++ b/reflex/components/core/banner.pyi @@ -199,7 +199,7 @@ class ConnectionToaster(Toaster): ] = None, **props ) -> "ConnectionToaster": - """Create the component. + """Create a connection toaster component. Args: *children: The children of the component. @@ -223,10 +223,10 @@ class ConnectionToaster(Toaster): class_name: The class name for the component. autofocus: Whether the component should take the focus once the page is loaded custom_attrs: custom attribute - **props: The props of the component. + **props: The properties of the component. Returns: - The component. + The connection toaster component. """ ... diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index 771aab6ae08..be9e78308ee 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -2,7 +2,7 @@ from __future__ import annotations -from typing import Any, Literal, Optional, Union +from typing import Any, ClassVar, Literal, Optional, Union from reflex.base import Base from reflex.components.component import Component, ComponentNamespace @@ -211,6 +211,9 @@ class Toaster(Component): # Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked. pause_when_page_is_hidden: Var[bool] + # Marked True when any Toast component is created. + is_used: ClassVar[bool] = False + def add_hooks(self) -> list[Var | str]: """Add hooks for the toaster component. @@ -231,7 +234,7 @@ def add_hooks(self) -> list[Var | str]: return [hook] @staticmethod - def send_toast(message: str, level: str | None = None, **props) -> EventSpec: + def send_toast(message: str = "", level: str | None = None, **props) -> EventSpec: """Send a toast message. Args: @@ -239,10 +242,19 @@ def send_toast(message: str, level: str | None = None, **props) -> EventSpec: level: The level of the toast. **props: The options for the toast. + Raises: + ValueError: If the Toaster component is not created. + Returns: The toast event. """ + if not Toaster.is_used: + raise ValueError( + "Toaster component must be created before sending a toast. (use `rx.toast.provider()`)" + ) toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref + if message == "" and ("title" not in props or "description" not in props): + raise ValueError("Toast message or title or descrition must be provided.") if props: args = serialize(ToastProps(**props)) # type: ignore toast = f"{toast_command}(`{message}`, {args})" @@ -331,6 +343,20 @@ def toast_dismiss(id: Var | str | None = None): ) return call_script(dismiss_action) + @classmethod + def create(cls, *children, **props) -> Component: + """Create a toaster component. + + Args: + *children: The children of the toaster. + **props: The properties of the toaster. + + Returns: + The toaster component. + """ + cls.is_used = True + return super().create(*children, **props) + # TODO: figure out why loading toast stay open forever # def toast_loading(message: str, **kwargs): diff --git a/reflex/components/sonner/toast.pyi b/reflex/components/sonner/toast.pyi index a69b5909082..593c0c5a234 100644 --- a/reflex/components/sonner/toast.pyi +++ b/reflex/components/sonner/toast.pyi @@ -7,7 +7,7 @@ from typing import Any, Dict, Literal, Optional, Union, overload from reflex.vars import Var, BaseVar, ComputedVar from reflex.event import EventChain, EventHandler, EventSpec from reflex.style import Style -from typing import Any, Literal, Optional, Union +from typing import Any, ClassVar, Literal, Optional, Union from reflex.base import Base from reflex.components.component import Component, ComponentNamespace from reflex.components.lucide.icon import Icon @@ -57,9 +57,13 @@ class ToastProps(PropsBase): def dict(self, *args, **kwargs) -> dict[str, Any]: ... class Toaster(Component): + is_used: ClassVar[bool] = False + def add_hooks(self) -> list[Var | str]: ... @staticmethod - def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ... + def send_toast( + message: str = "", level: str | None = None, **props + ) -> EventSpec: ... @staticmethod def toast_info(message: str, **kwargs): ... @staticmethod @@ -163,10 +167,10 @@ class Toaster(Component): ] = None, **props ) -> "Toaster": - """Create the component. + """Create a toaster component. Args: - *children: The children of the component. + *children: The children of the toaster. theme: the theme of the toast rich_colors: whether to show rich colors expand: whether to expand the toast @@ -187,10 +191,10 @@ class Toaster(Component): class_name: The class name for the component. autofocus: Whether the component should take the focus once the page is loaded custom_attrs: custom attribute - **props: The props of the component. + **props: The properties of the toaster. Returns: - The component. + The toaster component. """ ... @@ -205,7 +209,7 @@ class ToastNamespace(ComponentNamespace): @staticmethod def __call__( - message: str, level: Optional[str] = None, **props + message: str = "", level: Optional[str] = None, **props ) -> "Optional[EventSpec]": """Send a toast message. @@ -214,6 +218,9 @@ class ToastNamespace(ComponentNamespace): level: The level of the toast. **props: The options for the toast. + Raises: + ValueError: If the Toaster component is not created. + Returns: The toast event. """ diff --git a/reflex/config.py b/reflex/config.py index 08663aa04bb..a72f327ac56 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -219,6 +219,9 @@ class Config: # Number of gunicorn workers from user gunicorn_workers: Optional[int] = None + # Whether to + debug: bool = False + # Attributes that were explicitly set by the user. _non_default_attributes: Set[str] = pydantic.PrivateAttr(set()) diff --git a/reflex/state.py b/reflex/state.py index 56b28f9e802..f41935f5cb1 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -29,6 +29,8 @@ import dill +from reflex.config import get_config + try: import pydantic.v1 as pydantic except ModuleNotFoundError: @@ -1517,14 +1519,31 @@ async def _process_event( # If an error occurs, throw a window alert. except Exception as ex: + + def error_handler(ex: Exception): + from reflex.components.sonner.toast import Toaster, toast + + error_message = ( + [f"{type(ex).__name__}: {ex}.", "See logs for details."] + if get_config().debug is True + else ["Contact the website administrator."] + ) + if Toaster.is_used: + return toast( + level="error", + title="An error occurred.", + description="
".join(error_message), + position="top-center", + style={"width": "500px"}, + ) + else: + error_message.insert(0, "An error occurred.") + return window_alert("\n".join(error_message)) + error = traceback.format_exc() print(error) telemetry.send_error(ex, context="backend") - yield state._as_state_update( - handler, - window_alert("An error occurred. See logs for details."), - final=True, - ) + yield state._as_state_update(handler, error_handler(ex), final=True) def _mark_dirty_computed_vars(self) -> None: """Mark ComputedVars that need to be recalculated based on dirty_vars.""" From 23f97d287709e2f432b3e2b9cb2bf523075211b6 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Fri, 14 Jun 2024 17:38:52 +0200 Subject: [PATCH 2/8] fix tests --- tests/test_state.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/tests/test_state.py b/tests/test_state.py index 77f6bc606a0..b612c4c5b35 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -16,6 +16,7 @@ import reflex as rx from reflex.app import App from reflex.base import Base +from reflex.components.sonner.toast import Toaster from reflex.constants import CompileVars, RouteVar, SocketEvent from reflex.event import Event, EventHandler from reflex.state import ( @@ -1426,6 +1427,7 @@ async def test_state_with_invalid_yield(capsys): Args: capsys: Pytest fixture for capture standard streams. + """ class StateWithInvalidYield(BaseState): @@ -1444,10 +1446,25 @@ def invalid_handler(self): rx.event.Event(token="fake_token", name="invalid_handler") ): assert not update.delta - assert update.events == rx.event.fix_events( - [rx.window_alert("An error occurred. See logs for details.")], - token="", - ) + if Toaster.is_used: + assert update.events == rx.event.fix_events( + [ + rx.toast( + title="An error occurred.", + description="Contact the website administrator.", + level="error", + position="top-center", + style={"width": "500px"}, + ) # type: ignore + ], + token="", + ) + else: + assert update.events == [ + rx.window_alert( + "An error occurred.\n Contact the website administrator." + ) + ] captured = capsys.readouterr() assert "must only return/yield: None, Events or other EventHandlers" in captured.out From dcb1185c8c338401d1a984f4caa73a66fdba5f61 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Wed, 17 Jul 2024 13:57:20 +0200 Subject: [PATCH 3/8] add id for backend error toast --- reflex/app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/reflex/app.py b/reflex/app.py index bf2302be296..06ab86e944c 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -129,6 +129,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: title="An error occurred.", description="
".join(error_message), position="top-center", + id="backend_error", style={"width": "500px"}, ) # type: ignore else: From 3c5b09f548a356c0e44581dd2693503ffe69ac30 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Wed, 17 Jul 2024 15:17:32 +0200 Subject: [PATCH 4/8] fix tests --- tests/test_state.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/test_state.py b/tests/test_state.py index 7ea6ac16ee6..9e19525e8bf 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1553,6 +1553,7 @@ def invalid_handler(self): title="An error occurred.", description="Contact the website administrator.", level="error", + id="backend_error", position="top-center", style={"width": "500px"}, ) # type: ignore @@ -1560,11 +1561,14 @@ def invalid_handler(self): token="", ) else: - assert update.events == [ - rx.window_alert( - "An error occurred.\n Contact the website administrator." - ) - ] + assert update.events == rx.event.fix_events( + [ + rx.window_alert( + "An error occurred.\nContact the website administrator." + ) + ], + token="", + ) captured = capsys.readouterr() assert "must only return/yield: None, Events or other EventHandlers" in captured.out From f707756f219900d670f480efe21245d5de60bbf7 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Fri, 19 Jul 2024 18:02:04 +0200 Subject: [PATCH 5/8] do not delay call to default_overlay_component --- reflex/app.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/app.py b/reflex/app.py index 06ab86e944c..dd7e75a893f 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -201,7 +201,7 @@ class App(MiddlewareMixin, LifespanMixin, Base): # A component that is present on every page (defaults to the Connection Error banner). overlay_component: Optional[Union[Component, ComponentCallable]] = ( - default_overlay_component + default_overlay_component() ) # Error boundary component to wrap the app with. From 5d846fb462b2a6b121f7aaacad90f0818cbcfb03 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Fri, 19 Jul 2024 18:11:46 +0200 Subject: [PATCH 6/8] use prod mode to pick message --- reflex/app.py | 6 +++--- reflex/config.py | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/reflex/app.py b/reflex/app.py index dd7e75a893f..7e40a95bf7f 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -119,9 +119,9 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec: console.error(f"[Reflex Backend Exception]\n {error}\n") error_message = ( - [f"{type(exception).__name__}: {exception}.", "See logs for details."] - if get_config().debug is True - else ["Contact the website administrator."] + ["Contact the website administrator."] + if is_prod_mode() + else [f"{type(exception).__name__}: {exception}.", "See logs for details."] ) if Toaster.is_used: return toast( diff --git a/reflex/config.py b/reflex/config.py index 7a20633bfca..06d8c21933b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -219,9 +219,6 @@ class Config: # Number of gunicorn workers from user gunicorn_workers: Optional[int] = None - # If debug is true, more details will show when an error happens. - debug: bool = False - # Maximum expiration lock time for redis state manager redis_lock_expiration: int = constants.Expiration.LOCK From fe421643c384b5766ab4789052b1b61f64d4ca21 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Sat, 20 Jul 2024 01:53:22 +0200 Subject: [PATCH 7/8] fix tests --- tests/test_state.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_state.py b/tests/test_state.py index 9e19525e8bf..c998944ef7b 100644 --- a/tests/test_state.py +++ b/tests/test_state.py @@ -1551,7 +1551,7 @@ def invalid_handler(self): [ rx.toast( title="An error occurred.", - description="Contact the website administrator.", + description="TypeError: Your handler test_state_with_invalid_yield..StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).
See logs for details.", level="error", id="backend_error", position="top-center", From 7b4316afdb6493898e835980a59df8ef54874793 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Mon, 22 Jul 2024 22:01:36 +0200 Subject: [PATCH 8/8] fix typo --- reflex/components/sonner/toast.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/components/sonner/toast.py b/reflex/components/sonner/toast.py index f6519ea4bf1..d4df31e82c9 100644 --- a/reflex/components/sonner/toast.py +++ b/reflex/components/sonner/toast.py @@ -254,7 +254,7 @@ def send_toast(message: str = "", level: str | None = None, **props) -> EventSpe ) toast_command = f"{toast_ref}.{level}" if level is not None else toast_ref if message == "" and ("title" not in props or "description" not in props): - raise ValueError("Toast message or title or descrition must be provided.") + raise ValueError("Toast message or title or description must be provided.") if props: args = serialize(ToastProps(**props)) # type: ignore toast = f"{toast_command}(`{message}`, {args})"