Skip to content

Commit 2df55ef

Browse files
committed
frontend: Show feature flags
So far features were stored only in database. Add link to the topbar which will lead to the new features page. Features page will show all relevant features with their subfeatures.
1 parent b527df8 commit 2df55ef

File tree

10 files changed

+241
-3
lines changed

10 files changed

+241
-3
lines changed

src/db/types.rs

+5-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,8 @@ 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+
}
1519
}

src/test/fakes.rs

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

213+
pub(crate) fn features(mut self, opt_features: Option<HashMap<String, Vec<String>>>) -> Self {
214+
if let Some(features) = opt_features {
215+
self.package.features = features;
216+
} else {
217+
self.package.features = HashMap::new();
218+
}
219+
self
220+
}
221+
213222
/// Returns the release_id
214223
pub(crate) fn create(self) -> Result<i32, Error> {
215224
use std::fs;

src/web/crate_details.rs

+78
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,79 @@ mod tests {
741744
Ok(())
742745
});
743746
}
747+
748+
#[test]
749+
fn feature_flags_report_empty() {
750+
wrapper(|env| {
751+
env.fake_release()
752+
.name("library")
753+
.version("0.1.0")
754+
.binary(false)
755+
.features(None)
756+
.create()?;
757+
758+
let page = kuchiki::parse_html().one(
759+
env.frontend()
760+
.get("/crate/library/0.1.0/features")
761+
.send()?
762+
.text()?,
763+
);
764+
assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_ok());
765+
Ok(())
766+
});
767+
}
768+
769+
#[test]
770+
fn feature_private_feature_flags_are_hidden() {
771+
wrapper(|env| {
772+
let features = [("_private".into(), Vec::new())]
773+
.iter()
774+
.cloned()
775+
.collect::<HashMap<String, Vec<String>>>();
776+
env.fake_release()
777+
.name("library")
778+
.version("0.1.0")
779+
.binary(false)
780+
.features(Some(features))
781+
.create()?;
782+
783+
let page = kuchiki::parse_html().one(
784+
env.frontend()
785+
.get("/crate/library/0.1.0/features")
786+
.send()?
787+
.text()?,
788+
);
789+
assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_ok());
790+
Ok(())
791+
});
792+
}
793+
794+
#[test]
795+
fn feature_flags_without_default() {
796+
wrapper(|env| {
797+
let features = [("feature1".into(), Vec::new())]
798+
.iter()
799+
.cloned()
800+
.collect::<HashMap<String, Vec<String>>>();
801+
env.fake_release()
802+
.name("library")
803+
.version("0.1.0")
804+
.binary(false)
805+
.features(Some(features))
806+
.create()?;
807+
808+
let page = kuchiki::parse_html().one(
809+
env.frontend()
810+
.get("/crate/library/0.1.0/features")
811+
.send()?
812+
.text()?,
813+
);
814+
assert!(page.select_first(r#"p[data-id="empty-features"]"#).is_err());
815+
let def_len = page
816+
.select_first(r#"b[data-id="default-feature-len"]"#)
817+
.unwrap();
818+
assert_eq!(def_len.text_contents(), "0");
819+
Ok(())
820+
});
821+
}
744822
}

src/web/features.rs

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use crate::{
2+
db::Pool,
3+
impl_webpage,
4+
web::{page::WebPage, MetaData},
5+
};
6+
use iron::{IronResult, Request, Response};
7+
use router::Router;
8+
use serde::Serialize;
9+
10+
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
11+
struct FeaturesPage {
12+
metadata: MetaData,
13+
}
14+
15+
impl_webpage! {
16+
FeaturesPage = "crate/features.html",
17+
}
18+
19+
pub fn build_features_handler(req: &mut Request) -> IronResult<Response> {
20+
let router = extension!(req, Router);
21+
let name = cexpect!(req, router.find("name"));
22+
let version = cexpect!(req, router.find("version"));
23+
24+
let mut conn = extension!(req, Pool).get()?;
25+
26+
FeaturesPage {
27+
metadata: cexpect!(req, MetaData::from_crate(&mut conn, &name, &version)),
28+
}
29+
.into_response(req)
30+
}

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;
@@ -520,6 +522,7 @@ pub(crate) struct MetaData {
520522
pub(crate) default_target: String,
521523
pub(crate) doc_targets: Vec<String>,
522524
pub(crate) yanked: bool,
525+
pub(crate) features: Option<Vec<Feature>>,
523526
}
524527

525528
impl MetaData {
@@ -533,7 +536,8 @@ impl MetaData {
533536
releases.rustdoc_status,
534537
releases.default_target,
535538
releases.doc_targets,
536-
releases.yanked
539+
releases.yanked,
540+
releases.features
537541
FROM releases
538542
INNER JOIN crates ON crates.id = releases.crate_id
539543
WHERE crates.name = $1 AND releases.version = $2",
@@ -552,6 +556,7 @@ impl MetaData {
552556
default_target: row.get(5),
553557
doc_targets: MetaData::parse_doc_targets(row.get(6)),
554558
yanked: row.get(7),
559+
features: MetaData::parse_features(row.get(8)),
555560
})
556561
}
557562

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

571584
#[derive(Debug, Clone, PartialEq, Serialize)]
@@ -844,6 +857,7 @@ mod test {
844857
"arm64-unknown-linux-gnu".to_string(),
845858
],
846859
yanked: false,
860+
features: None,
847861
};
848862

849863
let correct_json = json!({
@@ -858,6 +872,7 @@ mod test {
858872
"arm64-unknown-linux-gnu",
859873
],
860874
"yanked": false,
875+
"features": null
861876
});
862877

863878
assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
@@ -875,6 +890,7 @@ mod test {
875890
"arm64-unknown-linux-gnu",
876891
],
877892
"yanked": false,
893+
"features": null,
878894
});
879895

