1use super::{AnalyticsError, TimeRange};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct DashboardConfig {
13 pub refresh_interval_seconds: u32,
15
16 pub default_time_range_hours: u32,
18
19 pub real_time_updates: bool,
21
22 pub max_chart_points: usize,
24
25 pub alerts_enabled: bool,
27}
28
29impl Default for DashboardConfig {
30 fn default() -> Self {
31 Self {
32 refresh_interval_seconds: 30,
33 default_time_range_hours: 24,
34 real_time_updates: true,
35 max_chart_points: 100,
36 alerts_enabled: true,
37 }
38 }
39}
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub enum WidgetType {
44 LineChart,
46 BarChart,
48 PieChart,
50 MetricCard,
52 Table,
54 HeatMap,
56 Gauge,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct DashboardWidget {
63 pub id: String,
65
66 pub title: String,
68
69 pub widget_type: WidgetType,
71
72 pub data_source: DataSource,
74
75 pub time_range: TimeRange,
77
78 pub layout: WidgetLayout,
80
81 pub refresh_interval: Option<u32>,
83
84 pub alert_thresholds: Option<AlertThresholds>,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct WidgetLayout {
91 pub x: u32,
93
94 pub y: u32,
96
97 pub width: u32,
99
100 pub height: u32,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub enum DataSource {
107 RoleUsage {
109 role_id: Option<String>,
110 group_by: Option<String>,
111 },
112 PermissionUsage {
114 permission_id: Option<String>,
115 group_by: Option<String>,
116 },
117 Compliance { metric_type: String },
119 Performance { metric_type: String },
121 EventCount {
123 event_type: Option<String>,
124 filters: HashMap<String, String>,
125 },
126 Custom {
128 query: String,
129 parameters: HashMap<String, String>,
130 },
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct AlertThresholds {
136 pub warning: f64,
138
139 pub critical: f64,
141
142 pub comparison: ThresholdComparison,
144}
145
146#[derive(Debug, Clone, Serialize, Deserialize)]
148pub enum ThresholdComparison {
149 GreaterThan,
150 LessThan,
151 Equals,
152 NotEquals,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct DataPoint {
158 pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
160
161 pub label: Option<String>,
163
164 pub value: f64,
166
167 pub metadata: HashMap<String, String>,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ChartSeries {
174 pub name: String,
176
177 pub data: Vec<DataPoint>,
179
180 pub color: Option<String>,
182
183 pub series_type: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct WidgetData {
190 pub widget_id: String,
192
193 pub updated_at: chrono::DateTime<chrono::Utc>,
195
196 pub series: Vec<ChartSeries>,
198
199 pub summary: Option<WidgetSummary>,
201
202 pub alert_status: AlertStatus,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct WidgetSummary {
209 pub total: f64,
211
212 pub average: f64,
214
215 pub minimum: f64,
217
218 pub maximum: f64,
220
221 pub change_percent: Option<f64>,
223}
224
225#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
227pub enum AlertStatus {
228 Normal,
229 Warning,
230 Critical,
231 Unknown,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize)]
236pub struct Dashboard {
237 pub id: String,
239
240 pub title: String,
242
243 pub description: Option<String>,
245
246 pub config: DashboardConfig,
248
249 pub widgets: Vec<DashboardWidget>,
251
252 pub tags: Vec<String>,
254
255 pub created_at: chrono::DateTime<chrono::Utc>,
257
258 pub updated_at: chrono::DateTime<chrono::Utc>,
260}
261
262pub struct DashboardManager {
264 config: DashboardConfig,
265 dashboards: HashMap<String, Dashboard>,
266}
267
268impl DashboardManager {
269 pub fn new(config: DashboardConfig) -> Self {
271 Self {
272 config,
273 dashboards: HashMap::new(),
274 }
275 }
276
277 pub async fn create_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
279 self.dashboards.insert(dashboard.id.clone(), dashboard);
280 Ok(())
281 }
282
283 pub async fn get_dashboard(
285 &self,
286 dashboard_id: &str,
287 ) -> Result<Option<Dashboard>, AnalyticsError> {
288 Ok(self.dashboards.get(dashboard_id).cloned())
289 }
290
291 pub async fn list_dashboards(&self) -> Result<Vec<Dashboard>, AnalyticsError> {
293 Ok(self.dashboards.values().cloned().collect())
294 }
295
296 pub async fn update_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
298 let mut updated_dashboard = dashboard;
299 updated_dashboard.updated_at = chrono::Utc::now();
300 self.dashboards
301 .insert(updated_dashboard.id.clone(), updated_dashboard);
302 Ok(())
303 }
304
305 pub async fn delete_dashboard(&mut self, dashboard_id: &str) -> Result<bool, AnalyticsError> {
307 Ok(self.dashboards.remove(dashboard_id).is_some())
308 }
309
310 pub async fn get_widget_data(
312 &self,
313 widget: &DashboardWidget,
314 ) -> Result<WidgetData, AnalyticsError> {
315 let series = match &widget.data_source {
316 DataSource::RoleUsage { role_id, group_by } => {
317 self.get_role_usage_series(
318 role_id.as_deref(),
319 group_by.as_deref(),
320 &widget.time_range,
321 )
322 .await?
323 }
324 DataSource::PermissionUsage {
325 permission_id,
326 group_by,
327 } => {
328 self.get_permission_usage_series(
329 permission_id.as_deref(),
330 group_by.as_deref(),
331 &widget.time_range,
332 )
333 .await?
334 }
335 DataSource::Compliance { metric_type } => {
336 self.get_compliance_series(metric_type, &widget.time_range)
337 .await?
338 }
339 DataSource::Performance { metric_type } => {
340 self.get_performance_series(metric_type, &widget.time_range)
341 .await?
342 }
343 DataSource::EventCount {
344 event_type,
345 filters,
346 } => {
347 self.get_event_count_series(event_type.as_deref(), filters, &widget.time_range)
348 .await?
349 }
350 DataSource::Custom { query, parameters } => {
351 self.get_custom_series(query, parameters, &widget.time_range)
352 .await?
353 }
354 };
355
356 let summary = self.calculate_widget_summary(&series);
357 let alert_status = self.check_alert_status(&summary, &widget.alert_thresholds);
358
359 Ok(WidgetData {
360 widget_id: widget.id.clone(),
361 updated_at: chrono::Utc::now(),
362 series,
363 summary: Some(summary),
364 alert_status,
365 })
366 }
367
368 pub async fn create_rbac_overview_dashboard(&mut self) -> Result<String, AnalyticsError> {
370 let dashboard_id = uuid::Uuid::new_v4().to_string();
371
372 let dashboard = Dashboard {
373 id: dashboard_id.clone(),
374 title: "RBAC Overview".to_string(),
375 description: Some("Comprehensive RBAC system overview".to_string()),
376 config: self.config.clone(),
377 widgets: vec![
378 DashboardWidget {
380 id: "permission_checks_timeline".to_string(),
381 title: "Permission Checks Over Time".to_string(),
382 widget_type: WidgetType::LineChart,
383 data_source: DataSource::EventCount {
384 event_type: Some("PermissionCheck".to_string()),
385 filters: HashMap::new(),
386 },
387 time_range: TimeRange::last_hours(24),
388 layout: WidgetLayout {
389 x: 0,
390 y: 0,
391 width: 6,
392 height: 3,
393 },
394 refresh_interval: None,
395 alert_thresholds: None,
396 },
397 DashboardWidget {
399 id: "role_usage_distribution".to_string(),
400 title: "Role Usage Distribution".to_string(),
401 widget_type: WidgetType::PieChart,
402 data_source: DataSource::RoleUsage {
403 role_id: None,
404 group_by: Some("role_name".to_string()),
405 },
406 time_range: TimeRange::last_hours(24),
407 layout: WidgetLayout {
408 x: 6,
409 y: 0,
410 width: 6,
411 height: 3,
412 },
413 refresh_interval: None,
414 alert_thresholds: None,
415 },
416 DashboardWidget {
418 id: "compliance_score".to_string(),
419 title: "Compliance Score".to_string(),
420 widget_type: WidgetType::Gauge,
421 data_source: DataSource::Compliance {
422 metric_type: "overall_compliance".to_string(),
423 },
424 time_range: TimeRange::last_hours(24),
425 layout: WidgetLayout {
426 x: 0,
427 y: 3,
428 width: 3,
429 height: 3,
430 },
431 refresh_interval: None,
432 alert_thresholds: Some(AlertThresholds {
433 warning: 85.0,
434 critical: 70.0,
435 comparison: ThresholdComparison::LessThan,
436 }),
437 },
438 DashboardWidget {
440 id: "avg_response_time".to_string(),
441 title: "Average Response Time".to_string(),
442 widget_type: WidgetType::MetricCard,
443 data_source: DataSource::Performance {
444 metric_type: "avg_permission_check_latency".to_string(),
445 },
446 time_range: TimeRange::last_hours(24),
447 layout: WidgetLayout {
448 x: 3,
449 y: 3,
450 width: 3,
451 height: 3,
452 },
453 refresh_interval: None,
454 alert_thresholds: Some(AlertThresholds {
455 warning: 100.0,
456 critical: 200.0,
457 comparison: ThresholdComparison::GreaterThan,
458 }),
459 },
460 DashboardWidget {
462 id: "top_resources".to_string(),
463 title: "Top Accessed Resources".to_string(),
464 widget_type: WidgetType::BarChart,
465 data_source: DataSource::EventCount {
466 event_type: Some("PermissionCheck".to_string()),
467 filters: HashMap::from([("result".to_string(), "Success".to_string())]),
468 },
469 time_range: TimeRange::last_hours(24),
470 layout: WidgetLayout {
471 x: 6,
472 y: 3,
473 width: 6,
474 height: 3,
475 },
476 refresh_interval: None,
477 alert_thresholds: None,
478 },
479 ],
480 tags: vec!["rbac".to_string(), "overview".to_string()],
481 created_at: chrono::Utc::now(),
482 updated_at: chrono::Utc::now(),
483 };
484
485 self.create_dashboard(dashboard).await?;
486 Ok(dashboard_id)
487 }
488
489 async fn get_role_usage_series(
491 &self,
492 _role_id: Option<&str>,
493 _group_by: Option<&str>,
494 _time_range: &TimeRange,
495 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
496 Ok(vec![
498 ChartSeries {
499 name: "Admin".to_string(),
500 data: vec![DataPoint {
501 timestamp: None,
502 label: Some("Admin".to_string()),
503 value: 45.0,
504 metadata: HashMap::new(),
505 }],
506 color: Some("#ff6b6b".to_string()),
507 series_type: None,
508 },
509 ChartSeries {
510 name: "User".to_string(),
511 data: vec![DataPoint {
512 timestamp: None,
513 label: Some("User".to_string()),
514 value: 120.0,
515 metadata: HashMap::new(),
516 }],
517 color: Some("#4ecdc4".to_string()),
518 series_type: None,
519 },
520 ])
521 }
522
523 async fn get_permission_usage_series(
524 &self,
525 _permission_id: Option<&str>,
526 _group_by: Option<&str>,
527 _time_range: &TimeRange,
528 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
529 Ok(vec![])
531 }
532
533 async fn get_compliance_series(
534 &self,
535 _metric_type: &str,
536 _time_range: &TimeRange,
537 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
538 Ok(vec![ChartSeries {
540 name: "Compliance Score".to_string(),
541 data: vec![DataPoint {
542 timestamp: None,
543 label: None,
544 value: 92.5,
545 metadata: HashMap::new(),
546 }],
547 color: Some("#45b7d1".to_string()),
548 series_type: None,
549 }])
550 }
551
552 async fn get_performance_series(
553 &self,
554 _metric_type: &str,
555 _time_range: &TimeRange,
556 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
557 Ok(vec![ChartSeries {
559 name: "Response Time".to_string(),
560 data: vec![DataPoint {
561 timestamp: None,
562 label: None,
563 value: 15.5,
564 metadata: HashMap::new(),
565 }],
566 color: Some("#96ceb4".to_string()),
567 series_type: None,
568 }])
569 }
570
571 async fn get_event_count_series(
572 &self,
573 _event_type: Option<&str>,
574 _filters: &HashMap<String, String>,
575 _time_range: &TimeRange,
576 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
577 Ok(vec![])
579 }
580
581 async fn get_custom_series(
582 &self,
583 _query: &str,
584 _parameters: &HashMap<String, String>,
585 _time_range: &TimeRange,
586 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
587 Ok(vec![])
589 }
590
591 fn calculate_widget_summary(&self, series: &[ChartSeries]) -> WidgetSummary {
592 let all_values: Vec<f64> = series
593 .iter()
594 .flat_map(|s| s.data.iter().map(|d| d.value))
595 .collect();
596
597 let total = all_values.iter().sum();
598 let count = all_values.len() as f64;
599 let average = if count > 0.0 { total / count } else { 0.0 };
600 let minimum = all_values.iter().copied().fold(f64::INFINITY, f64::min);
601 let maximum = all_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
602
603 WidgetSummary {
604 total,
605 average,
606 minimum: if minimum.is_infinite() { 0.0 } else { minimum },
607 maximum: if maximum.is_infinite() { 0.0 } else { maximum },
608 change_percent: None, }
610 }
611
612 fn check_alert_status(
613 &self,
614 summary: &WidgetSummary,
615 thresholds: &Option<AlertThresholds>,
616 ) -> AlertStatus {
617 let Some(thresholds) = thresholds else {
618 return AlertStatus::Normal;
619 };
620
621 let value = summary.average; let exceeds_critical = match thresholds.comparison {
624 ThresholdComparison::GreaterThan => value > thresholds.critical,
625 ThresholdComparison::LessThan => value < thresholds.critical,
626 ThresholdComparison::Equals => (value - thresholds.critical).abs() < f64::EPSILON,
627 ThresholdComparison::NotEquals => (value - thresholds.critical).abs() > f64::EPSILON,
628 };
629
630 let exceeds_warning = match thresholds.comparison {
631 ThresholdComparison::GreaterThan => value > thresholds.warning,
632 ThresholdComparison::LessThan => value < thresholds.warning,
633 ThresholdComparison::Equals => (value - thresholds.warning).abs() < f64::EPSILON,
634 ThresholdComparison::NotEquals => (value - thresholds.warning).abs() > f64::EPSILON,
635 };
636
637 if exceeds_critical {
638 AlertStatus::Critical
639 } else if exceeds_warning {
640 AlertStatus::Warning
641 } else {
642 AlertStatus::Normal
643 }
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use super::*;
650
651 #[test]
652 fn test_dashboard_config_default() {
653 let config = DashboardConfig::default();
654 assert_eq!(config.refresh_interval_seconds, 30);
655 assert!(config.real_time_updates);
656 assert!(config.alerts_enabled);
657 }
658
659 #[tokio::test]
660 async fn test_dashboard_manager_creation() {
661 let config = DashboardConfig::default();
662 let manager = DashboardManager::new(config);
663 assert_eq!(manager.dashboards.len(), 0);
664 }
665
666 #[tokio::test]
667 async fn test_create_rbac_overview_dashboard() {
668 let config = DashboardConfig::default();
669 let mut manager = DashboardManager::new(config);
670
671 let dashboard_id = manager.create_rbac_overview_dashboard().await.unwrap();
672 assert!(!dashboard_id.is_empty());
673
674 let dashboard = manager.get_dashboard(&dashboard_id).await.unwrap().unwrap();
675 assert_eq!(dashboard.title, "RBAC Overview");
676 assert_eq!(dashboard.widgets.len(), 5);
677 }
678
679 #[test]
680 fn test_alert_status_checking() {
681 let config = DashboardConfig::default();
682 let manager = DashboardManager::new(config);
683
684 let summary = WidgetSummary {
685 total: 100.0,
686 average: 150.0,
687 minimum: 100.0,
688 maximum: 200.0,
689 change_percent: None,
690 };
691
692 let thresholds = AlertThresholds {
693 warning: 100.0,
694 critical: 200.0,
695 comparison: ThresholdComparison::GreaterThan,
696 };
697
698 let status = manager.check_alert_status(&summary, &Some(thresholds));
699 assert_eq!(status, AlertStatus::Warning);
700 }
701}
702
703