Skip to content

Commit

Permalink
simplify toast banner logic (reflex-dev#4853)
Browse files Browse the repository at this point in the history
* simplify toast banner logic

* expose toast

* default back to title and desc, and replace brs with new lines
  • Loading branch information
adhami3310 authored Feb 20, 2025
1 parent 836e8f8 commit 8943341
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 131 deletions.
57 changes: 24 additions & 33 deletions reflex/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
from reflex.components.core.sticky import sticky
from reflex.components.core.upload import Upload, get_upload_dir
from reflex.components.radix import themes
from reflex.components.sonner.toast import toast
from reflex.config import ExecutorType, environment, get_config
from reflex.event import (
_EVENT_FIELDS,
Expand All @@ -84,7 +85,6 @@
EventType,
IndividualEventType,
get_hydrate_event,
window_alert,
)
from reflex.model import Model, get_db_status
from reflex.page import DECORATED_PAGES
Expand Down Expand Up @@ -144,7 +144,7 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
EventSpec: The window alert event.
"""
from reflex.components.sonner.toast import Toaster, toast
from reflex.components.sonner.toast import toast

error = traceback.format_exc()

Expand All @@ -155,18 +155,16 @@ def default_backend_exception_handler(exception: Exception) -> EventSpec:
if is_prod_mode()
else [f"{type(exception).__name__}: {exception}.", "See logs for details."]
)
if Toaster.is_used:
return toast(
"An error occurred.",
level="error",
description="<br/>".join(error_message),
position="top-center",
id="backend_error",
style={"width": "500px"},
)
else:
error_message.insert(0, "An error occurred.")
return window_alert("\n".join(error_message))

return toast(
"An error occurred.",
level="error",
fallback_to_alert=True,
description="<br/>".join(error_message),
position="top-center",
id="backend_error",
style={"width": "500px"},
)


def extra_overlay_function() -> Optional[Component]:
Expand Down Expand Up @@ -414,7 +412,7 @@ class App(MiddlewareMixin, LifespanMixin):
] = default_backend_exception_handler

# Put the toast provider in the app wrap.
bundle_toaster: bool = True
toaster: Component | None = dataclasses.field(default_factory=toast.provider)

@property
def api(self) -> FastAPI | None:
Expand Down Expand Up @@ -1100,10 +1098,6 @@ def get_compilation_time() -> str:
should_compile = self._should_compile()

if not should_compile:
if self.bundle_toaster:
from reflex.components.sonner.toast import Toaster

Toaster.is_used = True
with console.timing("Evaluate Pages (Backend)"):
for route in self._unevaluated_pages:
console.debug(f"Evaluating page: {route}")
Expand Down Expand Up @@ -1133,20 +1127,6 @@ def get_compilation_time() -> str:
+ adhoc_steps_without_executor,
)

if self.bundle_toaster:
from reflex.components.component import memo
from reflex.components.sonner.toast import toast

internal_toast_provider = toast.provider()

@memo
def memoized_toast_provider():
return internal_toast_provider

toast_provider = Fragment.create(memoized_toast_provider())

app_wrappers[(1, "ToasterProvider")] = toast_provider

with console.timing("Evaluate Pages (Frontend)"):
performance_metrics: list[tuple[str, float]] = []
for route in self._unevaluated_pages:
Expand Down Expand Up @@ -1207,6 +1187,17 @@ def memoized_toast_provider():
# Add the custom components from the page to the set.
custom_components |= component._get_all_custom_components()

if (toaster := self.toaster) is not None:
from reflex.components.component import memo

@memo
def memoized_toast_provider():
return toaster

toast_provider = Fragment.create(memoized_toast_provider())

app_wrappers[(1, "ToasterProvider")] = toast_provider

# Add the app wraps to the app.
for key, app_wrap in self.app_wraps.items():
component = app_wrap(self._state is not None)
Expand Down
15 changes: 8 additions & 7 deletions reflex/components/core/banner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Optional

from reflex import constants
from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.core.cond import cond
from reflex.components.el.elements.typography import Div
Expand All @@ -16,7 +17,7 @@
)
from reflex.components.radix.themes.layout.flex import Flex
from reflex.components.radix.themes.typography.text import Text
from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.components.sonner.toast import ToastProps, toast_ref
from reflex.config import environment
from reflex.constants import Dirs, Hooks, Imports
from reflex.constants.compiler import CompileVars
Expand Down Expand Up @@ -90,7 +91,7 @@ def default_connection_error() -> list[str | Var | Component]:
]


class ConnectionToaster(Toaster):
class ConnectionToaster(Fragment):
"""A connection toaster component."""

def add_hooks(self) -> list[str | Var]:
Expand All @@ -113,11 +114,11 @@ def add_hooks(self) -> list[str | Var]:
if environment.REFLEX_DOES_BACKEND_COLD_START.get():
loading_message = Var.create("Backend is starting.")
backend_is_loading_toast_var = Var(
f"toast.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
f"toast?.loading({loading_message!s}, {{...toast_props, description: '', closeButton: false, onDismiss: () => setUserDismissed(true)}},)"
)
backend_is_not_responding = Var.create("Backend is not responding.")
backend_is_down_toast_var = Var(
f"toast.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
f"toast?.error({backend_is_not_responding!s}, {{...toast_props, description: '', onDismiss: () => setUserDismissed(true)}},)"
)
toast_var = Var(
f"""
Expand All @@ -138,10 +139,11 @@ def add_hooks(self) -> list[str | Var]:
f"Cannot connect to server: {connection_error}."
)
toast_var = Var(
f"toast.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
f"toast?.error({loading_message!s}, {{...toast_props, onDismiss: () => setUserDismissed(true)}},)"
)

