754 lines
33 KiB
Python
754 lines
33 KiB
Python
"""
|
||
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"<b>File:</b> {os.path.basename(file_path)}<br/>"
|
||
file_info += f"<b>Packets:</b> {summary['total_packets']:,}<br/>"
|
||
file_info += f"<b>Flows:</b> {summary['unique_flows']}<br/>"
|
||
file_info += f"<b>IPs:</b> {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('<span style="color: #888888; font-style: italic;">Select a flow to view detailed information</span>')
|
||
|
||
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('<span style="color: #888888; font-style: italic;">No flow selected</span>')
|
||
return
|
||
|
||
max_sigma = analyzer.statistics_engine.get_max_sigma_deviation(flow)
|
||
|
||
html = f"""
|
||
<div style="color: #ffffff; font-family: 'Monaco', 'Menlo', 'DejaVu Sans Mono', monospace;">
|
||
<h3 style="color: #0078d4; margin-bottom: 12px;">{flow.src_ip} → {flow.dst_ip}</h3>
|
||
<table border="0" cellpadding="8" cellspacing="0" style="width: 100%; border-collapse: collapse;">
|
||
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Packets:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.frame_count:,}</td></tr>
|
||
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Total Bytes:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.total_bytes:,}</td></tr>
|
||
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Max Sigma Deviation:</td><td style="color: #ff6b6b; font-weight: bold; border: 1px solid #404040;">{max_sigma:.2f}σ</td></tr>
|
||
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Protocols:</td><td style="color: #ffffff; border: 1px solid #404040;">{', '.join(flow.protocols)}</td></tr>
|
||
"""
|
||
|
||
if flow.detected_protocol_types:
|
||
html += f'<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Enhanced Protocols:</td><td style="color: #ffffff; border: 1px solid #404040;">{", ".join(flow.detected_protocol_types)}</td></tr>'
|
||
|
||
if flow.avg_inter_arrival > 0:
|
||
html += f"""
|
||
<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Avg Inter-arrival:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.avg_inter_arrival:.6f}s</td></tr>
|
||
<tr style="background-color: #2d2d2d;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Std Deviation:</td><td style="color: #ffffff; border: 1px solid #404040;">{flow.std_inter_arrival:.6f}s</td></tr>
|
||
"""
|
||
|
||
if flow.outlier_frames:
|
||
html += f'<tr style="background-color: #252525;"><td style="color: #cccccc; font-weight: bold; border: 1px solid #404040;">Outlier Frames:</td><td style="color: #ff6b6b; font-weight: bold; border: 1px solid #404040;">{len(flow.outlier_frames)}</td></tr>'
|
||
|
||
html += "</table>"
|
||
|
||
# Frame type breakdown
|
||
if flow.frame_types:
|
||
html += '<h4 style="color: #0078d4; margin-top: 16px; margin-bottom: 8px;">Frame Types:</h4>'
|
||
html += '<ul style="list-style-type: none; padding-left: 0;">'
|
||
for frame_type, ft_stats in flow.frame_types.items():
|
||
avg_str = f"{ft_stats.avg_inter_arrival:.3f}s" if ft_stats.avg_inter_arrival > 0 else "N/A"
|
||
html += f'<li style="margin-bottom: 4px; padding: 4px 8px; background-color: #252525; border-radius: 4px; border-left: 3px solid #0078d4;"><span style="color: #cccccc; font-weight: bold;">{frame_type}:</span> <span style="color: #ffffff;">{ft_stats.count} packets, avg {avg_str}</span></li>'
|
||
html += "</ul>"
|
||
|
||
html += "</div>"
|
||
|
||
self.detail_text.setHtml(html) |