1use 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#[component]
16pub fn Dashboard() -> Element {
17 let app_state = use_app_state();
18 let loading = use_signal(|| false); let current_user = app_state.current_user.clone();
22
23 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#[component]
79fn RefreshButton(loading: Signal<bool>) -> Element {
80 let handle_refresh = move |_| {
81 loading.set(true);
82 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#[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#[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() { 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#[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 div {
221 class: "lg:col-span-2",
222 RecentActivityCard { activities: recent_activities }
223 }
224
225 div {
227 class: "lg:col-span-1",
228 QuickActionsCard { actions: quick_actions }
229 }
230 }
231 }
232}
233
234#[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#[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#[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() { QuickActionItem {
372 key: "{action.id}",
373 action: action.clone() }
375 }
376 }
377 }
378 }
379 }
380}
381
382#[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#[component]
453fn SystemHealthCard() -> Element {
454 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#[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#[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}