Implemented new InstancesDebugger.py. Based on the HierarchicalCanvasGraph.py
This commit is contained in:
@@ -7,6 +7,7 @@ from fasthtml.components import Div
|
||||
from fasthtml.xtend import Script
|
||||
|
||||
from myfasthtml.controls.BaseCommands import BaseCommands
|
||||
from myfasthtml.controls.Query import Query, QueryConf
|
||||
from myfasthtml.core.commands import Command
|
||||
from myfasthtml.core.dbmanager import DbObject
|
||||
from myfasthtml.core.instances import MultipleInstance
|
||||
@@ -47,6 +48,11 @@ class HierarchicalCanvasGraphState(DbObject):
|
||||
# Persisted: layout orientation ('horizontal' or 'vertical')
|
||||
self.layout_mode: str = 'horizontal'
|
||||
|
||||
# Persisted: filter state
|
||||
self.filter_text: Optional[str] = None # Text search filter
|
||||
self.filter_type: Optional[str] = None # Type filter (badge click)
|
||||
self.filter_kind: Optional[str] = None # Kind filter (border click)
|
||||
|
||||
# Not persisted: current selection (ephemeral)
|
||||
self.ns_selected_id: Optional[str] = None
|
||||
|
||||
@@ -65,6 +71,19 @@ class Commands(BaseCommands):
|
||||
self._owner,
|
||||
self._owner._handle_update_view_state
|
||||
).htmx(target=f"#{self._id}", swap='none')
|
||||
|
||||
def apply_filter(self):
|
||||
"""Apply current filter and update the graph display.
|
||||
|
||||
This command is called when the filter changes (search text, type, or kind).
|
||||
"""
|
||||
return Command(
|
||||
"ApplyFilter",
|
||||
"Apply filter to graph",
|
||||
self._owner,
|
||||
self._owner._handle_apply_filter,
|
||||
key="#{id}-apply-filter",
|
||||
).htmx(target=f"#{self._id}")
|
||||
|
||||
|
||||
class HierarchicalCanvasGraph(MultipleInstance):
|
||||
@@ -100,6 +119,11 @@ class HierarchicalCanvasGraph(MultipleInstance):
|
||||
self._state = HierarchicalCanvasGraphState(self)
|
||||
self.commands = Commands(self)
|
||||
|
||||
# Add Query component for filtering
|
||||
self._query = Query(self, QueryConf(placeholder="Filter instances..."), _id="-query")
|
||||
self._query.bind_command("QueryChanged", self.commands.apply_filter())
|
||||
self._query.bind_command("CancelQuery", self.commands.apply_filter())
|
||||
|
||||
logger.debug(f"HierarchicalCanvasGraph created with id={self._id}, "
|
||||
f"nodes={len(conf.nodes)}, edges={len(conf.edges)}")
|
||||
|
||||
@@ -148,25 +172,102 @@ class HierarchicalCanvasGraph(MultipleInstance):
|
||||
self._state.collapsed = list(collapsed_set)
|
||||
return self
|
||||
|
||||
def _handle_update_view_state(self, event_data: dict):
|
||||
def _handle_update_view_state(self, transform=None, layout_mode=None):
|
||||
"""Internal handler to update view state from client.
|
||||
|
||||
Args:
|
||||
event_data: Dictionary with 'transform' and/or 'layout_mode' keys
|
||||
transform: Optional dict with zoom/pan transform state
|
||||
layout_mode: Optional string with layout orientation
|
||||
|
||||
Returns:
|
||||
str: Empty string (no UI update needed)
|
||||
"""
|
||||
if 'transform' in event_data:
|
||||
self._state.transform = event_data['transform']
|
||||
if transform is not None:
|
||||
self._state.transform = transform
|
||||
logger.debug(f"Transform updated: {self._state.transform}")
|
||||
|
||||
if 'layout_mode' in event_data:
|
||||
self._state.layout_mode = event_data['layout_mode']
|
||||
if layout_mode is not None:
|
||||
self._state.layout_mode = layout_mode
|
||||
logger.debug(f"Layout mode updated: {self._state.layout_mode}")
|
||||
|
||||
return ""
|
||||
|
||||
def _handle_apply_filter(self, query_param="text", value=None):
|
||||
"""Internal handler to apply filter and re-render the graph.
|
||||
|
||||
Args:
|
||||
query_param: Type of filter - "text", "type", or "kind"
|
||||
value: The filter value (type name or kind name). Toggles off if same value clicked again.
|
||||
|
||||
Returns:
|
||||
self: For HTMX to render the updated graph
|
||||
"""
|
||||
# Save old values to detect toggle
|
||||
old_filter_type = self._state.filter_type
|
||||
old_filter_kind = self._state.filter_kind
|
||||
|
||||
# Reset all filters
|
||||
self._state.filter_text = None
|
||||
self._state.filter_type = None
|
||||
self._state.filter_kind = None
|
||||
|
||||
# Apply the requested filter
|
||||
if query_param == "text":
|
||||
# Text filter from Query component
|
||||
self._state.filter_text = self._query.get_query()
|
||||
|
||||
elif query_param == "type":
|
||||
# Type filter from badge click - toggle if same type clicked again
|
||||
if old_filter_type != value:
|
||||
self._state.filter_type = value
|
||||
|
||||
elif query_param == "kind":
|
||||
# Kind filter from border click - toggle if same kind clicked again
|
||||
if old_filter_kind != value:
|
||||
self._state.filter_kind = value
|
||||
|
||||
logger.debug(f"Applying filter: query_param={query_param}, value={value}, "
|
||||
f"text={self._state.filter_text}, type={self._state.filter_type}, kind={self._state.filter_kind}")
|
||||
|
||||
return self
|
||||
|
||||
def _calculate_filtered_nodes(self) -> Optional[list[str]]:
|
||||
"""Calculate which node IDs match the current filter criteria.
|
||||
|
||||
Returns:
|
||||
Optional[list[str]]:
|
||||
- None: No filter is active (all nodes visible, nothing dimmed)
|
||||
- []: Filter active but no matches (all nodes dimmed)
|
||||
- [ids]: Filter active with matches (only these nodes visible)
|
||||
"""
|
||||
# If no filters are active, return None (no filtering)
|
||||
if not self._state.filter_text and not self._state.filter_type and not self._state.filter_kind:
|
||||
return None
|
||||
|
||||
filtered_ids = []
|
||||
for node in self.conf.nodes:
|
||||
matches = True
|
||||
|
||||
# Check text filter (searches in id, label, type, kind)
|
||||
if self._state.filter_text:
|
||||
search_text = self._state.filter_text.lower()
|
||||
searchable = f"{node.get('id', '')} {node.get('label', '')} {node.get('type', '')} {node.get('kind', '')}".lower()
|
||||
if search_text not in searchable:
|
||||
matches = False
|
||||
|
||||
# Check type filter
|
||||
if self._state.filter_type and node.get('type') != self._state.filter_type:
|
||||
matches = False
|
||||
|
||||
# Check kind filter
|
||||
if self._state.filter_kind and node.get('kind') != self._state.filter_kind:
|
||||
matches = False
|
||||
|
||||
if matches:
|
||||
filtered_ids.append(node['id'])
|
||||
|
||||
return filtered_ids
|
||||
|
||||
def _prepare_options(self) -> dict:
|
||||
"""Prepare JavaScript options object.
|
||||
|
||||
@@ -179,17 +280,25 @@ class HierarchicalCanvasGraph(MultipleInstance):
|
||||
# Add internal handler for view state persistence
|
||||
events['_internal_update_state'] = self.commands.update_view_state().ajax_htmx_options()
|
||||
|
||||
# Add internal handlers for filtering by type and kind (badge/border clicks)
|
||||
events['_internal_filter_by_type'] = self.commands.apply_filter().ajax_htmx_options()
|
||||
events['_internal_filter_by_kind'] = self.commands.apply_filter().ajax_htmx_options()
|
||||
|
||||
# Add user-provided event handlers
|
||||
if self.conf.events_handlers:
|
||||
for event_name, command in self.conf.events_handlers.items():
|
||||
events[event_name] = command.ajax_htmx_options()
|
||||
|
||||
# Calculate filtered nodes
|
||||
filtered_nodes = self._calculate_filtered_nodes()
|
||||
|
||||
return {
|
||||
"nodes": self.conf.nodes,
|
||||
"edges": self.conf.edges,
|
||||
"collapsed": self._state.collapsed,
|
||||
"transform": self._state.transform,
|
||||
"layout_mode": self._state.layout_mode,
|
||||
"filtered_nodes": filtered_nodes,
|
||||
"events": events
|
||||
}
|
||||
|
||||
@@ -203,6 +312,9 @@ class HierarchicalCanvasGraph(MultipleInstance):
|
||||
options_json = json.dumps(options, indent=2)
|
||||
|
||||
return Div(
|
||||
# Query filter bar
|
||||
self._query,
|
||||
|
||||
# Canvas element (sized by JS to fill container)
|
||||
Div(
|
||||
id=f"{self._id}_container",
|
||||
|
||||
Reference in New Issue
Block a user