├── examples ├── requirements.txt ├── .streamlit │ └── config.toml └── streamlit_app.py ├── MANIFEST.in ├── streamlit_slickgrid ├── requirements.txt ├── frontend │ ├── .prettierrc │ ├── .env │ ├── tsconfig.json │ ├── src │ │ ├── index.tsx │ │ ├── style.scss │ │ └── StreamlitSlickGrid.tsx │ ├── public │ │ └── index.html │ └── package.json └── __init__.py ├── pyproject.toml ├── .devcontainer └── devcontainer.json ├── README.md ├── .gitignore └── LICENSE /examples/requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | streamlit-slickgrid 3 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include streamlit_slickgrid/frontend/build * 2 | -------------------------------------------------------------------------------- /streamlit_slickgrid/requirements.txt: -------------------------------------------------------------------------------- 1 | streamlit 2 | streamlit-slickgrid 3 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "semi": false, 4 | "trailingComma": "es5" 5 | } 6 | -------------------------------------------------------------------------------- /examples/.streamlit/config.toml: -------------------------------------------------------------------------------- 1 | [theme] 2 | primaryColor="#F63366" 3 | backgroundColor="#FFFFFF" 4 | secondaryBackgroundColor="#F0F2F6" 5 | textColor="#262730" 6 | font="sans serif" 7 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/.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 7 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/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 | } 20 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "streamlit-slickgrid" 3 | version = "0.2.0" # Also update src/frontend/package.json 4 | authors = [ 5 | { name = "Thiago Teixeira", email = "thiago.teixeira@snowflake.com" }, 6 | ] 7 | description = "A Streamlit component that wraps SlickGrid" 8 | readme = "README.md" 9 | keywords = [] 10 | #maintainers = [] 11 | license = { text = "Apache 2.0" } 12 | requires-python = ">=3.9.0" 13 | dependencies = [ 14 | # Version picked at random. 15 | "streamlit >= 1.40.0", 16 | ] 17 | classifiers = [] 18 | dynamic = [] 19 | entry-points = {} 20 | gui-scripts = {} 21 | scripts = {} 22 | urls = {} 23 | 24 | [build-system] 25 | requires = ["build", "setuptools"] 26 | 27 | [tool.setuptools] 28 | # Fix a bug in setuptools. 29 | # See: https://github.com/astral-sh/uv/issues/9513 30 | # Remove this when setuptools is fixed. 31 | license-files = [] 32 | 33 | [tool.setuptools.packages.find] 34 | include = ["streamlit_slickgrid"] 35 | # See also MANIFEST.in 36 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Snowflake Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import React from "react" 16 | import { createRoot } from "react-dom/client" 17 | import StreamlitSlickGrid from "./StreamlitSlickGrid" 18 | 19 | const element = document.getElementById("root") 20 | const root = createRoot(element as HTMLElement) 21 | 22 | root.render( 23 | 24 | ) 25 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Streamlit Component 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/src/style.scss: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Snowflake Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | @use '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-bootstrap.scss' with ( 16 | $slick-font-family: var(--font), 17 | ); 18 | 19 | body { 20 | background: var(--background-color); 21 | color: var(--text-color); 22 | font-family: var(--font); 23 | } 24 | 25 | .slickgrid-container .form-group { 26 | /* Fix issue with date filter position */ 27 | margin: 0; 28 | } 29 | 30 | .slickgrid-container * { 31 | font-family: var(--font) !important; 32 | } 33 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Python 3", 3 | // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile 4 | "image": "mcr.microsoft.com/devcontainers/python:1-3.11-bullseye", 5 | "customizations": { 6 | "codespaces": { 7 | "openFiles": [ 8 | "README.md", 9 | "examples/streamlit_app.py" 10 | ] 11 | }, 12 | "vscode": { 13 | "settings": {}, 14 | "extensions": [ 15 | "ms-python.python", 16 | "ms-python.vscode-pylance" 17 | ] 18 | } 19 | }, 20 | "updateContentCommand": "[ -f packages.txt ] && sudo apt update && sudo apt upgrade -y && sudo xargs apt install -y 0.2%", 18 | "not dead", 19 | "not op_mini all" 20 | ], 21 | "development": [ 22 | "last 1 chrome version", 23 | "last 1 firefox version", 24 | "last 1 safari version" 25 | ] 26 | }, 27 | "dependencies": { 28 | "@babel/plugin-proposal-private-property-in-object": "^7", 29 | "@slickgrid-universal/excel-export": "^5.12.2", 30 | "@slickgrid-universal/text-export": "^5.12.2", 31 | "react": "^19.0.0", 32 | "react-dom": "^19.0.0", 33 | "slickgrid-react": "^5.12.2", 34 | "streamlit-component-lib": "^2.0.0" 35 | }, 36 | "devDependencies": { 37 | "@types/node": "^22.13.1", 38 | "@types/react": "^19.0.8", 39 | "@types/react-dom": "^19.0.3", 40 | "react-scripts": "^5.0.1", 41 | "sass": "^1.84.0", 42 | "typescript": "^4.9.5" 43 | }, 44 | "overrides": { 45 | "react": "^19.0.0", 46 | "react-dom": "^19.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # streamlit-slickgrid 2 | 3 | A wrapper that allows you to use [SlickGrid](https://github.com/ghiscoding/slickgrid-universal) in Streamlit. 4 | 5 | View demo: 6 | 7 | [![Streamlit App](https://static.streamlit.io/badges/streamlit_badge_black_white.svg)](https://slickgrid.streamlit.app/) 8 | 9 | IMPORTANT: This is not an officially-maintained package by Streamlit. We built this to "scratch our own itch" with some internal Finance folks who needed it for their apps. Consider this a community project, for all intents and purposes. That said, we think it's awesome :) 10 | 11 | ## Installation instructions 12 | 13 | ```sh 14 | pip install streamlit-slickgrid 15 | ``` 16 | 17 | ## Usage instructions 18 | 19 | See [examples/streamlit_app.py](https://github.com/streamlit/streamlit-slickgrid/blob/main/examples/streamlit_app.py). 20 | 21 | ## Contributing 22 | 23 | ### Development setup 24 | 25 | In one terminal: 26 | 27 | ```sh 28 | cd [this folder] 29 | python -m venv .venv # One-time only. 30 | source .venv/bin/activate 31 | pip install -e . 32 | streamlit run examples/streamlit_app.py 33 | ``` 34 | 35 | In another terminal: 36 | 37 | ```sh 38 | cd [this folder] 39 | cd streamlit_slickgrid/frontend 40 | npm install 41 | npm run start 42 | ``` 43 | 44 | ### Building wheel file 45 | 46 | ```sh 47 | cd [this folder] 48 | 49 | # Build front end 50 | cd streamlit_slickgrid/frontend 51 | npm run build 52 | 53 | # Build Python library 54 | cd ../.. 55 | rm dist/* 56 | uv build 57 | # The wheel file is in dist/ now. 58 | ``` 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################################################################## 2 | # Python - https://github.com/github/gitignore/blob/master/Python.gitignore 3 | ######################################################################## 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # Distribution / packaging 10 | build/ 11 | dist/ 12 | eggs/ 13 | .eggs/ 14 | *.egg-info/ 15 | *.egg 16 | .prerelease-version 17 | 18 | # Unit test / coverage reports 19 | .coverage 20 | .coverage\.* 21 | .pytest_cache/ 22 | .mypy_cache/ 23 | test-reports 24 | htmlcov 25 | .hypothesis 26 | .ruff_cache 27 | 28 | # Test fixtures 29 | cffi_bin 30 | 31 | # Pyenv / uv Stuff 32 | .python-version 33 | venv 34 | .venv 35 | 36 | # Autogenerated Protobufs 37 | lib/streamlit/proto/*_pb2.py 38 | lib/streamlit/proto/*_pb2.pyi 39 | frontend/lib/src/proto.js 40 | frontend/lib/src/proto.d.ts 41 | 42 | ######################################################################## 43 | # OSX - https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 44 | ######################################################################## 45 | .DS_Store 46 | .DocumentRevisions-V100 47 | .fseventsd 48 | .Spotlight-V100 49 | .TemporaryItems 50 | .Trashes 51 | .VolumeIcon.icns 52 | .com.apple.timemachine.donotpresent 53 | 54 | ######################################################################## 55 | # node - https://github.com/github/gitignore/blob/master/Node.gitignore 56 | ######################################################################## 57 | # Logs 58 | npm-debug.log* 59 | yarn-debug.log* 60 | yarn-error.log* 61 | 62 | # Yarn files 63 | **/.yarn/* 64 | !**/.yarn/patches 65 | !**/.yarn/plugins 66 | !**/.yarn/releases 67 | !**/.yarn/sdks 68 | !**/.yarn/versions 69 | 70 | # Dependency directories 71 | node_modules/ 72 | 73 | # ESLint 74 | .eslintcache 75 | 76 | # Coverage directory used by tools like istanbul 77 | coverage/ 78 | -------------------------------------------------------------------------------- /streamlit_slickgrid/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Snowflake Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import streamlit as st 16 | import os 17 | import streamlit.components.v1 as components 18 | 19 | _RELEASE = True 20 | _NAME = "streamlit-slickgrid" 21 | 22 | if _RELEASE: 23 | parent_dir = os.path.dirname(os.path.abspath(__file__)) 24 | build_dir = os.path.join(parent_dir, "frontend/build") 25 | _component_func = components.declare_component( 26 | _NAME, 27 | path=build_dir, 28 | ) 29 | else: 30 | _component_func = components.declare_component( 31 | _NAME, 32 | url="http://localhost:3001", 33 | ) 34 | 35 | 36 | def slickgrid(data, columns, options=None, on_click=None, key=None): 37 | """Display a SlickGrid component. 38 | 39 | The best way to learn use SlickGrid is to check out the demos at: 40 | - https://ghiscoding.github.io/slickgrid-react-demos/#/example1 41 | 42 | Parameters 43 | ---------- 44 | data: list of dict 45 | The dataset to display, as a list of dicts. For example: 46 | 47 | data = [ 48 | {"id": 0, "continent": "america", "revenue": 20000, "paused": False}, 49 | {"id": 1, "continent": "africa", "revenue": 40100, "paused": False}, 50 | {"id": 2, "continent": "asia", "revenue": 10300, "paused": True}, 51 | {"id": 3, "continent": "europe", "revenue": 30200, "paused": False}, 52 | ... 53 | ] 54 | 55 | columns: list of dict 56 | Column definitions. Which columns to show, how to show them, how to 57 | filter them, etc. 58 | 59 | See full list of options at: 60 | - https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/column.interface.ts#L40 61 | 62 | Not all column options are supported, though! 63 | 64 | options: dict or None 65 | Global grid options. 66 | 67 | See full list of options at: 68 | - https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/gridOption.interface.ts#L76 69 | 70 | Not all grid options are supported, though! 71 | 72 | on_click: "rerun", "ignore", or None 73 | If "rerun", then the clicked cell [row, col] will be returned 74 | by this function. 75 | 76 | Returns 77 | ------- 78 | None or list of numbers 79 | If on_click is set to "rerun", the [row, col] indices of the clicked 80 | cell is returned. Otherwise, None. 81 | 82 | """ 83 | session_key = f"-streamlit-slickgrid-{key}" 84 | if session_key not in st.session_state: 85 | st.session_state[session_key] = None 86 | 87 | component_value = _component_func( 88 | data=data, 89 | columns=columns, 90 | options=options, 91 | onClick=on_click is not None and on_click != "ignore", 92 | key=key, 93 | default=None, 94 | ) 95 | 96 | change_detected = component_value != st.session_state[session_key] 97 | 98 | st.session_state[session_key] = component_value 99 | 100 | if change_detected: 101 | return component_value 102 | else: 103 | return None 104 | 105 | 106 | def add_tree_info(data, tree_fields, join_fields_as=None, id_field="id"): 107 | """Calculates tree fields data's structure. Returns a new data array. 108 | 109 | Parameters 110 | ---------- 111 | data: list of dict 112 | See slickgrid() data field. 113 | 114 | tree_fields: list of str 115 | List with name of fields to coalesce into a tree structure. 116 | 117 | join_fields_as: str 118 | Name of the new column that will be added with the coalesced fields. 119 | 120 | id_field: str 121 | Name of the ID field used in data. Defaults to "id". 122 | 123 | Returns 124 | ------- 125 | list of dict 126 | A copy of the data, but with 3 additional fields: 127 | - The join field: see join_field_as 128 | - __parent: a field holding parent/child relationships 129 | - __depth: a field holding parent/child depth information 130 | 131 | Example 132 | ------- 133 | 134 | Let's say `data` has the form: 135 | 136 | id, continent, country, city, population 137 | 138 | Then you'd call this: 139 | 140 | add_tree_info(data, ["continent", "country", "city"]) 141 | 142 | And end up with something like: 143 | __parent __depth id continent country city population 144 | None 0 0 A0 None None P1 145 | 0 1 1 A0 B1 C1 P2 146 | None 0 2 A2 None None P3 147 | None 2 3 A2 B3 None P4 148 | None 3 4 A2 B3 C4 P5 149 | None 3 5 A2 B3 C5 P6 150 | 151 | Which implies the following structure: 152 | 153 | id continent country city population 154 | + 0 A0 None None P1 155 | └─ 1 A0 B1 C1 P2 156 | + 2 A2 None None P3 157 | └─+ 3 A2 B3 None P4 158 | ├─ 4 A2 B3 C4 P5 159 | └─ 5 A2 B3 C5 P6 160 | 161 | You can also set join_fields_as to some string "my_field" to join 162 | all the tree fields into a new column, like this: 163 | 164 | __parent __depth id my_field continent country city population 165 | None 0 0 A0 A0 None None P1 166 | 0 1 1 B1 A0 B1 C1 P2 167 | None 0 2 A2 A2 None None P3 168 | None 2 3 B3 A2 B3 None P4 169 | None 3 4 C4 A2 B3 C5 P5 170 | None 3 5 C5 A2 B3 C6 P6 171 | 172 | Because, then, you can hide the other columns and get a pretty 173 | tree like this: 174 | 175 | my_field population 176 | + A0 P1 177 | └─ B1 P2 178 | + A2 P3 179 | └─+ B3 P4 180 | ├─ C4 P5 181 | └─ C5 P6 182 | """ 183 | 184 | new_data = [] 185 | parents = [] 186 | 187 | for i, item in enumerate(data): 188 | num_equal_fields = 0 189 | 190 | if i > 0: 191 | prev_item = data[i - 1] 192 | 193 | for field in tree_fields: 194 | if item[field] == prev_item[field]: 195 | num_equal_fields += 1 196 | else: 197 | break 198 | 199 | if num_equal_fields > len(parents): 200 | parents.append(prev_item) 201 | elif num_equal_fields < len(parents): 202 | parents = parents[:num_equal_fields] 203 | 204 | P = len(parents) 205 | new_item = {**item} 206 | new_data.append(new_item) 207 | 208 | new_item["__depth"] = P 209 | 210 | if P > 0: 211 | new_item["__parent"] = parents[-1][id_field] 212 | else: 213 | new_item["__parent"] = None 214 | 215 | if join_fields_as is not None: 216 | new_item[join_fields_as] = new_item[tree_fields[P]] 217 | 218 | return new_data 219 | 220 | 221 | class _JsModuleProxy: 222 | """Dummy class that produces strings pointing to JS functions.""" 223 | 224 | def __init__(self, js_module_name): 225 | self.name = js_module_name 226 | 227 | def __getattr__(self, key): 228 | return f"js${self.name}.{key}" 229 | 230 | 231 | def __getattr__(js_module_name): 232 | """Syntax sugar so you can do this: 233 | 234 | from streamlit_slickgrid import Foo 235 | 236 | Foo.bar 237 | # Returns "js$Foo.bar"! 238 | 239 | Why this is useful: SlickGrid's options are often JS functions that you 240 | need to pass by reference, and this allows us to pass them by name 241 | instead (since it's not possible to pass a function safely from Python 242 | to JS). 243 | 244 | This allows you to use any of the modules listed in the MODULE_PROXIES 245 | object, on the JS side. 246 | """ 247 | return _JsModuleProxy(js_module_name) 248 | -------------------------------------------------------------------------------- /examples/streamlit_app.py: -------------------------------------------------------------------------------- 1 | # Copyright 2025 Snowflake Inc. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | import streamlit as st 16 | import numpy as np 17 | import math 18 | import random 19 | from streamlit_slickgrid import ( 20 | add_tree_info, 21 | slickgrid, 22 | Formatters, 23 | Filters, 24 | FieldType, 25 | OperatorType, 26 | ExportServices, 27 | StreamlitSlickGridFormatters, 28 | StreamlitSlickGridSorters, 29 | ) 30 | 31 | st.set_page_config( 32 | layout="wide", 33 | ) 34 | 35 | 36 | @st.cache_resource 37 | def mockData(count): 38 | """Build some mock data.""" 39 | mockDataset = [] 40 | 41 | epics_in_milestone = 0 42 | tasks_in_epic = 0 43 | m = 0 44 | e = 0 45 | t = 0 46 | 47 | for i in range(count): 48 | randomYear = 2000 + math.floor(random.random() * 10) 49 | randomMonth = math.floor(random.random() * 11) 50 | randomDay = math.floor((random.random() * 29)) 51 | randomPercent = round(random.random() * 100) 52 | 53 | if t >= tasks_in_epic: 54 | tasks_in_epic = random.randint(2, 10) 55 | t = 0 56 | e += 1 57 | else: 58 | t += 1 59 | 60 | if e >= epics_in_milestone: 61 | epics_in_milestone = random.randint(2, 10) 62 | tasks_in_epic = 0 63 | m += 1 64 | e = 0 65 | t = 0 66 | 67 | mockDataset.append( 68 | { 69 | "id": i, 70 | "milestone": f"Milestone M{m:02}", 71 | "epic": None if e == 0 else f"Epic M{m:02}/E{e:02}", 72 | "task": None if t == 0 else f"Task M{m:02}/E{e:02}/T{t:02}", 73 | "stages": [round(random.random() * 100) for _ in range(3)], 74 | "duration": round(random.random() * 100), 75 | "percentComplete": randomPercent, 76 | "start": f"{randomYear:02}-{randomMonth + 1:02}-{randomDay:02}", 77 | "finish": f"{randomYear + 1:02}-{randomMonth + 1:02}-{randomDay:02}", 78 | "effortDriven": (i % 5 == 0), 79 | } 80 | ) 81 | 82 | return mockDataset 83 | 84 | 85 | """ 86 | # Streamlit-SlickGrid demo 87 | 88 | For more info, see https://github.com/streamlit/streamlit-slickgrid. 89 | """ 90 | 91 | # streamlit-slickgrid requires the data to be a list of dicts. 92 | # 93 | # For example: 94 | # 95 | # data = [ 96 | # {"id": 0, "continent": "america", "revenue": 20000, "paused": False}, 97 | # {"id": 1, "continent": "africa", "revenue": 40100, "paused": False}, 98 | # {"id": 2, "continent": "asia", "revenue": 10300, "paused": True}, 99 | # {"id": 3, "continent": "europe", "revenue": 30200, "paused": False}, 100 | # ... 101 | # ] 102 | # 103 | # Here we're just building a random dataset: 104 | data = mockData(1000) 105 | 106 | # Coalesce the milestone, epic, and task fields into a single one called title. 107 | data = add_tree_info( 108 | data, 109 | tree_fields=["milestone", "epic", "task"], 110 | join_fields_as="title", 111 | id_field="id", 112 | ) 113 | 114 | # Some nice colors to use in the table. 115 | red = "#ff4b4b" 116 | orange = "#ffa421" 117 | yellow = "#ffe312" 118 | green = "#21c354" 119 | teal = "#00c0f2" 120 | blue = "#1c83e1" 121 | violet = "#803df5" 122 | white = "#fafafa" 123 | gray = "#808495" 124 | black = "#262730" 125 | 126 | # Declare SlickGrid columns. 127 | # 128 | # See full list of options at: 129 | # - https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/column.interface.ts#L40 130 | # 131 | # Not all column options are supported, though! 132 | columns = [ 133 | { 134 | "id": "title", 135 | "name": "Title", 136 | "field": "title", 137 | "sortable": True, 138 | "minWidth": 50, 139 | "type": FieldType.string, 140 | "filterable": True, 141 | "formatter": Formatters.tree, 142 | "exportCustomFormatter": Formatters.treeExport, 143 | }, 144 | { 145 | "id": "duration", 146 | "name": "Duration (days)", 147 | "field": "duration", 148 | "sortable": True, 149 | "minWidth": 100, 150 | "type": FieldType.number, 151 | "filterable": True, 152 | "filter": { 153 | "model": Filters.slider, 154 | "operator": ">=", 155 | }, 156 | "formatter": StreamlitSlickGridFormatters.numberFormatter, 157 | "params": { 158 | "colors": [ 159 | # [maxValue, foreground, background] 160 | [20, blue, None], # None is the same as leaving out 161 | [50, green], 162 | [100, gray], 163 | ], 164 | "minDecimal": 0, 165 | "maxDecimal": 2, 166 | "numberSuffix": "d", 167 | # You can pass your own styles here. 168 | # "style": {"text-align": "left", "padding": "0 0.5ch"}, 169 | }, 170 | }, 171 | { 172 | "id": "stages", 173 | "name": "Stages", 174 | "field": "stages", 175 | "sortable": True, 176 | "sorter": StreamlitSlickGridSorters.numberArraySorter, 177 | "minWidth": 100, 178 | # Sorry, the "stages" field contains arrays, which aren't filterable. 179 | "filterable": False, 180 | "formatter": StreamlitSlickGridFormatters.stackedBarFormatter, 181 | "params": { 182 | "colors": [ 183 | # [maxValue, foreground, background] 184 | [20, white, red], 185 | [70, black, orange], 186 | [100, white, green], 187 | ], 188 | "minDecimal": 0, 189 | "maxDecimal": 2, 190 | "min": 0, 191 | "max": 300, 192 | # You can pass your own styles here. 193 | # "style": {"text-align": "left", "padding": "0 0.5ch"}, 194 | }, 195 | }, 196 | { 197 | "id": "%", 198 | "name": "% Complete", 199 | "field": "percentComplete", 200 | "sortable": True, 201 | "minWidth": 100, 202 | "type": FieldType.number, 203 | "filterable": True, 204 | "filter": { 205 | "model": Filters.sliderRange, 206 | "maxValue": 100, 207 | "operator": OperatorType.rangeInclusive, 208 | "filterOptions": {"hideSliderNumbers": False, "min": 0, "step": 5}, 209 | }, 210 | # Use the default progress bar formatter: 211 | # "formatter": Formatters.progressBar, 212 | # 213 | # Or use this fancy one that's ultra-configurable: 214 | "formatter": StreamlitSlickGridFormatters.barFormatter, 215 | "params": { 216 | "colors": [[50, white, red], [100, white, green]], 217 | "minDecimal": 0, 218 | "maxDecimal": 2, 219 | "numberSuffix": "%", 220 | # You can pass your own styles here. 221 | # "style": {"text-align": "left", "padding": "0 0.5ch"}, 222 | }, 223 | }, 224 | { 225 | "id": "start", 226 | "name": "Start", 227 | "field": "start", 228 | "type": FieldType.date, 229 | "filterable": True, 230 | "filter": {"model": Filters.compoundDate}, 231 | "formatter": Formatters.dateIso, 232 | }, 233 | { 234 | "id": "finish", 235 | "name": "Finish", 236 | "field": "finish", 237 | "type": FieldType.date, 238 | "filterable": True, 239 | "filter": {"model": Filters.dateRange}, 240 | "formatter": Formatters.dateIso, 241 | }, 242 | { 243 | "id": "effort-driven", 244 | "name": "Effort Driven", 245 | "field": "effortDriven", 246 | "sortable": True, 247 | "minWidth": 100, 248 | "type": FieldType.boolean, 249 | "filterable": True, 250 | "filter": { 251 | "model": Filters.singleSelect, 252 | "collection": [ 253 | {"value": "", "label": ""}, 254 | {"value": True, "label": "True"}, 255 | {"value": False, "label": "False"}, 256 | ], 257 | }, 258 | "formatter": Formatters.checkmarkMaterial, 259 | }, 260 | ] 261 | 262 | 263 | # Configure additional options streamlit-slickgrid. 264 | # 265 | # See full list of options at: 266 | # - https://github.com/ghiscoding/slickgrid-universal/blob/master/packages/common/src/interfaces/gridOption.interface.ts#L76 267 | # 268 | # Not all grid options are supported, though! 269 | options = { 270 | # 271 | # Allow filtering (based on column filter* properties) 272 | "enableFiltering": True, 273 | # -- 274 | # 275 | # Debounce/throttle the input text filter if you have lots of data 276 | # filterTypingDebounce: 250, 277 | # -- 278 | # 279 | # Set up export options. 280 | "enableTextExport": True, 281 | "enableExcelExport": True, 282 | "excelExportOptions": {"sanitizeDataExport": True}, 283 | "textExportOptions": {"sanitizeDataExport": True}, 284 | "externalResources": [ 285 | ExportServices.ExcelExportService, 286 | ExportServices.TextExportService, 287 | ], 288 | # -- 289 | # 290 | # Pin columns. 291 | # "frozenColumn": 0, 292 | # -- 293 | # 294 | # Pin rows. 295 | # "frozenRow": 0, 296 | # -- 297 | # 298 | # Don't scroll table when too big. Instead, just let it grow. 299 | # "autoHeight": True, 300 | # -- 301 | # 302 | "autoResize": { 303 | "minHeight": 500, 304 | }, 305 | # -- 306 | # 307 | # Set up tree. 308 | "enableTreeData": True, 309 | "multiColumnSort": False, 310 | "treeDataOptions": { 311 | "columnId": "title", 312 | "indentMarginLeft": 15, 313 | "initiallyCollapsed": True, 314 | # This is a field that add_tree_info() inserts in your data: 315 | "parentPropName": "__parent", 316 | # This is a field that add_tree_info() inserts in your data: 317 | "levelPropName": "__depth", 318 | # 319 | # If you're building your own tree (without add_tree_info), 320 | # you should configure the props above accordingly. 321 | # 322 | # See below for more info: 323 | # - https://ghiscoding.github.io/slickgrid-react-demos/#/example27 324 | # - https://ghiscoding.github.io/slickgrid-react-demos/#/example28 325 | }, 326 | } 327 | 328 | out = slickgrid(data, columns, options, key="mygrid", on_click="rerun") 329 | 330 | 331 | @st.dialog("Details", width="large") 332 | def show_dialog(item): 333 | st.write("Congrats! You clicked on the row below:") 334 | st.write(item) 335 | 336 | st.write("Here's a random chart for you:") 337 | st.write("") 338 | 339 | st.scatter_chart(np.random.randn(100, 5)) 340 | 341 | 342 | if out is not None: 343 | row, col = out 344 | show_dialog(data[row]) 345 | -------------------------------------------------------------------------------- /streamlit_slickgrid/frontend/src/StreamlitSlickGrid.tsx: -------------------------------------------------------------------------------- 1 | // Copyright 2025 Snowflake Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | import React, { useCallback, useEffect, useState, ReactElement } from "react" 16 | import { 17 | Column, 18 | createDomElement, 19 | decimalFormatter, 20 | FieldType, 21 | Filters, 22 | Formatters, 23 | getValueFromParamsOrFormatterOptions, 24 | GridOption, 25 | OperatorType, 26 | SlickGrid, 27 | SlickgridReact, 28 | } from "slickgrid-react" 29 | import { ExcelExportService } from "@slickgrid-universal/excel-export"; 30 | import { TextExportService } from "@slickgrid-universal/text-export"; 31 | 32 | import { 33 | Streamlit, 34 | withStreamlitConnection, 35 | ComponentProps, 36 | } from "streamlit-component-lib" 37 | 38 | import "./style.scss" 39 | 40 | function StreamlitSlickGrid({ args, disabled, theme }: ComponentProps): ReactElement { 41 | const [columns, setColumns] = useState(() => replaceJsStrings(args.columns)) 42 | const [options, setOptions] = useState(() => replaceJsStrings(args.options)) 43 | const [data, setData] = useState(args.data) 44 | 45 | useEffect(() => { 46 | setColumns(replaceJsStrings(args.columns)) 47 | setOptions(replaceJsStrings(args.options)) 48 | setData(args.data) 49 | }, [args, args.data, args.columns, args.options]) 50 | 51 | // @ts-ignore 52 | const onClick = useCallback((ev) => { 53 | // Ignore clicks on the expander element. 54 | if (ev.detail.eventData?.target?.classList.contains("slick-group-toggle")) 55 | return 56 | 57 | const data = ev.detail.args.grid.data 58 | const rowId = data.rows[ev.detail.args.row][data.idProperty] 59 | 60 | Streamlit.setComponentValue([ 61 | rowId, 62 | ev.detail.args.cell, 63 | ]) 64 | }, []) 65 | 66 | const onReactGridCreated = useCallback(() => { 67 | Streamlit.setFrameHeight() 68 | }, []) 69 | 70 | return ( 71 | 79 | ) 80 | } 81 | 82 | function replaceJsStrings(obj: any): any { 83 | const result = Array.isArray(obj) ? [] : {}; 84 | const stack = [{ source: obj, target: result }]; 85 | 86 | while (stack.length > 0) { 87 | const { source, target } = stack.pop() ?? {}; 88 | 89 | for (const key in source) { 90 | if (typeof source[key] === "string" && source[key].startsWith("js$")) { 91 | const [moduleStr, memberStr] = source[key].slice(3).split("."); 92 | // @ts-ignore 93 | const module = MODULE_PROXIES[moduleStr]; 94 | // @ts-ignore 95 | if (module && target) target[key] = module[memberStr] 96 | 97 | } else if (typeof source[key] === "object" && source[key] !== null) { 98 | // @ts-ignore 99 | target[key] = Array.isArray(source[key]) ? [] : {}; 100 | // @ts-ignore 101 | stack.push({ source: source[key], target: target[key] }); 102 | 103 | } else { 104 | // @ts-ignore 105 | target[key] = source[key]; 106 | } 107 | } 108 | } 109 | 110 | return result; 111 | } 112 | 113 | type ColorDefs = [number | null, string, string][] 114 | type ReplacementDefs = Record 115 | 116 | const StreamlitSlickGridFormatters = { 117 | /** 118 | * Adds styling and decorations to a number, inlcuding: colors, prefix, suffix, 119 | * number of decimal places, decimal separators, etc. 120 | * 121 | * Example: 122 | * 123 | * columns = { 124 | * ... 125 | * { 126 | * ... 127 | * "formatter": StreamlitSlickGridFormatters.numberFormatter, 128 | * 129 | * # Everything below is optional. 130 | * "params": { 131 | * 132 | * # Define color bands. The format is [maxValue, foregroundColor, backgroundColor]. 133 | * # The color is selected by testing each triplet left to right. The first to match wins. 134 | * # If a maxValue of null is reached, that set of colors is picked. 135 | * "colors": [[20, "#f00"], [40, "#ff0"], [80, "#0b0"], [100, "#00f"]], 136 | * 137 | * # Define number of decimal places. 138 | * "minDecimal": 2, 139 | * "maxDecimal": 4, 140 | * 141 | * # Define suffix/prefix: 142 | * "numberPrefix": "$", 143 | * "numberSuffix": "!", 144 | * 145 | * # Tweak locale information: 146 | * "decimalSeparator": ".", 147 | * "thousandSeparator": ",", 148 | * 149 | * # Use parentheses for negative numbers: 150 | * "wrapNegativeNumbers": false, 151 | * } 152 | * } 153 | * } 154 | * 155 | */ 156 | numberFormatter(row: any, cell: number, value: number, columnDef: Column, dataContext: Record, grid: SlickGrid) { 157 | const formattedStr = decimalFormatter(row, cell, value, columnDef, dataContext, grid) 158 | const [fgColor, bgColor] = getColor(value, columnDef, grid) 159 | 160 | const gridOptions = (grid && typeof grid.getOptions === "function" ? grid.getOptions() : {}) as GridOption 161 | const style: Record = getValueFromParamsOrFormatterOptions("style", columnDef, gridOptions, []) 162 | 163 | return createDomElement("span", { 164 | style: { 165 | color: fgColor, 166 | backgroundColor: bgColor, 167 | ...style 168 | }, 169 | textContent: formattedStr as string, 170 | }) 171 | }, 172 | 173 | /** 174 | * Replaces strings with others. 175 | * 176 | * Example: 177 | * 178 | * columns = { 179 | * ... 180 | * { 181 | * ... 182 | * "formatter": StreamlitSlickGridFormatters.stringReplacer, 183 | * 184 | * # Everything below is optional. 185 | * "params": { 186 | * "replacements": { 187 | * "true": "😃", 188 | * "false": "😭", 189 | * "null": "🫣", 190 | * }, 191 | * } 192 | * } 193 | * } 194 | * 195 | */ 196 | stringReplacer(_row: any, _cell: number, value: any, columnDef: Column, _dataContext: Record, grid: SlickGrid) { 197 | const gridOptions = (grid && typeof grid.getOptions === "function" ? grid.getOptions() : {}) as GridOption 198 | const replacementDefs: ReplacementDefs = getValueFromParamsOrFormatterOptions("replacements", columnDef, gridOptions, []) 199 | 200 | return replacementDefs?.[String(value)] ?? value 201 | }, 202 | 203 | /** 204 | * Formats the number as a bar. Same as SlickGrid's percentComplete formatter, but with color 205 | * configuration, and not specific to percentages. 206 | * 207 | * Example: 208 | * 209 | * columns = { 210 | * ... 211 | * { 212 | * ... 213 | * "formatter": StreamlitSlickGridFormatters.barFormatter, 214 | * 215 | * # Everything below is optional. 216 | * "params": { 217 | * # Supports everything numberFormatter does! 218 | * # In particular, don't forget to configure the colors. 219 | * } 220 | * } 221 | * } 222 | * 223 | */ 224 | barFormatter(row: any, cell: number, value: number, columnDef: Column, dataContext: Record, grid: SlickGrid) { 225 | return StreamlitSlickGridFormatters.stackedBarFormatter(row, cell, [value], columnDef, dataContext, grid) 226 | }, 227 | 228 | /** 229 | * Formats an array of numbers horizontally-stacked bar charts. The data should be in the format [number1, number2, ...]. 230 | * 231 | * Example: 232 | * 233 | * columns = { 234 | * ... 235 | * { 236 | * ... 237 | * "formatter": StreamlitSlickGridFormatters.stackedBarFormatter, 238 | * 239 | * # Everything below is optional. 240 | * "params": { 241 | * # Supports everything numberFormatter does! 242 | * # In particular, don't forget to configure the colors. 243 | * } 244 | * } 245 | * } 246 | * 247 | */ 248 | stackedBarFormatter(row: any, cell: number, values: number[], columnDef: Column, dataContext: Record, grid: SlickGrid) { 249 | if (!Array.isArray(values)) return "" 250 | 251 | const gridOptions = (grid && typeof grid.getOptions === "function" ? grid.getOptions() : {}) as GridOption 252 | const min = getValueFromParamsOrFormatterOptions("min", columnDef, gridOptions, 0) 253 | const max = getValueFromParamsOrFormatterOptions("max", columnDef, gridOptions, 100) 254 | const style: Record = getValueFromParamsOrFormatterOptions("style", columnDef, gridOptions, []) 255 | 256 | const container = createDomElement("div", { 257 | className: "progress", 258 | style: { 259 | gap: "1px" 260 | } 261 | }) 262 | 263 | for (let inputNumber of values) { 264 | const [fgColor, bgColor] = getColor(inputNumber, columnDef, grid) 265 | const inputPct = (inputNumber - min) / max * 100 266 | const formattedStr = decimalFormatter(row, cell, inputNumber, columnDef, dataContext, grid) as string 267 | 268 | container.appendChild( 269 | createDomElement("div", { 270 | className: "progress-bar", 271 | role: "progressbar", 272 | ariaValueNow: formattedStr, 273 | ariaValueMin: "0", 274 | ariaValueMax: "100", 275 | textContent: formattedStr, 276 | style: { 277 | minWidth: "2em", 278 | width: `${inputPct}%`, 279 | color: fgColor, 280 | backgroundColor: bgColor, 281 | ...style, 282 | }, 283 | }) 284 | ) 285 | } 286 | 287 | return container 288 | }, 289 | } 290 | 291 | const StreamlitSlickGridSorters = { 292 | numberArraySorter(a: number[], b: number[]): number { 293 | const sumA = a.reduce((x, y) => x + y, 0) 294 | const sumB = b.reduce((x, y) => x + y, 0) 295 | return sumA - sumB 296 | }, 297 | } 298 | 299 | function getColor(value: number, columnDef: Column, grid: SlickGrid): [string, string] | [] { 300 | const gridOptions = (grid && typeof grid.getOptions === "function" ? grid.getOptions() : {}) as GridOption 301 | const colorDefs: ColorDefs = getValueFromParamsOrFormatterOptions("colors", columnDef, gridOptions, []) 302 | 303 | for (let [v, fg, bg] of colorDefs) { 304 | if (!fg) fg = "unset" 305 | if (!bg) bg = "transparent" 306 | if (v == null) return [fg, bg] // null always wins. 307 | if (value <= v) return [fg, bg] 308 | } 309 | 310 | return [] 311 | } 312 | 313 | const MODULE_PROXIES = { 314 | "Formatters": Formatters, 315 | "FieldType": FieldType, 316 | "Filters": Filters, 317 | "OperatorType": OperatorType, 318 | "ExportServices": { 319 | "ExcelExportService": new ExcelExportService(), 320 | "TextExportService": new TextExportService(), 321 | }, 322 | "StreamlitSlickGridFormatters": StreamlitSlickGridFormatters, 323 | "StreamlitSlickGridSorters": StreamlitSlickGridSorters, 324 | } 325 | 326 | export default withStreamlitConnection(StreamlitSlickGrid) 327 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------