feat: added leptos web frontend poc

This commit is contained in:
əlemi 2023-10-20 04:21:48 +02:00
parent ac20d43b20
commit 02bfc8afc7
Signed by: alemi
GPG key ID: A4895B84D311642C
3 changed files with 234 additions and 0 deletions

View file

@ -16,6 +16,11 @@ name = "fedicharter-cli"
path = "src/cli/main.rs" path = "src/cli/main.rs"
required-features = ["cli"] required-features = ["cli"]
[[bin]]
name = "fedicharter-web"
path = "src/web/main.rs"
required-features = ["web"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
@ -37,8 +42,15 @@ reqwest = { version = "0.11.20", features = ["json"], optional = true }
sea-orm = { version = "0.12.3", features = ["runtime-tokio-native-tls", "sqlx-sqlite", "sqlx-postgres"], optional = true } sea-orm = { version = "0.12.3", features = ["runtime-tokio-native-tls", "sqlx-sqlite", "sqlx-postgres"], optional = true }
clap = { version = "4.4.6", features = ["derive"], optional = true } clap = { version = "4.4.6", features = ["derive"], optional = true }
# wasm and web stuff
console_error_panic_hook = { version = "0.1.7", optional = true }
console_log = { version = "1.0.0", optional = true }
leptos = { version = "0.5.1", features = ["csr"], optional = true }
reqwasm = { version = "0.5.0", optional = true }
[features] [features]
default = ["web", "cli"] default = ["web", "cli"]
db = ["dep:tokio", "dep:sea-orm", "dep:reqwest"] db = ["dep:tokio", "dep:sea-orm", "dep:reqwest"]
web = ["dep:console_error_panic_hook", "dep:console_log", "dep:leptos", "dep:reqwasm"]
cli = ["db", "dep:axum", "dep:tracing-subscriber", "dep:clap"] cli = ["db", "dep:axum", "dep:tracing-subscriber", "dep:clap"]

68
index.html Normal file
View file

@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>akkoma bubble network</title>
<script
type="text/javascript"
src="https://unpkg.com/vis-network/standalone/umd/vis-network.min.js"
></script>
<style type="text/css">
body {
background: #111111;
}
#netgraph {
width: 100%;
height: 98vh;
}
</style>
</head>
<body>
<div id="netgraph">
<h3 style="text-align: center;" id="loader"><font color="#BF616A">crunching fedi network data, be patient...</font></h3>
</div>
<script type="text/javascript">
let nodes_array = []
let edges_array = []
let domain;
const urlParams = new URLSearchParams(window.location.search);
const domainParam = urlParams.get('domain');
if (domainParam) {
domain = domainParam;
console.log("scanning domain " + domain);
} else {
domain = window.prompt("starting instance for charting", "ihatebeinga.live");
}
fetch(`https://api.alemi.dev/akkoma/bubble/crawl?domain=${domain}`)
.then((res) => res.json().then((graph) => {
const palette = [
'#81A1C1',
'#5B6EA3',
'#7468B0',
'#84508C',
'#AF875F',
'#EBCB8B',
'#2E8757',
'#05979A',
]
for (const i in graph[0]) {
if (graph[0][i].title === domain) { graph[0][i].color = '#BF616A' }
else { graph[0][i].color = palette[Math.floor(Math.random() * palette.length)] }
}
const nodes = new vis.DataSet(graph[0]);
const edges = new vis.DataSet(graph[1]);
// create a network
const container = document.getElementById("netgraph");
const data = {
nodes: nodes,
edges: edges,
};
const options = { edges: { dashes: true, arrows: 'to' }, nodes: { color: "#bf616a", shape: "box", scaling: { min: 1, max: 500, label: { enabled: true, min: 14, max: 56 }}}};
const network = new vis.Network(container, data, options);
}))
</script>
</body>
</html>

154
src/web/main.rs Normal file
View file

