From 94467aa4e40ebf9ed528d25ea8853e694ca58454 Mon Sep 17 00:00:00 2001
From: Edward Viaene <ward@in4it.io>
Date: Sun, 25 Aug 2024 11:24:41 -0500
Subject: [PATCH] units, date

---
 pkg/rest/router.go                   |  2 +-
 pkg/rest/stats.go                    | 45 ++++++++++++++++++------
 pkg/rest/types.go                    | 15 ++++----
 webapp/src/App.tsx                   |  1 +
 webapp/src/Routes/Home/UserStats.tsx | 52 +++++++++++++++++++---------
 5 files changed, 80 insertions(+), 35 deletions(-)

diff --git a/pkg/rest/router.go b/pkg/rest/router.go
index c01cae8..1206d01 100644
--- a/pkg/rest/router.go
+++ b/pkg/rest/router.go
@@ -62,7 +62,7 @@ func (c *Context) getRouter(assets fs.FS, indexHtml []byte) *http.ServeMux {
 	mux.Handle("/api/saml-setup/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.samlSetupElementHandler)))))
 	mux.Handle("/api/users", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.usersHandler)))))
 	mux.Handle("/api/user/{id}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userHandler)))))
-	mux.Handle("/api/stats/user", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userStatsHandler)))))
+	mux.Handle("/api/stats/user/{date}", c.authMiddleware(c.injectUserMiddleware(c.isAdminMiddleware(http.HandlerFunc(c.userStatsHandler)))))
 
 	return mux
 }
diff --git a/pkg/rest/stats.go b/pkg/rest/stats.go
index c39f7b8..229b5d4 100644
--- a/pkg/rest/stats.go
+++ b/pkg/rest/stats.go
@@ -5,6 +5,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"math"
 	"net/http"
 	"path"
 	"sort"
@@ -16,6 +17,24 @@ import (
 )
 
 func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) {
+	if r.PathValue("date") == "" {
+		c.returnError(w, fmt.Errorf("no date supplied"), http.StatusBadRequest)
+		return
+	}
+	date, err := time.Parse("2006-01-02", r.PathValue("date"))
+	if err != nil {
+		c.returnError(w, fmt.Errorf("invalid date: %s", err), http.StatusBadRequest)
+		return
+	}
+	unitAdjustment := int64(1)
+	switch r.FormValue("unit") {
+	case "KB":
+		unitAdjustment = 1024
+	case "MB":
+		unitAdjustment = 1024 * 1024
+	case "GB":
+		unitAdjustment = 1024 * 1024 * 1024
+	}
 	// get all users
 	users := c.UserStore.ListUsers()
 	userMap := make(map[string]string)
@@ -24,7 +43,7 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) {
 	}
 	// calculate stats
 	var userStatsResponse UserStatsResponse
-	statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log")
+	statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+date.Format("2006-01-02")+".log"))
 	if !c.Storage.Client.FileExists(statsFile) { // file does not exist so just return empty response
 		out, err := json.Marshal(userStatsResponse)
 		if err != nil {
@@ -69,14 +88,16 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) {
 			if _, ok := receiveBytesData[userID]; !ok {
 				receiveBytesData[userID] = []UserStatsDataPoint{}
 			}
-			receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: receiveBytes - receiveBytesLast[userID]})
+			value := math.Round(float64((receiveBytes-receiveBytesLast[userID])/unitAdjustment*100)) / 100
+			receiveBytesData[userID] = append(receiveBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value})
 		}
 		transmitBytes, err := strconv.ParseInt(inputSplit[4], 10, 64)
 		if err == nil {
 			if _, ok := transmitBytesData[userID]; !ok {
 				transmitBytesData[userID] = []UserStatsDataPoint{}
 			}
-			transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: transmitBytes - transmitBytesLast[userID]})
+			value := math.Round(float64((transmitBytes-transmitBytesLast[userID])/unitAdjustment*100)) / 100
+			transmitBytesData[userID] = append(transmitBytesData[userID], UserStatsDataPoint{X: inputSplit[0], Y: value})
 		}
 		receiveBytesLast[userID] = receiveBytes
 		transmitBytesLast[userID] = transmitBytes
@@ -98,10 +119,11 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) {
 			login = "unknown"
 		}
 		userStatsResponse.ReceiveBytes.Datasets = append(userStatsResponse.ReceiveBytes.Datasets, UserStatsDataset{
-			BorderColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)),
-			Label:       login,
-			Data:        data,
-			Tension:     0.1,
+			BorderColor:     getColor(len(userStatsResponse.ReceiveBytes.Datasets)),
+			BackgroundColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)),
+			Label:           login,
+			Data:            data,
+			Tension:         0.1,
 		})
 	}
 	for userID, data := range transmitBytesData {
@@ -110,10 +132,11 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) {
 			login = "unknown"
 		}
 		userStatsResponse.TransmitBytes.Datasets = append(userStatsResponse.TransmitBytes.Datasets, UserStatsDataset{
-			BorderColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)),
-			Label:       login,
-			Data:        data,
-			Tension:     0.1,
+			BorderColor:     getColor(len(userStatsResponse.TransmitBytes.Datasets)),
+			BackgroundColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)),
+			Label:           login,
+			Data:            data,
+			Tension:         0.1,
 		})
 	}
 
