1use crate::{ReportingError, Result};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::Path;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DashboardLayout {
15 pub id: String,
16 pub name: String,
17 pub description: Option<String>,
18 pub created_at: chrono::DateTime<chrono::Utc>,
19 pub updated_at: chrono::DateTime<chrono::Utc>,
20 pub author: String,
21 pub tags: Vec<String>,
22 pub is_public: bool,
23 pub grid_config: GridConfig,
24 pub widgets: Vec<Widget>,
25 pub filters: Vec<DashboardFilter>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct GridConfig {
31 pub columns: u32,
32 pub row_height: u32,
33 pub gap: u32,
34 pub responsive_breakpoints: HashMap<String, u32>,
35}
36
37impl Default for GridConfig {
38 fn default() -> Self {
39 let mut breakpoints = HashMap::new();
40 breakpoints.insert("mobile".to_string(), 1);
41 breakpoints.insert("tablet".to_string(), 2);
42 breakpoints.insert("desktop".to_string(), 4);
43 breakpoints.insert("wide".to_string(), 6);
44
45 Self {
46 columns: 12,
47 row_height: 60,
48 gap: 16,
49 responsive_breakpoints: breakpoints,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct Widget {
57 pub id: String,
58 pub widget_type: WidgetType,
59 pub title: String,
60 pub position: WidgetPosition,
61 pub data_source: DataSource,
62 pub refresh_interval_seconds: Option<u32>,
63 pub config: serde_json::Value,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
68pub struct WidgetPosition {
69 pub x: u32,
70 pub y: u32,
71 pub width: u32,
72 pub height: u32,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
77#[serde(rename_all = "snake_case")]
78pub enum WidgetType {
79 LineChart,
80 BarChart,
81 PieChart,
82 Gauge,
83 Counter,
84 Table,
85 Heatmap,
86 Flamegraph,
87 Timeline,
88 AlertList,
89 MetricComparison,
90 ScenarioStatus,
91 ServiceMap,
92 LogStream,
93 Custom,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct DataSource {
99 pub source_type: DataSourceType,
100 pub query: String,
101 pub aggregation: Option<AggregationType>,
102 pub time_range: TimeRange,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
107#[serde(rename_all = "snake_case")]
108pub enum DataSourceType {
109 Prometheus,
110 OpenTelemetry,
111 ChaosMetrics,
112 ScenarioExecutions,
113 AlertHistory,
114 CustomMetric,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
119#[serde(rename_all = "snake_case")]
120pub enum AggregationType {
121 Sum,
122 Average,
123 Min,
124 Max,
125 Count,
126 P50,
127 P95,
128 P99,
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct TimeRange {
134 pub range_type: TimeRangeType,
135 pub value: Option<u64>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
140#[serde(rename_all = "snake_case")]
141pub enum TimeRangeType {
142 Last15Minutes,
143 Last1Hour,
144 Last6Hours,
145 Last24Hours,
146 Last7Days,
147 Last30Days,
148 Custom,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct DashboardFilter {
154 pub id: String,
155 pub name: String,
156 pub filter_type: FilterType,
157 pub options: Vec<String>,
158 pub default_value: Option<String>,
159}
160
161#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
163#[serde(rename_all = "snake_case")]
164pub enum FilterType {
165 ServiceName,
166 Environment,
167 ScenarioType,
168 TimeRange,
169 Custom,
170}
171
172pub struct DashboardLayoutManager {
174 layouts_dir: String,
175}
176
177impl DashboardLayoutManager {
178 pub fn new(layouts_dir: String) -> Result<Self> {
180 fs::create_dir_all(&layouts_dir)?;
182
183 Ok(Self { layouts_dir })
184 }
185
186 pub fn save_layout(&self, layout: &DashboardLayout) -> Result<()> {
188 let file_path = self.get_layout_path(&layout.id);
189 let json = serde_json::to_string_pretty(layout)?;
190 fs::write(file_path, json)?;
191 Ok(())
192 }
193
194 pub fn load_layout(&self, layout_id: &str) -> Result<DashboardLayout> {
196 let file_path = self.get_layout_path(layout_id);
197
198 if !Path::new(&file_path).exists() {
199 return Err(ReportingError::Analysis(format!("Layout not found: {}", layout_id)));
200 }
201
202 let json = fs::read_to_string(file_path)?;
203 let layout: DashboardLayout = serde_json::from_str(&json)?;
204 Ok(layout)
205 }
206
207 pub fn list_layouts(&self) -> Result<Vec<DashboardLayoutInfo>> {
209 let mut layouts = Vec::new();
210
211 for entry in fs::read_dir(&self.layouts_dir)? {
212 let entry = entry?;
213 let path = entry.path();
214
215 if path.extension().and_then(|s| s.to_str()) == Some("json") {
216 if let Ok(json) = fs::read_to_string(&path) {
217 if let Ok(layout) = serde_json::from_str::<DashboardLayout>(&json) {
218 layouts.push(DashboardLayoutInfo {
219 id: layout.id,
220 name: layout.name,
221 description: layout.description,
222 author: layout.author,
223 tags: layout.tags,
224 created_at: layout.created_at,
225 updated_at: layout.updated_at,
226 widget_count: layout.widgets.len(),
227 });
228 }
229 }
230 }
231 }
232
233 Ok(layouts)
234 }
235
236 pub fn delete_layout(&self, layout_id: &str) -> Result<()> {
238 let file_path = self.get_layout_path(layout_id);
239
240 if Path::new(&file_path).exists() {
241 fs::remove_file(file_path)?;
242 Ok(())
243 } else {
244 Err(ReportingError::Analysis(format!("Layout not found: {}", layout_id)))
245 }
246 }
247
248 pub fn clone_layout(
250 &self,
251 source_id: &str,
252 new_name: &str,
253 new_author: &str,
254 ) -> Result<DashboardLayout> {
255 let mut layout = self.load_layout(source_id)?;
256
257 layout.id = uuid::Uuid::new_v4().to_string();
259 layout.name = new_name.to_string();
260 layout.author = new_author.to_string();
261 layout.created_at = chrono::Utc::now();
262 layout.updated_at = chrono::Utc::now();
263
264 self.save_layout(&layout)?;
265 Ok(layout)
266 }
267
268 pub fn export_layout(&self, layout_id: &str) -> Result<String> {
270 let layout = self.load_layout(layout_id)?;
271 let json = serde_json::to_string_pretty(&layout)?;
272 Ok(json)
273 }
274
275 pub fn import_layout(&self, json: &str) -> Result<DashboardLayout> {
277 let layout: DashboardLayout = serde_json::from_str(json)?;
278 self.save_layout(&layout)?;
279 Ok(layout)
280 }
281
282 fn get_layout_path(&self, layout_id: &str) -> String {
283 format!("{}/{}.json", self.layouts_dir, layout_id)
284 }
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
289pub struct DashboardLayoutInfo {
290 pub id: String,
291 pub name: String,
292 pub description: Option<String>,
293 pub author: String,
294 pub tags: Vec<String>,
295 pub created_at: chrono::DateTime<chrono::Utc>,
296 pub updated_at: chrono::DateTime<chrono::Utc>,
297 pub widget_count: usize,
298}
299
300pub struct DashboardLayoutBuilder {
302 layout: DashboardLayout,
303}
304
305impl DashboardLayoutBuilder {
306 pub fn new(name: &str, author: &str) -> Self {
308 let now = chrono::Utc::now();
309
310 Self {
311 layout: DashboardLayout {
312 id: uuid::Uuid::new_v4().to_string(),
313 name: name.to_string(),
314 description: None,
315 created_at: now,
316 updated_at: now,
317 author: author.to_string(),
318 tags: Vec::new(),
319 is_public: false,
320 grid_config: GridConfig::default(),
321 widgets: Vec::new(),
322 filters: Vec::new(),
323 },
324 }
325 }
326
327 pub fn description(mut self, description: &str) -> Self {
329 self.layout.description = Some(description.to_string());
330 self
331 }
332
333 pub fn tag(mut self, tag: &str) -> Self {
335 self.layout.tags.push(tag.to_string());
336 self
337 }
338
339 pub fn public(mut self, is_public: bool) -> Self {
341 self.layout.is_public = is_public;
342 self
343 }
344
345 pub fn grid_config(mut self, config: GridConfig) -> Self {
347 self.layout.grid_config = config;
348 self
349 }
350
351 pub fn add_widget(mut self, widget: Widget) -> Self {
353 self.layout.widgets.push(widget);
354 self
355 }
356
357 pub fn add_filter(mut self, filter: DashboardFilter) -> Self {
359 self.layout.filters.push(filter);
360 self
361 }
362
363 pub fn build(self) -> DashboardLayout {
365 self.layout
366 }
367}
368
369pub struct DashboardTemplates;
371
372impl DashboardTemplates {
373 pub fn chaos_overview() -> DashboardLayout {
375 DashboardLayoutBuilder::new("Chaos Engineering Overview", "MockForge")
376 .description("Real-time overview of chaos engineering activities")
377 .tag("chaos")
378 .tag("overview")
379 .public(true)
380 .add_widget(Widget {
381 id: "active-scenarios".to_string(),
382 widget_type: WidgetType::Counter,
383 title: "Active Scenarios".to_string(),
384 position: WidgetPosition {
385 x: 0,
386 y: 0,
387 width: 3,
388 height: 2,
389 },
390 data_source: DataSource {
391 source_type: DataSourceType::ScenarioExecutions,
392 query: "count(active_scenarios)".to_string(),
393 aggregation: Some(AggregationType::Count),
394 time_range: TimeRange {
395 range_type: TimeRangeType::Last15Minutes,
396 value: None,
397 },
398 },
399 refresh_interval_seconds: Some(5),
400 config: serde_json::json!({"color": "blue"}),
401 })
402 .add_widget(Widget {
403 id: "error-rate".to_string(),
404 widget_type: WidgetType::LineChart,
405 title: "Error Rate".to_string(),
406 position: WidgetPosition {
407 x: 3,
408 y: 0,
409 width: 6,
410 height: 4,
411 },
412 data_source: DataSource {
413 source_type: DataSourceType::ChaosMetrics,
414 query: "error_rate".to_string(),
415 aggregation: Some(AggregationType::Average),
416 time_range: TimeRange {
417 range_type: TimeRangeType::Last1Hour,
418 value: None,
419 },
420 },
421 refresh_interval_seconds: Some(10),
422 config: serde_json::json!({"yAxisLabel": "Error %"}),
423 })
424 .add_widget(Widget {
425 id: "latency-heatmap".to_string(),
426 widget_type: WidgetType::Heatmap,
427 title: "Latency Distribution".to_string(),
428 position: WidgetPosition {
429 x: 0,
430 y: 4,
431 width: 12,
432 height: 4,
433 },
434 data_source: DataSource {
435 source_type: DataSourceType::OpenTelemetry,
436 query: "histogram_quantile(0.95, latency)".to_string(),
437 aggregation: Some(AggregationType::P95),
438 time_range: TimeRange {
439 range_type: TimeRangeType::Last6Hours,
440 value: None,
441 },
442 },
443 refresh_interval_seconds: Some(30),
444 config: serde_json::json!({"colorScheme": "RdYlGn"}),
445 })
446 .build()
447 }
448
449 pub fn service_performance() -> DashboardLayout {
451 DashboardLayoutBuilder::new("Service Performance", "MockForge")
452 .description("Detailed service performance metrics")
453 .tag("performance")
454 .tag("services")
455 .public(true)
456 .add_widget(Widget {
457 id: "request-rate".to_string(),
458 widget_type: WidgetType::LineChart,
459 title: "Request Rate".to_string(),
460 position: WidgetPosition { x: 0, y: 0, width: 6, height: 4 },
461 data_source: DataSource {
462 source_type: DataSourceType::Prometheus,
463 query: "rate(http_requests_total[5m])".to_string(),
464 aggregation: None,
465 time_range: TimeRange {
466 range_type: TimeRangeType::Last1Hour,
467 value: None,
468 },
469 },
470 refresh_interval_seconds: Some(10),
471 config: serde_json::json!({}),
472 })
473 .add_widget(Widget {
474 id: "p95-latency".to_string(),
475 widget_type: WidgetType::Gauge,
476 title: "P95 Latency".to_string(),
477 position: WidgetPosition { x: 6, y: 0, width: 3, height: 4 },
478 data_source: DataSource {
479 source_type: DataSourceType::Prometheus,
480 query: "histogram_quantile(0.95, latency_seconds)".to_string(),
481 aggregation: Some(AggregationType::P95),
482 time_range: TimeRange {
483 range_type: TimeRangeType::Last15Minutes,
484 value: None,
485 },
486 },
487 refresh_interval_seconds: Some(5),
488 config: serde_json::json!({"max": 1000, "thresholds": [{"value": 500, "color": "yellow"}, {"value": 800, "color": "red"}]}),
489 })
490 .build()
491 }
492
493 pub fn resilience_testing() -> DashboardLayout {
495 DashboardLayoutBuilder::new("Resilience Testing", "MockForge")
496 .description("Monitor resilience patterns and circuit breaker status")
497 .tag("resilience")
498 .tag("testing")
499 .public(true)
500 .add_widget(Widget {
501 id: "circuit-breaker-status".to_string(),
502 widget_type: WidgetType::Table,
503 title: "Circuit Breaker Status".to_string(),
504 position: WidgetPosition { x: 0, y: 0, width: 6, height: 4 },
505 data_source: DataSource {
506 source_type: DataSourceType::ChaosMetrics,
507 query: "circuit_breaker_status".to_string(),
508 aggregation: None,
509 time_range: TimeRange {
510 range_type: TimeRangeType::Last15Minutes,
511 value: None,
512 },
513 },
514 refresh_interval_seconds: Some(5),
515 config: serde_json::json!({"columns": ["service", "status", "failures", "last_failure"]}),
516 })
517 .add_widget(Widget {
518 id: "retry-success-rate".to_string(),
519 widget_type: WidgetType::BarChart,
520 title: "Retry Success Rate".to_string(),
521 position: WidgetPosition { x: 6, y: 0, width: 6, height: 4 },
522 data_source: DataSource {
523 source_type: DataSourceType::ChaosMetrics,
524 query: "retry_success_rate".to_string(),
525 aggregation: Some(AggregationType::Average),
526 time_range: TimeRange {
527 range_type: TimeRangeType::Last1Hour,
528 value: None,
529 },
530 },
531 refresh_interval_seconds: Some(15),
532 config: serde_json::json!({}),
533 })
534 .build()
535 }
536}
537
538#[cfg(test)]
541mod tests {
542 use super::*;
543 use tempfile::tempdir;
544
545 #[test]
546 fn test_dashboard_layout_builder() {
547 let layout = DashboardLayoutBuilder::new("Test Dashboard", "test-user")
548 .description("Test description")
549 .tag("test")
550 .public(true)
551 .build();
552
553 assert_eq!(layout.name, "Test Dashboard");
554 assert_eq!(layout.author, "test-user");
555 assert!(layout.is_public);
556 assert_eq!(layout.tags, vec!["test"]);
557 }
558
559 #[test]
560 fn test_layout_manager_save_and_load() {
561 let temp_dir = tempdir().unwrap();
562 let manager =
563 DashboardLayoutManager::new(temp_dir.path().to_str().unwrap().to_string()).unwrap();
564
565 let layout = DashboardLayoutBuilder::new("Test", "author").build();
566
567 manager.save_layout(&layout).unwrap();
569
570 let loaded = manager.load_layout(&layout.id).unwrap();
572 assert_eq!(loaded.id, layout.id);
573 assert_eq!(loaded.name, layout.name);
574 }
575
576 #[test]
577 fn test_layout_templates() {
578 let chaos_layout = DashboardTemplates::chaos_overview();
579 assert_eq!(chaos_layout.name, "Chaos Engineering Overview");
580 assert!(!chaos_layout.widgets.is_empty());
581
582 let perf_layout = DashboardTemplates::service_performance();
583 assert_eq!(perf_layout.name, "Service Performance");
584 }
585}