individual_hooks = [
Var(f"const toast = {toast_ref};"),
f"const toast_props = {LiteralVar.create(props)!s};",
"const [userDismissed, setUserDismissed] = useState(false);",
"const [waitedForBackend, setWaitedForBackend] = useState(false);",
Expand All @@ -163,7 +165,7 @@ def add_hooks(self) -> list[str | Var]:
{toast_var!s}
}}
}} else {{
toast.dismiss("{toast_id}");
toast?.dismiss("{toast_id}");
setUserDismissed(false); // after reconnection reset dismissed state
}}
}}
Expand All @@ -189,7 +191,6 @@ def create(cls, *children, **props) -> Component:
Returns:
The connection toaster component.
"""
Toaster.is_used = True
return super().create(*children, **props)


Expand Down
53 changes: 2 additions & 51 deletions reflex/components/core/banner.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# ------------------------------------------------------
from typing import Any, Dict, Literal, Optional, Union, overload

from reflex.components.base.fragment import Fragment
from reflex.components.component import Component
from reflex.components.el.elements.typography import Div
from reflex.components.lucide.icon import Icon
from reflex.components.sonner.toast import Toaster, ToastProps
from reflex.constants.compiler import CompileVars
from reflex.event import EventType
from reflex.style import Style
Expand Down Expand Up @@ -41,48 +41,13 @@ class WebsocketTargetURL(Var):

def default_connection_error() -> list[str | Var | Component]: ...

class ConnectionToaster(Toaster):
class ConnectionToaster(Fragment):
def add_hooks(self) -> list[str | Var]: ...
@overload
@classmethod
def create( # type: ignore
cls,
*children,
theme: Optional[Union[Var[str], str]] = None,
rich_colors: Optional[Union[Var[bool], bool]] = None,
expand: Optional[Union[Var[bool], bool]] = None,
visible_toasts: Optional[Union[Var[int], int]] = None,
position: Optional[
Union[
Literal[
"bottom-center",
"bottom-left",
"bottom-right",
"top-center",
"top-left",
"top-right",
],
Var[
Literal[
"bottom-center",
"bottom-left",
"bottom-right",
"top-center",
"top-left",
"top-right",
]
],
]
] = None,
close_button: Optional[Union[Var[bool], bool]] = None,
offset: Optional[Union[Var[str], str]] = None,
dir: Optional[Union[Var[str], str]] = None,
hotkey: Optional[Union[Var[str], str]] = None,
invert: Optional[Union[Var[bool], bool]] = None,
toast_options: Optional[Union[ToastProps, Var[ToastProps]]] = None,
gap: Optional[Union[Var[int], int]] = None,
loading_icon: Optional[Union[Icon, Var[Icon]]] = None,
pause_when_page_is_hidden: Optional[Union[Var[bool], bool]] = None,
style: Optional[Style] = None,
key: Optional[Any] = None,
id: Optional[Any] = None,
Expand Down Expand Up @@ -110,20 +75,6 @@ class ConnectionToaster(Toaster):
Args:
*children: The children of the component.
theme: the theme of the toast
rich_colors: whether to show rich colors
expand: whether to expand the toast
visible_toasts: the number of toasts that are currently visible
position: the position of the toast
close_button: whether to show the close button
offset: offset of the toast
dir: directionality of the toast (default: ltr)
hotkey: Keyboard shortcut that will move focus to the toaster area.
invert: Dark toasts in light mode and vice versa.
toast_options: These will act as default options for all toasts. See toast() for all available options.
gap: Gap between toasts when expanded
loading_icon: Changes the default loading icon
pause_when_page_is_hidden: Pauses toast timers when the page is hidden, e.g., when the tab is backgrounded, the browser is minimized, or the OS is locked.
style: The style of the component.
key: A unique key for the component.
id: The id for the component.
Expand Down
33 changes: 22 additions & 11 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, ClassVar, Literal, Optional, Union
from typing import Any, Literal, Optional, Union

from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
Expand All @@ -17,6 +17,7 @@
from reflex.vars import VarData
from reflex.vars.base import LiteralVar, Var
from reflex.vars.function import FunctionVar
from reflex.vars.number import ternary_operation
from reflex.vars.object import ObjectVar

LiteralPosition = Literal[
Expand Down Expand Up @@ -217,9 +218,6 @@ 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 @@ -241,13 +239,17 @@ def add_hooks(self) -> list[Var | str]:

@staticmethod
def send_toast(
message: str | Var = "", level: str | None = None, **props
message: str | Var = "",
level: str | None = None,
fallback_to_alert: bool = False,
**props,
) -> EventSpec:
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:
Expand All @@ -256,11 +258,6 @@ def send_toast(
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 = (
ObjectVar.__getattr__(toast_ref.to(dict), level) if level else toast_ref
).to(FunctionVar)
Expand All @@ -277,6 +274,21 @@ def send_toast(
else:
toast = toast_command.call(message)

if fallback_to_alert:
toast = ternary_operation(
toast_ref.bool(),
toast,
FunctionVar("window.alert").call(
Var.create(
message
if isinstance(message, str) and message
else props.get("title", props.get("description", ""))
)
.to(str)
.replace("<br/>", "\n")
),
)

return run_script(toast)

@staticmethod
Expand Down Expand Up @@ -379,7 +391,6 @@ def create(cls, *children: Any, **props: Any) -> Component:
Returns:
The toaster component.
"""
cls.is_used = True
return super().create(*children, **props)


