//! Defines the HTTP-based Rojo UI. It uses ritz for templating, which is like //! JSX for Rust. Eventually we should probably replace this with a new //! framework, maybe using JS and client side rendering. //! //! These endpoints generally return HTML and SVG. use std::{borrow::Cow, sync::Arc, time::Duration}; use hyper::{header, Body, Method, Request, Response, StatusCode}; use maplit::hashmap; use rbx_dom_weak::types::{Ref, Variant}; use ritz::{html, Fragment, HtmlContent, HtmlSelfClosingTag}; use crate::{ serve_session::ServeSession, snapshot::RojoTree, web::{ assets, interface::{ErrorResponse, SERVER_VERSION}, util::json, }, }; pub async fn call(serve_session: Arc, request: Request) -> Response { let service = UiService::new(serve_session); match (request.method(), request.uri().path()) { (&Method::GET, "/") => service.handle_home(), (&Method::GET, "/logo.png") => service.handle_logo(), (&Method::GET, "/icon.png") => service.handle_icon(), (&Method::GET, "/show-instances") => service.handle_show_instances(), (_method, path) => json( ErrorResponse::not_found(format!("Route not found: {}", path)), StatusCode::NOT_FOUND, ), } } pub struct UiService { serve_session: Arc, } impl UiService { pub fn new(serve_session: Arc) -> Self { UiService { serve_session } } fn handle_logo(&self) -> Response { Response::builder() .header(header::CONTENT_TYPE, "image/png") .body(Body::from(assets::logo())) .unwrap() } fn handle_icon(&self) -> Response { Response::builder() .header(header::CONTENT_TYPE, "image/png") .body(Body::from(assets::icon())) .unwrap() } fn handle_home(&self) -> Response { let page = self.normal_page(html! {
{ Self::button("Rojo Documentation", "https://rojo.space/docs") } { Self::button("View instance tree state", "/show-instances") }
}); Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(format!("{}", page))) .unwrap() } fn handle_show_instances(&self) -> Response { let tree = self.serve_session.tree(); let root_id = tree.get_root_id(); let page = self.normal_page(html! { { Self::instance(&tree, root_id) } }); Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(format!("{}", page))) .unwrap() } fn instance(tree: &RojoTree, id: Ref) -> HtmlContent<'_> { let instance = tree.get_instance(id).unwrap(); let children_list: Vec<_> = instance .children() .iter() .copied() .map(|id| Self::instance(tree, id)) .collect(); let children_container = if children_list.is_empty() { HtmlContent::None } else { let section = ExpandableSection { title: "Children", class_name: "instance-children", id, expanded: true, content: html! { { Fragment::new(children_list) } }, }; section.render() }; let mut properties: Vec<_> = instance.properties().iter().collect(); properties.sort_by_key(|pair| pair.0); let property_list: Vec<_> = properties .into_iter() .map(|(key, value)| { html! {
{ key.clone() } ": " { format!("{:?}", value.ty()) }
} }) .collect(); let property_container = if property_list.is_empty() { HtmlContent::None } else { let section = ExpandableSection { title: "Properties", class_name: "instance-properties", id, expanded: false, content: html! { { Fragment::new(property_list) } }, }; section.render() }; let metadata_container = { let metadata = instance.metadata(); let relevant_paths = if metadata.relevant_paths.is_empty() { HtmlContent::None } else { let list = metadata .relevant_paths .iter() .map(|path| html! {
  • { format!("{}", path.display()) }
  • }); html! {
    "relevant_paths: "
      { Fragment::new(list) }
    } }; let content = html! { <>
    "ignore_unknown_instances: " { metadata.ignore_unknown_instances.to_string() }
    "instigating source: " { format!("{:?}", metadata.instigating_source) }
    { relevant_paths } }; let section = ExpandableSection { title: "Metadata", class_name: "instance-metadata", id, expanded: false, content, }; section.render() }; let class_name_specifier = if instance.name() == instance.class_name() { HtmlContent::None } else { html! { " (" { instance.class_name().to_owned() } ")" } }; html! {
    { metadata_container } { property_container } { children_container }
    } } fn display_value(value: &Variant) -> String { match value { Variant::String(value) => value.clone(), Variant::Bool(value) => value.to_string(), _ => format!("{:?}", value), } } fn stat_item>(name: &str, value: S) -> HtmlContent<'_> { html! { { name } ": " { value.into() } } } fn button<'a>(text: &'a str, href: &'a str) -> HtmlContent<'a> { html! { { text } } } fn normal_page<'a>(&'a self, body: HtmlContent<'a>) -> HtmlContent<'a> { let project_name = self.serve_session.project_name(); let uptime = { let elapsed = self.serve_session.start_time().elapsed(); // Round off all of our sub-second precision to make timestamps // nicer. let just_nanos = Duration::from_nanos(u64::from(elapsed.subsec_nanos())); let elapsed = elapsed - just_nanos; humantime::format_duration(elapsed).to_string() }; Self::page(html! {
    { Self::stat_item("Server Version", SERVER_VERSION) } { Self::stat_item("Project", project_name) } { Self::stat_item("Server Uptime", uptime) }
    { body }
    }) } fn page(body: HtmlContent<'_>) -> HtmlContent<'_> { html! { "Rojo Live Server" { body } } } } struct ExpandableSection<'a> { title: &'a str, class_name: &'a str, id: Ref, expanded: bool, content: HtmlContent<'a>, } impl<'a> ExpandableSection<'a> { fn render(self) -> HtmlContent<'a> { let input_id = format!("{}-{:?}", self.class_name, self.id); // We need to specify this input manually because Ritz doesn't have // support for conditional attributes like `checked`. let mut input = HtmlSelfClosingTag { name: Cow::Borrowed("input"), attributes: hashmap! { Cow::Borrowed("class") => Cow::Borrowed("expandable-input"), Cow::Borrowed("id") => Cow::Owned(input_id.clone()), Cow::Borrowed("type") => Cow::Borrowed("checkbox"), }, }; if self.expanded { input .attributes .insert(Cow::Borrowed("checked"), Cow::Borrowed("checked")); } html! {
    { input }

    { self.content }
    } } }