"""
Dockable panels for StreamLens GUI
"""
import os
from typing import Optional, List, TYPE_CHECKING, Dict
from PySide6.QtWidgets import (
QDockWidget, QWidget, QVBoxLayout, QHBoxLayout, QTableWidget,
QTableWidgetItem, QLabel, QHeaderView, QSplitter, QTextEdit
)
from PySide6.QtCore import Qt, Signal
from PySide6.QtGui import QFont, QColor
try:
# Matplotlib integration - lazy loaded
matplotlib = None
FigureCanvas = None
NavigationToolbar = None
Figure = None
plt = None
def _ensure_matplotlib_gui_loaded():
"""Lazy load matplotlib for GUI mode"""
global matplotlib, FigureCanvas, NavigationToolbar, Figure, plt
if matplotlib is not None:
return True
try:
import matplotlib as mpl
matplotlib = mpl
matplotlib.use('Qt5Agg') # Use Qt backend for matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FC
from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NT
from matplotlib.figure import Figure as Fig
import matplotlib.pyplot as pyplot
# Turn off interactive mode to prevent floating windows
pyplot.ioff()
# Ensure no figure windows are created
mpl.rcParams['figure.max_open_warning'] = 0
mpl.rcParams['backend'] = 'Qt5Agg'
# Prevent any automatic figure display
mpl.pyplot.show = lambda *args, **kwargs: None
pyplot.show = lambda *args, **kwargs: None
# Also prevent figure.show()
original_figure_show = Fig.show
Fig.show = lambda self, *args, **kwargs: None
FigureCanvas = FC
NavigationToolbar = NT
Figure = Fig
plt = pyplot
return True
except ImportError as e:
print(f"Matplotlib GUI integration not available: {e}")
return False
except ImportError as e:
print(f"GUI dependencies not available: {e}")
if TYPE_CHECKING:
from ..analysis.core import EthernetAnalyzer
from ..models.flow_stats import FlowStats
class FlowListDockWidget(QWidget):
"""Widget containing the flow list and file information with integrated plots"""
def __init__(self, parent=None):
super().__init__(parent)
self.analyzer: Optional['EthernetAnalyzer'] = None
self.integrated_mode = True # Always use integrated mode
self._signal_cache = {} # Cache for processed signal data
# Set up compact main layout
layout = QVBoxLayout()
layout.setContentsMargins(4, 4, 4, 4) # Tight margins
layout.setSpacing(2) # Minimal spacing between elements
# Compact file info section
self.file_info_label = QLabel("No file loaded")
self.file_info_label.setWordWrap(True)
self.file_info_label.setStyleSheet("""
QLabel {
padding: 6px 12px;
background-color: #2d2d2d;
border: 1px solid #404040;
border-radius: 6px;
font-size: 11px;
color: #cccccc;
font-weight: 500;
}
""")
self.file_info_label.setMaximumHeight(35) # Compact height
layout.addWidget(self.file_info_label)
# Compact flow table with comprehensive data
self.flows_table = QTableWidget()
self.flows_table.setColumnCount(10) # More columns for dense data
self.flows_table.setHorizontalHeaderLabels([
"Source IP", "Dest IP", "Pkts", "Bytes", "Max σ", "Avg ΔT", "Std ΔT", "Outliers", "Protocols", "Signal Plot"
])
# Configure table for compact, dense display with user-adjustable columns
header = self.flows_table.horizontalHeader()
# Make all columns user-resizable by dragging
header.setSectionResizeMode(QHeaderView.Interactive)
# Auto-resize headers to fit content first
for i in range(self.flows_table.columnCount()):
header.resizeSection(i, header.sectionSizeHint(i))
# Then resize to content for data columns
self.flows_table.resizeColumnsToContents()
# Set minimum widths for important columns and ensure plot column gets priority
header.setMinimumSectionSize(50) # Minimum for any column
self.flows_table.setColumnWidth(2, max(60, self.flows_table.columnWidth(2))) # Pkts
self.flows_table.setColumnWidth(3, max(80, self.flows_table.columnWidth(3))) # Bytes
self.flows_table.setColumnWidth(4, max(60, self.flows_table.columnWidth(4))) # Max σ
self.flows_table.setColumnWidth(7, max(60, self.flows_table.columnWidth(7))) # Outliers
self.flows_table.setColumnWidth(9, max(600, self.flows_table.columnWidth(9))) # Signal Plot gets 600px minimum
# Ensure the table uses the full available width
header.setStretchLastSection(True) # Last column (plots) stretches to fill remaining space
# Increased row height (25% taller: 24px -> 30px) and styling
self.flows_table.setSelectionBehavior(QTableWidget.SelectRows)
self.flows_table.setAlternatingRowColors(True)
self.flows_table.verticalHeader().setDefaultSectionSize(30) # 25% taller rows
self.flows_table.setShowGrid(True)
self.flows_table.setGridStyle(Qt.DotLine) # Subtle grid lines
self.flows_table.itemSelectionChanged.connect(self.on_flow_selected)
# Enable sorting
self.flows_table.setSortingEnabled(True)
# Modern dark theme styling for table
self.flows_table.setStyleSheet("""
QTableWidget {
background-color: #1e1e1e;
color: #ffffff;
font-size: 11px;
gridline-color: #404040;
border: 1px solid #404040;
border-radius: 6px;
selection-background-color: #0078d4;
alternate-background-color: #252525;
}
QTableWidget::item {
padding: 6px 8px;
border: none;
}
QTableWidget::item:selected {
background-color: #0078d4;
color: #ffffff;
}
QTableWidget::item:hover {
background-color: #2d2d2d;
}
QHeaderView::section {
background-color: #2d2d2d;
color: #ffffff;
padding: 8px 12px;
border: none;
border-right: 1px solid #404040;
border-bottom: 1px solid #404040;
font-weight: bold;
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
QHeaderView::section:hover {
background-color: #404040;
}
QHeaderView::section:pressed {
background-color: #505050;
}
/* Scrollbar styling */
QScrollBar:vertical {
background-color: #2d2d2d;
width: 12px;
border-radius: 6px;
}
QScrollBar::handle:vertical {
background-color: #505050;
border-radius: 6px;
min-height: 20px;
}
QScrollBar::handle:vertical:hover {
background-color: #606060;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
border: none;
background: none;
}
QScrollBar:horizontal {
background-color: #2d2d2d;
height: 12px;
border-radius: 6px;
}
QScrollBar::handle:horizontal {
background-color: #505050;
border-radius: 6px;
min-width: 20px;
}
QScrollBar::handle:horizontal:hover {
background-color: #606060;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
border: none;
background: none;
}
""")
layout.addWidget(self.flows_table)
# Compact status section
self.status_label = QLabel("Chapter 10 flows (highlighted) show integrated signal plots • All columns are resizable")
self.status_label.setStyleSheet("""
QLabel {
color: #888888;
font-style: italic;
padding: 4px 8px;
font-size: 10px;
background-color: #252525;
border-radius: 4px;
}
""")
self.status_label.setMaximumHeight(25)
layout.addWidget(self.status_label)
self.setLayout(layout)
def set_analyzer(self, analyzer: 'EthernetAnalyzer'):
"""Set the analyzer and populate flow data"""
self.analyzer = analyzer
# Clear cache when analyzer changes
self._signal_cache.clear()
self.populate_flows_table()
def update_file_info(self, file_path: str):
"""Update file information display"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
file_info = f"File: {os.path.basename(file_path)}
"
file_info += f"Packets: {summary['total_packets']:,}
"
file_info += f"Flows: {summary['unique_flows']}
"
file_info += f"IPs: {summary['unique_ips']}"
self.file_info_label.setText(file_info)
def populate_flows_table(self):
"""Populate the flows table with data"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
flows_list = list(summary['flows'].values())
# Sort by maximum sigma deviation
flows_list.sort(key=lambda x: (
self.analyzer.statistics_engine.get_max_sigma_deviation(x),
x.frame_count
), reverse=True)
self.flows_table.setRowCount(len(flows_list))
for row, flow in enumerate(flows_list):
# Source IP (column 0)
src_item = QTableWidgetItem(flow.src_ip)
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
if has_ch10:
font = QFont()
font.setBold(True)
src_item.setFont(font)
src_item.setBackground(QColor(0, 120, 212, 80)) # Modern blue highlight
src_item.setData(Qt.UserRole, flow) # Store flow object
self.flows_table.setItem(row, 0, src_item)
# Destination IP (column 1)
dst_item = QTableWidgetItem(flow.dst_ip)
if has_ch10:
font = QFont()
font.setBold(True)
dst_item.setFont(font)
dst_item.setBackground(QColor(0, 120, 212, 80)) # Modern blue highlight
self.flows_table.setItem(row, 1, dst_item)
# Packets (column 2)
packets_item = QTableWidgetItem(f"{flow.frame_count:,}")
packets_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 2, packets_item)
# Total Bytes (column 3)
bytes_item = QTableWidgetItem(f"{flow.total_bytes:,}")
bytes_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 3, bytes_item)
# Max sigma deviation (column 4)
max_sigma = self.analyzer.statistics_engine.get_max_sigma_deviation(flow)
sigma_item = QTableWidgetItem(f"{max_sigma:.2f}σ")
sigma_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
# Color code sigma values with modern colors
if max_sigma > 5.0:
sigma_item.setBackground(QColor(220, 53, 69, 120)) # Modern red for high sigma
sigma_item.setForeground(QColor(255, 255, 255)) # White text for contrast
elif max_sigma > 3.0:
sigma_item.setBackground(QColor(255, 193, 7, 120)) # Modern amber for medium sigma
sigma_item.setForeground(QColor(0, 0, 0)) # Black text for contrast
self.flows_table.setItem(row, 4, sigma_item)
# Average inter-arrival time (column 5)
avg_time = f"{flow.avg_inter_arrival:.6f}s" if flow.avg_inter_arrival > 0 else "N/A"
avg_item = QTableWidgetItem(avg_time)
avg_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 5, avg_item)
# Standard deviation (column 6)
std_time = f"{flow.std_inter_arrival:.6f}s" if flow.std_inter_arrival > 0 else "N/A"
std_item = QTableWidgetItem(std_time)
std_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, 6, std_item)
# Outlier count (column 7)
outlier_count = len(flow.outlier_frames)
outlier_item = QTableWidgetItem(str(outlier_count))
outlier_item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
if outlier_count > 0:
outlier_item.setBackground(QColor(220, 53, 69, 100)) # Modern red for outliers
outlier_item.setForeground(QColor(255, 255, 255)) # White text for contrast
self.flows_table.setItem(row, 7, outlier_item)
# Protocols (column 8) - compact format
protocols = []
if flow.detected_protocol_types:
protocols.extend(flow.detected_protocol_types)
protocols.extend([p for p in flow.protocols if p not in protocols])
protocols_text = ", ".join(protocols[:3]) # Limit to first 3 protocols
if len(protocols) > 3:
protocols_text += f" (+{len(protocols)-3})"
protocols_item = QTableWidgetItem(protocols_text)
self.flows_table.setItem(row, 8, protocols_item)
# Signal plot column (column 9)
if has_ch10:
plot_widget = self._create_integrated_plot_widget(flow, f"{flow.src_ip} → {flow.dst_ip}")
self.flows_table.setCellWidget(row, 9, plot_widget)
else:
plot_item = QTableWidgetItem("N/A")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
# Auto-resize numeric columns to fit content
for col in [2, 3, 4, 7]: # Packets, Bytes, Max σ, Outliers
self.flows_table.resizeColumnToContents(col)
# Update header with time range information if we have flows
self._update_signal_plot_header(flows_list)
def populate_flows_table_quick(self):
"""Populate the flows table quickly without plots for immediate display"""
if not self.analyzer:
return
summary = self.analyzer.get_summary()
flows_list = list(summary['flows'].values())
# Sort by maximum sigma deviation
flows_list.sort(key=lambda x: (
self.analyzer.statistics_engine.get_max_sigma_deviation(x),
x.frame_count
), reverse=True)
self.flows_table.setRowCount(len(flows_list))
for row, flow in enumerate(flows_list):
# Use same logic as populate_flows_table but with placeholder for plots
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
# Source IP (column 0)
src_item = QTableWidgetItem(flow.src_ip)
if has_ch10:
font = QFont()
font.setBold(True)
src_item.setFont(font)
src_item.setBackground(QColor(0, 120, 212, 80))
src_item.setData(Qt.UserRole, flow)
self.flows_table.setItem(row, 0, src_item)
# Destination IP (column 1)
dst_item = QTableWidgetItem(flow.dst_ip)
if has_ch10:
font = QFont()
font.setBold(True)
dst_item.setFont(font)
dst_item.setBackground(QColor(0, 120, 212, 80))
self.flows_table.setItem(row, 1, dst_item)
# Fill all other columns as in full populate method
for col, value in [
(2, f"{flow.frame_count:,}"),
(3, f"{flow.total_bytes:,}"),
(4, f"{self.analyzer.statistics_engine.get_max_sigma_deviation(flow):.2f}σ"),
(5, f"{flow.avg_inter_arrival:.6f}s" if flow.avg_inter_arrival > 0 else "N/A"),
(6, f"{flow.std_inter_arrival:.6f}s" if flow.std_inter_arrival > 0 else "N/A"),
(7, str(len(flow.outlier_frames))),
(8, ", ".join(list(flow.detected_protocol_types)[:2] if flow.detected_protocol_types else list(flow.protocols)[:2]))
]:
item = QTableWidgetItem(value)
if col in [2, 3, 4, 5, 6, 7]: # Right-align numeric columns
item.setTextAlignment(Qt.AlignRight | Qt.AlignVCenter)
self.flows_table.setItem(row, col, item)
# Plot column placeholder
if has_ch10:
plot_item = QTableWidgetItem("Loading plot...")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setForeground(QColor(128, 128, 128))
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
else:
plot_item = QTableWidgetItem("N/A")
plot_item.setTextAlignment(Qt.AlignCenter)
plot_item.setFlags(plot_item.flags() & ~Qt.ItemIsEditable)
self.flows_table.setItem(row, 9, plot_item)
# Auto-resize numeric columns to fit content
for col in [2, 3, 4, 7]: # Packets, Bytes, Max σ, Outliers
self.flows_table.resizeColumnToContents(col)
def _update_signal_plot_header(self, flows_list):
"""Update the Signal Plot column header with time range information"""
if not flows_list or not self.analyzer:
return
# Find earliest and latest timestamps across all flows
earliest_time = float('inf')
latest_time = float('-inf')
for flow in flows_list:
if flow.timestamps:
flow_start = min(flow.timestamps)
flow_end = max(flow.timestamps)
earliest_time = min(earliest_time, flow_start)
latest_time = max(latest_time, flow_end)
if earliest_time != float('inf') and latest_time != float('-inf'):
# Convert timestamps to time-of-day format (assuming they're epoch seconds)
import datetime
try:
start_time = datetime.datetime.fromtimestamp(earliest_time).strftime('%H:%M:%S')
end_time = datetime.datetime.fromtimestamp(latest_time).strftime('%H:%M:%S')
header_text = f"Signal Plot\n{start_time} → {end_time}"
except (ValueError, OSError):
# If timestamp conversion fails, show duration instead
duration = latest_time - earliest_time
header_text = f"Signal Plot\n{duration:.1f}s span"
else:
header_text = "Signal Plot"
# Update the header
self.flows_table.setHorizontalHeaderLabels([
"Source IP", "Dest IP", "Pkts", "Bytes", "Max σ", "Avg ΔT", "Std ΔT", "Outliers", "Protocols", header_text
])
def on_flow_selected(self):
"""Handle flow selection"""
selected_rows = self.flows_table.selectionModel().selectedRows()
if not selected_rows:
self.status_label.setText("Chapter 10 flows (highlighted) show integrated signal plots • All columns are resizable")
return
# Get selected flow
row = selected_rows[0].row()
flow_item = self.flows_table.item(row, 0)
flow = flow_item.data(Qt.UserRole)
if flow:
# Check if flow has Chapter 10 data
has_ch10 = any('CH10' in ft or 'TMATS' in ft for ft in flow.frame_types.keys())
if has_ch10:
self.status_label.setText("Chapter 10 flow selected - signal plot shown in table")
else:
self.status_label.setText("Selected flow does not contain Chapter 10 telemetry data")
def _create_integrated_plot_widget(self, flow: 'FlowStats', flow_key: str) -> QWidget:
"""Create a small matplotlib widget for integrated plots"""
if not _ensure_matplotlib_gui_loaded():
placeholder = QLabel("Matplotlib not available")
placeholder.setAlignment(Qt.AlignCenter)
return placeholder
# Create a compact matplotlib widget
plot_widget = QWidget()
layout = QVBoxLayout()
layout.setContentsMargins(2, 2, 2, 2)
# Create wider figure for integrated display with dark theme
figure = Figure(figsize=(8, 2.5), facecolor='#1e1e1e') # Wider and slightly taller
canvas = FigureCanvas(figure)
canvas.setMaximumHeight(150) # Increased height to match taller rows
# Apply dark theme to matplotlib
canvas.setStyleSheet("background-color: #1e1e1e; border: 1px solid #404040; border-radius: 4px;")
layout.addWidget(canvas)
plot_widget.setLayout(layout)
# Load and plot data immediately
self._populate_integrated_plot(figure, canvas, flow, flow_key)
return plot_widget
def _populate_integrated_plot(self, figure, canvas, flow: 'FlowStats', flow_key: str):
"""Populate an integrated plot with actual signal data"""
try:
if not self.analyzer:
self._create_error_plot(figure, canvas, "No analyzer available")
return
# Check cache first
cache_key = f"{flow.src_ip}_{flow.dst_ip}"
if cache_key in self._signal_cache:
signal_data_list = self._signal_cache[cache_key]
else:
# Get flow packets
flow_packets = self._get_flow_packets(flow)
if not flow_packets:
self._create_error_plot(figure, canvas, "No packets found")
return
# Extract signal data using the same method as floating plots
from ..utils.signal_visualizer import signal_visualizer
# Extract TMATS metadata and signals
tmats_metadata = signal_visualizer._extract_tmats_from_flow(flow_packets)
signal_data_list = signal_visualizer._extract_signals_from_flow(flow_packets, tmats_metadata)
# Cache the result
self._signal_cache[cache_key] = signal_data_list
if not signal_data_list:
self._create_error_plot(figure, canvas, "No decodable\nsignal data")
return
# Plot the first signal with dark theme styling
ax = figure.add_subplot(111, facecolor='#1e1e1e')
# Use the first signal for the integrated plot
signal_data = signal_data_list[0]
# Plot the first channel in the signal
if signal_data.channels:
channel_name, data = next(iter(signal_data.channels.items()))
# Downsample if too many points for integrated view
timestamps = signal_data.timestamps
if len(timestamps) > 200:
# Simple downsampling by taking every nth point
step = len(timestamps) // 200
timestamps = timestamps[::step]
data = data[::step]
# Modern color scheme - blue for primary data
ax.plot(timestamps, data, color='#0078d4', linewidth=1.2, alpha=0.9)
# Dark theme styling with larger, more legible text
ax.set_xlabel('Time (s)', fontsize=9, color='#ffffff')
# ax.set_ylabel('Amplitude', fontsize=9, color='#ffffff') # Removed for cleaner embedded view
ax.tick_params(labelsize=8, colors='#cccccc', left=False, labelleft=False, bottom=False, labelbottom=False) # Hide all tick labels
ax.grid(True, alpha=0.15, color='#404040')
# No legend needed for embedded plots - cleaner appearance
# Add Y-axis min/max indicators for better readability
y_min, y_max = ax.get_ylim()
ax.text(0.02, 0.95, f'Max: {y_max:.2f}', transform=ax.transAxes,
fontsize=7, color='#00ff88', weight='bold',
bbox=dict(boxstyle='round,pad=0.2', facecolor='#2d2d2d', alpha=0.8))
ax.text(0.02, 0.02, f'Min: {y_min:.2f}', transform=ax.transAxes,
fontsize=7, color='#ff6b6b', weight='bold',
bbox=dict(boxstyle='round,pad=0.2', facecolor='#2d2d2d', alpha=0.8))
# Style the spines
for spine in ax.spines.values():
spine.set_color('#404040')
spine.set_linewidth(0.5)
# Add compact title with time range info
time_start = timestamps[0] if len(timestamps) > 0 else 0
time_end = timestamps[-1] if len(timestamps) > 0 else 0
duration = time_end - time_start
title_text = f'{channel_name}'
if len(signal_data_list) > 1:
title_text += f' (+{len(signal_data_list)-1} more)'
title_text += f' | {duration:.2f}s span'
ax.set_title(title_text, fontsize=8, color='#ffffff', pad=8)
else:
self._create_error_plot(figure, canvas, "No channel data")
return
# Maximum plot area with ultra-minimal margins on all sides
figure.subplots_adjust(left=0.01, right=0.99, top=0.92, bottom=0.15)
canvas.draw_idle() # Use draw_idle to avoid window creation
except Exception as e:
self._create_error_plot(figure, canvas, f"Error:\n{str(e)[:30]}...")
def _get_flow_packets(self, flow: 'FlowStats'):
"""Get all packets for a specific flow"""
if not self.analyzer or not hasattr(self.analyzer, 'all_packets') or not self.analyzer.all_packets:
return []
flow_packets = []
for packet in self.analyzer.all_packets:
try:
if hasattr(packet, 'haslayer'):
from scapy.all import IP
if packet.haslayer(IP):
ip_layer = packet[IP]
if ip_layer.src == flow.src_ip and ip_layer.dst == flow.dst_ip:
flow_packets.append(packet)
except:
continue
return flow_packets
def _create_error_plot(self, figure, canvas, message: str):
"""Create an error message plot with dark theme"""
ax = figure.add_subplot(111, facecolor='#1e1e1e')
ax.text(0.5, 0.5, message,
ha='center', va='center', transform=ax.transAxes,
fontsize=8, color='#ff6b6b', style='italic') # Modern red for errors
ax.set_xlim(0, 1)
ax.set_ylim(0, 1)
ax.axis('off')
figure.subplots_adjust(left=0, right=1, top=1, bottom=0)
canvas.draw_idle() # Use draw_idle to avoid window creation
class FlowDetailDockWidget(QDockWidget):
"""Dockable widget showing detailed information about selected flow"""
def __init__(self, parent=None):
super().__init__("Flow Details", parent)
# Create main widget
main_widget = QWidget()
layout = QVBoxLayout()
# Detail text area with dark theme
self.detail_text = QTextEdit()
self.detail_text.setReadOnly(True)
self.detail_text.setMaximumHeight(150)
self.detail_text.setStyleSheet("""
QTextEdit {
background-color: #1e1e1e;
color: #ffffff;
border: 1px solid #404040;
border-radius: 6px;
padding: 8px;
font-family: 'Monaco', 'Menlo', 'DejaVu Sans Mono', monospace;
font-size: 11px;
selection-background-color: #0078d4;
}
""")
layout.addWidget(self.detail_text)
main_widget.setLayout(layout)
self.setWidget(main_widget)
# Set dock properties
self.setFeatures(QDockWidget.DockWidgetMovable | QDockWidget.DockWidgetFloatable)
self.setAllowedAreas(Qt.BottomDockWidgetArea | Qt.TopDockWidgetArea)
# Initial content with dark theme colors
self.detail_text.setHtml('Select a flow to view detailed information')
def update_flow_details(self, flow: 'FlowStats', analyzer: 'EthernetAnalyzer'):
"""Update the detail view with flow information"""
if not flow or not analyzer:
self.detail_text.setHtml('No flow selected')
return
max_sigma = analyzer.statistics_engine.get_max_sigma_deviation(flow)
html = f"""
| Packets: | {flow.frame_count:,} |
| Total Bytes: | {flow.total_bytes:,} |
| Max Sigma Deviation: | {max_sigma:.2f}σ |
| Protocols: | {', '.join(flow.protocols)} |
| Enhanced Protocols: | {", ".join(flow.detected_protocol_types)} |
| Avg Inter-arrival: | {flow.avg_inter_arrival:.6f}s |
| Std Deviation: | {flow.std_inter_arrival:.6f}s |
| Outlier Frames: | {len(flow.outlier_frames)} |