diff --git a/latest b/latest index 15e52dc..56130fb 100644 --- a/latest +++ b/latest @@ -1 +1 @@ -v1.1.1beta1 +v1.1.1 diff --git a/pkg/rest/stats.go b/pkg/rest/stats.go index 8fc7615..c39f7b8 100644 --- a/pkg/rest/stats.go +++ b/pkg/rest/stats.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "path" + "sort" "strconv" "strings" "time" @@ -15,6 +16,13 @@ import ( ) func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { + // get all users + users := c.UserStore.ListUsers() + userMap := make(map[string]string) + for _, user := range users { + userMap[user.ID] = user.Login + } + // calculate stats var userStatsResponse UserStatsResponse statsFile := c.Storage.Client.ConfigPath(path.Join(wireguard.VPN_STATS_DIR, "user-"+time.Now().Format("2006-01-02")) + ".log") if !c.Storage.Client.FileExists(statsFile) { // file does not exist so just return empty response @@ -85,18 +93,33 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { Datasets: []UserStatsDataset{}, } for userID, data := range receiveBytesData { + login, ok := userMap[userID] + if !ok { + login = "unknown" + } userStatsResponse.ReceiveBytes.Datasets = append(userStatsResponse.ReceiveBytes.Datasets, UserStatsDataset{ - Label: userID, - Data: data, + BorderColor: getColor(len(userStatsResponse.ReceiveBytes.Datasets)), + Label: login, + Data: data, + Tension: 0.1, }) } for userID, data := range transmitBytesData { + login, ok := userMap[userID] + if !ok { + login = "unknown" + } userStatsResponse.TransmitBytes.Datasets = append(userStatsResponse.TransmitBytes.Datasets, UserStatsDataset{ - Label: userID, - Data: data, + BorderColor: getColor(len(userStatsResponse.TransmitBytes.Datasets)), + Label: login, + Data: data, + Tension: 0.1, }) } + sort.Sort(userStatsResponse.ReceiveBytes.Datasets) + sort.Sort(userStatsResponse.TransmitBytes.Datasets) + out, err := json.Marshal(userStatsResponse) if err != nil { c.returnError(w, fmt.Errorf("user stats response marshal error: %s", err), http.StatusBadRequest) @@ -104,3 +127,14 @@ func (c *Context) userStatsHandler(w http.ResponseWriter, r *http.Request) { } c.write(w, out) } + +func getColor(i int) string { + colors := []string{ + "#DEEFB7", + "#98DFAF", + "#5FB49C", + "#414288", + "#682D63", + } + return colors[i%len(colors)] +} diff --git a/pkg/rest/types.go b/pkg/rest/types.go index 223b0ae..6c49f36 100644 --- a/pkg/rest/types.go +++ b/pkg/rest/types.go @@ -180,8 +180,9 @@ type UserStatsResponse struct { TransmitBytes UserStatsData `json:"transmitBytes"` } type UserStatsData struct { - Datasets []UserStatsDataset `json:"datasets"` + Datasets UserStatsDatasets `json:"datasets"` } +type UserStatsDatasets []UserStatsDataset type UserStatsDataset struct { Label string `json:"label"` Data []UserStatsDataPoint `json:"data"` diff --git a/pkg/rest/types_sort.go b/pkg/rest/types_sort.go new file mode 100644 index 0000000..ea7e9f7 --- /dev/null +++ b/pkg/rest/types_sort.go @@ -0,0 +1,13 @@ +package rest + +func (u UserStatsDatasets) Len() int { + return len(u) +} + +func (u UserStatsDatasets) Swap(i, j int) { + u[i], u[j] = u[j], u[i] +} + +func (u UserStatsDatasets) Less(i, j int) bool { + return u[i].Label < u[j].Label +} diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 8b044d5..7c054e3 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@mantine/core": "^7.9.2", + "@mantine/dates": "^7.12.1", "@mantine/form": "^7.10.0", "@mantine/hooks": "^7.9.2", "@tabler/icons-react": "^3.4.0", @@ -1049,9 +1050,10 @@ "license": "MIT" }, "node_modules/@mantine/core": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.10.1.tgz", - "integrity": "sha512-l9ypojKN3PjwO1CSLIsqxi7mA25+7w+xc71Q+JuCCREI0tuGwkZsKbIOpuTATIJOjPh8ycLiW7QxX1LYsRTq6w==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-7.12.1.tgz", + "integrity": "sha512-PXKIDaT1fpNB77dPQIcdFGM2NRnfmsJSVx3uuBccngBQWMIWI0wPyiO1Y26DK4LQrbrypeb+TS+Zxpgx6RoiCA==", + "license": "MIT", "dependencies": { "@floating-ui/react": "^0.26.9", "clsx": "^2.1.1", @@ -1061,7 +1063,23 @@ "type-fest": "^4.12.0" }, "peerDependencies": { - "@mantine/hooks": "7.10.1", + "@mantine/hooks": "7.12.1", + "react": "^18.2.0", + "react-dom": "^18.2.0" + } + }, + "node_modules/@mantine/dates": { + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/dates/-/dates-7.12.1.tgz", + "integrity": "sha512-+Dg5ZGoYPWYRWPY7HagLeW36ayVjKQIkTpdNvgGDwh5YpaFy5cHd6LK6USKUshTsRPuzM3oUKwXIBK8hsigMyA==", + "license": "MIT", + "dependencies": { + "clsx": "^2.1.1" + }, + "peerDependencies": { + "@mantine/core": "7.12.1", + "@mantine/hooks": "7.12.1", + "dayjs": ">=1.0.0", "react": "^18.2.0", "react-dom": "^18.2.0" } @@ -1079,9 +1097,10 @@ } }, "node_modules/@mantine/hooks": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.10.1.tgz", - "integrity": "sha512-0EH9WBWUdtQLGU3Ak+csQ77EtUxI6pPNfwZdRJQWcaA3f8SFOLo9h9CGxiikFExerhvuCeUlaTf3s+TB9Op/rw==", + "version": "7.12.1", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.1.tgz", + "integrity": "sha512-YPA3qiMHJkWID5+YzakBaLvjHtX3Fg3PdPY49iIb/CaWM9+lrJ+77TOVS7bsY7ZTBHXUfzft1/6Woqt3xSuweA==", + "license": "MIT", "peerDependencies": { "react": "^18.2.0" } @@ -2131,6 +2150,13 @@ "url": "https://github.com/sponsors/kossnocorp" } }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT", + "peer": true + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", @@ -3188,10 +3214,11 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" diff --git a/webapp/package.json b/webapp/package.json index 57f74a3..b33abd9 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@mantine/core": "^7.9.2", + "@mantine/dates": "^7.12.1", "@mantine/form": "^7.10.0", "@mantine/hooks": "^7.9.2", "@tabler/icons-react": "^3.4.0", diff --git a/webapp/src/Routes/Home/UserStats.tsx b/webapp/src/Routes/Home/UserStats.tsx index 46953ce..fa04a76 100644 --- a/webapp/src/Routes/Home/UserStats.tsx +++ b/webapp/src/Routes/Home/UserStats.tsx @@ -1,13 +1,14 @@ -import { Card } from "@mantine/core"; +import { Card, Center, Text } from "@mantine/core"; +//import { DatePicker } from '@mantine/dates'; import { useQuery } from "@tanstack/react-query"; import { useAuthContext } from "../../Auth/Auth"; import { AppSettings } from '../../Constants/Constants'; import { Chart } from 'react-chartjs-2'; import 'chartjs-adapter-date-fns'; -import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale } from 'chart.js'; +import { Chart as ChartJS, LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, ChartOptions, Legend } from 'chart.js'; export function UserStats() { - ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale); + ChartJS.register(LineController, LineElement, PointElement, LinearScale, Title, CategoryScale, TimeScale, Legend); const {authInfo} = useAuthContext() const { isPending, error, data } = useQuery({ @@ -25,30 +26,58 @@ export function UserStats() { ), enabled: authInfo.role === "admin", }) - const options = { + + const options:ChartOptions<"line"> = { responsive: true, plugins: { legend: { - position: 'top' as const, - }, - title: { + position: 'right' as const, display: true, - text: 'VPN Received (in bytes)', }, }, scales: { x: { type: 'time', + min: '00:00:00', + /*time: { + displayFormats: { + quarter: 'HHHH MM' + } + }*/ + }, + y: { + min: 0 } } } 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 ( - - + <> + +
+ VPN Data Received (bytes) + + + +
+ +
+ +
+ VPN Data Sent (bytes) +
+
+ ) } \ No newline at end of file diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 203064f..f280366 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -348,10 +348,10 @@ resolved "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz" integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== -"@mantine/core@^7.9.2": - version "7.10.1" - resolved "https://registry.npmjs.org/@mantine/core/-/core-7.10.1.tgz" - integrity sha512-l9ypojKN3PjwO1CSLIsqxi7mA25+7w+xc71Q+JuCCREI0tuGwkZsKbIOpuTATIJOjPh8ycLiW7QxX1LYsRTq6w== +"@mantine/core@^7.9.2", "@mantine/core@7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@mantine/core/-/core-7.12.1.tgz" + integrity sha512-PXKIDaT1fpNB77dPQIcdFGM2NRnfmsJSVx3uuBccngBQWMIWI0wPyiO1Y26DK4LQrbrypeb+TS+Zxpgx6RoiCA== dependencies: "@floating-ui/react" "^0.26.9" clsx "^2.1.1" @@ -360,6 +360,13 @@ react-textarea-autosize "8.5.3" type-fest "^4.12.0" +"@mantine/dates@^7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@mantine/dates/-/dates-7.12.1.tgz" + integrity sha512-+Dg5ZGoYPWYRWPY7HagLeW36ayVjKQIkTpdNvgGDwh5YpaFy5cHd6LK6USKUshTsRPuzM3oUKwXIBK8hsigMyA== + dependencies: + clsx "^2.1.1" + "@mantine/form@^7.10.0": version "7.10.1" resolved "https://registry.npmjs.org/@mantine/form/-/form-7.10.1.tgz" @@ -368,10 +375,10 @@ fast-deep-equal "^3.1.3" klona "^2.0.6" -"@mantine/hooks@^7.9.2", "@mantine/hooks@7.10.1": - version "7.10.1" - resolved "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.10.1.tgz" - integrity sha512-0EH9WBWUdtQLGU3Ak+csQ77EtUxI6pPNfwZdRJQWcaA3f8SFOLo9h9CGxiikFExerhvuCeUlaTf3s+TB9Op/rw== +"@mantine/hooks@^7.9.2", "@mantine/hooks@7.12.1": + version "7.12.1" + resolved "https://registry.npmjs.org/@mantine/hooks/-/hooks-7.12.1.tgz" + integrity sha512-YPA3qiMHJkWID5+YzakBaLvjHtX3Fg3PdPY49iIb/CaWM9+lrJ+77TOVS7bsY7ZTBHXUfzft1/6Woqt3xSuweA== "@nodelib/fs.scandir@2.1.5": version "2.1.5" @@ -899,6 +906,11 @@ date-fns@^3.6.0, date-fns@>=2.0.0: resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz" integrity sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww== +dayjs@>=1.0.0: + version "1.11.13" + resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz" + integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== + debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4, debug@^4.3.5: version "4.3.5" resolved "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz" @@ -1479,9 +1491,9 @@ merge2@^1.3.0, merge2@^1.4.1: integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== micromatch@^4.0.4: - version "4.0.7" - resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz" - integrity sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q== + version "4.0.8" + resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== dependencies: braces "^3.0.3" picomatch "^2.3.1"