diff --git a/pkg/rest/types.go b/pkg/rest/types.go
index 6c49f36..e3443a3 100644
--- a/pkg/rest/types.go
+++ b/pkg/rest/types.go
@@ -184,14 +184,15 @@ type UserStatsData struct {
 }
 type UserStatsDatasets []UserStatsDataset
 type UserStatsDataset struct {
-	Label       string               `json:"label"`
-	Data        []UserStatsDataPoint `json:"data"`
-	Fill        bool                 `json:"fill"`
-	BorderColor string               `json:"borderColor"`
-	Tension     float64              `json:"tension"`
+	Label           string               `json:"label"`
+	Data            []UserStatsDataPoint `json:"data"`
+	Fill            bool                 `json:"fill"`
+	BorderColor     string               `json:"borderColor"`
+	BackgroundColor string               `json:"backgroundColor"`
+	Tension         float64              `json:"tension"`
 }
 
 type UserStatsDataPoint struct {
-	X string `json:"x"`
-	Y int64  `json:"y"`
+	X string  `json:"x"`
+	Y float64 `json:"y"`
 }
diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx
index fc63aec..ed795a8 100644
--- a/webapp/src/App.tsx
+++ b/webapp/src/App.tsx
@@ -1,4 +1,5 @@
 import "@mantine/core/styles.css";
+import '@mantine/dates/styles.css';
 import { AppShell, MantineProvider } from "@mantine/core";
 import { theme } from "./theme";
 import { NavBar } from "./NavBar/NavBar";
diff --git a/webapp/src/Routes/Home/UserStats.tsx b/webapp/src/Routes/Home/UserStats.tsx
index fa04a76..60b44e0 100644
--- a/webapp/src/Routes/Home/UserStats.tsx
+++ b/webapp/src/Routes/Home/UserStats.tsx
@@ -1,20 +1,25 @@
-import { Card, Center, Text } from "@mantine/core";
-//import { DatePicker } from '@mantine/dates';
+import { Card, Center, Divider, Grid, Select, Text } from "@mantine/core";
+import { DatePickerInput } from '@mantine/dates';
 import { useQuery } from "@tanstack/react-query";
 import { useAuthContext } from "../../Auth/Auth";
 import { AppSettings } from '../../Constants/Constants';
+import { format } from "date-fns";
 import { Chart } from 'react-chartjs-2';
 import 'chartjs-adapter-date-fns';
 import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend } from 'chart.js';
+import { useState } from "react";
+  
 
 export function UserStats() {
     ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend);
 
     const {authInfo} = useAuthContext()
+    const [statsDate, setStatsDate] = useState<Date | null>(new Date());
+    const [unit, setUnit] = useState<string>("MB")
     const { isPending, error, data } = useQuery({
-        queryKey: ['userstats'],
+        queryKey: ['userstats', statsDate, unit],
         queryFn: () =>
-            fetch(AppSettings.url + '/stats/user', {
+            fetch(AppSettings.url + '/stats/user/' + format(statsDate === null ? new Date() : statsDate, "yyyy-MM-dd") + "?unit=" +unit, {
             headers: {
                 "Content-Type": "application/json",
                 "Authorization": "Bearer " + authInfo.token
@@ -54,27 +59,42 @@ export function UserStats() {
     if (isPending) return ''
     if (error) return 'cannot retrieve licensed users'
     
-    if(data.receivedBytes.datasets === null) {
-        data.receivedBytes.datasets = [{ data: [0], label: "no data"}]
-    }
-    if(data.transmitBytes.datasets === null) {
-        data.transmitBytes.datasets = [{ data: [0], label: "no data"}]
-    }
 
     return (
         <>
         <Card withBorder radius="md" bg="var(--mantine-color-body)" mt={20}>
-            <Center>
-            <Text fw={500} size="lg">VPN Data Received (bytes)</Text>
-            <Text>
-            </Text>
-            
+            <Grid>
+            <Grid.Col span={6}></Grid.Col>
+
+            <Grid.Col span={4}>
+                <DatePickerInput
+                value={statsDate}
+                onChange={setStatsDate}
+                size="xs"
+                />
+                </Grid.Col>
+            <Grid.Col span={2}>
+            <Select
+                data={['Bytes', 'KB', 'MB', 'GB']}
+                defaultValue={"MB"}
+                allowDeselect={false}
+                size="xs"
+                withCheckIcon={false}
+                value={unit}
+                onChange={(_value) => setUnit(_value === null ? "" : _value)}
+                />
+            </Grid.Col>
+            </Grid>
+
+            <Center mt={10}>
+            <Text fw={500} size="lg">Data Received by VPN
+            </Text>          
             </Center>
             <Chart type="line" data={data.receivedBytes} options={options} />
         </Card>
         <Card withBorder radius="md" bg="var(--mantine-color-body)" mt={20}>
             <Center>
-            <Text fw={500} size="lg">VPN Data Sent (bytes)</Text>
+            <Text fw={500} size="lg">Data Sent by VPN</Text>
             </Center>
             <Chart type="line" data={data.transmitBytes} options={options} />
         </Card>