Aspirational examples for app developer contract.
This commit is contained in:
105
aspirational_examples/01_async_data_and_effects.rs
Normal file
105
aspirational_examples/01_async_data_and_effects.rs
Normal file
@@ -0,0 +1,105 @@
|
||||
//! Aspirational RUIN app API for async resources and effects.
|
||||
//!
|
||||
//! Intentionally non-compiling; this is a design sketch.
|
||||
|
||||
use ruin::prelude::*;
|
||||
|
||||
#[ruin_runtime::async_main]
|
||||
async fn main() -> ruin::Result<()> {
|
||||
App::new()
|
||||
.window(Window::new().title("RUIN Issue Dashboard").size(1240.0, 820.0))
|
||||
.mount::<IssueDashboard>()
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn IssueDashboard() -> impl View {
|
||||
let repo = use_signal(|| "wtemple/ruin".to_string());
|
||||
let selected_issue = use_signal(|| None::<u64>);
|
||||
|
||||
let issues = use_resource(move || {
|
||||
let repo = repo.get();
|
||||
async move { github::list_issues(&repo).await }
|
||||
});
|
||||
|
||||
let details = use_resource(move || {
|
||||
let repo = repo.get();
|
||||
let issue_id = selected_issue.get();
|
||||
async move {
|
||||
let issue_id = issue_id?;
|
||||
github::issue_details(&repo, issue_id).await.ok()
|
||||
}
|
||||
});
|
||||
|
||||
use_effect(move || {
|
||||
tracing::info!(
|
||||
repo = %repo.get(),
|
||||
selected_issue = ?selected_issue.get(),
|
||||
"dashboard dependencies changed"
|
||||
);
|
||||
});
|
||||
|
||||
view! {
|
||||
row(fill = true, gap = 20, padding = 20) {
|
||||
block(width = 360, gap = 12) {
|
||||
text(role = TextRole::Heading(1), size = 28, weight = FontWeight::Semibold) {
|
||||
"Issues"
|
||||
}
|
||||
|
||||
text_input(value = repo, placeholder = "owner/repo") {}
|
||||
|
||||
suspense(fallback = || view! { ProgressSpinner(label = "Loading issues...") {} }) {
|
||||
match issues.read() {
|
||||
Ok(items) => view! {
|
||||
list(gap = 8) {
|
||||
for issue in items keyed by issue.id {
|
||||
button(
|
||||
kind = if selected_issue.get() == Some(issue.id) {
|
||||
ButtonKind::Primary
|
||||
} else {
|
||||
ButtonKind::Secondary
|
||||
},
|
||||
on_press = move |_| selected_issue.set(Some(issue.id)),
|
||||
) {
|
||||
issue.title
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
Err(error) => view! {
|
||||
ErrorPanel(
|
||||
title = "Failed to load issues",
|
||||
detail = error.to_string(),
|
||||
) {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
block(fill = true, gap = 16) {
|
||||
text(role = TextRole::Heading(2), size = 24, weight = FontWeight::Semibold) {
|
||||
"Details"
|
||||
}
|
||||
|
||||
match details.read() {
|
||||
Pending => view! { ProgressSpinner(label = "Loading issue details...") {} },
|
||||
Ready(Some(issue)) => view! {
|
||||
column(gap = 12) {
|
||||
text(role = TextRole::Heading(3), size = 22, weight = FontWeight::Semibold) {
|
||||
issue.title
|
||||
}
|
||||
Markdown(source = issue.body) {}
|
||||
}
|
||||
},
|
||||
Ready(None) => view! {
|
||||
EmptyState(title = "Pick an issue") {
|
||||
"The detail panel should track the current selection and re-run only \
|
||||
the async work that actually depends on it."
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user