Files
StreamLens/analyzer/tui/textual/widgets/tabbed_flow_view.py

279 lines
11 KiB
Python

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