diff --git a/reflex/.templates/web/utils/state.js b/reflex/.templates/web/utils/state.js index 93c664ef1..009910a32 100644 --- a/reflex/.templates/web/utils/state.js +++ b/reflex/.templates/web/utils/state.js @@ -106,6 +106,18 @@ export const getBackendURL = (url_str) => { return endpoint; }; +/** + * Check if the backend is disabled. + * + * @returns True if the backend is disabled, false otherwise. + */ +export const isBackendDisabled = () => { + const cookie = document.cookie + .split("; ") + .find((row) => row.startsWith("backend-enabled=")); + return cookie !== undefined && cookie.split("=")[1] == "false"; +}; + /** * Determine if any event in the event queue is stateful. * @@ -301,10 +313,7 @@ export const applyEvent = async (event, socket) => { // Send the event to the server. if (socket) { - socket.emit( - "event", - event, - ); + socket.emit("event", event); return true; } @@ -497,7 +506,7 @@ export const uploadFiles = async ( return false; } - const upload_ref_name = `__upload_controllers_${upload_id}` + const upload_ref_name = `__upload_controllers_${upload_id}`; if (refs[upload_ref_name]) { console.log("Upload already in progress for ", upload_id); @@ -815,7 +824,7 @@ export const useEventLoop = ( return; } // only use websockets if state is present - if (Object.keys(initialState).length > 1) { + if (Object.keys(initialState).length > 1 && !isBackendDisabled()) { // Initialize the websocket connection. if (!socket.current) { connect( diff --git a/reflex/app.py b/reflex/app.py index 9fe0f2992..ad123a655 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -59,7 +59,11 @@ from reflex.components.component import ( ComponentStyle, evaluate_style_namespaces, ) -from reflex.components.core.banner import connection_pulser, connection_toaster +from reflex.components.core.banner import ( + backend_disabled, + connection_pulser, + connection_toaster, +) from reflex.components.core.breakpoints import set_breakpoints from reflex.components.core.client_side_routing import ( Default404Page, @@ -158,9 +162,12 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ + config = get_config() + return Fragment.create( connection_pulser(), connection_toaster(), + *([backend_disabled()] if config.is_reflex_cloud else []), *codespaces.codespaces_auto_redirect(), ) diff --git a/reflex/components/core/banner.py b/reflex/components/core/banner.py index 6479bf3b2..68e6380ea 100644 --- a/reflex/components/core/banner.py +++ b/reflex/components/core/banner.py @@ -6,6 +6,7 @@ from typing import Optional from reflex.components.component import Component from reflex.components.core.cond import cond +from reflex.components.datadisplay.logo import svg_logo from reflex.components.el.elements.typography import Div from reflex.components.lucide.icon import Icon from reflex.components.radix.themes.components.dialog import ( @@ -293,7 +294,82 @@ class ConnectionPulser(Div): ) +class BackendDisabled(Div): + """A component that displays a message when the backend is disabled.""" + + @classmethod + def create(cls, **props) -> Component: + """Create a backend disabled component. + + Args: + **props: The properties of the component. + + Returns: + The backend disabled component. + """ + import reflex as rx + + is_backend_disabled = Var( + "backendDisabled", + _var_type=bool, + _var_data=VarData( + hooks={ + "const [backendDisabled, setBackendDisabled] = useState(false);": None, + "useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None, + }, + imports={ + "$/utils/state.js": [ImportVar(tag="isBackendDisabled")], + }, + ), + ) + + return super().create( + rx.cond( + is_backend_disabled, + rx.box( + rx.box( + rx.card( + rx.vstack( + svg_logo(), + rx.text( + "You ran out of compute credits.", + ), + rx.callout( + rx.fragment( + "Please upgrade your plan or raise your compute credits at ", + rx.link( + "Reflex Cloud.", + href="https://cloud.reflex.dev/", + ), + ), + width="100%", + icon="info", + variant="surface", + ), + ), + font_size="20px", + font_family='"Inter", "Helvetica", "Arial", sans-serif', + variant="classic", + ), + position="fixed", + top="50%", + left="50%", + transform="translate(-50%, -50%)", + width="40ch", + max_width="90vw", + ), + position="fixed", + z_index=9999, + backdrop_filter="grayscale(1) blur(5px)", + width="100dvw", + height="100dvh", + ), + ) + ) + + connection_banner = ConnectionBanner.create connection_modal = ConnectionModal.create connection_toaster = ConnectionToaster.create connection_pulser = ConnectionPulser.create +backend_disabled = BackendDisabled.create diff --git a/reflex/components/core/banner.pyi b/reflex/components/core/banner.pyi index f44ee7992..a13461f09 100644 --- a/reflex/components/core/banner.pyi +++ b/reflex/components/core/banner.pyi @@ -350,7 +350,105 @@ class ConnectionPulser(Div): """ ... +IS_BACKEND_DISABLED = Var( + "backendDisabled", + _var_type=bool, + _var_data=VarData( + hooks={ + "const [backendDisabled, setBackendDisabled] = useState(false);": None, + "useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None, + }, + imports={"$/utils/state.js": [ImportVar(tag="isBackendDisabled")]}, + ), +) + +class BackendDisabled(Div): + @overload + @classmethod + def create( # type: ignore + cls, + *children, + access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + auto_capitalize: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + content_editable: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + context_menu: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + enter_key_hint: Optional[ + Union[Var[Union[bool, int, str]], bool, int, str] + ] = None, + hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None, + title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = 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, Any]]] = None, + on_blur: Optional[EventType[[], BASE_STATE]] = None, + on_click: Optional[EventType[[], BASE_STATE]] = None, + on_context_menu: Optional[EventType[[], BASE_STATE]] = None, + on_double_click: Optional[EventType[[], BASE_STATE]] = None, + on_focus: Optional[EventType[[], BASE_STATE]] = None, + on_mount: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_down: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_move: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_out: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_over: Optional[EventType[[], BASE_STATE]] = None, + on_mouse_up: Optional[EventType[[], BASE_STATE]] = None, + on_scroll: Optional[EventType[[], BASE_STATE]] = None, + on_unmount: Optional[EventType[[], BASE_STATE]] = None, + **props, + ) -> "BackendDisabled": + """Create a backend disabled component. + + Args: + access_key: Provides a hint for generating a keyboard shortcut for the current element. + auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user. + content_editable: Indicates whether the element's content is editable. + context_menu: Defines the ID of a element which will serve as the element's context menu. + dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left) + draggable: Defines whether the element can be dragged. + enter_key_hint: Hints what media types the media element is able to play. + hidden: Defines whether the element is hidden. + input_mode: Defines the type of the element. + item_prop: Defines the name of the element for metadata purposes. + lang: Defines the language used in the element. + role: Defines the role of the element. + slot: Assigns a slot in a shadow DOM shadow tree to an element. + spell_check: Defines whether the element may be checked for spelling errors. + tab_index: Defines the position of the current element in the tabbing order. + title: Defines a tooltip for the element. + 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 properties of the component. + + Returns: + The backend disabled component. + """ + ... + connection_banner = ConnectionBanner.create connection_modal = ConnectionModal.create connection_toaster = ConnectionToaster.create connection_pulser = ConnectionPulser.create +backend_disabled = BackendDisabled.create diff --git a/reflex/components/radix/themes/components/card.py b/reflex/components/radix/themes/components/card.py index 30823de56..c61ce8255 100644 --- a/reflex/components/radix/themes/components/card.py +++ b/reflex/components/radix/themes/components/card.py @@ -20,7 +20,7 @@ class Card(elements.Div, RadixThemesComponent): # Card size: "1" - "5" size: Var[Responsive[Literal["1", "2", "3", "4", "5"],]] - # Variant of Card: "solid" | "soft" | "outline" | "ghost" + # Variant of Card: "surface", "classic", "ghost" variant: Var[Literal["surface", "classic", "ghost"]] diff --git a/reflex/config.py b/reflex/config.py index f6992f8b5..6609067f9 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -703,6 +703,9 @@ class Config(Base): # Path to file containing key-values pairs to override in the environment; Dotenv format. env_file: Optional[str] = None + # Whether the app is running in the reflex cloud environment. + is_reflex_cloud: bool = False + def __init__(self, *args, **kwargs): """Initialize the config values.