1use crate::common::render_vertical_scrollbar;
2use crate::ui::format_title;
3use ratatui::prelude::*;
4use ratatui::widgets::{Axis, Block, BorderType, Borders, Chart, Dataset, GraphType, Paragraph};
5
6pub trait MonitoringState {
8 fn is_metrics_loading(&self) -> bool;
9 fn set_metrics_loading(&mut self, loading: bool);
10 fn monitoring_scroll(&self) -> usize;
11 fn set_monitoring_scroll(&mut self, scroll: usize);
12 fn clear_metrics(&mut self);
13}
14
15pub struct MetricChart<'a> {
16 pub title: &'a str,
17 pub data: &'a [(i64, f64)],
18 pub y_axis_label: &'a str,
19 pub x_axis_label: Option<String>,
20}
21
22pub struct MultiDatasetChart<'a> {
23 pub title: &'a str,
24 pub datasets: Vec<(&'a str, &'a [(i64, f64)])>,
25 pub y_axis_label: &'a str,
26 pub y_axis_step: u32,
27 pub x_axis_label: Option<String>,
28}
29
30pub struct DualAxisChart<'a> {
31 pub title: &'a str,
32 pub left_dataset: (&'a str, &'a [(i64, f64)]),
33 pub right_dataset: (&'a str, &'a [(i64, f64)]),
34 pub left_y_label: &'a str,
35 pub right_y_label: &'a str,
36 pub x_axis_label: Option<String>,
37}
38
39pub fn render_monitoring_tab(
40 frame: &mut Frame,
41 area: Rect,
42 single_charts: &[MetricChart],
43 multi_charts: &[MultiDatasetChart],
44 dual_charts: &[DualAxisChart],
45 trailing_single_charts: &[MetricChart],
46 scroll_position: usize,
47) {
48 let available_height = area.height as usize;
49 let total_charts =
50 single_charts.len() + multi_charts.len() + dual_charts.len() + trailing_single_charts.len();
51
52 let mut y_offset = 0;
53 let mut chart_idx = 0;
54
55 for chart in single_charts.iter() {
56 if chart_idx < scroll_position {
57 chart_idx += 1;
58 continue;
59 }
60 if y_offset + 14 > available_height {
61 break;
62 }
63 let chart_height = 20.min((available_height - y_offset) as u16);
64 let chart_rect = Rect {
65 x: area.x,
66 y: area.y + y_offset as u16,
67 width: area.width.saturating_sub(1),
68 height: chart_height,
69 };
70 render_chart(frame, chart, chart_rect);
71 y_offset += 20;
72 chart_idx += 1;
73 }
74
75 for chart in multi_charts.iter() {
76 if chart_idx < scroll_position {
77 chart_idx += 1;
78 continue;
79 }
80 if y_offset + 14 > available_height {
81 break;
82 }
83 let chart_height = 20.min((available_height - y_offset) as u16);
84 let chart_rect = Rect {
85 x: area.x,
86 y: area.y + y_offset as u16,
87 width: area.width.saturating_sub(1),
88 height: chart_height,
89 };
90 render_multi_dataset_chart(frame, chart, chart_rect);
91 y_offset += 20;
92 chart_idx += 1;
93 }
94
95 for chart in dual_charts.iter() {
96 if chart_idx < scroll_position {
97 chart_idx += 1;
98 continue;
99 }
100 if y_offset + 14 > available_height {
101 break;
102 }
103 let chart_height = 20.min((available_height - y_offset) as u16);
104 let chart_rect = Rect {
105 x: area.x,
106 y: area.y + y_offset as u16,
107 width: area.width.saturating_sub(1),
108 height: chart_height,
109 };
110 render_dual_axis_chart(frame, chart, chart_rect);
111 y_offset += 20;
112 chart_idx += 1;
113 }
114
115 for chart in trailing_single_charts.iter() {
116 if chart_idx < scroll_position {
117 chart_idx += 1;
118 continue;
119 }
120 if y_offset + 14 > available_height {
121 break;
122 }
123 let chart_height = 20.min((available_height - y_offset) as u16);
124 let chart_rect = Rect {
125 x: area.x,
126 y: area.y + y_offset as u16,
127 width: area.width.saturating_sub(1),
128 height: chart_height,
129 };
130 render_chart(frame, chart, chart_rect);
131 y_offset += 20;
132 chart_idx += 1;
133 }
134
135 let total_height = total_charts * 20;
136 let scroll_offset = scroll_position * 20;
137 render_vertical_scrollbar(frame, area, total_height, scroll_offset);
138}
139
140fn render_chart(frame: &mut Frame, chart: &MetricChart, area: Rect) {
141 let block = Block::default()
142 .title(format_title(chart.title))
143 .borders(Borders::ALL)
144 .border_type(BorderType::Rounded)
145 .border_style(Style::default().fg(Color::Gray));
146
147 if chart.data.is_empty() {
148 let inner = block.inner(area);
149 frame.render_widget(block, area);
150 let paragraph = Paragraph::new("--");
151 frame.render_widget(paragraph, inner);
152 return;
153 }
154
155 let data: Vec<(f64, f64)> = chart
156 .data
157 .iter()
158 .map(|(timestamp, value)| (*timestamp as f64, *value))
159 .collect();
160
161 let min_x = data.iter().map(|(x, _)| *x).fold(f64::INFINITY, f64::min);
162 let max_x = data
163 .iter()
164 .map(|(x, _)| *x)
165 .fold(f64::NEG_INFINITY, f64::max);
166 let max_y = data
167 .iter()
168 .map(|(_, y)| *y)
169 .fold(0.0_f64, f64::max)
170 .max(1.0);
171
172 let dataset = Dataset::default()
173 .name(chart.title)
174 .marker(symbols::Marker::Braille)
175 .graph_type(GraphType::Line)
176 .style(Style::default().fg(Color::Cyan))
177 .data(&data);
178
179 let x_labels: Vec<Span> = {
180 let mut labels = Vec::new();
181 let step = 1800;
182 let mut current = (min_x as i64 / step) * step;
183 while current <= max_x as i64 {
184 let time = chrono::DateTime::from_timestamp(current, 0)
185 .unwrap_or_default()
186 .format("%H:%M")
187 .to_string();
188 labels.push(Span::raw(time));
189 current += step;
190 }
191 labels
192 };
193
194 let mut x_axis = Axis::default()
195 .style(Style::default().fg(Color::Gray))
196 .bounds([min_x, max_x])
197 .labels(x_labels);
198
199 if let Some(label) = &chart.x_axis_label {
200 x_axis = x_axis.title(label.as_str());
201 }
202
203 let y_labels: Vec<Span> = {
204 let mut labels = Vec::new();
205 let mut current = 0.0;
206 let max = max_y * 1.1;
207 let step = if max <= 10.0 {
208 0.5
209 } else {
210 (max / 10.0).ceil()
211 };
212 while current <= max {
213 labels.push(Span::raw(format!("{:.1}", current)));
214 current += step;
215 }
216 labels
217 };
218
219 let y_axis = Axis::default()
220 .title(chart.y_axis_label)
221 .style(Style::default().fg(Color::Gray))
222 .bounds([0.0, max_y * 1.1])
223 .labels(y_labels);
224
225 let chart_widget = Chart::new(vec![dataset])
226 .block(block)
227 .x_axis(x_axis)
228 .y_axis(y_axis);
229
230 frame.render_widget(chart_widget, area);
231}
232
233fn render_multi_dataset_chart(frame: &mut Frame, chart: &MultiDatasetChart, area: Rect) {
234 let block = Block::default()
235 .title(format_title(chart.title))
236 .borders(Borders::ALL)
237 .border_type(BorderType::Rounded)
238 .border_style(Style::default().fg(Color::Gray));
239
240 let all_empty = chart.datasets.iter().all(|(_, data)| data.is_empty());
241 if all_empty {
242 let inner = block.inner(area);
243 frame.render_widget(block, area);
244 let paragraph = Paragraph::new("--");
245 frame.render_widget(paragraph, inner);
246 return;
247 }
248
249 let colors = [Color::Cyan, Color::Yellow, Color::Magenta];
250 let mut converted_data: Vec<Vec<(f64, f64)>> = Vec::new();
251 let mut min_x = f64::INFINITY;
252 let mut max_x = f64::NEG_INFINITY;
253 let mut max_y = 0.0_f64;
254
255 for (_, data) in chart.datasets.iter() {
256 if data.is_empty() {
257 converted_data.push(Vec::new());
258 continue;
259 }
260 let converted: Vec<(f64, f64)> = data
261 .iter()
262 .map(|(timestamp, value)| (*timestamp as f64, *value))
263 .collect();
264
265 min_x = min_x.min(
266 converted
267 .iter()
268 .map(|(x, _)| *x)
269 .fold(f64::INFINITY, f64::min),
270 );
271 max_x = max_x.max(
272 converted
273 .iter()
274 .map(|(x, _)| *x)
275 .fold(f64::NEG_INFINITY, f64::max),
276 );
277 max_y = max_y.max(converted.iter().map(|(_, y)| *y).fold(0.0_f64, f64::max));
278
279 converted_data.push(converted);
280 }
281
282 let mut datasets_vec = Vec::new();
283 for (idx, ((name, _), data)) in chart.datasets.iter().zip(converted_data.iter()).enumerate() {
284 if data.is_empty() {
285 continue;
286 }
287 let dataset = Dataset::default()
288 .name(*name)
289 .marker(symbols::Marker::Braille)
290 .graph_type(GraphType::Line)
291 .style(Style::default().fg(colors[idx % colors.len()]))
292 .data(data);
293
294 datasets_vec.push(dataset);
295 }
296
297 max_y = max_y.max(1.0);
298
299 let x_labels: Vec<Span> = {
300 let mut labels = Vec::new();
301 let step = 1800;
302 let mut current = (min_x as i64 / step) * step;
303 while current <= max_x as i64 {
304 let time = chrono::DateTime::from_timestamp(current, 0)
305 .unwrap_or_default()
306 .format("%H:%M")
307 .to_string();
308 labels.push(Span::raw(time));
309 current += step;
310 }
311 labels
312 };
313
314 let mut x_axis = Axis::default()
315 .style(Style::default().fg(Color::Gray))
316 .bounds([min_x, max_x])
317 .labels(x_labels);
318
319 if let Some(label) = &chart.x_axis_label {
320 x_axis = x_axis.title(label.as_str());
321 }
322
323 let y_labels: Vec<Span> = {
324 let mut labels = Vec::new();
325 let mut current = 0.0;
326 let max = max_y * 1.1;
327 let step = chart.y_axis_step as f64;
328 while current <= max {
329 let label = if step >= 1000.0 {
330 format!("{}K", (current / 1000.0) as u32)
331 } else {
332 format!("{:.0}", current)
333 };
334 labels.push(Span::raw(label));
335 current += step;
336 }
337 labels
338 };
339
340 let y_axis = Axis::default()
341 .title(chart.y_axis_label)
342 .style(Style::default().fg(Color::Gray))
343 .bounds([0.0, max_y * 1.1])
344 .labels(y_labels);
345
346 let chart_widget = Chart::new(datasets_vec)
347 .block(block)
348 .x_axis(x_axis)
349 .y_axis(y_axis);
350
351 frame.render_widget(chart_widget, area);
352}
353
354fn render_dual_axis_chart(frame: &mut Frame, chart: &DualAxisChart, area: Rect) {
355 let block = Block::default()
356 .title(format_title(chart.title))
357 .borders(Borders::ALL)
358 .border_type(BorderType::Rounded)
359 .border_style(Style::default().fg(Color::Gray));
360
361 let (left_name, left_data) = chart.left_dataset;
362 let (right_name, right_data) = chart.right_dataset;
363
364 if left_data.is_empty() && right_data.is_empty() {
365 let inner = block.inner(area);
366 frame.render_widget(block, area);
367 let paragraph = Paragraph::new("--");
368 frame.render_widget(paragraph, inner);
369 return;
370 }
371
372 let left_converted: Vec<(f64, f64)> = left_data
373 .iter()
374 .map(|(timestamp, value)| (*timestamp as f64, *value))
375 .collect();
376
377 let right_converted: Vec<(f64, f64)> = right_data
378 .iter()
379 .map(|(timestamp, value)| (*timestamp as f64, *value))
380 .collect();
381
382 let mut min_x = f64::INFINITY;
383 let mut max_x = f64::NEG_INFINITY;
384 let mut max_left_y = 0.0_f64;
385 let max_right_y = 100.0;
386
387 if !left_converted.is_empty() {
388 min_x = min_x.min(
389 left_converted
390 .iter()
391 .map(|(x, _)| *x)
392 .fold(f64::INFINITY, f64::min),
393 );
394 max_x = max_x.max(
395 left_converted
396 .iter()
397 .map(|(x, _)| *x)
398 .fold(f64::NEG_INFINITY, f64::max),
399 );
400 max_left_y = left_converted
401 .iter()
402 .map(|(_, y)| *y)
403 .fold(0.0_f64, f64::max);
404 }
405
406 if !right_converted.is_empty() {
407 min_x = min_x.min(
408 right_converted
409 .iter()
410 .map(|(x, _)| *x)
411 .fold(f64::INFINITY, f64::min),
412 );
413 max_x = max_x.max(
414 right_converted
415 .iter()
416 .map(|(x, _)| *x)
417 .fold(f64::NEG_INFINITY, f64::max),
418 );
419 }
420
421 max_left_y = max_left_y.max(1.0);
422
423 let normalized_right: Vec<(f64, f64)> = right_converted
424 .iter()
425 .map(|(x, y)| (*x, y * max_left_y / max_right_y))
426 .collect();
427
428 let left_dataset = Dataset::default()
429 .name(left_name)
430 .marker(symbols::Marker::Braille)
431 .graph_type(GraphType::Line)
432 .style(Style::default().fg(Color::Red))
433 .data(&left_converted);
434
435 let right_dataset = Dataset::default()
436 .name(right_name)
437 .marker(symbols::Marker::Braille)
438 .graph_type(GraphType::Line)
439 .style(Style::default().fg(Color::Green))
440 .data(&normalized_right);
441
442 let x_labels: Vec<Span> = {
443 let mut labels = Vec::new();
444 let step = 1800;
445 let mut current = (min_x as i64 / step) * step;
446 while current <= max_x as i64 {
447 let time = chrono::DateTime::from_timestamp(current, 0)
448 .unwrap_or_default()
449 .format("%H:%M")
450 .to_string();
451 labels.push(Span::raw(time));
452 current += step;
453 }
454 labels
455 };
456
457 let mut x_axis = Axis::default()
458 .style(Style::default().fg(Color::Gray))
459 .bounds([min_x, max_x])
460 .labels(x_labels);
461
462 if let Some(label) = &chart.x_axis_label {
463 x_axis = x_axis.title(label.as_str());
464 }
465
466 let y_labels: Vec<Span> = {
467 let mut labels = Vec::new();
468 let mut current = 0.0;
469 let max = max_left_y * 1.1;
470 let step = if max <= 10.0 {
471 0.5
472 } else {
473 (max / 10.0).ceil()
474 };
475 while current <= max {
476 labels.push(Span::raw(format!("{:.0}", current)));
477 current += step;
478 }
479 labels
480 };
481
482 let y_axis = Axis::default()
483 .title(chart.left_y_label)
484 .style(Style::default().fg(Color::Gray))
485 .bounds([0.0, max_left_y * 1.1])
486 .labels(y_labels);
487
488 let chart_widget = Chart::new(vec![left_dataset, right_dataset])
489 .block(block)
490 .x_axis(x_axis)
491 .y_axis(y_axis);
492
493 frame.render_widget(chart_widget, area);
494}
495
496#[cfg(test)]
497mod tests {
498 use super::*;
499
500 #[test]
501 fn test_metric_chart_creation() {
502 let data = vec![(1700000000, 5.0), (1700000060, 10.0)];
503 let chart = MetricChart {
504 title: "Test Metric",
505 data: &data,
506 y_axis_label: "Count",
507 x_axis_label: None,
508 };
509 assert_eq!(chart.title, "Test Metric");
510 assert_eq!(chart.data.len(), 2);
511 assert_eq!(chart.y_axis_label, "Count");
512 assert_eq!(chart.x_axis_label, None);
513 }
514
515 #[test]
516 fn test_empty_chart_data() {
517 let data: Vec<(i64, f64)> = vec![];
518 let chart = MetricChart {
519 title: "Empty Chart",
520 data: &data,
521 y_axis_label: "Value",
522 x_axis_label: None,
523 };
524 assert!(chart.data.is_empty());
525 }
526
527 #[test]
528 fn test_metric_chart_with_x_axis_label() {
529 let data = vec![(1700000000, 5.0), (1700000060, 10.0)];
530 let chart = MetricChart {
531 title: "Invocations",
532 data: &data,
533 y_axis_label: "Count",
534 x_axis_label: Some("Invocations [sum: 15]".to_string()),
535 };
536 assert_eq!(
537 chart.x_axis_label,
538 Some("Invocations [sum: 15]".to_string())
539 );
540 }
541
542 #[test]
543 fn test_multi_dataset_chart_creation() {
544 let min_data = vec![(1700000000, 100.0), (1700000060, 150.0)];
545 let avg_data = vec![(1700000000, 200.0), (1700000060, 250.0)];
546 let max_data = vec![(1700000000, 300.0), (1700000060, 350.0)];
547
548 let chart = MultiDatasetChart {
549 title: "Duration",
550 datasets: vec![
551 ("Minimum", &min_data),
552 ("Average", &avg_data),
553 ("Maximum", &max_data),
554 ],
555 y_axis_label: "Milliseconds",
556 y_axis_step: 1000,
557 x_axis_label: Some("Minimum [100], Average [200], Maximum [300]".to_string()),
558 };
559
560 assert_eq!(chart.title, "Duration");
561 assert_eq!(chart.datasets.len(), 3);
562 assert_eq!(chart.y_axis_label, "Milliseconds");
563 assert_eq!(chart.y_axis_step, 1000);
564 }
565
566 #[test]
567 fn test_multi_dataset_chart_empty() {
568 let empty: Vec<(i64, f64)> = vec![];
569 let chart = MultiDatasetChart {
570 title: "Empty Duration",
571 datasets: vec![
572 ("Minimum", &empty),
573 ("Average", &empty),
574 ("Maximum", &empty),
575 ],
576 y_axis_label: "Milliseconds",
577 y_axis_step: 1000,
578 x_axis_label: None,
579 };
580
581 assert!(chart.datasets.iter().all(|(_, data)| data.is_empty()));
582 }
583
584 #[test]
585 fn test_duration_label_format() {
586 let min = 100.0;
587 let avg = 200.5;
588 let max = 350.0;
589 let label = format!(
590 "Minimum [{:.0}], Average [{:.0}], Maximum [{:.0}]",
591 min, avg, max
592 );
593 assert_eq!(label, "Minimum [100], Average [200], Maximum [350]");
594 }
595
596 #[test]
597 fn test_y_axis_label_formatting_1k() {
598 let value = 1000.0;
599 let label = format!("{}K", (value / 1000.0) as u32);
600 assert_eq!(label, "1K");
601 }
602
603 #[test]
604 fn test_y_axis_label_formatting_5k() {
605 let value = 5000.0;
606 let label = format!("{}K", (value / 1000.0) as u32);
607 assert_eq!(label, "5K");
608 }
609
610 #[test]
611 fn test_y_axis_step_1000() {
612 let step = 1000;
613 assert_eq!(step, 1000);
614 let values = [0, 1000, 2000, 3000, 4000, 5000];
615 for (i, val) in values.iter().enumerate() {
616 assert_eq!(*val, i * step);
617 }
618 }
619
620 #[test]
621 fn test_duration_min_calculation() {
622 let data = [(1700000000, 100.0), (1700000060, 50.0), (1700000120, 75.0)];
623 let min: f64 = data
624 .iter()
625 .map(|(_, v)| v)
626 .fold(f64::INFINITY, |a, &b| a.min(b));
627 assert_eq!(min, 50.0);
628 }
629
630 #[test]
631 fn test_duration_avg_calculation() {
632 let data = [
633 (1700000000, 100.0),
634 (1700000060, 200.0),
635 (1700000120, 300.0),
636 ];
637 let avg: f64 = data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64;
638 assert_eq!(avg, 200.0);
639 }
640
641 #[test]
642 fn test_duration_max_calculation() {
643 let data = [
644 (1700000000, 100.0),
645 (1700000060, 350.0),
646 (1700000120, 200.0),
647 ];
648 let max: f64 = data
649 .iter()
650 .map(|(_, v)| v)
651 .fold(f64::NEG_INFINITY, |a, &b| a.max(b));
652 assert_eq!(max, 350.0);
653 }
654
655 #[test]
656 fn test_duration_empty_data_min() {
657 let data: Vec<(i64, f64)> = vec![];
658 let min: f64 = data
659 .iter()
660 .map(|(_, v)| v)
661 .fold(f64::INFINITY, |a, &b| a.min(b));
662 assert!(min.is_infinite() && min.is_sign_positive());
663 }
664
665 #[test]
666 fn test_duration_empty_data_avg() {
667 let data: Vec<(i64, f64)> = vec![];
668 let avg: f64 = if !data.is_empty() {
669 data.iter().map(|(_, v)| v).sum::<f64>() / data.len() as f64
670 } else {
671 0.0
672 };
673 assert_eq!(avg, 0.0);
674 }
675
676 #[test]
677 fn test_dual_axis_chart_creation() {
678 let errors = vec![(1700000000, 5.0), (1700000060, 10.0)];
679 let success_rate = vec![(1700000000, 95.0), (1700000060, 90.0)];
680
681 let chart = DualAxisChart {
682 title: "Error count and success rate",
683 left_dataset: ("Errors", &errors),
684 right_dataset: ("Success rate", &success_rate),
685 left_y_label: "Count",
686 right_y_label: "%",
687 x_axis_label: Some("Errors [max: 10] and Success rate [min: 90%]".to_string()),
688 };
689
690 assert_eq!(chart.title, "Error count and success rate");
691 assert_eq!(chart.left_y_label, "Count");
692 assert_eq!(chart.right_y_label, "%");
693 }
694
695 #[test]
696 fn test_dual_axis_normalization() {
697 let max_left_y = 10.0;
698 let max_right_y = 100.0;
699 let right_value = 95.0;
700 let normalized = right_value * max_left_y / max_right_y;
701 assert_eq!(normalized, 9.5);
702 }
703
704 #[test]
705 fn test_chart_renders_with_14_lines_available() {
706 let available_height = 34; let y_offset = 20; assert!(y_offset + 14 <= available_height);
712 }
713
714 #[test]
715 fn test_chart_skips_with_13_lines_available() {
716 let available_height = 33; let y_offset = 20; assert!(y_offset + 14 > available_height);
722 }
723}