Creating a widget for running SPL (Splunk) queries and mo.state #5402
-
I'm building a custom widget to execute Splunk Processing Language (SPL) queries. So, essentially it's code editor that allows the user to:
The main challenge I'm facing is state management. When users are working interactively with SPL queries, and they re-run a cell containing the widget, it gets recreated and loses its state - query text and table names get reset, breaking the workflow. I'm currently using marimo's state functionality to persist widget state across cell re-executions with mo.state(), which lets the widget maintain query content and settings even when cells are re-run. The front-end is just using codemirror. class SplunkAPI:
"""Simple Splunk widget manager"""
def __init__(self, token, host="localhost", port=8089, autoLogin=False, con=None):
# intentionally leak strings to avoid race condition where __del__
# cleanup destroys state of newly created widgets with same ID
self._widget_states = {}
# store the ibis connection - create default if none provided
if con is None:
import ibis
import duckdb
self.con = ibis.duckdb.from_connection(duckdb)
else:
self.con = con
# connect to splunk
self.service = client.connect(
host=host, port=port, splunkToken=token, autologin=autoLogin
)
print(f"[SPL] created splunkapi connected to {host}:{port}")
def spl(self, **kwargs):
"""Create a new SPL editor widget"""
return SPLWidget(context=self, **kwargs)
class SPLWidget(anywidget.AnyWidget):
_esm = pathlib.Path(__file__).parent / "static" / "widget.js"
_css = pathlib.Path(__file__).parent / "static" / "widget.css"
query = traitlets.Unicode("").tag(sync=True)
table_name = traitlets.Unicode("").tag(sync=True)
def __init__(self, context: SplunkAPI, **kwargs):
# get the variable name as widget ID from marimo cell definitions
# mo.defs() returns a list of variable names assigned in the current cell
# this gives us a unique identifier for state persistence across cell re-runs
cell_defs = mo.defs()
if not cell_defs:
# this fails when the widget isn't assigned to a variable
# e.g., just calling splunk.spl() without assignment
raise ValueError(
"Widget must be assigned to a variable (e.g., 'my_widget = splunk.spl()')"
)
self.context = context
self.widget_id = cell_defs[0] # use the first (and usually only) variable name
print(f"[SPL] creating widget with ID: {self.widget_id}")
# create or get existing state for both query and table_name
# if widget_id already exists, we reuse the same marimo state
# this allows state persistence when cells are re-run
if self.widget_id not in self.context._widget_states:
print(f"[SPL] creating new state for {self.widget_id}")
get_query, set_query = mo.state("")
get_table_name, set_table_name = mo.state("")
self.context._widget_states[self.widget_id] = {
"query": (get_query, set_query),
"table_name": (get_table_name, set_table_name),
}
else:
print(f"[SPL] reusing existing state for {self.widget_id}")
current_query = self._get_state_value("query")
current_table_name = self._get_state_value("table_name")
print(
f"[SPL] initializing with query: '{current_query}', table_name: '{current_table_name}'"
)
super().__init__(query=current_query, table_name=current_table_name, **kwargs)
self.observe(self._on_query_change, names=["query"])
self.observe(self._on_table_name_change, names=["table_name"])
# handle messages from frontend
self.on_msg(self._handle_frontend_msg)
def _on_query_change(self, change):
"""Handle query changes with logging"""
print(f"[SPL] query changed for {self.widget_id}: '{change['new']}'")
self._set_state_value("query", change["new"])
def _on_table_name_change(self, change):
"""Handle table_name changes with logging"""
print(f"[SPL] table name changed for {self.widget_id}: '{change['new']}'")
self._set_state_value("table_name", change["new"])
def search(self):
"""Execute search and create view"""
if not self.query.strip():
print(f"[SPL] no query to execute for {self.widget_id}")
return None
print(f"[SPL] executing search for {self.widget_id}: {self.query}")
search_results = self.context.service.jobs.oneshot(
self.query, output_mode="json"
)
reader = results.JSONResultsReader(search_results)
data = pa.Table.from_pylist(
[result for result in reader if isinstance(result, dict)]
)
print(f"[SPL] creating table '{self.table_name}' with {len(data)} rows")
return self.context.con.create_table(self.table_name, data, overwrite=True)
def _get_state(self, key):
"""Get state getter/setter tuple for a key"""
return self.context._widget_states[self.widget_id][key]
def _set_state_value(self, key, value):
"""Set a state value using the setter function"""
_, setter = self._get_state(key)
setter(value)
def _get_state_value(self, key):
"""Get current state value using the getter function"""
getter, _ = self._get_state(key)
return getter()
def _handle_frontend_msg(self, widget, content, buffers):
"""Handle messages from the frontend"""
if content.get("type") == "execute_search":
print(f"[SPL] execute button clicked for {self.widget_id}")
table_name = self.table_name.strip()
if table_name:
try:
result = self.search()
print(
f"[SPL] search executed successfully, created table: {table_name}"
)
except Exception as e:
print(f"[SPL] error executing search: {e}")
else:
print(f"[SPL] no table name provided for {self.widget_id}") Would love any feedback on this approach! 🙏🏻 |
Beta Was this translation helpful? Give feedback.
Replies: 1 comment 1 reply
-
You could use localstorage on the JS side to persist per user/ app. State doesn't work well unless exposed as a toplevel variable. This works just by virtue of having hidden object references (even without state)- refactoring without state should make things a little cleaner Another option is using But cool! Feedback on DevX is appreciated |
Beta Was this translation helpful? Give feedback.
You could use localstorage on the JS side to persist per user/ app.
State doesn't work well unless exposed as a toplevel variable. This works just by virtue of having hidden object references (even without state)- refactoring without state should make things a little cleaner
Another option is using
mo.watch.file
, which could save the user query if the queries are meant to be relatively persistent.But cool! Feedback on DevX is appreciated