Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added stats to admin page #4

Merged
merged 1 commit into from
Mar 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 3 additions & 63 deletions client/src/admin/index.tsx
Original file line number Diff line number Diff line change
@@ -1,71 +1,11 @@
import AdminStatsPanel from '../components/admin/AdminStatsPanel';
import JuryHeader from '../components/JuryHeader';

const Admin = () => {
return (
<>
<JuryHeader withLogout />
<div className="panel">
<div className="stats-row">
<div className="stat">
<p id="seen" className="stat-title">
0
</p>
<p className="stat-subtitle">Seen</p>
</div>
<div className="stat">
<p id="votes" className="stat-title">
0
</p>
<p className="stat-subtitle">Votes</p>
</div>
<div className="stat">
<p id="time" className="stat-title">
0
</p>
<p className="stat-subtitle">Judging Time</p>
</div>
<div className="stat">
<p id="mu" className="stat-title">
0
</p>
<p className="stat-subtitle">Average Mu</p>
</div>
<div className="stat">
<p id="sigma" className="stat-title">
0
</p>
<p className="stat-subtitle">Average Sigma^2</p>
</div>
</div>
<div className="control-row">
<div className="batch">
<p>Batch Ops</p>
<div className="batch-icon">Prioritize</div>
<div className="batch-icon">Edit</div>
<div className="batch-icon">Hide</div>
<div className="batch-icon">Delete</div>
</div>
<div className="tabs">
<div className="tab-text"></div>
<div className="tab-text"></div>
<div className="tab-text"></div>
<div className="tab-rect"></div>
</div>
<div className="add-container">
<p className="add-judges">Add Judges</p>
</div>
</div>
<table>
<tr>
<th>Name</th>
<th>Votes</th>
<th>Alpha</th>
<th>Beta</th>
<th>Updated</th>
<th>Actions &lbrack;Delete,Hide,Edit&rbrack;</th>
</tr>
</table>
</div>
<JuryHeader withLogout isAdmin />
<AdminStatsPanel />
</>
);
};
Expand Down
15 changes: 14 additions & 1 deletion client/src/admin/login.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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<HTMLInputElement>) => {
setPassword(e.target.value);
};
Expand Down
35 changes: 30 additions & 5 deletions client/src/components/JuryHeader.tsx
Original file line number Diff line number Diff line change
@@ -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();

Expand All @@ -12,13 +13,37 @@ const JuryHeader = (props: { withLogout?: boolean }) => {
navigate('/');
};

const adminCenter = props.isAdmin ? 'text-center' : '';

return (
<div className="px-2 relative md:w-[30rem] mx-auto mt-2">
<div className="font-bold text-4xl">Jury</div>
<div className="font-bold text-primary">{process.env.REACT_APP_JURY_NAME}</div>
<div
className={twMerge(
'md:px-2 px-4 relative mx-auto mt-2 w-full flex flex-col',
props.isAdmin ? 'items-center' : 'md:w-[30rem]'
)}
>
<a
href="/"
className={twMerge(
'font-bold hover:text-primary duration-200 block max-w-fit',
props.isAdmin ? 'text-5xl' : 'text-4xl',
adminCenter
)}
>
{props.isAdmin ? 'Jury Admin' : 'Jury'}
</a>
<div
className={twMerge(
'font-bold text-primary',
props.isAdmin && 'text-[1.5rem]',
adminCenter
)}
>
{process.env.REACT_APP_JURY_NAME}
</div>
{props.withLogout && (
<div
className="absolute top-4 right-4 flex items-center cursor-pointer border-none bg-transparent"
className="absolute top-4 right-4 flex items-center cursor-pointer border-none bg-transparent hover:scale-110 duration-200"
onClick={logout}
>
<div className="text-light text-xl mr-2">Logout</div>
Expand Down
8 changes: 8 additions & 0 deletions client/src/components/admin/AdminStat.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const AdminStat = (props: { name: string; value: string | number }) => {
return <div className="text-center">
<div className="text-6xl">{props.value}</div>
<div className="text-light">{props.name}</div>
</div>;
};

export default AdminStat;
47 changes: 47 additions & 0 deletions client/src/components/admin/AdminStatsPanel.tsx
Original file line number Diff line number Diff line change
@@ -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<Stats>({
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 (
<div className="flex justify-evenly w-full mt-4">
<AdminStat name="Projects" value={stats.projects} />
<AdminStat name="Seen" value={stats.seen} />
<AdminStat name="Votes" value={stats.votes} />
<AdminStat name="Judging Time" value="01:46:23" />
<AdminStat name="Average Mu" value={stats.avg_mu} />
<AdminStat name="Average Sigma^2" value={stats.avg_sigma} />
<AdminStat name="Judges" value={stats.judges} />
</div>
);
};

export default AdminStatsPanel;
5 changes: 4 additions & 1 deletion client/src/judge/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
36 changes: 27 additions & 9 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
@@ -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 = "<body>")]
pub async fn login(body: Json<AdminLogin<'_>>) -> (Status, String) {
Expand All @@ -25,3 +24,22 @@ pub async fn login(body: Json<AdminLogin<'_>>) -> (Status, String) {
)
}
}

#[rocket::get("/admin/stats")]
pub async fn get_stats(_password: AdminPassword, db: &State<Database>) -> (Status, Json<Stats>) {
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,
}),
),
}
}
17 changes: 0 additions & 17 deletions src/api/catchers.rs

This file was deleted.

1 change: 0 additions & 1 deletion src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
86 changes: 86 additions & 0 deletions src/db/admin.rs
Original file line number Diff line number Diff line change
@@ -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<Stats, Error> {
// Fetch the collections we will use
let judges_col = db.collection::<Judge>("judges");
let projects_col = db.collection::<Project>("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,
})
}
1 change: 1 addition & 0 deletions src/db/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod init;
pub mod models;
pub mod judge;
pub mod admin;
2 changes: 2 additions & 0 deletions src/db/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading