1use anyhow::{anyhow, Result};
14use chrono::{DateTime, Utc};
15use dashmap::DashMap;
16use scirs2_core::ndarray_ext::Array2;
17use serde::{Deserialize, Serialize};
18use std::collections::{HashMap, VecDeque};
19use std::sync::Arc;
20use std::time::Duration;
21use tokio::sync::RwLock;
22use tracing::info;
23
24#[derive(Clone)]
26pub struct AdvancedVisualization {
27 #[allow(dead_code)]
28 config: VisualizationConfig,
29 dashboards: Arc<DashMap<String, Dashboard>>,
30 metrics_collector: Arc<MetricsCollector>,
31 chart_generator: Arc<ChartGenerator>,
32 topology_visualizer: Arc<TopologyVisualizer>,
33 alert_visualizer: Arc<AlertVisualizer>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
38pub struct VisualizationConfig {
39 pub enable_realtime: bool,
41 pub update_interval: Duration,
43 pub default_theme: ChartTheme,
45 pub max_data_points: usize,
47 pub enable_export: bool,
49 pub default_export_format: ExportFormat,
51}
52
53impl Default for VisualizationConfig {
54 fn default() -> Self {
55 Self {
56 enable_realtime: true,
57 update_interval: Duration::from_secs(5),
58 default_theme: ChartTheme::Dark,
59 max_data_points: 1000,
60 enable_export: true,
61 default_export_format: ExportFormat::SVG,
62 }
63 }
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub enum ChartTheme {
69 Light,
70 Dark,
71 HighContrast,
72 Custom(CustomTheme),
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct CustomTheme {
78 pub background_color: String,
79 pub text_color: String,
80 pub primary_color: String,
81 pub secondary_color: String,
82 pub grid_color: String,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
87pub enum ExportFormat {
88 PNG,
89 SVG,
90 JSON,
91 CSV,
92 PDF,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct Dashboard {
98 pub id: String,
99 pub name: String,
100 pub description: String,
101 pub layout: DashboardLayout,
102 pub widgets: Vec<Widget>,
103 pub created_at: DateTime<Utc>,
104 pub last_updated: DateTime<Utc>,
105 pub auto_refresh: bool,
106 pub refresh_interval: Duration,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum DashboardLayout {
112 Grid { rows: usize, cols: usize },
113 Flexible,
114 SingleColumn,
115 TwoColumn,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct Widget {
121 pub id: String,
122 pub widget_type: WidgetType,
123 pub title: String,
124 pub position: WidgetPosition,
125 pub size: WidgetSize,
126 pub data_source: DataSource,
127 pub visualization_type: VisualizationType,
128 pub config: WidgetConfig,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct WidgetPosition {
134 pub row: usize,
135 pub col: usize,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct WidgetSize {
141 pub width: usize, pub height: usize, }
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub enum WidgetType {
148 Chart,
149 Table,
150 Map,
151 Topology,
152 Alert,
153 Metric,
154 Text,
155}
156
157#[derive(Debug, Clone, Serialize, Deserialize)]
159pub enum DataSource {
160 QueryMetrics,
161 FederationTopology,
162 SecurityAlerts,
163 ComplianceStatus,
164 PerformanceMetrics,
165 ServiceHealth,
166 Custom(String),
167}
168
169#[derive(Debug, Clone, Serialize, Deserialize)]
171pub enum VisualizationType {
172 LineChart,
173 BarChart,
174 PieChart,
175 ScatterPlot,
176 Heatmap,
177 NetworkGraph,
178 Gauge,
179 Table,
180 TreeMap,
181 Sankey,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct WidgetConfig {
187 pub show_legend: bool,
188 pub show_grid: bool,
189 pub x_axis_label: Option<String>,
190 pub y_axis_label: Option<String>,
191 pub color_scheme: Vec<String>,
192 pub custom_options: HashMap<String, serde_json::Value>,
193}
194
195impl Default for WidgetConfig {
196 fn default() -> Self {
197 Self {
198 show_legend: true,
199 show_grid: true,
200 x_axis_label: None,
201 y_axis_label: None,
202 color_scheme: vec![
203 "#1f77b4".to_string(),
204 "#ff7f0e".to_string(),
205 "#2ca02c".to_string(),
206 "#d62728".to_string(),
207 "#9467bd".to_string(),
208 ],
209 custom_options: HashMap::new(),
210 }
211 }
212}
213
214pub struct MetricsCollector {
216 time_series: Arc<RwLock<HashMap<String, TimeSeries>>>,
217 #[allow(dead_code)]
218 aggregations: Arc<RwLock<HashMap<String, MetricAggregation>>>,
219}
220
221impl Default for MetricsCollector {
222 fn default() -> Self {
223 Self {
224 time_series: Arc::new(RwLock::new(HashMap::new())),
225 aggregations: Arc::new(RwLock::new(HashMap::new())),
226 }
227 }
228}
229
230impl MetricsCollector {
231 pub fn new() -> Self {
232 Self::default()
233 }
234
235 pub async fn record_metric(&self, metric_name: &str, value: f64) -> Result<()> {
237 let mut time_series = self.time_series.write().await;
238 let series = time_series
239 .entry(metric_name.to_string())
240 .or_insert_with(|| TimeSeries {
241 name: metric_name.to_string(),
242 data_points: VecDeque::new(),
243 unit: "".to_string(),
244 });
245
246 series.data_points.push_back(DataPoint {
247 timestamp: Utc::now(),
248 value,
249 });
250
251 if series.data_points.len() > 1000 {
253 series.data_points.pop_front();
254 }
255
256 Ok(())
257 }
258
259 pub async fn get_time_series(&self, metric_name: &str) -> Option<TimeSeries> {
261 let time_series = self.time_series.read().await;
262 time_series.get(metric_name).cloned()
263 }
264
265 pub async fn calculate_aggregation(
267 &self,
268 metric_name: &str,
269 aggregation_type: AggregationType,
270 window: Duration,
271 ) -> Result<f64> {
272 let time_series = self.time_series.read().await;
273 let series = time_series
274 .get(metric_name)
275 .ok_or_else(|| anyhow!("Metric not found: {}", metric_name))?;
276
277 let cutoff = Utc::now() - chrono::Duration::from_std(window)?;
278 let recent_values: Vec<f64> = series
279 .data_points
280 .iter()
281 .filter(|dp| dp.timestamp >= cutoff)
282 .map(|dp| dp.value)
283 .collect();
284
285 if recent_values.is_empty() {
286 return Ok(0.0);
287 }
288
289 let result = match aggregation_type {
290 AggregationType::Average => {
291 recent_values.iter().sum::<f64>() / recent_values.len() as f64
292 }
293 AggregationType::Sum => recent_values.iter().sum(),
294 AggregationType::Min => recent_values.iter().cloned().fold(f64::INFINITY, f64::min),
295 AggregationType::Max => recent_values
296 .iter()
297 .cloned()
298 .fold(f64::NEG_INFINITY, f64::max),
299 AggregationType::Count => recent_values.len() as f64,
300 AggregationType::Percentile(p) => {
301 let mut sorted = recent_values.clone();
302 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
303 let index = ((p / 100.0) * sorted.len() as f64) as usize;
304 sorted[index.min(sorted.len() - 1)]
305 }
306 };
307
308 Ok(result)
309 }
310}
311
312#[derive(Debug, Clone)]
314pub struct TimeSeries {
315 pub name: String,
316 pub data_points: VecDeque<DataPoint>,
317 pub unit: String,
318}
319
320#[derive(Debug, Clone)]
322pub struct DataPoint {
323 pub timestamp: DateTime<Utc>,
324 pub value: f64,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329pub enum AggregationType {
330 Average,
331 Sum,
332 Min,
333 Max,
334 Count,
335 Percentile(f64),
336}
337
338#[derive(Debug, Clone)]
340pub struct MetricAggregation {
341 pub metric_name: String,
342 pub aggregation_type: AggregationType,
343 pub value: f64,
344 pub calculated_at: DateTime<Utc>,
345}
346
347pub struct ChartGenerator {
349 #[allow(dead_code)]
350 theme: ChartTheme,
351}
352
353impl ChartGenerator {
354 pub fn new(theme: ChartTheme) -> Self {
355 Self { theme }
356 }
357
358 pub async fn generate_line_chart(
360 &self,
361 data: &TimeSeries,
362 config: &WidgetConfig,
363 ) -> Result<ChartData> {
364 let points: Vec<(f64, f64)> = data
365 .data_points
366 .iter()
367 .enumerate()
368 .map(|(i, dp)| (i as f64, dp.value))
369 .collect();
370
371 Ok(ChartData {
372 chart_type: VisualizationType::LineChart,
373 series: vec![ChartSeries {
374 name: data.name.clone(),
375 data: points,
376 color: config.color_scheme.first().cloned(),
377 }],
378 x_axis_label: config.x_axis_label.clone(),
379 y_axis_label: config.y_axis_label.clone(),
380 show_legend: config.show_legend,
381 show_grid: config.show_grid,
382 })
383 }
384
385 pub async fn generate_bar_chart(
387 &self,
388 _categories: Vec<String>,
389 values: Vec<f64>,
390 config: &WidgetConfig,
391 ) -> Result<ChartData> {
392 let points: Vec<(f64, f64)> = values
393 .iter()
394 .enumerate()
395 .map(|(i, &v)| (i as f64, v))
396 .collect();
397
398 Ok(ChartData {
399 chart_type: VisualizationType::BarChart,
400 series: vec![ChartSeries {
401 name: "Values".to_string(),
402 data: points,
403 color: config.color_scheme.first().cloned(),
404 }],
405 x_axis_label: config.x_axis_label.clone(),
406 y_axis_label: config.y_axis_label.clone(),
407 show_legend: config.show_legend,
408 show_grid: config.show_grid,
409 })
410 }
411
412 pub async fn generate_pie_chart(
414 &self,
415 labels: Vec<String>,
416 values: Vec<f64>,
417 config: &WidgetConfig,
418 ) -> Result<PieChartData> {
419 let total: f64 = values.iter().sum();
420 let slices: Vec<PieSlice> = labels
421 .into_iter()
422 .zip(values.iter())
423 .enumerate()
424 .map(|(i, (label, &value))| PieSlice {
425 label,
426 value,
427 percentage: (value / total) * 100.0,
428 color: config
429 .color_scheme
430 .get(i % config.color_scheme.len())
431 .cloned(),
432 })
433 .collect();
434
435 Ok(PieChartData {
436 slices,
437 show_legend: config.show_legend,
438 })
439 }
440
441 pub async fn generate_heatmap(
443 &self,
444 data: Array2<f64>,
445 row_labels: Vec<String>,
446 col_labels: Vec<String>,
447 _config: &WidgetConfig,
448 ) -> Result<HeatmapData> {
449 let (rows, cols) = data.dim();
450 let mut cells = Vec::new();
451
452 for i in 0..rows {
453 for j in 0..cols {
454 cells.push(HeatmapCell {
455 row: i,
456 col: j,
457 value: data[[i, j]],
458 });
459 }
460 }
461
462 Ok(HeatmapData {
463 cells,
464 row_labels,
465 col_labels,
466 color_scale: ColorScale::Viridis,
467 })
468 }
469}
470
471#[derive(Debug, Clone, Serialize)]
473pub struct ChartData {
474 pub chart_type: VisualizationType,
475 pub series: Vec<ChartSeries>,
476 pub x_axis_label: Option<String>,
477 pub y_axis_label: Option<String>,
478 pub show_legend: bool,
479 pub show_grid: bool,
480}
481
482#[derive(Debug, Clone, Serialize)]
484pub struct ChartSeries {
485 pub name: String,
486 pub data: Vec<(f64, f64)>,
487 pub color: Option<String>,
488}
489
490#[derive(Debug, Clone, Serialize)]
492pub struct PieChartData {
493 pub slices: Vec<PieSlice>,
494 pub show_legend: bool,
495}
496
497#[derive(Debug, Clone, Serialize)]
499pub struct PieSlice {
500 pub label: String,
501 pub value: f64,
502 pub percentage: f64,
503 pub color: Option<String>,
504}
505
506#[derive(Debug, Clone, Serialize)]
508pub struct HeatmapData {
509 pub cells: Vec<HeatmapCell>,
510 pub row_labels: Vec<String>,
511 pub col_labels: Vec<String>,
512 pub color_scale: ColorScale,
513}
514
515#[derive(Debug, Clone, Serialize)]
517pub struct HeatmapCell {
518 pub row: usize,
519 pub col: usize,
520 pub value: f64,
521}
522
523#[derive(Debug, Clone, Serialize)]
525pub enum ColorScale {
526 Viridis,
527 Plasma,
528 Inferno,
529 Magma,
530 Grayscale,
531}
532
533pub struct TopologyVisualizer {
535 nodes: Arc<RwLock<Vec<TopologyNode>>>,
536 edges: Arc<RwLock<Vec<TopologyEdge>>>,
537}
538
539impl Default for TopologyVisualizer {
540 fn default() -> Self {
541 Self {
542 nodes: Arc::new(RwLock::new(Vec::new())),
543 edges: Arc::new(RwLock::new(Vec::new())),
544 }
545 }
546}
547
548impl TopologyVisualizer {
549 pub fn new() -> Self {
550 Self::default()
551 }
552
553 pub async fn add_node(&self, node: TopologyNode) -> Result<()> {
555 let mut nodes = self.nodes.write().await;
556 nodes.push(node);
557 Ok(())
558 }
559
560 pub async fn add_edge(&self, edge: TopologyEdge) -> Result<()> {
562 let mut edges = self.edges.write().await;
563 edges.push(edge);
564 Ok(())
565 }
566
567 pub async fn generate_topology(&self) -> Result<TopologyVisualization> {
569 let nodes = self.nodes.read().await.clone();
570 let edges = self.edges.read().await.clone();
571
572 let positioned_nodes = self.calculate_layout(&nodes, &edges).await?;
574
575 Ok(TopologyVisualization {
576 nodes: positioned_nodes,
577 edges,
578 layout_algorithm: LayoutAlgorithm::ForceDirected,
579 })
580 }
581
582 async fn calculate_layout(
584 &self,
585 nodes: &[TopologyNode],
586 _edges: &[TopologyEdge],
587 ) -> Result<Vec<PositionedNode>> {
588 let n = nodes.len();
590 let radius = 200.0;
591 let center_x = 300.0;
592 let center_y = 300.0;
593
594 let positioned: Vec<PositionedNode> = nodes
595 .iter()
596 .enumerate()
597 .map(|(i, node)| {
598 let angle = (i as f64 / n as f64) * 2.0 * std::f64::consts::PI;
599 let x = center_x + radius * angle.cos();
600 let y = center_y + radius * angle.sin();
601
602 PositionedNode {
603 node: node.clone(),
604 x,
605 y,
606 }
607 })
608 .collect();
609
610 Ok(positioned)
611 }
612}
613
614#[derive(Debug, Clone, Serialize)]
616pub struct TopologyNode {
617 pub id: String,
618 pub label: String,
619 pub node_type: NodeType,
620 pub status: NodeStatus,
621 pub metadata: HashMap<String, String>,
622}
623
624#[derive(Debug, Clone, Serialize)]
626pub enum NodeType {
627 Service,
628 DataSource,
629 Gateway,
630 Cache,
631 LoadBalancer,
632}
633
634#[derive(Debug, Clone, Serialize)]
636pub enum NodeStatus {
637 Healthy,
638 Degraded,
639 Unhealthy,
640 Unknown,
641}
642
643#[derive(Debug, Clone, Serialize)]
645pub struct TopologyEdge {
646 pub source_id: String,
647 pub target_id: String,
648 pub edge_type: EdgeType,
649 pub weight: f64,
650 pub label: Option<String>,
651}
652
653#[derive(Debug, Clone, Serialize)]
655pub enum EdgeType {
656 Query,
657 DataFlow,
658 Federation,
659 Replication,
660}
661
662#[derive(Debug, Clone, Serialize)]
664pub struct PositionedNode {
665 pub node: TopologyNode,
666 pub x: f64,
667 pub y: f64,
668}
669
670#[derive(Debug, Clone, Serialize)]
672pub struct TopologyVisualization {
673 pub nodes: Vec<PositionedNode>,
674 pub edges: Vec<TopologyEdge>,
675 pub layout_algorithm: LayoutAlgorithm,
676}
677
678#[derive(Debug, Clone, Serialize)]
680pub enum LayoutAlgorithm {
681 ForceDirected,
682 Hierarchical,
683 Circular,
684 Grid,
685}
686
687pub struct AlertVisualizer {
689 alerts: Arc<RwLock<VecDeque<Alert>>>,
690}
691
692impl Default for AlertVisualizer {
693 fn default() -> Self {
694 Self {
695 alerts: Arc::new(RwLock::new(VecDeque::new())),
696 }
697 }
698}
699
700impl AlertVisualizer {
701 pub fn new() -> Self {
702 Self::default()
703 }
704
705 pub async fn add_alert(&self, alert: Alert) -> Result<()> {
707 let mut alerts = self.alerts.write().await;
708 alerts.push_back(alert);
709
710 if alerts.len() > 100 {
712 alerts.pop_front();
713 }
714
715 Ok(())
716 }
717
718 pub async fn get_alerts_by_severity(&self, severity: AlertSeverity) -> Vec<Alert> {
720 let alerts = self.alerts.read().await;
721 alerts
722 .iter()
723 .filter(|a| a.severity == severity)
724 .cloned()
725 .collect()
726 }
727
728 pub async fn generate_alert_timeline(&self) -> Result<AlertTimeline> {
730 let alerts = self.alerts.read().await.clone().into_iter().collect();
731
732 Ok(AlertTimeline {
733 alerts,
734 group_by: AlertGrouping::Severity,
735 })
736 }
737}
738
739#[derive(Debug, Clone, Serialize)]
741pub struct Alert {
742 pub id: String,
743 pub timestamp: DateTime<Utc>,
744 pub severity: AlertSeverity,
745 pub title: String,
746 pub description: String,
747 pub source: String,
748 pub acknowledged: bool,
749}
750
751#[derive(Debug, Clone, Serialize, PartialEq)]
753pub enum AlertSeverity {
754 Critical,
755 High,
756 Medium,
757 Low,
758 Info,
759}
760
761#[derive(Debug, Clone, Serialize)]
763pub struct AlertTimeline {
764 pub alerts: Vec<Alert>,
765 pub group_by: AlertGrouping,
766}
767
768#[derive(Debug, Clone, Serialize)]
770pub enum AlertGrouping {
771 Severity,
772 Source,
773 Time,
774}
775
776impl AdvancedVisualization {
777 pub fn new(config: VisualizationConfig) -> Self {
779 Self {
780 config: config.clone(),
781 dashboards: Arc::new(DashMap::new()),
782 metrics_collector: Arc::new(MetricsCollector::new()),
783 chart_generator: Arc::new(ChartGenerator::new(config.default_theme.clone())),
784 topology_visualizer: Arc::new(TopologyVisualizer::new()),
785 alert_visualizer: Arc::new(AlertVisualizer::new()),
786 }
787 }
788
789 pub async fn create_dashboard(&self, mut dashboard: Dashboard) -> Result<String> {
791 dashboard.created_at = Utc::now();
792 dashboard.last_updated = Utc::now();
793 let id = dashboard.id.clone();
794
795 self.dashboards.insert(id.clone(), dashboard);
796 info!("Created dashboard: {}", id);
797
798 Ok(id)
799 }
800
801 pub async fn get_dashboard(&self, dashboard_id: &str) -> Option<Dashboard> {
803 self.dashboards.get(dashboard_id).map(|d| d.clone())
804 }
805
806 pub async fn update_dashboard(
808 &self,
809 dashboard_id: &str,
810 mut dashboard: Dashboard,
811 ) -> Result<()> {
812 dashboard.last_updated = Utc::now();
813 self.dashboards.insert(dashboard_id.to_string(), dashboard);
814 Ok(())
815 }
816
817 pub async fn delete_dashboard(&self, dashboard_id: &str) -> Result<()> {
819 self.dashboards.remove(dashboard_id);
820 Ok(())
821 }
822
823 pub async fn record_metric(&self, metric_name: &str, value: f64) -> Result<()> {
825 self.metrics_collector
826 .record_metric(metric_name, value)
827 .await
828 }
829
830 pub async fn generate_widget_chart(&self, widget: &Widget) -> Result<ChartData> {
832 match widget.data_source {
833 DataSource::QueryMetrics => {
834 if let Some(series) = self
835 .metrics_collector
836 .get_time_series("query_latency")
837 .await
838 {
839 self.chart_generator
840 .generate_line_chart(&series, &widget.config)
841 .await
842 } else {
843 Err(anyhow!("No data available for query metrics"))
844 }
845 }
846 DataSource::PerformanceMetrics => {
847 if let Some(series) = self.metrics_collector.get_time_series("performance").await {
848 self.chart_generator
849 .generate_line_chart(&series, &widget.config)
850 .await
851 } else {
852 Err(anyhow!("No data available for performance metrics"))
853 }
854 }
855 DataSource::FederationTopology => {
856 if let Some(series) = self
857 .metrics_collector
858 .get_time_series("federation_topology")
859 .await
860 {
861 self.chart_generator
862 .generate_line_chart(&series, &widget.config)
863 .await
864 } else {
865 Err(anyhow!(
866 "No data available for federation topology; record metrics under \
867 the 'federation_topology' key via record_metric()"
868 ))
869 }
870 }
871 DataSource::SecurityAlerts => {
872 if let Some(series) = self
873 .metrics_collector
874 .get_time_series("security_alerts")
875 .await
876 {
877 self.chart_generator
878 .generate_line_chart(&series, &widget.config)
879 .await
880 } else {
881 Err(anyhow!(
882 "No data available for security alerts; record metrics under \
883 the 'security_alerts' key via record_metric()"
884 ))
885 }
886 }
887 DataSource::ComplianceStatus => {
888 if let Some(series) = self
889 .metrics_collector
890 .get_time_series("compliance_status")
891 .await
892 {
893 self.chart_generator
894 .generate_line_chart(&series, &widget.config)
895 .await
896 } else {
897 Err(anyhow!(
898 "No data available for compliance status; record metrics under \
899 the 'compliance_status' key via record_metric()"
900 ))
901 }
902 }
903 DataSource::ServiceHealth => {
904 if let Some(series) = self
905 .metrics_collector
906 .get_time_series("service_health")
907 .await
908 {
909 self.chart_generator
910 .generate_line_chart(&series, &widget.config)
911 .await
912 } else {
913 Err(anyhow!(
914 "No data available for service health; record metrics under \
915 the 'service_health' key via record_metric()"
916 ))
917 }
918 }
919 DataSource::Custom(ref key) => {
920 if let Some(series) = self.metrics_collector.get_time_series(key).await {
921 self.chart_generator
922 .generate_line_chart(&series, &widget.config)
923 .await
924 } else {
925 Err(anyhow!(
926 "No data available for custom data source '{}'; \
927 record metrics under that key via record_metric()",
928 key
929 ))
930 }
931 }
932 }
933 }
934
935 pub async fn generate_topology_visualization(&self) -> Result<TopologyVisualization> {
937 self.topology_visualizer.generate_topology().await
938 }
939
940 pub async fn add_topology_node(&self, node: TopologyNode) -> Result<()> {
942 self.topology_visualizer.add_node(node).await
943 }
944
945 pub async fn add_topology_edge(&self, edge: TopologyEdge) -> Result<()> {
947 self.topology_visualizer.add_edge(edge).await
948 }
949
950 pub async fn add_alert(&self, alert: Alert) -> Result<()> {
952 self.alert_visualizer.add_alert(alert).await
953 }
954
955 pub async fn get_alert_timeline(&self) -> Result<AlertTimeline> {
957 self.alert_visualizer.generate_alert_timeline().await
958 }
959
960 pub async fn export_dashboard(
962 &self,
963 dashboard_id: &str,
964 format: ExportFormat,
965 ) -> Result<Vec<u8>> {
966 let dashboard = self
967 .get_dashboard(dashboard_id)
968 .await
969 .ok_or_else(|| anyhow!("Dashboard not found"))?;
970
971 match format {
972 ExportFormat::JSON => {
973 let json = serde_json::to_string_pretty(&dashboard)?;
974 Ok(json.into_bytes())
975 }
976 ExportFormat::SVG => {
977 let svg = format!(
979 r#"<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
980 <text x="10" y="20">{}</text>
981 </svg>"#,
982 dashboard.name
983 );
984 Ok(svg.into_bytes())
985 }
986 ExportFormat::CSV => {
987 let mut csv = String::new();
989 csv.push_str("id,title,widget_type,data_source\n");
990 for widget in &dashboard.widgets {
991 let widget_type = format!("{:?}", widget.widget_type);
992 let data_source = format!("{:?}", widget.data_source);
993 let escape = |s: &str| {
995 if s.contains(',') || s.contains('"') || s.contains('\n') {
996 format!("\"{}\"", s.replace('"', "\"\""))
997 } else {
998 s.to_string()
999 }
1000 };
1001 csv.push_str(&format!(
1002 "{},{},{},{}\n",
1003 escape(&widget.id),
1004 escape(&widget.title),
1005 escape(&widget_type),
1006 escape(&data_source),
1007 ));
1008 }
1009 Ok(csv.into_bytes())
1010 }
1011 ExportFormat::PNG => Err(anyhow!(
1012 "PNG export requires SVG rasterization; use ExportFormat::SVG and convert \
1013 externally (resvg/tiny-skia depend on miniz_oxide which is prohibited by \
1014 COOLJAPAN Pure Rust Policy)"
1015 )),
1016 ExportFormat::PDF => Err(anyhow!(
1017 "PDF export is not supported; export as SVG or JSON and convert using an \
1018 external tool"
1019 )),
1020 }
1021 }
1022
1023 pub async fn create_default_performance_dashboard(&self) -> Result<String> {
1025 let dashboard = Dashboard {
1026 id: uuid::Uuid::new_v4().to_string(),
1027 name: "Performance Dashboard".to_string(),
1028 description: "Real-time performance metrics".to_string(),
1029 layout: DashboardLayout::Grid { rows: 2, cols: 2 },
1030 widgets: vec![
1031 Widget {
1032 id: uuid::Uuid::new_v4().to_string(),
1033 widget_type: WidgetType::Chart,
1034 title: "Query Latency".to_string(),
1035 position: WidgetPosition { row: 0, col: 0 },
1036 size: WidgetSize {
1037 width: 1,
1038 height: 1,
1039 },
1040 data_source: DataSource::QueryMetrics,
1041 visualization_type: VisualizationType::LineChart,
1042 config: WidgetConfig::default(),
1043 },
1044 Widget {
1045 id: uuid::Uuid::new_v4().to_string(),
1046 widget_type: WidgetType::Chart,
1047 title: "Throughput".to_string(),
1048 position: WidgetPosition { row: 0, col: 1 },
1049 size: WidgetSize {
1050 width: 1,
1051 height: 1,
1052 },
1053 data_source: DataSource::PerformanceMetrics,
1054 visualization_type: VisualizationType::BarChart,
1055 config: WidgetConfig::default(),
1056 },
1057 Widget {
1058 id: uuid::Uuid::new_v4().to_string(),
1059 widget_type: WidgetType::Topology,
1060 title: "Federation Topology".to_string(),
1061 position: WidgetPosition { row: 1, col: 0 },
1062 size: WidgetSize {
1063 width: 2,
1064 height: 1,
1065 },
1066 data_source: DataSource::FederationTopology,
1067 visualization_type: VisualizationType::NetworkGraph,
1068 config: WidgetConfig::default(),
1069 },
1070 ],
1071 created_at: Utc::now(),
1072 last_updated: Utc::now(),
1073 auto_refresh: true,
1074 refresh_interval: Duration::from_secs(5),
1075 };
1076
1077 self.create_dashboard(dashboard).await
1078 }
1079
1080 pub async fn create_security_dashboard(&self) -> Result<String> {
1082 let dashboard = Dashboard {
1083 id: uuid::Uuid::new_v4().to_string(),
1084 name: "Security Monitoring".to_string(),
1085 description: "Real-time security alerts and metrics".to_string(),
1086 layout: DashboardLayout::TwoColumn,
1087 widgets: vec![
1088 Widget {
1089 id: uuid::Uuid::new_v4().to_string(),
1090 widget_type: WidgetType::Alert,
1091 title: "Security Alerts".to_string(),
1092 position: WidgetPosition { row: 0, col: 0 },
1093 size: WidgetSize {
1094 width: 1,
1095 height: 1,
1096 },
1097 data_source: DataSource::SecurityAlerts,
1098 visualization_type: VisualizationType::Table,
1099 config: WidgetConfig::default(),
1100 },
1101 Widget {
1102 id: uuid::Uuid::new_v4().to_string(),
1103 widget_type: WidgetType::Chart,
1104 title: "Threat Level".to_string(),
1105 position: WidgetPosition { row: 0, col: 1 },
1106 size: WidgetSize {
1107 width: 1,
1108 height: 1,
1109 },
1110 data_source: DataSource::SecurityAlerts,
1111 visualization_type: VisualizationType::Gauge,
1112 config: WidgetConfig::default(),
1113 },
1114 ],
1115 created_at: Utc::now(),
1116 last_updated: Utc::now(),
1117 auto_refresh: true,
1118 refresh_interval: Duration::from_secs(10),
1119 };
1120
1121 self.create_dashboard(dashboard).await
1122 }
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127 use super::*;
1128
1129 #[tokio::test]
1130 async fn test_visualization_creation() {
1131 let config = VisualizationConfig::default();
1132 let viz = AdvancedVisualization::new(config);
1133
1134 let dashboard_id = viz
1135 .create_default_performance_dashboard()
1136 .await
1137 .expect("async operation should succeed");
1138 assert!(!dashboard_id.is_empty());
1139
1140 let dashboard = viz.get_dashboard(&dashboard_id).await;
1141 assert!(dashboard.is_some());
1142 }
1143
1144 #[tokio::test]
1145 async fn test_metrics_collection() {
1146 let collector = MetricsCollector::new();
1147
1148 collector
1149 .record_metric("test_metric", 100.0)
1150 .await
1151 .expect("async operation should succeed");
1152 collector
1153 .record_metric("test_metric", 200.0)
1154 .await
1155 .expect("async operation should succeed");
1156
1157 let series = collector.get_time_series("test_metric").await;
1158 assert!(series.is_some());
1159 assert_eq!(
1160 series.expect("operation should succeed").data_points.len(),
1161 2
1162 );
1163 }
1164
1165 #[tokio::test]
1166 async fn test_aggregation() {
1167 let collector = MetricsCollector::new();
1168
1169 for i in 1..=10 {
1170 collector
1171 .record_metric("test", i as f64)
1172 .await
1173 .expect("async operation should succeed");
1174 }
1175
1176 let avg = collector
1177 .calculate_aggregation("test", AggregationType::Average, Duration::from_secs(3600))
1178 .await
1179 .expect("operation should succeed");
1180
1181 assert!((avg - 5.5).abs() < 0.1);
1182 }
1183
1184 #[tokio::test]
1185 async fn test_chart_generation() {
1186 let config = WidgetConfig::default();
1187 let generator = ChartGenerator::new(ChartTheme::Dark);
1188
1189 let time_series = TimeSeries {
1190 name: "test".to_string(),
1191 data_points: (1..=5)
1192 .map(|i| DataPoint {
1193 timestamp: Utc::now(),
1194 value: i as f64,
1195 })
1196 .collect(),
1197 unit: "ms".to_string(),
1198 };
1199
1200 let chart = generator
1201 .generate_line_chart(&time_series, &config)
1202 .await
1203 .expect("operation should succeed");
1204
1205 assert_eq!(chart.series.len(), 1);
1206 assert_eq!(chart.series[0].data.len(), 5);
1207 }
1208
1209 #[tokio::test]
1210 async fn test_topology_visualization() {
1211 let visualizer = TopologyVisualizer::new();
1212
1213 visualizer
1214 .add_node(TopologyNode {
1215 id: "node1".to_string(),
1216 label: "Service 1".to_string(),
1217 node_type: NodeType::Service,
1218 status: NodeStatus::Healthy,
1219 metadata: HashMap::new(),
1220 })
1221 .await
1222 .expect("operation should succeed");
1223
1224 visualizer
1225 .add_node(TopologyNode {
1226 id: "node2".to_string(),
1227 label: "Service 2".to_string(),
1228 node_type: NodeType::Service,
1229 status: NodeStatus::Healthy,
1230 metadata: HashMap::new(),
1231 })
1232 .await
1233 .expect("operation should succeed");
1234
1235 visualizer
1236 .add_edge(TopologyEdge {
1237 source_id: "node1".to_string(),
1238 target_id: "node2".to_string(),
1239 edge_type: EdgeType::Query,
1240 weight: 1.0,
1241 label: None,
1242 })
1243 .await
1244 .expect("operation should succeed");
1245
1246 let topology = visualizer
1247 .generate_topology()
1248 .await
1249 .expect("async operation should succeed");
1250 assert_eq!(topology.nodes.len(), 2);
1251 assert_eq!(topology.edges.len(), 1);
1252 }
1253
1254 #[tokio::test]
1255 async fn test_alert_visualization() {
1256 let visualizer = AlertVisualizer::new();
1257
1258 visualizer
1259 .add_alert(Alert {
1260 id: uuid::Uuid::new_v4().to_string(),
1261 timestamp: Utc::now(),
1262 severity: AlertSeverity::Critical,
1263 title: "Test Alert".to_string(),
1264 description: "Test description".to_string(),
1265 source: "test".to_string(),
1266 acknowledged: false,
1267 })
1268 .await
1269 .expect("operation should succeed");
1270
1271 let timeline = visualizer
1272 .generate_alert_timeline()
1273 .await
1274 .expect("async operation should succeed");
1275 assert_eq!(timeline.alerts.len(), 1);
1276 }
1277
1278 #[tokio::test]
1279 async fn test_dashboard_export() {
1280 let config = VisualizationConfig::default();
1281 let viz = AdvancedVisualization::new(config);
1282
1283 let dashboard_id = viz
1284 .create_default_performance_dashboard()
1285 .await
1286 .expect("async operation should succeed");
1287
1288 let json_export = viz
1289 .export_dashboard(&dashboard_id, ExportFormat::JSON)
1290 .await
1291 .expect("operation should succeed");
1292 assert!(!json_export.is_empty());
1293
1294 let svg_export = viz
1295 .export_dashboard(&dashboard_id, ExportFormat::SVG)
1296 .await
1297 .expect("operation should succeed");
1298 assert!(!svg_export.is_empty());
1299 }
1300
1301 #[tokio::test]
1302 async fn test_export_dashboard_csv() {
1303 let config = VisualizationConfig::default();
1304 let viz = AdvancedVisualization::new(config);
1305
1306 let dashboard_id = viz
1307 .create_default_performance_dashboard()
1308 .await
1309 .expect("async operation should succeed");
1310
1311 let csv_bytes = viz
1312 .export_dashboard(&dashboard_id, ExportFormat::CSV)
1313 .await
1314 .expect("CSV export should succeed");
1315
1316 let csv = String::from_utf8(csv_bytes).expect("CSV must be valid UTF-8");
1317 assert!(
1318 csv.starts_with("id,title,widget_type,data_source\n"),
1319 "must have header row"
1320 );
1321 assert!(csv.contains("Query Latency"), "must contain widget title");
1322 }
1323
1324 #[tokio::test]
1325 async fn test_export_dashboard_png_returns_error() {
1326 let config = VisualizationConfig::default();
1327 let viz = AdvancedVisualization::new(config);
1328
1329 let dashboard_id = viz
1330 .create_default_performance_dashboard()
1331 .await
1332 .expect("async operation should succeed");
1333
1334 let result = viz.export_dashboard(&dashboard_id, ExportFormat::PNG).await;
1335
1336 assert!(
1337 result.is_err(),
1338 "PNG export should return an error per policy"
1339 );
1340 let msg = result.unwrap_err().to_string();
1341 assert!(msg.contains("PNG"), "Error message must mention PNG: {msg}");
1342 }
1343
1344 #[tokio::test]
1345 async fn test_widget_chart_custom_data_source() {
1346 let config = VisualizationConfig::default();
1347 let viz = AdvancedVisualization::new(config);
1348
1349 viz.record_metric("my_custom_metric", 42.0)
1351 .await
1352 .expect("record_metric should succeed");
1353
1354 let widget = Widget {
1355 id: uuid::Uuid::new_v4().to_string(),
1356 widget_type: WidgetType::Chart,
1357 title: "Custom".to_string(),
1358 position: WidgetPosition { row: 0, col: 0 },
1359 size: WidgetSize {
1360 width: 1,
1361 height: 1,
1362 },
1363 data_source: DataSource::Custom("my_custom_metric".to_string()),
1364 visualization_type: VisualizationType::LineChart,
1365 config: WidgetConfig::default(),
1366 };
1367
1368 let chart = viz.generate_widget_chart(&widget).await;
1369 assert!(
1370 chart.is_ok(),
1371 "Custom data source with recorded data should succeed"
1372 );
1373 }
1374
1375 #[tokio::test]
1376 async fn test_widget_chart_security_alerts_no_data() {
1377 let config = VisualizationConfig::default();
1378 let viz = AdvancedVisualization::new(config);
1379
1380 let widget = Widget {
1381 id: uuid::Uuid::new_v4().to_string(),
1382 widget_type: WidgetType::Alert,
1383 title: "Alerts".to_string(),
1384 position: WidgetPosition { row: 0, col: 0 },
1385 size: WidgetSize {
1386 width: 1,
1387 height: 1,
1388 },
1389 data_source: DataSource::SecurityAlerts,
1390 visualization_type: VisualizationType::Table,
1391 config: WidgetConfig::default(),
1392 };
1393
1394 let result = viz.generate_widget_chart(&widget).await;
1396 assert!(
1397 result.is_err(),
1398 "Missing data should return an error, not panic"
1399 );
1400 let msg = result.unwrap_err().to_string();
1401 assert!(
1402 msg.contains("security_alerts"),
1403 "Error must mention the metric key: {msg}"
1404 );
1405 }
1406}