qorzen_oxide/ui/pages/
dashboard.rs

1// src/ui/pages/dashboard.rs - Main dashboard page with overview and stats
2
3use crate::ui::{
4    pages::{PageWrapper, StatCard, StatTrend},
5    router::Route,
6    state::use_app_state,
7};
8use crate::utils::Time;
9use dioxus::prelude::*;
10#[allow(unused_imports)]
11use dioxus_router::prelude::*;
12
13/// Main dashboard component
14// Main dashboard component
15#[component]
16pub fn Dashboard() -> Element {
17    let app_state = use_app_state();
18    let loading = use_signal(|| false); // Remove mut since we're not modifying
19
20    // Clone user data to avoid borrowing issues
21    let current_user = app_state.current_user.clone();
22
23    // Mock data - in real app this would come from API
24    let stats = get_dashboard_stats();
25    let recent_activities = get_recent_activities();
26    let quick_actions = get_quick_actions();
27
28    let page_actions = rsx! {
29        div {
30            class: "flex space-x-3",
31            RefreshButton { loading: loading }
32            Link {
33                to: Route::Settings {},
34                class: "inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
35                svg {
36                    class: "-ml-1 mr-2 h-4 w-4",
37                    xmlns: "http://www.w3.org/2000/svg",
38                    fill: "none",
39                    view_box: "0 0 24 24",
40                    stroke: "currentColor",
41                    path {
42                        stroke_linecap: "round",
43                        stroke_linejoin: "round",
44                        stroke_width: "2",
45                        d: "M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
46                    }
47                    path {
48                        stroke_linecap: "round",
49                        stroke_linejoin: "round",
50                        stroke_width: "2",
51                        d: "M15 12a3 3 0 11-6 0 3 3 0 016 0z"
52                    }
53                }
54                "Settings"
55            }
56        }
57    };
58
59    rsx! {
60        PageWrapper {
61            title: "Dashboard".to_string(),
62            subtitle: Some("Welcome back! Here's what's happening.".to_string()),
63            actions: Some(page_actions),
64
65            WelcomeMessage { user: current_user }
66            StatisticsCards { stats: stats }
67            MainContentGrid {
68                recent_activities: recent_activities,
69                quick_actions: quick_actions
70            }
71            SystemHealthCard {}
72        }
73    }
74}
75
76/// Refresh button component
77/// Refresh button component
78#[component]
79fn RefreshButton(loading: Signal<bool>) -> Element {
80    let handle_refresh = move |_| {
81        loading.set(true);
82        // Simulate refresh
83        spawn(async move {
84            #[cfg(not(target_arch = "wasm32"))]
85            tokio::time::sleep(std::time::Duration::from_millis(1000)).await;
86            #[cfg(target_arch = "wasm32")]
87            gloo_timers::future::TimeoutFuture::new(1000).await;
88            loading.set(false);
89        });
90    };
91
92    let button_content = if loading() {
93        rsx! {
94            svg {
95                class: "animate-spin -ml-1 mr-2 h-4 w-4",
96                xmlns: "http://www.w3.org/2000/svg",
97                fill: "none",
98                view_box: "0 0 24 24",
99                circle {
100                    class: "opacity-25",
101                    cx: "12",
102                    cy: "12",
103                    r: "10",
104                    stroke: "currentColor",
105                    stroke_width: "4",
106                }
107                path {
108                    class: "opacity-75",
109                    fill: "currentColor",
110                    d: "M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
111                }
112            }
113        }
114    } else {
115        rsx! {
116            svg {
117                class: "-ml-1 mr-2 h-4 w-4",
118                xmlns: "http://www.w3.org/2000/svg",
119                fill: "none",
120                view_box: "0 0 24 24",
121                stroke: "currentColor",
122                path {
123                    stroke_linecap: "round",
124                    stroke_linejoin: "round",
125                    stroke_width: "2",
126                    d: "M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
127                }
128            }
129        }
130    };
131
132    rsx! {
133        button {
134            r#type: "button",
135            class: "inline-flex items-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500",
136            onclick: handle_refresh,
137            {button_content}
138            "Refresh"
139        }
140    }
141}
142
143/// Welcome message component
144#[component]
145fn WelcomeMessage(user: Option<crate::ui::state::User>) -> Element {
146    fn fmt_last_login_time(ts: chrono::DateTime<chrono::Utc>) -> String {
147        ts.format("%B %d, %Y at %H:%M").to_string()
148    }
149
150    if let Some(user) = user {
151        let last_login_message = if let Some(last_login) = user.last_login {
152            rsx! {
153                p { "Last login: {fmt_last_login_time(last_login)}" }
154            }
155        } else {
156            rsx! {
157                p { "This is your first login. Welcome to Qorzen!" }
158            }
159        };
160
161        rsx! {
162            div {
163                class: "bg-blue-50 border border-blue-200 rounded-md p-4 mb-6",
164                div {
165                    class: "flex",
166                    div {
167                        class: "flex-shrink-0",
168                        span {
169                            class: "text-2xl",
170                            "👋"
171                        }
172                    }
173                    div {
174                        class: "ml-3",
175                        h3 {
176                            class: "text-sm font-medium text-blue-800",
177                            "Welcome back, {user.profile.display_name}!"
178                        }
179                        div {
180                            class: "mt-2 text-sm text-blue-700",
181                            {last_login_message}
182                        }
183                    }
184                }
185            }
186        }
187    } else {
188        rsx! {}
189    }
190}
191
192/// Statistics cards component
193#[component]
194fn StatisticsCards(stats: Vec<DashboardStat>) -> Element {
195    rsx! {
196        div {
197            class: "grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8",
198            for stat in stats.iter() {  // Use .iter() to avoid moving
199                StatCard {
200                    key: "{stat.id}",
201                    title: stat.title.clone(),
202                    value: stat.value.clone(),
203                    change: stat.change.clone(),
204                    trend: stat.trend.clone(),
205                    icon: stat.icon.clone()
206                }
207            }
208        }
209    }
210}
211
212/// Main content grid component
213#[component]
214fn MainContentGrid(recent_activities: Vec<Activity>, quick_actions: Vec<QuickAction>) -> Element {
215    rsx! {
216        div {
217            class: "grid grid-cols-1 lg:grid-cols-3 gap-6",
218
219            // Recent Activity (2/3 width)
220            div {
221                class: "lg:col-span-2",
222                RecentActivityCard { activities: recent_activities }
223            }
224
225            // Quick Actions (1/3 width)
226            div {
227                class: "lg:col-span-1",
228                QuickActionsCard { actions: quick_actions }
229            }
230        }
231    }
232}
233
234/// Recent activity card component
235#[component]
236fn RecentActivityCard(activities: Vec<Activity>) -> Element {
237    let activity_content = if activities.is_empty() {
238        rsx! {
239            div {
240                class: "text-center py-6",
241                span {
242                    class: "text-4xl mb-2 block",
243                    "📝"
244                }
245                p {
246                    class: "text-gray-500",
247                    "No recent activity"
248                }
249            }
250        }
251    } else {
252        rsx! {
253            div {
254                class: "flow-root",
255                ul {
256                    class: "-mb-8",
257                    for (i, activity) in activities.iter().enumerate() {
258                        ActivityListItem {
259                            key: "{activity.id}",
260                            activity: activity.clone(),
261                            show_line: i < activities.len() - 1
262                        }
263                    }
264                }
265            }
266        }
267    };
268
269    rsx! {
270        div {
271            class: "bg-white shadow rounded-lg",
272            div {
273                class: "px-4 py-5 sm:px-6 border-b border-gray-200",
274                h3 {
275                    class: "text-lg leading-6 font-medium text-gray-900",
276                    "Recent Activity"
277                }
278                p {
279                    class: "mt-1 max-w-2xl text-sm text-gray-500",
280                    "Latest system events and updates"
281                }
282            }
283            div {
284                class: "px-4 py-5 sm:p-6",
285                {activity_content}
286            }
287        }
288    }
289}
290
291/// Individual activity list item
292#[component]
293fn ActivityListItem(activity: Activity, show_line: bool) -> Element {
294    let timeline_line = if show_line {
295        rsx! {
296            span {
297                class: "absolute top-5 left-5 -ml-px h-full w-0.5 bg-gray-200"
298            }
299        }
300    } else {
301        rsx! {}
302    };
303
304    let activity_description = if let Some(description) = &activity.description {
305        rsx! {
306            p {
307                class: "mt-1 text-sm text-gray-600",
308                "{description}"
309            }
310        }
311    } else {
312        rsx! {}
313    };
314
315    fn fmt_activity_time(ts: chrono::DateTime<chrono::Utc>) -> String {
316        ts.format("%H:%M").to_string()
317    }
318
319    rsx! {
320        li {
321            div {
322                class: "relative pb-8",
323                {timeline_line}
324                div {
325                    class: "relative flex items-start space-x-3",
326                    div {
327                        class: "relative",
328                        span {
329                            class: "h-10 w-10 rounded-full flex items-center justify-center text-white {activity.color}",
330                            "{activity.icon}"
331                        }
332                    }
333                    div {
334                        class: "min-w-0 flex-1",
335                        div {
336                            p {
337                                class: "text-sm text-gray-900",
338                                "{activity.title}"
339                            }
340                            {activity_description}
341                        }
342                        div {
343                            class: "mt-2 text-xs text-gray-500",
344                            "{fmt_activity_time(activity.timestamp)}"
345                        }
346                    }
347                }
348            }
349        }
350    }
351}
352
353/// Quick actions card component
354#[component]
355fn QuickActionsCard(actions: Vec<QuickAction>) -> Element {
356    rsx! {
357        div {
358            class: "bg-white shadow rounded-lg",
359            div {
360                class: "px-4 py-5 sm:px-6 border-b border-gray-200",
361                h3 {
362                    class: "text-lg leading-6 font-medium text-gray-900",
363                    "Quick Actions"
364                }
365            }
366            div {
367                class: "px-4 py-5 sm:p-6",
368                div {
369                    class: "space-y-3",
370                    for action in actions.iter() {  // Use .iter() to avoid moving
371                        QuickActionItem {
372                            key: "{action.id}",
373                            action: action.clone()  // Clone the action
374                        }
375                    }
376                }
377            }
378        }
379    }
380}
381
382/// Individual quick action item
383#[component]
384fn QuickActionItem(action: QuickAction) -> Element {
385    if let Some(route) = &action.route {
386        rsx! {
387            Link {
388                to: route.clone(),
389                class: "group flex items-center p-3 rounded-md hover:bg-gray-50 transition-colors",
390                div {
391                    class: "flex-shrink-0",
392                    span {
393                        class: "text-2xl",
394                        "{action.icon}"
395                    }
396                }
397                div {
398                    class: "ml-3 flex-1",
399                    p {
400                        class: "text-sm font-medium text-gray-900 group-hover:text-blue-600",
401                        "{action.title}"
402                    }
403                    p {
404                        class: "text-sm text-gray-500",
405                        "{action.description}"
406                    }
407                }
408                div {
409                    class: "ml-3 flex-shrink-0",
410                    svg {
411                        class: "h-5 w-5 text-gray-400 group-hover:text-blue-500",
412                        xmlns: "http://www.w3.org/2000/svg",
413                        view_box: "0 0 20 20",
414                        fill: "currentColor",
415                        path {
416                            fill_rule: "evenodd",
417                            d: "M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 111.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z",
418                            clip_rule: "evenodd"
419                        }
420                    }
421                }
422            }
423        }
424    } else {
425        rsx! {
426            div {
427                class: "flex items-center p-3 rounded-md",
428                div {
429                    class: "flex-shrink-0",
430                    span {
431                        class: "text-2xl",
432                        "{action.icon}"
433                    }
434                }
435                div {
436                    class: "ml-3 flex-1",
437                    p {
438                        class: "text-sm font-medium text-gray-900",
439                        "{action.title}"
440                    }
441                    p {
442                        class: "text-sm text-gray-500",
443                        "{action.description}"
444                    }
445                }
446            }
447        }
448    }
449}
450
451/// System health monitoring card
452#[component]
453fn SystemHealthCard() -> Element {
454    // Mock system health data
455    let health_metrics = vec![
456        ("CPU Usage", "23%", "bg-green-500"),
457        ("Memory", "67%", "bg-yellow-500"),
458        ("Storage", "45%", "bg-green-500"),
459        ("Network", "12%", "bg-green-500"),
460    ];
461
462    rsx! {
463        div {
464            class: "mt-6",
465            div {
466                class: "bg-white shadow rounded-lg",
467                div {
468                    class: "px-4 py-5 sm:px-6 border-b border-gray-200",
469                    h3 {
470                        class: "text-lg leading-6 font-medium text-gray-900",
471                        "System Health"
472                    }
473                    p {
474                        class: "mt-1 max-w-2xl text-sm text-gray-500",
475                        "Current system performance metrics"
476                    }
477                }
478                div {
479                    class: "px-4 py-5 sm:p-6",
480                    div {
481                        class: "grid grid-cols-2 md:grid-cols-4 gap-4",
482                        for (name, value, color) in health_metrics {
483                            SystemHealthMetric {
484                                key: "{name}",
485                                name: name.to_string(),
486                                value: value.to_string(),
487                                color: color.to_string()
488                            }
489                        }
490                    }
491                }
492            }
493        }
494    }
495}
496
497/// Individual system health metric
498#[component]
499fn SystemHealthMetric(name: String, value: String, color: String) -> Element {
500    rsx! {
501        div {
502            class: "text-center",
503            div {
504                class: "mx-auto w-16 h-16 rounded-full {color} flex items-center justify-center text-white font-bold",
505                "{value}"
506            }
507            p {
508                class: "mt-2 text-sm font-medium text-gray-900",
509                "{name}"
510            }
511        }
512    }
513}
514
515// Data structures for dashboard
516#[derive(Debug, Clone, PartialEq)]
517struct DashboardStat {
518    id: String,
519    title: String,
520    value: String,
521    change: Option<String>,
522    trend: Option<StatTrend>,
523    icon: Option<String>,
524}
525
526#[derive(Debug, Clone, PartialEq)]
527struct Activity {
528    id: String,
529    title: String,
530    description: Option<String>,
531    timestamp: chrono::DateTime<chrono::Utc>,
532    icon: String,
533    color: String,
534}
535
536#[derive(Debug, Clone, PartialEq)]
537struct QuickAction {
538    id: String,
539    title: String,
540    description: String,
541    icon: String,
542    route: Option<Route>,
543}
544
545fn get_dashboard_stats() -> Vec<DashboardStat> {
546    vec![
547        DashboardStat {
548            id: "users".to_string(),
549            title: "Total Users".to_string(),
550            value: "1,234".to_string(),
551            change: Some("+12%".to_string()),
552            trend: Some(StatTrend::Up),
553            icon: Some("👥".to_string()),
554        },
555        DashboardStat {
556            id: "plugins".to_string(),
557            title: "Active Plugins".to_string(),
558            value: "8".to_string(),
559            change: Some("+2".to_string()),
560            trend: Some(StatTrend::Up),
561            icon: Some("🧩".to_string()),
562        },
563        DashboardStat {
564            id: "sessions".to_string(),
565            title: "Active Sessions".to_string(),
566            value: "87".to_string(),
567            change: Some("-5%".to_string()),
568            trend: Some(StatTrend::Down),
569            icon: Some("🔐".to_string()),
570        },
571        DashboardStat {
572            id: "uptime".to_string(),
573            title: "System Uptime".to_string(),
574            value: "99.9%".to_string(),
575            change: None,
576            trend: None,
577            icon: Some("⚡".to_string()),
578        },
579    ]
580}
581
582fn get_recent_activities() -> Vec<Activity> {
583    let now = Time::now();
584    vec![
585        Activity {
586            id: "1".to_string(),
587            title: "New user registered".to_string(),
588            description: Some("john.doe@example.com joined the platform".to_string()),
589            timestamp: now - chrono::Duration::minutes(15),
590            icon: "👤".to_string(),
591            color: "bg-green-500".to_string(),
592        },
593        Activity {
594            id: "2".to_string(),
595            title: "Plugin installed".to_string(),
596            description: Some("Inventory Management plugin was activated".to_string()),
597            timestamp: now - chrono::Duration::hours(2),
598            icon: "🧩".to_string(),
599            color: "bg-blue-500".to_string(),
600        },
601        Activity {
602            id: "3".to_string(),
603            title: "System backup completed".to_string(),
604            description: Some("Daily backup finished successfully".to_string()),
605            timestamp: now - chrono::Duration::hours(6),
606            icon: "💾".to_string(),
607            color: "bg-gray-500".to_string(),
608        },
609        Activity {
610            id: "4".to_string(),
611            title: "Security scan completed".to_string(),
612            description: Some("No vulnerabilities detected".to_string()),
613            timestamp: now - chrono::Duration::hours(12),
614            icon: "🔒".to_string(),
615            color: "bg-green-600".to_string(),
616        },
617    ]
618}
619
620fn get_quick_actions() -> Vec<QuickAction> {
621    vec![
622        QuickAction {
623            id: "profile".to_string(),
624            title: "Update Profile".to_string(),
625            description: "Manage your account settings".to_string(),
626            icon: "👤".to_string(),
627            route: Some(Route::Profile {}),
628        },
629        QuickAction {
630            id: "plugins".to_string(),
631            title: "Browse Plugins".to_string(),
632            description: "Discover and install new plugins".to_string(),
633            icon: "🧩".to_string(),
634            route: Some(Route::Plugins {}),
635        },
636        QuickAction {
637            id: "settings".to_string(),
638            title: "System Settings".to_string(),
639            description: "Configure application preferences".to_string(),
640            icon: "⚙️".to_string(),
641            route: Some(Route::Settings {}),
642        },
643        QuickAction {
644            id: "admin".to_string(),
645            title: "Administration".to_string(),
646            description: "Manage users and system".to_string(),
647            icon: "👑".to_string(),
648            route: Some(Route::Admin {}),
649        },
650    ]
651}
652
653#[cfg(test)]
654mod tests {
655    use super::*;
656
657    #[test]
658    fn test_dashboard_stats() {
659        let stats = get_dashboard_stats();
660        assert!(!stats.is_empty());
661        assert!(stats.iter().any(|s| s.id == "users"));
662    }
663
664    #[test]
665    fn test_dashboard_activities() {
666        let activities = get_recent_activities();
667        assert!(!activities.is_empty());
668    }
669
670    #[test]
671    fn test_quick_actions() {
672        let actions = get_quick_actions();
673        assert!(!actions.is_empty());
674        assert!(actions.iter().any(|a| a.id == "profile"));
675    }
676}