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