""" Split Flow Details Panel - Top/Bottom layout for flow and sub-flow details """ from textual.widget import Widget from textual.containers import Vertical from textual.widgets import Static from textual.reactive import reactive from rich.text import Text from rich.panel import Panel from rich.console import RenderableType, Group from rich.table import Table from typing import TYPE_CHECKING, Optional if TYPE_CHECKING: from ....models import FlowStats, FrameTypeStats class FlowMainDetailsPanel(Vertical): """Top panel showing main flow details""" DEFAULT_CSS = """ FlowMainDetailsPanel { height: 1fr; padding: 1; } FlowMainDetailsPanel Static { margin-bottom: 0; } """ def __init__(self, **kwargs): super().__init__(**kwargs) self.current_flow = None def compose(self): """Create the main flow details layout""" yield Static("Main Flow Details", classes="panel-header") yield Static( "Select a flow to view details", id="main-details-content" ) def update_flow(self, flow: Optional['FlowStats']) -> None: """Update panel with main flow details""" self.current_flow = flow content_widget = self.query_one("#main-details-content", Static) if not flow: content_widget.update("Select a flow to view details") return details = self._create_main_flow_details(flow) content_widget.update(details) def _create_main_flow_details(self, flow: 'FlowStats') -> RenderableType: """Create main flow details display""" sections = [] # Flow identification id_table = Table(show_header=False, box=None, padding=0) id_table.add_column(style="dim", width=12) id_table.add_column() id_table.add_row("Source:", f"{flow.src_ip}:{flow.src_port}") id_table.add_row("Destination:", f"{flow.dst_ip}:{flow.dst_port}") id_table.add_row("Protocol:", flow.transport_protocol) id_table.add_row("Packets:", f"{flow.frame_count:,}") id_table.add_row("Volume:", self._format_bytes(flow.total_bytes)) sections.append(Text("Flow Information", style="bold blue")) sections.append(id_table) # Enhanced analysis if flow.enhanced_analysis.decoder_type != "Standard": enhanced_table = Table(show_header=False, box=None, padding=0) enhanced_table.add_column(style="dim", width=12) enhanced_table.add_column() enhanced_table.add_row("Decoder:", flow.enhanced_analysis.decoder_type) enhanced_table.add_row("Quality:", f"{flow.enhanced_analysis.avg_frame_quality:.1f}%") enhanced_table.add_row("Fields:", str(flow.enhanced_analysis.field_count)) if flow.enhanced_analysis.frame_types: types_str = ", ".join(list(flow.enhanced_analysis.frame_types)[:3]) if len(flow.enhanced_analysis.frame_types) > 3: types_str += f" +{len(flow.enhanced_analysis.frame_types) - 3}" enhanced_table.add_row("Types:", types_str) sections.append(Text("Enhanced Analysis", style="bold green")) sections.append(enhanced_table) # Timing analysis with new columns timing_table = Table(show_header=False, box=None, padding=0) timing_table.add_column(style="dim", width=12) timing_table.add_column() timing_table.add_row("Duration:", f"{flow.duration:.2f}s") timing_table.add_row("Avg ΔT:", f"{flow.avg_inter_arrival * 1000:.1f}ms") timing_table.add_row("Std σ:", f"{flow.std_inter_arrival * 1000:.1f}ms") timing_table.add_row("Outliers:", f"{len(flow.outlier_frames)}") timing_table.add_row("Jitter:", f"{flow.jitter * 1000:.2f}ms") timing_table.add_row("First Seen:", self._format_timestamp(flow.first_seen)) timing_table.add_row("Last Seen:", self._format_timestamp(flow.last_seen)) sections.append(Text("Timing Analysis", style="bold cyan")) sections.append(timing_table) return Group(*sections) def _format_bytes(self, bytes_count: int) -> str: """Format byte count with units""" if bytes_count >= 1_000_000_000: return f"{bytes_count / 1_000_000_000:.2f} GB" elif bytes_count >= 1_000_000: return f"{bytes_count / 1_000_000:.2f} MB" elif bytes_count >= 1_000: return f"{bytes_count / 1_000:.2f} KB" else: return f"{bytes_count} B" def _format_timestamp(self, timestamp: float) -> str: """Format timestamp for display""" import datetime dt = datetime.datetime.fromtimestamp(timestamp) return dt.strftime("%H:%M:%S.%f")[:-3] class SubFlowDetailsPanel(Vertical): """Bottom panel showing sub-flow details or summary""" DEFAULT_CSS = """ SubFlowDetailsPanel { height: 1fr; padding: 1; } SubFlowDetailsPanel Static { margin-bottom: 0; } """ def __init__(self, **kwargs): super().__init__(**kwargs) self.current_flow = None self.selected_subflow = None def compose(self): """Create the sub-flow details layout""" yield Static("Sub-Flow Details", classes="panel-header") yield Static( "No sub-flow selected", id="sub-details-content" ) def update_flow(self, flow: Optional['FlowStats'], subflow_type: Optional[str] = None) -> None: """Update panel with sub-flow details or summary""" self.current_flow = flow self.selected_subflow = subflow_type content_widget = self.query_one("#sub-details-content", Static) if not flow: content_widget.update("No sub-flow selected") return if subflow_type and subflow_type in flow.frame_types: # Show specific sub-flow details details = self._create_subflow_details(flow, flow.frame_types[subflow_type]) else: # Show summary of all sub-flows details = self._create_subflow_summary(flow) content_widget.update(details) def _create_subflow_details(self, flow: 'FlowStats', subflow: 'FrameTypeStats') -> RenderableType: """Create specific sub-flow details""" sections = [] # Sub-flow identification id_table = Table(show_header=False, box=None, padding=0) id_table.add_column(style="dim", width=12) id_table.add_column() id_table.add_row("Frame Type:", subflow.frame_type) id_table.add_row("Packets:", f"{subflow.count:,}") id_table.add_row("Volume:", self._format_bytes(subflow.total_bytes)) percentage = (subflow.count / flow.frame_count * 100) if flow.frame_count > 0 else 0 id_table.add_row("% of Flow:", f"{percentage:.1f}%") sections.append(Text("Sub-Flow Information", style="bold yellow")) sections.append(id_table) # Sub-flow timing timing_table = Table(show_header=False, box=None, padding=0) timing_table.add_column(style="dim", width=12) timing_table.add_column() timing_table.add_row("Avg ΔT:", f"{subflow.avg_inter_arrival * 1000:.1f}ms" if subflow.avg_inter_arrival > 0 else "N/A") timing_table.add_row("Std σ:", f"{subflow.std_inter_arrival * 1000:.1f}ms" if subflow.std_inter_arrival > 0 else "N/A") timing_table.add_row("Outliers:", f"{len(subflow.outlier_frames)}") sections.append(Text("Sub-Flow Timing", style="bold cyan")) sections.append(timing_table) # Outlier details if any if subflow.outlier_frames and subflow.outlier_details: outlier_table = Table(show_header=True, box=None) outlier_table.add_column("Frame#", justify="right") outlier_table.add_column("ΔT(ms)", justify="right") for frame_num, delta_t in subflow.outlier_details[:5]: # Show first 5 outliers outlier_table.add_row( str(frame_num), f"{delta_t * 1000:.1f}" ) if len(subflow.outlier_details) > 5: outlier_table.add_row("...", f"+{len(subflow.outlier_details) - 5} more") sections.append(Text("Outlier Details", style="bold red")) sections.append(outlier_table) return Group(*sections) def _create_subflow_summary(self, flow: 'FlowStats') -> RenderableType: """Create summary of all sub-flows""" if not flow.frame_types or len(flow.frame_types) <= 1: return Text("No sub-flows available", style="dim") sections = [] sections.append(Text("Sub-Flow Summary", style="bold yellow")) # Frame type breakdown table frame_table = Table(show_header=True, box=None) frame_table.add_column("Frame Type", style="blue") frame_table.add_column("Count", justify="right") frame_table.add_column("%", justify="right", style="dim") frame_table.add_column("ΔT(ms)", justify="right", style="cyan") frame_table.add_column("σ(ms)", justify="right", style="cyan") frame_table.add_column("Out", justify="right", style="red") total = flow.frame_count for frame_type, stats in sorted( flow.frame_types.items(), key=lambda x: x[1].count, reverse=True ): percentage = (stats.count / total * 100) if total > 0 else 0 delta_t = f"{stats.avg_inter_arrival * 1000:.1f}" if stats.avg_inter_arrival > 0 else "N/A" sigma = f"{stats.std_inter_arrival * 1000:.1f}" if stats.std_inter_arrival > 0 else "N/A" outliers = str(len(stats.outlier_frames)) frame_table.add_row( frame_type[:15], f"{stats.count:,}", f"{percentage:.1f}%", delta_t, sigma, outliers ) sections.append(frame_table) return Group(*sections) def _format_bytes(self, bytes_count: int) -> str: """Format byte count with units""" if bytes_count >= 1_000_000_000: return f"{bytes_count / 1_000_000_000:.2f} GB" elif bytes_count >= 1_000_000: return f"{bytes_count / 1_000_000:.2f} MB" elif bytes_count >= 1_000: return f"{bytes_count / 1_000:.2f} KB" else: return f"{bytes_count} B" class SplitFlowDetailsPanel(Vertical): """Combined panel with top/bottom split for flow and sub-flow details""" DEFAULT_CSS = """ SplitFlowDetailsPanel { height: 1fr; padding: 0; } SplitFlowDetailsPanel > FlowMainDetailsPanel { height: 3fr; } SplitFlowDetailsPanel > SubFlowDetailsPanel { height: 2fr; } """ def __init__(self, **kwargs): super().__init__(**kwargs) self.current_flow = None def compose(self): """Create the split layout""" yield FlowMainDetailsPanel(id="main-flow-details") yield SubFlowDetailsPanel(id="sub-flow-details") def update_flow(self, flow: Optional['FlowStats'], subflow_type: Optional[str] = None) -> None: """Update both panels with flow data""" self.current_flow = flow # Update main flow details main_panel = self.query_one("#main-flow-details", FlowMainDetailsPanel) main_panel.update_flow(flow) # Update sub-flow details sub_panel = self.query_one("#sub-flow-details", SubFlowDetailsPanel) sub_panel.update_flow(flow, subflow_type)