- Show updated_at as relative time (e.g. "5m ago", "3d ago") instead of the last_scanned_commit hex SHA which was not a date - Add Graph link button next to Scan button for quick navigation to the repository's code knowledge graph Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
187 lines
8.9 KiB
Rust
187 lines
8.9 KiB
Rust
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::<Toasts>();
|
|
|
|
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..." }
|
|
},
|
|
}
|
|
}
|
|
}
|