mirror of
https://git.alemi.dev/fedicharter.git
synced 2024-11-25 01:24:48 +01:00
feat: added leptos web frontend poc
This commit is contained in:
parent
ac20d43b20
commit
02bfc8afc7
3 changed files with 234 additions and 0 deletions
12
Cargo.toml
12
Cargo.toml
|
@ -16,6 +16,11 @@ name = "fedicharter-cli"
|
|||
path = "src/cli/main.rs"
|
||||
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
|
||||
|
||||
[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 }
|
||||
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]
|
||||
default = ["web", "cli"]
|
||||
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"]
|
||||
|
|
68
index.html
Normal file
68
index.html
Normal 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
154
src/web/main.rs
Normal 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>
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue