Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Coming soon] Multi-page apps: Improved API and new navigation UI features #8388

Open
sfc-gh-jcarroll opened this issue Mar 27, 2024 · 25 comments
Labels
feature:multipage-apps type:enhancement Requests for feature enhancements or new features

Comments

@sfc-gh-jcarroll
Copy link
Collaborator

sfc-gh-jcarroll commented Mar 27, 2024

Hi folks! 馃憢

I wanted to preview a new way to define multi-page Streamlit apps and some new features coming to the side navigation. We're aiming to release these updates within the next 2 months.

鈿狅笍馃憠 NOTE: this will be additive, the current MPA approach with pages/ will still work just fine 馃檪

Update: Check out the 馃憠 demo app 馃憟 & whl file!

https://multipage-v2-preview.streamlit.app/

Summary

Using the new API, when you do streamlit run streamlit_app.py, the contents of streamlit_app.py will automatically run before every page instead of defining the home page. Any common code can go here.

  • We're building a new API called st.navigation() with st.Page() to programmatically define the available pages of your app.
  • This means the available and displayed pages can change in a given session / rerun! (see code example below)
  • Besides defining your pages / nav, you can also add any common session setup code, authorization check, page_config, styles, etc only once in this file.
  • We also plan to add native support for an app logo at the top left, text headings between page groups in the native navigation, and support for Material icons in addition to emojis

Here's how a native side navigation might look using all the new features:

image

Here's the simplest example of how this would look in your app code:

# streamlit_app.py

# Define all the available pages, and return the current page
current_page = st.navigation([
    st.Page("hello.py", title="Hello World", icon=":material:Plane:"),
    st.Page("north_star.py", title="North Star", icon=":material:Star:"),
    # ...
])

# call run() to execute the current page
current_page.run()

Pages can be defined by path to a python file, or passing in a function.

def Page(
    page: str | Callable,
    *,
    title: str = None,
    icon: str = None, # emojis and material icons
    default: bool = False, # Explicitly set a default page ("/") for the app
    url_path: str = None, # set an identifier and /url-path for the page, otherwise inferred
)

New navigation UI

Here's a fuller example with a logo and section headers.

st.logo("my_logo.png")

# Define all the available pages, and return the current page
current_page = st.navigation({
    "Overview": [
        st.Page("hello.py", title="Hello World", icon=":material:Plane:"),
        st.Page("north_star.py", title="North Star", icon=":material:Star:"),
    ],
    "Metrics": [
        st.Page("core_metrics.py", title="Core Metrics", icon=":material:Hourglass:"),
        # ...
    ],
})

# current_page is also a Page object you can .run()
current_page.run()

Logos

Calling st.logo(image) adds an app logo in the top left of your app, floating above the navigation / sidebar.

Material icons

You'll be able to use a wide range of Material icons for page navigation and other elements that support icon= today. Our current plan is to support the Material icons built into @emotion. You can specify these via shortcode, such as icon=":material:Archive". The final details of this might change a bit before release.

Navigation headers

By default, st.navigation expects a list of Pages (List[st.Page]). However you can also pass Dict[str: List[st.Page]]. In this case, each dictionary key becomes a section header in the navigation with the listed pages below. E.g.

current_page = st.navigation({
    "Overview": [ # "Overview" becomes a section header
        st.Page("hello.py"),
        st.Page("north_star.py"),
    ],
    "Metrics": [ # "Metrics becomes a section header
        st.Page("core_metrics.py"),
        # ...
    ],
})

Dynamic navigation

The available pages are re-assessed on each rerun. So, for example, if you want to add some pages only if the user is authenticated, you can just append them to the list passed to st.navigation based on some check. E.g.:

import streamlit as st

pages = [st.Page("home.py", title="Home", icon="馃彔", default=True)]

if is_authenticated():
    pages.append(st.Page("step_1.py", title="Step 1", icon="1锔忊儯"))
    pages.append(st.Page("step_2.py", title="Step 2", icon="2锔忊儯"))

page = st.navigation(pages)

page.run()

You can also set position="hidden" on st.navigation if you want to use the new API while defining your own navigation UI (such as via st.page_link and st.switch_page).

# streamlit_app.py
import streamlit as st

pages = [
    st.Page("page1.py", title="Page 1", icon="馃搳"),
    st.Page("page2.py", title="Page 2", icon="馃寑"),
    st.Page("page3.py", title="Page 3", icon="馃З"),
]

# Makes pages available, but position="hidden" means it doesn't draw the nav
# This is equivalent to setting config.toml: client.showSidebarNavigation = false
page = st.navigation([pages], position="hidden")

page.run()

