From 916ae96c0e3d73616571938cdcb5b2fa1457074d Mon Sep 17 00:00:00 2001 From: Michael Zhao Date: Wed, 1 Mar 2023 09:48:30 -0600 Subject: [PATCH] Added stats to admin page --- client/src/admin/index.tsx | 66 +------------- client/src/admin/login.tsx | 15 +++- client/src/components/JuryHeader.tsx | 35 ++++++-- client/src/components/admin/AdminStat.tsx | 8 ++ .../src/components/admin/AdminStatsPanel.tsx | 47 ++++++++++ client/src/judge/login.tsx | 5 +- src/api/admin.rs | 36 ++++++-- src/api/catchers.rs | 17 ---- src/api/mod.rs | 1 - src/db/admin.rs | 86 +++++++++++++++++++ src/db/mod.rs | 1 + src/db/models.rs | 2 + src/main.rs | 4 +- src/util/mod.rs | 1 + src/util/types.rs | 19 ++++ 15 files changed, 244 insertions(+), 99 deletions(-) create mode 100644 client/src/components/admin/AdminStat.tsx create mode 100644 client/src/components/admin/AdminStatsPanel.tsx delete mode 100644 src/api/catchers.rs create mode 100644 src/db/admin.rs create mode 100644 src/util/types.rs diff --git a/client/src/admin/index.tsx b/client/src/admin/index.tsx index bbf10f8..c0764f7 100644 --- a/client/src/admin/index.tsx +++ b/client/src/admin/index.tsx @@ -1,71 +1,11 @@ +import AdminStatsPanel from '../components/admin/AdminStatsPanel'; import JuryHeader from '../components/JuryHeader'; const Admin = () => { return ( <> - -
-
-
-

- 0 -

-

Seen

-
-
-

- 0 -

-

Votes

-
-
-

- 0 -

-

Judging Time

-
-
-

- 0 -

-

Average Mu

-
-
-

- 0 -

-

Average Sigma^2

-
-
-
-
-

Batch Ops

-
Prioritize
-
Edit
-
Hide
-
Delete
-
-
-
-
-
-
-
-
-

Add Judges

-
-
- - - - - - - - - -
NameVotesAlphaBetaUpdatedActions [Delete,Hide,Edit]
-
+ + ); }; diff --git a/client/src/admin/login.tsx b/client/src/admin/login.tsx index 80217eb..0e08e6a 100644 --- a/client/src/admin/login.tsx +++ b/client/src/admin/login.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import Cookies from 'universal-cookie'; import Button from '../components/Button'; @@ -14,6 +14,19 @@ const AdminLogin = () => { const cookies = new Cookies(); const navigate = useNavigate(); + // If token cookie is already defined and valid, redirect to admin page + useEffect(() => { + const cookies = new Cookies(); + // TODO: Check if the cookie is valid lmao + if (cookies.get('admin-pass')) { + navigate('/admin') + return; + } + + // If invalid, delete the token + cookies.remove('admin-pass'); + }, [navigate]); + const handleChange = (e: React.ChangeEvent) => { setPassword(e.target.value); }; diff --git a/client/src/components/JuryHeader.tsx b/client/src/components/JuryHeader.tsx index bad110f..2220aa3 100644 --- a/client/src/components/JuryHeader.tsx +++ b/client/src/components/JuryHeader.tsx @@ -1,8 +1,9 @@ import { useNavigate } from 'react-router-dom'; +import { twMerge } from 'tailwind-merge'; import Cookies from 'universal-cookie'; import logoutButton from '../assets/logout.svg'; -const JuryHeader = (props: { withLogout?: boolean }) => { +const JuryHeader = (props: { withLogout?: boolean; isAdmin?: boolean }) => { const navigate = useNavigate(); const cookies = new Cookies(); @@ -12,13 +13,37 @@ const JuryHeader = (props: { withLogout?: boolean }) => { navigate('/'); }; + const adminCenter = props.isAdmin ? 'text-center' : ''; + return ( -
-
Jury
-
{process.env.REACT_APP_JURY_NAME}
+
+ + {props.isAdmin ? 'Jury Admin' : 'Jury'} + +
+ {process.env.REACT_APP_JURY_NAME} +
{props.withLogout && (
Logout
diff --git a/client/src/components/admin/AdminStat.tsx b/client/src/components/admin/AdminStat.tsx new file mode 100644 index 0000000..1140319 --- /dev/null +++ b/client/src/components/admin/AdminStat.tsx @@ -0,0 +1,8 @@ +const AdminStat = (props: { name: string; value: string | number }) => { + return
+
{props.value}
+
{props.name}
+
; +}; + +export default AdminStat; diff --git a/client/src/components/admin/AdminStatsPanel.tsx b/client/src/components/admin/AdminStatsPanel.tsx new file mode 100644 index 0000000..01bc52e --- /dev/null +++ b/client/src/components/admin/AdminStatsPanel.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import AdminStat from './AdminStat'; + +interface Stats { + projects: number; + seen: number; + votes: number; + time: number; + avg_mu: number; + avg_sigma: number; + judges: number; +} + +const AdminStatsPanel = () => { + const [stats, setStats] = useState({ + projects: 0, + seen: 0, + votes: 0, + time: 0, + avg_mu: 0, + avg_sigma: 0, + judges: 0, + }); + useEffect(() => { + (async () => { + const fetchedStats = await fetch(`${process.env.REACT_APP_JURY_URL}/admin/stats`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + credentials: 'include', + }).then((data) => data.json()); + setStats(fetchedStats); + })(); + }, []); + return ( +
+ + + + + + + +
+ ); +}; + +export default AdminStatsPanel; diff --git a/client/src/judge/login.tsx b/client/src/judge/login.tsx index 36f2ccf..430891c 100644 --- a/client/src/judge/login.tsx +++ b/client/src/judge/login.tsx @@ -18,7 +18,10 @@ const JudgeLogin = () => { useEffect(() => { const cookies = new Cookies(); // TODO: Check if the cookie is valid lmao - if (cookies.get('token')) navigate('/judge'); + if (cookies.get('token')) { + navigate('/judge'); + return; + } // If invalid, delete the token cookies.remove('token'); diff --git a/src/api/admin.rs b/src/api/admin.rs index 5c3ca76..8c687a2 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -1,14 +1,13 @@ -use rocket::{ - http::Status, - serde::{json::Json, Deserialize}, -}; +use mongodb::Database; +use rocket::{http::Status, serde::json::Json, State}; use std::env; -#[derive(Deserialize)] -#[serde()] -pub struct AdminLogin<'r> { - password: &'r str, -} +use crate::{ + db::admin::aggregate_stats, + util::types::{AdminLogin, Stats}, +}; + +use super::util::AdminPassword; #[rocket::post("/admin/login", data = "")] pub async fn login(body: Json>) -> (Status, String) { @@ -25,3 +24,22 @@ pub async fn login(body: Json>) -> (Status, String) { ) } } + +#[rocket::get("/admin/stats")] +pub async fn get_stats(_password: AdminPassword, db: &State) -> (Status, Json) { + match aggregate_stats(&db).await { + Ok(stats) => (Status::Ok, Json(stats)), + Err(_) => ( + Status::InternalServerError, + Json(Stats { + projects: 0, + seen: 0, + votes: 0, + time: 0, + avg_mu: 0.0, + avg_sigma: 0.0, + judges: 0, + }), + ), + } +} diff --git a/src/api/catchers.rs b/src/api/catchers.rs deleted file mode 100644 index 3aeaec3..0000000 --- a/src/api/catchers.rs +++ /dev/null @@ -1,17 +0,0 @@ -use rocket::{catch, response::Redirect, Request}; - -#[catch(401)] -pub fn unauthorized(req: &Request) -> Redirect { - let route = match req.route() { - Some(r) => r, - None => { - return Redirect::to("/"); - } - }; - let path = route.uri.path(); - if path.contains("judge") { - Redirect::to("/judge/login") - } else { - Redirect::to("/admin/login") - } -} diff --git a/src/api/mod.rs b/src/api/mod.rs index 5910d51..f92ed9b 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,5 @@ pub mod client; pub mod judge; pub mod admin; pub mod guards; -pub mod catchers; pub mod request_types; pub mod util; diff --git a/src/db/admin.rs b/src/db/admin.rs new file mode 100644 index 0000000..0203ee1 --- /dev/null +++ b/src/db/admin.rs @@ -0,0 +1,86 @@ +use bson::doc; +use mongodb::error::Error; +use mongodb::Database; + +use super::models::{Judge, Project}; +use crate::util::types::Stats; + +pub async fn aggregate_stats(db: &Database) -> Result { + // Fetch the collections we will use + let judges_col = db.collection::("judges"); + let projects_col = db.collection::("projects"); + + // Sum to calculate the # of judges + projects + let judges = judges_col.estimated_document_count(None).await?; + let projects = projects_col.estimated_document_count(None).await?; + + // Aggregation for total seen + let seen_pipeline = vec![doc! { + "$group": { + "_id": null, + "total_seen": { + "$sum": "$seen" + } + } + }]; + let mut seen_cursor = projects_col.aggregate(seen_pipeline, None).await?; + let mut seen = 0; + if seen_cursor.advance().await? { + seen = seen_cursor.current().get_i32("total_seen").unwrap_or_else(|_| 0); + } + + // Aggregation for total votes + let seen_pipeline = vec![doc! { + "$group": { + "_id": null, + "total_votes": { + "$sum": "$votes" + } + } + }]; + let mut votes_cursor = projects_col.aggregate(seen_pipeline, None).await?; + let mut votes = 0; + if votes_cursor.advance().await? { + votes = votes_cursor.current().get_i32("total_seen").unwrap_or_else(|_| 0); + } + + // Aggregation for average mu + let seen_pipeline = vec![doc! { + "$group": { + "_id": null, + "avg_mu": { + "$avg": "$mu" + } + } + }]; + let mut mu_cursor = projects_col.aggregate(seen_pipeline, None).await?; + let mut avg_mu = 0.0; + if mu_cursor.advance().await? { + avg_mu = mu_cursor.current().get_f64("avg_mu").unwrap_or_else(|_| 0.0); + } + + // Aggregation for average sigma squared + let seen_pipeline = vec![doc! { + "$group": { + "_id": null, + "avg_sigma": { + "$avg": "$sigma_sq" + } + } + }]; + let mut mu_cursor = projects_col.aggregate(seen_pipeline, None).await?; + let mut avg_sigma = 0.0; + if mu_cursor.advance().await? { + avg_sigma = mu_cursor.current().get_f64("avg_sigma").unwrap_or_else(|_| 0.0); + } + + Ok(Stats { + projects, + seen: seen.try_into().unwrap_or_else(|_| 0), + votes: votes.try_into().unwrap_or_else(|_| 0), + time: 0, // TODO: find a way to store this in rocket's managed state + avg_mu, + avg_sigma, + judges, + }) +} diff --git a/src/db/mod.rs b/src/db/mod.rs index bcc22b2..f7db438 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,3 +1,4 @@ pub mod init; pub mod models; pub mod judge; +pub mod admin; diff --git a/src/db/models.rs b/src/db/models.rs index ce6b801..c545662 100644 --- a/src/db/models.rs +++ b/src/db/models.rs @@ -9,6 +9,8 @@ pub struct Project { pub name: String, pub location: String, pub description: String, + pub seen: u64, + pub votes: u64, pub mu: f64, pub sigma_sq: f64, pub active: bool, diff --git a/src/main.rs b/src/main.rs index 199cd5d..dd452ea 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use jury::api::client::CORS; use rocket::fs::{relative, FileServer}; use std::env; -use jury::api::{admin, catchers, client, judge}; +use jury::api::{admin, client, judge}; use jury::{db, util}; #[macro_use] @@ -39,10 +39,10 @@ async fn rocket() -> _ { judge::new_judge, judge::judge_read_welcome, admin::login, + admin::get_stats, ], ) .mount("/", routes![client::home, client::all_options]) - .register("/", catchers![catchers::unauthorized]) .mount("/", files) .attach(CORS) } diff --git a/src/util/mod.rs b/src/util/mod.rs index 3e703d1..32d6e91 100644 --- a/src/util/mod.rs +++ b/src/util/mod.rs @@ -1,2 +1,3 @@ pub mod crowd_bt; pub mod check_env; +pub mod types; diff --git a/src/util/types.rs b/src/util/types.rs new file mode 100644 index 0000000..e91b6f2 --- /dev/null +++ b/src/util/types.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +#[serde()] +pub struct AdminLogin<'r> { + pub password: &'r str, +} + +#[derive(Serialize, Deserialize)] +#[serde()] +pub struct Stats { + pub projects: u64, + pub seen: u64, + pub votes: u64, + pub time: u64, + pub avg_mu: f64, + pub avg_sigma: f64, + pub judges: u64, +}