widgetti / solara

A Pure Python, React-style Framework for Scaling Your Jupyter and Web Apps
https://solara.dev
MIT License
1.6k stars 104 forks source link

creating a multi-page app without forcing an appbar #584

Open havok2063 opened 1 month ago

havok2063 commented 1 month ago

Is it possible to create a multi-page app without forcing the creation of an app bar with tabbed pages? I effectively have two "single-page" apps, each with their own top-level Page component, that I need to combine into a multi-page app, with each behind a route. I tried something like the following:

import solara

from sdss_solara.pages.jdaviz_embed import Page as Embed
from sdss_explorer.pages import Page as Dashboard

@solara.component
def Home():
    solara.Markdown("Solara Home")

routes = [
    solara.Route(path="/", component=Home, label="Home"),
    solara.Route(path="embed", component=Embed, label="Embed"),
    solara.Route(path="dashboard", component=Dashboard, label="Dashboard"),
]

but this forces an AppBar, which covers up components on the "embed" page. Additionally, navigating to the home page first correctly displays the "Solara Home" text, but then toggling to the embed page and back to home, the "Solara Home" content is missing.

In this case I don't need the page navigation, I just need to point to "site/solara/embed" and "site/solara/dashboard" separately and have them render as expected.

Also for reference, I am not running this with the solara server, the combined app will be sub-mounted into an existing FastAPI app. I think that just means that the SOLARA_APP env variable should point to the top-level solara application module.

I tried disabling the layout by adding the following in the above top-level module but that didn't seem to change anything.

@solara.component
def Layout(children=[]):
    # there will only be 1 child, which is the Page()
    return children[0]
iisakkirotko commented 3 weeks ago

Hi Brian!

I think I found the issue - you manually define routes in the top-level module, which overrides the default behaviour (Solara trying to pick up Page and Layout components automagically). What you have should work if you change your root route to manually define the layout, like so:

routes = [
    solara.Route(path="/", component=Home, label="Home", layout=Layout),
    ...
]

I also see that we used to warn about this before https://github.com/widgetti/solara/commit/ce4a3a0f2ed1eaddb74e0aaeebb81aefa6d15295. I'll reintroduce the warning for the next person to stumble into this!

havok2063 commented 3 weeks ago

@iisakkirotko Thanks, that almost does the trick! The remaining issue is the dashboard sub page I have has its own layout, with its own AppBar and SidePanel, that I'd like to preserve. I tried the following, which mostly works. It brings back its layout but also adds the tabs back to the "Home" and "Embed" pages, which I'd like to remove. I'd like the dashboard's original layout. Is that possible?

import solara

from sdss_solara.pages.jdaviz_embed import Page as Embed
from sdss_explorer.pages import Page as Dashboard, Layout as DashLayout

@solara.component
def Layout(children=[]):
    # there will only be 1 child, which is the Page()
    return children[0]

@solara.component
def Home():
    solara.Markdown("Solara Home")

routes = [
    solara.Route(path="/", component=Home, label="Home", layout=Layout),
    solara.Route(path="embed", component=Embed, label="Embed"),
    solara.Route(path="dashboard", component=Dashboard, label="Dashboard", layout=DashLayout),
]
havok2063 commented 3 weeks ago

The dashboard Page and Layout are roughly defined like

import solara as sl
import reacton.ipyvuetify as rv
from components import sidebar, ObjectGrid, LoginButton, AlertSystem, State

@sl.component
def Page():

    sl.Title("Visboard")
    with sl.AppBar():
        # main title object
        sl.AppBarTitle(children=[rv.Icon(children=["mdi-orbit"]), " SDSS"])

        # dataset selection
        btn = sl.Button(label=State.dataset.value,
                        icon_name="mdi-database",
                        text=True)
        if State.dataset.value is not None:
            with lab.Menu(activator=btn, close_on_content_click=True):
                with sl.Column(gap="0px"):
                    [
                        sl.Button(
                            label=dataset,
                            on_click=create_callable(dataset),
                        ) for dataset in State.datasets
                        if dataset != State.dataset.value
                    ]

        # appbar buttons
        LoginButton()

    # SIDEBAR
    sidebar()

    # MAIN GRID
    ObjectGrid()

    # snackbar
    AlertSystem()

@sl.component
def Layout(children):
    return sl.AppLayout(sidebar_open=False, children=children, color="purple")

If I leave out the layout in the base route definition solara.Route(path="dashboard", component=Dashboard, label="Dashboard"), then only the dashboard's main content area from the Page, the ObjectGrid, renders.

iisakkirotko commented 3 weeks ago

Hey again!

Indeed the tabs are added for sibling routes. You can avoid this happening by doing something like:

routes = [
    solara.Route(path="/", component=Home, label="Home", layout=Layout),
    solara.Route(path="dashboard", layout=DashLayout, children=[
        solara.Route(path="/", component=Dashboard, label="Dashboard")
    ]),
    ...
]
havok2063 commented 3 weeks ago

Hmm, that didn't seem to work. Both the home and embed pages render correctly, but the dashboard page still displays the sibling tabs.

iisakkirotko commented 3 weeks ago

Ah, I see what I missed. I think solara.AppLayout picks up the other routes, as you can see from

https://github.com/widgetti/solara/blob/fbd9192dc853c501840b3a5e3e0dce28dcf4c403/solara/components/applayout.py#L282-L286

I think for now you'd need to make a custom implementation of the AppLayout, since navigation=False removes the whole AppBar from the layout. I'll take another look at this next week and get back to you, but I feel like perhaps the condition at

https://github.com/widgetti/solara/blob/fbd9192dc853c501840b3a5e3e0dce28dcf4c403/solara/components/applayout.py#L260

isn't the most optimal, and could be changed to allow for an AppBar to still be displayed without the tabs.

iisakkirotko commented 2 weeks ago

Hey again @havok2063! I think you could achieve what you're after for the dashboard page by calling solara.use_route() in DashLayout to override the base route, and using solara.AppBarTitle in the Dashboard component to force the Appbar to display.