diff --git a/pynecone/.templates/jinja/web/pages/index.js.jinja2 b/pynecone/.templates/jinja/web/pages/index.js.jinja2 index a50029bf7..1328697b8 100644 --- a/pynecone/.templates/jinja/web/pages/index.js.jinja2 +++ b/pynecone/.templates/jinja/web/pages/index.js.jinja2 @@ -11,6 +11,7 @@ export default function Component() { const [{{state_name}}, {{state_name|react_setter}}] = useState({{initial_state|json_dumps}}) const [{{const.result}}, {{const.result|react_setter}}] = useState({{const.initial_result|json_dumps}}) + const [notConnected, setNotConnected] = useState(false) const {{const.router}} = useRouter() const {{const.socket}} = useRef(null) const { isReady } = {{const.router}} @@ -35,7 +36,7 @@ export default function Component() { return; } if (!{{const.socket}}.current) { - connect({{const.socket}}, {{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{transports}}) + connect({{const.socket}}, {{state_name}}, {{state_name|react_setter}}, {{const.result}}, {{const.result|react_setter}}, {{const.router}}, {{transports}}, setNotConnected) } const update = async () => { if ({{const.result}}.{{const.state}} != null){ @@ -70,7 +71,12 @@ export default function Component() { {% endfor %} return ( + + {%- if err_comp -%} + {{ utils.render(err_comp, indent_width=1) }} + {%- endif -%} {{utils.render(render, indent_width=0)}} + ) } {% endblock %} diff --git a/pynecone/.templates/web/utils/state.js b/pynecone/.templates/web/utils/state.js index 08af47241..9d6384ba3 100644 --- a/pynecone/.templates/web/utils/state.js +++ b/pynecone/.templates/web/utils/state.js @@ -193,7 +193,8 @@ export const connect = async ( result, setResult, router, - transports + transports, + setNotConnected ) => { // Get backend URL object from the endpoint const endpoint_url = new URL(EVENTURL); @@ -207,6 +208,11 @@ export const connect = async ( // Once the socket is open, hydrate the page. socket.current.on("connect", () => { updateState(state, setState, result, setResult, router, socket.current); + setNotConnected(false) + }); + + socket.current.on('connect_error', (error) => { + setNotConnected(true) }); // On each received message, apply the delta and set the result. diff --git a/pynecone/app.py b/pynecone/app.py index 828423ee8..229237032 100644 --- a/pynecone/app.py +++ b/pynecone/app.py @@ -24,6 +24,7 @@ from pynecone.base import Base from pynecone.compiler import compiler from pynecone.compiler import utils as compiler_utils from pynecone.components.component import Component, ComponentStyle +from pynecone.components.overlay.banner import ConnectionBanner from pynecone.config import get_config from pynecone.event import Event, EventHandler from pynecone.middleware import HydrateMiddleware, Middleware @@ -76,6 +77,9 @@ class App(Base): # List of event handlers to trigger when a page loads. load_events: Dict[str, List[EventHandler]] = {} + # The component to render if there is a connection error to the server. + connect_error_component: Optional[Component] = ConnectionBanner.create() + def __init__(self, *args, **kwargs): """Initialize the app. @@ -411,7 +415,12 @@ class App(Base): custom_components = set() for route, component in self.pages.items(): component.add_style(self.style) - compiler.compile_page(route, component, self.state) + compiler.compile_page( + route, + component, + self.state, + self.connect_error_component, + ) # Add the custom components from the page to the set. custom_components |= component.get_custom_components() diff --git a/pynecone/compiler/compiler.py b/pynecone/compiler/compiler.py index a00974f2c..2f23b482d 100644 --- a/pynecone/compiler/compiler.py +++ b/pynecone/compiler/compiler.py @@ -15,6 +15,7 @@ from pynecone.vars import ImportVar # Imports to be included in every Pynecone app. DEFAULT_IMPORTS: imports.ImportDict = { "react": { + ImportVar(tag="Fragment"), ImportVar(tag="useEffect"), ImportVar(tag="useRef"), ImportVar(tag="useState"), @@ -31,7 +32,11 @@ DEFAULT_IMPORTS: imports.ImportDict = { ImportVar(tag="getRefValue"), }, "": {ImportVar(tag="focus-visible/dist/focus-visible")}, - "@chakra-ui/react": {ImportVar(tag=constants.USE_COLOR_MODE)}, + "@chakra-ui/react": { + ImportVar(tag=constants.USE_COLOR_MODE), + ImportVar(tag="Box"), + ImportVar(tag="Text"), + }, } @@ -62,12 +67,15 @@ def _compile_theme(theme: dict) -> str: return templates.THEME.render(theme=theme) -def _compile_page(component: Component, state: Type[State]) -> str: +def _compile_page( + component: Component, state: Type[State], connect_error_component +) -> str: """Compile the component given the app state. Args: component: The component to compile. state: The app state. + connect_error_component: The component to render on sever connection error. Returns: The compiled component. @@ -85,6 +93,7 @@ def _compile_page(component: Component, state: Type[State]) -> str: hooks=component.get_hooks(), render=component.render(), transports=constants.Transports.POLLING_WEBSOCKET.get_transports(), + err_comp=connect_error_component.render() if connect_error_component else None, ) @@ -188,7 +197,10 @@ def compile_theme(style: Style) -> Tuple[str, str]: @write_output def compile_page( - path: str, component: Component, state: Type[State] + path: str, + component: Component, + state: Type[State], + connect_error_component: Component, ) -> Tuple[str, str]: """Compile a single page. @@ -196,6 +208,7 @@ def compile_page( path: The path to compile the page to. component: The component to compile. state: The app state. + connect_error_component: The component to render on sever connection error. Returns: The path and code of the compiled page. @@ -204,7 +217,7 @@ def compile_page( output_path = utils.get_page_path(path) # Add the style to the component. - code = _compile_page(component, state) + code = _compile_page(component, state, connect_error_component) return output_path, code diff --git a/pynecone/components/__init__.py b/pynecone/components/__init__.py index 8cde02596..8bb5b418f 100644 --- a/pynecone/components/__init__.py +++ b/pynecone/components/__init__.py @@ -29,6 +29,7 @@ component = Component.create badge = Badge.create code = Code.create code_block = CodeBlock.create +connection_banner = ConnectionBanner.create data_table = DataTable.create divider = Divider.create list = List.create diff --git a/pynecone/components/overlay/__init__.py b/pynecone/components/overlay/__init__.py index 322d09318..227fdf536 100644 --- a/pynecone/components/overlay/__init__.py +++ b/pynecone/components/overlay/__init__.py @@ -8,6 +8,7 @@ from .alertdialog import ( AlertDialogHeader, AlertDialogOverlay, ) +from .banner import ConnectionBanner from .drawer import ( Drawer, DrawerBody, diff --git a/pynecone/components/overlay/banner.py b/pynecone/components/overlay/banner.py new file mode 100644 index 000000000..3bdcf70a3 --- /dev/null +++ b/pynecone/components/overlay/banner.py @@ -0,0 +1,33 @@ +"""Banner components.""" +from typing import Optional + +from pynecone.components.component import Component +from pynecone.components.layout import Box, Cond, Fragment +from pynecone.components.typography import Text +from pynecone.vars import Var + + +class ConnectionBanner(Cond): + """A connection banner component.""" + + @classmethod + def create(cls, comp: Optional[Component] = None) -> Component: + """Create a connection banner component. + + Args: + comp: The component to render when there's a server connection error. + + Returns: + The connection banner component. + """ + if not comp: + comp = Box.create( + Text.create( + "cannot connect to server. Check if server is reachable", + bg="red", + color="white", + ), + textAlign="center", + ) + + return super().create(Var.create("notConnected"), comp, Fragment.create()) # type: ignore