279 lines
11 KiB
Python
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
|