""" Tabbed Flow View Widget - Shows Overview + Frame Type specific tabs """ from textual.widgets import TabbedContent, TabPane, DataTable, Static from textual.containers import Vertical, Horizontal from textual.reactive import reactive from typing import TYPE_CHECKING, Dict, List, Optional, Set from rich.text import Text from rich.table import Table from rich.panel import Panel from .flow_table_v2 import EnhancedFlowTable if TYPE_CHECKING: from ....analysis.core import EthernetAnalyzer from ....models import FlowStats class FrameTypeFlowTable(DataTable): """Flow table filtered for a specific frame type""" def __init__(self, frame_type: str, **kwargs): super().__init__(**kwargs) self.frame_type = frame_type self.cursor_type = "row" self.zebra_stripes = True self.show_header = True self.show_row_labels = False def setup_columns(self): """Setup columns for frame-type specific view""" self.add_column("Flow", width=4, key="flow_id") self.add_column("Source IP", width=16, key="src_ip") self.add_column("Src Port", width=8, key="src_port") self.add_column("Dest IP", width=16, key="dst_ip") self.add_column("Dst Port", width=8, key="dst_port") self.add_column("Protocol", width=8, key="protocol") self.add_column("Packets", width=8, key="packets") self.add_column("Avg ΔT", width=10, key="avg_delta") self.add_column("Std ΔT", width=10, key="std_delta") self.add_column("Outliers", width=8, key="outliers") self.add_column("Quality", width=8, key="quality") class FrameTypeStatsPanel(Static): """Statistics panel for a specific frame type""" def __init__(self, frame_type: str, **kwargs): super().__init__(**kwargs) self.frame_type = frame_type self._stats_content = f"Statistics for {self.frame_type}\n\nNo data available yet." def render(self): """Render frame type statistics""" return Panel( self._stats_content, title=f"📊 {self.frame_type} Statistics", border_style="blue" ) def update_content(self, content: str): """Update the statistics content""" self._stats_content = content self.refresh() class FrameTypeTabContent(Vertical): """Content for a specific frame type tab""" def __init__(self, frame_type: str, analyzer: 'EthernetAnalyzer', **kwargs): super().__init__(**kwargs) self.frame_type = frame_type self.analyzer = analyzer def compose(self): """Compose the frame type tab content""" with Horizontal(): # Left side - Flow table for this frame type (sanitize ID) table_id = f"table-{self.frame_type.replace('-', '_').replace(':', '_')}" yield FrameTypeFlowTable(self.frame_type, id=table_id) # Right side - Frame type statistics (sanitize ID) stats_id = f"stats-{self.frame_type.replace('-', '_').replace(':', '_')}" yield FrameTypeStatsPanel(self.frame_type, id=stats_id) def on_mount(self): """Initialize the frame type tab""" table_id = f"#table-{self.frame_type.replace('-', '_').replace(':', '_')}" table = self.query_one(table_id, FrameTypeFlowTable) table.setup_columns() self.refresh_data() def refresh_data(self): """Refresh data for this frame type""" try: table_id = f"#table-{self.frame_type.replace('-', '_').replace(':', '_')}" table = self.query_one(table_id, FrameTypeFlowTable) # Clear existing data table.clear() # Get flows that have this frame type flows_with_frametype = [] flow_list = list(self.analyzer.flows.values()) for i, flow in enumerate(flow_list): if self.frame_type in flow.frame_types: ft_stats = flow.frame_types[self.frame_type] flows_with_frametype.append((i, flow, ft_stats)) # Add rows for flows with this frame type for flow_idx, flow, ft_stats in flows_with_frametype: # Calculate quality score quality_score = self._calculate_quality_score(ft_stats) quality_text = self._format_quality(quality_score) # Format timing statistics avg_delta = f"{ft_stats.avg_inter_arrival * 1000:.1f}ms" if ft_stats.avg_inter_arrival > 0 else "N/A" std_delta = f"{ft_stats.std_inter_arrival * 1000:.1f}ms" if ft_stats.std_inter_arrival > 0 else "N/A" row_data = [ str(flow_idx + 1), # Flow ID flow.src_ip, str(flow.src_port), flow.dst_ip, str(flow.dst_port), flow.transport_protocol, str(ft_stats.count), avg_delta, std_delta, str(len(ft_stats.outlier_frames)), quality_text ] table.add_row(*row_data, key=f"flow-{flow_idx}") # Update statistics panel self._update_stats_panel(flows_with_frametype) except Exception as e: # Handle case where widgets aren't ready yet pass def _calculate_quality_score(self, ft_stats) -> float: """Calculate quality score for frame type stats""" if ft_stats.count == 0: return 0.0 # Base score on outlier rate and timing consistency outlier_rate = len(ft_stats.outlier_frames) / ft_stats.count consistency = 1.0 - min(outlier_rate * 2, 1.0) # Lower outlier rate = higher consistency return consistency * 100 def _format_quality(self, quality_score: float) -> Text: """Format quality score with color coding""" if quality_score >= 90: return Text(f"{quality_score:.0f}%", style="green") elif quality_score >= 70: return Text(f"{quality_score:.0f}%", style="yellow") else: return Text(f"{quality_score:.0f}%", style="red") def _update_stats_panel(self, flows_with_frametype): """Update the statistics panel with current data""" try: stats_id = f"#stats-{self.frame_type.replace('-', '_').replace(':', '_')}" stats_panel = self.query_one(stats_id, FrameTypeStatsPanel) if not flows_with_frametype: stats_content = f"No flows found with {self.frame_type} frames" else: # Calculate aggregate statistics total_flows = len(flows_with_frametype) total_packets = sum(ft_stats.count for _, _, ft_stats in flows_with_frametype) total_outliers = sum(len(ft_stats.outlier_frames) for _, _, ft_stats in flows_with_frametype) # Calculate average timing avg_timings = [ft_stats.avg_inter_arrival for _, _, ft_stats in flows_with_frametype if ft_stats.avg_inter_arrival > 0] overall_avg = sum(avg_timings) / len(avg_timings) if avg_timings else 0 # Format statistics stats_content = f"""Flows: {total_flows} Total Packets: {total_packets:,} Total Outliers: {total_outliers} Outlier Rate: {(total_outliers/total_packets*100):.1f}% Avg Inter-arrival: {overall_avg*1000:.1f}ms""" # Update the panel content using the new method stats_panel.update_content(stats_content) except Exception as e: pass class TabbedFlowView(TabbedContent): """Tabbed view showing Overview + Frame Type specific tabs""" active_frame_types = reactive(set()) def __init__(self, analyzer: 'EthernetAnalyzer', **kwargs): super().__init__(**kwargs) self.analyzer = analyzer self.overview_table = None self.frame_type_tabs = {} def compose(self): """Create the tabbed interface""" # Overview tab (always present) with TabPane("Overview", id="tab-overview"): self.overview_table = EnhancedFlowTable(self.analyzer, id="overview-flow-table") yield self.overview_table # Create tabs for common frame types (based on detection analysis) common_frame_types = ["CH10-Data", "CH10-ACTTS", "TMATS", "PTP-Sync", "PTP-Signaling", "UDP", "IGMP"] for frame_type in common_frame_types: tab_id = f"tab-{frame_type.lower().replace('-', '_').replace(':', '_')}" content_id = f"content-{frame_type.replace('-', '_').replace(':', '_')}" with TabPane(frame_type, id=tab_id): tab_content = FrameTypeTabContent(frame_type, self.analyzer, id=content_id) self.frame_type_tabs[frame_type] = tab_content yield tab_content def _create_frame_type_tabs(self): """Create tabs for detected frame types""" frame_types = self._get_detected_frame_types() for frame_type in sorted(frame_types): tab_id = f"tab-{frame_type.lower().replace('-', '_').replace(':', '_')}" with TabPane(frame_type, id=tab_id): tab_content = FrameTypeTabContent(frame_type, self.analyzer, id=f"content-{frame_type}") self.frame_type_tabs[frame_type] = tab_content yield tab_content def _get_detected_frame_types(self) -> Set[str]: """Get all detected frame types from current flows""" frame_types = set() for flow in self.analyzer.flows.values(): frame_types.update(flow.frame_types.keys()) return frame_types def on_mount(self): """Initialize tabs""" self.refresh_all_tabs() def refresh_all_tabs(self): """Refresh data in all tabs""" # Refresh overview tab if self.overview_table: self.overview_table.refresh_data() # Get detected frame types detected_frame_types = self._get_detected_frame_types() # Refresh frame type tabs that have data for frame_type, tab_content in self.frame_type_tabs.items(): if frame_type in detected_frame_types: tab_content.refresh_data() # Tab has data, it will show content when selected pass else: # Tab has no data, it will show empty when selected pass def update_tabs(self): """Update tabs based on newly detected frame types""" current_frame_types = self._get_detected_frame_types() # Check if we need to add new tabs new_frame_types = current_frame_types - self.active_frame_types if new_frame_types: # This would require rebuilding the widget # For now, just refresh existing tabs self.refresh_all_tabs() self.active_frame_types = current_frame_types