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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic sidebar #29

Merged
merged 7 commits into from
Apr 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ show_pages(
Section("My section", icon="🎈️"),
# Pages after a section will be indented
Page("Another page", icon="💪"),
# Unless you explicitly say in_section=False
Page("Not in a section", in_section=False)
]
)
```
Expand Down Expand Up @@ -125,6 +127,11 @@ is_section = true
[[pages]]
name = "Another page"
icon = "💪"

# Unless you explicitly say in_section = false`
[[pages]]
name = "Not in a section"
in_section = false
```

Streamlit code:
Expand All @@ -138,3 +145,10 @@ add_page_title()

show_pages_from_config()
```

# Hiding pages

You can now pass a list of page names to `hide_pages` to hide pages dynamically for each
user. Note that these pages are only hidden via CSS, and can still be visited by the URL.
However, this could be a good option if you simply want a way to visually direct your
user where they should be able to go next.
1 change: 1 addition & 0 deletions example_app/.streamlit/pages_sections.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,4 @@ path = "example_app/example_three.py"
path = "example_app/example_five.py"
name = "Example Five"
icon = "🧰"
in_section = false
14 changes: 13 additions & 1 deletion example_app/example_four.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import streamlit as st

from st_pages import add_page_title
from st_pages import add_page_title, hide_pages

add_page_title()

st.write("This is just a sample page!")

selection = st.radio(
"Test page hiding",
["Show all pages", "Hide pages 1 and 2", "Hide Other apps Section"],
)

if selection == "Show all pages":
hide_pages([])
elif selection == "Hide pages 1 and 2":
hide_pages(["Example One", "Example Two"])
elif selection == "Hide Other apps Section":
hide_pages(["Other apps"])
3 changes: 2 additions & 1 deletion example_app/streamlit_app_sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
# Will use the default icon and name based on the filename if you don't
# pass them
Page("example_app/example_three.py"),
Page("example_app/example_five.py", "Example Five", "🧰"),
# You can also pass in_section=False to a page to make it un-indented
Page("example_app/example_five.py", "Example Five", "🧰", in_section=False),
]
)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "st-pages"
version = "0.3.5"
version = "0.4.0"
description = "An experimental version of Streamlit Multi-Page Apps"
authors = ["Zachary Blackwood <zachary@streamlit.io>"]
readme = "README.md"
Expand Down
66 changes: 63 additions & 3 deletions src/st_pages/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ def page_icon_and_name(script_path: Path) -> tuple[str, str]:
from streamlit.util import calc_md5


def _add_page_title(add_icon: bool = True, also_indent: bool = True, **kwargs):
def _add_page_title(
add_icon: bool = True,
also_indent: bool = True,
hidden_pages: list[str] | None = None,
**kwargs,
):
"""
Adds the icon and page name to the page as an st.title, and also sets the
page title and favicon in the browser tab.
Expand Down Expand Up @@ -90,6 +95,9 @@ def _add_page_title(add_icon: bool = True, also_indent: bool = True, **kwargs):
if also_indent:
add_indentation()

if hidden_pages:
hide_pages(hidden_pages)


add_page_title = _gather_metrics("st_pages.add_page_title", _add_page_title)

Expand Down Expand Up @@ -136,6 +144,7 @@ class Page:
name: str | None = None
icon: str | None = None
is_section: bool = False
in_section: bool = True

@property
def page_path(self) -> Path:
Expand Down Expand Up @@ -174,6 +183,7 @@ def to_dict(self) -> dict[str, str | bool]:
"icon": self.page_icon,
"script_path": str(self.page_path),
"is_section": self.is_section,
"in_section": self.in_section,
"relative_page_hash": self.relative_page_hash,
}

Expand All @@ -184,6 +194,7 @@ def from_dict(cls, page_dict: dict[str, str | bool]) -> Page:
name=str(page_dict["page_name"]),
icon=str(page_dict["icon"]),
is_section=bool(page_dict["is_section"]),
in_section=bool(page_dict["in_section"]),
)


Expand Down Expand Up @@ -281,9 +292,7 @@ def _show_pages_from_config(path: str = ".streamlit/pages.toml"):
def _get_indentation_code() -> str:
styling = ""
current_pages = get_pages("")

is_indented = False

for idx, val in enumerate(current_pages.values()):
if val.get("is_section"):
styling += f"""
Expand All @@ -292,6 +301,10 @@ def _get_indentation_code() -> str:
}}
"""
is_indented = True
elif is_indented and not val.get("in_section"):
# Page is specifically unnested
# Un-indent all pages until next section
is_indented = False
elif is_indented:
# Unless specifically unnested, indent all pages that aren't section headers
styling += f"""
Expand Down Expand Up @@ -332,3 +345,50 @@ def _add_indentation():


