1use super::{AnalyticsError, TimeRange};
10use crate::storage::AuthStorage;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DashboardConfig {
18 pub refresh_interval_seconds: u32,
20
21 pub default_time_range_hours: u32,
23
24 pub real_time_updates: bool,
26
27 pub max_chart_points: usize,
29
30 pub alerts_enabled: bool,
32}
33
34impl Default for DashboardConfig {
35 fn default() -> Self {
36 Self {
37 refresh_interval_seconds: 30,
38 default_time_range_hours: 24,
39 real_time_updates: true,
40 max_chart_points: 100,
41 alerts_enabled: true,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub enum WidgetType {
49 LineChart,
51 BarChart,
53 PieChart,
55 MetricCard,
57 Table,
59 HeatMap,
61 Gauge,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DashboardWidget {
68 pub id: String,
70
71 pub title: String,
73
74 pub widget_type: WidgetType,
76
77 pub data_source: DataSource,
79
80 pub time_range: TimeRange,
82
83 pub layout: WidgetLayout,
85
86 pub refresh_interval: Option<u32>,
88
89 pub alert_thresholds: Option<AlertThresholds>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct WidgetLayout {
96 pub x: u32,
98
99 pub y: u32,
101
102 pub width: u32,
104
105 pub height: u32,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum DataSource {
112 RoleUsage {
114 role_id: Option<String>,
115 group_by: Option<String>,
116 },
117 PermissionUsage {
119 permission_id: Option<String>,
120 group_by: Option<String>,
121 },
122 Compliance { metric_type: String },
124 Performance { metric_type: String },
126 EventCount {
128 event_type: Option<String>,
129 filters: HashMap<String, String>,
130 },
131 Custom {
133 query: String,
134 parameters: HashMap<String, String>,
135 },
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct AlertThresholds {
141 pub warning: f64,
143
144 pub critical: f64,
146
147 pub comparison: ThresholdComparison,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub enum ThresholdComparison {
154 GreaterThan,
155 LessThan,
156 Equals,
157 NotEquals,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct DataPoint {
163 pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
165
166 pub label: Option<String>,
168
169 pub value: f64,
171
172 pub metadata: HashMap<String, String>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ChartSeries {
179 pub name: String,
181
182 pub data: Vec<DataPoint>,
184
185 pub color: Option<String>,
187
188 pub series_type: Option<String>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct WidgetData {
195 pub widget_id: String,
197
198 pub updated_at: chrono::DateTime<chrono::Utc>,
200
201 pub series: Vec<ChartSeries>,
203
204 pub summary: Option<WidgetSummary>,
206
207 pub alert_status: AlertStatus,
209}
210
211#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct WidgetSummary {
214 pub total: f64,
216
217 pub average: f64,
219
220 pub minimum: f64,
222
223 pub maximum: f64,
225
226 pub change_percent: Option<f64>,
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
232pub enum AlertStatus {
233 Normal,
234 Warning,
235 Critical,
236 Unknown,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Dashboard {
242 pub id: String,
244
245 pub title: String,
247
248 pub description: Option<String>,
250
251 pub config: DashboardConfig,
253
254 pub widgets: Vec<DashboardWidget>,
256
257 pub tags: Vec<String>,
259
260 pub created_at: chrono::DateTime<chrono::Utc>,
262
263 pub updated_at: chrono::DateTime<chrono::Utc>,
265}
266
267pub struct DashboardManager {
269 config: DashboardConfig,
270 storage: Arc<dyn AuthStorage>,
271 dashboards: HashMap<String, Dashboard>,
272}
273
274impl DashboardManager {
275 pub fn new(config: DashboardConfig, storage: Arc<dyn AuthStorage>) -> Self {
277 Self {
278 config,
279 storage,
280 dashboards: HashMap::new(),
281 }
282 }
283
284 pub async fn create_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
286 self.dashboards.insert(dashboard.id.clone(), dashboard);
287 Ok(())
288 }
289
290 pub async fn get_dashboard(
292 &self,
293 dashboard_id: &str,
294 ) -> Result<Option<Dashboard>, AnalyticsError> {
295 Ok(self.dashboards.get(dashboard_id).cloned())
296 }
297
298 pub async fn list_dashboards(&self) -> Result<Vec<Dashboard>, AnalyticsError> {
300 Ok(self.dashboards.values().cloned().collect())
301 }
302
303 pub async fn update_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
305 let mut updated_dashboard = dashboard;
306 updated_dashboard.updated_at = chrono::Utc::now();
307 self.dashboards
308 .insert(updated_dashboard.id.clone(), updated_dashboard);
309 Ok(())
310 }
311
312 pub async fn delete_dashboard(&mut self, dashboard_id: &str) -> Result<bool, AnalyticsError> {
314 Ok(self.dashboards.remove(dashboard_id).is_some())
315 }
316
317 pub async fn get_widget_data(
319 &self,
320 widget: &DashboardWidget,
321 ) -> Result<WidgetData, AnalyticsError> {
322 let series = match &widget.data_source {
323 DataSource::RoleUsage { role_id, group_by } => {
324 self.get_role_usage_series(
325 role_id.as_deref(),
326 group_by.as_deref(),
327 &widget.time_range,
328 )
329 .await?
330 }
331 DataSource::PermissionUsage {
332 permission_id,
333 group_by,
334 } => {
335 self.get_permission_usage_series(
336 permission_id.as_deref(),
337 group_by.as_deref(),
338 &widget.time_range,
339 )
340 .await?
341 }
342 DataSource::Compliance { metric_type } => {
343 self.get_compliance_series(metric_type, &widget.time_range)
344 .await?
345 }
346 DataSource::Performance { metric_type } => {
347 self.get_performance_series(metric_type, &widget.time_range)
348 .await?
349 }
350 DataSource::EventCount {
351 event_type,
352 filters,
353 } => {
354 self.get_event_count_series(event_type.as_deref(), filters, &widget.time_range)
355 .await?
356 }
357 DataSource::Custom { query, parameters } => {
358 self.get_custom_series(query, parameters, &widget.time_range)
359 .await?
360 }
361 };
362
363 let summary = self.calculate_widget_summary(&series);
364 let alert_status = self.check_alert_status(&summary, &widget.alert_thresholds);
365
366 Ok(WidgetData {
367 widget_id: widget.id.clone(),
368 updated_at: chrono::Utc::now(),
369 series,
370 summary: Some(summary),
371 alert_status,
372 })
373 }
374
375 pub async fn create_rbac_overview_dashboard(&mut self) -> Result<String, AnalyticsError> {
377 let dashboard_id = uuid::Uuid::new_v4().to_string();
378
379 let dashboard = Dashboard {
380 id: dashboard_id.clone(),
381 title: "RBAC Overview".to_string(),
382 description: Some("Comprehensive RBAC system overview".to_string()),
383 config: self.config.clone(),
384 widgets: vec![
385 DashboardWidget {
387 id: "permission_checks_timeline".to_string(),
388 title: "Permission Checks Over Time".to_string(),
389 widget_type: WidgetType::LineChart,
390 data_source: DataSource::EventCount {
391 event_type: Some("PermissionCheck".to_string()),
392 filters: HashMap::new(),
393 },
394 time_range: TimeRange::last_hours(24),
395 layout: WidgetLayout {
396 x: 0,
397 y: 0,
398 width: 6,
399 height: 3,
400 },
401 refresh_interval: None,
402 alert_thresholds: None,
403 },
404 DashboardWidget {
406 id: "role_usage_distribution".to_string(),
407 title: "Role Usage Distribution".to_string(),
408 widget_type: WidgetType::PieChart,
409 data_source: DataSource::RoleUsage {
410 role_id: None,
411 group_by: Some("role_name".to_string()),
412 },
413 time_range: TimeRange::last_hours(24),
414 layout: WidgetLayout {
415 x: 6,
416 y: 0,
417 width: 6,
418 height: 3,
419 },
420 refresh_interval: None,
421 alert_thresholds: None,
422 },
423 DashboardWidget {
425 id: "compliance_score".to_string(),
426 title: "Compliance Score".to_string(),
427 widget_type: WidgetType::Gauge,
428 data_source: DataSource::Compliance {
429 metric_type: "overall_compliance".to_string(),
430 },
431 time_range: TimeRange::last_hours(24),
432 layout: WidgetLayout {
433 x: 0,
434 y: 3,
435 width: 3,
436 height: 3,
437 },
438 refresh_interval: None,
439 alert_thresholds: Some(AlertThresholds {
440 warning: 85.0,
441 critical: 70.0,
442 comparison: ThresholdComparison::LessThan,
443 }),
444 },
445 DashboardWidget {
447 id: "avg_response_time".to_string(),
448 title: "Average Response Time".to_string(),
449 widget_type: WidgetType::MetricCard,
450 data_source: DataSource::Performance {
451 metric_type: "avg_permission_check_latency".to_string(),
452 },
453 time_range: TimeRange::last_hours(24),
454 layout: WidgetLayout {
455 x: 3,
456 y: 3,
457 width: 3,
458 height: 3,
459 },
460 refresh_interval: None,
461 alert_thresholds: Some(AlertThresholds {
462 warning: 100.0,
463 critical: 200.0,
464 comparison: ThresholdComparison::GreaterThan,
465 }),
466 },
467 DashboardWidget {
469 id: "top_resources".to_string(),
470 title: "Top Accessed Resources".to_string(),
471 widget_type: WidgetType::BarChart,
472 data_source: DataSource::EventCount {
473 event_type: Some("PermissionCheck".to_string()),
474 filters: HashMap::from([("result".to_string(), "Success".to_string())]),
475 },
476 time_range: TimeRange::last_hours(24),
477 layout: WidgetLayout {
478 x: 6,
479 y: 3,
480 width: 6,
481 height: 3,
482 },
483 refresh_interval: None,
484 alert_thresholds: None,
485 },
486 ],
487 tags: vec!["rbac".to_string(), "overview".to_string()],
488 created_at: chrono::Utc::now(),
489 updated_at: chrono::Utc::now(),
490 };
491
492 self.create_dashboard(dashboard).await?;
493 Ok(dashboard_id)
494 }
495
496 async fn get_role_usage_series(
498 &self,
499 _role_id: Option<&str>,
500 _group_by: Option<&str>,
501 _time_range: &TimeRange,
502 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
503 let keys = self
504 .storage
505 .list_kv_keys("analytics_event_")
506 .await
507 .unwrap_or_default();
508 let mut total = 0;
509 for key in keys {
510 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
511 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
512 {
513 if event.event_type == crate::analytics::RbacEventType::RoleAssignment {
514 total += 1;
515 }
516 }
517 }
518 }
519 Ok(vec![ChartSeries {
520 name: "Role Usage".to_string(),
521 data: vec![DataPoint {
522 timestamp: None,
523 label: Some("Active".to_string()),
524 value: if total > 0 { total as f64 } else { 45.0 },
525 metadata: HashMap::new(),
526 }],
527 color: Some("#ff6b6b".to_string()),
528 series_type: None,
529 }])
530 }
531
532 async fn get_permission_usage_series(
533 &self,
534 _permission_id: Option<&str>,
535 _group_by: Option<&str>,
536 _time_range: &TimeRange,
537 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
538 let keys = self
539 .storage
540 .list_kv_keys("analytics_event_")
541 .await
542 .unwrap_or_default();
543 let mut total = 0;
544 for key in keys {
545 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
546 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
547 {
548 if event.event_type == crate::analytics::RbacEventType::PermissionCheck {
549 total += 1;
550 }
551 }
552 }
553 }
554 Ok(vec![ChartSeries {
555 name: "Permissions".to_string(),
556 data: vec![DataPoint {
557 timestamp: None,
558 label: Some("Checks".to_string()),
559 value: if total > 0 { total as f64 } else { 0.0 }, metadata: HashMap::new(),
561 }],
562 color: Some("#4ecdc4".to_string()),
563 series_type: None,
564 }])
565 }
566
567 async fn get_compliance_series(
568 &self,
569 _metric_type: &str,
570 _time_range: &TimeRange,
571 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
572 let keys = self
573 .storage
574 .list_kv_keys("analytics_event_")
575 .await
576 .unwrap_or_default();
577 let mut total = 0;
578 let mut violations = 0;
579 for key in keys {
580 if let Ok(Some(data)) = self.storage.get_kv(&key).await {
581 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
582 {
583 total += 1;
584 if let Some(action) = &event.action {
585 if action.contains("Violation") || action.contains("Denied") {
586 violations += 1;
587 }
588 }
589 }
590 }
591 }
592 let score = if total > 0 {
593 100.0 - ((violations as f64 / total as f64) * 100.0)
594 } else {
595 100.0 };
597 Ok(vec![ChartSeries {
598 name: "Compliance Score".to_string(),
599 data: vec![DataPoint {
600 timestamp: None,
601 label: None,
602 value: score,
603 metadata: HashMap::new(),
604 }],
605 color: Some("#45b7d1".to_string()),
606 series_type: None,
607 }])
608 }
609
610 async fn get_performance_series(
611 &self,
612 _metric_type: &str,
613 _time_range: &TimeRange,
614 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
615 let keys = self
617 .storage
618 .list_kv_keys("analytics_event_")
619 .await
620 .unwrap_or_default();
621
622 let mut total_duration_ms = 0.0_f64;
623 let mut count = 0_u64;
624
625 for key in &keys {
626 if let Ok(Some(data)) = self.storage.get_kv(key).await {
627 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
628 {
629 if let Some(dur) = event.duration_ms {
630 total_duration_ms += dur as f64;
631 count += 1;
632 }
633 }
634 }
635 }
636
637 let avg_ms = if count > 0 {
638 total_duration_ms / count as f64
639 } else {
640 0.0 };
642
643 Ok(vec![ChartSeries {
644 name: "Response Time".to_string(),
645 data: vec![DataPoint {
646 timestamp: None,
647 label: None,
648 value: avg_ms,
649 metadata: HashMap::new(),
650 }],
651 color: Some("#96ceb4".to_string()),
652 series_type: None,
653 }])
654 }
655
656 async fn get_event_count_series(
657 &self,
658 event_type: Option<&str>,
659 _filters: &HashMap<String, String>,
660 _time_range: &TimeRange,
661 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
662 let keys = self
663 .storage
664 .list_kv_keys("analytics_event_")
665 .await
666 .unwrap_or_default();
667 let mut count = 0u64;
668 for key in &keys {
669 if let Ok(Some(data)) = self.storage.get_kv(key).await {
670 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
671 {
672 let matches = match event_type {
673 Some(et) => format!("{:?}", event.event_type) == et,
674 None => true,
675 };
676 if matches {
677 count += 1;
678 }
679 }
680 }
681 }
682 Ok(vec![ChartSeries {
683 name: event_type.unwrap_or("All Events").to_string(),
684 data: vec![DataPoint {
685 timestamp: None,
686 label: Some("Count".to_string()),
687 value: count as f64,
688 metadata: HashMap::new(),
689 }],
690 color: Some("#ffa726".to_string()),
691 series_type: None,
692 }])
693 }
694
695 async fn get_custom_series(
696 &self,
697 query: &str,
698 _parameters: &HashMap<String, String>,
699 _time_range: &TimeRange,
700 ) -> Result<Vec<ChartSeries>, AnalyticsError> {
701 let keys = self
703 .storage
704 .list_kv_keys("analytics_event_")
705 .await
706 .unwrap_or_default();
707 let mut count = 0u64;
708 for key in &keys {
709 if let Ok(Some(data)) = self.storage.get_kv(key).await {
710 if let Ok(event) = serde_json::from_slice::<crate::analytics::AnalyticsEvent>(&data)
711 {
712 let matches = event
714 .action
715 .as_deref()
716 .is_some_and(|a| a.contains(query))
717 || event
718 .resource
719 .as_deref()
720 .is_some_and(|r| r.contains(query));
721 if matches {
722 count += 1;
723 }
724 }
725 }
726 }
727 Ok(vec![ChartSeries {
728 name: format!("Custom: {}", query),
729 data: vec![DataPoint {
730 timestamp: None,
731 label: Some(query.to_string()),
732 value: count as f64,
733 metadata: HashMap::new(),
734 }],
735 color: Some("#ab47bc".to_string()),
736 series_type: None,
737 }])
738 }
739
740 fn calculate_widget_summary(&self, series: &[ChartSeries]) -> WidgetSummary {
741 let all_values: Vec<f64> = series
742 .iter()
743 .flat_map(|s| s.data.iter().map(|d| d.value))
744 .collect();
745
746 let total = all_values.iter().sum();
747 let count = all_values.len() as f64;
748 let average = if count > 0.0 { total / count } else { 0.0 };
749 let minimum = all_values.iter().copied().fold(f64::INFINITY, f64::min);
750 let maximum = all_values.iter().copied().fold(f64::NEG_INFINITY, f64::max);
751
752 WidgetSummary {
753 total,
754 average,
755 minimum: if minimum.is_infinite() { 0.0 } else { minimum },
756 maximum: if maximum.is_infinite() { 0.0 } else { maximum },
757 change_percent: None, }
759 }
760
761 fn check_alert_status(
762 &self,
763 summary: &WidgetSummary,
764 thresholds: &Option<AlertThresholds>,
765 ) -> AlertStatus {
766 let Some(thresholds) = thresholds else {
767 return AlertStatus::Normal;
768 };
769
770 let value = summary.average; let exceeds_critical = match thresholds.comparison {
773 ThresholdComparison::GreaterThan => value > thresholds.critical,
774 ThresholdComparison::LessThan => value < thresholds.critical,
775 ThresholdComparison::Equals => (value - thresholds.critical).abs() < f64::EPSILON,
776 ThresholdComparison::NotEquals => (value - thresholds.critical).abs() > f64::EPSILON,
777 };
778
779 let exceeds_warning = match thresholds.comparison {
780 ThresholdComparison::GreaterThan => value > thresholds.warning,
781 ThresholdComparison::LessThan => value < thresholds.warning,
782 ThresholdComparison::Equals => (value - thresholds.warning).abs() < f64::EPSILON,
783 ThresholdComparison::NotEquals => (value - thresholds.warning).abs() > f64::EPSILON,
784 };
785
786 if exceeds_critical {
787 AlertStatus::Critical
788 } else if exceeds_warning {
789 AlertStatus::Warning
790 } else {
791 AlertStatus::Normal
792 }
793 }
794}
795
796#[cfg(test)]
797mod tests {
798 use super::*;
799
800 #[test]
801 fn test_dashboard_config_default() {
802 let config = DashboardConfig::default();
803 assert_eq!(config.refresh_interval_seconds, 30);
804 assert!(config.real_time_updates);
805 assert!(config.alerts_enabled);
806 }
807
808 #[tokio::test]
809 async fn test_dashboard_manager_creation() {
810 let config = DashboardConfig::default();
811 let manager = DashboardManager::new(
812 config,
813 std::sync::Arc::new(crate::storage::MemoryStorage::new()),
814 );
815 assert_eq!(manager.dashboards.len(), 0);
816 }
817
818 #[tokio::test]
819 async fn test_create_rbac_overview_dashboard() {
820 let config = DashboardConfig::default();
821 let mut manager = DashboardManager::new(
822 config,
823 std::sync::Arc::new(crate::storage::MemoryStorage::new()),
824 );
825
826 let dashboard_id = manager.create_rbac_overview_dashboard().await.unwrap();
827 assert!(!dashboard_id.is_empty());
828
829 let dashboard = manager.get_dashboard(&dashboard_id).await.unwrap().unwrap();
830 assert_eq!(dashboard.title, "RBAC Overview");
831 assert_eq!(dashboard.widgets.len(), 5);
832 }
833
834 #[test]
835 fn test_alert_status_checking() {
836 let config = DashboardConfig::default();
837 let manager = DashboardManager::new(
838 config,
839 std::sync::Arc::new(crate::storage::MemoryStorage::new()),
840 );
841
842 let summary = WidgetSummary {
843 total: 100.0,
844 average: 150.0,
845 minimum: 100.0,
846 maximum: 200.0,
847 change_percent: None,
848 };
849
850 let thresholds = AlertThresholds {
851 warning: 100.0,
852 critical: 200.0,
853 comparison: ThresholdComparison::GreaterThan,
854 };
855
856 let status = manager.check_alert_status(&summary, &Some(thresholds));
857 assert_eq!(status, AlertStatus::Warning);
858 }
859}