Simple reports view

This commit is contained in:
Adrian Wozniak 2020-05-31 16:08:48 +02:00
parent f1d075865c
commit 8947c409b7
6 changed files with 201 additions and 55 deletions

View File

@ -0,0 +1,34 @@
#reports {
}
#reports > .top > .graph {
margin-top: 15px;
}
#reports > .top > .graph > .graphHeader {
margin-bottom: 15px;
}
#reports > .top > .issueList {
display: block;
}
#reports > .top > .issueList > .issue {
display: flex;
justify-content: space-between;
justify-items: flex-start;
}
#reports > .top > .issueList > .issue > * {
width: 25%;
}
#reports > .top > .issueList > .issue.selected {
color: var(--primary);
font-family: var(--font-bold);
}
#reports > .top > .issueList > .issue.nonSelected {
color: var(--textLight);
font-family: var(--font-regular);
}

View File

@ -31,3 +31,4 @@
@import "./css/register.css";
@import "./css/users.css";
@import "./css/invite.css";
@import "./css/reports.css";

View File

@ -55,6 +55,12 @@ pub enum InvitationPageChange {
SubmitForm,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ReportsPageChange {
DayHovered(Option<chrono::NaiveDate>),
DaySelected(Option<chrono::NaiveDate>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum PageChanged {
Users(UsersPageChange),
@ -62,6 +68,7 @@ pub enum PageChanged {
Profile(ProfilePageChange),
Board(BoardPageChange),
Invitation(InvitationPageChange),
Reports(ReportsPageChange),
}
#[derive(Debug)]

View File

@ -1,5 +1,6 @@
use std::collections::hash_map::HashMap;
use chrono::{prelude::*, NaiveDate};
use seed::browser::web_socket::WebSocket;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
@ -431,7 +432,28 @@ impl ProfilePage {
}
#[derive(Debug)]
pub struct ReportsPage {}
pub struct ReportsPage {
pub selected_day: Option<chrono::NaiveDate>,
pub hovered_day: Option<chrono::NaiveDate>,
pub first_day: NaiveDate,
pub last_day: NaiveDate,
}
impl ReportsPage {
pub fn new() -> Self {
let first_day = chrono::Utc::today().with_day(1).unwrap().naive_local();
let last_day = (first_day + chrono::Duration::days(32))
.with_day(1)
.unwrap()
- chrono::Duration::days(1);
Self {
first_day,
last_day,
selected_day: None,
hovered_day: None,
}
}
}
#[derive(Debug)]
pub enum PageContent {

View File

@ -2,6 +2,7 @@ use seed::prelude::*;
use jirs_data::WsMsg;
use crate::changes::{PageChanged, ReportsPageChange};
use crate::model::{Model, Page, PageContent, ReportsPage};
use crate::ws::enqueue_ws_msg;
use crate::{Msg, WebSocketChanged};
@ -14,7 +15,7 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
_ => (),
}
let _page = match &mut model.page_content {
let page = match &mut model.page_content {
PageContent::Reports(page) => page,
_ => return,
};
@ -28,12 +29,18 @@ pub fn update(msg: Msg, model: &mut crate::model::Model, orders: &mut impl Order
| Msg::ChangePage(Page::Reports) => {
init_load(model, orders);
}
Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DayHovered(v))) => {
page.hovered_day = v;
}
Msg::PageChanged(PageChanged::Reports(ReportsPageChange::DaySelected(v))) => {
page.selected_day = v;
}
_ => {}
}
}
fn build_page_content(model: &mut Model) {
model.page_content = PageContent::Reports(Box::new(ReportsPage {}))
model.page_content = PageContent::Reports(Box::new(ReportsPage::new()))
}
fn init_load(model: &mut Model, orders: &mut impl Orders<Msg>) {

View File

@ -5,91 +5,166 @@ use seed::{prelude::*, *};
use jirs_data::Issue;
use crate::model::Model;
use crate::model::{Model, PageContent, ReportsPage};
use crate::shared::inner_layout;
use crate::Msg;
use crate::{Msg, PageChanged, ReportsPageChange};
const SVG_MARGIN_X: u32 = 5;
const SVG_MARGIN_X: u32 = 10;
const SVG_DRAWABLE_HEIGHT: u32 = 300;
const SVG_HEIGHT: u32 = SVG_DRAWABLE_HEIGHT + 30;
const SVG_WIDTH: u32 = 1060;
const SVG_BAR_WIDTH: u32 = 25;
const SVG_BAR_MARGIN: u32 = 10;
pub fn view(model: &Model) -> Node<Msg> {
let body = section![
h1![class!["header"], "Reports"],
div![this_month_graph(model)],
];
let page = match &model.page_content {
PageContent::Reports(page) => page,
_ => return empty![],
};
let this_month_updated = this_month_updated(model, page);
let graph = this_month_graph(page, &this_month_updated);
let list = issue_list(page, &this_month_updated);
let body = section![class!["top"], h1![class!["header"], "Reports"], graph, list];
inner_layout(model, "reports", vec![body])
}
fn this_month_graph(model: &Model) -> Node<Msg> {
let first_day = chrono::Utc::today().with_day(1).unwrap().naive_local();
let last_day = (first_day + chrono::Duration::days(32))
.with_day(1)
.unwrap()
- chrono::Duration::days(1);
let this_month_updated: Vec<&Issue> = model
.issues
.iter()
.filter(|issue| issue.updated_at.date() >= first_day)
.collect();
fn this_month_graph(page: &Box<ReportsPage>, this_month_updated: &Vec<&Issue>) -> Node<Msg> {
let mut dominant = 0;
let mut issues: HashMap<u32, Vec<&Issue>> = HashMap::new();
let list: Vec<Node<Msg>> = this_month_updated
.into_iter()
.map(|issue| {
let date = issue.updated_at.date();
issues.entry(date.day0()).or_default().push(issue);
let day = issue.updated_at.date().format("%Y-%m-%d").to_string();
li![span![issue.title.as_str()], span![day.as_str()]]
})
.collect();
for issue in this_month_updated {
let date = issue.updated_at.date();
let v = issues.entry(date.day0()).or_default();
v.push(issue);
if dominant < v.len() {
dominant = v.len();
}
}
let legend_margin_width = (dominant as f64).log10() * SVG_MARGIN_X as f64;
let mut columns: Vec<Node<Msg>> = vec![];
let x_origin: Node<Msg> = seed::rect![attrs![
At::X => SVG_MARGIN_X,
At::Y => SVG_HEIGHT - SVG_MARGIN_X - 20,
At::Width => SVG_WIDTH - (SVG_MARGIN_X * 2),
At::Height => 2,
At::Style => "fill: var(--textDark);"
]];
for day in (first_day.day0())..(last_day.day0()) {
let x = (SVG_BAR_WIDTH * day) + (SVG_BAR_MARGIN * day) + (SVG_MARGIN_X * 2);
let piece_height = SVG_DRAWABLE_HEIGHT as f64 / dominant as f64;
let piece_width = (SVG_WIDTH as f64 - (legend_margin_width + SVG_MARGIN_X as f64))
/ page.last_day.day() as f64;
let resolution = 10;
let mut legend_parts: Vec<Node<Msg>> = vec![];
for y in 0..(resolution + 1) {
let current = dominant as f64 * (y as f64 / resolution as f64);
legend_parts.push(seed::text![
attrs![
At::X => 0,
At::Y => SVG_DRAWABLE_HEIGHT as f64 - (current as f64 * piece_height) + 12f64,
At::Style => "fill: var(--textLight); font-family: var(--font-regular); font-size: 10px;",
],
format!("{:.1}", current),
]);
legend_parts.push(seed::rect![attrs![
At::X => legend_margin_width + SVG_MARGIN_X as f64,
At::Y => SVG_DRAWABLE_HEIGHT as f64 - (current as f64 * piece_height),
At::Width => SVG_WIDTH as f64 - (legend_margin_width + SVG_MARGIN_X as f64),
At::Height => 1,
At::Style => "fill: var(--textLight);",
],]);
}
columns.push(seed::g![legend_parts]);
for day in (page.first_day.day0())..(page.last_day.day()) {
let num_issues = issues.get(&day).map(|v| v.len()).unwrap_or_default() as u32;
let height = num_issues * SVG_BAR_WIDTH;
if num_issues == 0 {
continue;
}
let x = (piece_width * day as f64)
+ (SVG_BAR_MARGIN * day) as f64
+ (legend_margin_width + SVG_MARGIN_X as f64);
let height = num_issues as f64 * piece_height;
let day = first_day.with_day0(day).unwrap();
let day = page.first_day.with_day0(day).unwrap();
columns.push(seed::rect![attrs![
At::X => x,
At::Y => SVG_DRAWABLE_HEIGHT - height, // reverse draw origin
At::Width => SVG_BAR_WIDTH,
At::Height => height,
At::Style => "fill: rgb(255,0,0);",
At::Title => format!("Number of issues: {}", num_issues),
]]);
let on_hover: EventHandler<Msg> = mouse_ev(Ev::MouseEnter, move |_| {
Some(Msg::PageChanged(PageChanged::Reports(
ReportsPageChange::DayHovered(Some(day.clone())),
)))
});
let on_blur: EventHandler<Msg> = mouse_ev(Ev::MouseLeave, move |_| {
Some(Msg::PageChanged(PageChanged::Reports(
ReportsPageChange::DayHovered(None),
)))
});
columns.push(seed::rect![
on_hover,
on_blur,
attrs![
At::X => x,
At::Y => SVG_DRAWABLE_HEIGHT as f64 - height, // reverse draw origin
At::Width => piece_width,
At::Height => height,
At::Style => "fill: rgb(255, 0, 0);",
At::Title => format!("Number of issues: {}", num_issues),
]
]);
columns.push(seed::text![
attrs![
At::X => x,
At::Y => SVG_HEIGHT,
At::Style => "fill: var(--textDark); font-family: var(--font-regular); font-size: 10px;",
At::Style => "fill: var(--textLight); font-family: var(--font-regular); font-size: 10px;",
],
day.format("%d/%m").to_string(),
]);
}
div![
class!["graph"],
h5![class!["graphHeader"], "Last updated"],
svg![
attrs![At::Height => SVG_HEIGHT, At::Width => SVG_WIDTH],
x_origin,
columns,
],
ul![list],
]
}
fn issue_list(page: &Box<ReportsPage>, this_month_updated: &Vec<&Issue>) -> Node<Msg> {
let mut children: Vec<Node<Msg>> = vec![];
for issue in this_month_updated.into_iter() {
let date = issue.updated_at.date();
let day = date.format("%Y-%m-%d").to_string();
let active_class = match (page.hovered_day.as_ref(), page.selected_day.as_ref()) {
(Some(d), _) if *d == date => "selected",
(_, Some(d)) if *d == date => "selected",
(Some(_), _) | (_, Some(_)) => "nonSelected",
_ => "",
};
let Issue {
title,
issue_type,
priority,
description: _,
issue_status_id: _,
..
} = issue;
children.push(li![
class!["issue"],
class![active_class],
span![title.as_str()],
span![format!("{}", issue_type)],
span![format!("{}", priority)],
span![day.as_str()]
]);
}
div![class!["issueList"], children]
}
fn this_month_updated<'a>(model: &'a Model, page: &Box<ReportsPage>) -> Vec<&'a Issue> {
model
.issues
.iter()
.filter(|issue| {
issue.updated_at.date() >= page.first_day && issue.updated_at.date() <= page.last_day
})
.collect()
}