Skip to content

Commit 008a04a

Browse files
committed
frontend: Show feature flags
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.
1 parent 9ca2681 commit 008a04a

File tree

10 files changed

+250
-4
lines changed

10 files changed

+250
-4
lines changed

src/db/types.rs

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use postgres_types::{FromSql, ToSql};
22
use serde::Serialize;
33

4-
#[derive(Debug, Clone, PartialEq, Serialize, FromSql, ToSql)]
4+
#[derive(Debug, Clone, PartialEq, Eq, Serialize, FromSql, ToSql)]
55
#[postgres(name = "feature")]
66
pub struct Feature {
77
name: String,
@@ -12,4 +12,16 @@ impl Feature {
1212
pub fn new(name: String, subfeatures: Vec<String>) -> Self {
1313
Feature { name, subfeatures }
1414
}
15+
16+
pub fn is_private(&self) -> bool {
17+
self.name.starts_with('_')
18+
}
19+
20+
pub fn is_default(&self) -> bool {
21+
self.name == "default"
22+
}
23+
24+
pub fn subfeature_len(&self) -> usize {
25+
self.subfeatures.len()
26+
}
1527
}

src/test/fakes.rs

+5
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,11 @@ impl<'a> FakeRelease<'a> {
210210
self
211211
}
212212

213+
pub(crate) fn features(mut self, features: HashMap<String, Vec<String>>) -> Self {
214+
self.package.features = features;
215+
self
216+
}
217+
213218
/// Returns the release_id
214219
pub(crate) fn create(self) -> Result<i32, Error> {
215220
use std::fs;

src/web/crate_details.rs

+41
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ impl CrateDetails {
103103
releases.license,
104104
releases.documentation_url,
105105
releases.default_target,
106+
releases.features,
106107
doc_coverage.total_items,
107108
doc_coverage.documented_items,
108109
doc_coverage.total_items_needing_examples,
@@ -148,6 +149,7 @@ impl CrateDetails {
148149
default_target: krate.get("default_target"),
149150
doc_targets: MetaData::parse_doc_targets(krate.get("doc_targets")),
150151
yanked: krate.get("yanked"),
152+
features: MetaData::parse_features(krate.get("features")),
151153
};
152154

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

329332
fn assert_last_successful_build_equals(
330333
db: &TestDatabase,
@@ -741,4 +744,42 @@ mod tests {
741744
Ok(())
742745
});
743746
}
747+
748+
#[test]
749+
fn feature_flags_is_hidden_when_empty() {
750+
wrapper(|env| {
751+
env.fake_release()
752+
.name("binary")
753+
.version("0.1.0")
754+
.binary(true)
755+
.features(HashMap::new())
756+
.create()?;
757+
758+
let page = kuchiki::parse_html()
759+
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
760+
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
761+
Ok(())
762+
});
763+
}
764+
765+
#[test]
766+
fn feature_private_feature_flags_are_hidden() {
767+
wrapper(|env| {
768+
let features = [("_private".into(), Vec::new())]
769+
.iter()
770+
.cloned()
771+
.collect::<HashMap<String, Vec<String>>>();
772+
env.fake_release()
773+
.name("binary")
774+
.version("0.1.0")
775+
.binary(true)
776+
.features(features)
777+
.create()?;
778+
779+
let page = kuchiki::parse_html()
780+
.one(env.frontend().get("/crate/binary/0.1.0").send()?.text()?);
781+
assert!(page.select_first(r#"a[aria-label="Feature"]"#).is_err());
782+
Ok(())
783+
});
784+
}
744785
}

src/web/features.rs

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use crate::db::types::Feature;
2+
use crate::{
3+
db::Pool,
4+
impl_webpage,
5+
web::{page::WebPage, MetaData},
6+
};
7+
use iron::{IronResult, Request, Response};
8+
use router::Router;
9+
use serde::Serialize;
10+
11+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
12+
struct FeaturesPage {
13+
metadata: MetaData,
14+
features: Option<Vec<Feature>>,
15+
feature_len: usize,
16+
default_feature_len: usize,
17+
}
18+
19+
impl_webpage! {
20+
FeaturesPage = "crate/features.html",
21+
}
22+
23+
pub fn build_features_handler(req: &mut Request) -> IronResult<Response> {
24+
let router = extension!(req, Router);
25+
let name = cexpect!(req, router.find("name"));
26+
let version = cexpect!(req, router.find("version"));
27+
28+
let mut conn = extension!(req, Pool).get()?;
29+
30+
let query = ctry!(
31+
req,
32+
conn.query(
33+
"SELECT crates.name,
34+
releases.features
35+
FROM builds
36+
INNER JOIN releases ON releases.id = builds.rid
37+
INNER JOIN crates ON releases.crate_id = crates.id
38+
WHERE crates.name = $1 AND releases.version = $2
39+
ORDER BY releases.id DESC",
40+
&[&name, &version]
41+
)
42+
);
43+
let row = cexpect!(req, query.get(0));
44+
let features = MetaData::parse_features(row.get("features"));
45+
let mut feature_len = 0;
46+
let mut default_feature_len = 0;
47+
if let Some(ref feature_list) = features {
48+
feature_len = feature_list.len();
49+
if let Some(first) = feature_list.first() {
50+
if first.is_default() {
51+
default_feature_len = first.subfeature_len();
52+
}
53+
}
54+
}
55+
56+
FeaturesPage {
57+
metadata: cexpect!(req, MetaData::from_crate(&mut conn, &name, &version)),
58+
features,
59+
feature_len,
60+
default_feature_len,
61+
}
62+
.into_response(req)
63+
}

src/web/mod.rs

+18-1
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ mod builds;
8181
mod crate_details;
8282
mod error;
8383
mod extensions;
84+
mod features;
8485
mod file;
8586
pub(crate) mod metrics;
8687
mod releases;
@@ -90,6 +91,7 @@ mod sitemap;
9091
mod source;
9192
mod statics;
9293

94+
use crate::db::types::Feature;
9395
use crate::{impl_webpage, Context};
9496
use chrono::{DateTime, Utc};
9597
use error::Nope;
@@ -519,6 +521,7 @@ pub(crate) struct MetaData {
519521
pub(crate) default_target: String,
520522
pub(crate) doc_targets: Vec<String>,
521523
pub(crate) yanked: bool,
524+
pub(crate) features: Option<Vec<Feature>>,
522525
}
523526

524527
impl MetaData {
@@ -532,7 +535,8 @@ impl MetaData {
532535
releases.rustdoc_status,
533536
releases.default_target,
534537
releases.doc_targets,
535-
releases.yanked
538+
releases.yanked,
539+
releases.features
536540
FROM releases
537541
INNER JOIN crates ON crates.id = releases.crate_id
538542
WHERE crates.name = $1 AND releases.version = $2",
@@ -551,6 +555,7 @@ impl MetaData {
551555
default_target: row.get(5),
552556
doc_targets: MetaData::parse_doc_targets(row.get(6)),
553557
yanked: row.get(7),
558+
features: MetaData::parse_features(row.get(8)),
554559
})
555560
}
556561

@@ -565,6 +570,14 @@ impl MetaData {
565570
})
566571
.unwrap_or_else(Vec::new)
567572
}
573+
574+
pub(crate) fn parse_features(features: Option<Vec<Feature>>) -> Option<Vec<Feature>> {
575+
features.map(|vec| {
576+
vec.into_iter()
577+
.filter(|feature| !feature.is_private())
578+
.collect()
579+
})
580+
}
568581
}
569582