add_indentation = _gather_metrics("st_pages.add_indentation", _add_indentation)


def _get_page_hiding_code(pages_to_hide: list[str]) -> str:
styling = ""
current_pages = get_pages("")
section_hidden = False
for idx, val in enumerate(current_pages.values()):
page_name = val.get("page_name")
if val.get("is_section"):
# Set whole section as hidden
section_hidden = page_name in pages_to_hide
elif not val.get("in_section"):
# Reset whole section hiding if we hit a page thats not in a section
section_hidden = False
if page_name in pages_to_hide or section_hidden:
styling += f"""
li:nth-child({idx + 1}) {{
display: none;
}}
"""

styling = f"""
<style>
{styling}
</style>
"""

return styling


def _hide_pages(hidden_pages: list[str]):
"""
For an app that wants to dynmically hide specific pages from the navigation bar.
Note - this simply uses CSS to hide the menu item, it does not remove the page
If using this with any security / permissions in mind,
you also need to block the hidden page from executing
"""

styling = _get_page_hiding_code(hidden_pages)

st.write(
styling,
unsafe_allow_html=True,
)


hide_pages = _gather_metrics("st_pages.hide_pages", _hide_pages)
63 changes: 63 additions & 0 deletions tests/test_frontend_sections.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,66 @@ def test_deprecation_warning(page: Page):
expect(
page.get_by_text("st.experimental_singleton is deprecated")
).not_to_be_visible()


def test_in_section_false(page: Page):
bbox_not_in_section = (
page.get_by_role("link", name="Example Five")
.get_by_text("Example Five")
.bounding_box()
)
bbox_in_section = (
page.get_by_role("link", name="Example Four")
.get_by_text("Example Four")
.bounding_box()
)

assert bbox_in_section is not None
assert bbox_not_in_section is not None

# Check that the in_section=False page is at least 10 pixels to the left of the
# in_section=True page
assert bbox_not_in_section["x"] < bbox_in_section["x"] - 10


def test_page_hiding(page: Page):
page.get_by_role("link", name="Example Four").click()
expect(page.get_by_role("link", name="Example one")).to_be_visible()
expect(page.get_by_role("link", name="Example two")).to_be_visible()
expect(
page.get_by_test_id("stSidebarNav")
.locator("div")
.filter(has_text="🐴Other apps")
).to_be_visible()
expect(page.get_by_role("link", name="Other apps")).to_be_visible()
expect(page.get_by_role("link", name="Example three")).to_be_visible()

page.get_by_text("Hide pages 1 and 2").click()
expect(page.get_by_role("link", name="Example one")).to_be_hidden()
expect(page.get_by_role("link", name="Example two")).to_be_hidden()
expect(
page.get_by_test_id("stSidebarNav")
.locator("div")
.filter(has_text="🐴Other apps")
).to_be_visible()
expect(page.get_by_role("link", name="Example three")).to_be_visible()

page.get_by_text("Hide Other apps Section").click()
expect(page.get_by_role("link", name="Example one")).to_be_visible()
expect(page.get_by_role("link", name="Example two")).to_be_visible()
expect(
page.get_by_test_id("stSidebarNav")
.locator("div")
.filter(has_text="🐴Other apps")
).to_be_hidden()
expect(page.get_by_role("link", name="Example three")).to_be_hidden()

page.get_by_text("Show all pages").click()
expect(page.get_by_role("link", name="Example one")).to_be_visible()
expect(page.get_by_role("link", name="Example two")).to_be_visible()
expect(
page.get_by_test_id("stSidebarNav")
.locator("div")
.filter(has_text="🐴Other apps")
).to_be_visible()
expect(page.get_by_role("link", name="Example three")).to_be_visible()