Implemented new InstancesDebugger.py. Based on the HierarchicalCanvasGraph.py

This commit is contained in:
2026-02-22 17:51:39 +01:00
parent 8b8172231a
commit 0686103a8f
8 changed files with 536 additions and 141 deletions

View File

@@ -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",