Skip to content

Commit 36eaf17

Browse files
committed
Add JS code injection feature
1 parent 68c28e0 commit 36eaf17

File tree

5 files changed

+459
-277
lines changed

5 files changed

+459
-277
lines changed

streamlit_condition_tree/__init__.py

Lines changed: 80 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2-
import streamlit.components.v1 as components
2+
33
import streamlit as st
4+
import streamlit.components.v1 as components
5+
from streamlit.components.v1.custom_component import MarshallComponentException
46

57
_RELEASE = False
68

@@ -14,7 +16,6 @@
1416
build_dir = os.path.join(parent_dir, "frontend/build")
1517
_component_func = components.declare_component("streamlit_condition_tree", path=build_dir)
1618

17-
1819
type_mapper = {
1920
'b': 'boolean',
2021
'i': 'number',
@@ -30,6 +31,54 @@
3031
}
3132

3233

34+
# stole from https://github.com/andfanilo/streamlit-echarts/blob/master/streamlit_echarts/frontend/src/utils.js
35+
# Thanks andfanilo
36+
class JsCode:
37+
def __init__(self, js_code: str):
38+
"""Wrapper around a js function to be injected on gridOptions.
39+
code is not checked at all.
40+
set allow_unsafe_jscode=True on AgGrid call to use it.
41+
Code is rebuilt on client using new Function Syntax (https://javascript.info/new-function)
42+
43+
Args:
44+
js_code (str): javascript function code as str
45+
"""
46+
import re
47+
match_js_comment_expression = r"\/\*[\s\S]*?\*\/|([^\\:]|^)\/\/.*$"
48+
js_code = re.sub(re.compile(match_js_comment_expression, re.MULTILINE), r"\1", js_code)
49+
50+
match_js_spaces = r"\s+(?=(?:[^\'\"]*[\'\"][^\'\"]*[\'\"])*[^\'\"]*$)"
51+
one_line_jscode = re.sub(match_js_spaces, " ", js_code, flags=re.MULTILINE)
52+
53+
js_placeholder = "::JSCODE::"
54+
one_line_jscode = re.sub(r"\s+|\r\s*|\n+", " ", js_code, flags=re.MULTILINE)
55+
56+
self.js_code = f"{js_placeholder}{one_line_jscode}{js_placeholder}"
57+
58+
59+
# Stole from https://github.com/PablocFonseca/streamlit-aggrid/blob/main/st_aggrid/shared.py
60+
# Thanks PablocFonseca
61+
def walk_config(config, func):
62+
"""Recursively walk config applying func at each leaf node
63+
64+
Args:
65+
config (dict): config dictionary
66+
func (callable): a function to apply at leaf nodes
67+
"""
68+
from collections.abc import Mapping
69+
70+
if isinstance(config, (Mapping, list)):
71+
for i, k in enumerate(config):
72+
73+
if isinstance(config[k], Mapping):
74+
walk_config(config[k], func)
75+
elif isinstance(config[k], list):
76+
for j in config[k]:
77+
walk_config(j, func)
78+
else:
79+
config[k] = func(config[k])
80+
81+
3382
def config_from_dataframe(dataframe):
3483
"""Return a basic configuration from dataframe columns"""
3584

@@ -58,7 +107,8 @@ def condition_tree(config: dict,
58107
min_height: int = 400,
59108
placeholder: str = '',
60109
always_show_buttons: bool = False,
61-
key: str = None):
110+
key: str = None,
111+
allow_unsafe_jscode: bool = False, ):
62112
"""Create a new instance of condition_tree.
63113
64114
Parameters
@@ -88,6 +138,9 @@ def condition_tree(config: dict,
88138
None, and the component's arguments are changed, the component will
89139
be re-mounted in the Streamlit frontend and lose its current state.
90140
Can also be used to access the condition tree through st.session_state.
141+
allow_unsafe_jscode: bool
142+
Allows jsCode to be injected in gridOptions.
143+
Defaults to False.
91144
92145
Returns
93146
-------
@@ -97,7 +150,7 @@ def condition_tree(config: dict,
97150
"""
98151

99152
if return_type == 'queryString':
100-
# Add backticks to fields with space in their name
153+
# Add backticks to fields having spaces in their name
101154
fields = {}
102155
for field_name, field_config in config['fields'].items():
103156
if ' ' in field_name:
@@ -106,19 +159,31 @@ def condition_tree(config: dict,
106159

107160
config['fields'] = fields
108161

109-
output_tree, component_value = _component_func(
110-
config=config,
111-
return_type=return_type,
112-
tree=tree,
113-
key='_' + key if key else None,
114-
min_height=min_height,
115-
placeholder=placeholder,
116-
always_show_buttons=always_show_buttons,
117-
default=['', '']
118-
)
162+
if allow_unsafe_jscode:
163+
walk_config(config, lambda v: v.js_code if isinstance(v, JsCode) else v)
164+
165+
try:
166+
output_tree, component_value = _component_func(
167+
config=config,
168+
return_type=return_type,
169+
tree=tree,
170+
key='_' + key if key else None,
171+
min_height=min_height,
172+
placeholder=placeholder,
173+
always_show_buttons=always_show_buttons,
174+
default=['', ''],
175+
allow_unsafe_jscode=allow_unsafe_jscode,
176+
)
177+
178+
except MarshallComponentException as e:
179+
# Uses a more complete error message.
180+
args = list(e.args)
181+
args[0] += ". If you're using custom JsCode objects on config, ensure that allow_unsafe_jscode is True."
182+
e = MarshallComponentException(*args)
183+
raise (e)
119184

120185
if return_type == 'queryString' and not component_value:
121-
# Default string that returns all the values in DataFrame.query
186+
# Default string that applies no filter in DataFrame.query
122187
component_value = 'index in index'
123188

124189
st.session_state[key] = output_tree

0 commit comments

Comments
 (0)