Rustelo/features/content-graph/templates/components/graph_view.rs
Jesús Pérez c0281e759c
chore: update
2026-07-18 20:14:32 +01:00

137 lines
5.1 KiB
Rust

// content_graph/graph_view.rs
//
// Full-page interactive knowledge graph via Cytoscape.js.
// Follows the SSR+WASM delegation pattern used across the codebase.
//
// SSR: renders container div + embedded JSON data script tag.
// WASM: Effect::new calls window.ContentGraph.render() after mount.
use leptos::prelude::*;
use super::GRAPH_JSON;
// ── Entry point (delegating component) ───────────────────────────────────────
/// Full-page knowledge graph view.
///
/// `node_id`: when provided, the graph centers on that node (ego view).
#[component]
pub fn GraphView(
#[prop(optional)] node_id: Option<String>,
#[prop(default = String::from("light"))] theme: String,
) -> impl IntoView {
#[cfg(not(target_arch = "wasm32"))]
return view! { <GraphViewInner node_id=node_id theme=theme /> };
#[cfg(target_arch = "wasm32")]
return view! { <GraphViewClient node_id=node_id theme=theme /> };
}
// ── Inner (SSR) ───────────────────────────────────────────────────────────────
#[component]
fn GraphViewInner(
node_id: Option<String>,
theme: String,
) -> impl IntoView {
let focus = node_id.unwrap_or_default();
let graph_json_safe = GRAPH_JSON.replace("</", "\\/");
view! {
<div class="content-graph-page w-full">
// Embedded graph data — read by the JS shim
<script
type="application/json"
id="content-graph-data"
inner_html=graph_json_safe
/>
// Cytoscape container
<div
id="content-graph-cyto"
data-focus=focus
data-theme=theme
class="w-full h-[600px] ds-bg-page ds-border ds-rounded-lg overflow-hidden"
aria-label="Knowledge graph visualisation"
role="img"
>
<noscript>
<p class="p-4 ds-caption text-center">
"Interactive graph requires JavaScript."
</p>
</noscript>
</div>
</div>
}
}
// ── Client (WASM) ─────────────────────────────────────────────────────────────
#[component]
fn GraphViewClient(
node_id: Option<String>,
theme: String,
) -> impl IntoView {
let focus = node_id.unwrap_or_default();
let focus_clone = focus.clone();
let theme_clone = theme.clone();
// Initialise Cytoscape after the component mounts in the browser
Effect::new(move |_| {
let focus = focus_clone.clone();
let theme = theme_clone.clone();
call_cytoscape_render("content-graph-cyto", &focus, &theme);
});
// On unmount, destroy the Cytoscape instance
on_cleanup(move || {
call_cytoscape_destroy("content-graph-cyto");
});
view! {
<div class="content-graph-page w-full">
<div
id="content-graph-cyto"
data-focus=focus
data-theme=theme
class="w-full h-[600px] ds-bg-page ds-border ds-rounded-lg overflow-hidden"
aria-label="Knowledge graph visualisation"
role="img"
/>
</div>
}
}
// ── JS interop ────────────────────────────────────────────────────────────────
#[cfg(target_arch = "wasm32")]
fn call_cytoscape_render(container_id: &str, focus: &str, theme: &str) {
use wasm_bindgen::JsValue;
let Some(window) = web_sys::window() else { return };
let Ok(content_graph) = js_sys::Reflect::get(&window, &JsValue::from_str("ContentGraph")) else { return };
let Ok(render_fn) = js_sys::Reflect::get(&content_graph, &JsValue::from_str("render")) else { return };
let render_fn: js_sys::Function = render_fn.dyn_into().unwrap_or_else(|_| js_sys::Function::new_no_args(""));
let _ = render_fn.call3(
&content_graph,
&JsValue::from_str(container_id),
&JsValue::from_str(focus),
&JsValue::from_str(theme),
);
}
#[cfg(not(target_arch = "wasm32"))]
fn call_cytoscape_render(_container_id: &str, _focus: &str, _theme: &str) {}
#[cfg(target_arch = "wasm32")]
fn call_cytoscape_destroy(container_id: &str) {
use wasm_bindgen::JsValue;
let Some(window) = web_sys::window() else { return };
let Ok(content_graph) = js_sys::Reflect::get(&window, &JsValue::from_str("ContentGraph")) else { return };
let Ok(destroy_fn) = js_sys::Reflect::get(&content_graph, &JsValue::from_str("destroy")) else { return };
let destroy_fn: js_sys::Function = destroy_fn.dyn_into().unwrap_or_else(|_| js_sys::Function::new_no_args(""));
let _ = destroy_fn.call1(&content_graph, &JsValue::from_str(container_id));
}
#[cfg(not(target_arch = "wasm32"))]
fn call_cytoscape_destroy(_container_id: &str) {}