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

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:
Sharang Parnerkar
2026-03-11 09:57:34 +01:00
parent 895c070239
commit be4b43ed64
2 changed files with 45 additions and 23 deletions

View File

@@ -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

View File

@@ -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)
}