use std::time::Duration;
use dot_ix::{
model::{
common::{DotSrcAndStyles, GraphvizDotTheme},
info_graph::InfoGraph,
},
rt::IntoGraphvizDotSrc,
web_components::{DotSvg, FlexDiag},
};
use leptos::*;
#[cfg(target_arch = "wasm32")]
use super::QUERY_PARAM_DIAGRAM_ONLY;
const INFO_GRAPH_DEMO: &str = include_str!("info_graph_example.yaml");
#[cfg(target_arch = "wasm32")]
const QUERY_PARAM_SRC: &str = "src";
#[cfg(target_arch = "wasm32")]
fn info_graph_src_init(set_info_graph_src: WriteSignal<String>) {
use js_sys::Array;
use lz_str::decompress_from_encoded_uri_component;
use web_sys::{console, Document, Url, UrlSearchParams};
create_effect(move |_| {
if let Some(window) = web_sys::window() {
let url_search_params = {
let url = Url::new(&String::from(window.location().to_string()))
.expect("Expected URL to be valid.");
let hash = url.hash();
if hash.is_empty() {
Some(url.search_params())
} else {
let hash = hash.replacen('#', "?", 1);
match UrlSearchParams::new_with_str(hash.as_str()) {
Ok(search_params) => Some(search_params),
Err(error) => {
let message = Array::new_with_length(1);
message.set(0, error);
console::log(&message);
None
}
}
}
};
if let Some(url_search_params) = url_search_params {
let info_graph_src_initial = url_search_params
.get(QUERY_PARAM_SRC)
.map(|src| {
if src.contains("\n") {
src
} else {
decompress_from_encoded_uri_component(&src).map_or_else(
|| format!("# deserialize src error: invalid data"),
|s| {
String::from_utf16(&s).unwrap_or_else(|_| {
format!("# deserialize src error: invalid data")
})
},
)
}
})
.unwrap_or_else(|| String::from(INFO_GRAPH_DEMO));
set_info_graph_src.set(info_graph_src_initial);
set_timeout(
move || {
let _ = window
.document()
.as_ref()
.and_then(Document::body)
.as_deref()
.map(|element| element.append_with_str_1(""));
},
Duration::from_millis(200),
);
}
} else {
set_info_graph_src.set(String::from("# Could not extract search params."));
}
});
}
#[component]
pub fn InfoGraph(diagram_only: ReadSignal<bool>) -> impl IntoView {
let (info_graph_src, set_info_graph_src) = create_signal(String::from(INFO_GRAPH_DEMO));
let flex_diag_radio = create_node_ref::<html::Input>();
let (flex_diag_visible, flex_diag_visible_set) = create_signal(false);
let flex_diag_visible_update = move |_ev| {
flex_diag_visible_set.set(
flex_diag_radio
.get()
.map(|input| input.checked())
.unwrap_or(true),
);
};
let layout_classes = move || {
if diagram_only.get() {
"flex items-start"
} else {
"flex items-start flex-wrap"
}
};
let textbox_div_display_classes = move || {
if diagram_only.get() {
"hidden"
} else {
"tabs basis-full grow md:basis-1/2"
}
};
let (error_text, set_error_text) = create_signal(None::<String>);
let (dot_src, set_dot_src) = create_signal(None::<String>);
let (styles, set_styles) = create_signal(None::<String>);
let dot_src_and_styles = move || {
dot_src
.get()
.zip(styles.get())
.map(|(dot_src, styles)| DotSrcAndStyles { dot_src, styles })
};
#[cfg(target_arch = "wasm32")]
info_graph_src_init(set_info_graph_src);
let (info_graph, set_info_graph) = create_signal(InfoGraph::default());
create_effect(move |_| {
let info_graph_src = info_graph_src.get();
let merge_key_exists = info_graph_src.lines().any(|line| {
line.trim_start().starts_with("<<:")
});
let info_graph_result = if merge_key_exists {
let info_graph_value = serde_yaml::from_str::<serde_yaml::Value>(&info_graph_src);
info_graph_value
.and_then(|mut value| {
value.apply_merge()?;
Ok(value)
})
.and_then(serde_yaml::from_value::<InfoGraph>)
} else {
serde_yaml::from_str::<InfoGraph>(&info_graph_src)
};
let info_graph_result = &info_graph_result;
match info_graph_result {
Ok(info_graph) => {
set_info_graph.set(info_graph.clone());
let DotSrcAndStyles { dot_src, styles } =
IntoGraphvizDotSrc::into(info_graph, &GraphvizDotTheme::default());
set_dot_src.set(Some(dot_src));
set_styles.set(Some(styles));
set_error_text.set(None);
#[cfg(target_arch = "wasm32")]
{
use lz_str::compress_to_encoded_uri_component;
let src_compressed = compress_to_encoded_uri_component(&info_graph_src);
if let Some(window) = web_sys::window() {
let url = {
let url =
web_sys::Url::new(&String::from(window.location().to_string()))
.expect("Expected URL to be valid.");
url.search_params().delete(QUERY_PARAM_SRC);
url.search_params().delete(QUERY_PARAM_DIAGRAM_ONLY);
let fragment = {
let mut fragment = String::with_capacity(
QUERY_PARAM_SRC.len() + src_compressed.len() + 64,
);
fragment.push_str(QUERY_PARAM_SRC);
fragment.push_str("=");
fragment.push_str(&src_compressed);
if diagram_only.get() {
fragment.push_str("&");
fragment.push_str(QUERY_PARAM_DIAGRAM_ONLY);
fragment.push_str("=true");
}
fragment
};
url.set_hash(&fragment);
url.to_string()
.as_string()
.expect("# Failed to decode src parameter")
};
let _ = window
.history() .and_then(|h| {
h.replace_state_with_url(&"".into(), "".into(), Some(&url))
});
}
}
}
Err(error) => {
set_dot_src.set(None);
set_styles.set(None);
set_error_text.set(Some(format!("{error}")));
}
}
});
view! {
<div class={ layout_classes }>
<div class={ textbox_div_display_classes }>
<input type="radio" name="src_tabs" id="tab_info_graph_yml" checked="checked" />
<label for="tab_info_graph_yml">"info_graph.yml"</label>
<div class="tab">
<textarea
id="info_graph_yml"
name="info_graph_yml"
class="
border
border-slate-400
bg-slate-100
font-mono
min-w-full
min-h-full
p-2
rounded
text-xs
"
on:input=leptos_dom::helpers::debounce(Duration::from_millis(200), move |ev| {
let info_graph_src = event_target_value(&ev);
set_info_graph_src.set(info_graph_src);
})
prop:value=info_graph_src />
<br />
<div
class={
move || {
let error_text = error_text.get();
let error_text_empty = error_text
.as_deref()
.map(str::is_empty)
.unwrap_or(true);
if error_text_empty {
"hidden"
} else {
"
border
border-amber-300
bg-gradient-to-b from-amber-100 to-amber-200
rounded
"
}
}
}
>{
move || {
let error_text = error_text.get();
error_text.as_deref()
.unwrap_or("")
.to_string()
}
}</div>
</div>
<input type="radio" name="src_tabs" id="tab_info_graph_dot" />
<label for="tab_info_graph_dot">"info_graph.dot"</label>
<div class="tab">
<textarea
id="info_graph_dot"
name="info_graph_dot"
class="
border
border-slate-400
bg-slate-100
min-w-full
min-h-full
font-mono
p-2
rounded
text-xs
"
on:input=leptos_dom::helpers::debounce(Duration::from_millis(400), move |ev| {
let dot_src = event_target_value(&ev);
set_dot_src.set(Some(dot_src));
})
prop:value={
move || {
let dot_src = dot_src.get();
dot_src.as_deref()
.unwrap_or("")
.to_string()
}
} />
</div>
</div>
<div
class={move || {
if diagram_only.get() {
"tabs basis-full grow"
} else {
"tabs basis-full grow md:basis-1/2"
}
}}
>
<input
type="radio"
name="diagram_tabs"
id="tab_dot_svg"
on:change=flex_diag_visible_update
checked="checked"
/>
<label
for="tab_dot_svg"
style={move || if diagram_only.get() { "display: none;" } else { "" }}
>"Dot SVG"</label>
<div class="tab">
<div class="diagram">
<DotSvg
dot_src_and_styles=dot_src_and_styles.into()
diagram_only=diagram_only.into()
/>
</div>
</div>
<input
type="radio"
name="diagram_tabs"
id="tab_flex_diag"
node_ref=flex_diag_radio
on:change=flex_diag_visible_update
/>
<label
for="tab_flex_diag"
style={move || if diagram_only.get() { "display: none;" } else { "" }}
>"Flex Diagram"</label>
<div class="tab">
<div class="diagram">
<FlexDiag
info_graph=info_graph
visible=flex_diag_visible.into()
/>
</div>
</div>
</div>
</div>
}
}