Skip to main content

auth_framework/analytics/
dashboard.rs

1//! RBAC Analytics Dashboard
2//!
3//! This module provides dashboard components for visualizing
4//! RBAC analytics data and system performance metrics.
5//!
6//! > **Status:** Dashboard widgets currently surface the metrics that can be
7//! > derived from stored analytics events and snapshots.
8
9use super::{AnalyticsError, TimeRange};
10use crate::storage::AuthStorage;
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14
15/// Dashboard configuration
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct DashboardConfig {
18    /// Refresh interval for real-time data
19    pub refresh_interval_seconds: u32,
20
21    /// Default time range for widgets
22    pub default_time_range_hours: u32,
23
24    /// Enable real-time updates
25    pub real_time_updates: bool,
26
27    /// Maximum number of data points per chart
28    pub max_chart_points: usize,
29
30    /// Enable alerts and notifications
31    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/// Dashboard widget types
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub enum WidgetType {
49    /// Line chart for time series data
50    LineChart,
51    /// Bar chart for categorical data
52    BarChart,
53    /// Pie chart for distribution data
54    PieChart,
55    /// Single metric display
56    MetricCard,
57    /// Data table
58    Table,
59    /// Heat map
60    HeatMap,
61    /// Gauge/progress indicator
62    Gauge,
63}
64
65/// Dashboard widget configuration
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct DashboardWidget {
68    /// Widget identifier
69    pub id: String,
70
71    /// Widget title
72    pub title: String,
73
74    /// Widget type
75    pub widget_type: WidgetType,
76
77    /// Data source query
78    pub data_source: DataSource,
79
80    /// Time range for data
81    pub time_range: TimeRange,
82
83    /// Widget position and size
84    pub layout: WidgetLayout,
85
86    /// Refresh interval override
87    pub refresh_interval: Option<u32>,
88
89    /// Alert thresholds
90    pub alert_thresholds: Option<AlertThresholds>,
91}
92
93/// Widget layout configuration
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct WidgetLayout {
96    /// X position (grid units)
97    pub x: u32,
98
99    /// Y position (grid units)
100    pub y: u32,
101
102    /// Width (grid units)
103    pub width: u32,
104
105    /// Height (grid units)
106    pub height: u32,
107}
108
109/// Data source configuration for widgets
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub enum DataSource {
112    /// Role usage statistics
113    RoleUsage {
114        role_id: Option<String>,
115        group_by: Option<String>,
116    },
117    /// Permission usage statistics
118    PermissionUsage {
119        permission_id: Option<String>,
120        group_by: Option<String>,
121    },
122    /// Compliance metrics
123    Compliance { metric_type: String },
124    /// Performance metrics
125    Performance { metric_type: String },
126    /// Event count with filters
127    EventCount {
128        event_type: Option<String>,
129        filters: HashMap<String, String>,
130    },
131    /// Custom query
132    Custom {
133        query: String,
134        parameters: HashMap<String, String>,
135    },
136}
137
138/// Alert threshold configuration
139#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct AlertThresholds {
141    /// Warning threshold
142    pub warning: f64,
143
144    /// Critical threshold
145    pub critical: f64,
146
147    /// Threshold comparison type
148    pub comparison: ThresholdComparison,
149}
150
151/// Threshold comparison types
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub enum ThresholdComparison {
154    GreaterThan,
155    LessThan,
156    Equals,
157    NotEquals,
158}
159
160/// Dashboard data point
161#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct DataPoint {
163    /// Timestamp (for time series)
164    pub timestamp: Option<chrono::DateTime<chrono::Utc>>,
165
166    /// Category label (for categorical data)
167    pub label: Option<String>,
168
169    /// Numeric value
170    pub value: f64,
171
172    /// Additional metadata
173    pub metadata: HashMap<String, String>,
174}
175
176/// Chart data series
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ChartSeries {
179    /// Series name
180    pub name: String,
181
182    /// Data points
183    pub data: Vec<DataPoint>,
184
185    /// Series color
186    pub color: Option<String>,
187
188    /// Series type (for mixed charts)
189    pub series_type: Option<String>,
190}
191
192/// Widget data response
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct WidgetData {
195    /// Widget ID
196    pub widget_id: String,
197
198    /// Last updated timestamp
199    pub updated_at: chrono::DateTime<chrono::Utc>,
200
201    /// Chart series data
202    pub series: Vec<ChartSeries>,
203
204    /// Summary statistics
205    pub summary: Option<WidgetSummary>,
206
207    /// Alert status
208    pub alert_status: AlertStatus,
209}
210
211/// Widget summary statistics
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct WidgetSummary {
214    /// Total count
215    pub total: f64,
216
217    /// Average value
218    pub average: f64,
219
220    /// Minimum value
221    pub minimum: f64,
222
223    /// Maximum value
224    pub maximum: f64,
225
226    /// Change from previous period
227    pub change_percent: Option<f64>,
228}
229
230/// Alert status for widgets
231#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
232pub enum AlertStatus {
233    Normal,
234    Warning,
235    Critical,
236    Unknown,
237}
238
239/// Complete dashboard configuration
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct Dashboard {
242    /// Dashboard identifier
243    pub id: String,
244
245    /// Dashboard title
246    pub title: String,
247
248    /// Dashboard description
249    pub description: Option<String>,
250
251    /// Dashboard configuration
252    pub config: DashboardConfig,
253
254    /// Widgets in this dashboard
255    pub widgets: Vec<DashboardWidget>,
256
257    /// Dashboard tags for organization
258    pub tags: Vec<String>,
259
260    /// Created timestamp
261    pub created_at: chrono::DateTime<chrono::Utc>,
262
263    /// Last modified timestamp
264    pub updated_at: chrono::DateTime<chrono::Utc>,
265}
266
267/// Dashboard manager
268pub struct DashboardManager {
269    config: DashboardConfig,
270    storage: Arc<dyn AuthStorage>,
271    dashboards: HashMap<String, Dashboard>,
272}
273
274impl DashboardManager {
275    /// Create new dashboard manager
276    pub fn new(config: DashboardConfig, storage: Arc<dyn AuthStorage>) -> Self {
277        Self {
278            config,
279            storage,
280            dashboards: HashMap::new(),
281        }
282    }
283
284    /// Create a new dashboard
285    pub async fn create_dashboard(&mut self, dashboard: Dashboard) -> Result<(), AnalyticsError> {
286        self.dashboards.insert(dashboard.id.clone(), dashboard);
287        Ok(())
288    }
289
290    /// Get dashboard by ID
291    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    /// List all dashboards
299    pub async fn list_dashboards(&self) -> Result<Vec<Dashboard>, AnalyticsError> {
300        Ok(self.dashboards.values().cloned().collect())
301    }
302
303    /// Update dashboard
304    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    /// Delete dashboard
313    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    /// Get widget data
318    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    /// Create predefined RBAC overview dashboard
376    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                // Permission checks over time
386                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                // Role usage distribution
405                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                // Compliance score
424                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                // Average response time
446                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                // Top accessed resources
468                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    // Private helper methods for data series generation
497    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 }, // 0.0 = no data available
560                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 // No events = no violations = fully compliant
596        };
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        // Query actual performance data from analytics events
616        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 // No performance data available
641        };
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        // Execute the custom query against stored analytics events
702        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                    // Match events whose action or resource contains the query string
713                    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, // Would calculate from historical data
758        }
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; // Use average for threshold comparison
771
772        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}