Skip to content

Commit

Permalink
frontend: Show feature flags
Browse files Browse the repository at this point in the history
So far features were stored only in database.
Show their names in topbar menu with link
to the new features page. Features page will show
all relevant features with their subfeatures.
  • Loading branch information
almusil committed Nov 9, 2020
1 parent 9ca2681 commit 008a04a
Show file tree
Hide file tree
Showing 10 changed files with 250 additions and 4 deletions.
14 changes: 13 additions & 1 deletion src/db/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use postgres_types::{FromSql, ToSql};
use serde::Serialize;

#[derive(Debug, Clone, PartialEq, Serialize, FromSql, ToSql)]
#[derive(Debug, Clone, PartialEq, Eq, Serialize, FromSql, ToSql)]
#[postgres(name = "feature")]
pub struct Feature {
name: String,
Expand All @@ -12,4 +12,16 @@ impl Feature {
pub fn new(name: String, subfeatures: Vec<String>) -> Self {
Feature { name, subfeatures }
}

pub fn is_private(&self) -> bool {
self.name.starts_with('_')
}

pub fn is_default(&self) -> bool {
self.name == "default"
}

pub fn subfeature_len(&self) -> usize {
self.subfeatures.len()
}
}
5 changes: 5 additions & 0 deletions src/test/fakes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,11 @@ impl<'a> FakeRelease<'a> {
self
}

pub(crate) fn features(mut self, features: HashMap<String, Vec<String>>) -> Self {
self.package.features = features;
self
}

/// Returns the release_id
pub(crate) fn create(self) -> Result<i32, Error> {
use std::fs;
Expand Down
41 changes: 41 additions & 0 deletions src/web/crate_details.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ impl CrateDetails {
releases.license,
releases.documentation_url,
releases.default_target,
releases.features,
doc_coverage.total_items,
doc_coverage.documented_items,
doc_coverage.total_items_needing_examples,
Expand Down Expand Up @@ -148,6 +149,7 @@ impl CrateDetails {
default_target: krate.get("default_target"),
doc_targets: MetaData::parse_doc_targets(krate.get("doc_targets")),
yanked: krate.get("yanked"),
features: MetaData::parse_features(krate.get("features")),
};

let documented_items: Option<i32> = krate.get("documented_items");
Expand Down Expand Up @@ -325,6 +327,7 @@ mod tests {
use crate::test::{wrapper, TestDatabase};
use failure::Error;
use kuchiki::traits::TendrilSink;
use std::collections::HashMap;

fn assert_last_successful_build_equals(
db: &TestDatabase,
Expand Down Expand Up @@ -741,4 +744,42 @@ mod tests {
Ok(())
});
}

#[test]
fn feature_flags_is_hidden_when_empty() {
wrapper(|env| {
env.fake_release()
.name("binary")
.version("0.1.0")
.binary(true)
.features(HashMap::new())
.create()?;

let page = kuchiki::parse_html()
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
Ok(())
});
}

#[test]
fn feature_private_feature_flags_are_hidden() {
wrapper(|env| {
let features = [("_private".into(), Vec::new())]
.iter()
.cloned()
.collect::<HashMap<String, Vec<String>>>();
env.fake_release()
.name("binary")
.version("0.1.0")
.binary(true)
.features(features)
.create()?;

let page = kuchiki::parse_html()
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
Ok(())
});
}
}
63 changes: 63 additions & 0 deletions src/web/features.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::db::types::Feature;
use crate::{
db::Pool,
impl_webpage,
web::{page::WebPage, MetaData},
};
use iron::{IronResult, Request, Response};
use router::Router;
use serde::Serialize;

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
struct FeaturesPage {
metadata: MetaData,
features: Option<Vec<Feature>>,
feature_len: usize,
default_feature_len: usize,
}

impl_webpage! {
FeaturesPage = "crate/features.html",
}

pub fn build_features_handler(req: &mut Request) -> IronResult<Response> {
let router = extension!(req, Router);
let name = cexpect!(req, router.find("name"));
let version = cexpect!(req, router.find("version"));

let mut conn = extension!(req, Pool).get()?;

let query = ctry!(
req,
conn.query(
"SELECT crates.name,
releases.features
FROM builds
INNER JOIN releases ON releases.id = builds.rid
INNER JOIN crates ON releases.crate_id = crates.id
WHERE crates.name = $1 AND releases.version = $2
ORDER BY releases.id DESC",
&[&name, &version]
)
);
let row = cexpect!(req, query.get(0));
let features = MetaData::parse_features(row.get("features"));
let mut feature_len = 0;
let mut default_feature_len = 0;
if let Some(ref feature_list) = features {
feature_len = feature_list.len();
if let Some(first) = feature_list.first() {
if first.is_default() {
default_feature_len = first.subfeature_len();
}
}
}

FeaturesPage {
metadata: cexpect!(req, MetaData::from_crate(&mut conn, &name, &version)),
features,
feature_len,
default_feature_len,
}
.into_response(req)
}
19 changes: 18 additions & 1 deletion src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ mod builds;
mod crate_details;
mod error;
mod extensions;
mod features;
mod file;
pub(crate) mod metrics;
mod releases;
Expand All @@ -90,6 +91,7 @@ mod sitemap;
mod source;
mod statics;

use crate::db::types::Feature;
use crate::{impl_webpage, Context};
use chrono::{DateTime, Utc};
use error::Nope;
Expand Down Expand Up @@ -519,6 +521,7 @@ pub(crate) struct MetaData {
pub(crate) default_target: String,
pub(crate) doc_targets: Vec<String>,
pub(crate) yanked: bool,
pub(crate) features: Option<Vec<Feature>>,
}

impl MetaData {
Expand All @@ -532,7 +535,8 @@ impl MetaData {
releases.rustdoc_status,
releases.default_target,
releases.doc_targets,
releases.yanked
releases.yanked,
releases.features
FROM releases
INNER JOIN crates ON crates.id = releases.crate_id
WHERE crates.name = $1 AND releases.version = $2",
Expand All @@ -551,6 +555,7 @@ impl MetaData {
default_target: row.get(5),
doc_targets: MetaData::parse_doc_targets(row.get(6)),
yanked: row.get(7),
features: MetaData::parse_features(row.get(8)),
})
}

Expand All @@ -565,6 +570,14 @@ impl MetaData {
})
.unwrap_or_else(Vec::new)
}

