- 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.
128 lines
4.6 KiB
Python
128 lines
4.6 KiB
Python
"""
|
|
Main StreamLens Textual Application
|
|
Modern TUI interface using Textual framework
|
|
"""
|
|
|
|
from textual.app import App, ComposeResult
|
|
from textual.containers import Container, Horizontal, Vertical
|
|
from textual.widgets import Header, Footer, TabbedContent, TabPane, Static, DataTable
|
|
from textual.reactive import reactive
|
|
from typing import TYPE_CHECKING
|
|
|
|
from .widgets.flow_table import FlowAnalysisWidget
|
|
from .widgets.packet_viewer import PacketDecoderWidget
|
|
from .widgets.metrics_dashboard import StatisticalAnalysisWidget
|
|
|
|
if TYPE_CHECKING:
|
|
from ...analysis.core import EthernetAnalyzer
|
|
|
|
|
|
class StreamLensApp(App):
|
|
"""
|
|
StreamLens Textual TUI Application
|
|
|
|
Modern interface with three main tabs:
|
|
- Flow Analysis: Interactive flow table with hierarchical protocol breakdown
|
|
- Packet Decoder: 3-panel packet inspection interface
|
|
- Statistical Analysis: Real-time metrics and performance analysis
|
|
"""
|
|
|
|
CSS_PATH = "styles/streamlens.tcss"
|
|
|
|
BINDINGS = [
|
|
("1", "show_tab('flows')", "Flow Analysis"),
|
|
("2", "show_tab('decoder')", "Packet Decoder"),
|
|
("3", "show_tab('stats')", "Statistics"),
|
|
("q", "quit", "Quit"),
|
|
("ctrl+c", "quit", "Quit"),
|
|
("?", "toggle_help", "Help"),
|
|
]
|
|
|
|
# Reactive attributes for live data updates
|
|
total_flows = reactive(0)
|
|
total_packets = reactive(0)
|
|
live_status = reactive("Stopped")
|
|
|
|
def __init__(self, analyzer: 'EthernetAnalyzer'):
|
|
super().__init__()
|
|
self.analyzer = analyzer
|
|
self.title = "StreamLens - Ethernet Traffic Analyzer"
|
|
self.sub_title = "Modern Network Flow Analysis"
|
|
|
|
def compose(self) -> ComposeResult:
|
|
"""Create the application layout"""
|
|
yield Header()
|
|
|
|
with Container(id="main-container"):
|
|
# Status summary bar
|
|
yield Static(
|
|
f"Flows: {self.total_flows} | Packets: {self.total_packets} | Status: {self.live_status}",
|
|
id="status-summary"
|
|
)
|
|
|
|
# Main tabbed interface
|
|
with TabbedContent(initial="flows"):
|
|
# Flow Analysis Tab
|
|
with TabPane("Flow Analysis", id="flows"):
|
|
yield FlowAnalysisWidget(self.analyzer, id="flow-analysis")
|
|
|
|
# Packet Decoder Tab
|
|
with TabPane("Packet Decoder", id="decoder"):
|
|
yield PacketDecoderWidget(self.analyzer, id="packet-decoder")
|
|
|
|
# Statistical Analysis Tab
|
|
with TabPane("Statistics", id="stats"):
|
|
yield StatisticalAnalysisWidget(self.analyzer, id="statistics")
|
|
|
|
yield Footer()
|
|
|
|
def on_mount(self) -> None:
|
|
"""Initialize the application"""
|
|
self.update_status()
|
|
# Set up periodic updates for live analysis
|
|
if self.analyzer.is_live:
|
|
self.set_interval(1.0, self.update_status)
|
|
|
|
def update_status(self) -> None:
|
|
"""Update reactive status attributes"""
|
|
summary = self.analyzer.get_summary()
|
|
self.total_flows = summary.get('unique_flows', 0)
|
|
self.total_packets = summary.get('total_packets', 0)
|
|
self.live_status = "Live" if self.analyzer.is_live else "Offline"
|
|
|
|
# Update status summary display
|
|
status_widget = self.query_one("#status-summary", Static)
|
|
status_widget.update(
|
|
f"Flows: {self.total_flows} | Packets: {self.total_packets} | Status: {self.live_status}"
|
|
)
|
|
|
|
def action_show_tab(self, tab_id: str) -> None:
|
|
"""Switch to specified tab"""
|
|
tabs = self.query_one(TabbedContent)
|
|
tabs.active = tab_id
|
|
|
|
def action_toggle_help(self) -> None:
|
|
"""Toggle help information"""
|
|
# TODO: Implement help modal
|
|
pass
|
|
|
|
def watch_total_flows(self, new_value: int) -> None:
|
|
"""React to flow count changes"""
|
|
# Trigger updates in child widgets
|
|
flow_widget = self.query_one("#flow-analysis", FlowAnalysisWidget)
|
|
flow_widget.refresh_data()
|
|
|
|
def watch_total_packets(self, new_value: int) -> None:
|
|
"""React to packet count changes"""
|
|
# Trigger updates in decoder widget
|
|
decoder_widget = self.query_one("#packet-decoder", PacketDecoderWidget)
|
|
decoder_widget.refresh_data()
|
|
|
|
def get_selected_flow(self):
|
|
"""Get currently selected flow from flow analysis widget"""
|
|
flow_widget = self.query_one("#flow-analysis", FlowAnalysisWidget)
|
|
return flow_widget.get_selected_flow()
|
|
|
|
def action_quit(self) -> None:
|
|
"""Clean exit"""
|
|
self.exit() |