Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

notifying frontend about backend error looks better #3491

Merged
merged 9 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions reflex/components/core/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
6 changes: 3 additions & 3 deletions reflex/components/core/banner.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
"""
...

Expand Down
30 changes: 28 additions & 2 deletions reflex/components/sonner/toast.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand All @@ -231,18 +234,27 @@ 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:
message: The message to display.
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})"
Expand Down Expand Up @@ -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):
Expand Down
21 changes: 14 additions & 7 deletions reflex/components/sonner/toast.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
"""
...

Expand All @@ -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.

Expand All @@ -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.
"""
Expand Down
3 changes: 3 additions & 0 deletions reflex/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())

Expand Down
29 changes: 24 additions & 5 deletions reflex/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@

import dill

from reflex.config import get_config

try:
import pydantic.v1 as pydantic
except ModuleNotFoundError:
Expand Down Expand Up @@ -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="<br/>".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."""
Expand Down
25 changes: 21 additions & 4 deletions tests/test_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -1426,6 +1427,7 @@ async def test_state_with_invalid_yield(capsys):

Args:
capsys: Pytest fixture for capture standard streams.

"""

class StateWithInvalidYield(BaseState):
Expand All @@ -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

Expand Down
Loading