pub(crate) fn parse_features(features: Option<Vec<Feature>>) -> Option<Vec<Feature>> {
features.map(|vec| {
vec.into_iter()
.filter(|feature| !feature.is_private())
.collect()
})
}
}

#[derive(Debug, Clone, PartialEq, Serialize)]
Expand Down Expand Up @@ -843,6 +856,7 @@ mod test {
"arm64-unknown-linux-gnu".to_string(),
],
yanked: false,
features: None,
};

let correct_json = json!({
Expand All @@ -857,6 +871,7 @@ mod test {
"arm64-unknown-linux-gnu",
],
"yanked": false,
"features": null
});

assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
Expand All @@ -874,6 +889,7 @@ mod test {
"arm64-unknown-linux-gnu",
],
"yanked": false,
"features": null,
});

assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
Expand All @@ -891,6 +907,7 @@ mod test {
"arm64-unknown-linux-gnu",
],
"yanked": false,
"features": null,
});

assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
Expand Down
4 changes: 4 additions & 0 deletions src/web/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ pub(super) fn build_routes() -> Routes {
"/crate/:name/:version/builds/:id",
super::builds::build_list_handler,
);
routes.internal_page(
"/crate/:name/:version/features",
super::features::build_features_handler,
);
routes.internal_page(
"/crate/:name/:version/source",
SimpleRedirect::new(|url| url.set_path(&format!("{}/", url.path()))),
Expand Down
4 changes: 3 additions & 1 deletion src/web/source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@ impl FileList {
releases.files,
releases.default_target,
releases.doc_targets,
releases.yanked
releases.yanked,
releases.features
FROM releases
LEFT OUTER JOIN crates ON crates.id = releases.crate_id
WHERE crates.name = $1 AND releases.version = $2",
Expand Down Expand Up @@ -137,6 +138,7 @@ impl FileList {
default_target: rows[0].get(6),
doc_targets: MetaData::parse_doc_targets(rows[0].get(7)),
yanked: rows[0].get(8),
features: MetaData::parse_features(rows[0].get(9)),
},
files: file_list,
})
Expand Down
70 changes: 70 additions & 0 deletions templates/crate/features.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
{%- extends "base.html" -%}
{%- import "header/package_navigation.html" as navigation -%}

