diff --git a/reflex/app.py b/reflex/app.py index bb3e1d403..e06956b20 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -80,7 +80,7 @@ from reflex.state import ( _substate_key, code_uses_state_contexts, ) -from reflex.utils import console, exceptions, format, prerequisites, types +from reflex.utils import codespaces, console, exceptions, format, prerequisites, types from reflex.utils.exec import is_testing_env, should_skip_compile from reflex.utils.imports import ImportVar @@ -95,7 +95,11 @@ def default_overlay_component() -> Component: Returns: The default overlay_component, which is a connection_modal. """ - return Fragment.create(connection_pulser(), connection_toaster()) + return Fragment.create( + connection_pulser(), + connection_toaster(), + *codespaces.codespaces_auto_redirect(), + ) class OverlayFragment(Fragment): @@ -346,6 +350,10 @@ class App(LifespanMixin, Base): StaticFiles(directory=get_upload_dir()), name="uploaded_files", ) + if codespaces.is_running_in_codespaces(): + self.api.get(str(constants.Endpoint.AUTH_CODESPACE))( + codespaces.auth_codespace + ) def _add_cors(self): """Add CORS middleware to the app.""" diff --git a/reflex/components/next/image.pyi b/reflex/components/next/image.pyi index 41484e2fe..c8abc57bd 100644 --- a/reflex/components/next/image.pyi +++ b/reflex/components/next/image.pyi @@ -19,8 +19,8 @@ class Image(NextComponent): def create( # type: ignore cls, *children, - width: Optional[Union[str, int]] = None, - height: Optional[Union[str, int]] = None, + width: Optional[Union[int, str]] = None, + height: Optional[Union[int, str]] = None, src: Optional[Union[Var[Any], Any]] = None, alt: Optional[Union[Var[str], str]] = None, loader: Optional[Union[Var[Any], Any]] = None, diff --git a/reflex/constants/event.py b/reflex/constants/event.py index fcfe9cdfc..16a2c6a5c 100644 --- a/reflex/constants/event.py +++ b/reflex/constants/event.py @@ -10,6 +10,7 @@ class Endpoint(Enum): PING = "ping" EVENT = "_event" UPLOAD = "_upload" + AUTH_CODESPACE = "auth-codespace" def __str__(self) -> str: """Get the string representation of the endpoint. diff --git a/reflex/utils/codespaces.py b/reflex/utils/codespaces.py new file mode 100644 index 000000000..7ff686129 --- /dev/null +++ b/reflex/utils/codespaces.py @@ -0,0 +1,94 @@ +"""Utilities for working with Github Codespaces.""" + +from __future__ import annotations + +import os + +from fastapi.responses import HTMLResponse + +from reflex.components.base.script import Script +from reflex.components.component import Component +from reflex.components.core.banner import has_connection_errors +from reflex.components.core.cond import cond +from reflex.constants import Endpoint + +redirect_script = """ +const thisUrl = new URL(window.location.href); +const params = new URLSearchParams(thisUrl.search) + +function doRedirect(url) { + if (!window.sessionStorage.getItem("authenticated_github_codespaces")) { + const a = document.createElement("a"); + if (params.has("redirect_to")) { + a.href = params.get("redirect_to") + } else if (!window.location.href.startsWith(url)) { + a.href = url + `?redirect_to=${window.location.href}` + } else { + return + } + a.hidden = true; + a.click(); + a.remove(); + window.sessionStorage.setItem("authenticated_github_codespaces", "true") + } +} +doRedirect("%s") +""" % Endpoint.AUTH_CODESPACE.get_url() + + +def codespaces_port_forwarding_domain() -> str | None: + """Get the domain for port forwarding in Github Codespaces. + + Returns: + The domain for port forwarding in Github Codespaces, or None if not running in Codespaces. + """ + GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN = os.getenv( + "GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN" + ) + return GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN + + +def is_running_in_codespaces() -> bool: + """Check if the app is running in Github Codespaces. + + Returns: + True if running in Github Codespaces, False otherwise. + """ + return codespaces_port_forwarding_domain() is not None + + +def codespaces_auto_redirect() -> list[Component]: + """Get the components for automatically redirecting back to the app after authenticating a codespace port forward. + + Returns: + A list containing the conditional redirect component, or empty list. + """ + if is_running_in_codespaces(): + return [cond(has_connection_errors, Script.create(redirect_script))] + return [] + + +async def auth_codespace() -> HTMLResponse: + """Page automatically redirecting back to the app after authenticating a codespace port forward. + + Returns: + An HTML response with an embedded script to redirect back to the app. + """ + return HTMLResponse( + """ + + + Reflex Github Codespace Forward Successfully Authenticated + + +
+

Successfully Authenticated

+
+ + + + """ + % redirect_script + )