Skip to content

Commit

Permalink
Merge pull request blackary#29 from mfriedy/dynamic-sidebar
Browse files Browse the repository at this point in the history
Dynamic sidebar
  • Loading branch information
blackary authored Apr 5, 2023
2 parents 0aebe56 + edb5b30 commit 5a7bc4e
Show file tree
Hide file tree
Showing 7 changed files with 157 additions and 6 deletions.
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 <[email protected]>"]
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()

0 comments on commit 5a7bc4e

Please sign in to comment.