{%- block title -%}
{{ macros::doc_title(name=metadata.name, version=metadata.version) }}
{%- endblock title -%}

{%- block topbar -%}
{%- set latest_version = "" -%}
{%- set latest_path = "" -%}
{%- set target = "" -%}
{%- set inner_path = metadata.target_name ~ "/index.html" -%}
{%- set is_latest_version = true -%}
{%- set is_prerelease = false -%}
{%- include "rustdoc/topbar.html" -%}
{%- endblock topbar -%}

{%- block header -%}
{{ navigation::package_navigation(metadata=metadata, active_tab="features") }}
{%- endblock header -%}

{%- block body -%}
<div class="container package-page-container">
<div class="pure-g">
<div class="pure-u-1 pure-u-sm-7-24 pure-u-md-5-24">
<div class="pure-menu package-menu">
<ul class="pure-menu-list">
<li class="pure-menu-heading">Feature flags</li>
{%- if features -%}
{%- for feature in features -%}
<li class="pure-menu-item">
<a href="#{{ feature.name }}" class="pure-menu-link" style="text-align:center;">
{{ feature.name }}
</a>
</li>
{%- endfor -%}
{%- else -%}
<li class="pure-menu-item">
<span style="font-size: 13px;">Feature flags are not available for this version.</span>
</li>
{%- endif -%}
</ul>
</div>
</div>

<div class="pure-u-1 pure-u-sm-17-24 pure-u-md-19-24 package-details" id="main">
<h1>{{ metadata.name }}</h1>
{%- if features -%}
<p>This version has <b>{{ feature_len }}</b> feature flags, <b>{{ default_feature_len }}</b> of them being enabled by <b>default</b>.</p>
{%- for feature in features -%}
<h3 id="{{ feature.name }}">{{ feature.name }}</h3>
<ul class="pure-menu-list">
{%- if feature.subfeatures -%}
{%- for subfeature in feature.subfeatures -%}
<li class="pure-menu-item">
<span>{{ subfeature }}</span>
</li>
{%- endfor -%}
{%- else -%}
<p>This feature flag does not enable additional features.</p>
{%- endif -%}
</ul>
{%- endfor -%}
{%- else -%}
<p>Feature flags are not available for this version.</p>
{%- endif -%}
</div>
</div>
</div>
{%- endblock body -%}
12 changes: 12 additions & 0 deletions templates/header/package_navigation.html
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* `crate`
* `source`
* `builds`
* `features`

Note: `false` here is acting as a pseudo-null value since you can't directly construct null values
and tera requires all parameters without defaults to be filled
Expand Down Expand Up @@ -85,6 +86,17 @@ <h1 id="crate-title">
<span class="title"> Builds</span>
</a>
</li>

{# The features tab #}
{%- if metadata.features -%}
<li class="pure-menu-item">
<a href="/crate/{{ crate_path | safe }}/features"
class="pure-menu-link{% if active_tab == 'features' %} pure-menu-active{% endif %}">
{{ "flag" | fas }}
<span class="title">Feature flags</span>
</a>
</li>
{%- endif -%}
</ul>
</div>
</div>
Expand Down
Loading

0 comments on commit 008a04a

Please sign in to comment.