wip connection toaster (#3242)
* wip connection toaster * never duplicate toast for websocket-error * wip update banner * clean up PR * fix for 3.8 * update pyi * ConnectionToaster tweaks * Use `has_too_many_connection_errors` to avoid showing the banner immediately * Increase toast duration to avoid frequent, distracting flashing of the toast * Automatically dismiss the toast when the connection comes back up * Include `close_button` for user to dismiss the toast * If the user dismisses the toast, do not show it again until the connection comes back and drops again * Use `connection_error` var instead of a custom util_hook to get the message * ConnectionPulser: hide behind toast * Hide the connection pulser behind the toast (33x33) * Add a title (tooltip) that shows the connection error * Re-add connection pulser to default overlay_component If the user dismisses the toast, we still want to indicate that the backend is actually down. * Fix pre-commit issue from main --------- Co-authored-by: Masen Furer <m_github@0x26.net>
This commit is contained in:
parent
99d59104ad
commit
9ba179410b
@ -41,7 +41,6 @@ from reflex.base import Base
|
||||
from reflex.compiler import compiler
|
||||
from reflex.compiler import utils as compiler_utils
|
||||
from reflex.compiler.compiler import ExecutorSafeFunctions
|
||||
from reflex.components import connection_modal, connection_pulser
|
||||
from reflex.components.base.app_wrap import AppWrap
|
||||
from reflex.components.base.fragment import Fragment
|
||||
from reflex.components.component import (
|
||||
@ -49,6 +48,7 @@ from reflex.components.component import (
|
||||
ComponentStyle,
|
||||
evaluate_style_namespaces,
|
||||
)
|
||||
from reflex.components.core import connection_pulser, connection_toaster
|
||||
from reflex.components.core.client_side_routing import (
|
||||
Default404Page,
|
||||
wait_for_client_redirect,
|
||||
@ -91,7 +91,7 @@ def default_overlay_component() -> Component:
|
||||
Returns:
|
||||
The default overlay_component, which is a connection_modal.
|
||||
"""
|
||||
return Fragment.create(connection_pulser(), connection_modal())
|
||||
return Fragment.create(connection_pulser(), connection_toaster())
|
||||
|
||||
|
||||
class OverlayFragment(Fragment):
|
||||
|
@ -1,7 +1,12 @@
|
||||
"""Core Reflex components."""
|
||||
|
||||
from . import layout as layout
|
||||
from .banner import ConnectionBanner, ConnectionModal, ConnectionPulser
|
||||
from .banner import (
|
||||
ConnectionBanner,
|
||||
ConnectionModal,
|
||||
ConnectionPulser,
|
||||
ConnectionToaster,
|
||||
)
|
||||
from .colors import color
|
||||
from .cond import Cond, color_mode_cond, cond
|
||||
from .debounce import DebounceInput
|
||||
@ -26,6 +31,7 @@ from .upload import (
|
||||
|
||||
connection_banner = ConnectionBanner.create
|
||||
connection_modal = ConnectionModal.create
|
||||
connection_toaster = ConnectionToaster.create
|
||||
connection_pulser = ConnectionPulser.create
|
||||
debounce_input = DebounceInput.create
|
||||
foreach = Foreach.create
|
||||
|
@ -16,8 +16,11 @@ from reflex.components.radix.themes.components.dialog import (
|
||||
)
|
||||
from reflex.components.radix.themes.layout import Flex
|
||||
from reflex.components.radix.themes.typography.text import Text
|
||||
from reflex.components.sonner.toast import Toaster, ToastProps
|
||||
from reflex.constants import Dirs, Hooks, Imports
|
||||
from reflex.constants.compiler import CompileVars
|
||||
from reflex.utils import imports
|
||||
from reflex.utils.serializers import serialize
|
||||
from reflex.vars import Var, VarData
|
||||
|
||||
connect_error_var_data: VarData = VarData( # type: ignore
|
||||
@ -25,6 +28,13 @@ connect_error_var_data: VarData = VarData( # type: ignore
|
||||
hooks={Hooks.EVENTS: None},
|
||||
)
|
||||
|
||||
connect_errors: Var = Var.create_safe(
|
||||
value=CompileVars.CONNECT_ERROR,
|
||||
_var_is_local=True,
|
||||
_var_is_string=False,
|
||||
_var_data=connect_error_var_data,
|
||||
)
|
||||
|
||||
connection_error: Var = Var.create_safe(
|
||||
value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''",
|
||||
_var_is_local=False,
|
||||
@ -85,6 +95,64 @@ def default_connection_error() -> list[str | Var | Component]:
|
||||
]
|
||||
|
||||
|
||||
class ConnectionToaster(Toaster):
|
||||
"""A connection toaster component."""
|
||||
|
||||
def add_hooks(self) -> list[str]:
|
||||
"""Add the hooks for the connection toaster.
|
||||
|
||||
Returns:
|
||||
The hooks for the connection toaster.
|
||||
"""
|
||||
toast_id = "websocket-error"
|
||||
target_url = WebsocketTargetURL.create()
|
||||
props = ToastProps( # type: ignore
|
||||
description=Var.create(
|
||||
f"`Check if server is reachable at ${target_url}`",
|
||||
_var_is_string=False,
|
||||
_var_is_local=False,
|
||||
),
|
||||
close_button=True,
|
||||
duration=120000,
|
||||
id=toast_id,
|
||||
)
|
||||
hook = Var.create(
|
||||
f"""
|
||||
const toast_props = {serialize(props)};
|
||||
const [userDismissed, setUserDismissed] = useState(false);
|
||||
useEffect(() => {{
|
||||
if ({has_too_many_connection_errors}) {{
|
||||
if (!userDismissed) {{
|
||||
toast.error(
|
||||
`Cannot connect to server: {connection_error}.`,
|
||||
{{...toast_props, onDismiss: () => setUserDismissed(true)}},
|
||||
)
|
||||
}}
|
||||
}} else {{
|
||||
toast.dismiss("{toast_id}");
|
||||
setUserDismissed(false); // after reconnection reset dismissed state
|
||||
}}
|
||||
}}, [{connect_errors}]);"""
|
||||
)
|
||||
|
||||
hook._var_data = VarData.merge( # type: ignore
|
||||
connect_errors._var_data,
|
||||
VarData(
|
||||
imports={
|
||||
"react": [
|
||||
imports.ImportVar(tag="useEffect"),
|
||||
imports.ImportVar(tag="useState"),
|
||||
],
|
||||
**target_url._get_imports(),
|
||||
}
|
||||
),
|
||||
)
|
||||
return [
|
||||
Hooks.EVENTS,
|
||||
hook, # type: ignore
|
||||
]
|
||||
|
||||
|
||||
class ConnectionBanner(Component):
|
||||
"""A connection banner component."""
|
||||
|
||||
@ -162,8 +230,8 @@ class WifiOffPulse(Icon):
|
||||
size=props.pop("size", 32),
|
||||
z_index=props.pop("z_index", 9999),
|
||||
position=props.pop("position", "fixed"),
|
||||
bottom=props.pop("botton", "30px"),
|
||||
right=props.pop("right", "30px"),
|
||||
bottom=props.pop("botton", "33px"),
|
||||
right=props.pop("right", "33px"),
|
||||
animation=Var.create(f"${{pulse}} 1s infinite", _var_is_string=True),
|
||||
**props,
|
||||
)
|
||||
@ -205,6 +273,7 @@ class ConnectionPulser(Div):
|
||||
has_connection_errors,
|
||||
WifiOffPulse.create(**props),
|
||||
),
|
||||
title=f"Connection Error: {connection_error}",
|
||||
position="fixed",
|
||||
width="100vw",
|
||||
height="0",
|
||||
|
@ -20,11 +20,15 @@ from reflex.components.radix.themes.components.dialog import (
|
||||
)
|
||||
from reflex.components.radix.themes.layout import Flex
|
||||
from reflex.components.radix.themes.typography.text import Text
|
||||
from reflex.components.sonner.toast import Toaster, ToastProps
|
||||
from reflex.constants import Dirs, Hooks, Imports
|
||||
from reflex.constants.compiler import CompileVars
|
||||
from reflex.utils import imports
|
||||
from reflex.utils.serializers import serialize
|
||||
from reflex.vars import Var, VarData
|
||||
|
||||
connect_error_var_data: VarData
|
||||
connect_errors: Var
|
||||
connection_error: Var
|
||||
connection_errors_count: Var
|
||||
has_connection_errors: Var
|
||||
@ -99,6 +103,132 @@ class WebsocketTargetURL(Bare):
|
||||
|
||||
def default_connection_error() -> list[str | Var | Component]: ...
|
||||
|
||||
class ConnectionToaster(Toaster):
|
||||
def add_hooks(self) -> list[str]: ...
|
||||
@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[
|
||||
Var[
|
||||
Literal[
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-center",
|
||||
"bottom-right",
|
||||
]
|
||||
],
|
||||
Literal[
|
||||
"top-left",
|
||||
"top-center",
|
||||
"top-right",
|
||||
"bottom-left",
|
||||
"bottom-center",
|
||||
"bottom-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[Var[ToastProps], ToastProps]] = None,
|
||||
gap: Optional[Union[Var[int], int]] = None,
|
||||
loading_icon: Optional[Union[Var[Icon], 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,
|
||||
class_name: Optional[Any] = None,
|
||||
autofocus: Optional[bool] = None,
|
||||
custom_attrs: Optional[Dict[str, Union[Var, str]]] = None,
|
||||
on_blur: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_click: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_context_menu: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_double_click: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_focus: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mount: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_down: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_enter: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_leave: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_move: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_out: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_over: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_mouse_up: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_scroll: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
on_unmount: Optional[
|
||||
Union[EventHandler, EventSpec, list, function, BaseVar]
|
||||
] = None,
|
||||
**props
|
||||
) -> "ConnectionToaster":
|
||||
"""Create the component.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
Returns:
|
||||
The component.
|
||||
"""
|
||||
...
|
||||
|
||||
class ConnectionBanner(Component):
|
||||
@overload
|
||||
@classmethod
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Literal, Optional
|
||||
from typing import Any, Literal, Optional, Union
|
||||
|
||||
from reflex.base import Base
|
||||
from reflex.components.component import Component, ComponentNamespace
|
||||
@ -74,7 +74,7 @@ class ToastProps(PropsBase):
|
||||
"""Props for the toast component."""
|
||||
|
||||
# Toast's description, renders underneath the title.
|
||||
description: Optional[str]
|
||||
description: Optional[Union[str, Var]]
|
||||
|
||||
# Whether to show the close button.
|
||||
close_button: Optional[bool]
|
||||
|
@ -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
|
||||
from typing import Any, Literal, Optional, Union
|
||||
from reflex.base import Base
|
||||
from reflex.components.component import Component, ComponentNamespace
|
||||
from reflex.components.lucide.icon import Icon
|
||||
@ -37,7 +37,7 @@ class ToastAction(Base):
|
||||
def serialize_action(action: ToastAction) -> dict: ...
|
||||
|
||||
class ToastProps(PropsBase):
|
||||
description: Optional[str]
|
||||
description: Optional[Union[str, Var]]
|
||||
close_button: Optional[bool]
|
||||
invert: Optional[bool]
|
||||
important: Optional[bool]
|
||||
|
@ -110,7 +110,7 @@ class ClientStateVar(Var):
|
||||
f"{_client_state_ref(setter_name)} = {setter_name}": None,
|
||||
},
|
||||
imports={
|
||||
"react": {ImportVar(tag="useState", install=False)},
|
||||
"react": [ImportVar(tag="useState", install=False)],
|
||||
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
|
||||
},
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user