fix: SBOM export now triggers browser file download instead of showing JSON
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Failing after 3m59s
CI / Security Audit (push) Successful in 1m47s
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
Some checks failed
CI / Format (push) Successful in 3s
CI / Clippy (push) Failing after 3m59s
CI / Security Audit (push) Successful in 1m47s
CI / Tests (push) Has been skipped
CI / Detect Changes (push) Has been skipped
CI / Deploy Agent (push) Has been skipped
CI / Deploy Dashboard (push) Has been skipped
CI / Deploy Docs (push) Has been skipped
CI / Deploy MCP (push) Has been skipped
Replace the inline <pre> JSON display with a proper browser download using Blob + URL.createObjectURL. Clicking "Download" now saves a .json file (CycloneDX or SPDX format) directly to the user's downloads folder. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ path = "../bin/main.rs"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
web = ["dioxus/web", "dioxus/router", "dioxus/fullstack", "dep:web-sys", "dep:gloo-timers"]
|
||||
web = ["dioxus/web", "dioxus/router", "dioxus/fullstack", "dep:web-sys", "dep:js-sys", "dep:wasm-bindgen", "dep:gloo-timers"]
|
||||
server = [
|
||||
"dioxus/server",
|
||||
"dioxus/router",
|
||||
@@ -51,7 +51,9 @@ thiserror = { workspace = true }
|
||||
|
||||
# Web-only
|
||||
reqwest = { workspace = true, optional = true }
|
||||
web-sys = { version = "0.3", optional = true }
|
||||
web-sys = { version = "0.3", optional = true, features = ["Blob", "BlobPropertyBag", "HtmlAnchorElement", "Url", "Document", "Window"] }
|
||||
js-sys = { version = "0.3", optional = true }
|
||||
wasm-bindgen = { version = "0.2", optional = true }
|
||||
gloo-timers = { version = "0.3", features = ["futures"], optional = true }
|
||||
|
||||
# Server-only
|
||||
|
||||
@@ -23,7 +23,6 @@ pub fn SbomPage() -> Element {
|
||||
// ── Export state ──
|
||||
let mut show_export = use_signal(|| false);
|
||||
let mut export_format = use_signal(|| "cyclonedx".to_string());
|
||||
let mut export_result = use_signal(|| Option::<String>::None);
|
||||
|
||||
// ── Diff state ──
|
||||
let mut diff_repo_a = use_signal(String::new);
|
||||
@@ -213,8 +212,15 @@ pub fn SbomPage() -> Element {
|
||||
let repo = repo_filter();
|
||||
let fmt = export_format();
|
||||
spawn(async move {
|
||||
match fetch_sbom_export(repo, fmt).await {
|
||||
Ok(json) => export_result.set(Some(json)),
|
||||
match fetch_sbom_export(repo.clone(), fmt.clone()).await {
|
||||
Ok(json) => {
|
||||
let filename = if fmt == "spdx" {
|
||||
format!("sbom-{repo}-spdx.json")
|
||||
} else {
|
||||
format!("sbom-{repo}-cyclonedx.json")
|
||||
};
|
||||
trigger_download(&json, &filename);
|
||||
}
|
||||
Err(e) => tracing::error!("Export failed: {e}"),
|
||||
}
|
||||
});
|
||||
@@ -229,24 +235,6 @@ pub fn SbomPage() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Export result display ──
|
||||
if let Some(json) = export_result() {
|
||||
div { class: "card sbom-export-result",
|
||||
div { class: "sbom-export-result-header",
|
||||
strong { "Exported SBOM" }
|
||||
button {
|
||||
class: "btn btn-secondary",
|
||||
onclick: move |_| export_result.set(None),
|
||||
"Close"
|
||||
}
|
||||
}
|
||||
pre {
|
||||
style: "max-height: 400px; overflow: auto; font-size: 12px;",
|
||||
"{json}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SBOM table ──
|
||||
match &*sbom.read() {
|
||||
Some(Some(resp)) => {
|
||||
@@ -685,3 +673,35 @@ fn license_type_class(is_copyleft: bool) -> &'static str {
|
||||
"license-permissive"
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "web")]
|
||||
fn trigger_download(content: &str, filename: &str) {
|
||||
use wasm_bindgen::JsCast;
|
||||
let window = web_sys::window().expect("no window");
|
||||
let document = window.document().expect("no document");
|
||||
|
||||
let blob_parts = js_sys::Array::new();
|
||||
blob_parts.push(&wasm_bindgen::JsValue::from_str(content));
|
||||
|
||||
let mut opts = web_sys::BlobPropertyBag::new();
|
||||
opts.type_("application/json");
|
||||
let blob = web_sys::Blob::new_with_str_sequence_and_options(&blob_parts, &opts).expect("blob");
|
||||
|
||||
let url = web_sys::Url::create_object_url_with_blob(&blob).expect("object url");
|
||||
|
||||
let a: web_sys::HtmlAnchorElement = document
|
||||
.create_element("a")
|
||||
.expect("create a")
|
||||
.dyn_into()
|
||||
.expect("cast");
|
||||
a.set_href(&url);
|
||||
a.set_download(filename);
|
||||
a.click();
|
||||
|
||||
let _ = web_sys::Url::revoke_object_url(&url);
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "web"))]
|
||||
fn trigger_download(_content: &str, _filename: &str) {
|
||||
// Server-side: no-op (downloads only happen in the browser)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user