├── 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 | [](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 |
--------------------------------------------------------------------------------