notifying frontend about backend error looks better (#3491)

This commit is contained in:
Thomas Brandého 2024-07-23 22:58:15 +02:00 committed by GitHub
parent ea016314b0
commit b9927b6f49
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 105 additions and 20 deletions

View File

@ -112,11 +112,29 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
from reflex.components.sonner.toast import Toaster, toast
error = traceback.format_exc()
console.error(f"[Reflex Backend Exception]\n {error}\n")
return window_alert("An error occurred. See logs for details.")
error_message = (
["Contact the website administrator."]
if is_prod_mode()
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
)
if Toaster.is_used:
return toast(
level="error",
title="An error occurred.",
description="<br/>".join(error_message),
position="top-center",
id="backend_error",
style={"width": "500px"},
) # type: ignore
else:
error_message.insert(0, "An error occurred.")
return window_alert("\n".join(error_message))
def default_overlay_component() -> Component:
@ -183,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.

View File

@ -153,6 +153,20 @@ useEffect(() => {{
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."""

View File

@ -187,7 +187,7 @@ class ConnectionToaster(Toaster):
] = None,
**props,
) -> "ConnectionToaster":
"""Create the component.
"""Create a connection toaster component.
Args:
*children: The children of the component.
@ -211,10 +211,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.
"""
...

View File

@ -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 @@ class Toaster(Component):
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 @@ class Toaster(Component):
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 description must be provided.")
if props:
args = serialize(ToastProps(**props)) # type: ignore
toast = f"{toast_command}(`{message}`, {args})"
@ -331,6 +343,20 @@ class Toaster(Component):
)
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):

View File

@ -3,7 +3,7 @@
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, Callable, Dict, Literal, Optional, Union, overload
from typing import Any, Callable, ClassVar, Dict, Literal, Optional, Union, overload
from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
@ -52,9 +52,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
@ -158,10 +162,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
@ -182,10 +186,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.
"""
...
@ -200,7 +204,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.
@ -209,6 +213,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.
"""

View File

@ -31,6 +31,8 @@ from typing import (
import dill
from sqlalchemy.orm import DeclarativeBase
from reflex.config import get_config
try:
import pydantic.v1 as pydantic
except ModuleNotFoundError:
@ -42,7 +44,6 @@ from redis.exceptions import ResponseError
from reflex import constants
from reflex.base import Base
from reflex.config import get_config
from reflex.event import (
BACKGROUND_TASK_MARKER,
Event,

View File

@ -19,6 +19,7 @@ import reflex.config
from reflex import constants
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 (
@ -1527,7 +1528,6 @@ async def test_state_with_invalid_yield(capsys, mock_app):
Args:
capsys: Pytest fixture for capture standard streams.
mock_app: Mock app fixture.
"""
class StateWithInvalidYield(BaseState):
@ -1546,10 +1546,29 @@ async def test_state_with_invalid_yield(capsys, mock_app):
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="TypeError: Your handler test_state_with_invalid_yield.<locals>.StateWithInvalidYield.invalid_handler must only return/yield: None, Events or other EventHandlers referenced by their class (not using `self`).<br/>See logs for details.",
level="error",
id="backend_error",
position="top-center",
style={"width": "500px"},
) # type: ignore
],
token="",
)
else:
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