Add page visibility parameter to st.Page#13905
Conversation
Implement the `visibility` parameter for `st.Page` to allow hiding pages from navigation while remaining accessible via direct URL, st.page_link, and st.switch_page. Includes: - Backend: Page visibility parameter with validation - Frontend: Hidden page filtering in SidebarNav and TopNav - E2E tests: st.switch_page, sections with all hidden pages - Unit tests: TopNav hidden page tests, section visibility tests - Code cleanup: Removed unused navSections parameter from shouldShowNavigation
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
✅ PR preview is ready!
|
st.Page
st.Pagest.Page
There was a problem hiding this comment.
Pull request overview
Adds a visibility parameter to st.Page to support “hidden” pages that remain routable and linkable (URL, st.page_link, st.switch_page) while being excluded from rendered navigation UI.
Changes:
- Backend: Add
visibilitytost.Page/StreamlitPagewith validation and forward it via protobuf (AppPage.is_hidden). - Frontend: Filter hidden pages out of SidebarNav/TopNav rendering and update nav visibility logic to consider only visible pages.
- Tests/specs: Add unit + E2E coverage for hidden-page routing and navigation rendering behavior, plus product/tech specs.
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| specs/0009-page-visibility/tech-spec.md | Technical design for backend/proto/frontend changes and edge cases. |
| specs/0009-page-visibility/product-spec.md | Product/API spec describing the new visibility parameter and behaviors. |
| proto/streamlit/proto/AppPage.proto | Adds is_hidden field to wire hidden-page state to the frontend. |
| lib/streamlit/navigation/page.py | Introduces visibility parameter, validates it, and stores _visibility. |
| lib/streamlit/commands/navigation.py | Populates AppPage.is_hidden based on StreamlitPage._visibility. |
| lib/tests/streamlit/navigation/page_test.py | Adds unit tests for default/hidden/visible/invalid visibility. |
| lib/tests/streamlit/commands/navigation_test.py | Adds unit tests ensuring is_hidden is set correctly in navigation messages and routing still works. |
| frontend/app/src/components/Navigation/utils.ts | Updates shouldShowNavigation to count only visible pages; adds filterVisiblePages; simplifies section handling. |
| frontend/app/src/components/Navigation/utils.test.ts | Expands tests for hidden-page behavior and adds tests for filterVisiblePages. |
| frontend/app/src/components/Navigation/SidebarNav.tsx | Filters hidden pages for display and updates visible page counting logic. |
| frontend/app/src/components/Navigation/SidebarNav.test.tsx | Adds tests asserting hidden pages/sections don’t render and visible counts drive “View more”. |
| frontend/app/src/components/Navigation/TopNav.tsx | Filters hidden pages for display and ensures section data is built from visible pages only. |
| frontend/app/src/components/Navigation/TopNav.test.tsx | Adds tests asserting hidden pages/sections don’t render in top nav. |
| frontend/app/src/components/AppView/AppView.tsx | Updates shouldShowNavigation call signature usage. |
| frontend/app/src/components/AppView/AppView.test.tsx | Updates tests for the new shouldShowNavigation(appPages) signature. |
| frontend/app/src/components/Sidebar/Sidebar.tsx | Removes unused navSections usage and updates shouldShowNavigation call. |
| frontend/vitest.setup.ts | Adds a localStorage mock fallback and adjusts global assignments typing. |
| frontend/app/tsconfig.json | Includes vitest.setup.ts in the app TS project includes. |
| e2e_playwright/st_page_visibility.py | New E2E app exercising hidden pages, sections, and top/sidebar nav positions. |
| e2e_playwright/st_page_visibility_test.py | New E2E tests validating hidden pages aren’t displayed but remain accessible via URL/link/switch_page. |
Fix mypy error by adding explicit type annotation allowing both list and dict types for the pages variable.
- Use toBeVisible() instead of toBeInTheDocument() for RTL assertions - Add try/catch guard for localStorage access in vitest setup - Reuse filterVisiblePages() to avoid duplicate filtering logic - Use to_have_count(0) for more precise E2E assertions - Add is_hidden field to proto compatibility test - Fix E2E tests to use stTopNavLink instead of non-existent stTopNav
When pages are in sections, TopNav renders them inside section dropdowns (stTopNavSection) rather than as individual links (stTopNavLink). Updated test to check for the section dropdown correctly.
|
@cursor review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.
- Fix sidebar visibility check to use shouldShowNavigation() instead of appPages.length > 1, ensuring hidden pages are excluded from the count - Use StreamlitValueError for invalid visibility parameter validation - Move E2E tests to multipage_apps_v2 directory - Add explicit test that sidebar doesn't mount when only 1 visible page
StreamlitValueError takes (parameter, valid_values) not a custom message.
st.Pagest.Page
Consolidated Code Review: Add page visibility parameter to
|
- Add public read-only `visibility` property to StreamlitPage for consistency with existing public properties (title, icon, url_path) - Add typing tests in navigation_types.py to lock down the Literal["visible", "hidden"] type contract at type-check time - Update unit tests to use the public property instead of private attr
- Simplify redundant conditional check in Navigation utils - Remove redundant test_visibility_can_be_set_to_visible test - Consolidate filterVisiblePages tests using it.each
sfc-gh-bnisco
left a comment
There was a problem hiding this comment.
One question inline, but LGTM after
|
|
||
| Object.entries(navSections).forEach(([header, pages]) => { | ||
| // Only include non-empty section headers | ||
| if (header && header !== "" && header !== "undefined") { |
There was a problem hiding this comment.
question: Are we sure this logic for "undefined" isn't needed?
If the logic only cared about truthiness, then this code would just read if (header). It seems like a code smell that the previous logic checked against the string "undefined", and it makes me wonder if it was intentional.
There was a problem hiding this comment.
I looked into this again and also asked Codex and Claude. I believe this was accidentally added, since there isn't any hint for "undefined" as a literal value in the codebase
Codex Response
You were right to question it. I investigated the full path and the current if (header !== "") is the correct logic.
- The old check (
header && header !== "" && header !== "undefined") came from older TopNav code that grouped by rawsectionHeader, which could create a literal"undefined"key whensectionHeaderwas missing. - That old flow was refactored. Now missing headers are normalized earlier in
groupPagesBySectionviapage.sectionHeader || "", so missing values become""(not"undefined"). - Backend/protobuf also treats
section_headeras a string field (default empty string), so runtime missing values should map to empty string semantics. - Keeping
header !== "undefined"would actually hide a valid user-defined section literally named"undefined".
I added explicit tests and a clarifying comment:
frontend/app/src/components/Navigation/utils.ts:81frontend/app/src/components/Navigation/utils.test.ts:277frontend/app/src/components/Navigation/utils.test.ts:290frontend/app/src/components/Navigation/utils.test.ts:410
Validation I ran:
cd frontend/app && yarn test src/components/Navigation/utils.test.ts(pass, 29 tests)cd frontend/app && yarn test src/components/Navigation/TopNav.test.tsx(pass, 4 tests)cd frontend/app && yarn test src/components/Navigation/SidebarNav.test.tsx(fails in this environment due existing localStorage test harness issue:window.localStorage.clear is not a function, not related to this guard change)
So for this specific review comment: yes, the simplification is safe, and the new tests lock that behavior in.
Claude Response
Investigation Summary
What Changed
The condition was simplified from:
if (header && header !== "" && header !== "undefined") {to:
if (header !== "") {Why the "undefined" Check Was Unnecessary
-
The data flow prevents it: In
groupPagesBySection(line 44), the code uses:groupBy(appPages, page => page.sectionHeader || "")
This normalizes
undefined,null, and""all to the empty string""- not the literal string"undefined". -
Lodash's
groupBydoesn't do string coercion: It uses the callback's return value directly. The|| ""fallback handles theundefinedcase, so the literal string"undefined"can only appear if a user explicitly names their section that way. -
Tests explicitly verify this behavior (lines 277-310 in
utils.test.ts):normalizes missing section headers to the empty section key- confirmsundefined/nullbecome"", not"undefined"preserves a literal "undefined" section name- confirms a user-provided"undefined"section should be valid
Why the Original Code Existed
Looking at the git history:
- Commit
029b3e4e3dby @nico-bellante introduced the check in bothhasNonEmptySectionsandprocessNavigationStructure - Commit
e9379ccdb5(also by Nico) removed it fromhasNonEmptySectionsbut missedprocessNavigationStructure - This was likely defensive coding or a misunderstanding that was partially corrected later
The Change is Safe
The simplified check is actually more correct because:
- It no longer filters out user-defined sections named
"undefined" - The existing test
keeps sections named "undefined" when mixed with empty sections(line 410-436) now correctly verifies this behavior
The previous code was actually a bug - it would have incorrectly hidden any section a user deliberately named "undefined".
There was a problem hiding this comment.
Simplified it to if (header)
Address PR review comment about the "undefined" string check: - Simplify `if (header !== "")` to `if (header)` for idiomatic JS - Add tests verifying that JS undefined/null normalize to "" key - Add tests verifying literal "undefined" section name is preserved - Add clarifying comment explaining the behavior
## Describe your changes Implements the `visibility` parameter for `st.Page` to allow pages to be hidden from navigation menus while remaining accessible via direct URL, `st.page_link`, and `st.switch_page`. This addresses issues #10738 and #9195. - [Product spec](https://github.com/streamlit/streamlit/blob/5d331ef2a46f5f2053f909ac73f974f31ab3e10b/specs/0009-page-visibility/product-spec.md) - [Tech spec](https://github.com/streamlit/streamlit/blob/5d331ef2a46f5f2053f909ac73f974f31ab3e10b/specs/0009-page-visibility/tech-spec.md) **Key changes:** - Backend: Added `visibility` parameter with validation (visible/hidden) - Frontend: Filtering of hidden pages in SidebarNav and TopNav components - Tests: Comprehensive E2E and unit tests for the new feature - Code improvements: Simplified `shouldShowNavigation` logic and removed unused parameters ## Testing Plan - Unit Tests: 26 Navigation utility tests + TopNav hidden page tests + SidebarNav section tests pass - E2E Tests: 4 new E2E tests cover st.switch_page, sections with all hidden pages, and both nav positions - All existing tests pass: 66 Python tests + 160 frontend tests - All checks pass: format, lint, type checking The feature is fully backward compatible - `visibility` defaults to "visible" maintaining existing behavior.
Describe your changes
Implements the
visibilityparameter forst.Pageto allow pages to be hidden from navigation menus while remaining accessible via direct URL,st.page_link, andst.switch_page. This addresses issues #10738 and #9195.Key changes:
visibilityparameter with validation (visible/hidden)shouldShowNavigationlogic and removed unused parametersGithub Issues
Testing Plan
The feature is fully backward compatible -
visibilitydefaults to "visible" maintaining existing behavior.