├── .gitignore ├── .python-version ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── pyproject.toml ├── requirements-dev.lock ├── requirements.lock └── streamlit_superapp ├── __init__.py ├── components.py ├── index.py ├── navigation.py ├── page.py ├── page_loader.py ├── state.py ├── typing.py ├── web ├── breadcrumbs │ ├── .env │ ├── build │ │ ├── SourceSansPro-Regular.woff2 │ │ ├── asset-manifest.json │ │ ├── index.html │ │ └── static │ │ │ └── js │ │ │ ├── main.b9b1312b.js │ │ │ ├── main.b9b1312b.js.LICENSE.txt │ │ │ └── main.b9b1312b.js.map │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── SourceSansPro-Regular.woff2 │ │ └── index.html │ ├── src │ │ ├── Breadcrumbs.tsx │ │ ├── index.tsx │ │ └── react-app-env.d.ts │ └── tsconfig.json └── page_index │ ├── .env │ ├── build │ ├── SourceSansPro-Regular.woff2 │ ├── asset-manifest.json │ ├── index.html │ └── static │ │ └── js │ │ ├── main.cdd3c30b.js │ │ ├── main.cdd3c30b.js.LICENSE.txt │ │ └── main.cdd3c30b.js.map │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── SourceSansPro-Regular.woff2 │ └── index.html │ ├── src │ ├── Card.tsx │ ├── Grid.tsx │ ├── index.tsx │ └── react-app-env.d.ts │ └── tsconfig.json └── widgets.py /.gitignore: -------------------------------------------------------------------------------- 1 | # python generated files 2 | __pycache__/ 3 | *.py[oc] 4 | build/ 5 | dist/ 6 | wheels/ 7 | *.egg-info 8 | 9 | # venv 10 | .venv 11 | 12 | 13 | # node 14 | node_modules/ 15 | 16 | # front-end 17 | !streamlit_superapp/web/**/build/ 18 | pages 19 | app.py -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.9.18 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Python: Remote Attach", 9 | "type": "python", 10 | "request": "attach", 11 | "connect": { 12 | "host": "localhost", 13 | "port": 5678 14 | }, 15 | "pathMappings": [ 16 | { 17 | "localRoot": "${workspaceFolder}", 18 | "remoteRoot": "." 19 | } 20 | ], 21 | "justMyCode": true 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2023] [Wilian Silva] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Streamlit Super App 2 | 3 | Enhance your Streamlit app development experience with **Streamlit Super App**! This package provides features that streamline the creation and management of multipage apps and offers improved state management for widgets. 4 | 5 | ## Features 6 | 7 | - **Multipage/Tree Router**: Automatically generate multi-page routing based on folder structure. 8 | - **Persistent State Management**: Seamlessly manage the state of widgets across different pages. 9 | 10 | ## Installation 11 | 12 | Install Streamlit Super App using pip: 13 | 14 | ```sh 15 | pip install streamlit-superapp 16 | ``` 17 | 18 | ## Getting Started 19 | 20 | ### Multipage Routing 21 | 22 | Create a `pages` folder in the root directory of your Streamlit app and organize your pages as shown below: 23 | 24 | ``` 25 | pages/ 26 | ├─ __init__.py 27 | └─ hello/__init__.py 28 | ``` 29 | 30 | - **You can go beyond that, create as many levels as you want!** 31 | 32 | For instance, `pages/hello/__init__.py` can be: 33 | 34 | ```python 35 | import streamlit as st 36 | 37 | NAME = "Demo" 38 | DESCRIPTION = "Sample page to demonstrate Streamlit Super App." 39 | ICON = "🌍" 40 | 41 | def main(): 42 | st.write("Hello World!") 43 | ``` 44 | 45 | In your main file, call streamlit_superapp's "run" function 46 | 47 | ```python 48 | import streamlit_superapp as app 49 | 50 | app.run() 51 | ``` 52 | 53 | ### Managing DataFrame State and Editing 54 | 55 | Easily edit and manage the state of DataFrames. 56 | 57 | ```python 58 | import pandas as pd 59 | import streamlit as st 60 | from streamlit_superapp.state import State 61 | 62 | ICON = "📊" 63 | 64 | def main(): 65 | state = State("df", default_value=get_base_input()) 66 | 67 | 68 | with st.sidebar: 69 | if st.button("✖️ Multiply"): 70 | # The "state.value" is always the most updated value 71 | # So we can manipulate it before rendering it again 72 | state.initial_value = calculate(state.value) 73 | 74 | if st.button("🔄 Reset"): 75 | # Forcing a value update before rendering 76 | state.initial_value = get_base_input() 77 | 78 | # setting the "initial_value" and "key" is mandatory 79 | df = st.data_editor(data=state.initial_value, key=state.key, hide_index=True) 80 | 81 | # binding the new value to the state is mandatory! 82 | # plus you get the previous value for free! 83 | previous_df = state.bind(df) 84 | 85 | if not df.equals(previous_df): 86 | st.success("Data changed!") 87 | 88 | 89 | def get_base_input(): 90 | return pd.DataFrame(index=[0, 1, 2], columns=["a", "b"], dtype=float) 91 | 92 | 93 | def calculate(df: pd.DataFrame): 94 | df["result"] = df["a"] * df["b"] 95 | 96 | return df 97 | 98 | ``` 99 | 100 | ### Basic Counter 101 | 102 | Create counters with persistent state. 103 | 104 | ```python 105 | import streamlit as st 106 | from streamlit_superapp import State 107 | 108 | NAME = "Counter" 109 | TAG = "{A:}📚 Studies" # This page will appear in a group "📚 Studies" at the top of a index page 110 | ICON = "🔢" 111 | 112 | def main(page): 113 | counter = State("counter", default_value=0, key=page) 114 | 115 | if st.button("Increment"): 116 | # This is the same as binding a new value 117 | counter.value += 1 118 | 119 | # Initial value only updates after changing pages 120 | # or if we update it manually 121 | st.write(f"initial_value:" {counter.initial_value}) 122 | st.write(f"current value: {counter.value}") 123 | ``` 124 | 125 | ### Shared State Across Pages 126 | 127 | Maintain the state of TextInput across different pages. 128 | 129 | ```python 130 | import streamlit as st 131 | from streamlit_superapp import State 132 | 133 | NAME = "Persistent Text Input" 134 | 135 | def main(): 136 | 137 | # You can access the state "text" on another page too! 138 | state = State("text", default_value="Wilian") 139 | 140 | text = st.text_input("Your Name", value=state.initial_value, key=state.key) 141 | 142 | previous_text = state.bind(text) 143 | 144 | if text != previous_text: 145 | st.success("Input changed") 146 | 147 | st.success(f"Hello {text}!") 148 | ``` 149 | 150 | ### Page Private State 151 | 152 | Create a persistent TextInput that is private to a page. 153 | 154 | ```python 155 | from streamlit_superapp import State, Page 156 | import streamlit as st 157 | 158 | NAME = "Page Only State" 159 | 160 | # Super app will provide the current page to your function 161 | def main(page: Page): 162 | 163 | # Providing the state with the page as key will make it private 164 | # Even tho it has the same "text" key state 165 | state = State("text", default_value="", key=page) 166 | 167 | value = st.text_input("This is not shared between pages", value=state.initial_value) 168 | 169 | previous_value = state.bind(value) 170 | 171 | st.write(value) 172 | ``` 173 | 174 | ## Contributing 175 | 176 | We welcome contributions to Streamlit Super App! Please feel free to open issues or submit pull requests. 177 | 178 | ## License 179 | 180 | Streamlit Super App is licensed under the [MIT License](LICENSE). 181 | 182 | --- 183 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "streamlit-superapp" 3 | version = "1.3.0" 4 | description = "" 5 | authors = [{ name = "Wilian", email = "wilianzilv@gmail.com" }] 6 | dependencies = [ 7 | ] 8 | readme = "README.md" 9 | requires-python = ">= 3.8" 10 | 11 | [project.urls] 12 | Repository = "https://github.com/WilianZilv/streamlit_superapp" 13 | 14 | [build-system] 15 | requires = ["hatchling"] 16 | build-backend = "hatchling.build" 17 | 18 | [tool.rye] 19 | managed = true 20 | dev-dependencies = [ 21 | "streamlit>=1.28.1", 22 | "ptvsd>=4.3.2", 23 | ] 24 | 25 | [tool.hatch.metadata] 26 | allow-direct-references = true 27 | -------------------------------------------------------------------------------- /requirements-dev.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | altair==5.1.2 11 | attrs==23.1.0 12 | blinker==1.7.0 13 | cachetools==5.3.2 14 | certifi==2023.7.22 15 | charset-normalizer==3.3.2 16 | click==8.1.7 17 | colorama==0.4.6 18 | gitdb==4.0.11 19 | gitpython==3.1.40 20 | idna==3.4 21 | importlib-metadata==6.8.0 22 | jinja2==3.1.2 23 | jsonschema==4.19.2 24 | jsonschema-specifications==2023.7.1 25 | markdown-it-py==3.0.0 26 | markupsafe==2.1.3 27 | mdurl==0.1.2 28 | numpy==1.26.1 29 | packaging==23.2 30 | pandas==2.1.2 31 | pillow==10.1.0 32 | protobuf==4.25.0 33 | ptvsd==4.3.2 34 | pyarrow==14.0.0 35 | pydeck==0.8.1b0 36 | pygments==2.16.1 37 | python-dateutil==2.8.2 38 | pytz==2023.3.post1 39 | referencing==0.30.2 40 | requests==2.31.0 41 | rich==13.6.0 42 | rpds-py==0.12.0 43 | six==1.16.0 44 | smmap==5.0.1 45 | streamlit==1.28.1 46 | tenacity==8.2.3 47 | toml==0.10.2 48 | toolz==0.12.0 49 | tornado==6.3.3 50 | typing-extensions==4.8.0 51 | tzdata==2023.3 52 | tzlocal==5.2 53 | urllib3==2.0.7 54 | validators==0.22.0 55 | watchdog==3.0.0 56 | zipp==3.17.0 57 | -------------------------------------------------------------------------------- /requirements.lock: -------------------------------------------------------------------------------- 1 | # generated by rye 2 | # use `rye lock` or `rye sync` to update this lockfile 3 | # 4 | # last locked with the following flags: 5 | # pre: false 6 | # features: [] 7 | # all-features: false 8 | 9 | -e file:. 10 | -------------------------------------------------------------------------------- /streamlit_superapp/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | import streamlit as st 3 | except ModuleNotFoundError: 4 | raise ModuleNotFoundError( 5 | "Streamlit is not installed. Please install it with `pip install streamlit`." 6 | ) 7 | 8 | try: 9 | st.query_params 10 | except AttributeError: 11 | raise AttributeError( 12 | "Streamlit version is too old. Please upgrade it with `pip install streamlit --upgrade`." 13 | ) 14 | 15 | try: 16 | st.session_state 17 | except AttributeError: 18 | raise AttributeError( 19 | "Streamlit version is too old. Please upgrade it with `pip install streamlit --upgrade`." 20 | ) 21 | 22 | 23 | from streamlit_superapp.navigation import Navigation 24 | from streamlit_superapp.page_loader import PageLoader 25 | from streamlit_superapp.state import State 26 | from streamlit_superapp.page import Page 27 | from streamlit_superapp.widgets import * 28 | 29 | PageLoader.initialize() 30 | 31 | inject = Navigation.inject 32 | 33 | 34 | def run( 35 | hide_index_description: bool = False, 36 | hide_home_button: bool = False, 37 | hide_back_button: bool = False, 38 | hide_page_title: bool = False, 39 | hide_breadcrumbs: bool = False, 40 | use_query_params: bool = True, 41 | ): 42 | Navigation.hide_index_description = hide_index_description 43 | Navigation.hide_home_button = hide_home_button 44 | Navigation.hide_back_button = hide_back_button 45 | Navigation.hide_page_title = hide_page_title 46 | Navigation.hide_breadcrumbs = hide_breadcrumbs 47 | Navigation.use_query_params = use_query_params 48 | 49 | Navigation.initialize() 50 | -------------------------------------------------------------------------------- /streamlit_superapp/components.py: -------------------------------------------------------------------------------- 1 | import streamlit as st 2 | from streamlit import session_state as ss 3 | from streamlit_superapp.state import State 4 | 5 | from streamlit_superapp.typing import Navigation, Page 6 | 7 | 8 | import os 9 | from typing import List, Literal 10 | import streamlit.components.v1 as components 11 | 12 | 13 | def small_link(path: str, name: str, use_container_width=False): 14 | if st.button( 15 | name, key="link:" + path + ":" + name, use_container_width=use_container_width 16 | ): 17 | ss["navigation"].go(path) 18 | 19 | 20 | def go_home_link(): 21 | navigation: Navigation = ss["navigation"] 22 | 23 | root = navigation.root() 24 | 25 | small_link( 26 | path=root.path, 27 | name="🏠 Home", 28 | use_container_width=True, 29 | ) 30 | 31 | 32 | def go_back_link(): 33 | navigation: Navigation = ss["navigation"] 34 | 35 | small_link( 36 | path=navigation.previous_path(), name="← Go back", use_container_width=True 37 | ) 38 | 39 | 40 | def sidebar(page: Page, variant: Literal["selectbox", "radio"] = "radio", label=None): 41 | parent = page.parent 42 | 43 | if not parent: 44 | return 45 | 46 | pages = parent.children 47 | 48 | if len(pages) == 1: 49 | return 50 | 51 | text = [page.icon + " " + page.name for page in pages] 52 | paths = [page.path for page in pages] 53 | 54 | index = paths.index(page.path) 55 | 56 | def format_func(path: str): 57 | return text[paths.index(path)] 58 | 59 | value = None 60 | 61 | state = State("page_index", default_value=index, key=parent) 62 | 63 | if state.initial_value != index: 64 | state.initial_value = index 65 | 66 | label = label or parent.name 67 | 68 | if variant == "selectbox": 69 | value = st.sidebar.selectbox( 70 | label, 71 | index=state.initial_value, 72 | options=paths, 73 | format_func=format_func, 74 | key=state.key + ":selectbox", 75 | ) 76 | 77 | if variant == "radio": 78 | value = st.sidebar.radio( 79 | label, 80 | index=state.initial_value, 81 | options=paths, 82 | format_func=format_func, 83 | key=state.key + ":radio", 84 | ) 85 | 86 | if value is None: 87 | return 88 | 89 | if value != page.path: 90 | state.initial_value = paths.index(value) 91 | navigation: Navigation = ss["navigation"] 92 | navigation.go(value) 93 | 94 | 95 | def search(page): 96 | sidebar(page, variant="selectbox", label="Search") 97 | 98 | 99 | _RELEASE = True 100 | 101 | 102 | def declare_component(name: str): 103 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 104 | 105 | if not _RELEASE: 106 | with open(os.path.join(parent_dir, "web", name, ".env"), "r") as f: 107 | _env = f.readlines() 108 | port = [line for line in _env if line.startswith("PORT=")][0].split("=")[1] 109 | 110 | return components.declare_component( 111 | name, 112 | url=f"http://localhost:{port}", 113 | ) 114 | 115 | build_dir = os.path.join(parent_dir, f"web/{name}/build") 116 | return components.declare_component(name, path=build_dir) 117 | 118 | 119 | def page_index(pages: List[Page], key=None): 120 | _component_func = declare_component("page_index") 121 | 122 | _pages = [page.serializable_dict() for page in pages] 123 | return _component_func(pages=_pages, key=key) 124 | 125 | 126 | def breadcrumbs(current_path: str): 127 | _component_func = declare_component("breadcrumbs") 128 | 129 | navigation: Navigation = ss["navigation"] 130 | 131 | current_path = navigation.current_path() 132 | 133 | current_page = navigation.find_page(current_path) 134 | 135 | if current_page is None: 136 | return 137 | 138 | ancestors = [] 139 | 140 | while True: 141 | if current_page is None: 142 | break 143 | 144 | ancestors = [current_page, *ancestors] 145 | 146 | current_page = current_page.parent 147 | 148 | pages = [page.serializable_dict() for page in ancestors] 149 | 150 | k = "navigation:breadcrumbs:path" 151 | 152 | previous_value = ss.get(k, None) 153 | 154 | next_value = _component_func(pages=pages, current_path=current_path, default=None) 155 | 156 | ss[k] = next_value 157 | 158 | if previous_value == next_value: 159 | return 160 | 161 | if next_value is not None: 162 | navigation.go(next_value) 163 | st.rerun() 164 | -------------------------------------------------------------------------------- /streamlit_superapp/index.py: -------------------------------------------------------------------------------- 1 | from streamlit_superapp.components import page_index 2 | from streamlit_superapp.typing import Navigation, Page 3 | import streamlit as st 4 | from streamlit import session_state as ss 5 | 6 | 7 | class Index: 8 | @staticmethod 9 | def main(page: Page): 10 | navigation: Navigation = ss["navigation"] 11 | 12 | pages = page.children 13 | 14 | if page.description and not navigation.hide_index_description: 15 | st.write(page.description) 16 | 17 | path = page_index(pages) 18 | 19 | if path is not None: 20 | navigation.go(path) 21 | -------------------------------------------------------------------------------- /streamlit_superapp/navigation.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Optional, Union, cast 2 | from streamlit import session_state as ss 3 | import streamlit as st 4 | 5 | import inspect 6 | from streamlit_superapp import components 7 | from streamlit_superapp.page_loader import PageLoader 8 | from streamlit_superapp.state import State 9 | from streamlit_superapp.typing import Page 10 | 11 | 12 | class Navigation: 13 | hide_page_title = False 14 | hide_home_button = False 15 | hide_back_button = False 16 | hide_index_description = False 17 | hide_breadcrumbs = False 18 | use_query_params = True 19 | 20 | @staticmethod 21 | def initialize(): 22 | if "session_id" not in ss: 23 | ss.session_id = "global_session" 24 | 25 | ss["navigation"] = Navigation 26 | 27 | PageLoader.initialize() 28 | 29 | path = Navigation.current_path() 30 | 31 | page = Navigation.find_page(path) 32 | 33 | if page is None: 34 | page = Navigation.root() 35 | path = page.path 36 | 37 | if page.index is not None: 38 | if not page.index: 39 | children = page.children 40 | if len(children): 41 | page = children[0] 42 | path = page.path 43 | 44 | result = handle_redirect(page) 45 | 46 | if isinstance(result, tuple): 47 | page, path = result 48 | 49 | if page is None: 50 | not_found(path) 51 | st.stop() 52 | raise Exception("Streamlit Super App not configured.") 53 | 54 | if page.access is not None: 55 | params = Navigation.discover_params(page.access, page) 56 | if not page.access(**params): 57 | page = Navigation.root() 58 | path = page.path 59 | 60 | if page.access is None: 61 | guard_page = page.parent 62 | 63 | while True: 64 | if guard_page is None: 65 | break 66 | 67 | if guard_page.access is not None: 68 | params = Navigation.discover_params(guard_page.access, guard_page) 69 | if not guard_page.access(**params): 70 | page = Navigation.root() 71 | path = page.path 72 | 73 | break 74 | guard_page = guard_page.parent 75 | 76 | Navigation.go(path) 77 | 78 | parent = page.parent 79 | 80 | if parent is not None: 81 | with st.sidebar: 82 | if not Navigation.hide_home_button or not Navigation.hide_back_button: 83 | c1, c2 = st.columns(2) 84 | 85 | if not Navigation.hide_home_button: 86 | with c1: 87 | components.go_home_link() 88 | 89 | if not Navigation.hide_back_button: 90 | with c2: 91 | components.go_back_link() 92 | 93 | if parent.search: 94 | components.search(page) 95 | 96 | if parent.sidebar is not None: 97 | components.sidebar(page, variant=parent.sidebar) 98 | 99 | if not Navigation.hide_breadcrumbs: 100 | components.breadcrumbs(Navigation.current_path()) 101 | 102 | if "do_rerun" not in ss: 103 | ss.do_rerun = False 104 | 105 | if not ss.do_rerun: 106 | Navigation.render_page(page) 107 | 108 | if ss.do_rerun: 109 | ss.do_rerun = False 110 | st.rerun() 111 | 112 | @staticmethod 113 | def pages(verify_access=True) -> List["Page"]: 114 | pages: List[Page] = ss.pages 115 | 116 | if not verify_access: 117 | return pages 118 | 119 | _pages: List[Page] = [] 120 | 121 | for page in pages: 122 | if page.access is True: 123 | continue 124 | 125 | if callable(page.access): 126 | params = Navigation.discover_params(page.access, page) 127 | 128 | if not page.access(**params): 129 | continue 130 | 131 | _pages.append(page) 132 | 133 | return _pages 134 | 135 | @staticmethod 136 | def previous_path(path: Optional[str] = None): 137 | current_path = path 138 | if current_path is None: 139 | current_path = Navigation.current_path() 140 | 141 | if "." not in current_path: 142 | return current_path 143 | 144 | tree = current_path.split(".") 145 | path = ".".join(tree[:-1]) 146 | 147 | page = Navigation.find_page(path) 148 | 149 | if page is None: 150 | return current_path 151 | 152 | if page.index is not None: 153 | if not page.index: 154 | return Navigation.previous_path(page.path) 155 | 156 | return path 157 | 158 | @staticmethod 159 | def go(path: Union[str, Page]): 160 | page = cast(Page, path) 161 | 162 | if isinstance(path, str): 163 | page = Navigation.find_page(path) 164 | if page is None: 165 | page = Navigation.root() 166 | 167 | if not isinstance(path, str): 168 | path = path.path 169 | 170 | previous_path = Navigation.current_path(path) 171 | 172 | ss["navigation:previous_path"] = previous_path 173 | 174 | page_changed = previous_path != path 175 | 176 | if Navigation.use_query_params: 177 | st.query_params['path'] = path 178 | else: 179 | path_state = State("navigation:path", default_value=path) 180 | path_state.initial_value = path 181 | 182 | page_state = State("navigation:current_page", default_value=page) 183 | page_state.initial_value = page 184 | 185 | if page_changed: 186 | State.save_all() 187 | # print("go:", previous_path, "->", path) 188 | ss["do_rerun"] = True 189 | 190 | @staticmethod 191 | def current_path(default: str = PageLoader.root): 192 | if Navigation.use_query_params: 193 | return st.query_params.get("path", default) 194 | 195 | path_state = State("navigation:path", default_value=default) 196 | 197 | return path_state.initial_value 198 | 199 | @staticmethod 200 | def current_page(): 201 | page_state = State[Page]("navigation:current_page", default_value=None) 202 | 203 | return page_state.initial_value 204 | 205 | @staticmethod 206 | def find_page(path: str): 207 | if "pages" not in ss: 208 | PageLoader.initialize() 209 | 210 | pages = Navigation.pages(verify_access=False) 211 | 212 | for page in pages: 213 | if page.path == path: 214 | return page 215 | 216 | @staticmethod 217 | def root(): 218 | root = Navigation.find_page(PageLoader.root) 219 | if root is None: 220 | not_configured() 221 | st.stop() 222 | raise Exception("Streamlit Super App not configured.") 223 | 224 | return root 225 | 226 | @staticmethod 227 | def inject(**kwargs): 228 | if "page" in kwargs or "navigation" in kwargs: 229 | raise Exception("Cannot inject 'page' or 'navigation'.") 230 | 231 | previous = ss.get("navigation:inject", None) 232 | 233 | ss["navigation:inject"] = kwargs 234 | 235 | if previous != kwargs: 236 | st.rerun() 237 | 238 | @staticmethod 239 | def discover_params(func: Callable, page: Page): 240 | signature = inspect.signature(func).parameters 241 | 242 | params = {} 243 | 244 | if "page" in signature: 245 | params["page"] = page 246 | 247 | if "navigation" in signature: 248 | params["navigation"] = Navigation 249 | 250 | if "navigation:inject" in ss: 251 | for key, value in ss["navigation:inject"].items(): 252 | if key in signature: 253 | params[key] = value 254 | 255 | for key, value in signature.items(): 256 | if key not in params: 257 | params[key] = None 258 | 259 | return params 260 | 261 | @staticmethod 262 | def render_page(page: Page): 263 | params = Navigation.discover_params(page.main, page) 264 | 265 | if not Navigation.hide_page_title: 266 | st.header(page.icon + " " + page.name) 267 | 268 | return page.main(**params) 269 | 270 | 271 | def handle_redirect(page: Page): 272 | if page.redirect is None: 273 | return 274 | 275 | if isinstance(page.redirect, tuple): 276 | func, path = page.redirect 277 | 278 | params = Navigation.discover_params(func, page) 279 | valid = func(**params) 280 | 281 | if valid: 282 | return Navigation.find_page(path), path 283 | 284 | func = page.redirect 285 | 286 | if callable(func): 287 | params = Navigation.discover_params(func, page) 288 | path = func(**params) 289 | 290 | if isinstance(path, str): 291 | return Navigation.find_page(path), path 292 | 293 | 294 | def not_configured(): 295 | st.write("Streamlit Super App needs to be configured.") 296 | 297 | st.write( 298 | "Please create a `pages` folder in the root directory of your Streamlit app." 299 | ) 300 | 301 | st.code( 302 | """ 303 | pages/ 304 | ├─ __init__.py 305 | └─ hello/__init__.py 306 | """ 307 | ) 308 | 309 | st.write("add this to") 310 | st.code("pages/hello/__init__.py") 311 | 312 | st.code( 313 | """ 314 | import streamlit as st 315 | 316 | NAME = "Demo" # Optional 317 | DESCRIPTION = "Sample page to demonstrate Streamlit Super App." # Optional 318 | ICON = "🌍" # Optional 319 | 320 | # main function is required 321 | def main(): 322 | st.write("Hello World!") 323 | 324 | """ 325 | ) 326 | 327 | 328 | def not_found(path: str): 329 | st.write("Page not found.") 330 | 331 | st.write(f"Path: `{path}`") 332 | -------------------------------------------------------------------------------- /streamlit_superapp/page.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from typing import Callable, List, Literal, Optional, Tuple, Union, cast 4 | from streamlit import session_state as ss 5 | 6 | from streamlit_superapp.typing import Navigation 7 | 8 | 9 | @dataclass 10 | class Page: 11 | path: str 12 | main: Callable 13 | name: str 14 | icon: str 15 | description: Optional[str] = None 16 | tag: Optional[str] = None 17 | order: Optional[str] = None 18 | sidebar: Optional[Literal["selectbox", "radio"]] = None 19 | index: Optional[bool] = None 20 | search: Optional[bool] = None 21 | hidden: bool = False 22 | access: Optional[Callable] = None 23 | redirect: Optional[Union[Callable, Tuple[Callable, str]]] = None 24 | 25 | def serializable_dict(self): 26 | return { 27 | "path": self.path, 28 | "name": self.name, 29 | "icon": self.icon, 30 | "description": self.description, 31 | "tag": self.tag, 32 | "order": self.order, 33 | "index": self.index, 34 | "hidden": self.hidden, 35 | } 36 | 37 | @property 38 | def is_active(self): 39 | navigation: Navigation = ss["navigation"] 40 | return navigation.current_path() == self.path 41 | 42 | @property 43 | def parent(self): 44 | navigation: Navigation = ss["navigation"] 45 | 46 | parent_path = ".".join(self.path.split(".")[:-1]) 47 | return navigation.find_page(parent_path) 48 | 49 | @property 50 | def children(self): 51 | navigation: Navigation = ss["navigation"] 52 | 53 | pages = navigation.pages() 54 | target = self.path + "." 55 | 56 | def is_child(page: Page): 57 | if page.path == self.path: 58 | return False 59 | 60 | if not page.path.startswith(target): 61 | return False 62 | 63 | return "." not in page.path[len(target) :] 64 | 65 | return [page for page in pages if is_child(cast(Page, page))] 66 | 67 | @property 68 | def neighbors(self) -> List["Page"]: 69 | parent = self.parent 70 | 71 | if parent is None: 72 | return [] 73 | 74 | return cast( 75 | List[Page], [page for page in parent.children if page.path != self.path] 76 | ) 77 | 78 | def __str__(self) -> str: 79 | return self.name or self.path 80 | -------------------------------------------------------------------------------- /streamlit_superapp/page_loader.py: -------------------------------------------------------------------------------- 1 | import glob 2 | from importlib import import_module 3 | import os 4 | import sys 5 | import time 6 | from typing import List 7 | from streamlit_superapp.index import Index 8 | 9 | 10 | from streamlit_superapp.page import Page 11 | from streamlit import session_state as ss 12 | 13 | last_page_update = -1 14 | 15 | 16 | class PageLoader: 17 | root = "pages" 18 | 19 | @staticmethod 20 | def initialize(): 21 | now = time.time() 22 | 23 | global last_page_update 24 | 25 | _pages = ss.get("pages", []) 26 | 27 | if len(_pages): 28 | if now - last_page_update < 1: 29 | return 30 | 31 | paths = ss.get("page_loader_paths", None) 32 | 33 | if paths is None: 34 | paths = glob.glob(f"./{PageLoader.root}/**/*.py", recursive=True) 35 | ss.page_loader_paths = paths 36 | 37 | pages: List[Page] = [] 38 | 39 | if "page_loader" not in ss: 40 | ss.page_loader = {} 41 | 42 | for path in paths: 43 | module_path = path[2:].replace(os.path.sep, ".").replace(".py", "") 44 | page_path = module_path.split(".") 45 | 46 | is__init__file = page_path[-1] == "__init__" 47 | 48 | if is__init__file: 49 | page_path = page_path[:-1] 50 | 51 | file_name = page_path[-1] 52 | page_path = ".".join(page_path) 53 | 54 | try: 55 | file_mtime = os.path.getmtime(path) 56 | except Exception as _: 57 | ss["page_loader_paths"] = None 58 | return PageLoader.initialize() 59 | 60 | last_mtime = ss.page_loader.get(page_path, 0) 61 | 62 | ss.page_loader[page_path] = file_mtime 63 | 64 | if module_path in sys.modules and file_mtime != last_mtime: 65 | del sys.modules[module_path] 66 | 67 | module = import_module(module_path) 68 | 69 | main = get_module_attr(module, "main") 70 | name = get_module_attr(module, "NAME", None) 71 | description = get_module_attr(module, "DESCRIPTION", None) 72 | tag = get_module_attr(module, "TAG", None) 73 | icon = get_module_attr(module, "ICON", None) 74 | order = get_module_attr(module, "ORDER", None) 75 | file_name_normalized = file_name.replace("_", " ").title() 76 | sidebar = get_module_attr(module, "SIDEBAR", None) 77 | index = get_module_attr(module, "INDEX", None) 78 | search = get_module_attr(module, "SEARCH", None) 79 | hidden = get_module_attr(module, "HIDDEN", False) 80 | access = get_module_attr(module, "ACCESS", None) 81 | redirect = get_module_attr(module, "REDIRECT", None) 82 | 83 | if access is not None: 84 | if not callable(access): 85 | raise ValueError(f"ACCESS must be a function, not {type(access)}.") 86 | 87 | if isinstance(order, int): 88 | order = str(order) 89 | 90 | if main is None: 91 | main = Index.main 92 | name = name or file_name_normalized 93 | icon = icon or "📖" 94 | 95 | search = search or False 96 | 97 | if index is False: 98 | sidebar = sidebar or "radio" 99 | 100 | if index is None: 101 | index = True 102 | 103 | if main is None: 104 | continue 105 | 106 | page = Page( 107 | path=page_path, 108 | main=main, 109 | name=name or file_name_normalized, 110 | description=description, 111 | tag=tag, 112 | icon=icon or "📄", 113 | order=order, 114 | sidebar=sidebar, 115 | index=index, 116 | search=search, 117 | hidden=hidden, 118 | access=access, 119 | redirect=redirect, 120 | ) 121 | pages.append(page) 122 | 123 | pages = sorted(pages, key=lambda page: page.order or page.name) 124 | 125 | ss.pages = pages 126 | last_page_update = now 127 | 128 | 129 | def get_module_attr(module, attr, default=None): 130 | try: 131 | return object.__getattribute__(module, attr) 132 | except AttributeError: 133 | return default 134 | -------------------------------------------------------------------------------- /streamlit_superapp/state.py: -------------------------------------------------------------------------------- 1 | from typing import Any, Generic, Optional, TypeVar, Union, cast 2 | from uuid import uuid4 3 | from streamlit import session_state as ss 4 | 5 | from streamlit_superapp.typing import Page 6 | 7 | T = TypeVar("T") 8 | 9 | STATES_KEY = "streamlit_superapp:states" 10 | 11 | 12 | class Store: 13 | data = {} 14 | 15 | @staticmethod 16 | def get(key: str, default: Any = None) -> Any: 17 | return Store.data.get(ss.session_id, {}).get(key, default) 18 | 19 | @staticmethod 20 | def set(key: str, value: T) -> T: 21 | global data 22 | 23 | if ss.session_id not in Store.data: 24 | Store.data[ss.session_id] = {} 25 | Store.data[ss.session_id][key] = value 26 | ss[key] = value 27 | return value 28 | 29 | @staticmethod 30 | def restore(key: str, default_value: Optional[Any] = None): 31 | ss[key] = Store.get(key, default_value) 32 | return ss[key] 33 | 34 | 35 | class State(Generic[T]): 36 | name: str 37 | default_value: Optional[T] = None 38 | 39 | def __init__( 40 | self, 41 | name: str, 42 | default_value: Optional[T] = None, 43 | key: Optional[Union[Page, str]] = None, 44 | cache: bool = True, 45 | ): 46 | if key is not None: 47 | if not isinstance(key, str): 48 | key = key.path 49 | 50 | name = f"{key}:{name}" 51 | 52 | updated_name = f"updated:{name}" 53 | key_name = f"key:{name}" 54 | previous_name = f"previous:{name}" 55 | default_name = f"default:{name}" 56 | restored_name = f"restored:{name}" 57 | 58 | if STATES_KEY not in ss: 59 | ss[STATES_KEY] = {} 60 | 61 | ss[STATES_KEY][name] = self 62 | 63 | if default_value is None: 64 | self.default_value = Store.get(default_name, None) 65 | 66 | if default_value is not None: 67 | self.default_value = Store.set(default_name, default_value) 68 | 69 | if restored_name not in ss and cache: 70 | Store.restore(key_name, str(uuid4())) 71 | Store.restore(updated_name, default_value) 72 | ss[name] = ss[updated_name] 73 | ss[previous_name] = ss[updated_name] 74 | 75 | ss[restored_name] = True 76 | 77 | self.key = Store.get(key_name, str(uuid4())) 78 | Store.set(key_name, self.key) 79 | 80 | self.name = name 81 | self.updated_name = updated_name 82 | self.key_name = key_name 83 | self.previous_name = previous_name 84 | self.default_name = default_name 85 | self.restored_name = restored_name 86 | 87 | @staticmethod 88 | def save_all(): 89 | if STATES_KEY not in ss: 90 | return 91 | 92 | [state.save() for state in ss[STATES_KEY].values()] 93 | 94 | def save(self): 95 | Store.set(self.name, ss.get(self.updated_name, self.default_value)) 96 | 97 | @property 98 | def initial_value(self) -> T: 99 | return cast(T, ss.get(self.name, self.default_value)) 100 | 101 | @initial_value.setter 102 | def initial_value(self, value: T): 103 | Store.set(self.name, value) 104 | Store.set(self.updated_name, value) 105 | 106 | self.key = Store.set(self.key_name, str(uuid4())) 107 | 108 | @property 109 | def value(self) -> T: 110 | return cast(T, ss.get(self.updated_name, ss.get(self.name, self.default_value))) 111 | 112 | @value.setter 113 | def value(self, value: T): 114 | self.bind(value) 115 | 116 | @property 117 | def previous_value(self) -> T: 118 | return cast(T, ss.get(self.previous_name, self.default_value)) 119 | 120 | def bind(self, value: Optional[T]): 121 | previous_value = self.value 122 | 123 | Store.set(self.previous_name, previous_value) 124 | Store.set(self.updated_name, value) 125 | 126 | return previous_value 127 | -------------------------------------------------------------------------------- /streamlit_superapp/typing.py: -------------------------------------------------------------------------------- 1 | from typing import Callable, List, Literal, Optional, Protocol, Tuple, Union 2 | 3 | 4 | class Page(Protocol): 5 | path: str 6 | main: Callable 7 | name: str 8 | icon: str 9 | description: Optional[str] = None 10 | tag: Optional[str] = None 11 | order: Optional[str] = None 12 | sidebar: Optional[Literal["selectbox", "radio"]] = None 13 | index: Optional[bool] = None 14 | search: Optional[bool] = None 15 | hidden: bool = False 16 | access: Optional[Callable] = None 17 | redirect: Optional[Union[Callable, Tuple[Callable, str]]] = None 18 | 19 | def serializable_dict(self) -> dict: 20 | ... 21 | 22 | @property 23 | def is_active(self) -> bool: 24 | ... 25 | 26 | @property 27 | def parent(self) -> Optional["Page"]: 28 | ... 29 | 30 | @property 31 | def children(self) -> List["Page"]: 32 | ... 33 | 34 | @property 35 | def neighbors(self) -> List["Page"]: 36 | ... 37 | 38 | @property 39 | def nearest_gallery(self) -> Optional["Page"]: 40 | ... 41 | 42 | 43 | class Navigation(Protocol): 44 | hide_page_title: bool 45 | hide_index_description: bool 46 | hide_home_button: bool 47 | hide_back_button: bool 48 | hide_breadcrumbs: bool 49 | use_query_params: bool 50 | 51 | @staticmethod 52 | def previous_path() -> str: 53 | ... 54 | 55 | @staticmethod 56 | def find_page(path: str) -> Optional[Page]: 57 | ... 58 | 59 | @staticmethod 60 | def root() -> Page: 61 | ... 62 | 63 | @staticmethod 64 | def render_page(page: Page) -> None: 65 | ... 66 | 67 | @staticmethod 68 | def go(path: Union[str, Page]) -> None: 69 | ... 70 | 71 | @staticmethod 72 | def current_path(default: str = "pages") -> str: 73 | ... 74 | 75 | @staticmethod 76 | def pages() -> List[Page]: 77 | ... 78 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/.env: -------------------------------------------------------------------------------- 1 | # Run the component's dev server on :3001 2 | # (The Streamlit dev server already runs on :3000) 3 | PORT=3002 4 | 5 | # Don't automatically open the web browser on `npm run start`. 6 | BROWSER=none -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/build/SourceSansPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilianZilv/streamlit_superapp/da1fa5ed385a042fc10156bba83b66619bf58153/streamlit_superapp/web/breadcrumbs/build/SourceSansPro-Regular.woff2 -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "./static/js/main.b9b1312b.js", 4 | "index.html": "./index.html", 5 | "main.b9b1312b.js.map": "./static/js/main.b9b1312b.js.map" 6 | }, 7 | "entrypoints": [ 8 | "static/js/main.b9b1312b.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/build/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/build/static/js/main.b9b1312b.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.19.1 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-is.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v16.14.0 26 | * react-dom.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v16.14.0 35 | * react-jsx-runtime.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** @license React v16.14.0 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_component", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "streamlit-component-lib": "^2.0.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "homepage": ".", 32 | "devDependencies": { 33 | "@types/node": "^12.0.0", 34 | "@types/react": "^16.9.0", 35 | "@types/react-dom": "^16.9.0", 36 | "react-scripts": "^5.0.1", 37 | "typescript": "^4.2.0" 38 | } 39 | } -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/public/SourceSansPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilianZilv/streamlit_superapp/da1fa5ed385a042fc10156bba83b66619bf58153/streamlit_superapp/web/breadcrumbs/public/SourceSansPro-Regular.woff2 -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/public/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/src/Breadcrumbs.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | StreamlitComponentBase, 4 | withStreamlitConnection, 5 | } from "streamlit-component-lib"; 6 | import React, { ReactNode } from "react"; 7 | 8 | const containerStyle: React.CSSProperties = { 9 | fontFamily: "Source Sans Pro Regular", 10 | cursor: "pointer", 11 | marginLeft: "-8px", 12 | paddingBottom: "4px", 13 | }; 14 | 15 | const currentPathStyle: React.CSSProperties = { 16 | fontFamily: "Source Sans Pro Regular", 17 | cursor: "pointer", 18 | fontWeight: "bold", 19 | }; 20 | 21 | interface IPage { 22 | name: string; 23 | path: string; 24 | index: boolean | null; 25 | } 26 | 27 | interface LinkProps { 28 | page: IPage; 29 | is_last: boolean; 30 | } 31 | 32 | function Link({ page, is_last }: LinkProps) { 33 | function handleClick() { 34 | if (page.index !== null) { 35 | if (page.index === false) { 36 | return; 37 | } 38 | } 39 | 40 | Streamlit.setComponentValue(page.path); 41 | } 42 | 43 | const step = !is_last ? " / " : ""; 44 | 45 | const style = is_last ? currentPathStyle : {}; 46 | 47 | return ( 48 | 49 | {page.name} 50 | {step} 51 | 52 | ); 53 | } 54 | 55 | class Breadcrumbs extends StreamlitComponentBase { 56 | public render = (): ReactNode => { 57 | const pages: IPage[] = this.props.args["pages"]; 58 | 59 | if (pages.length === 1) { 60 | return null; 61 | } 62 | 63 | return ( 64 |
65 | {pages.map((page: IPage, index: number) => ( 66 | 67 | ))} 68 |
69 | ); 70 | }; 71 | } 72 | 73 | export default withStreamlitConnection(Breadcrumbs); 74 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import Breadcrumbs from "./Breadcrumbs"; 4 | 5 | ReactDOM.render( 6 | 7 | 8 | , 9 | document.getElementById("root") 10 | ); 11 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /streamlit_superapp/web/breadcrumbs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/.env: -------------------------------------------------------------------------------- 1 | # Run the component's dev server on :3001 2 | # (The Streamlit dev server already runs on :3000) 3 | PORT=3001 4 | 5 | # Don't automatically open the web browser on `npm run start`. 6 | BROWSER=none -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/build/SourceSansPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilianZilv/streamlit_superapp/da1fa5ed385a042fc10156bba83b66619bf58153/streamlit_superapp/web/page_index/build/SourceSansPro-Regular.woff2 -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.js": "./static/js/main.cdd3c30b.js", 4 | "index.html": "./index.html", 5 | "main.cdd3c30b.js.map": "./static/js/main.cdd3c30b.js.map" 6 | }, 7 | "entrypoints": [ 8 | "static/js/main.cdd3c30b.js" 9 | ] 10 | } -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/build/index.html: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/build/static/js/main.cdd3c30b.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /** @license React v0.19.1 8 | * scheduler.production.min.js 9 | * 10 | * Copyright (c) Facebook, Inc. and its affiliates. 11 | * 12 | * This source code is licensed under the MIT license found in the 13 | * LICENSE file in the root directory of this source tree. 14 | */ 15 | 16 | /** @license React v16.13.1 17 | * react-is.production.min.js 18 | * 19 | * Copyright (c) Facebook, Inc. and its affiliates. 20 | * 21 | * This source code is licensed under the MIT license found in the 22 | * LICENSE file in the root directory of this source tree. 23 | */ 24 | 25 | /** @license React v16.14.0 26 | * react-dom.production.min.js 27 | * 28 | * Copyright (c) Facebook, Inc. and its affiliates. 29 | * 30 | * This source code is licensed under the MIT license found in the 31 | * LICENSE file in the root directory of this source tree. 32 | */ 33 | 34 | /** @license React v16.14.0 35 | * react-jsx-runtime.production.min.js 36 | * 37 | * Copyright (c) Facebook, Inc. and its affiliates. 38 | * 39 | * This source code is licensed under the MIT license found in the 40 | * LICENSE file in the root directory of this source tree. 41 | */ 42 | 43 | /** @license React v16.14.0 44 | * react.production.min.js 45 | * 46 | * Copyright (c) Facebook, Inc. and its affiliates. 47 | * 48 | * This source code is licensed under the MIT license found in the 49 | * LICENSE file in the root directory of this source tree. 50 | */ 51 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my_component", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "react": "^16.13.1", 7 | "react-dom": "^16.13.1", 8 | "streamlit-component-lib": "^2.0.0" 9 | }, 10 | "scripts": { 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test", 14 | "eject": "react-scripts eject" 15 | }, 16 | "eslintConfig": { 17 | "extends": "react-app" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | }, 31 | "homepage": ".", 32 | "devDependencies": { 33 | "@types/node": "^12.0.0", 34 | "@types/react": "^16.9.0", 35 | "@types/react-dom": "^16.9.0", 36 | "react-scripts": "^5.0.1", 37 | "typescript": "^4.2.0" 38 | } 39 | } -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/public/SourceSansPro-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WilianZilv/streamlit_superapp/da1fa5ed385a042fc10156bba83b66619bf58153/streamlit_superapp/web/page_index/public/SourceSansPro-Regular.woff2 -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/public/index.html: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/src/Card.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | StreamlitComponentBase, 4 | withStreamlitConnection, 5 | } from "streamlit-component-lib"; 6 | import React, { ReactNode } from "react"; 7 | 8 | export interface IPage { 9 | name: string; 10 | icon: string; 11 | description: string; 12 | path: string; 13 | tag: string | null; 14 | theme: any; 15 | hidden: boolean; 16 | } 17 | 18 | interface State { 19 | hover: boolean; 20 | count: number; 21 | isFocused: boolean; 22 | } 23 | 24 | const containerStyle: React.CSSProperties = { 25 | fontFamily: "Source Sans Pro Regular", 26 | minWidth: "250px", 27 | maxWidth: "300px", 28 | flex: 1, 29 | fontSize: "1.2rem", 30 | cursor: "pointer", 31 | padding: "16px", 32 | borderRadius: "8px", 33 | userSelect: "none", 34 | transition: "transform 0.2s ease-in-out", 35 | }; 36 | 37 | //box-shadow: 38 | 39 | // container expand on hover 40 | 41 | const descriptionStyle: React.CSSProperties = { 42 | fontSize: "1rem", 43 | fontWeight: 200, 44 | lineHeight: "1.5rem", 45 | }; 46 | 47 | const gap: React.CSSProperties = { 48 | height: "8px", 49 | }; 50 | 51 | /** 52 | * This is a React-based component template. The `render()` function is called 53 | * automatically when your component should be re-rendered. 54 | */ 55 | 56 | // define props 57 | interface Props { 58 | page: IPage; 59 | theme: any; 60 | onClick: (page: IPage) => void; 61 | } 62 | 63 | export default function Page({ page, theme, onClick }: Props) { 64 | const { name, icon, description }: IPage = page; 65 | 66 | const [hover, setHover] = React.useState(false); 67 | 68 | function onMouseEnter() { 69 | setHover(true); 70 | } 71 | function onMouseLeave() { 72 | setHover(false); 73 | } 74 | 75 | function onClicked() { 76 | onClick(page); 77 | } 78 | 79 | const transform = hover ? "scale(1.02)" : "scale(1)"; 80 | 81 | let backgroundColor = "rgb(26, 28, 36)"; 82 | let color = "white"; 83 | 84 | if (theme) { 85 | backgroundColor = 86 | theme.base == "dark" ? backgroundColor : theme.secondaryBackgroundColor; 87 | color = theme.textColor; 88 | } 89 | 90 | return ( 91 |
97 | 98 | {icon} {name} 99 | 100 |
101 | {description && 102 | description.split("\n").map((line: string, i: number) => ( 103 | 104 | {line} 105 |
106 |
107 | ))} 108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/src/Grid.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Streamlit, 3 | StreamlitComponentBase, 4 | withStreamlitConnection, 5 | } from "streamlit-component-lib"; 6 | import React, { Fragment, ReactNode } from "react"; 7 | import Page, { IPage } from "./Card"; 8 | 9 | const gridStyle: React.CSSProperties = { 10 | display: "flex", 11 | flexWrap: "wrap", 12 | gap: "8px", 13 | }; 14 | 15 | const tagStyle: React.CSSProperties = { 16 | fontFamily: "Source Sans Pro Regular", 17 | }; 18 | 19 | const EMPTY_TAG = "{ZZZ:}🏷️ Untagged"; 20 | const TAG_SORT_PATTERN = "{([^}]+):}"; 21 | 22 | class Grid extends StreamlitComponentBase { 23 | private handlePageClick = (page: IPage) => { 24 | Streamlit.setComponentValue(page.path); 25 | }; 26 | 27 | public render = (): ReactNode => { 28 | let pages: IPage[] = this.props.args["pages"]; 29 | const { theme } = this.props; 30 | 31 | pages = pages.filter((page) => !page.hidden); 32 | 33 | let tags: string[] = []; 34 | 35 | for (const page of pages) { 36 | let _tag = page.tag || EMPTY_TAG; 37 | if (!tags.includes(_tag)) tags.push(_tag); 38 | } 39 | 40 | function key(tag: string | null) { 41 | if (!tag) { 42 | tag = EMPTY_TAG; 43 | } 44 | 45 | let match = tag.match(TAG_SORT_PATTERN); 46 | if (match === null) { 47 | return tag; 48 | } 49 | 50 | return match[1]; 51 | } 52 | 53 | tags = tags.sort((a, b) => key(a).localeCompare(key(b))); 54 | 55 | function resolveTag(tag: string) { 56 | let match = tag.match(TAG_SORT_PATTERN); 57 | if (match === null) { 58 | return tag; 59 | } 60 | 61 | return tag.replace(match[0], ""); 62 | } 63 | 64 | let groups: any = {}; 65 | 66 | for (const tag of tags) { 67 | groups[resolveTag(tag)] = pages.filter( 68 | (page) => (page.tag || EMPTY_TAG) === tag 69 | ); 70 | } 71 | 72 | const render_tags = tags.length > 1; 73 | 74 | return Object.keys(groups).map((tag) => { 75 | return ( 76 | 77 | {render_tags &&

{tag}

} 78 |
79 | {groups[tag].map((page: IPage) => { 80 | return ( 81 | 87 | ); 88 | })} 89 |
90 |
91 | ); 92 | }); 93 | }; 94 | } 95 | 96 | export default withStreamlitConnection(Grid); 97 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | //import Card from "./Card"; 4 | import Grid from "./Grid"; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById("root") 11 | ); 12 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /streamlit_superapp/web/page_index/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react" 17 | }, 18 | "include": ["src"] 19 | } -------------------------------------------------------------------------------- /streamlit_superapp/widgets.py: -------------------------------------------------------------------------------- 1 | from typing import Literal, Optional, Union 2 | import streamlit as st 3 | from streamlit_superapp.navigation import Navigation 4 | 5 | from streamlit_superapp.state import State 6 | from streamlit.type_util import Key, LabelVisibility 7 | from streamlit.runtime.state.common import WidgetCallback, WidgetArgs, WidgetKwargs 8 | 9 | from streamlit_superapp.typing import Page 10 | 11 | 12 | def experimental_text_input( 13 | label: str, 14 | value: str = "", 15 | max_chars: Optional[int] = None, 16 | key: Optional[Union[Page, str]] = None, 17 | type: Literal["default", "password"] = "default", 18 | help: Optional[str] = None, 19 | autocomplete: Optional[str] = None, 20 | on_change: Optional[WidgetCallback] = None, 21 | args: Optional[WidgetArgs] = None, 22 | kwargs: Optional[WidgetKwargs] = None, 23 | *, 24 | placeholder: Optional[str] = None, 25 | disabled: bool = False, 26 | label_visibility: LabelVisibility = "visible", 27 | private: Union[bool, Page] = True, 28 | ): 29 | key = key or label 30 | 31 | page = private or None 32 | 33 | if page is True: 34 | page = Navigation.current_page() 35 | 36 | state = State(f"widget:text_input:{key}", default_value=value, key=page) 37 | 38 | state.bind( 39 | st.text_input( 40 | label, 41 | value=state.initial_value, 42 | max_chars=max_chars, 43 | key=state.key, 44 | type=type, 45 | help=help, 46 | autocomplete=autocomplete, 47 | on_change=on_change, 48 | args=args, 49 | kwargs=kwargs, 50 | placeholder=placeholder, 51 | disabled=disabled, 52 | label_visibility=label_visibility, 53 | ) 54 | ) 55 | 56 | return state 57 | 58 | 59 | __all__ = ["experimental_text_input"] 60 | --------------------------------------------------------------------------------