add first prototype of landing page

This commit is contained in:
Sam Rijs 2018-02-03 20:12:00 +11:00
parent 3d6ea4be6f
commit dff0f8d6e3
8 changed files with 173 additions and 149 deletions

View file

@ -6,13 +6,15 @@ use hyper::client::HttpConnector;
use hyper_tls::HttpsConnector;
use slog::Logger;
use ::models::repo::RepoPath;
use ::models::repo::{Repository, RepoPath};
use ::models::crates::{CrateName, CrateRelease, CrateManifest, AnalyzedDependencies};
use ::parsers::manifest::{ManifestParseError, parse_manifest_toml};
use ::interactors::crates::{QueryCrateError, query_crate};
use ::interactors::github::{RetrieveFileAtPathError, retrieve_file_at_path};
use ::interactors::github::get_popular_repos;
pub use ::interactors::github::GetPopularReposError;
use self::analyzer::DependencyAnalyzer;
@ -37,6 +39,12 @@ pub struct AnalyzeDependenciesOutcome {
}
impl Engine {
pub fn get_popular_repos(&self) ->
impl Future<Item=Vec<Repository>, Error=GetPopularReposError>
{
get_popular_repos(self.client.clone())
}
pub fn analyze_dependencies(&self, repo_path: RepoPath) ->
impl Future<Item=AnalyzeDependenciesOutcome, Error=AnalyzeDependenciesError>
{

View file

@ -3,10 +3,13 @@ use std::string::FromUtf8Error;
use futures::{Future, IntoFuture, Stream, future};
use hyper::{Error as HyperError, Method, Request, Response, StatusCode};
use hyper::error::UriError;
use hyper::header::UserAgent;
use tokio_service::Service;
use serde_json;
use ::models::repo::RepoPath;
use ::models::repo::{Repository, RepoPath, RepoValidationError};
const GITHUB_API_BASE_URI: &'static str = "https://api.github.com";
const GITHUB_USER_CONTENT_BASE_URI: &'static str = "https://raw.githubusercontent.com";
#[derive(Debug)]
@ -44,3 +47,60 @@ pub fn retrieve_file_at_path<S>(service: S, repo_path: &RepoPath, file_path: &st
})
})
}
#[derive(Debug)]
pub enum GetPopularReposError {
Uri(UriError),
Transport(HyperError),
Status(StatusCode),
Decode(serde_json::Error),
Validate(RepoValidationError)
}
#[derive(Deserialize)]
struct GithubSearchResponse {
items: Vec<GithubRepo>
}
#[derive(Deserialize)]
struct GithubRepo {
name: String,
owner: GithubOwner,
description: String
}
#[derive(Deserialize)]
struct GithubOwner {
login: String
}
pub fn get_popular_repos<S>(service: S) ->
impl Future<Item=Vec<Repository>, Error=GetPopularReposError>
where S: Service<Request=Request, Response=Response, Error=HyperError>
{
let uri_future = format!("{}/search/repositories?q=language:rust&sort=stars", GITHUB_API_BASE_URI)
.parse().into_future().map_err(GetPopularReposError::Uri);
uri_future.and_then(move |uri| {
let mut request = Request::new(Method::Get, uri);
request.headers_mut().set(UserAgent::new("deps.rs"));
service.call(request).map_err(GetPopularReposError::Transport).and_then(|response| {
let status = response.status();
if !status.is_success() {
future::Either::A(future::err(GetPopularReposError::Status(status)))
} else {
let body_future = response.body().concat2().map_err(GetPopularReposError::Transport);
let decode_future = body_future
.and_then(|body| serde_json::from_slice(body.as_ref()).map_err(GetPopularReposError::Decode));
future::Either::B(decode_future.and_then(|search_response: GithubSearchResponse| {
search_response.items.into_iter().map(|item| {
let path = RepoPath::from_parts("github", &item.owner.login, &item.name)
.map_err(GetPopularReposError::Validate)?;
Ok(Repository { path, description: item.description })
}).collect::<Result<Vec<_>, _>>()
}))
}
})
})
}

View file