# page1.py
st.write("Welcome to my app. Explore the sections below.")
col1, col2, col3 = st.columns(3)
col1.page_link("page1.py")
col2.page_link("page2.py")
col3.page_link("page3.py")

# page2.py
st.markdown(long_about_text)
if st.button("Back"):
    st.switch_page("page1.py")

We're still putting the final touches on this feature so the final API and UI might change slightly. But we're excited about this and wanted to share, so you know what's coming and in case you have early feedback! Thanks!! 馃巿馃巿

@sfc-gh-jcarroll sfc-gh-jcarroll added the type:enhancement Requests for feature enhancements or new features label Mar 27, 2024
Copy link

To help Streamlit prioritize this feature, react with a 馃憤 (thumbs up emoji) to the initial post.

Your vote helps us identify which enhancements matter most to our users.

Visits

@sfc-gh-jcarroll sfc-gh-jcarroll pinned this issue Mar 27, 2024
@gaspardc-met
Copy link

Hello @sfc-gh-jcarroll ,
Thank you for the great announcement post - love the new features !
Navigation headers are great and of course the dynamic evaluation of accessible pages opens up a lot of use cases.

Would it be possible to play with the current version on a small demo or to try it out locally on my repo (in order to provide better feeback, not to deploy it like that) ?
I tried cloning and installing the branch but I am missing some protos at the moment (totally normal on a draft PR)

Real icons in addition to emojis will be awesome as well: is this going to be only for the navigation to start with, or rolled out globally before the navigation ?

Also, I guess the "hidden" navigation arguments opens up the possibility of using the API with say a custom topbar/navbar from Material UI for example ?

Looking forward to try this out !

@sfc-gh-jcarroll
Copy link
Collaborator Author

Glad you like it! We'll add the real icons in other places soon too. Yes, I think you could use a custom topbar / navbar too (BTW this is already possible by setting config client.showSidebarNavigation = false and using st.switch_page()

We don't yet have a demoable WHL file but will share it when we do :)

@sfc-gh-jcarroll
Copy link
Collaborator Author

We have a preview whl file available for early testing and a demo app. Check it out here. The whl is linked once you click "Login"

https://multipage-v2-preview.streamlit.app/

Note: there are some known bugs on the whl file since it's an early version

@RusabKhan
Copy link

Looks really good but the sidebar keeps closing and reopening when I change pages. It's frustrating and makes navigation difficult. Is this intentional or a bug? Also, will there be an option to control this behavior in the config.toml file?

@sfc-gh-jcarroll
Copy link
Collaborator Author

Hi @RusabKhan - it doesn't sound like an expected behavior, is it something you could record a quick video to show the behavior you are seeing? We are also improving and fixing bugs in the preview WHL so it's possible an upcoming improvement will address this. Thank you!

@gaspardc-met
Copy link

Hi @sfc-gh-jcarroll ,

I have finally had time to give it a go, love it so far 馃殌
At the moment my pages where in project/Pages, and when I configured them all in the Navigation, only the first one worked and the others couldn't be found.
I tried switching them and the first always worked.

I then moved all the pages to project and then it worked for all pages 馃憤
It's great to have icons and large and collapsed logos !

As for the one entrypoint to execute all common code (like auth) it's a great idea.
Currently I have a function to initialize each page, call set_page_config, set the favicon, change the styling and it mostly works (page width for example) but the favicon is dropped.

Great potential 馃憤

@RusabKhan
Copy link

Hi @RusabKhan - it doesn't sound like an expected behavior, is it something you could record a quick video to show the behavior you are seeing? We are also improving and fixing bugs in the preview WHL so it's possible an upcoming improvement will address this. Thank you!

trim.DBDB18DD-3367-4EC7-A3C3-9F7454A092BD.MOV

@sfc-gh-jcarroll
Copy link
Collaborator Author

Thanks @RusabKhan - no this is definitely not intended behavior. Can I ask about your device / browser and internet speed? It looks like Safari? Thanks for the video!

@RusabKhan
Copy link

@sfc-gh-jcarroll your welcome,

Device: iPad 10th Gen
OS: 17.4.1
Browser: Safari

@nozwock
Copy link

nozwock commented Apr 27, 2024

Visiting pages via the URL doesn't work, it fails to find the page and redirects to the default page instead. (Which in turn also affects page reloading.)
For eg. If I were to visit the URL http://localhost:8501/p1, I'd get this:

page_dict={'106a6c241b8797f52e1e77317b96a201': Page(page=<function home at 0x795e6414d440>, title='home', icon=':material/home:', default=True, _url_path=''), '17e477fe899e29fb93a2117a6a9baadf': Page(page=<function p1 at 0x795e6414d4e0>, title='page 1', icon='', default=False, _url_path='p1'), 'd10adbb5b95064f051384d087c73ff59': Page(page=<function p2 at 0x795e6414d580>, title='page 2', icon='', default=False, _url_path='p2')}
could not find page for 53b9ec3d2e04403081a24ab499835919, falling back to default page
import streamlit as st


