Enhanced TUI with split flow details and timing analysis
- Added ΔT (deltaT), σ (sigma), and outlier count columns to flow table - Split right panel into top (main flow) and bottom (sub-flow) sections - Removed inner panel borders for clean 3-panel layout - Added sub-flow selection logic with detailed timing statistics - Implemented per-frame-type timing analysis in sub-flow details - Color-coded outliers and timing data for quick visual assessment
This commit is contained in:
@@ -75,6 +75,9 @@ class EnhancedFlowTable(Vertical):
|
||||
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.refresh_data()
|
||||
|
||||
@@ -95,6 +98,7 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
# Clear row mapping
|
||||
self.row_to_flow_map.clear()
|
||||
self.row_to_subflow_map = {} # Map row keys to (flow_index, subflow_type)
|
||||
|
||||
# Get and sort flows
|
||||
self.flows_list = self._get_sorted_flows()
|
||||
@@ -139,10 +143,14 @@ class EnhancedFlowTable(Vertical):
|
||||
# Add sub-rows for protocol breakdown
|
||||
if self._should_show_subrows(flow):
|
||||
sub_rows = self._create_protocol_subrows(flow)
|
||||
combinations = self._get_protocol_frame_combinations(flow)
|
||||
for j, sub_row in enumerate(sub_rows):
|
||||
sub_key = table.add_row(*sub_row, key=f"flow_{i}_sub_{j}")
|
||||
# Map sub-row to parent flow
|
||||
# Map sub-row to parent flow and subflow type
|
||||
self.row_to_flow_map[sub_key] = i
|
||||
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:
|
||||
@@ -188,24 +196,37 @@ class EnhancedFlowTable(Vertical):
|
||||
size_value = self._format_bytes(flow.total_bytes)
|
||||
size_text = Text(f"{size_value:>8}")
|
||||
|
||||
# Quality with bar chart and color
|
||||
quality_bar, quality_color = self._create_quality_bar(flow)
|
||||
quality_value = self._get_quality_score(flow)
|
||||
quality_text = Text(f"{quality_value:>3}% {quality_bar}", style=quality_color)
|
||||
# Delta T (average time between packets in ms)
|
||||
if flow.avg_inter_arrival > 0:
|
||||
delta_t_ms = flow.avg_inter_arrival * 1000
|
||||
if delta_t_ms >= 1000:
|
||||
delta_t_str = f"{delta_t_ms/1000:.1f}s"
|
||||
else:
|
||||
delta_t_str = f"{delta_t_ms:.1f}"
|
||||
else:
|
||||
delta_t_str = "N/A"
|
||||
delta_t_text = Text(delta_t_str, justify="right")
|
||||
|
||||
# Status indicator
|
||||
status = self._get_flow_status(flow)
|
||||
status_color = {
|
||||
"Normal": "green",
|
||||
"Enhanced": "bold green",
|
||||
"Warning": "yellow",
|
||||
"Alert": "red"
|
||||
}.get(status, "white")
|
||||
status_text = Text(status, style=status_color)
|
||||
# Sigma (standard deviation in ms)
|
||||
if flow.std_inter_arrival > 0:
|
||||
sigma_ms = flow.std_inter_arrival * 1000
|
||||
if sigma_ms >= 1000:
|
||||
sigma_str = f"{sigma_ms/1000:.1f}s"
|
||||
else:
|
||||
sigma_str = f"{sigma_ms:.1f}"
|
||||
else:
|
||||
sigma_str = "N/A"
|
||||
sigma_text = Text(sigma_str, justify="right")
|
||||
|
||||
# Outlier count (packets outside tolerance)
|
||||
outlier_count = len(flow.outlier_frames)
|
||||
outlier_text = Text(str(outlier_count), justify="right",
|
||||
style="red" if outlier_count > 0 else "green")
|
||||
|
||||
return [
|
||||
num_text, source_text, proto_text, dest_text,
|
||||
extended_text, frame_text, rate_text, size_text
|
||||
extended_text, frame_text, rate_text, size_text,
|
||||
delta_t_text, sigma_text, outlier_text
|
||||
]
|
||||
|
||||
def _create_rate_sparkline(self, history: List[float]) -> str:
|
||||
@@ -308,6 +329,21 @@ class EnhancedFlowTable(Vertical):
|
||||
combinations = self._get_protocol_frame_combinations(flow)
|
||||
|
||||
for extended_proto, frame_type, count, percentage in combinations[:3]: # Max 3 subrows
|
||||
# Calculate timing for this frame type if available
|
||||
frame_delta_t = ""
|
||||
frame_sigma = ""
|
||||
frame_outliers = ""
|
||||
|
||||
if frame_type in flow.frame_types:
|
||||
ft_stats = flow.frame_types[frame_type]
|
||||
if ft_stats.avg_inter_arrival > 0:
|
||||
dt_ms = ft_stats.avg_inter_arrival * 1000
|
||||
frame_delta_t = f"{dt_ms:.1f}" if dt_ms < 1000 else f"{dt_ms/1000:.1f}s"
|
||||
if ft_stats.std_inter_arrival > 0:
|
||||
sig_ms = ft_stats.std_inter_arrival * 1000
|
||||
frame_sigma = f"{sig_ms:.1f}" if sig_ms < 1000 else f"{sig_ms/1000:.1f}s"
|
||||
frame_outliers = str(len(ft_stats.outlier_frames))
|
||||
|
||||
subrow = [
|
||||
Text(""), # Empty flow number
|
||||
Text(""), # Empty source
|
||||
@@ -316,7 +352,10 @@ class EnhancedFlowTable(Vertical):
|
||||
Text(f" {extended_proto}", style="dim yellow"),
|
||||
Text(frame_type, style="dim blue"),
|
||||
Text(f"{count}", style="dim", justify="right"),
|
||||
Text(f"{self._format_bytes(count * (flow.total_bytes // flow.frame_count) if flow.frame_count > 0 else 0):>8}", style="dim")
|
||||
Text(f"{self._format_bytes(count * (flow.total_bytes // flow.frame_count) if flow.frame_count > 0 else 0):>8}", style="dim"),
|
||||
Text(frame_delta_t, style="dim", justify="right"),
|
||||
Text(frame_sigma, style="dim", justify="right"),
|
||||
Text(frame_outliers, style="dim red" if frame_outliers and int(frame_outliers) > 0 else "dim", justify="right")
|
||||
]
|
||||
subrows.append(subrow)
|
||||
|
||||
@@ -348,8 +387,9 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
class FlowSelected(Message):
|
||||
"""Message sent when a flow is selected"""
|
||||
def __init__(self, flow: Optional['FlowStats']) -> None:
|
||||
def __init__(self, flow: Optional['FlowStats'], subflow_type: Optional[str] = None) -> None:
|
||||
self.flow = flow
|
||||
self.subflow_type = subflow_type
|
||||
super().__init__()
|
||||
|
||||
def get_selected_flow(self) -> Optional['FlowStats']:
|
||||
@@ -372,10 +412,31 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
return None
|
||||
|
||||
def get_selected_subflow_type(self) -> Optional[str]:
|
||||
"""Get currently selected sub-flow type if applicable"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
if table.cursor_row is None or not table.rows:
|
||||
return None
|
||||
|
||||
# Get the row key at cursor position
|
||||
row_keys = list(table.rows.keys())
|
||||
if table.cursor_row >= len(row_keys):
|
||||
return None
|
||||
|
||||
row_key = row_keys[table.cursor_row]
|
||||
|
||||
# Check if this is a sub-row
|
||||
if row_key in self.row_to_subflow_map:
|
||||
_, subflow_type = self.row_to_subflow_map[row_key]
|
||||
return subflow_type
|
||||
|
||||
return None
|
||||
|
||||
def on_data_table_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
|
||||
"""Handle row highlight to update selection"""
|
||||
selected_flow = self.get_selected_flow()
|
||||
self.post_message(self.FlowSelected(selected_flow))
|
||||
subflow_type = self.get_selected_subflow_type()
|
||||
self.post_message(self.FlowSelected(selected_flow, subflow_type))
|
||||
|
||||
# Helper methods from original implementation
|
||||
def _get_extended_protocol(self, flow: 'FlowStats') -> str:
|
||||
|
||||
Reference in New Issue
Block a user