@ -1,5 +1,11 @@
use std::str::FromStr;
#[derive(Clone)]
pub struct Repository {
pub path: RepoPath,
pub description: String
}
#[derive(Clone)]
pub struct RepoPath {
pub site: RepoSite,

View file

@ -26,6 +26,7 @@ enum StaticFile {
}
enum Route {
Index,
Static(StaticFile),
Status(StatusFormat)
}
@ -40,6 +41,8 @@ impl Server {
pub fn new(engine: Engine) -> Server {
let mut router = Router::new();
router.add("/", Route::Index);
router.add("/static/style.css", Route::Static(StaticFile::StyleCss));
router.add("/repo/:site/:qual/:name", Route::Status(StatusFormat::Html));
@ -59,6 +62,11 @@ impl Service for Server {
fn call(&self, req: Request) -> Self::Future {
if let Ok(route_match) = self.router.recognize(req.uri().path()) {
match route_match.handler {
&Route::Index => {
if *req.method() == Method::Get {
return Box::new(self.index(req, route_match.params));
}
},
&Route::Status(format) => {
if *req.method() == Method::Get {
return Box::new(self.status(req, route_match.params, format));
@ -80,6 +88,23 @@ impl Service for Server {
}
impl Server {
fn index(&self, _req: Request, _params: Params) ->
impl Future<Item=Response, Error=HyperError>
{
self.engine.get_popular_repos().then(|popular_result| {
match popular_result {
Err(err) => {
let mut response = Response::new();
response.set_status(StatusCode::BadRequest);
response.set_body(format!("{:?}", err));
future::ok(response)
},
Ok(popular) =>
future::ok(views::html::index::render(popular))
}
})
}
fn status(&self, _req: Request, params: Params, format: StatusFormat) ->
impl Future<Item=Response, Error=HyperError>
{

View file

@ -0,0 +1,54 @@
use hyper::Response;
use maud::{Markup, html};
use ::models::repo::Repository;
fn popular_table(popular: Vec<Repository>) -> Markup {
html! {
h2 class="title is-3" "Popular"
table class="table is-fullwidth is-striped is-hoverable" {
thead {
tr {
th "Repository"
th class="has-text-right" "Status"
}
}
tbody {
@for repo in popular {
tr {
td {
a href=(format!("{}/repo/{}/{}/{}", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref())) {
(format!("{} / {}", repo.path.qual.as_ref(), repo.path.name.as_ref()))
}
}
td class="has-text-right" {
img src=(format!("{}/repo/{}/{}/{}/status.svg", &super::SELF_BASE_URL as &str, repo.path.site.as_ref(), repo.path.qual.as_ref(), repo.path.name.as_ref()));
}
}
}
}
}
}
}
pub fn render(popular: Vec<Repository>) -> Response {
super::render_html("Keep your dependencies up-to-date - Deps.rs", html! {
section class="hero is-light" {
div class="hero-head" (super::render_navbar())
div class="hero-body" {
div class="container" {
p class="title is-1" "Keep your dependencies up-to-date"
p {
"Docs.rs uses semantic versioning to detect outdated or insecure dependencies in your project's"
code "Cargo.toml"
"."
}
}
}
}
section class="section" {
div class="container" (popular_table(popular))
}
})
}

View file

@ -2,8 +2,9 @@ use std::env;
use hyper::Response;
use hyper::header::ContentType;
use maud::{Render, html};
use maud::{Markup, Render, html};
pub mod index;
pub mod status;
lazy_static! {
@ -35,3 +36,17 @@ fn render_html<B: Render>(title: &str, body: B) -> Response {
.with_header(ContentType::html())
.with_body(rendered.0)
}
fn render_navbar() -> Markup {
html! {
header class="navbar" {
div class="container" {
div class="navbar-brand" {
a class="navbar-item is-dark" href=(SELF_BASE_URL) {
h1 class="title is-3" "Deps.rs"
}
}
}
}
}
}

View file

@ -63,7 +63,7 @@ fn dependency_table(title: &str, deps: BTreeMap<CrateName, AnalyzedDependency>)
pub fn render(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Response {
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref());
let status_base_url = format!("{}/{}", &super::SELF_BASE_URL as &str, self_path);
let title = format!("{} / {} - Dependency Status", repo_path.qual.as_ref(), repo_path.name.as_ref());
let title = format!("{} / {} - Deps.rs", repo_path.qual.as_ref(), repo_path.name.as_ref());
let (hero_class, status_asset) = if analysis_outcome.deps.any_outdated() {
("is-warning", assets::BADGE_OUTDATED_SVG.as_ref())
@ -75,6 +75,7 @@ pub fn render(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath)
super::render_html(&title, html! {
section class=(format!("hero {}", hero_class)) {
div class="hero-head" (super::render_navbar())
div class="hero-body" {
div class="container" {
h1 class="title is-1" {

View file

@ -1,145 +0,0 @@
use std::collections::BTreeMap;
use std::env;
use base64::display::Base64Display;
use hyper::Response;
use hyper::header::ContentType;
use maud::{Markup, html};
use ::engine::AnalyzeDependenciesOutcome;
use ::models::crates::{CrateName, AnalyzedDependency};
use ::models::repo::RepoPath;
use ::server::assets;
lazy_static! {
static ref SELF_BASE_URL: String = {
env::var("BASE_URL")
.unwrap_or_else(|_| "http://localhost:8080".to_string())
};
}
fn dependency_table(title: &str, deps: BTreeMap<CrateName, AnalyzedDependency>) -> Markup {
let count_total = deps.len();
let count_outdated = deps.iter().filter(|&(_, dep)| dep.is_outdated()).count();
html! {
h3 class="title is-4" (title)
p class="subtitle is-5" {
@if count_outdated > 0 {
(format!(" ({} total, {} up-to-date, {} outdated)", count_total, count_total - count_outdated, count_outdated))
} @else {
(format!(" ({} total, all up-to-date)", count_total))
}
}
table class="table is-fullwidth is-striped is-hoverable" {
thead {
tr {
th "Crate"
th class="has-text-right" "Required"
th class="has-text-right" "Latest"
th class="has-text-right" "Status"
}
}
tbody {
@for (name, dep) in deps {
tr {
td {
a href=(format!("https://crates.io/crates/{}", name.as_ref())) (name.as_ref())
}
td class="has-text-right" code (dep.required.to_string())
td class="has-text-right" {
@if let Some(ref latest) = dep.latest {
code (latest.to_string())
} @else {
"N/A"
}
}
td class="has-text-right" {
@if dep.is_outdated() {
span class="tag is-warning" "out of date"
} @else {
span class="tag is-success" "up to date"
}
}
}
}
}
}
}
}
pub fn status_html(analysis_outcome: AnalyzeDependenciesOutcome, repo_path: RepoPath) -> Response {
let self_path = format!("repo/{}/{}/{}", repo_path.site.as_ref(), repo_path.qual.as_ref(), repo_path.name.as_ref());
let status_base_url = format!("{}/{}", &SELF_BASE_URL as &str, self_path);
let title = format!("{} / {} - Dependency Status", repo_path.qual.as_ref(), repo_path.name.as_ref());
let (hero_class, status_asset) = if analysis_outcome.deps.any_outdated() {
("is-warning", assets::BADGE_OUTDATED_SVG.as_ref())
} else {
("is-success", assets::BADGE_UPTODATE_SVG.as_ref())
};
let status_data_url = format!("data:image/svg+xml;base64,{}", Base64Display::standard(status_asset));
let rendered = html! {
html {
head {
meta charset="utf-8";
meta name="viewport" content="width=device-width, initial-scale=1";
title (title)
link rel="stylesheet" type="text/css" href="/static/style.css";
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Fira+Sans:400,500,600";
link rel="stylesheet" type="text/css" href="https://fonts.googleapis.com/css?family=Source+Code+Pro";
link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/font-awesome/4.7.0/css/font-awesome.min.css";
}
body {
section class=(format!("hero {}", hero_class)) {
div class="hero-body" {
div class="container" {
h1 class="title is-1" {
a href=(format!("{}/{}/{}", repo_path.site.to_base_uri(), repo_path.qual.as_ref(), repo_path.name.as_ref())) {
i class="fa fa-github" ""
(format!(" {} / {}", repo_path.qual.as_ref(), repo_path.name.as_ref()))
}
}
img src=(status_data_url);
}
}
div class="hero-footer" {
div class="container" {
pre class="is-size-7" {
(format!("[![dependency status]({}/status.svg)]({})", status_base_url, status_base_url))
}
}
}
}
section class="section" {
div class="container" {
h2 class="title is-3" {
"Crate "
code (analysis_outcome.name.as_ref())
}
@if !analysis_outcome.deps.main.is_empty() {
(dependency_table("Dependencies", analysis_outcome.deps.main))
}
@if !analysis_outcome.deps.dev.is_empty() {
(dependency_table("Dev dependencies", analysis_outcome.deps.dev))
}
@if !analysis_outcome.deps.build.is_empty() {
(dependency_table("Build dependencies", analysis_outcome.deps.build))
}
}
}
}
}
};
Response::new()
.with_header(ContentType::html())
.with_body(rendered.0)
}