Files
StreamLens/analyzer/tui/textual/app_v2.py
noisedestroyers 36a576dc2c Enhanced Textual TUI with proper API usage and documentation
- Fixed DataTable row selection and event handling
- Added explicit column keys to prevent auto-generated keys
- Implemented row-to-flow mapping for reliable selection tracking
- Converted left metrics panel to horizontal top bar
- Fixed all missing FlowStats/EnhancedAnalysisData attributes
- Created comprehensive Textual API documentation in Documentation/textual/
- Added validation checklist to prevent future API mismatches
- Preserved cursor position during data refreshes
- Fixed RowKey type handling and event names

The TUI now properly handles flow selection, displays metrics in a compact top bar,
and correctly correlates selected rows with the details pane.
2025-07-27 18:37:55 -04:00

284 lines
10 KiB
Python

"""
StreamLens Textual Application V2 - TipTop-Inspired Design
Modern TUI with real-time metrics, sparklines, and professional monitoring aesthetic
"""
from textual.app import App, ComposeResult
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
from textual.widgets import Header, Footer, Static, DataTable, Label
from textual.reactive import reactive
from textual.timer import Timer
from typing import TYPE_CHECKING
from rich.text import Text
from rich.console import Group
from rich.panel import Panel
from rich.table import Table
import time
from .widgets.sparkline import SparklineWidget
from .widgets.metric_card import MetricCard
from .widgets.flow_table_v2 import EnhancedFlowTable
from .widgets.flow_details import FlowDetailsPanel
if TYPE_CHECKING:
from ...analysis.core import EthernetAnalyzer
class StreamLensAppV2(App):
"""
StreamLens TipTop-Inspired Interface
Features:
- Real-time metrics with sparklines
- Color-coded quality indicators
- Compact information display
- Multi-column layout
- Smooth live updates
"""
CSS_PATH = "styles/streamlens_v2.tcss"
BINDINGS = [
("q", "quit", "Quit"),
("1", "sort('flows')", "Sort Flows"),
("2", "sort('packets')", "Sort Packets"),
("3", "sort('volume')", "Sort Volume"),
("4", "sort('quality')", "Sort Quality"),
("p", "toggle_pause", "Pause"),
("d", "show_details", "Details"),
("?", "toggle_help", "Help"),
]
# Reactive attributes
total_flows = reactive(0)
total_packets = reactive(0)
packets_per_sec = reactive(0.0)
bytes_per_sec = reactive(0.0)
enhanced_flows = reactive(0)
outlier_count = reactive(0)
# Update timers
metric_timer: Timer = None
flow_timer: Timer = None
def __init__(self, analyzer: 'EthernetAnalyzer'):
super().__init__()
self.analyzer = analyzer
self.title = "StreamLens"
self.sub_title = "Network Flow Analysis"
self.paused = False
# Metrics history for sparklines
self.packets_history = []
self.bytes_history = []
self.flows_history = []
self.max_history = 60 # 60 seconds of history
def compose(self) -> ComposeResult:
"""Create TipTop-inspired layout"""
yield Header()
with Container(id="main-container"):
# Top metrics bar - compact like TipTop
with Horizontal(id="metrics-bar"):
yield MetricCard(
"Flows",
f"{self.total_flows}",
trend="stable",
id="flows-metric"
)
yield MetricCard(
"Packets/s",
f"{self.packets_per_sec:.1f}",
trend="up",
sparkline=True,
id="packets-metric"
)
yield MetricCard(
"Volume/s",
self._format_bytes_per_sec(self.bytes_per_sec),
trend="stable",
sparkline=True,
id="volume-metric"
)
yield MetricCard(
"Enhanced",
f"{self.enhanced_flows}",
color="success",
id="enhanced-metric"
)
yield MetricCard(
"Outliers",
f"{self.outlier_count}",
color="warning" if self.outlier_count > 0 else "normal",
id="outliers-metric"
)
# Main content area with horizontal split
with Horizontal(id="content-area"):
# Left - Enhanced flow table (wider)
with Vertical(id="left-panel", classes="panel-wide"):
yield EnhancedFlowTable(
self.analyzer,
id="flow-table"
)
# Right - Selected flow details
with Vertical(id="right-panel", classes="panel"):
yield FlowDetailsPanel(
id="flow-details"
)
yield Footer()
def on_mount(self) -> None:
"""Initialize the application with TipTop-style updates"""
self.update_metrics()
# Set up update intervals like TipTop
self.metric_timer = self.set_interval(0.5, self.update_metrics) # 2Hz for smooth graphs
self.flow_timer = self.set_interval(1.0, self.update_flows) # 1Hz for flow data
# Initialize sparkline history
self._initialize_history()
def _initialize_history(self):
"""Initialize metrics history arrays"""
current_time = time.time()
for _ in range(self.max_history):
self.packets_history.append(0)
self.bytes_history.append(0)
self.flows_history.append(0)
def update_metrics(self) -> None:
"""Update real-time metrics and sparklines"""
if self.paused:
return
# Get current metrics
summary = self.analyzer.get_summary()
self.total_flows = summary.get('unique_flows', 0)
self.total_packets = summary.get('total_packets', 0)
# Calculate rates (simplified for now)
# In real implementation, track deltas over time
current_time = time.time()
if not hasattr(self, '_start_time'):
self._start_time = current_time
elapsed = max(1, current_time - self._start_time)
self.packets_per_sec = self.total_packets / elapsed
self.bytes_per_sec = summary.get('total_bytes', 0) / elapsed
# Count enhanced and outliers
enhanced = 0
outliers = 0
for flow in self.analyzer.flows.values():
if flow.enhanced_analysis.decoder_type != "Standard":
enhanced += 1
outliers += len(flow.outlier_frames)
self.enhanced_flows = enhanced
self.outlier_count = outliers
# Update metric cards
self._update_metric_cards()
# Update sparklines (removed - no longer in left panel)
# self._update_sparklines()
def _update_metric_cards(self):
"""Update the metric card displays"""
# Update flows metric
flows_card = self.query_one("#flows-metric", MetricCard)
flows_card.update_value(f"{self.total_flows}")
# Update packets/s with color coding
packets_card = self.query_one("#packets-metric", MetricCard)
packets_card.update_value(f"{self.packets_per_sec:.1f}")
if self.packets_per_sec > 10000:
packets_card.color = "warning"
elif self.packets_per_sec > 50000:
packets_card.color = "error"
else:
packets_card.color = "success"
# Update volume/s
volume_card = self.query_one("#volume-metric", MetricCard)
volume_card.update_value(self._format_bytes_per_sec(self.bytes_per_sec))
# Update enhanced flows
enhanced_card = self.query_one("#enhanced-metric", MetricCard)
enhanced_card.update_value(f"{self.enhanced_flows}")
# Update outliers with color
outliers_card = self.query_one("#outliers-metric", MetricCard)
outliers_card.update_value(f"{self.outlier_count}")
if self.outlier_count > 100:
outliers_card.color = "error"
elif self.outlier_count > 10:
outliers_card.color = "warning"
else:
outliers_card.color = "normal"
def _update_sparklines(self):
"""Update sparkline charts with latest data"""
# Add new data points
self.packets_history.append(self.packets_per_sec)
self.bytes_history.append(self.bytes_per_sec)
self.flows_history.append(self.total_flows)
# Keep only recent history
if len(self.packets_history) > self.max_history:
self.packets_history.pop(0)
self.bytes_history.pop(0)
self.flows_history.pop(0)
# Update sparkline widgets
flow_spark = self.query_one("#flow-rate-spark", SparklineWidget)
flow_spark.update_data(self.flows_history)
packet_spark = self.query_one("#packet-rate-spark", SparklineWidget)
packet_spark.update_data(self.packets_history)
def update_flows(self) -> None:
"""Update flow table data"""
if self.paused:
return
# Update flow table
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
flow_table.refresh_data()
def on_enhanced_flow_table_flow_selected(self, event: EnhancedFlowTable.FlowSelected) -> None:
"""Handle flow selection events"""
if event.flow:
details_panel = self.query_one("#flow-details", FlowDetailsPanel)
details_panel.update_flow(event.flow)
def _format_bytes_per_sec(self, bps: float) -> str:
"""Format bytes per second with appropriate units"""
if bps >= 1_000_000_000:
return f"{bps / 1_000_000_000:.1f} GB/s"
elif bps >= 1_000_000:
return f"{bps / 1_000_000:.1f} MB/s"
elif bps >= 1_000:
return f"{bps / 1_000:.1f} KB/s"
else:
return f"{bps:.0f} B/s"
def action_toggle_pause(self) -> None:
"""Toggle pause state"""
self.paused = not self.paused
status = "PAUSED" if self.paused else "LIVE"
self.sub_title = f"Network Flow Analysis - {status}"
def action_sort(self, key: str) -> None:
"""Sort flow table by specified key"""
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
flow_table.sort_by(key)
def action_show_details(self) -> None:
"""Show detailed view for selected flow"""
# TODO: Implement detailed flow modal
pass