880896
assert_eq!(correct_json, serde_json::to_value(&metadata).unwrap());
@@ -892,6 +908,7 @@ mod test {
892908
"arm64-unknown-linux-gnu",
893909
],
894910
"yanked": false,
911+
"features": null,
895912
});
896913

897914
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

+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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 metadata.features -%}
30+
{%- for feature in metadata.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 metadata.features -%}
49+
<p>This version has <b>{{ metadata.features | length }}</b> feature flags, <b data-id="default-feature-len">
50+
{%- if metadata.features[0].name == 'default' -%}
51+
{{ metadata.features[0].subfeatures | length }}
52+
{%- else -%}
53+
0
54+
{%- endif -%}
55+
</b> of them being enabled by <b>default</b>.</p>
56+
{%- for feature in metadata.features -%}
57+
<h3 id="{{ feature.name }}">{{ feature.name }}</h3>
58+
<ul class="pure-menu-list">
59+
{%- if feature.subfeatures -%}
60+
{%- for subfeature in feature.subfeatures -%}
61+
<li class="pure-menu-item">
62+
<span>{{ subfeature }}</span>
63+
</li>
64+
{%- endfor -%}
65+
{%- else -%}
66+
<p>This feature flag does not enable additional features.</p>
67+
{%- endif -%}
68+
</ul>
69+
{%- endfor -%}
70+
{%- else -%}
71+
<p data-id="empty-features">Feature flags are not available for this release.</p>
72+
{%- endif -%}
73+
</div>
74+
</div>
75+
</div>
76+
{%- endblock body -%}

templates/header/package_navigation.html

+10
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,15 @@ <h1 id="crate-title">
8586
<span class="title"> Builds</span>
8687
</a>
8788
</li>
89+
90+
{# The features tab #}
91+
<li class="pure-menu-item">
92+
<a href="/crate/{{ crate_path | safe }}/features"
93+
class="pure-menu-link{% if active_tab == 'features' %} pure-menu-active{% endif %}">
94+
{{ "flag" | fas }}
95+
<span class="title">Feature flags</span>
96+
</a>
97+
</li>
8898
</ul>
8999
</div>
90100
</div>

0 commit comments

Comments
 (0)