570583
#[derive(Debug, Clone, PartialEq, Serialize)]
@@ -843,6 +856,7 @@ mod test {
843856
"arm64-unknown-linux-gnu".to_string(),
844857
],
845858
yanked: false,
859+
features: None,
846860
};
847861

848862
let correct_json = json!({
@@ -857,6 +871,7 @@ mod test {
857871
"arm64-unknown-linux-gnu",
858872
],
859873
"yanked": false,
874+
"features": null
860875
});
861876

862877
assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
@@ -874,6 +889,7 @@ mod test {
874889
"arm64-unknown-linux-gnu",
875890
],
876891
"yanked": false,
892+
"features": null,
877893
});
878894

879895
assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
@@ -891,6 +907,7 @@ mod test {
891907
"arm64-unknown-linux-gnu",
892908
],
893909
"yanked": false,
910+
"features": null,
894911
});
895912

896913
assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());

src/web/routes.rs

+4
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,10 @@ pub(super) fn build_routes() -> Routes {
8787
"/crate/:name/:version/builds/:id",
8888
super::builds::build_list_handler,
8989
);
90+
routes.internal_page(
91+
"/crate/:name/:version/features",
92+
super::features::build_features_handler,
93+
);
9094
routes.internal_page(
9195
"/crate/:name/:version/source",
9296
SimpleRedirect::new(|url| url.set_path(&format!("{}/", url.path()))),

src/web/source.rs

+3-1
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,8 @@ impl FileList {
5858
releases.files,
5959
releases.default_target,
6060
releases.doc_targets,
61-
releases.yanked
61+
releases.yanked,
62+
releases.features
6263
FROM releases
6364
LEFT OUTER JOIN crates ON crates.id = releases.crate_id
6465
WHERE crates.name = $1 AND releases.version = $2",
@@ -137,6 +138,7 @@ impl FileList {
137138
default_target: rows[0].get(6),
138139
doc_targets: MetaData::parse_doc_targets(rows[0].get(7)),
139140
yanked: rows[0].get(8),
141+
features: MetaData::parse_features(rows[0].get(9)),
140142
},
141143
files: file_list,
142144
})

