//! Defines the HTTP-based UI. These endpoints generally return HTML and SVG. use std::{path::Path, sync::Arc, time::Duration}; use futures::{future, Future}; use hyper::{header, service::Service, Body, Method, Request, Response, StatusCode}; use rbx_dom_weak::{RbxId, RbxValue}; use ritz::{html, Fragment, HtmlContent}; use crate::{ imfs::{Imfs, ImfsDebug, ImfsFetcher}, serve_session::ServeSession, snapshot::RojoTree, web::{ assets, interface::{ErrorResponse, SERVER_VERSION}, util::json, }, }; pub struct UiService { serve_session: Arc>, } impl Service for UiService { type ReqBody = Body; type ResBody = Body; type Error = hyper::Error; type Future = Box, Error = Self::Error> + Send>; fn call(&mut self, request: Request) -> Self::Future { let response = match (request.method(), request.uri().path()) { (&Method::GET, "/") => self.handle_home(), (&Method::GET, "/logo.png") => self.handle_logo(), (&Method::GET, "/icon.png") => self.handle_icon(), (&Method::GET, "/show-instances") => self.handle_show_instances(), (&Method::GET, "/show-imfs") => self.handle_show_imfs(), (_method, path) => { return json( ErrorResponse::not_found(format!("Route not found: {}", path)), StatusCode::NOT_FOUND, ) } }; Box::new(future::ok(response)) } } 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 in-memory filesystem state", "/show-imfs") } { 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 handle_show_imfs(&self) -> Response { let imfs = self.serve_session.imfs(); let orphans: Vec<_> = imfs .debug_orphans() .into_iter() .map(|path| Self::render_imfs_path(&imfs, path, true)) .collect(); let watched_list: Vec<_> = imfs .debug_watched_paths() .into_iter() .map(|path| { html! {
  • { format!("{}", path.display()) }
  • } }) .collect(); let page = self.normal_page(html! { <>

    "Known FS Items"

    { Fragment::new(orphans) }

    "Watched Paths"

      { Fragment::new(watched_list) }
    }); Response::builder() .header(header::CONTENT_TYPE, "text/html") .body(Body::from(format!("{}", page))) .unwrap() } fn render_imfs_path(imfs: &Imfs, path: &Path, is_root: bool) -> HtmlContent<'static> { let is_file = imfs.debug_is_file(path); let (note, children) = if is_file { (HtmlContent::None, Vec::new()) } else { let (is_exhaustive, mut children) = imfs.debug_children(path).unwrap(); // Sort files above directories, then sort how Path does after that. children.sort_unstable_by(|a, b| { let a_is_file = imfs.debug_is_file(a); let b_is_file = imfs.debug_is_file(b); b_is_file.cmp(&a_is_file).then_with(|| a.cmp(b)) }); let children: Vec<_> = children .into_iter() .map(|child| Self::render_imfs_path(imfs, child, false)) .collect(); let note = if is_exhaustive { HtmlContent::None } else { html!({ " (not enumerated)" }) }; (note, children) }; // For root entries, we want the full path to contextualize the path. let mut name = if is_root { path.to_str().unwrap().to_owned() } else { path.file_name().unwrap().to_str().unwrap().to_owned() }; // Directories should end with `/` in the UI to mark them. if !is_file && !name.ends_with('/') && !name.ends_with('\\') { name.push('/'); } html! {
    { name } { note }
    { Fragment::new(children) }
    } } fn instance(tree: &RojoTree, id: RbxId) -> 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 { html! {
    { Fragment::new(children_list) }
    } }; 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.get_type()) }
    } }) .collect(); let property_container = if property_list.is_empty() { HtmlContent::None } else { html! {
    { Fragment::new(property_list) }
    } }; let class_name_specifier = if instance.name() == instance.class_name() { HtmlContent::None } else { html! { " (" { instance.class_name().to_owned() } ")" } }; html! {
    { instance.name().to_owned() } { class_name_specifier }
    { property_container } { children_container }
    } } fn display_value(value: &RbxValue) -> String { match value { RbxValue::String { value } => value.clone(), RbxValue::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().unwrap_or(""); 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(elapsed.subsec_nanos() as u64); 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 } } } }