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.
|
EventSpec: The window alert event.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from reflex.components.sonner.toast import Toaster, toast
|
||||||
|
|
||||||
error = traceback.format_exc()
|
error = traceback.format_exc()
|
||||||
|
|
||||||
console.error(f"[Reflex Backend Exception]\n {error}\n")
|
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:
|
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).
|
# A component that is present on every page (defaults to the Connection Error banner).
|
||||||
overlay_component: Optional[Union[Component, ComponentCallable]] = (
|
overlay_component: Optional[Union[Component, ComponentCallable]] = (
|
||||||
default_overlay_component
|
default_overlay_component()
|
||||||
)
|
)
|
||||||
|
|
||||||
# Error boundary component to wrap the app with.
|
# Error boundary component to wrap the app with.
|
||||||
|
@ -153,6 +153,20 @@ useEffect(() => {{
|
|||||||
hook,
|
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):
|
class ConnectionBanner(Component):
|
||||||
"""A connection banner component."""
|
"""A connection banner component."""
|
||||||
|
@ -187,7 +187,7 @@ class ConnectionToaster(Toaster):
|
|||||||
] = None,
|
] = None,
|
||||||
**props,
|
**props,
|
||||||
) -> "ConnectionToaster":
|
) -> "ConnectionToaster":
|
||||||
"""Create the component.
|
"""Create a connection toaster component.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the component.
|
||||||
@ -211,10 +211,10 @@ class ConnectionToaster(Toaster):
|
|||||||
class_name: The class name for the component.
|
class_name: The class name for the component.
|
||||||
autofocus: Whether the component should take the focus once the page is loaded
|
autofocus: Whether the component should take the focus once the page is loaded
|
||||||
custom_attrs: custom attribute
|
custom_attrs: custom attribute
|
||||||
**props: The props of the component.
|
**props: The properties of the component.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The component.
|
The connection toaster component.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
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.base import Base
|
||||||
from reflex.components.component import Component, ComponentNamespace
|
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.
|
# 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]
|
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]:
|
def add_hooks(self) -> list[Var | str]:
|
||||||
"""Add hooks for the toaster component.
|
"""Add hooks for the toaster component.
|
||||||
|
|
||||||
@ -231,7 +234,7 @@ class Toaster(Component):
|
|||||||
return [hook]
|
return [hook]
|
||||||
|
|
||||||
@staticmethod
|
@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.
|
"""Send a toast message.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -239,10 +242,19 @@ class Toaster(Component):
|
|||||||
level: The level of the toast.
|
level: The level of the toast.
|
||||||
**props: The options for the toast.
|
**props: The options for the toast.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the Toaster component is not created.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The toast event.
|
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
|
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:
|
if props:
|
||||||
args = serialize(ToastProps(**props)) # type: ignore
|
args = serialize(ToastProps(**props)) # type: ignore
|
||||||
toast = f"{toast_command}(`{message}`, {args})"
|
toast = f"{toast_command}(`{message}`, {args})"
|
||||||
@ -331,6 +343,20 @@ class Toaster(Component):
|
|||||||
)
|
)
|
||||||
return call_script(dismiss_action)
|
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
|
# TODO: figure out why loading toast stay open forever
|
||||||
# def toast_loading(message: str, **kwargs):
|
# def toast_loading(message: str, **kwargs):
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
# ------------------- DO NOT EDIT ----------------------
|
# ------------------- DO NOT EDIT ----------------------
|
||||||
# This file was generated by `reflex/utils/pyi_generator.py`!
|
# 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.base import Base
|
||||||
from reflex.components.component import Component, ComponentNamespace
|
from reflex.components.component import Component, ComponentNamespace
|
||||||
@ -52,9 +52,13 @@ class ToastProps(PropsBase):
|
|||||||
def dict(self, *args, **kwargs) -> dict[str, Any]: ...
|
def dict(self, *args, **kwargs) -> dict[str, Any]: ...
|
||||||
|
|
||||||
class Toaster(Component):
|
class Toaster(Component):
|
||||||
|
is_used: ClassVar[bool] = False
|
||||||
|
|
||||||
def add_hooks(self) -> list[Var | str]: ...
|
def add_hooks(self) -> list[Var | str]: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def send_toast(message: str, level: str | None = None, **props) -> EventSpec: ...
|
def send_toast(
|
||||||
|
message: str = "", level: str | None = None, **props
|
||||||
|
) -> EventSpec: ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def toast_info(message: str, **kwargs): ...
|
def toast_info(message: str, **kwargs): ...
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -158,10 +162,10 @@ class Toaster(Component):
|
|||||||
] = None,
|
] = None,
|
||||||
**props,
|
**props,
|
||||||
) -> "Toaster":
|
) -> "Toaster":
|
||||||
"""Create the component.
|
"""Create a toaster component.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
*children: The children of the component.
|
*children: The children of the toaster.
|
||||||
theme: the theme of the toast
|
theme: the theme of the toast
|
||||||
rich_colors: whether to show rich colors
|
rich_colors: whether to show rich colors
|
||||||
expand: whether to expand the toast
|
expand: whether to expand the toast
|
||||||
@ -182,10 +186,10 @@ class Toaster(Component):
|
|||||||
class_name: The class name for the component.
|
class_name: The class name for the component.
|
||||||
autofocus: Whether the component should take the focus once the page is loaded
|
autofocus: Whether the component should take the focus once the page is loaded
|
||||||
custom_attrs: custom attribute
|
custom_attrs: custom attribute
|
||||||
**props: The props of the component.
|
**props: The properties of the toaster.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The component.
|
The toaster component.
|
||||||
"""
|
"""
|
||||||
...
|
...
|
||||||
|
|
||||||
@ -200,7 +204,7 @@ class ToastNamespace(ComponentNamespace):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __call__(
|
def __call__(
|
||||||
message: str, level: Optional[str] = None, **props
|
message: str = "", level: Optional[str] = None, **props
|
||||||
) -> "Optional[EventSpec]":
|
) -> "Optional[EventSpec]":
|
||||||
"""Send a toast message.
|
"""Send a toast message.
|
||||||
|
|
||||||
@ -209,6 +213,9 @@ class ToastNamespace(ComponentNamespace):
|
|||||||
level: The level of the toast.
|
level: The level of the toast.
|
||||||
**props: The options for the toast.
|
**props: The options for the toast.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
ValueError: If the Toaster component is not created.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
The toast event.
|
The toast event.
|
||||||
"""
|
"""
|
||||||
|
@ -31,6 +31,8 @@ from typing import (
|
|||||||
import dill
|
import dill
|
||||||
from sqlalchemy.orm import DeclarativeBase
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
from reflex.config import get_config
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import pydantic.v1 as pydantic
|
import pydantic.v1 as pydantic
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
@ -42,7 +44,6 @@ from redis.exceptions import ResponseError
|
|||||||
|
|
||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
from reflex.config import get_config
|
|
||||||
from reflex.event import (
|
from reflex.event import (
|
||||||
BACKGROUND_TASK_MARKER,
|
BACKGROUND_TASK_MARKER,
|
||||||
Event,
|
Event,
|
||||||
|
@ -19,6 +19,7 @@ import reflex.config
|
|||||||
from reflex import constants
|
from reflex import constants
|
||||||
from reflex.app import App
|
from reflex.app import App
|
||||||
from reflex.base import Base
|
from reflex.base import Base
|
||||||
|
from reflex.components.sonner.toast import Toaster
|
||||||
from reflex.constants import CompileVars, RouteVar, SocketEvent
|
from reflex.constants import CompileVars, RouteVar, SocketEvent
|
||||||
from reflex.event import Event, EventHandler
|
from reflex.event import Event, EventHandler
|
||||||
from reflex.state import (
|
from reflex.state import (
|
||||||
@ -1527,7 +1528,6 @@ async def test_state_with_invalid_yield(capsys, mock_app):
|
|||||||
Args:
|
Args:
|
||||||
capsys: Pytest fixture for capture standard streams.
|
capsys: Pytest fixture for capture standard streams.
|
||||||
mock_app: Mock app fixture.
|
mock_app: Mock app fixture.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
class StateWithInvalidYield(BaseState):
|
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")
|
rx.event.Event(token="fake_token", name="invalid_handler")
|
||||||
):
|
):
|
||||||
assert not update.delta
|
assert not update.delta
|
||||||
assert update.events == rx.event.fix_events(
|
if Toaster.is_used:
|
||||||
[rx.window_alert("An error occurred. See logs for details.")],
|
assert update.events == rx.event.fix_events(
|
||||||
token="",
|
[
|
||||||
)
|
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()
|
captured = capsys.readouterr()
|
||||||
assert "must only return/yield: None, Events or other EventHandlers" in captured.out
|
assert "must only return/yield: None, Events or other EventHandlers" in captured.out
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user