Files
StreamLens/analyzer/tui/panels/flow_list.py

182 lines
8.2 KiB
Python
Raw Normal View History

2025-07-25 15:52:16 -04:00
"""
Left panel - Flow list with frame type breakdowns
"""
from typing import List, Optional
import curses
from ...models import FlowStats
class FlowListPanel:
"""Left panel showing flows and frame type breakdowns"""
def __init__(self):
self.selected_item = 0
self.scroll_offset = 0
def draw(self, stdscr, x_offset: int, y_offset: int, width: int, height: int,
flows_list: List[FlowStats], selected_flow: int):
"""Draw the flow list panel"""
2025-07-26 16:51:37 -04:00
# Draw flows table header with adjusted column widths for better alignment
2025-07-25 15:52:16 -04:00
stdscr.addstr(y_offset, x_offset, "FLOWS:", curses.A_BOLD)
2025-07-26 16:51:37 -04:00
headers = f"{'Src:Port':22} {'Dst:Port':22} {'Proto':6} {'Cast':5} {'#Frames':>7} {'Bytes':>7} {'Encoding':12} {'ΔT Avg':>9}"
2025-07-25 15:52:16 -04:00
stdscr.addstr(y_offset + 1, x_offset, headers[:width-1], curses.A_UNDERLINE)
# Calculate scrolling parameters
start_row = y_offset + 2
max_rows = height - 3 # Account for header and title
total_items = self._get_total_display_items(flows_list)
# Calculate scroll offset to keep selected item visible
scroll_offset = self._calculate_scroll_offset(selected_flow, max_rows, total_items)
# Draw flows list with frame type breakdowns
current_row = start_row
display_item = 0 # Track selectable items (flows + frame types)
visible_items = 0 # Track items actually drawn
for flow_idx, flow in enumerate(flows_list):
# Check if main flow line should be displayed
if display_item >= scroll_offset and visible_items < max_rows:
2025-07-26 16:51:37 -04:00
# Draw main flow line with new column layout
src_endpoint = f"{flow.src_ip}:{flow.src_port}" if flow.src_port > 0 else flow.src_ip
dst_endpoint = f"{flow.dst_ip}:{flow.dst_port}" if flow.dst_port > 0 else flow.dst_ip
# Format bytes with K/M suffix
bytes_str = self._format_bytes(flow.total_bytes)
# Get encoding information (primary detected protocol)
encoding_str = self._get_encoding_display(flow)
# Format average time
2025-07-25 15:52:16 -04:00
avg_time = f"{flow.avg_inter_arrival:.3f}s" if flow.avg_inter_arrival > 0 else "N/A"
2025-07-26 16:51:37 -04:00
# Abbreviate traffic classification
cast_abbrev = flow.traffic_classification[:4] if flow.traffic_classification != "Unknown" else "Unk"
line = f"{src_endpoint:22} {dst_endpoint:22} {flow.transport_protocol:6} {cast_abbrev:5} {flow.frame_count:>7} {bytes_str:>7} {encoding_str:12} {avg_time:>9}"
2025-07-25 15:52:16 -04:00
if display_item == selected_flow:
stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_REVERSE)
else:
stdscr.addstr(current_row, x_offset, line[:width-1], curses.A_BOLD)
current_row += 1
visible_items += 1
display_item += 1
# Draw frame type breakdowns for this flow
if flow.frame_types:
sorted_frame_types = sorted(flow.frame_types.items(), key=lambda x: x[1].count, reverse=True)
for frame_type, ft_stats in sorted_frame_types:
if display_item >= scroll_offset and visible_items < max_rows:
# Calculate frame type timing display
ft_avg = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A"
outlier_count = len(ft_stats.outlier_details) if ft_stats.outlier_details else 0
2025-07-26 16:51:37 -04:00
# Create frame type line aligned with new column layout
bytes_str_ft = self._format_bytes(ft_stats.total_bytes)
ft_line = f" └─{frame_type:18} {'':22} {'':6} {'':5} {ft_stats.count:>7} {bytes_str_ft:>7} {'':12} {ft_avg:>9}"
2025-07-25 15:52:16 -04:00
if display_item == selected_flow:
stdscr.addstr(current_row, x_offset, ft_line[:width-1], curses.A_REVERSE)
else:
stdscr.addstr(current_row, x_offset, ft_line[:width-1])
current_row += 1
visible_items += 1
display_item += 1
def _get_protocol_display(self, flow: FlowStats) -> str:
"""Get display string for flow protocols"""
if flow.detected_protocol_types:
# Prioritize specialized protocols
specialized = {'CHAPTER10', 'PTP', 'IENA'}
found_specialized = flow.detected_protocol_types & specialized
if found_specialized:
return list(found_specialized)[0]
# Use first detected protocol type
return list(flow.detected_protocol_types)[0]
# Fallback to basic protocols
if flow.protocols:
return list(flow.protocols)[0]
return "Unknown"
def _get_total_display_items(self, flows_list: List[FlowStats]) -> int:
"""Calculate total number of selectable items (flows + frame types)"""
total = 0
for flow in flows_list:
total += 1 # Flow itself
total += len(flow.frame_types) # Frame types under this flow
return total
def _calculate_scroll_offset(self, selected_item: int, max_visible: int, total_items: int) -> int:
"""Calculate scroll offset to keep selected item visible"""
if total_items <= max_visible:
return 0 # No scrolling needed
# Keep selected item in the middle third of visible area when possible
middle_position = max_visible // 3
# Calculate ideal scroll offset
scroll_offset = max(0, selected_item - middle_position)
# Ensure we don't scroll past the end
max_scroll = max(0, total_items - max_visible)
scroll_offset = min(scroll_offset, max_scroll)
return scroll_offset
def get_total_display_items(self, flows_list: List[FlowStats]) -> int:
"""Public method to get total display items"""
2025-07-26 16:51:37 -04:00
return self._get_total_display_items(flows_list)
def _format_bytes(self, bytes_count: int) -> str:
"""Format byte count with K/M/G suffixes, always include magnitude indicator"""
if bytes_count >= 1_000_000_000:
return f"{bytes_count / 1_000_000_000:.1f}G"
elif bytes_count >= 1_000_000:
return f"{bytes_count / 1_000_000:.1f}M"
elif bytes_count >= 1_000:
return f"{bytes_count / 1_000:.1f}K"
else:
return f"{bytes_count}B" # Add "B" for plain bytes
def _get_encoding_display(self, flow: FlowStats) -> str:
"""Get the primary encoding/application protocol for display"""
# Prioritize specialized protocols (Chapter 10, PTP, IENA)
if flow.detected_protocol_types:
specialized = {'CH10', 'PTP', 'IENA', 'Chapter10', 'TMATS'}
found_specialized = flow.detected_protocol_types.intersection(specialized)
if found_specialized:
return list(found_specialized)[0]
# Use first detected protocol type
return list(flow.detected_protocol_types)[0]
# Fallback to frame types if available
if flow.frame_types:
frame_types = list(flow.frame_types.keys())
# Look for interesting frame types first
priority_types = ['TMATS', 'CH10-Data', 'PTP-Sync', 'IENA-P', 'IENA-D']
for ptype in priority_types:
if ptype in frame_types:
return ptype
return frame_types[0]
# Last resort - check basic protocols
if flow.protocols:
app_protocols = {'DNS', 'HTTP', 'HTTPS', 'NTP', 'DHCP'}
found_app = flow.protocols.intersection(app_protocols)
if found_app:
return list(found_app)[0]
return "Unknown"