notifying frontend about backend error looks better (#3491)
This commit is contained in:
parent
ea016314b0
commit
b9927b6f49
@ -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.
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
"""
|
||||
...
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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.
|
||||
"""
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user