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 compiler
|
||||||
from reflex.compiler import utils as compiler_utils
|
from reflex.compiler import utils as compiler_utils
|
||||||
from reflex.compiler.compiler import ExecutorSafeFunctions
|
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.app_wrap import AppWrap
|
||||||
from reflex.components.base.fragment import Fragment
|
from reflex.components.base.fragment import Fragment
|
||||||
from reflex.components.component import (
|
from reflex.components.component import (
|
||||||
@ -49,6 +48,7 @@ from reflex.components.component import (
|
|||||||
ComponentStyle,
|
ComponentStyle,
|
||||||
evaluate_style_namespaces,
|
evaluate_style_namespaces,
|
||||||
)
|
)
|
||||||
|
from reflex.components.core import connection_pulser, connection_toaster
|
||||||
from reflex.components.core.client_side_routing import (
|
from reflex.components.core.client_side_routing import (
|
||||||
Default404Page,
|
Default404Page,
|
||||||
wait_for_client_redirect,
|
wait_for_client_redirect,
|
||||||
@ -91,7 +91,7 @@ def default_overlay_component() -> Component:
|
|||||||
Returns:
|
Returns:
|
||||||
The default overlay_component, which is a connection_modal.
|
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):
|
class OverlayFragment(Fragment):
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
"""Core Reflex components."""
|
"""Core Reflex components."""
|
||||||
|
|
||||||
from . import layout as layout
|
from . import layout as layout
|
||||||
from .banner import ConnectionBanner, ConnectionModal, ConnectionPulser
|
from .banner import (
|
||||||
|
ConnectionBanner,
|
||||||
|
ConnectionModal,
|
||||||
|
ConnectionPulser,
|
||||||
|
ConnectionToaster,
|
||||||
|
)
|
||||||
from .colors import color
|
from .colors import color
|
||||||
from .cond import Cond, color_mode_cond, cond
|
from .cond import Cond, color_mode_cond, cond
|
||||||
from .debounce import DebounceInput
|
from .debounce import DebounceInput
|
||||||
@ -26,6 +31,7 @@ from .upload import (
|
|||||||
|
|
||||||
connection_banner = ConnectionBanner.create
|
connection_banner = ConnectionBanner.create
|
||||||
connection_modal = ConnectionModal.create
|
connection_modal = ConnectionModal.create
|
||||||
|
connection_toaster = ConnectionToaster.create
|
||||||
connection_pulser = ConnectionPulser.create
|
connection_pulser = ConnectionPulser.create
|
||||||
debounce_input = DebounceInput.create
|
debounce_input = DebounceInput.create
|
||||||
foreach = Foreach.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.layout import Flex
|
||||||
from reflex.components.radix.themes.typography.text import Text
|
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 import Dirs, Hooks, Imports
|
||||||
|
from reflex.constants.compiler import CompileVars
|
||||||
from reflex.utils import imports
|
from reflex.utils import imports
|
||||||
|
from reflex.utils.serializers import serialize
|
||||||
from reflex.vars import Var, VarData
|
from reflex.vars import Var, VarData
|
||||||
|
|
||||||
connect_error_var_data: VarData = VarData( # type: ignore
|
connect_error_var_data: VarData = VarData( # type: ignore
|
||||||
@ -25,6 +28,13 @@ connect_error_var_data: VarData = VarData( # type: ignore
|
|||||||
hooks={Hooks.EVENTS: None},
|
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(
|
connection_error: Var = Var.create_safe(
|
||||||
value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''",
|
value="(connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : ''",
|
||||||
_var_is_local=False,
|
_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):
|
class ConnectionBanner(Component):
|
||||||
"""A connection banner component."""
|
"""A connection banner component."""
|
||||||
|
|
||||||
@ -162,8 +230,8 @@ class WifiOffPulse(Icon):
|
|||||||
size=props.pop("size", 32),
|
size=props.pop("size", 32),
|
||||||
z_index=props.pop("z_index", 9999),
|
z_index=props.pop("z_index", 9999),
|
||||||
position=props.pop("position", "fixed"),
|
position=props.pop("position", "fixed"),
|
||||||
bottom=props.pop("botton", "30px"),
|
bottom=props.pop("botton", "33px"),
|
||||||
right=props.pop("right", "30px"),
|
right=props.pop("right", "33px"),
|
||||||
animation=Var.create(f"${{pulse}} 1s infinite", _var_is_string=True),
|
animation=Var.create(f"${{pulse}} 1s infinite", _var_is_string=True),
|
||||||
**props,
|
**props,
|
||||||
)
|
)
|
||||||
@ -205,6 +273,7 @@ class ConnectionPulser(Div):
|
|||||||
has_connection_errors,
|
has_connection_errors,
|
||||||
WifiOffPulse.create(**props),
|
WifiOffPulse.create(**props),
|
||||||
),
|
),
|
||||||
|
title=f"Connection Error: {connection_error}",
|
||||||
position="fixed",
|
position="fixed",
|
||||||
width="100vw",
|
width="100vw",
|
||||||
height="0",
|
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.layout import Flex
|
||||||
from reflex.components.radix.themes.typography.text import Text
|
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 import Dirs, Hooks, Imports
|
||||||
|
from reflex.constants.compiler import CompileVars
|
||||||
from reflex.utils import imports
|
from reflex.utils import imports
|
||||||
|
from reflex.utils.serializers import serialize
|
||||||
from reflex.vars import Var, VarData
|
from reflex.vars import Var, VarData
|
||||||
|
|
||||||
connect_error_var_data: VarData
|
connect_error_var_data: VarData
|
||||||
|
connect_errors: Var
|
||||||
connection_error: Var
|
connection_error: Var
|
||||||
connection_errors_count: Var
|
connection_errors_count: Var
|
||||||
has_connection_errors: Var
|
has_connection_errors: Var
|
||||||
@ -99,6 +103,132 @@ class WebsocketTargetURL(Bare):
|
|||||||
|
|
||||||
def default_connection_error() -> list[str | Var | Component]: ...
|
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):
|
class ConnectionBanner(Component):
|
||||||
@overload
|
@overload
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any, Literal, Optional
|
from typing import Any, 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
|
||||||
@ -74,7 +74,7 @@ class ToastProps(PropsBase):
|
|||||||
"""Props for the toast component."""
|
"""Props for the toast component."""
|
||||||
|
|
||||||
# Toast's description, renders underneath the title.
|
# Toast's description, renders underneath the title.
|
||||||
description: Optional[str]
|
description: Optional[Union[str, Var]]
|
||||||
|
|
||||||
# Whether to show the close button.
|
# Whether to show the close button.
|
||||||
close_button: Optional[bool]
|
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.vars import Var, BaseVar, ComputedVar
|
||||||
from reflex.event import EventChain, EventHandler, EventSpec
|
from reflex.event import EventChain, EventHandler, EventSpec
|
||||||
from reflex.style import Style
|
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.base import Base
|
||||||
from reflex.components.component import Component, ComponentNamespace
|
from reflex.components.component import Component, ComponentNamespace
|
||||||
from reflex.components.lucide.icon import Icon
|
from reflex.components.lucide.icon import Icon
|
||||||
@ -37,7 +37,7 @@ class ToastAction(Base):
|
|||||||
def serialize_action(action: ToastAction) -> dict: ...
|
def serialize_action(action: ToastAction) -> dict: ...
|
||||||
|
|
||||||
class ToastProps(PropsBase):
|
class ToastProps(PropsBase):
|
||||||
description: Optional[str]
|
description: Optional[Union[str, Var]]
|
||||||
close_button: Optional[bool]
|
close_button: Optional[bool]
|
||||||
invert: Optional[bool]
|
invert: Optional[bool]
|
||||||
important: Optional[bool]
|
important: Optional[bool]
|
||||||
|
@ -110,7 +110,7 @@ class ClientStateVar(Var):
|
|||||||
f"{_client_state_ref(setter_name)} = {setter_name}": None,
|
f"{_client_state_ref(setter_name)} = {setter_name}": None,
|
||||||
},
|
},
|
||||||
imports={
|
imports={
|
||||||
"react": {ImportVar(tag="useState", install=False)},
|
"react": [ImportVar(tag="useState", install=False)],
|
||||||
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
|
f"/{constants.Dirs.STATE_PATH}": [ImportVar(tag="refs")],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
Loading…
Reference in New Issue
Block a user