pretty good
This commit is contained in:
@@ -8,6 +8,7 @@ from textual.containers import Container, Horizontal, Vertical, ScrollableContai
|
||||
from textual.widgets import Header, Footer, Static, DataTable, Label
|
||||
from textual.reactive import reactive
|
||||
from textual.timer import Timer
|
||||
from textual.events import MouseDown, MouseMove
|
||||
from typing import TYPE_CHECKING
|
||||
from rich.text import Text
|
||||
from rich.console import Group
|
||||
@@ -37,6 +38,8 @@ class StreamLensAppV2(App):
|
||||
"""
|
||||
|
||||
CSS_PATH = "styles/streamlens_v2.tcss"
|
||||
ENABLE_COMMAND_PALETTE = False
|
||||
AUTO_FOCUS = None
|
||||
|
||||
BINDINGS = [
|
||||
("q", "quit", "Quit"),
|
||||
@@ -79,40 +82,13 @@ class StreamLensAppV2(App):
|
||||
yield Header()
|
||||
|
||||
with Container(id="main-container"):
|
||||
# Top metrics bar - compact like TipTop
|
||||
# Ultra-compact metrics bar
|
||||
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"
|
||||
)
|
||||
yield MetricCard("Flows", f"{self.total_flows}", id="flows-metric")
|
||||
yield MetricCard("Pkts/s", f"{self.packets_per_sec:.0f}", id="packets-metric")
|
||||
yield MetricCard("Vol/s", self._format_bytes_per_sec(self.bytes_per_sec), 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"):
|
||||
@@ -132,7 +108,7 @@ class StreamLensAppV2(App):
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
"""Initialize the application with TipTop-style updates"""
|
||||
"""Initialize the application with TipTop-style updates"""
|
||||
self.update_metrics()
|
||||
|
||||
# Set up update intervals like TipTop
|
||||
@@ -141,7 +117,20 @@ class StreamLensAppV2(App):
|
||||
|
||||
# Initialize sparkline history
|
||||
self._initialize_history()
|
||||
|
||||
# Set initial focus to the flow table for immediate keyboard navigation
|
||||
self.call_after_refresh(self._set_initial_focus)
|
||||
|
||||
def _set_initial_focus(self):
|
||||
"""Set initial focus to the flow table after widgets are ready"""
|
||||
try:
|
||||
flow_table = self.query_one("#flow-table", EnhancedFlowTable)
|
||||
data_table = flow_table.query_one("#flows-data-table", DataTable)
|
||||
data_table.focus()
|
||||
except Exception:
|
||||
# If table isn't ready yet, try again after a short delay
|
||||
self.set_timer(0.1, self._set_initial_focus)
|
||||
|
||||
def _initialize_history(self):
|
||||
"""Initialize metrics history arrays"""
|
||||
current_time = time.time()
|
||||
@@ -281,4 +270,12 @@ class StreamLensAppV2(App):
|
||||
def action_show_details(self) -> None:
|
||||
"""Show detailed view for selected flow"""
|
||||
# TODO: Implement detailed flow modal
|
||||
pass
|
||||
pass
|
||||
|
||||
def on_mouse_down(self, event: MouseDown) -> None:
|
||||
"""Prevent default mouse down behavior to disable mouse interaction."""
|
||||
event.prevent_default()
|
||||
|
||||
def on_mouse_move(self, event: MouseMove) -> None:
|
||||
"""Prevent default mouse move behavior to disable mouse interaction."""
|
||||
event.prevent_default()
|
||||
@@ -1,88 +1,72 @@
|
||||
/* StreamLens V2 - TipTop-Inspired Styling */
|
||||
|
||||
/* Color Scheme - Dark theme with vibrant accents */
|
||||
$primary: #0080ff;
|
||||
$primary-lighten-1: #3399ff;
|
||||
$primary-lighten-2: #66b3ff;
|
||||
$primary-lighten-3: #99ccff;
|
||||
|
||||
$accent: #00ffcc;
|
||||
$success: #00ff88;
|
||||
$warning: #ffcc00;
|
||||
$error: #ff3366;
|
||||
|
||||
$surface: #1a1a1a;
|
||||
$surface-lighten-1: #262626;
|
||||
$surface-lighten-2: #333333;
|
||||
$background: #0d0d0d;
|
||||
$text: #ffffff;
|
||||
$text-muted: #999999;
|
||||
/* StreamLens V2 - Compact Styling */
|
||||
|
||||
/* Main Application Layout */
|
||||
Screen {
|
||||
background: $background;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
#main-container {
|
||||
height: 1fr;
|
||||
background: $background;
|
||||
background: #0d0d0d;
|
||||
}
|
||||
|
||||
/* Metrics Bar - Horizontal compact display at top */
|
||||
/* Metrics Bar - Ultra compact display at top */
|
||||
#metrics-bar {
|
||||
height: 7;
|
||||
padding: 1;
|
||||
background: $surface;
|
||||
border-bottom: thick $primary;
|
||||
height: 3;
|
||||
padding: 0;
|
||||
background: #1a1a1a;
|
||||
border-bottom: solid #0080ff;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
MetricCard {
|
||||
width: 1fr;
|
||||
height: 5;
|
||||
margin: 0 1;
|
||||
max-width: 20;
|
||||
border: tall $primary-lighten-2;
|
||||
padding: 0 1;
|
||||
height: 3;
|
||||
margin: 0;
|
||||
max-width: 18;
|
||||
border: none;
|
||||
padding: 0;
|
||||
align: center middle;
|
||||
}
|
||||
|
||||
/* Content Area - Three column layout */
|
||||
/* Content Area - Maximized for grid */
|
||||
#content-area {
|
||||
height: 1fr;
|
||||
padding: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Panel Styling */
|
||||
/* Panel Styling - Minimal borders */
|
||||
.panel {
|
||||
border: solid $primary-lighten-3;
|
||||
padding: 1;
|
||||
margin: 0 1;
|
||||
border: solid #99ccff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-wide {
|
||||
border: solid $primary-lighten-3;
|
||||
padding: 1;
|
||||
margin: 0 1;
|
||||
border: solid #99ccff;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
text-align: center;
|
||||
text-style: bold;
|
||||
color: $accent;
|
||||
color: #00ffcc;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
/* Left Panel - Main Flow Table (expanded) */
|
||||
/* Left Panel - Main Flow Table (maximized) */
|
||||
#left-panel {
|
||||
width: 70%;
|
||||
background: $surface;
|
||||
width: 75%;
|
||||
background: #1a1a1a;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Right Panel - Details */
|
||||
/* Right Panel - Details (compact) */
|
||||
#right-panel {
|
||||
width: 30%;
|
||||
background: $surface;
|
||||
width: 25%;
|
||||
background: #1a1a1a;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Sparkline Charts */
|
||||
@@ -95,114 +79,118 @@ SparklineWidget {
|
||||
/* Enhanced Flow Table */
|
||||
#flows-data-table {
|
||||
height: 1fr;
|
||||
scrollbar-background: $surface-lighten-1;
|
||||
scrollbar-color: $primary;
|
||||
scrollbar-background: #262626;
|
||||
scrollbar-color: #0080ff;
|
||||
scrollbar-size: 1 1;
|
||||
}
|
||||
|
||||
#flows-data-table > .datatable--header {
|
||||
background: $surface-lighten-2;
|
||||
color: $accent;
|
||||
background: #333333;
|
||||
color: #00ffcc;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
#flows-data-table > .datatable--cursor {
|
||||
background: $primary 30%;
|
||||
color: $text;
|
||||
background: #0080ff 30%;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
#flows-data-table > .datatable--hover {
|
||||
background: $primary 20%;
|
||||
background: #0080ff 20%;
|
||||
}
|
||||
|
||||
#flows-data-table > .datatable--odd-row {
|
||||
background: $surface;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
#flows-data-table > .datatable--even-row {
|
||||
background: $surface-lighten-1;
|
||||
background: #262626;
|
||||
}
|
||||
|
||||
/* Flow Details Panel */
|
||||
/* Flow Details Panel - Compact */
|
||||
FlowDetailsPanel {
|
||||
padding: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
FlowDetailsPanel Panel {
|
||||
margin-bottom: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Status Colors */
|
||||
.status-normal {
|
||||
color: $success;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
color: $warning;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: $error;
|
||||
color: #ff3366;
|
||||
}
|
||||
|
||||
.status-enhanced {
|
||||
color: $accent;
|
||||
color: #00ffcc;
|
||||
text-style: bold;
|
||||
}
|
||||
|
||||
/* Quality Indicators */
|
||||
.quality-high {
|
||||
color: $success;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
.quality-medium {
|
||||
color: $warning;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
.quality-low {
|
||||
color: $error;
|
||||
color: #ff3366;
|
||||
}
|
||||
|
||||
/* Animations and Transitions */
|
||||
.updating {
|
||||
background: $primary 10%;
|
||||
background: #0080ff 10%;
|
||||
transition: background 200ms;
|
||||
}
|
||||
|
||||
/* Header and Footer */
|
||||
/* Header and Footer - Ultra compact */
|
||||
Header {
|
||||
background: $surface;
|
||||
color: $text;
|
||||
border-bottom: solid $primary;
|
||||
background: #1a1a1a;
|
||||
color: #ffffff;
|
||||
border-bottom: solid #0080ff;
|
||||
height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
Footer {
|
||||
background: $surface;
|
||||
color: $text-muted;
|
||||
border-top: solid $primary;
|
||||
background: #1a1a1a;
|
||||
color: #999999;
|
||||
border-top: solid #0080ff;
|
||||
height: 1;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Scrollbars */
|
||||
Vertical {
|
||||
scrollbar-size: 1 1;
|
||||
scrollbar-background: $surface-lighten-1;
|
||||
scrollbar-color: $primary;
|
||||
scrollbar-background: #262626;
|
||||
scrollbar-color: #0080ff;
|
||||
}
|
||||
|
||||
Horizontal {
|
||||
scrollbar-size: 1 1;
|
||||
scrollbar-background: $surface-lighten-1;
|
||||
scrollbar-color: $primary;
|
||||
scrollbar-background: #262626;
|
||||
scrollbar-color: #0080ff;
|
||||
}
|
||||
|
||||
/* Focus States */
|
||||
DataTable:focus {
|
||||
border: solid $accent;
|
||||
border: solid #00ffcc;
|
||||
}
|
||||
|
||||
/* Panel Borders */
|
||||
Static {
|
||||
border: round $primary;
|
||||
border: round #0080ff;
|
||||
}
|
||||
|
||||
/* End of styles */
|
||||
@@ -69,9 +69,11 @@ class FlowAnalysisWidget(Vertical):
|
||||
if not self.flow_table:
|
||||
return
|
||||
|
||||
# Preserve cursor position
|
||||
# Preserve cursor and scroll positions
|
||||
cursor_row = self.flow_table.cursor_row
|
||||
cursor_column = self.flow_table.cursor_column
|
||||
scroll_x = self.flow_table.scroll_x
|
||||
scroll_y = self.flow_table.scroll_y
|
||||
selected_row_key = None
|
||||
if self.flow_table.rows and cursor_row < len(self.flow_table.rows):
|
||||
selected_row_key = list(self.flow_table.rows.keys())[cursor_row]
|
||||
@@ -108,6 +110,9 @@ class FlowAnalysisWidget(Vertical):
|
||||
# If original selection not found, try to maintain row position
|
||||
new_row = min(cursor_row, self.flow_table.row_count - 1)
|
||||
self.flow_table.move_cursor(row=new_row, column=cursor_column, animate=False)
|
||||
|
||||
# Restore scroll position
|
||||
self.flow_table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
|
||||
|
||||
def _create_flow_row(self, flow_num: int, flow: 'FlowStats') -> List[Text]:
|
||||
"""Create main flow row with rich text formatting"""
|
||||
|
||||
@@ -29,11 +29,15 @@ class EnhancedFlowTable(Vertical):
|
||||
DEFAULT_CSS = """
|
||||
EnhancedFlowTable {
|
||||
height: 1fr;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
EnhancedFlowTable DataTable {
|
||||
height: 1fr;
|
||||
scrollbar-gutter: stable;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -62,17 +66,15 @@ class EnhancedFlowTable(Vertical):
|
||||
"""Initialize the table"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
# Add columns with explicit keys to avoid auto-generated keys
|
||||
table.add_column("#", width=3, key="num")
|
||||
table.add_column("Source", width=22, key="source")
|
||||
table.add_column("Proto", width=6, key="proto")
|
||||
table.add_column("Destination", width=22, key="dest")
|
||||
table.add_column("Extended", width=10, key="extended")
|
||||
table.add_column("Frame Type", width=12, key="frame_type")
|
||||
table.add_column("Rate", width=12, key="rate")
|
||||
table.add_column("Volume", width=12, key="volume")
|
||||
table.add_column("Quality", width=12, key="quality")
|
||||
table.add_column("Status", width=8, key="status")
|
||||
# Compact columns optimized for data density
|
||||
table.add_column("#", width=2, key="num")
|
||||
table.add_column("Source", width=18, key="source")
|
||||
table.add_column("Proto", width=4, key="proto")
|
||||
table.add_column("Destination", width=18, key="dest")
|
||||
table.add_column("Extended", width=8, key="extended")
|
||||
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")
|
||||
|
||||
self.refresh_data()
|
||||
|
||||
@@ -80,9 +82,11 @@ class EnhancedFlowTable(Vertical):
|
||||
"""Refresh flow table with enhanced visualizations"""
|
||||
table = self.query_one("#flows-data-table", DataTable)
|
||||
|
||||
# Preserve cursor position
|
||||
# Preserve cursor and scroll positions
|
||||
cursor_row = table.cursor_row
|
||||
cursor_column = table.cursor_column
|
||||
scroll_x = table.scroll_x
|
||||
scroll_y = table.scroll_y
|
||||
selected_row_key = None
|
||||
if table.rows and cursor_row < len(table.rows):
|
||||
selected_row_key = list(table.rows.keys())[cursor_row]
|
||||
@@ -148,6 +152,9 @@ class EnhancedFlowTable(Vertical):
|
||||
# If original selection not found, try to maintain row position
|
||||
new_row = min(cursor_row, table.row_count - 1)
|
||||
table.move_cursor(row=new_row, column=cursor_column, animate=False)
|
||||
|
||||
# Restore scroll position
|
||||
table.scroll_to(x=scroll_x, y=scroll_y, animate=False)
|
||||
|
||||
def _create_enhanced_row(self, num: int, flow: 'FlowStats', metrics: dict) -> List[Text]:
|
||||
"""Create enhanced row with inline visualizations"""
|
||||
@@ -177,10 +184,9 @@ class EnhancedFlowTable(Vertical):
|
||||
rate_spark = self._create_rate_sparkline(metrics['rate_history'])
|
||||
rate_text = Text(f"{metrics['rate_history'][-1]:.0f} {rate_spark}")
|
||||
|
||||
# Volume with bar chart
|
||||
volume_bar = self._create_volume_bar(flow.total_bytes)
|
||||
volume_value = self._format_bytes(flow.total_bytes)
|
||||
volume_text = Text(f"{volume_value:>6} {volume_bar}")
|
||||
# Size with actual value
|
||||
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)
|
||||
@@ -199,8 +205,7 @@ class EnhancedFlowTable(Vertical):
|
||||
|
||||
return [
|
||||
num_text, source_text, proto_text, dest_text,
|
||||
extended_text, frame_text, rate_text, volume_text,
|
||||
quality_text, status_text
|
||||
extended_text, frame_text, rate_text, size_text
|
||||
]
|
||||
|
||||
def _create_rate_sparkline(self, history: List[float]) -> str:
|
||||
@@ -308,12 +313,10 @@ class EnhancedFlowTable(Vertical):
|
||||
Text(""), # Empty source
|
||||
Text(""), # Empty protocol
|
||||
Text(""), # Empty destination
|
||||
Text(f" └─ {extended_proto}", style="dim yellow"),
|
||||
Text(f" {extended_proto}", style="dim yellow"),
|
||||
Text(frame_type, style="dim blue"),
|
||||
Text(f"{count}", style="dim", justify="right"),
|
||||
Text(f"{percentage:.0f}%", style="dim"),
|
||||
Text(""), # Empty quality
|
||||
Text("") # Empty status
|
||||
Text(f"{self._format_bytes(count * (flow.total_bytes // flow.frame_count) if flow.frame_count > 0 else 0):>8}", style="dim")
|
||||
]
|
||||
subrows.append(subrow)
|
||||
|
||||
|
||||
@@ -29,19 +29,23 @@ class MetricCard(Widget):
|
||||
MetricCard {
|
||||
width: 1fr;
|
||||
height: 3;
|
||||
margin: 0 1;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
MetricCard.success {
|
||||
border: solid $success;
|
||||
border: none;
|
||||
color: #00ff88;
|
||||
}
|
||||
|
||||
MetricCard.warning {
|
||||
border: solid $warning;
|
||||
border: none;
|
||||
color: #ffcc00;
|
||||
}
|
||||
|
||||
MetricCard.error {
|
||||
border: solid $error;
|
||||
border: none;
|
||||
color: #ff3366;
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -106,18 +110,15 @@ class MetricCard(Widget):
|
||||
if self.sparkline and self.spark_data:
|
||||
spark_str = " " + self._create_mini_spark()
|
||||
|
||||
# Format content
|
||||
# Ultra compact - single line format
|
||||
content = Text()
|
||||
content.append(f"{self.title}\n", style="dim")
|
||||
content.append(f"{self.title}: ", style="dim")
|
||||
content.append(f"{self.value}", style=f"bold {style}")
|
||||
content.append(trend_icon, style=style)
|
||||
content.append(spark_str, style="dim cyan")
|
||||
|
||||
return Panel(
|
||||
content,
|
||||
height=3,
|
||||
border_style=style if self.color != "normal" else "dim"
|
||||
)
|
||||
# Super compact - no panel, just text
|
||||
return content
|
||||
|
||||
def _create_mini_spark(self) -> str:
|
||||
"""Create mini sparkline for inline display"""
|
||||
|
||||
Reference in New Issue
Block a user