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.
This commit is contained in:
140
analyzer/tui/textual/widgets/metric_card.py
Normal file
140
analyzer/tui/textual/widgets/metric_card.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
Metric Card Widget - Compact metric display inspired by TipTop
|
||||
"""
|
||||
|
||||
from textual.widget import Widget
|
||||
from textual.reactive import reactive
|
||||
from rich.text import Text
|
||||
from rich.console import RenderableType
|
||||
from rich.panel import Panel
|
||||
from typing import Optional, Literal
|
||||
|
||||
|
||||
ColorType = Literal["normal", "success", "warning", "error"]
|
||||
TrendType = Literal["up", "down", "stable"]
|
||||
|
||||
|
||||
class MetricCard(Widget):
|
||||
"""
|
||||
Compact metric display card with optional sparkline
|
||||
|
||||
Features:
|
||||
- Title and value display
|
||||
- Color coding for status
|
||||
- Optional trend indicator
|
||||
- Optional inline sparkline
|
||||
"""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
MetricCard {
|
||||
width: 1fr;
|
||||
height: 3;
|
||||
margin: 0 1;
|
||||
}
|
||||
|
||||
MetricCard.success {
|
||||
border: solid $success;
|
||||
}
|
||||
|
||||
MetricCard.warning {
|
||||
border: solid $warning;
|
||||
}
|
||||
|
||||
MetricCard.error {
|
||||
border: solid $error;
|
||||
}
|
||||
"""
|
||||
|
||||
value = reactive("0")
|
||||
color = reactive("normal")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
title: str,
|
||||
value: str = "0",
|
||||
color: ColorType = "normal",
|
||||
trend: Optional[TrendType] = None,
|
||||
sparkline: bool = False,
|
||||
**kwargs
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.color = color
|
||||
self.trend = trend
|
||||
self.sparkline = sparkline
|
||||
self.spark_data = []
|
||||
|
||||
def update_value(self, new_value: str) -> None:
|
||||
"""Update the metric value"""
|
||||
self.value = new_value
|
||||
|
||||
def update_color(self, new_color: ColorType) -> None:
|
||||
"""Update the color status"""
|
||||
self.color = new_color
|
||||
self.add_class(new_color)
|
||||
|
||||
def add_spark_data(self, value: float) -> None:
|
||||
"""Add data point for sparkline"""
|
||||
self.spark_data.append(value)
|
||||
if len(self.spark_data) > 10: # Keep last 10 points
|
||||
self.spark_data.pop(0)
|
||||
|
||||
def render(self) -> RenderableType:
|
||||
"""Render the metric card"""
|
||||
# Determine color style
|
||||
color_map = {
|
||||
"normal": "white",
|
||||
"success": "green",
|
||||
"warning": "yellow",
|
||||
"error": "red"
|
||||
}
|
||||
style = color_map.get(self.color, "white")
|
||||
|
||||
# Create trend indicator
|
||||
trend_icon = ""
|
||||
if self.trend:
|
||||
trend_map = {
|
||||
"up": "↑",
|
||||
"down": "↓",
|
||||
"stable": "→"
|
||||
}
|
||||
trend_icon = f" {trend_map.get(self.trend, '')}"
|
||||
|
||||
# Create sparkline if enabled
|
||||
spark_str = ""
|
||||
if self.sparkline and self.spark_data:
|
||||
spark_str = " " + self._create_mini_spark()
|
||||
|
||||
# Format content
|
||||
content = Text()
|
||||
content.append(f"{self.title}\n", 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"
|
||||
)
|
||||
|
||||
def _create_mini_spark(self) -> str:
|
||||
"""Create mini sparkline for inline display"""
|
||||
if not self.spark_data:
|
||||
return ""
|
||||
|
||||
spark_chars = " ▁▂▃▄▅▆▇█"
|
||||
data_min = min(self.spark_data)
|
||||
data_max = max(self.spark_data)
|
||||
|
||||
if data_max == data_min:
|
||||
return "▄" * len(self.spark_data)
|
||||
|
||||
result = []
|
||||
for value in self.spark_data:
|
||||
normalized = (value - data_min) / (data_max - data_min)
|
||||
char_index = int(normalized * 8)
|
||||
result.append(spark_chars[char_index])
|
||||
|
||||
return "".join(result)
|
||||
Reference in New Issue
Block a user