Expand Down
15 changes: 10 additions & 5 deletions reflex/components/sonner/toast.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
# ------------------- DO NOT EDIT ----------------------
# This file was generated by `reflex/utils/pyi_generator.py`!
# ------------------------------------------------------
from typing import Any, ClassVar, Dict, Literal, Optional, Union, overload
from typing import Any, Dict, Literal, Optional, Union, overload

from reflex.base import Base
from reflex.components.component import Component, ComponentNamespace
Expand Down Expand Up @@ -60,12 +60,13 @@ class ToastProps(PropsBase, NoExtrasAllowedProps):
def dict(self, *args: Any, **kwargs: Any) -> dict[str, Any]: ...

class Toaster(Component):
is_used: ClassVar[bool] = False

def add_hooks(self) -> list[Var | str]: ...
@staticmethod
def send_toast(
message: str | Var = "", level: str | None = None, **props
message: str | Var = "",
level: str | None = None,
fallback_to_alert: bool = False,
**props,
) -> EventSpec: ...
@staticmethod
def toast_info(message: str | Var = "", **kwargs: Any): ...
Expand Down Expand Up @@ -185,13 +186,17 @@ class ToastNamespace(ComponentNamespace):

@staticmethod
def __call__(
message: Union[str, Var] = "", level: Optional[str] = None, **props
message: Union[str, Var] = "",
level: Optional[str] = None,
fallback_to_alert: bool = False,
**props,
) -> "EventSpec":
"""Send a toast message.
Args:
message: The message to display.
level: The level of the toast.
fallback_to_alert: Whether to fallback to an alert if the toaster is not created.
**props: The options for the toast.
Raises:
Expand Down
Loading

0 comments on commit 8943341

Please sign in to comment.