Skip to content

Commit

Permalink
Add support for custom views
Browse files Browse the repository at this point in the history
By defining a view in the configuration file, the view can be accessed
by adding the querystring paramter ?view=myviewname in the frontend app.

Closes #29
  • Loading branch information
patriksvensson committed Feb 6, 2020
1 parent 425ca4a commit b90a53a
Show file tree
Hide file tree
Showing 15 changed files with 1,029 additions and 123 deletions.
654 changes: 654 additions & 0 deletions schemas/v0.7.json

Large diffs are not rendered by default.

28 changes: 27 additions & 1 deletion src/api/endpoints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ use actix_web::{HttpResponse, Responder};

use crate::engine::state::EngineState;

use super::models::{BuildViewModel, ServerInfoModel};
use super::models::{BuildViewModel, ServerInfoModel, ViewInfoModel};

#[get("/server")]
pub fn server_info(state: web::Data<Arc<EngineState>>) -> impl Responder {
let info = ServerInfoModel {
title: &state.title[..],
views: state
.views
.get_views()
.iter()
.map(ViewInfoModel::from)
.collect(),
};
let json = serde_json::to_string(&info).unwrap();
HttpResponse::Ok()
Expand All @@ -34,3 +40,23 @@ pub fn get_builds(state: web::Data<Arc<EngineState>>) -> impl Responder {
.content_type("application/json")
.body(json)
}

#[get("/builds/view/{id}")]
pub fn get_builds_for_view(
id: web::Path<String>,
state: web::Data<Arc<EngineState>>,
) -> impl Responder {
// Convert to view models
let builds: Vec<BuildViewModel> = state
.builds
.for_view(&state.views, &id[..])
.iter()
.map(BuildViewModel::from)
.collect();

// Serialize to JSON and return.
let json = serde_json::to_string(&builds).unwrap();
HttpResponse::Ok()
.content_type("application/json")
.body(json)
}
3 changes: 2 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ pub fn start_and_block(
.register_data(state.clone())
.wrap(Cors::new())
.service(endpoints::server_info)
.service(endpoints::get_builds);
.service(endpoints::get_builds)
.service(endpoints::get_builds_for_view);

if cfg!(feature = "docker") {
// Serve static files from the ui directory.
Expand Down
17 changes: 17 additions & 0 deletions src/api/models.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
use serde::Serialize;

use crate::builds::{Build, BuildStatus};
use crate::config::ViewConfiguration;

#[derive(Serialize, Clone)]
pub struct ServerInfoModel<'a> {
pub title: &'a str,
pub views: Vec<ViewInfoModel>,
}

#[derive(Serialize, Clone)]
pub struct ViewInfoModel {
pub slug: String,
pub name: String,
}

impl<'a> From<&ViewConfiguration> for ViewInfoModel {
fn from(view: &ViewConfiguration) -> Self {
ViewInfoModel {
slug: view.id.clone(),
name: view.name.clone(),
}
}
}

#[derive(Serialize, Clone)]
Expand Down
15 changes: 15 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub struct Configuration {
/// The update interval in seconds
#[serde(default)]
pub interval: Option<Interval>,
/// # Views
pub views: Option<Vec<ViewConfiguration>>,
/// # Duck frontend title
/// The title that is displayed in the UI
#[serde(default)]
Expand Down Expand Up @@ -97,6 +99,19 @@ impl Configuration {
}
}

#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct ViewConfiguration {
/// # View ID
/// The ID of the view
pub id: String,
/// # View name
/// the name of the view
pub name: String,
/// # Included collectors
/// The collectors included in this view
pub collectors: Vec<String>,
}

#[derive(Serialize, Deserialize, JsonSchema, Debug, Clone)]
pub struct Interval(pub u32);
impl Default for Interval {
Expand Down
47 changes: 46 additions & 1 deletion src/config/validation.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use regex::Regex;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};

use log::warn;

Expand All @@ -12,13 +12,31 @@ impl Validate for Configuration {
warn!("No collectors have been specified.");
}

validate_views(&self)?;
validate_ids(&self)?;
validate_collector_references(&self)?;

Ok(())
}
}

fn validate_views(configuration: &Configuration) -> DuckResult<()> {
if let Some(views) = &configuration.views {
let mut known_ids = HashSet::<String>::new();
for view in views.iter() {
if known_ids.contains(&view.id) {
return Err(format_err!(
"Found duplicate view id '{}' in configuration.",
view.id
));
}
known_ids.insert(view.id.clone());
}
};

Ok(())
}

fn validate_ids(configuration: &Configuration) -> DuckResult<()> {
// Make sure that all ids are unique and well formed.
let mut unique_ids = std::collections::HashSet::<String>::new();
Expand Down Expand Up @@ -118,6 +136,33 @@ mod tests {
use super::*;
use crate::utils::text::TestVariableProvider;

#[test]
#[should_panic(expected = "Found duplicate view id \\'foo\\' in configuration.")]
fn should_return_error_if_views_have_the_same_id() {
let config = Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [ ],
"views": [
{
"id": "foo",
"name": "Foo",
"collectors": [ ]
},
{
"id": "foo",
"name": "Bar",
"collectors": [ ]
}
]
}
"#,
)
.unwrap();
config.validate().unwrap();
}

