Skip to content

Commit 335816c

Browse files
adhami3310masenf
andauthored
add backend disabled dialog (#4715)
* add backend disabled dialog * pyi that guy * pyi the other guy * extend test_connection_banner to also test the cloud banner * oops, need asyncio _inside_ the app * Update reflex/components/core/banner.py Co-authored-by: Masen Furer <[email protected]> * use universal cookies * fix pre-commit * revert universal cookie 🍪 --------- Co-authored-by: Masen Furer <[email protected]>
1 parent 6231f82 commit 335816c

File tree

8 files changed

+295
-17
lines changed

8 files changed

+295
-17
lines changed

reflex/.templates/web/utils/state.js

+15-6
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,18 @@ export const getBackendURL = (url_str) => {
106106
return endpoint;
107107
};
108108

109+
/**
110+
* Check if the backend is disabled.
111+
*
112+
* @returns True if the backend is disabled, false otherwise.
113+
*/
114+
export const isBackendDisabled = () => {
115+
const cookie = document.cookie
116+
.split("; ")
117+
.find((row) => row.startsWith("backend-enabled="));
118+
return cookie !== undefined && cookie.split("=")[1] == "false";
119+
};
120+
109121
/**
110122
* Determine if any event in the event queue is stateful.
111123
*
@@ -301,10 +313,7 @@ export const applyEvent = async (event, socket) => {
301313

302314
// Send the event to the server.
303315
if (socket) {
304-
socket.emit(
305-
"event",
306-
event,
307-
);
316+
socket.emit("event", event);
308317
return true;
309318
}
310319

@@ -497,7 +506,7 @@ export const uploadFiles = async (
497506
return false;
498507
}
499508

500-
const upload_ref_name = `__upload_controllers_${upload_id}`
509+
const upload_ref_name = `__upload_controllers_${upload_id}`;
501510

502511
if (refs[upload_ref_name]) {
503512
console.log("Upload already in progress for ", upload_id);
@@ -815,7 +824,7 @@ export const useEventLoop = (
815824
return;
816825
}
817826
// only use websockets if state is present
818-
if (Object.keys(initialState).length > 1) {
827+
if (Object.keys(initialState).length > 1 && !isBackendDisabled()) {
819828
// Initialize the websocket connection.
820829
if (!socket.current) {
821830
connect(

reflex/app.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,11 @@
5959
ComponentStyle,
6060
evaluate_style_namespaces,
6161
)
62-
from reflex.components.core.banner import connection_pulser, connection_toaster
62+
from reflex.components.core.banner import (
63+
backend_disabled,
64+
connection_pulser,
65+
connection_toaster,
66+
)
6367
from reflex.components.core.breakpoints import set_breakpoints
6468
from reflex.components.core.client_side_routing import (
6569
Default404Page,
@@ -158,9 +162,12 @@ def default_overlay_component() -> Component:
158162
Returns:
159163
The default overlay_component, which is a connection_modal.
160164
"""
165+
config = get_config()
166+
161167
return Fragment.create(
162168
connection_pulser(),
163169
connection_toaster(),
170+
*([backend_disabled()] if config.is_reflex_cloud else []),
164171
*codespaces.codespaces_auto_redirect(),
165172
)
166173

reflex/components/core/banner.py

+79
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44

55
from typing import Optional
66

7+
from reflex import constants
78
from reflex.components.component import Component
89
from reflex.components.core.cond import cond
10+
from reflex.components.datadisplay.logo import svg_logo
911
from reflex.components.el.elements.typography import Div
1012
from reflex.components.lucide.icon import Icon
1113
from reflex.components.radix.themes.components.dialog import (
@@ -293,7 +295,84 @@ def create(cls, **props) -> Component:
293295
)
294296

295297

298+
class BackendDisabled(Div):
299+
"""A component that displays a message when the backend is disabled."""
300+
301+
@classmethod
302+
def create(cls, **props) -> Component:
303+
"""Create a backend disabled component.
304+
305+
Args:
306+
**props: The properties of the component.
307+
308+
Returns:
309+
The backend disabled component.
310+
"""
311+
import reflex as rx
312+
313+
is_backend_disabled = Var(
314+
"backendDisabled",
315+
_var_type=bool,
316+
_var_data=VarData(
317+
hooks={
318+
"const [backendDisabled, setBackendDisabled] = useState(false);": None,
319+
"useEffect(() => { setBackendDisabled(isBackendDisabled()); }, []);": None,
320+
},
321+
imports={
322+
f"$/{constants.Dirs.STATE_PATH}": [
323+
ImportVar(tag="isBackendDisabled")
324+
],
325+
},
326+
),
327+
)
328+
329+
return super().create(
330+
rx.cond(
331+
is_backend_disabled,
332+
rx.box(
333+
rx.box(
334+
rx.card(
335+
rx.vstack(
336+
svg_logo(),
337+
rx.text(
338+
"You ran out of compute credits.",
339+
),
340+
rx.callout(
341+
rx.fragment(
342+
"Please upgrade your plan or raise your compute credits at ",
343+
rx.link(
344+
"Reflex Cloud.",
345+
href="https://cloud.reflex.dev/",
346+
),
347+
),
348+
width="100%",
349+
icon="info",
350+
variant="surface",
351+
),
352+
),
353+
font_size="20px",
354+
font_family='"Inter", "Helvetica", "Arial", sans-serif',
355+
variant="classic",
356+
),
357+
position="fixed",
358+
top="50%",
359+
left="50%",
360+
transform="translate(-50%, -50%)",
361+
width="40ch",
362+
max_width="90vw",
363+
),
364+
position="fixed",
365+
z_index=9999,
366+
backdrop_filter="grayscale(1) blur(5px)",
367+
width="100dvw",
368+
height="100dvh",
369+
),
370+
)
371+
)
372+
373+
296374
connection_banner = ConnectionBanner.create
297375
connection_modal = ConnectionModal.create
298376
connection_toaster = ConnectionToaster.create
299377
connection_pulser = ConnectionPulser.create
378+
backend_disabled = BackendDisabled.create

reflex/components/core/banner.pyi

+86
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,93 @@ class ConnectionPulser(Div):
350350
"""
351351
...
352352

353+
class BackendDisabled(Div):
354+
@overload
355+
@classmethod
356+
def create( # type: ignore
357+
cls,
358+
*children,
359+
access_key: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
360+
auto_capitalize: Optional[
361+
Union[Var[Union[bool, int, str]], bool, int, str]
362+
] = None,
363+
content_editable: Optional[
364+
Union[Var[Union[bool, int, str]], bool, int, str]
365+
] = None,
366+
context_menu: Optional[
367+
Union[Var[Union[bool, int, str]], bool, int, str]
368+
] = None,
369+
dir: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
370+
draggable: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
371+
enter_key_hint: Optional[
372+
Union[Var[Union[bool, int, str]], bool, int, str]
373+
] = None,
374+
hidden: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
375+
input_mode: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
376+
item_prop: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
377+
lang: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
378+
role: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
379+
slot: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
380+
spell_check: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
381+
tab_index: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
382+
title: Optional[Union[Var[Union[bool, int, str]], bool, int, str]] = None,
383+
style: Optional[Style] = None,
384+
key: Optional[Any] = None,
385+
id: Optional[Any] = None,
386+
class_name: Optional[Any] = None,
387+
autofocus: Optional[bool] = None,
388+
custom_attrs: Optional[Dict[str, Union[Var, Any]]] = None,
389+
on_blur: Optional[EventType[[], BASE_STATE]] = None,
390+
on_click: Optional[EventType[[], BASE_STATE]] = None,
391+
on_context_menu: Optional[EventType[[], BASE_STATE]] = None,
392+
on_double_click: Optional[EventType[[], BASE_STATE]] = None,
393+
on_focus: Optional[EventType[[], BASE_STATE]] = None,
394+
on_mount: Optional[EventType[[], BASE_STATE]] = None,
395+
on_mouse_down: Optional[EventType[[], BASE_STATE]] = None,
396+
on_mouse_enter: Optional[EventType[[], BASE_STATE]] = None,
397+
on_mouse_leave: Optional[EventType[[], BASE_STATE]] = None,
398+
on_mouse_move: Optional[EventType[[], BASE_STATE]] = None,
399+
on_mouse_out: Optional[EventType[[], BASE_STATE]] = None,
400+
on_mouse_over: Optional[EventType[[], BASE_STATE]] = None,
401+
on_mouse_up: Optional[EventType[[], BASE_STATE]] = None,
402+
on_scroll: Optional[EventType[[], BASE_STATE]] = None,
403+
on_unmount: Optional[EventType[[], BASE_STATE]] = None,
404+
**props,
405+
) -> "BackendDisabled":
406+
"""Create a backend disabled component.
407+
408+
Args:
409+
access_key: Provides a hint for generating a keyboard shortcut for the current element.
410+
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
411+
content_editable: Indicates whether the element's content is editable.
412+
context_menu: Defines the ID of a <menu> element which will serve as the element's context menu.
413+
dir: Defines the text direction. Allowed values are ltr (Left-To-Right) or rtl (Right-To-Left)
414+
draggable: Defines whether the element can be dragged.
415+
enter_key_hint: Hints what media types the media element is able to play.
416+
hidden: Defines whether the element is hidden.
417+
input_mode: Defines the type of the element.
418+
item_prop: Defines the name of the element for metadata purposes.
419+
lang: Defines the language used in the element.
420+
role: Defines the role of the element.
421+
slot: Assigns a slot in a shadow DOM shadow tree to an element.
422+
spell_check: Defines whether the element may be checked for spelling errors.
423+
tab_index: Defines the position of the current element in the tabbing order.
424+
title: Defines a tooltip for the element.
425+
style: The style of the component.
426+
key: A unique key for the component.
427+
id: The id for the component.
428+
class_name: The class name for the component.
429+
autofocus: Whether the component should take the focus once the page is loaded
430+
custom_attrs: custom attribute
431+
**props: The properties of the component.
432+
433+
Returns:
434+
The backend disabled component.
435+
"""
436+
...
437+
353438
connection_banner = ConnectionBanner.create
354439
connection_modal = ConnectionModal.create
355440
connection_toaster = ConnectionToaster.create
356441
connection_pulser = ConnectionPulser.create
442+
backend_disabled = BackendDisabled.create

reflex/components/radix/themes/components/card.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ class Card(elements.Div, RadixThemesComponent):
2020
# Card size: "1" - "5"
2121
size: Var[Responsive[Literal["1", "2", "3", "4", "5"],]]
2222

23-
# Variant of Card: "solid" | "soft" | "outline" | "ghost"
23+
# Variant of Card: "surface" | "classic" | "ghost"
2424
variant: Var[Literal["surface", "classic", "ghost"]]
2525

2626

reflex/components/radix/themes/components/card.pyi

+1-1
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ class Card(elements.Div, RadixThemesComponent):
9494
*children: Child components.
9595
as_child: Change the default rendered element for the one passed as a child, merging their props and behavior.
9696
size: Card size: "1" - "5"
97-
variant: Variant of Card: "solid" | "soft" | "outline" | "ghost"
97+
variant: Variant of Card: "surface" | "classic" | "ghost"
9898
access_key: Provides a hint for generating a keyboard shortcut for the current element.
9999
auto_capitalize: Controls whether and how text input is automatically capitalized as it is entered/edited by the user.
100100
content_editable: Indicates whether the element's content is editable.

reflex/config.py

+3
Original file line numberDiff line numberDiff line change
@@ -703,6 +703,9 @@ class Config: # pyright: ignore [reportIncompatibleVariableOverride]
703703
# Path to file containing key-values pairs to override in the environment; Dotenv format.
704704
env_file: Optional[str] = None
705705

706+
# Whether the app is running in the reflex cloud environment.
707+
is_reflex_cloud: bool = False
708+
706709
def __init__(self, *args, **kwargs):
707710
"""Initialize the config values.
708711

0 commit comments

Comments
 (0)