progress?
This commit is contained in:
@@ -6,7 +6,7 @@ from textual.widgets import DataTable
|
||||
from textual.containers import Vertical
|
||||
from textual.reactive import reactive
|
||||
from textual.message import Message
|
||||
from typing import TYPE_CHECKING, List, Optional
|
||||
from typing import TYPE_CHECKING, List, Optional, Dict, Tuple
|
||||
from rich.text import Text
|
||||
from rich.box import ROUNDED
|
||||
|
||||
@@ -43,6 +43,7 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
selected_flow_index = reactive(0)
|
||||
sort_key = reactive("flows")
|
||||
simplified_view = reactive(False) # Toggle between detailed and simplified view
|
||||
|
||||
def __init__(self, analyzer: 'EthernetAnalyzer', **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
@@ -50,6 +51,7 @@ class EnhancedFlowTable(Vertical):
|
||||
self.flows_list = []
|
||||
self.row_to_flow_map = {} # Map row keys to flow indices
|
||||
self.flow_metrics = {} # Store per-flow metrics history
|
||||
self.view_mode_changed = False # Track when view mode changes
|
||||
|
||||
def compose(self):
|
||||
"""Create the enhanced flow table"""
|
||||
@@ -64,25 +66,49 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
def on_mount(self):
|
||||
"""Initialize the table"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
# Compact columns optimized for data density
|
||||
table.add_column("#", width=2, key="num")
|
||||
table.add_column("Source", width=18, key="source")
|
||||
table.add_column("Proto", width=4, key="proto")
|
||||
table.add_column("Destination", width=18, key="dest")
|
||||
table.add_column("Extended", width=8, key="extended")
|
||||
table.add_column("Frame Type", width=10, key="frame_type")
|
||||
table.add_column("Pkts", width=6, key="rate")
|
||||
table.add_column("Size", width=8, key="volume")
|
||||
table.add_column("ΔT(ms)", width=8, key="delta_t")
|
||||
table.add_column("σ(ms)", width=8, key="sigma")
|
||||
table.add_column("Out", width=5, key="outliers")
|
||||
|
||||
self._setup_table_columns()
|
||||
self.refresh_data()
|
||||
|
||||
def _setup_table_columns(self):
|
||||
"""Setup table columns based on current view mode"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
# Clear existing columns if any
|
||||
if table.columns:
|
||||
table.clear(columns=True)
|
||||
|
||||
if self.simplified_view:
|
||||
# Simplified view - only main flows with summary data
|
||||
table.add_column("#", width=3, key="num")
|
||||
table.add_column("Source", width=18, key="source")
|
||||
table.add_column("Destination", width=18, key="dest")
|
||||
table.add_column("Protocol", width=8, key="protocol")
|
||||
table.add_column("Packets", width=8, key="packets")
|
||||
table.add_column("Volume", width=10, key="volume")
|
||||
table.add_column("Avg ΔT", width=8, key="avg_delta")
|
||||
table.add_column("Quality", width=8, key="quality")
|
||||
table.add_column("Status", width=10, key="status")
|
||||
else:
|
||||
# Detailed view - original layout with subflows
|
||||
table.add_column("#", width=2, key="num")
|
||||
table.add_column("Source", width=18, key="source")
|
||||
table.add_column("Proto", width=4, key="proto")
|
||||
table.add_column("Destination", width=18, key="dest")
|
||||
table.add_column("Extended", width=8, key="extended")
|
||||
table.add_column("Frame Type", width=10, key="frame_type")
|
||||
table.add_column("Pkts", width=6, key="rate")
|
||||
table.add_column("Size", width=8, key="volume")
|
||||
table.add_column("ΔT(ms)", width=8, key="delta_t")
|
||||
table.add_column("σ(ms)", width=8, key="sigma")
|
||||
table.add_column("Out", width=5, key="outliers")
|
||||
|
||||
def refresh_data(self):
|
||||
"""Refresh flow table with enhanced visualizations"""
|
||||
"""Refresh flow table with current view mode"""
|
||||
# Check if view mode changed and rebuild table structure if needed
|
||||
if self.view_mode_changed:
|
||||
self._setup_table_columns()
|
||||
self.view_mode_changed = False
|
||||
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
# Preserve cursor and scroll positions
|
||||
@@ -103,7 +129,39 @@ class EnhancedFlowTable(Vertical):
|
||||
# Get and sort flows
|
||||
self.flows_list = self._get_sorted_flows()
|
||||
|
||||
# Add flows with enhanced display
|
||||
if self.simplified_view:
|
||||
self._populate_simplified_view()
|
||||
else:
|
||||
self._populate_detailed_view()
|
||||
|
||||
# Restore cursor position
|
||||
if selected_row_key and selected_row_key in table.rows:
|
||||
row_index = list(table.rows.keys()).index(selected_row_key)
|
||||
table.move_cursor(row=row_index, column=cursor_column, animate=False)
|
||||
elif table.row_count > 0:
|
||||
# If original selection not found, try to maintain row position
|
||||
new_row = min(cursor_row, table.row_count - 1)
|
||||
table.move_cursor(row=new_row, column=cursor_column, animate=False)
|
||||
|
||||
# Restore scroll position
|
||||
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
|
||||
|
||||
def _populate_simplified_view(self):
|
||||
"""Populate table with simplified flow summary data"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
for i, flow in enumerate(self.flows_list):
|
||||
# Create simplified row data - no subflows shown
|
||||
row_data = self._create_simplified_row(i + 1, flow)
|
||||
row_key = table.add_row(*row_data, key=f"flow_{i}")
|
||||
|
||||
# Map row key to flow index
|
||||
self.row_to_flow_map[row_key] = i
|
||||
|
||||
def _populate_detailed_view(self):
|
||||
"""Populate table with detailed flow data including subflows"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
for i, flow in enumerate(self.flows_list):
|
||||
# Track metrics for this flow
|
||||
flow_key = f"{flow.src_ip}:{flow.src_port}-{flow.dst_ip}:{flow.dst_port}"
|
||||
@@ -127,20 +185,14 @@ class EnhancedFlowTable(Vertical):
|
||||
metrics['last_packet_count'] = flow.frame_count
|
||||
metrics['last_update'] = flow.last_seen
|
||||
|
||||
# Create row with visualizations
|
||||
# Create row with detailed visualizations
|
||||
row_data = self._create_enhanced_row(i + 1, flow, metrics)
|
||||
row_key = table.add_row(*row_data, key=f"flow_{i}")
|
||||
|
||||
# Map row key to flow index
|
||||
self.row_to_flow_map[row_key] = i
|
||||
|
||||
# Apply row styling based on status
|
||||
style = self._get_flow_style(flow)
|
||||
if style:
|
||||
# Note: DataTable doesn't have set_row_style, using CSS classes instead
|
||||
pass
|
||||
|
||||
# Add sub-rows for protocol breakdown
|
||||
# Add sub-rows for protocol breakdown (only in detailed view)
|
||||
if self._should_show_subrows(flow):
|
||||
sub_rows = self._create_protocol_subrows(flow)
|
||||
combinations = self._get_protocol_frame_combinations(flow)
|
||||
@@ -151,18 +203,6 @@ class EnhancedFlowTable(Vertical):
|
||||
if j < len(combinations):
|
||||
_, frame_type, _, _ = combinations[j]
|
||||
self.row_to_subflow_map[sub_key] = (i, frame_type)
|
||||
|
||||
# Restore cursor position
|
||||
if selected_row_key and selected_row_key in table.rows:
|
||||
row_index = list(table.rows.keys()).index(selected_row_key)
|
||||
table.move_cursor(row=row_index, column=cursor_column, animate=False)
|
||||
elif table.row_count > 0:
|
||||
# If original selection not found, try to maintain row position
|
||||
new_row = min(cursor_row, table.row_count - 1)
|
||||
table.move_cursor(row=new_row, column=cursor_column, animate=False)
|
||||
|
||||
# Restore scroll position
|
||||
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
|
||||
|
||||
def _create_enhanced_row(self, num: int, flow: 'FlowStats', metrics: dict) -> List[Text]:
|
||||
"""Create enhanced row with inline visualizations"""
|
||||
@@ -229,6 +269,64 @@ class EnhancedFlowTable(Vertical):
|
||||
delta_t_text, sigma_text, outlier_text
|
||||
]
|
||||
|
||||
def _create_simplified_row(self, num: int, flow: 'FlowStats') -> List[Text]:
|
||||
"""Create simplified row with summary data only"""
|
||||
# Flow number
|
||||
num_text = Text(str(num), justify="right")
|
||||
|
||||
# Source (IP only for simplified view)
|
||||
source_text = Text(flow.src_ip)
|
||||
|
||||
# Destination (IP only for simplified view)
|
||||
dest_text = Text(flow.dst_ip)
|
||||
|
||||
# Main protocol (transport + extended if available)
|
||||
extended = self._get_extended_protocol(flow)
|
||||
if extended != "-":
|
||||
protocol_str = f"{flow.transport_protocol}/{extended}"
|
||||
else:
|
||||
protocol_str = flow.transport_protocol
|
||||
protocol_text = Text(protocol_str, style="bold cyan")
|
||||
|
||||
# Total packet count
|
||||
packets_text = Text(str(flow.frame_count), justify="right")
|
||||
|
||||
# Total volume
|
||||
volume_text = Text(self._format_bytes(flow.total_bytes), justify="right")
|
||||
|
||||
# Average delta T
|
||||
if flow.avg_inter_arrival > 0:
|
||||
delta_t_ms = flow.avg_inter_arrival * 1000
|
||||
if delta_t_ms >= 1000:
|
||||
avg_delta_str = f"{delta_t_ms/1000:.1f}s"
|
||||
else:
|
||||
avg_delta_str = f"{delta_t_ms:.1f}ms"
|
||||
else:
|
||||
avg_delta_str = "N/A"
|
||||
avg_delta_text = Text(avg_delta_str, justify="right")
|
||||
|
||||
# Quality score as percentage
|
||||
quality_score = self._get_quality_score(flow)
|
||||
quality_text = Text(f"{quality_score}%", justify="right",
|
||||
style="green" if quality_score >= 90 else
|
||||
"yellow" if quality_score >= 70 else "red")
|
||||
|
||||
# Flow status
|
||||
status = self._get_flow_status(flow)
|
||||
status_color = {
|
||||
"Enhanced": "bold blue",
|
||||
"Alert": "bold red",
|
||||
"Warning": "yellow",
|
||||
"Normal": "green"
|
||||
}.get(status, "white")
|
||||
status_text = Text(status, style=status_color)
|
||||
|
||||
return [
|
||||
num_text, source_text, dest_text, protocol_text,
|
||||
packets_text, volume_text, avg_delta_text,
|
||||
quality_text, status_text
|
||||
]
|
||||
|
||||
def _create_rate_sparkline(self, history: List[float]) -> str:
|
||||
"""Create mini sparkline for rate"""
|
||||
if not history:
|
||||
@@ -319,16 +417,60 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
def _should_show_subrows(self, flow: 'FlowStats') -> bool:
|
||||
"""Determine if flow should show protocol breakdown"""
|
||||
# Show subrows for flows with multiple frame types or enhanced analysis
|
||||
return (len(flow.frame_types) > 1 or
|
||||
flow.enhanced_analysis.decoder_type != "Standard")
|
||||
# Only show subrows if there are enhanced frame types
|
||||
enhanced_frame_types = self._get_enhanced_frame_types(flow)
|
||||
return len(enhanced_frame_types) > 0
|
||||
|
||||
def _get_enhanced_frame_types(self, flow: 'FlowStats') -> Dict[str, 'FrameTypeStats']:
|
||||
"""Get only frame types that belong to enhanced protocols"""
|
||||
enhanced_protocols = {'CHAPTER10', 'CH10', 'PTP', 'IENA'}
|
||||
enhanced_frame_types = {}
|
||||
|
||||
for frame_type, stats in flow.frame_types.items():
|
||||
# Check if this frame type belongs to an enhanced protocol
|
||||
if any(enhanced_proto in frame_type for enhanced_proto in enhanced_protocols):
|
||||
enhanced_frame_types[frame_type] = stats
|
||||
elif frame_type.startswith(('CH10-', 'PTP-', 'IENA-')):
|
||||
enhanced_frame_types[frame_type] = stats
|
||||
elif frame_type in ('TMATS', 'TMATS-Data'): # TMATS is part of Chapter 10
|
||||
enhanced_frame_types[frame_type] = stats
|
||||
|
||||
return enhanced_frame_types
|
||||
|
||||
def _get_enhanced_protocol_frame_combinations(self, flow: 'FlowStats', enhanced_frame_types: Dict[str, 'FrameTypeStats']) -> List[Tuple[str, str, int, float]]:
|
||||
"""Get protocol/frame combinations for enhanced protocols only"""
|
||||
combinations = []
|
||||
total_packets = flow.frame_count
|
||||
|
||||
# Group enhanced frame types by extended protocol
|
||||
protocol_frames = {}
|
||||
|
||||
for frame_type, ft_stats in enhanced_frame_types.items():
|
||||
# Determine extended protocol for this frame type
|
||||
extended_proto = self._get_extended_protocol_for_frame(flow, frame_type)
|
||||
|
||||
if extended_proto not in protocol_frames:
|
||||
protocol_frames[extended_proto] = []
|
||||
|
||||
protocol_frames[extended_proto].append((frame_type, ft_stats.count))
|
||||
|
||||
# Convert to list of tuples with percentages
|
||||
for extended_proto, frame_list in protocol_frames.items():
|
||||
for frame_type, count in frame_list:
|
||||
percentage = (count / total_packets * 100) if total_packets > 0 else 0
|
||||
combinations.append((extended_proto, frame_type, count, percentage))
|
||||
|
||||
# Sort by count (descending)
|
||||
combinations.sort(key=lambda x: x[2], reverse=True)
|
||||
return combinations
|
||||
|
||||
def _create_protocol_subrows(self, flow: 'FlowStats') -> List[List[Text]]:
|
||||
"""Create sub-rows for protocol/frame type breakdown"""
|
||||
"""Create sub-rows for enhanced protocol/frame type breakdown only"""
|
||||
subrows = []
|
||||
combinations = self._get_protocol_frame_combinations(flow)
|
||||
enhanced_frame_types = self._get_enhanced_frame_types(flow)
|
||||
combinations = self._get_enhanced_protocol_frame_combinations(flow, enhanced_frame_types)
|
||||
|
||||
for extended_proto, frame_type, count, percentage in combinations: # Show all subrows
|
||||
for extended_proto, frame_type, count, percentage in combinations: # Show all enhanced subrows
|
||||
# Calculate timing for this frame type if available
|
||||
frame_delta_t = ""
|
||||
frame_sigma = ""
|
||||
@@ -385,6 +527,16 @@ class EnhancedFlowTable(Vertical):
|
||||
self.sort_key = key
|
||||
self.refresh_data()
|
||||
|
||||
def toggle_view_mode(self):
|
||||
"""Toggle between simplified and detailed view modes"""
|
||||
self.simplified_view = not self.simplified_view
|
||||
self.view_mode_changed = True
|
||||
self.refresh_data()
|
||||
|
||||
def get_current_view_mode(self) -> str:
|
||||
"""Get current view mode as string"""
|
||||
return "SIMPLIFIED" if self.simplified_view else "DETAILED"
|
||||
|
||||
class FlowSelected(Message):
|
||||
"""Message sent when a flow is selected"""
|
||||
def __init__(self, flow: Optional['FlowStats'], subflow_type: Optional[str] = None) -> None:
|
||||
@@ -451,6 +603,19 @@ class EnhancedFlowTable(Vertical):
|
||||
self.post_message(self.FlowSelected(selected_flow, subflow_type))
|
||||
|
||||
# Helper methods from original implementation
|
||||
def _get_extended_protocol_for_frame(self, flow: 'FlowStats', frame_type: str) -> str:
|
||||
"""Get extended protocol for a specific frame type"""
|
||||
if frame_type.startswith('CH10') or frame_type == 'TMATS':
|
||||
return 'CH10'
|
||||
elif frame_type.startswith('PTP'):
|
||||
return 'PTP'
|
||||
elif frame_type == 'IENA':
|
||||
return 'IENA'
|
||||
elif frame_type == 'NTP':
|
||||
return 'NTP'
|
||||
else:
|
||||
return self._get_extended_protocol(flow)
|
||||
|
||||
def _get_extended_protocol(self, flow: 'FlowStats') -> str:
|
||||
"""Get extended protocol"""
|
||||
if flow.detected_protocol_types:
|
||||
|
||||
Reference in New Issue
Block a user