streamlit / streamlit

Streamlit — A faster way to build and share data apps.
https://streamlit.io
Apache License 2.0
34.73k stars 3.01k forks source link

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

Closed sfc-gh-jcarroll closed 3 months ago

sfc-gh-jcarroll commented 6 months ago

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 :point_right: demo app :point_left: & 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.

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!! 🎈🎈

github-actions[bot] commented 6 months ago

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

gaspardc-met commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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 commented 5 months ago

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!

https://github.com/streamlit/streamlit/assets/74367699/e68c6f70-4b7b-452d-9205-ad7e846a1a88

sfc-gh-jcarroll commented 5 months ago

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 commented 5 months ago

@sfc-gh-jcarroll your welcome,

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

nozwock commented 5 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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.

Pballer commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

^ Yep what @nozwock said is correct

dowlle commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

@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!! 😄

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

sfc-gh-jcarroll commented 4 months ago

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.

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 commented 4 months ago

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.

https://github.com/streamlit/streamlit/assets/74367699/5fec6d5b-9712-4d9a-8d9d-0ba3e44e2f2d

gaspardc-met commented 4 months ago

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 commented 4 months ago

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 commented 3 months ago

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.

lucasrodes commented 3 months ago

@gaspardc-met, in case it helps:

we have a home-page with a list of cards of the various pages in the app. Don't have a public app running, but the code is here: https://github.com/owid/etl/blob/master/apps/wizard/home.py. In addition, we use a config yaml file, which contains the links to the various pages (so we don't define the pages in multiple places).

@sfc-gh-jcarroll: thanks for your work. ~We are currently using st-pages to work with multiple pages. I had the same question than @nozwock ー will url links to pages work? Currently we are struggling with this when using st-pages. We get the Page not found error~

UPDATE: I've installed streamlit-nightly, and been testing this. I think that URL to pages works, but for some reason the warning message is still shown! I think that the floating message should not be shown, since the page is actually loading!

As a workaround, I am using st.query_params to link to specific pages of the app.

image

Finally, I was in bryce a month ago and loved it 😄

lucasrodes commented 3 months ago

@sfc-gh-jcarroll I've been trying it using the nightly version, and I have to say that this is looking great! It could solve several issues on our end.

I was wondering if the page config could be set when defining st.Page, like:

# THIS IS A PROPOSAL, NOT ACTUALLY WORKING ATM

def Page(
    page: str | Callable,
    *,
    config: Dict[str, Any] = None,
    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
)

Otherwise, one needs to define this within each page using st.page_config.

gaspardc-met commented 3 months ago

Hey @lucasrodes ,

Thanks for the code snippet, will try it out. Another question, probably stupid: how to you use the nightly ? I installed the latest streamlit-nightly in my venv and for some reason both streamlit --version and streamlit-nightly --version returned an error, and I was unable to run streamlit against my MPAv2 entrypoint.

Probably a path issue or just a venv issue, will try again from scratch.

lucasrodes commented 3 months ago

@gaspardc-met I did first uninstall the stable version of streamlit, and the simply installed the nightly version with pip install streamlit-nightly. It worked for me like that. Tested running print(st.__version__) from the python shell.

Just as a heads up, I ran into an issue with the nightly version and MPAv2, which I reported here: https://github.com/streamlit/streamlit/issues/8827 (being solved soon).

sfc-gh-jcarroll commented 3 months ago

Hey @lucasrodes glad you like the feature! thank you very much for that bug report!! And for the idea to add config= in st.Page. Would you mind opening a separate enhancement issue for this so we can track the interest and discuss further?

As a solution today, you can do something like this:

# app.py

pages = [
    st.Page("analysis.py", title="Data Analysis", icon=":material/analytics:"),
    st.Page("eval.py", title="Automated Evaluation", icon=":material/quiz:"),
    st.Page("users.py", title="User Management", icon=":material/group:"),
]

pg = st.navigation(pages)

st.set_page_config(
    page_title=f"GenAI Evaluation: {pg.title}",
    layout="wide" if pg.url_path == "analysis" else "centered",
)

pg.run()
gaspardc-met commented 3 months ago

Hey @lucasrodes : finally managed to use the nightly, great idea thanks.

Hey @sfc-gh-jcarroll : everytime the code changes (rerun on change) the app errors because of the classic "set_page_config" can only be set once per page and must be the first thing executed. It's the first line of the main function I use to run the module that creates the navigation with all the pages, and to the best of my knowledge it's only called once in the pages (should I remove it from pages ?) I must reload the browser (Firefox) anytime I want a change and want to see it. Apart from that, nightly version is great 👍

I have created some navigation cards, will share with anonymous pages and images.

sfc-gh-jcarroll commented 3 months ago

@gaspardc-met

everytime the code changes (rerun on change) the app errors because of the classic "set_page_config"

Yes, you'll want to only call it once - EITHER in the main function, OR in each of the pages. Does that resolve the issue? If not would be awesome to see a more detailed repro so we can investigate.

Seems like this is causing trouble for many of the people who tried the feature so far, so we'll make sure we have good documentation for the initial launch and probably make some more improvements here soon so it's clearer and easier to use with a custom set_page_config.

lucasrodes commented 3 months ago

Hey @lucasrodes glad you like the feature! thank you very much for that bug report!!

Hi @sfc-gh-jcarroll, in case it helps, I've reported the aforementioned bug (Page not found in MPAv2) in https://github.com/streamlit/streamlit/issues/8860

Thanks!

nbobr1 commented 3 months ago

Question; is it possible with "nested multi-page apps".

App/

I want the st.navigation (bar) to be interactively based on which "app" you're currently in.

Options (/st.navigation) when in the main_page.py

Options (/st.navigation) when in the some_app.py

Options (/st.navigation) when in the other_app.py

Goal:

@sfc-gh-jcarroll

sfc-gh-jcarroll commented 3 months ago

Hi @nbobr1

We don't have an out-of-the-box support for that use case but it should be possible to do what you described natively with st.navigation.

Without thinking too hard about it, I would keep track in session state of which "sub-app" the viewer is currently in by checking the current page object on each run. Then, check the session state value at the beginning of the run and show the relevant pages for that sub-app.

For returning to the main page / parent app, you could use st.switch_page or st.page_link to draw that outside the navigation, such as at the bottom of the sidebar.

sfc-gh-jcarroll commented 3 months ago

Hey good news, the feature has launched with 1.36 release 🚀 https://github.com/streamlit/streamlit/releases/tag/1.36.0

Full docs should be available in the next day or so. Enjoy and let us know how it is! We'll plan some small improvements over the next few releases.

michalmar commented 1 week ago

I love this feature! Not sure if this has been solved (i am on the latest version by time of posting) but when I am on specific page (some other than main) and hit refresh it keep saying the page cannot be found but everything seems to be working... any ideas? worksrounds?

image