#[test]
#[should_panic(expected = "Found duplicate id \\'foo\\' in configuration.")]
fn should_return_error_if_two_collectors_have_the_same_id() {
Expand Down
3 changes: 2 additions & 1 deletion src/engine/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ use crate::providers::observers::*;
use crate::providers::*;
use crate::utils::DuckResult;

use self::state::{BuildUpdateResult, EngineState};
use self::state::builds::BuildUpdateResult;
use self::state::EngineState;

use log::{debug, error, info};
use waithandle::{EventWaitHandle, WaitHandle};
Expand Down
32 changes: 17 additions & 15 deletions src/engine/state.rs → src/engine/state/builds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,9 @@ use std::collections::HashSet;
use std::sync::Mutex;

use crate::builds::{Build, BuildStatus};
use crate::config::Configuration;
use crate::engine::state::views::ViewRepository;
use crate::providers::collectors::CollectorInfo;

pub struct EngineState {
pub title: String,
pub builds: BuildRepository,
}

impl EngineState {
pub fn new(config: &Configuration) -> Self {
return EngineState {
title: config.get_title().to_string(),
builds: BuildRepository::new(),
};
}
}

pub struct BuildRepository {
builds: Mutex<Vec<Build>>,
statuses: Mutex<HashMap<u64, BuildStatus>>,
Expand All @@ -45,6 +31,22 @@ impl BuildRepository {
self.builds.lock().unwrap().clone()
}

pub fn for_view(&self, views: &ViewRepository, id: &str) -> Vec<Build> {
let builds = self.builds.lock().unwrap();

if let Some(collectors) = views.get_collectors(id) {
let mut result = Vec::<Build>::new();
for build in builds.iter() {
if collectors.contains(&build.collector) {
result.push(build.clone());
}
}
return result;
}

return vec![];
}

#[allow(clippy::block_in_if_condition_stmt)] // Clippy does not like what fmt does...
pub fn update(&self, build: &Build) -> BuildUpdateResult {
let mut builds = self.builds.lock().unwrap();
Expand Down
22 changes: 22 additions & 0 deletions src/engine/state/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
use crate::config::Configuration;
use crate::engine::state::builds::BuildRepository;
use crate::engine::state::views::ViewRepository;

pub mod builds;
pub mod views;

pub struct EngineState {
pub title: String,
pub builds: BuildRepository,
pub views: ViewRepository,
}

impl EngineState {
pub fn new(config: &Configuration) -> Self {
return EngineState {
title: config.get_title().to_string(),
builds: BuildRepository::new(),
views: ViewRepository::new(config),
};
}
}
88 changes: 88 additions & 0 deletions src/engine/state/views.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
use std::collections::HashMap;
use std::collections::HashSet;

use crate::config::{Configuration, ViewConfiguration};

pub struct ViewRepository {
views: Vec<ViewConfiguration>,
collectors: HashMap<String, HashSet<String>>,
}

impl ViewRepository {
pub fn new(config: &Configuration) -> Self {
let mut map = HashMap::<String, HashSet<String>>::new();
if let Some(views) = &config.views {
for view in views.iter() {
let mut collectors = HashSet::<String>::new();
for collector in view.collectors.iter() {
collectors.insert(collector.clone());
}
map.insert(view.id.clone(), collectors);
}
}

Self {
collectors: map,
views: match &config.views {
Some(views) => views.clone(),
None => vec![],
},
}
}

pub fn get_collectors(&self, view_id: &str) -> Option<&HashSet<String>> {
self.collectors.get(view_id)
}

pub fn get_views(&self) -> &Vec<ViewConfiguration> {
&self.views
}
}

#[cfg(test)]
mod tests {
use super::*;
use crate::utils::text::TestVariableProvider;

#[test]
fn should_return_collectors_for_view() {
// Given
let views = ViewRepository::new(
&Configuration::from_json(
&TestVariableProvider::new(),
r#"
{
"collectors": [ ],
"views": [
{
"id": "foo",
"name": "Foo",
"collectors": [ "a1", "a2" ]
},
{
"id": "bar",
"name": "Bar",
"collectors": [ "b1", "b2", "b3" ]
},
{
"id": "baz",
"name": "Bar",
"collectors": [ "c1", "c2", "c3", "c4" ]
}
]
}
"#,
)
.unwrap(),
);

// When
let collectors = views.get_collectors("bar").unwrap();

// Then
assert_eq!(3, collectors.len());
assert!(collectors.contains("b1"));
assert!(collectors.contains("b2"));
assert!(collectors.contains("b3"));
}
}
2 changes: 1 addition & 1 deletion src/providers/collectors/azure/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ impl AzureBuild {
match &self.status[..] {
"notStarted" => return BuildStatus::Queued,
"inProgress" => return BuildStatus::Running,
_ => { },
_ => {}
}
match self.result.as_ref().unwrap().as_ref() {
"succeeded" => BuildStatus::Success,
Expand Down
Loading

0 comments on commit b90a53a

Please sign in to comment.