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..882975f2f 100644
--- a/reflex/components/core/banner.py
+++ b/reflex/components/core/banner.py
@@ -4,8 +4,10 @@ from __future__ import annotations
from typing import Optional
+from reflex import constants
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 +295,84 @@ 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={
+ f"$/{constants.Dirs.STATE_PATH}": [
+ 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..2ea514965 100644
--- a/reflex/components/core/banner.pyi
+++ b/reflex/components/core/banner.pyi
@@ -350,7 +350,93 @@ class ConnectionPulser(Div):
"""
...
+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