diff --git a/lumen/ai/views.py b/lumen/ai/views.py index eb1972122..97d6ea43c 100644 --- a/lumen/ai/views.py +++ b/lumen/ai/views.py @@ -22,12 +22,13 @@ from panel.widgets import CodeEditor from panel_gwalker import GraphicWalker from panel_material_ui import ( - Alert, Button, Checkbox, CircularProgress, Column, FileDownload, - MenuButton, + Alert, Button, Checkbox, CircularProgress, Column, FileDownload, FlexBox, + MenuButton, MenuToggle, ) from ..base import Component from ..config import dump_yaml, load_yaml +from ..filters import WidgetFilter from ..pipeline import Pipeline from ..transforms.sql import SQLLimit from ..views.base import Panel, Table, View @@ -489,6 +490,61 @@ def export(self, fmt: str) -> str | bytes: sio.seek(0) return sio + def _render_editor(self): + editor = super()._render_editor() + self._filters = {} + self._filter_area = FlexBox(width_policy="max") + editor.insert(0, self._filter_area) + return editor + + def _add_filter(self, item): + field = item["label"] + if item["toggled"]: + if field in self._filters: + filt = self._filters[field] + else: + self._filters[field] = filt = WidgetFilter(field=field, schema=self.component.schema) + self._filter_area.append(filt.widget) + self.component.add_filter(filt) + return + removed_filters = [filt for filt in self.component.filters if filt.field == field] + removed_widgets = [filt.widget for filt in removed_filters] + self._filter_area[:] = [w for w in self._filter_area if w not in removed_widgets] + self.component.filters = [filt for filt in self.component.filters if filt not in removed_filters] + + def render_controls(self, task: Task, interface: ChatFeed): + controls = super().render_controls(task, interface) + items = [] + for col in self.component.data.columns: + if col not in self.component.schema: + continue + col_schema = self.component.schema[col] + col_type = col_schema["type"] + if col_type == "string": + if "enum" in col_schema: + icon = "format_list_bulleted" + elif col_schema.get("format") == "datetime": + icon = "calendar_month" + else: + icon = "text_fields" + elif col_type == "number": + icon = "calculate" + elif col_type == "integer": + icon = "numbers" + else: + icon = "help" + items.append({"label": col, "icon": icon}) + filter_controls = MenuToggle( + items=items, + label="Add Filter", + icon="filter_list", + margin=0, + variant="text", + on_click=self._add_filter + ) + controls.insert(1, filter_controls) + return controls + def render_explorer(self): return GraphicWalker( self.component.param.data, diff --git a/lumen/filters/base.py b/lumen/filters/base.py index 0dc47805e..5d6d9615a 100644 --- a/lumen/filters/base.py +++ b/lumen/filters/base.py @@ -327,15 +327,17 @@ def __init__(self, **params): 'type would be more sensible or raise the max_options. ' ) self.widget.options = options - self.widget.name = self.label - self.widget.visible = self.visible - self.widget.disabled = self.disabled + self.widget.param.update( + disabled=self.disabled, + label=self.label or self.field, + visible=self.visible + ) val = self.value self.widget.link(self, bidirectional=True, value='value', visible='visible', disabled='disabled') if val is not None: - self.widget.value = val + self.widget.value = self.widget.param.value.deserialize(val) elif self.default is not None: - self.widget.value = self.default + self.widget.value = self.widget.param.value.deserialize(self.default) self._setup_sync() @classmethod diff --git a/lumen/schema.py b/lumen/schema.py index 5a98413a2..f9c2356f3 100644 --- a/lumen/schema.py +++ b/lumen/schema.py @@ -2,6 +2,12 @@ import panel as pn import param # type: ignore +from panel_material_ui import ( + Checkbox, DatePicker, DateRangeSlider, DatetimeInput, DatetimePicker, + FloatInput, FloatSlider, IntInput, IntRangeSlider, IntSlider, LiteralInput, + MultiChoice, RangeSlider, Select, TextInput, +) + from .util import resolve_module_reference @@ -30,33 +36,33 @@ class JSONSchema(pn.pane.PaneBase): _unpack = True - _select_widget = pn.widgets.Select - _multi_select_widget = pn.widgets.MultiSelect - _bounded_number_widget = pn.widgets.FloatSlider - _bounded_number_range_widget = pn.widgets.RangeSlider - _unbounded_number_widget = pn.widgets.FloatInput - _list_select_widget = pn.widgets.MultiSelect - _bounded_int_widget = pn.widgets.IntSlider - _bounded_int_range_widget = pn.widgets.IntRangeSlider - _unbounded_int_widget = pn.widgets.IntInput - _string_widget = pn.widgets.TextInput - _boolean_widget = pn.widgets.Checkbox - _literal_widget = pn.widgets.LiteralInput - _unbounded_date_widget = pn.widgets.DatePicker - _date_range_widget = pn.widgets.DateRangeSlider + _select_widget = Select + _multi_select_widget = MultiChoice + _bounded_number_widget = FloatSlider + _bounded_number_range_widget = RangeSlider + _unbounded_number_widget = FloatInput + _list_select_widget = MultiChoice + _bounded_int_widget = IntSlider + _bounded_int_range_widget = IntRangeSlider + _unbounded_int_widget = IntInput + _string_widget = TextInput + _boolean_widget = Checkbox + _literal_widget = LiteralInput + _unbounded_date_widget = DatePicker + _date_range_widget = DateRangeSlider # Not available until Panel 0.11 try: _datetime_range_widget = pn.widgets.DatetimeRangeInput except Exception: - _datetime_range_widget = pn.widgets.DateRangeSlider + _datetime_range_widget = DateRangeSlider # Not available until Panel 0.12 try: - _unbounded_datetime_widget = pn.widgets.DatetimePicker + _unbounded_datetime_widget = DatetimePicker _datetime_range_widget = pn.widgets.DatetimeRangePicker except Exception: - _unbounded_datetime_widget = pn.widgets.DatetimeInput + _unbounded_datetime_widget = DatetimeInput def _array_type(self, schema): if 'items' in schema and not schema.get('additionalItems', True): @@ -156,7 +162,7 @@ def _update_widgets_from_schema(self): values = {} if self.object is None else self.object widgets = [] for p, schema in self.schema.items(): - if self.properties and p not in self.properties: + if p == '__len__' or self.properties and p not in self.properties: continue for prop in self._precedence: if prop in schema: @@ -177,7 +183,7 @@ def _update_widgets_from_schema(self): if isinstance(wtype, str): wtype = resolve_module_reference(wtype, pn.widgets.Widget) - if isinstance(wtype, pn.widgets.Widget): + if isinstance(wtype, pn.widgets.WidgetBase): widget = wtype else: if p in values: diff --git a/lumen/transforms/sql.py b/lumen/transforms/sql.py index dc0859d09..ab5649112 100644 --- a/lumen/transforms/sql.py +++ b/lumen/transforms/sql.py @@ -563,7 +563,8 @@ def _build_filter_conditions(self, conditions: list) -> list: v2_str = str(v2) range_filters.append(column_expr.between(SQLLiteral.string(v1_str), SQLLiteral.string(v2_str))) - filters.append(or_(*range_filters)) + if range_filters: + filters.append(or_(*range_filters)) else: # Handle None values separately in lists non_none_values = [v for v in val if v is not None]