use dioxus::prelude::*; use crate::app::Route; use crate::components::page_header::PageHeader; use crate::components::pagination::Pagination; use crate::components::toast::{ToastType, Toasts}; #[component] pub fn RepositoriesPage() -> Element { let mut page = use_signal(|| 1u64); let mut show_add_form = use_signal(|| false); let mut name = use_signal(String::new); let mut git_url = use_signal(String::new); let mut branch = use_signal(|| "main".to_string()); let mut toasts = use_context::(); let mut repos = use_resource(move || { let p = page(); async move { crate::infrastructure::repositories::fetch_repositories(p) .await .ok() } }); rsx! { PageHeader { title: "Repositories", description: "Tracked git repositories", } div { style: "margin-bottom: 16px;", button { class: "btn btn-primary", onclick: move |_| show_add_form.toggle(), if show_add_form() { "Cancel" } else { "+ Add Repository" } } } if show_add_form() { div { class: "card", div { class: "card-header", "Add Repository" } div { class: "form-group", label { "Name" } input { r#type: "text", placeholder: "my-project", value: "{name}", oninput: move |e| name.set(e.value()), } } div { class: "form-group", label { "Git URL" } input { r#type: "text", placeholder: "https://github.com/org/repo.git", value: "{git_url}", oninput: move |e| git_url.set(e.value()), } } div { class: "form-group", label { "Default Branch" } input { r#type: "text", placeholder: "main", value: "{branch}", oninput: move |e| branch.set(e.value()), } } button { class: "btn btn-primary", onclick: move |_| { let n = name(); let u = git_url(); let b = branch(); spawn(async move { match crate::infrastructure::repositories::add_repository(n, u, b).await { Ok(_) => { toasts.push(ToastType::Success, "Repository added"); repos.restart(); } Err(e) => toasts.push(ToastType::Error, e.to_string()), } }); show_add_form.set(false); name.set(String::new()); git_url.set(String::new()); }, "Add" } } } match &*repos.read() { Some(Some(resp)) => { let total_pages = resp.total.unwrap_or(0).div_ceil(20).max(1); rsx! { div { class: "card", div { class: "table-wrapper", table { thead { tr { th { "Name" } th { "Git URL" } th { "Branch" } th { "Findings" } th { "Last Scanned" } th { "Actions" } } } tbody { for repo in &resp.data { { let repo_id = repo.id.as_ref().map(|id| id.to_hex()).unwrap_or_default(); let repo_id_clone = repo_id.clone(); rsx! { tr { td { "{repo.name}" } td { style: "font-size: 12px; font-family: monospace;", "{repo.git_url}" } td { "{repo.default_branch}" } td { "{repo.findings_count}" } td { { let now = chrono::Utc::now(); let diff = now.signed_duration_since(repo.updated_at); let label = if diff.num_minutes() < 1 { "just now".to_string() } else if diff.num_hours() < 1 { format!("{}m ago", diff.num_minutes()) } else if diff.num_days() < 1 { format!("{}h ago", diff.num_hours()) } else if diff.num_days() < 30 { format!("{}d ago", diff.num_days()) } else { repo.updated_at.format("%Y-%m-%d").to_string() }; rsx! { span { style: "font-size: 12px;", "{label}" } } } } td { style: "display: flex; gap: 4px;", Link { to: Route::GraphExplorerPage { repo_id: repo_id.clone() }, class: "btn btn-ghost", "Graph" } button { class: "btn btn-ghost", onclick: move |_| { let id = repo_id_clone.clone(); spawn(async move { match crate::infrastructure::repositories::trigger_repo_scan(id).await { Ok(_) => toasts.push(ToastType::Success, "Scan triggered"), Err(e) => toasts.push(ToastType::Error, e.to_string()), } }); }, "Scan" } } } } } } } } } Pagination { current_page: page(), total_pages: total_pages, on_page_change: move |p| page.set(p), } } } }, Some(None) => rsx! { div { class: "card", p { "Failed to load repositories." } } }, None => rsx! { div { class: "loading", "Loading repositories..." } }, } } }