templates/crate/features.html

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
{%- extends "base.html" -%}
2+
{%- import "header/package_navigation.html" as navigation -%}
3+
4+
{%- block title -%}
5+
{{ macros::doc_title(name=metadata.name, version=metadata.version) }}
6+
{%- endblock title -%}
7+
8+
{%- block topbar -%}
9+
{%- set latest_version = "" -%}
10+
{%- set latest_path = "" -%}
11+
{%- set target = "" -%}
12+
{%- set inner_path = metadata.target_name ~ "/index.html" -%}
13+
{%- set is_latest_version = true -%}
14+
{%- set is_prerelease = false -%}
15+
{%- include "rustdoc/topbar.html" -%}
16+
{%- endblock topbar -%}
17+
18+
{%- block header -%}
19+
{{ navigation::package_navigation(metadata=metadata, active_tab="features") }}
20+
{%- endblock header -%}
21+
22+
{%- block body -%}
23+
<div class="container package-page-container">
24+
<div class="pure-g">
25+
<div class="pure-u-1 pure-u-sm-7-24 pure-u-md-5-24">
26+
<div class="pure-menu package-menu">
27+
<ul class="pure-menu-list">
28+
<li class="pure-menu-heading">Feature flags</li>
29+
{%- if features -%}
30+
{%- for feature in features -%}
31+
<li class="pure-menu-item">
32+
<a href="#{{ feature.name }}" class="pure-menu-link" style="text-align:center;">
33+
{{ feature.name }}
34+
</a>
35+
</li>
36+
{%- endfor -%}
37+
{%- else -%}
38+
<li class="pure-menu-item">
39+
<span style="font-size: 13px;">Feature flags are not available for this version.</span>
40+
</li>
41+
{%- endif -%}
42+
</ul>
43+
</div>
44+
</div>
45+
46+
<div class="pure-u-1 pure-u-sm-17-24 pure-u-md-19-24 package-details" id="main">
47+
<h1>{{ metadata.name }}</h1>
48+
{%- if features -%}
49+
<p>This version has <b>{{ feature_len }}</b> feature flags, <b>{{ default_feature_len }}</b> of them being enabled by <b>default</b>.</p>
50+
{%- for feature in features -%}
51+
<h3 id="{{ feature.name }}">{{ feature.name }}</h3>
52+
<ul class="pure-menu-list">
53+
{%- if feature.subfeatures -%}
54+
{%- for subfeature in feature.subfeatures -%}
55+
<li class="pure-menu-item">
56+
<span>{{ subfeature }}</span>
57+
</li>
58+
{%- endfor -%}
59+
{%- else -%}
60+
<p>This feature flag does not enable additional features.</p>
61+
{%- endif -%}
62+
</ul>
63+
{%- endfor -%}
64+
{%- else -%}
65+
<p>Feature flags are not available for this version.</p>
66+
{%- endif -%}
67+
</div>
68+
</div>
69+
</div>
70+
{%- endblock body -%}

templates/header/package_navigation.html

+12
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* `crate`
99
* `source`
1010
* `builds`
11+
* `features`
1112

1213
Note: `false` here is acting as a pseudo-null value since you can't directly construct null values
1314
and tera requires all parameters without defaults to be filled
@@ -85,6 +86,17 @@ <h1 id="crate-title">
8586
<span class="title"> Builds</span>
8687
</a>
8788
</li>
89+
90+
{# The features tab #}
91+
{%- if metadata.features -%}
92+
<li class="pure-menu-item">
93+
<a href="/crate/{{ crate_path | safe }}/features"
94+
class="pure-menu-link{% if active_tab == 'features' %} pure-menu-active{% endif %}">
95+
{{ "flag" | fas }}
96+
<span class="title">Feature flags</span>
97+
</a>
98+
</li>
99+
{%- endif -%}
88100
</ul>
89101
</div>
90102
</div>

0 commit comments

Comments
 (0)