diff --git a/client/package-lock.json b/client/package-lock.json index ccc4bc54..602b2b03 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -12,11 +12,13 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@vitejs/plugin-react": "^3.1.0", + "chart.js": "^4.2.1", "cron-parser": "^4.7.1", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.1.1", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", "react-i18next": "^12.1.5", "sass": "^1.58.3", @@ -2020,6 +2022,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2497,6 +2504,17 @@ "node": ">=8" } }, + "node_modules/chart.js": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz", + "integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==", + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": "^7.0.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -3810,6 +3828,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", @@ -6063,6 +6090,11 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, + "@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==" + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -6405,6 +6437,14 @@ } } }, + "chart.js": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.2.1.tgz", + "integrity": "sha512-6YbpQ0nt3NovAgOzbkSSeeAQu/3za1319dPUQTXn9WcOpywM8rGKxJHrhS8V8xEkAlk8YhEfjbuAPfUyp6jIsw==", + "requires": { + "@kurkle/color": "^0.3.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -7340,6 +7380,12 @@ "loose-envify": "^1.1.0" } }, + "react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "requires": {} + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", diff --git a/client/package.json b/client/package.json index 32b67848..14ee29ed 100644 --- a/client/package.json +++ b/client/package.json @@ -11,13 +11,15 @@ "@fortawesome/free-solid-svg-icons": "^6.3.0", "@fortawesome/react-fontawesome": "^0.2.0", "@vitejs/plugin-react": "^3.1.0", + "chart.js": "^4.2.1", "cron-parser": "^4.7.1", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.1.1", - "react-i18next": "^12.1.5", "react": "^18.2.0", + "react-chartjs-2": "^5.2.0", "react-dom": "^18.2.0", + "react-i18next": "^12.1.5", "sass": "^1.58.3", "vite": "^4.1.2", "vite-plugin-pwa": "^0.14.4" diff --git a/client/public/locales/de.json b/client/public/locales/de.json index 7b06407b..ba52cf97 100644 --- a/client/public/locales/de.json +++ b/client/public/locales/de.json @@ -42,6 +42,7 @@ "resume_tests": "Tests fortsetzen", "healthchecks": "Healthchecks", "language": "Sprache ändern", + "view": "Ansicht wechseln", "info": "Infos zum Projekt" }, "options": { @@ -98,7 +99,8 @@ "healthchecks": "HealthChecks Integration", "healthchecks_url": "HealthChecks Server URL", "healthchecks_activated": "Die Healthchecks wurden deaktiviert", - "language": "Sprache ändern" + "language": "Sprache ändern", + "view_title": "Ansicht wechseln" }, "header": { "title": "Netzwerkanalyse", @@ -107,7 +109,11 @@ "new_update": "Update verfügbar", "paused": "Speedtests sind aktuell pausiert. Bitte setze sie fort, wenn du einen machen möchtest.", "running": "Es läuft bereits ein Speedtest. Bitte warte noch einen Moment.", - "admin_login": "Admin-Login" + "admin_login": "Admin-Login", + "beta": { + "title": "Beta-Version", + "description": "Diese Funktion befindet sich noch in der Beta-Phase. Wenn du Fehler findest, melde sie bitte hier." + } }, "latest": { "ping": "Ping", @@ -171,6 +177,17 @@ "custom": "Benutzerdefiniert", "average": "Durchschnitt", "auto": "Automatisiert" + }, + "views": { + "list": "Test-Übersicht", + "statistic": "Test-Statistik" + }, + "overview": { + "title": "Test-Übersicht der letzten {{amount}}", + "1": "24 Stunden", + "2": "2 Tage", + "3": "7 Tage", + "4": "30 Tage" } }, "errors": { @@ -181,6 +198,42 @@ "no_route": "Der Test konnte nicht durchgeführt werden, da keine Route zum Host existierte", "connection_refused": "Der Test konnte nicht durchgeführt werden, da die Verbindung abgelehnt wurde", "timed_out": "Die Internetverbindung war in der Zeit des Tests instabil", - "config": "Die Konfigurationsdatei konnte nicht geladen werden" + "config": "Die Konfigurationsdatei konnte nicht geladen werden", + "invalid_view": "Ungültige Ansicht" + }, + "statistics": { + "overview": { + "total_title": "Insgesamte Tests", + "total_description": "Die Anzahl der ausgeführten Tests", + "failed_title": "Fehlgeschlagene Tests", + "failed_description": "Die Anzahl der fehlgeschlagenen Tests", + "average_title": "Durchschnittsdauer", + "average_description": "Die durchschnittliche Dauer der Tests" + }, + "failed": { + "title": "Fehlgeschlagene Tests", + "success": "Erfolgreich", + "failed": "Fehlgeschlagen", + "label": "Tests" + }, + "speed": { + "title": "Geschwindigkeit" + }, + "manual": { + "title": "Manuelle Tests", + "yes": "Manuell erstellt", + "no": "Automatisch erstellt" + }, + "duration": { + "title": "Testdauer", + "label": "Anzahl" + }, + "values": { + "min": "Mindestens", + "max": "Höchstens", + "avg": "Durchschnitt", + "down": "Download-Werte", + "up": "Upload-Werte" + } } } diff --git a/client/public/locales/en.json b/client/public/locales/en.json index f4452953..650383f5 100644 --- a/client/public/locales/en.json +++ b/client/public/locales/en.json @@ -42,6 +42,7 @@ "resume_tests": "Resume tests", "healthchecks": "Healthchecks", "language": "Change language", + "view": "Switch view", "info": "About the project" }, "options": { @@ -98,7 +99,8 @@ "healthchecks": "HealthChecks Integration", "healthchecks_url": "HealthChecks Server URL", "healthchecks_activated": "The health checks have been disabled", - "language": "Change language" + "language": "Change language", + "view_title": "Switch view" }, "header": { "title": "Network Analysis", @@ -107,7 +109,11 @@ "new_update": "Update available", "paused": "Speedtests are currently paused. Please continue them if you want to do one.", "running": "A speedtest is already running. Please wait a moment.", - "admin_login": "Admin Login" + "admin_login": "Admin Login", + "beta": { + "title": "Beta Version", + "description": "This feature is still in beta. If you find any bugs, please report them here." + } }, "latest": { "ping": "Ping", @@ -171,6 +177,17 @@ "custom": "Custom", "average": "Average", "auto": "Automated" + }, + "views": { + "list": "Test Overview", + "statistic": "Test statistics" + }, + "overview": { + "title": "Test overview of the last {{amount}}", + "1": "24 hours", + "2": "2 days", + "3": "7 days", + "4": "30 days" } }, "errors": { @@ -181,6 +198,42 @@ "no_route": "The test could not be performed because there was no route to the host", "connection_refused": "The test could not be performed because the connection was rejected", "timed_out": "Internet connection was unstable during the time of the test", - "config": "The configuration file could not be loaded" + "config": "The configuration file could not be loaded", + "invalid_view": "Invalid view" + }, + "statistics": { + "overview": { + "total_title": "Total tests", + "total_description": "The number of executed tests", + "failed_title": "Failed tests", + "failed_description": "The number of failed tests", + "average_title": "Average duration", + "average_description": "The average duration of the tests" + }, + "failed": { + "title": "Failed tests", + "success": "Successful", + "failed": "Failed", + "label": "Tests" + }, + "speed": { + "title": "Speed" + }, + "manual": { + "title": "Manual tests", + "yes": "Created manually", + "no": "Created automatically" + }, + "duration": { + "title": "Test duration", + "label": "Amount" + }, + "values": { + "min": "Minimum", + "max": "Maximum", + "avg": "Average", + "down": "Download values", + "up": "Upload values" + } } -} \ No newline at end of file +} diff --git a/client/src/App.jsx b/client/src/App.jsx index ce3b9305..ae26d563 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -5,11 +5,25 @@ import {SpeedtestProvider} from "./common/contexts/Speedtests"; import {ConfigProvider} from "./common/contexts/Config"; import {StatusProvider} from "./common/contexts/Status"; import {InputDialogProvider} from "@/common/contexts/InputDialog/InputDialog"; -import {useState} from "react"; +import {useContext, useState} from "react"; import i18n from './i18n'; import Loading from "@/pages/Loading"; import "@/common/styles/spinner.sass"; import Error from "@/pages/Error"; +import {ViewContext, ViewProvider} from "@/common/contexts/View"; +import Statistics from "@/pages/Statistics"; +import {t} from "i18next"; + +const MainContent = () => { + const [view] = useContext(ViewContext); + return ( +
+ {view === 0 && } + {view === 1 && } + {view !== 0 && view !== 1 && } +
+ ); +} const App = () => { const [translationsLoaded, setTranslationsLoaded] = useState(false); @@ -24,14 +38,14 @@ const App = () => { {translationError && } {translationsLoaded && !translationError && - - - -
- -
-
-
+ + + + + + + +
} diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx index 3f4d927a..f5ed4e56 100644 --- a/client/src/common/components/Dropdown/DropdownComponent.jsx +++ b/client/src/common/components/Dropdown/DropdownComponent.jsx @@ -1,8 +1,7 @@ -import React, {useContext, useEffect, useRef} from "react"; +import React, {useContext, useEffect, useRef, useState} from "react"; import "./styles.sass"; -import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { - faArrowDown, faArrowUp, faCalendarDays, faCircleNodes, faClock, faClose, faFileExport, + faArrowDown, faArrowUp, faCalendarDays, faCircleNodes, faClock, faClose, faFileExport, faChartSimple, faGear, faGlobeEurope, faInfo, faKey, faPause, faPingPongPaddleBall, faPlay, faServer, faWandMagicSparkles } from "@fortawesome/free-solid-svg-icons"; import {ConfigContext} from "@/common/contexts/Config"; @@ -11,9 +10,14 @@ import {InputDialogContext} from "@/common/contexts/InputDialog"; import {SpeedtestContext} from "@/common/contexts/Speedtests"; import {downloadRequest, jsonRequest, patchRequest, postRequest} from "@/common/utils/RequestUtil"; import {creditsInfo, healthChecksInfo, recommendationsInfo} from "@/common/components/Dropdown/utils/infos"; -import {exportOptions, languageOptions, levelOptions, selectOptions, timeOptions} from "@/common/components/Dropdown/utils/options"; +import { + exportOptions, languageOptions, levelOptions, + selectOptions, timeOptions +} from "@/common/components/Dropdown/utils/options"; import {parseCron, stringifyCron} from "@/common/components/Dropdown/utils/utils"; import {changeLanguage, t} from "i18next"; +import ViewDialog from "@/common/components/ViewDialog"; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; let icon; @@ -36,6 +40,7 @@ function DropdownComponent() { const [status, updateStatus] = useContext(StatusContext); const updateTests = useContext(SpeedtestContext)[1]; const [setDialog] = useContext(InputDialogContext); + const [showViewDialog, setShowViewDialog] = useState(false); const ref = useRef(); useEffect(() => { @@ -225,6 +230,11 @@ function DropdownComponent() { }); } + const updateView = () => { + toggleDropdown(); + setShowViewDialog(true); + } + const showCredits = () => { toggleDropdown(); setDialog({title: "MySpeed", description: creditsInfo(), buttonText: t("dialog.close")}); @@ -243,29 +253,36 @@ function DropdownComponent() { {run: exportDialog, icon: faFileExport, text: t("dropdown.export")}, {run: togglePause, icon: status.paused ? faPlay : faPause, text: t("dropdown." + (status.paused ? "resume_tests" : "pause_tests"))}, {run: updateIntegration, icon: faCircleNodes, text: t("dropdown.healthchecks")}, + {hr: true, key: 2}, {run: updateLanguage, icon: faGlobeEurope, text: t("dropdown.language"), allowView: true}, + {run: updateView, icon: faChartSimple, allowView: true, text: t("dropdown.view")}, {run: showCredits, icon: faInfo, text: t("dropdown.info"), allowView: true} ]; return ( -