def home():
    pass


def p1():
    pass


def p2():
    pass


main_page = st.navigation(
    {
        "Overview": [
            st.Page(home, title="home", default=True, icon=":material/home:"),
        ],
        "Other": [
            st.Page(p1, title="page 1"),
            st.Page(p2, title="page 2"),
        ],
    }
)

main_page.run()

@amanchaudhary-95
Copy link

This is a great feature. As I understand (correct me if I'm wrong), the st.navigation() will create navigation in sidebar only. Can it be used outside of sidebar or can we use this to create the actual navbar? I saw this custom component link. If the proposed st.navigation() can be used to create the actual navbar, it will be very useful. Streamlit doesn't has the navbar component.

@Pballer
Copy link

Pballer commented May 7, 2024

I am running your demo script. In my config, if I set fileWatcherType = "auto", then I get this error:

File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/runtime/scriptrunner/script_runner.py", line 579, in _run_script
    exec(code, module.__dict__)
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/configuration/app/Test_Home.py", line 17, in <module>
    pg = st.navigation([st.Page(empty_page, title="Streamlit Multi-Page V2")], position="hidden")
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/commands/pages.py", line 143, in navigation
    ctx.pages_manager.set_pages(pgs.as_pages_dict())
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/runtime/pages_manager.py", line 297, in set_pages
    self._on_pages_changed.send()
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/blinker/base.py", line 279, in send
    result = receiver(sender, **kwargs)
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/runtime/app_session.py", line 461, in _on_pages_changed
    self._local_sources_watcher.update_watched_pages()
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/watcher/local_sources_watcher.py", line 70, in update_watched_pages
    self._register_watcher(
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/watcher/local_sources_watcher.py", line 129, in _register_watcher
    watcher=PathWatcher(filepath, self.on_file_changed),
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/watcher/polling_path_watcher.py", line 70, in __init__
    self._modification_time = util.path_modification_time(
File "/Volumes/workplace/myteam/MyTestInfrastructure/src/MyTestImageBuild/.venv/lib/python3.9/site-packages/streamlit/watcher/util.py", line 84, in path_modification_time
    return os.stat(path).st_mtime

If I set fileWatcherType = "none", then it works. Any idea why file watcher is breaking?

@sfc-gh-jcarroll
Copy link
Collaborator Author

Yeah, the latest whl has an issue if watchdog isn't installed. We're working on fixing it :) in the meantime, pip install watchdog should also resolve that issue.

@LukasMasuch LukasMasuch pinned this issue May 14, 2024
@Pballer
Copy link

Pballer commented May 16, 2024

Will this feature support pages defined in subfolders? For example, will we be able to do this?
st.Page("pages/entertainment/movies.py", title="Movie Explorer", icon=":material/movie_filter:"),

@nozwock
Copy link

nozwock commented May 16, 2024

Will this feature support pages defined in subfolders? For example, will we be able to do this?
st.Page("pages/entertainment/movies.py", title="Movie Explorer", icon=":material/movie_filter:"),

It already does, just not the pages sub-directory in the root, as it's hard-coded with the previous navigation API.

So, anything other than pages would work fine.

st.Page("app_pages/entertainment/movies.py")

@sfc-gh-jcarroll
Copy link
Collaborator Author

^ Yep what @nozwock said is correct

@dowlle
Copy link

dowlle commented May 22, 2024

Would it be possible to set the page slug independently of the page title? I would like to dynamically load my multipage app with different languages, but I want to keep URL's the same for easier reference for users when tracking bugs. I see the 'key' variable has been removed, but I feel like that would be a great addition to this new functionality to give us some more options.

@gaspardc-met
Copy link

Hey @sfc-gh-jcarroll ,
Had a second run with the latest wheel (1.34.0) and built a bigger demo, so I ran into more specific questions.
It was great using it, both in terms of UX and in terms of appearance. Developer experience is also so much better with a fine control of the navigation. I really like the logo/collapsed logo and the fact that the sidebar collapses on page change (this is the old behavior, still relevant here tho)

First a remark, currently when you start the webapp there is a small time window where the layout in the sidebar appears "as usual" : name of the entrypoint file (demo in my case) then a list of all the file names in /pages. Then I guess it reaches a point of navigation rendering and corrects itself to show the expected layout with section headers and pages defined in page navigation. Probably known already on your side.

With this new navigation, how are you going to handle page_link and other direct navigation app_url/app_page from now on ? Current page links broke (expected) but I don't know if I can already hack them to work with MPAv2 ?

The favicon changing to the current page icon is very nifty, but not very convenient in my use case: will this be optional ?

Finally, is there a plan to create a kind of default "landing page" with cards leading to the pages mapped in the navigation grouped by their section headers ? I could probably make it myself, just wondering if it was in the plans 馃憤

Thanks again,

@sfc-gh-jcarroll
Copy link
Collaborator Author

@dowlle yes, we had a little complexity around that feature but we plan to add the kwarg back for setting a different title vs url-path before launch (or maaaybe soon afterward if we run into problems but I think we have a clear path)

@gaspardc-met thank you for the encouraging feedback!! 馃槃

  • Small window with the old layout: There are some known issue blips which we're fixing. Also, if you put the page files anywhere other than in a folder called pages/ it should resolve that (I think??). We talked about whether we need some config to leave the files in pages/ but it seems like overkill. Does it make sense? are you seeing something else?
  • page_link broken: For files-as-pages, in the shipped version you should be able to specify a string filename like today. We'll also support passing an st.Page() object into a page_link or switch_page (which could have a filename or a callable defining the page content). I know there were some bugs during development so you may have seen that version.
  • Fixed favicon: There was a bug but in the final version you should be able to do st.set_page_config(page_icon=SOME_STATIC_ICON) and it will override the current page icon.
  • Landing page: We don't have a specific plan for this right now but I agree it would be a useful template! If you have some ideas on how to support that natively, would welcome an enhancement request.

Let me know if those answer the questions / concerns or if anything is still unclear!

@sfc-gh-jcarroll
Copy link
Collaborator Author

Hi folks, we recently posted an updated preview WHL file for this feature which has the near-final behavior. We'll plan to launch this in the next release version 1.36 in mid-June.

  • Please share any bugs or un-intuitive behavior you find in the WHL file so we can address it before the launch. In our testing it looks pretty good!!
  • This version includes the url_path= kwarg in st.Page() to explicitly set the slug, e.g. st.Page(fn, url_path="metrics", title="Key Metrics"). The basic case works, however we will fix some encoding bugs and also might change the behavior a bit before launch.

Again, you can see it in action at https://multipage-v2-preview.streamlit.app/

Thanks for all the amazing feedback to improve this feature! Looking forward to the launch! 馃殌

@RusabKhan
Copy link

Hey @sf-gh-jcaroll, the original issue I reported in this thread has changed its behavior. Now, when changing pages from the sidebar, the first click does not register, and on the second click, the sidebar collapses.

trim.D0B23796-13FE-4842-9E83-CD77AC66E3B1.MOV

@gaspardc-met
Copy link

Hello @sfc-gh-jcarroll,
Thanks for the feedback and updates!

Small window with the old layout:
Yes indeed it seems that just renaming my 'pages' folder would do the trick then, this doesn't need solving then 馃槄

page_link broken:
Awesome, what is planned seems entirely sufficient with both path and callable 馃憤馃徏

Fixed favicon:
Perfect so the default page favicon would be the webapp favicon from the config or the page icon from the navigation? Both are understandable.

Landing page:
I'll try to come up with a visual mock-up of what I had in mind, will keep you posted if it looks any good.

We've deployed the 1.34 mpa-v2 wheel to our demo environment to great positive feedback internally, so kudos to the streamlit team 馃憦馃徏

@gaspardc-met
Copy link

P.S.: Would there be a way to show all navigation headers and pages by default in the sidebar ?
A capacity to set "View more" to True by default for example
image

@sfc-gh-jcarroll
Copy link
Collaborator Author

Thanks @RusabKhan - I think the "duplicated pages" is just that I was lazy and made the same content on the first two pages of that app 馃槄

The other behavior of the sidebar collapsing seems to be an existing Streamlit behavior whenever the screen width is below a certain point, not specific to the updated Multipage App (I can see the same thing on llm-examples.streamlit.app for example). I'm not sure if that behavior changed from an earlier version. If you think it's a bug or that the behavior should change, feel free to file a separate issue for discussion (you can tag me if you like)

@gaspardc-met for favicon:

  1. st.set_page_config(page_icon="foo") will take highest precedence if specified
  2. st.Page(icon="foo") for current page is next
  3. The default Streamlit crown favicon if neither of those ^ is set

Would there be a way to show all navigation headers and pages by default in the sidebar ? A capacity to set "View more" to True by default for example

Oh, interesting idea! The current behavior is similar to the existing MPA. Since we have st.navigation() it seems like an expanded=True similar to st.expander() would be possible. I am going to make a note to investigate that beyond the initial release, feel free to file a separate enhancement issue too if you'd like, to gauge interest.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature:multipage-apps type:enhancement Requests for feature enhancements or new features
Projects
None yet
Development

No branches or pull requests

8 participants