@ -0,0 +1,154 @@
use std::sync::Arc;
use leptos::{*, error::Result};
use fedicharter::nodeinfo::model::NodeInfoOwned;
use wasm_bindgen::JsCast;
fn main() {
_ = console_log::init_with_level(log::Level::Debug);
console_error_panic_hook::set_once();
mount_to(
document().get_element_by_id("#app")
.expect("no #app element")
.unchecked_into(), // lmao i found it like this on leptops examples
|| view! { <App/> }
);
}
async fn fetch_node_info(domain: String) -> Result<(Arc<NodeInfoOwned>, String)> {
Ok((
Arc::new(
reqwasm::http::Request::get(
&format!("https://api.alemi.dev/akkoma/bubble/instance?domain={}", &domain)
).send()
.await?
.json::<NodeInfoOwned>()
.await?
),
domain,
))
}
#[component]
fn App() -> impl IntoView {
// let (live, live_tx) = create_signal("://".to_string());
let (submitted, submitted_tx) = create_signal("social.alemi.dev".to_string());
let nodeinfo = create_local_resource(move || submitted.get(), fetch_node_info);
let fallback = move |errors: RwSignal<Errors>| {
let error_list = move || {
errors.with(|errors| {
errors
.iter()
.map(|(_, e)| view! { <li>{e.to_string()}</li> })
.collect_view()
})
};
view! {
<div class="error">
<h2>"Error"</h2>
<ul>{error_list}</ul>
</div>
}
};
// the renderer can handle Option<_> and Result<_> states
// by displaying nothing for None if the resource is still loading
// and by using the ErrorBoundary fallback to catch Err(_)
// so we'll just use `.and_then()` to map over the happy path
let instance_view = move || {
nodeinfo.and_then(|(data, domain)| {
view! { <InstanceDetails domain=domain.clone() instance=data.clone() /> }
})
};
view ! {
<Searchbar submitted=submitted_tx placeholder="social.alemi.dev".to_string() />
<ErrorBoundary fallback>
<Transition fallback=move || {
view! { <div>"Loading (Suspense Fallback)..."</div> }
}>
<div>
{instance_view}
</div>
</Transition>
</ErrorBoundary>
}
}
#[component]
fn Searchbar(
#[prop(optional)]
live: Option<WriteSignal<String>>,
#[prop(optional)]
submitted: Option<WriteSignal<String>>,
#[prop(default = "".to_string())]
placeholder: String,
) -> impl IntoView {
let input_element: NodeRef<html::Input> = create_node_ref();
let on_submit = move |ev: web_sys::SubmitEvent| {
ev.prevent_default();
if let Some(tx) = submitted {
tx.set(
input_element.get()
.expect("no input field")
.value()
);
}
};
view! {
<div>
<form on:submit=on_submit>
<input type="text"
on:input=move |ev| {
if let Some(tx) = live {
tx.set(event_target_value(&ev));
}
}
node_ref=input_element
prop:value=placeholder
/>
<input type="submit" value="Go"/>
</form>
</div>
}
}
#[component]
fn InstanceDetails(
domain: String,
instance: Arc<NodeInfoOwned>,
) -> impl IntoView {
let title = instance.metadata.get("nodeName")
.map(|x| x.as_str().expect("name is not a string").to_string())
.unwrap_or(domain);
view! {
<div>
<h2>{ &title }</h2>
<h3>"["{instance.protocols.join(",")}"]~ " { &instance.software.name }<sup><a prop:href=instance.software.homepage.as_ref()>1</a></sup> " v" { &instance.software.version }<sup><a prop:href=instance.software.repository.as_ref()>src</a></sup></h3>
<ul>
<li>"users:"
<ul>
<li>"registered: " <b>{ instance.usage.users.total }</b></li>
<li>"active monthly: " <b>{ instance.usage.users.active_month }</b></li>
<li>"active half year: " <b>{ instance.usage.users.active_halfyear }</b></li>
</ul>
</li>
<li>"posts: " <b>{ instance.usage.local_posts }</b></li>
<li>"comments: " <b>{ instance.usage.local_comments }</b></li>
<li>"open registration: " <b>{ instance.open_registrations }</b></li>
<li>"services:"
<ul>
<li>"rx: " <b>"["{ instance.services.inbound.join(", ") }"]"</b></li>
<li>"tx: " <b>"["{ instance.services.outbound.join(", ") }"]"</b></li>
</ul>
</li>
<li>"metadata: " <pre>{ serde_json::to_string_pretty(&instance.metadata) }</pre></li>
</ul>
</div>
}
}