feat: findings refinement, new scanners, and deployment tooling (#6)
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m3s
CI / Security Audit (push) Successful in 1m38s
CI / Tests (push) Successful in 4m44s
CI / Detect Changes (push) Successful in 2s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Failing after 2s
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Successful in 4m3s
CI / Security Audit (push) Successful in 1m38s
CI / Tests (push) Successful in 4m44s
CI / Detect Changes (push) Successful in 2s
CI / Deploy Agent (push) Successful in 2s
CI / Deploy Dashboard (push) Successful in 2s
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Failing after 2s
This commit was merged in pull request #6.
This commit is contained in:
@@ -5,6 +5,17 @@ use crate::components::page_header::PageHeader;
|
||||
use crate::components::pagination::Pagination;
|
||||
use crate::components::toast::{ToastType, Toasts};
|
||||
|
||||
async fn async_sleep_5s() {
|
||||
#[cfg(feature = "web")]
|
||||
{
|
||||
gloo_timers::future::TimeoutFuture::new(5_000).await;
|
||||
}
|
||||
#[cfg(not(feature = "web"))]
|
||||
{
|
||||
tokio::time::sleep(std::time::Duration::from_secs(5)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn RepositoriesPage() -> Element {
|
||||
let mut page = use_signal(|| 1u64);
|
||||
@@ -12,8 +23,15 @@ pub fn RepositoriesPage() -> Element {
|
||||
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 auth_token = use_signal(String::new);
|
||||
let mut auth_username = use_signal(String::new);
|
||||
let mut show_auth = use_signal(|| false);
|
||||
let mut show_ssh_key = use_signal(|| false);
|
||||
let mut ssh_public_key = use_signal(String::new);
|
||||
let mut adding = use_signal(|| false);
|
||||
let mut toasts = use_context::<Toasts>();
|
||||
let mut confirm_delete = use_signal(|| Option::<(String, String)>::None); // (id, name)
|
||||
let mut scanning_ids = use_signal(Vec::<String>::new);
|
||||
|
||||
let mut repos = use_resource(move || {
|
||||
let p = page();
|
||||
@@ -54,7 +72,7 @@ pub fn RepositoriesPage() -> Element {
|
||||
label { "Git URL" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "https://github.com/org/repo.git",
|
||||
placeholder: "https://github.com/org/repo.git or git@github.com:org/repo.git",
|
||||
value: "{git_url}",
|
||||
oninput: move |e| git_url.set(e.value()),
|
||||
}
|
||||
@@ -68,26 +86,105 @@ pub fn RepositoriesPage() -> Element {
|
||||
oninput: move |e| branch.set(e.value()),
|
||||
}
|
||||
}
|
||||
|
||||
// Private repo auth section
|
||||
div { style: "margin-top: 8px;",
|
||||
button {
|
||||
class: "btn btn-ghost",
|
||||
style: "font-size: 12px; padding: 4px 8px;",
|
||||
onclick: move |_| {
|
||||
show_auth.toggle();
|
||||
if !show_ssh_key() {
|
||||
// Fetch SSH key on first open
|
||||
show_ssh_key.set(true);
|
||||
spawn(async move {
|
||||
match crate::infrastructure::repositories::fetch_ssh_public_key().await {
|
||||
Ok(key) => ssh_public_key.set(key),
|
||||
Err(_) => ssh_public_key.set("(not available)".to_string()),
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
if show_auth() { "Hide auth options" } else { "Private repository?" }
|
||||
}
|
||||
}
|
||||
|
||||
if show_auth() {
|
||||
div { class: "auth-section", style: "margin-top: 12px; padding: 12px; border: 1px solid var(--border-subtle); border-radius: 8px;",
|
||||
// SSH deploy key display
|
||||
div { style: "margin-bottom: 12px;",
|
||||
label { style: "font-size: 12px; color: var(--text-secondary);",
|
||||
"For SSH URLs: add this deploy key (read-only) to your repository"
|
||||
}
|
||||
div {
|
||||
style: "margin-top: 4px; padding: 8px; background: var(--bg-secondary); border-radius: 4px; font-family: monospace; font-size: 11px; word-break: break-all; user-select: all;",
|
||||
if ssh_public_key().is_empty() {
|
||||
"Loading..."
|
||||
} else {
|
||||
"{ssh_public_key}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HTTPS auth fields
|
||||
p { style: "font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;",
|
||||
"For HTTPS URLs: provide an access token (PAT) or username/password"
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Auth Token / Password" }
|
||||
input {
|
||||
r#type: "password",
|
||||
placeholder: "ghp_xxxx or personal access token",
|
||||
value: "{auth_token}",
|
||||
oninput: move |e| auth_token.set(e.value()),
|
||||
}
|
||||
}
|
||||
div { class: "form-group",
|
||||
label { "Username (optional, defaults to x-access-token)" }
|
||||
input {
|
||||
r#type: "text",
|
||||
placeholder: "x-access-token",
|
||||
value: "{auth_username}",
|
||||
oninput: move |e| auth_username.set(e.value()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
disabled: adding(),
|
||||
onclick: move |_| {
|
||||
let n = name();
|
||||
let u = git_url();
|
||||
let b = branch();
|
||||
let tok = {
|
||||
let v = auth_token();
|
||||
if v.is_empty() { None } else { Some(v) }
|
||||
};
|
||||
let usr = {
|
||||
let v = auth_username();
|
||||
if v.is_empty() { None } else { Some(v) }
|
||||
};
|
||||
adding.set(true);
|
||||
spawn(async move {
|
||||
match crate::infrastructure::repositories::add_repository(n, u, b).await {
|
||||
match crate::infrastructure::repositories::add_repository(n, u, b, tok, usr).await {
|
||||
Ok(_) => {
|
||||
toasts.push(ToastType::Success, "Repository added");
|
||||
repos.restart();
|
||||
}
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
adding.set(false);
|
||||
});
|
||||
show_add_form.set(false);
|
||||
show_auth.set(false);
|
||||
name.set(String::new());
|
||||
git_url.set(String::new());
|
||||
auth_token.set(String::new());
|
||||
auth_username.set(String::new());
|
||||
},
|
||||
"Add"
|
||||
if adding() { "Validating..." } else { "Add" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +255,7 @@ pub fn RepositoriesPage() -> Element {
|
||||
let repo_id_scan = repo_id.clone();
|
||||
let repo_id_del = repo_id.clone();
|
||||
let repo_name_del = repo.name.clone();
|
||||
let is_scanning = scanning_ids().contains(&repo_id);
|
||||
rsx! {
|
||||
tr {
|
||||
td { "{repo.name}" }
|
||||
@@ -192,17 +290,44 @@ pub fn RepositoriesPage() -> Element {
|
||||
"Graph"
|
||||
}
|
||||
button {
|
||||
class: "btn btn-ghost",
|
||||
class: if is_scanning { "btn btn-ghost btn-scanning" } else { "btn btn-ghost" },
|
||||
disabled: is_scanning,
|
||||
onclick: move |_| {
|
||||
let id = repo_id_scan.clone();
|
||||
// Add to scanning set
|
||||
let mut ids = scanning_ids();
|
||||
ids.push(id.clone());
|
||||
scanning_ids.set(ids);
|
||||
spawn(async move {
|
||||
match crate::infrastructure::repositories::trigger_repo_scan(id).await {
|
||||
Ok(_) => toasts.push(ToastType::Success, "Scan triggered"),
|
||||
match crate::infrastructure::repositories::trigger_repo_scan(id.clone()).await {
|
||||
Ok(_) => {
|
||||
toasts.push(ToastType::Success, "Scan triggered");
|
||||
// Poll until scan completes
|
||||
loop {
|
||||
async_sleep_5s().await;
|
||||
match crate::infrastructure::repositories::check_repo_scanning(id.clone()).await {
|
||||
Ok(false) => break,
|
||||
Ok(true) => continue,
|
||||
Err(_) => break,
|
||||
}
|
||||
}
|
||||
toasts.push(ToastType::Success, "Scan complete");
|
||||
repos.restart();
|
||||
}
|
||||
Err(e) => toasts.push(ToastType::Error, e.to_string()),
|
||||
}
|
||||
// Remove from scanning set
|
||||
let mut ids = scanning_ids();
|
||||
ids.retain(|i| i != &id);
|
||||
scanning_ids.set(ids);
|
||||
});
|
||||
},
|
||||
"Scan"
|
||||
if is_scanning {
|
||||
span { class: "spinner" }
|
||||
"Scanning..."
|
||||
} else {
|
||||
"Scan"
|
||||
}
|
||||
}
|
||||
button {
|
||||
class: "btn btn-ghost btn-ghost-danger",
|
||||
|
||||
Reference in New Issue
Block a user