banner.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381
  1. """Banner components."""
  2. from __future__ import annotations
  3. from typing import Optional
  4. from reflex.components.base.fragment import Fragment
  5. from reflex import constants
  6. from reflex.components.component import Component
  7. from reflex.components.core.cond import cond
  8. from reflex.components.datadisplay.logo import svg_logo
  9. from reflex.components.el.elements.typography import Div
  10. from reflex.components.lucide.icon import Icon
  11. from reflex.components.radix.themes.components.dialog import (
  12. DialogContent,
  13. DialogRoot,
  14. DialogTitle,
  15. )
  16. from reflex.components.radix.themes.layout.flex import Flex
  17. from reflex.components.radix.themes.typography.text import Text
  18. from reflex.components.sonner.toast import Toaster, ToastProps
  19. from reflex.constants import Dirs, Hooks, Imports
  20. from reflex.constants.compiler import CompileVars
  21. from reflex.utils.imports import ImportVar
  22. from reflex.vars import VarData
  23. from reflex.vars.base import LiteralVar, Var
  24. from reflex.vars.function import FunctionStringVar
  25. from reflex.vars.number import BooleanVar
  26. from reflex.vars.sequence import LiteralArrayVar
  27. connect_error_var_data: VarData = VarData(
  28. imports=Imports.EVENTS,
  29. hooks={Hooks.EVENTS: None},
  30. )
  31. connect_errors = Var(
  32. _js_expr=CompileVars.CONNECT_ERROR, _var_data=connect_error_var_data
  33. )
  34. connection_error = Var(
  35. _js_expr="((connectErrors.length > 0) ? connectErrors[connectErrors.length - 1].message : '')",
  36. _var_data=connect_error_var_data,
  37. )
  38. connection_errors_count = Var(
  39. _js_expr="connectErrors.length", _var_data=connect_error_var_data
  40. )
  41. has_connection_errors = Var(
  42. _js_expr="(connectErrors.length > 0)", _var_data=connect_error_var_data
  43. ).to(BooleanVar)
  44. has_too_many_connection_errors = Var(
  45. _js_expr="(connectErrors.length >= 2)", _var_data=connect_error_var_data
  46. ).to(BooleanVar)
  47. class WebsocketTargetURL(Var):
  48. """A component that renders the websocket target URL."""
  49. @classmethod
  50. def create(cls) -> Var:
  51. """Create a websocket target URL component.
  52. Returns:
  53. The websocket target URL component.
  54. """
  55. return Var(
  56. _js_expr="getBackendURL(env.EVENT).href",
  57. _var_data=VarData(
  58. imports={
  59. "$/env.json": [ImportVar(tag="env", is_default=True)],
  60. f"$/{Dirs.STATE_PATH}": [ImportVar(tag="getBackendURL")],
  61. },
  62. ),
  63. _var_type=WebsocketTargetURL,
  64. )
  65. def default_connection_error() -> list[str | Var | Component]:
  66. """Get the default connection error message.
  67. Returns:
  68. The default connection error message.
  69. """
  70. return [
  71. "Cannot connect to server: ",
  72. connection_error,
  73. ". Check if server is reachable at ",
  74. WebsocketTargetURL.create(),
  75. ]
  76. class ConnectionToaster(Toaster):
  77. """A connection toaster component."""
  78. def add_hooks(self) -> list[str | Var]:
  79. """Add the hooks for the connection toaster.
  80. Returns:
  81. The hooks for the connection toaster.
  82. """
  83. toast_id = "websocket-error"
  84. target_url = WebsocketTargetURL.create()
  85. props = ToastProps(
  86. description=LiteralVar.create(
  87. f"Check if server is reachable at {target_url}",
  88. ),
  89. close_button=True,
  90. duration=120000,
  91. id=toast_id,
  92. ) # pyright: ignore [reportCallIssue]
  93. individual_hooks = [
  94. f"const toast_props = {LiteralVar.create(props)!s};",
  95. "const [userDismissed, setUserDismissed] = useState(false);",
  96. FunctionStringVar(
  97. "useEffect",
  98. _var_data=VarData(
  99. imports={
  100. "react": ["useEffect", "useState"],
  101. **dict(target_url._get_all_var_data().imports), # pyright: ignore [reportArgumentType, reportOptionalMemberAccess]
  102. }
  103. ),
  104. ).call(
  105. # TODO: This breaks the assumption that Vars are JS expressions
  106. Var(
  107. _js_expr=f"""
  108. () => {{
  109. if ({has_too_many_connection_errors!s}) {{
  110. if (!userDismissed) {{
  111. toast.error(
  112. `Cannot connect to server: ${{{connection_error}}}.`,
  113. {{...toast_props, onDismiss: () => setUserDismissed(true)}},
  114. )
  115. }}
  116. }} else {{
  117. toast.dismiss("{toast_id}");
  118. setUserDismissed(false); // after reconnection reset dismissed state
  119. }}
  120. }}
  121. """
  122. ),
  123. LiteralArrayVar.create([connect_errors]),
  124. ),
  125. ]
  126. return [
  127. Hooks.EVENTS,
  128. *individual_hooks,
  129. ]
  130. @classmethod
  131. def create(cls, *children, **props) -> Component:
  132. """Create a connection toaster component.
  133. Args:
  134. *children: The children of the component.
  135. **props: The properties of the component.
  136. Returns:
  137. The connection toaster component.
  138. """
  139. Toaster.is_used = True
  140. return super().create(*children, **props)
  141. class ConnectionBanner(Fragment):
  142. """A connection banner component."""
  143. @classmethod
  144. def create(cls, comp: Optional[Component] = None) -> Component:
  145. """Create a connection banner component.
  146. Args:
  147. comp: The component to render when there's a server connection error.
  148. Returns:
  149. The connection banner component.
  150. """
  151. if not comp:
  152. comp = Flex.create(
  153. Text.create(
  154. *default_connection_error(),
  155. color="black",
  156. size="4",
  157. ),
  158. justify="center",
  159. background_color="crimson",
  160. width="100vw",
  161. padding="5px",
  162. position="fixed",
  163. )
  164. return super().create(cond(has_connection_errors, comp))
  165. class ConnectionModal(Fragment):
  166. """A connection status modal window."""
  167. @classmethod
  168. def create(cls, comp: Optional[Component] = None) -> Component:
  169. """Create a connection banner component.
  170. Args:
  171. comp: The component to render when there's a server connection error.
  172. Returns:
  173. The connection banner component.
  174. """
  175. if not comp:
  176. comp = Text.create(*default_connection_error())
  177. return super().create(
  178. cond(
  179. has_too_many_connection_errors,
  180. DialogRoot.create(
  181. DialogContent.create(
  182. DialogTitle.create("Connection Error"),
  183. comp,
  184. ),
  185. open=has_too_many_connection_errors,
  186. z_index=9999,
  187. ),
  188. )
  189. )
  190. class WifiOffPulse(Icon):
  191. """A wifi_off icon with an animated opacity pulse."""
  192. @classmethod
  193. def create(cls, *children, **props) -> Icon:
  194. """Create a wifi_off icon with an animated opacity pulse.
  195. Args:
  196. *children: The children of the component.
  197. **props: The properties of the component.
  198. Returns:
  199. The icon component with default props applied.
  200. """
  201. pulse_var = Var(_js_expr="pulse")
  202. return super().create(
  203. "wifi_off",
  204. color=props.pop("color", "crimson"),
  205. size=props.pop("size", 32),
  206. z_index=props.pop("z_index", 9999),
  207. position=props.pop("position", "fixed"),
  208. bottom=props.pop("bottom", "33px"),
  209. right=props.pop("right", "33px"),
  210. animation=LiteralVar.create(f"{pulse_var} 1s infinite"),
  211. **props,
  212. )
  213. def add_imports(self) -> dict[str, str | ImportVar | list[str | ImportVar]]:
  214. """Add imports for the WifiOffPulse component.
  215. Returns:
  216. The import dict.
  217. """
  218. return {"@emotion/react": [ImportVar(tag="keyframes")]}
  219. def _get_custom_code(self) -> str | None:
  220. return """
  221. const pulse = keyframes`
  222. 0% {
  223. opacity: 0;
  224. }
  225. 100% {
  226. opacity: 1;
  227. }
  228. `
  229. """
  230. class ConnectionPulser(Div):
  231. """A connection pulser component."""
  232. @classmethod
  233. def create(cls, **props) -> Component:
  234. """Create a connection pulser component.
  235. Args:
  236. **props: The properties of the component.
  237. Returns:
  238. The connection pulser component.
  239. """
  240. return super().create(
  241. cond(
  242. has_connection_errors,
  243. WifiOffPulse.create(**props),
  244. ),
  245. title=f"Connection Error: {connection_error}",
  246. position="fixed",
  247. width="100vw",
  248. height="0",
  249. )
  250. class BackendDisabled(Div):
  251. """A component that displays a message when the backend is disabled."""
  252. @classmethod
  253. def create(cls, **props) -> Component:
  254. """Create a backend disabled component.
  255. Args:
  256. **props: The properties of the component.
  257. Returns:
  258. The backend disabled component.
  259. """
  260. import reflex as rx
  261. is_backend_disabled = Var(
  262. "backendDisabled",
  263. _var_type=bool,
  264. _var_data=VarData(
  265. hooks={
  266. "const [backendDisabled, setBackendDisabled] = useState(false);": None,
  267. "useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None,
  268. },
  269. imports={
  270. f"$/{constants.Dirs.STATE_PATH}": [
  271. ImportVar(tag="isBackendDisabled")
  272. ],
  273. },
  274. ),
  275. )
  276. return super().create(
  277. rx.cond(
  278. is_backend_disabled,
  279. rx.box(
  280. rx.box(
  281. rx.card(
  282. rx.vstack(
  283. svg_logo(),
  284. rx.text(
  285. "You ran out of compute credits.",
  286. ),
  287. rx.callout(
  288. rx.fragment(
  289. "Please upgrade your plan or raise your compute credits at ",
  290. rx.link(
  291. "Reflex Cloud.",
  292. href="https://cloud.reflex.dev/",
  293. ),
  294. ),
  295. width="100%",
  296. icon="info",
  297. variant="surface",
  298. ),
  299. ),
  300. font_size="20px",
  301. font_family='"Inter", "Helvetica", "Arial", sans-serif',
  302. variant="classic",
  303. ),
  304. position="fixed",
  305. top="50%",
  306. left="50%",
  307. transform="translate(-50%, -50%)",
  308. width="40ch",
  309. max_width="90vw",
  310. ),
  311. position="fixed",
  312. z_index=9999,
  313. backdrop_filter="grayscale(1) blur(5px)",
  314. width="100dvw",
  315. height="100dvh",
  316. ),
  317. )
  318. )
  319. connection_banner = ConnectionBanner.create
  320. connection_modal = ConnectionModal.create
  321. connection_toaster = ConnectionToaster.create
  322. connection_pulser = ConnectionPulser.create
  323. backend_disabled = BackendDisabled.create