diff --git a/client/package-lock.json b/client/package-lock.json index bc8c134a..88370a59 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "client", - "version": "1.0.4", + "version": "1.0.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "client", - "version": "1.0.4", + "version": "1.0.5", "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.2.1", "@fortawesome/free-solid-svg-icons": "^6.2.1", diff --git a/client/package.json b/client/package.json index 85a41fc6..a7887751 100644 --- a/client/package.json +++ b/client/package.json @@ -1,6 +1,6 @@ { "name": "client", - "version": "1.0.4", + "version": "1.0.5", "scripts": { "dev": "vite", "build": "vite build", diff --git a/client/public/locales/de.json b/client/public/locales/de.json index f2039897..88b06e6e 100644 --- a/client/public/locales/de.json +++ b/client/public/locales/de.json @@ -7,20 +7,26 @@ "update": "Aktualisieren", "close": "Schließen", "unset": "Entfernen", + "retry": "Erneut versuchen", + "login": "Anmelden", "password": { "title": "Passwort erforderlich", "placeholder": "Dein Passwort", "wrong": "Das von dir eingegebene Passwort ist falsch" }, "accept": { - "title": "Nutzungsbedingungen akzeptieren", - "description": "Mit dem Klick auf Akzeptieren bestätigst du, dass du die EULA, Datenschutzerklärung und Nutzungsbedingungen von Ookla gelesen hast und diesen zustimmst.", + "title": "Wir brauchen deine Genehmigung", + "description": "Wir nutzen Dienste von Ookla. Mit dem Klick auf Akzeptieren bestätigst du, dass du die EULA, Datenschutzerklärung und Nutzungsbedingungen von Ookla gelesen hast und diesen zustimmst.", "button": "Akzeptieren" + }, + "api": { + "title": "API nicht erreichbar", + "description": "MySpeed konnte die API dieser Instanz nicht erreichen. Bitte versuche es später erneut." } }, "dropdown": { "settings": "Einstellungen", - "changes_applied": "Deine Änderungen wurden übernommen.", + "changes_applied": "Die Änderungen wurden gespeichert.", "changes_unsaved": "Deine Änderungen wurden nicht übernommen. Überprüfe deine Eingabe.", "invalid": "Eingabe ungültig", "ping": "Optimaler Ping", @@ -36,7 +42,7 @@ "resume_tests": "Tests fortsetzen", "healthchecks": "Healthchecks", "language": "Sprache ändern", - "info": "Info" + "info": "Infos zum Projekt" }, "options": { "time": { @@ -55,6 +61,10 @@ "export": { "json": "JSON-Datei", "csv": "CSV-Datei" + }, + "level": { + "no_access": "Kein Zugriff", + "read_access": "Nur Lesezugriff" } }, "update": { @@ -64,7 +74,7 @@ "upload_placeholder": "Up-Speed (Mbit/s)", "download_title": "Optimalen Down-Speed setzen (Mbit/s)", "download_placeholder": "Down-Speed (Mbit/s)", - "recommendations_title": "Automatische Empfehlungen", + "recommendations_title": "Optimale Empfehlungen", "recommendations_set": "Automatische Empfehlungen setzen?", "server_title": "Speedtest-Server setzen", "manually": "Manuell festlegen", @@ -73,6 +83,8 @@ "new_password": "Neues Passwort festlegen", "password_placeholder": "Neues Passwort", "password_removed": "Die Passwortsperre wurde aufgehoben und das festgelegte Passwort wurde entfernt.", + "level": "Stufe ändern", + "level_title": "Rechte für Besucher", "cron_title": "Test-Häufigkeit einstellen", "cron_rules": "Cron-Regel", "cron_next_test": "Nächster Test:", @@ -94,7 +106,8 @@ "start_tooltip": "Speedtest starten", "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 gedulde dich ein wenig, bis dieser fertig ist." + "running": "Es läuft bereits ein Speedtest. Bitte warte noch einen Moment.", + "admin_login": "Admin-Login" }, "latest": { "ping": "Ping", @@ -125,7 +138,7 @@ }, "latest": { "title": "Letzter Test", - "description": "Dies ist die Zeit, die dir zeigt, wann der letzte Test ausgeführt wurde. In diesem Fall wurde der letzte Test am {{date}} um {{time}} ausgeführt." + "description": "Dies ist die Zeit, die dir zeigt, wann der letzte Test ausgeführt wurde. In diesem Fall fand der letzte am {{date}} um {{time}} statt." } }, "time": { @@ -142,7 +155,7 @@ "not_available": "Es liegen aktuell keine Tests vor", "unknown_error": "Unbekannter Fehler:", "failed": "Test fehlgeschlagen", - "recheck": "Bitte überprüfe weitestgehend, ob das öfters passiert.", + "recheck": "Bitte überprüfe weitestgehend, ob das öfter passiert.", "delete": "Test löschen", "average": { "title": "Durchschnittsgeschwindigkeit", @@ -161,13 +174,13 @@ } }, "errors": { - "network_unreachable": "Die Internetverbindung scheint unterbrochen gewesen zu sein", + "network_unreachable": "Die Internetverbindung war in der Zeit des Tests instabil", "took_too_long": "Der Test hat zu lange gedauert und wurde abgebrochen", "no_permission": "MySpeed hat keine Berechtigung, diesen Test zu starten", - "resource_unavailable": "Der Test konnte nicht durchgeführt werden, da die Ressource vorübergehend nicht verfügbar ist", - "no_route": "Der Test konnte nicht durchgeführt werden, da keine Route zum Host existiert", + "resource_unavailable": "Der Test konnte nicht durchgeführt werden, da die Ressource vorübergehend nicht verfügbar war", + "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 scheint unterbrochen gewesen zu sein", + "timed_out": "Die Internetverbindung war in der Zeit des Tests instabil", "config": "Die Konfigurationsdatei konnte nicht geladen werden" } } \ No newline at end of file diff --git a/client/public/locales/en.json b/client/public/locales/en.json index 1cabbf91..f4452953 100644 --- a/client/public/locales/en.json +++ b/client/public/locales/en.json @@ -7,20 +7,26 @@ "update": "Update", "close": "Close", "unset": "Remove", + "retry": "Try again", + "login": "Login", "password": { "title": "Password required", "placeholder": "Your password", "wrong": "The password you entered is incorrect" }, "accept": { - "title": "Accept Terms", - "description": "By clicking Accept, you acknowledge that you have read and agree to Ookla's EULA, Privacy Statement and Terms of Use.", + "title": "We need your permission", + "description": "We use services from Ookla. By clicking Accept, you acknowledge that you have read and agree to Ookla's EULA, Privacy Statement and Terms of Use.", "button": "Accept" + }, + "api": { + "title": "API not reachable", + "description": "MySpeed could not reach the API of this instance. Please try again later." } }, "dropdown": { "settings": "Settings", - "changes_applied": "Your changes have been applied.", + "changes_applied": "Your changes have been saved.", "changes_unsaved": "Your changes were not applied. Check your input.", "invalid": "Input invalid", "ping": "Optimal ping", @@ -36,7 +42,7 @@ "resume_tests": "Resume tests", "healthchecks": "Healthchecks", "language": "Change language", - "info": "Info" + "info": "About the project" }, "options": { "time": { @@ -55,6 +61,10 @@ "export": { "json": "JSON file", "csv": "CSV file" + }, + "level": { + "no_access": "No Access", + "read_access": "Read-only Access" } }, "update": { @@ -64,7 +74,7 @@ "upload_placeholder": "Up speed (Mbps)", "download_title": "Set optimal down-speed (Mbps)", "download_placeholder": "Down speed (Mbps)", - "recommendations_title": "Automatic recommendations", + "recommendations_title": "Optimal recommendations", "recommendations_set": "Set automatic recommendations?", "server_title": "Set speedtest server", "manually": "Set manually", @@ -73,6 +83,8 @@ "new_password": "Set a new password", "password_placeholder": "New password", "password_removed": "The password lock has been removed and the set password has been removed.", + "level": "Change level", + "level_title": "Rights for visitors", "cron_title": "Set test frequency", "cron_rules": "Cron rule", "cron_next_test": "Next Test:", @@ -94,7 +106,8 @@ "start_tooltip": "Start speedtest", "new_update": "Update available", "paused": "Speedtests are currently paused. Please continue them if you want to do one.", - "running": "A speed test is already running. Please be patient until it is finished." + "running": "A speedtest is already running. Please wait a moment.", + "admin_login": "Admin Login" }, "latest": { "ping": "Ping", @@ -109,7 +122,7 @@ "healthchecks": "MySpeed uses HealthChecks to notify you when your internet is down. To enable this, put your ping URL in the text box. Read more here", "credits": "MySpeed is provided by GNMYT and uses the Speedtest CLI from Ookla.", "recommendations_error": "You have to do at least 10 tests to get an average. It doesn't matter if the tests were done manually or automatically.", - "recommendations_info": "Based on the last 10 test results, it was found that the optimal ping was {{ping}} ms, the download at {{down}} Mbit/s and the upload at {{up}} Mbit/s. It is best to orientate yourself on your internet contract and only adopt it if it matches that.", + "recommendations_info": "Based on the last 10 tests, it was found that the optimal ping was {{ping}} ms, the download at {{down}} Mbit/s and the upload at {{up}} Mbit/s. It is best to orientate yourself on your internet contract and only adopt it if it matches that.", "update": "An update to version {{version}} is available. See the changes and download the update.", "down": { "title": "Download speed", @@ -125,7 +138,7 @@ }, "latest": { "title": "Last test", - "description": "This is the time that shows you when the last test was run. In this case, the last test was run on {{date}} at {{time}}." + "description": "This is the time that shows you when the last test was run. In this case, the last one took place on {{date}} at {{time}}." } }, "time": { @@ -161,13 +174,13 @@ } }, "errors": { - "network_unreachable": "The Internet connection seems to have been interrupted", + "network_unreachable": "Internet connection was unstable during the time of the test", "took_too_long": "The test took too long and was canceled", "no_permission": "MySpeed has no permission to start this test", - "resource_unavailable": "The test could not be performed because the resource is temporarily unavailable", - "no_route": "The test could not be performed because there is no route to the host", + "resource_unavailable": "The test could not be performed because the resource was temporarily unavailable", + "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": "The Internet connection seems to have been interrupted", + "timed_out": "Internet connection was unstable during the time of the test", "config": "The configuration file could not be loaded" } } \ No newline at end of file diff --git a/client/src/App.sass b/client/src/App.sass index 19762a0d..adf121e2 100644 --- a/client/src/App.sass +++ b/client/src/App.sass @@ -59,11 +59,19 @@ hr @keyframes fadeIn 0% opacity: 0 - transform: scale(1.6) + transform: scale(0.4) filter: blur(5px) 100% opacity: 1 +@keyframes fadeOut + 0% + opacity: 1 + 100% + opacity: 0 + transform: scale(0.4) + filter: blur(5px) + @media (max-width: 730px) ::-webkit-scrollbar width: 5px \ No newline at end of file diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx index e4b4d470..56e9c390 100644 --- a/client/src/common/components/Dropdown/DropdownComponent.jsx +++ b/client/src/common/components/Dropdown/DropdownComponent.jsx @@ -11,7 +11,7 @@ import {DialogContext} from "@/common/contexts/Dialog"; 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, 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"; @@ -63,13 +63,12 @@ function DropdownComponent() { }); const patchDialog = async (key, dialog, toggle = true, postValue = (val) => val) => { - if (toggle) toggleDropdown(); + toggle ? toggleDropdown() : setDialog(); - setDialog({ - ...(await dialog(config[key])), + setTimeout(async () => setDialog({...(await dialog(config[key])), onSuccess: value => patchRequest(`/config/${key}`, {value: postValue(value)}) .then(res => showFeedback(!res.ok ? t("dropdown.changes_unsaved") : undefined)) - }) + }), 160); } const updatePing = async () => patchDialog("ping", (value) => ({ @@ -119,7 +118,7 @@ function DropdownComponent() { const updatePassword = async () => { toggleDropdown(); setDialog({ - title: t("update.new_password"), + title: <>{t("update.new_password")} » {t("update.level")}, placeholder: t("update.password_placeholder"), type: "password", unsetButton: localStorage.getItem("password") != null ? "Sperre aufheben" : undefined, @@ -132,6 +131,10 @@ function DropdownComponent() { }) } + const updatePasswordLevel = () => patchDialog("passwordLevel", async (value) => ({ + title: t("update.level_title"), select: true, selectOptions: levelOptions(), value + }), false); + const updateCron = async () => { toggleDropdown(); setDialog({ @@ -200,7 +203,7 @@ function DropdownComponent() { placeholder: t("update.healthchecks_url"), value, buttonText: t("dialog.update"), unsetButton: !value.includes("") ? "Deaktivieren" : undefined, - onClear: () => patchDialog("/config/healthChecksUrl", {value: "https://hc-ping.com/"}) + onClear: () => patchRequest("/config/healthChecksUrl", {value: "https://hc-ping.com/"}) .then(() => showFeedback(t("update.healthchecks_activated"))) })); @@ -235,12 +238,12 @@ function DropdownComponent() { {run: updateServer, icon: faServer, text: t("dropdown.server")}, {run: updatePassword, icon: faKey, text: t("dropdown.password")}, {run: updateCron, icon: faClock, text: t("dropdown.cron")}, - {run: updateTime, icon: faCalendarDays, text: t("dropdown.time")}, + {run: updateTime, icon: faCalendarDays, text: t("dropdown.time"), allowView: true}, {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")}, - {run: updateLanguage, icon: faGlobeEurope, text: t("dropdown.language")}, - {run: showCredits, icon: faInfo, text: t("dropdown.info")} + {run: updateLanguage, icon: faGlobeEurope, text: t("dropdown.language"), allowView: true}, + {run: showCredits, icon: faInfo, text: t("dropdown.info"), allowView: true} ]; return ( @@ -248,14 +251,16 @@ function DropdownComponent() {

{t("dropdown.settings")}

- {options.map(entry => !entry.hr ? ( -
- -

{entry.text}

-
- ) :
-
-
)} + {options.map(entry => { + if (!config.viewMode || (config.viewMode && entry.allowView)) { + if (!entry.hr) { + return (
+ +

{entry.text}

+
); + } else return (

); + } + })}
diff --git a/client/src/common/components/Dropdown/styles.sass b/client/src/common/components/Dropdown/styles.sass index 1ce2b4f9..7486aa34 100644 --- a/client/src/common/components/Dropdown/styles.sass +++ b/client/src/common/components/Dropdown/styles.sass @@ -1,11 +1,17 @@ @import "@/common/styles/colors" +.dropdown + visibility: visible + opacity: 1 + transition: opacity 0.1s linear + user-select: none + .dropdown-content float: right margin-right: 10% display: inline-block position: absolute - width: 320px + width: auto overflow: auto border-radius: 10px box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2) @@ -16,6 +22,8 @@ .dropdown-invisible visibility: hidden + opacity: 0 + transition: visibility 0s 0.1s, opacity 0.1s linear .dropdown-content h2 color: $darker-white diff --git a/client/src/common/components/Dropdown/utils/options.js b/client/src/common/components/Dropdown/utils/options.js index 32d9299d..6f6019fd 100644 --- a/client/src/common/components/Dropdown/utils/options.js +++ b/client/src/common/components/Dropdown/utils/options.js @@ -7,6 +7,11 @@ export const timeOptions = () => ({ 4: t("options.time.30days") }); +export const levelOptions = () => ({ + "none": t("options.level.no_access"), + "read": t("options.level.read_access") +}); + export const selectOptions = () => ({ "* * * * *": t("options.cron.continuous"), "0,30 * * * *": t("options.cron.frequent"), diff --git a/client/src/common/components/Header/HeaderComponent.jsx b/client/src/common/components/Header/HeaderComponent.jsx index b81f3f20..9fffe45d 100644 --- a/client/src/common/components/Header/HeaderComponent.jsx +++ b/client/src/common/components/Header/HeaderComponent.jsx @@ -1,6 +1,6 @@ import "./styles.sass"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCircleArrowUp, faGaugeHigh, faGear} from "@fortawesome/free-solid-svg-icons"; +import {faCircleArrowUp, faGaugeHigh, faGear, faLock} from "@fortawesome/free-solid-svg-icons"; import {useContext, useEffect, useState} from "react"; import DropdownComponent, {toggleDropdown} from "../Dropdown/DropdownComponent"; import {DialogContext} from "@/common/contexts/Dialog"; @@ -9,18 +9,36 @@ import {SpeedtestContext} from "@/common/contexts/Speedtests"; import {jsonRequest, postRequest} from "@/common/utils/RequestUtil"; import {updateInfo} from "@/common/components/Header/utils/infos"; import {t} from "i18next"; +import {ConfigContext} from "@/common/contexts/Config"; function HeaderComponent() { const [setDialog] = useContext(DialogContext); const [icon, setIcon] = useState(faGear); const [status, updateStatus] = useContext(StatusContext); const updateTests = useContext(SpeedtestContext)[1]; + const [config, reloadConfig, checkConfig] = useContext(ConfigContext); const [updateAvailable, setUpdateAvailable] = useState(""); function switchDropdown() { toggleDropdown(setIcon); } + const showPasswordDialog = () => setDialog({ + title: t("header.admin_login"), + placeholder: t("dialog.password.placeholder"), + description: localStorage.getItem("password") ? {t("dialog.password.wrong")} : "", + type: "password", + buttonText: t("dialog.login"), + onSuccess: (value) => { + localStorage.setItem("password", value); + reloadConfig(); + checkConfig().then((config) => config?.viewMode ? showPasswordDialog() : false).catch(() => showPasswordDialog()); + }, + onClose: () => { + localStorage.removeItem("password"); + } + }); + const startSpeedtest = async () => { await updateStatus(); if (status.paused) return setDialog({ @@ -41,6 +59,7 @@ function HeaderComponent() { } useEffect(() => { + if (Object.keys(config).length === 0) return; async function updateVersion() { const version = await jsonRequest("/info/version"); @@ -48,8 +67,8 @@ function HeaderComponent() { setUpdateAvailable(version.remote); } - updateVersion(); - }, []); + if (!config.viewMode) updateVersion(); + }, [config]); return (
@@ -64,13 +83,17 @@ function HeaderComponent() { description: updateInfo(updateAvailable) })}/> : <>} - {!status.paused ?
+ {!(status.paused || config.viewMode) ?
{t("header." + (status.running ? "running_tooltip" : "start_tooltip"))}
: <>} + {(config.viewMode ?
+ + {t("header.admin_login")} +
: <>)}
diff --git a/client/src/common/contexts/Config/ConfigContext.jsx b/client/src/common/contexts/Config/ConfigContext.jsx index 440415da..20c44307 100644 --- a/client/src/common/contexts/Config/ConfigContext.jsx +++ b/client/src/common/contexts/Config/ConfigContext.jsx @@ -1,7 +1,7 @@ -import React, {useState, createContext, useEffect, useContext} from "react"; +import React, {createContext, useContext, useEffect, useState} from "react"; import {DialogContext} from "../Dialog"; import {request} from "@/common/utils/RequestUtil"; -import {acceptDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog"; +import {acceptDialog, apiErrorDialog, passwordRequiredDialog} from "@/common/contexts/Config/dialog"; export const ConfigContext = createContext({}); @@ -11,14 +11,22 @@ export const ConfigProvider = (props) => { const [setDialog] = useContext(DialogContext); const reloadConfig = () => { - request("/config").then(res => { - if (!res.ok) throw "No connection to server"; - return res.json(); + request("/config").then(async res => { + if (res.status === 401) throw 1; + if (!res.ok) throw 2; + + try { + return JSON.parse(await res.text()); + } catch (e) { + throw 2; + } }) .then(result => setConfig(result)) - .catch(() => setDialog(passwordRequiredDialog())); + .catch((code) => setDialog(code === 1 ? passwordRequiredDialog() : apiErrorDialog())); } + const checkConfig = async () => (await request("/config")).json(); + useEffect(() => { if (config.acceptOoklaLicense !== undefined && config.acceptOoklaLicense === "false") setDialog(acceptDialog()); @@ -27,7 +35,7 @@ export const ConfigProvider = (props) => { useEffect(reloadConfig, []); return ( - + {props.children} ) diff --git a/client/src/common/contexts/Config/dialog.jsx b/client/src/common/contexts/Config/dialog.jsx index a6934b39..168d80a1 100644 --- a/client/src/common/contexts/Config/dialog.jsx +++ b/client/src/common/contexts/Config/dialog.jsx @@ -12,7 +12,7 @@ export const passwordRequiredDialog = () => ({ placeholder: t("dialog.password.placeholder"), description: localStorage.getItem("password") ? {t("dialog.password.wrong")} : "", type: "password", - buttonText: t("dialog.done"), + buttonText: t("dialog.login"), disableCloseButton: true, onSuccess: (value) => { localStorage.setItem("password", value); @@ -20,6 +20,14 @@ export const passwordRequiredDialog = () => ({ } }); +export const apiErrorDialog = () => ({ + title: t("dialog.api.title"), + description: {t("dialog.api.description")}, + buttonText: t("dialog.retry"), + disableCloseButton: true, + onSuccess: () => window.location.reload() +}); + export const acceptDialog = () => ({ title: t("dialog.accept.title"), description: , EULA: , diff --git a/client/src/common/contexts/Dialog/DialogContext.jsx b/client/src/common/contexts/Dialog/DialogContext.jsx index dfafd5f8..03a385ad 100644 --- a/client/src/common/contexts/Dialog/DialogContext.jsx +++ b/client/src/common/contexts/Dialog/DialogContext.jsx @@ -7,7 +7,10 @@ import {t} from "i18next"; export const DialogContext = createContext({}); const Dialog = ({dialog, setDialog}) => { + if (!dialog) return; + const [value, setValue] = useState(dialog.value || ""); + const ref = useRef(); document.onkeyup = e => { @@ -27,24 +30,30 @@ const Dialog = ({dialog, setDialog}) => { setValue(e.target.value); } + function closeSlow() { + if (ref.current == null) return; + ref.current.classList.add("dialog-hidden"); + setTimeout(() => setDialog(), 150); + } + function closeDialog() { - setDialog(); + closeSlow(); if (dialog.onClose) dialog.onClose(); } function submit() { if (!dialog.description && !value) return; - setDialog(); - if (dialog.onSuccess) dialog.onSuccess(value); + closeSlow(); + if (dialog.onSuccess) setTimeout(() => dialog.onSuccess(value), 160); } function clear() { - setDialog(); - if (dialog.onClear) dialog.onClear(); + closeSlow(); + if (dialog.onClear) setTimeout(() => dialog.onClear(), 160); } if (dialog.speedtest) return ( -
+
@@ -96,7 +105,7 @@ export const DialogProvider = (props) => { return ( - {dialog ? : <>} + {props.children} ) diff --git a/client/src/common/contexts/Dialog/styles.sass b/client/src/common/contexts/Dialog/styles.sass index 43f0cc45..39a34cec 100644 --- a/client/src/common/contexts/Dialog/styles.sass +++ b/client/src/common/contexts/Dialog/styles.sass @@ -15,13 +15,18 @@ justify-content: center backdrop-filter: blur(2px) +.dialog-hidden + visibility: hidden + opacity: 0 + animation: fadeOut 0.3s + .dialog width: 480px padding: 15px background-color: $dark-gray border-radius: 15px transition: all 0.2s - animation: fadeIn 0.2s + animation: fadeIn 0.3s .dialog-speedtest display: flex diff --git a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx index 6141405b..cf1edd0f 100644 --- a/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx +++ b/client/src/pages/Home/components/LatestTest/LatestTestComponent.jsx @@ -14,7 +14,7 @@ import {t} from "i18next"; function LatestTestComponent() { const status = useContext(StatusContext)[0]; const [latest, setLatest] = useState({}); - const [latestTestTime, setLatestTestTime] = useState("-"); + const [latestTestTime, setLatestTestTime] = useState("N/A"); const [setDialog] = useContext(DialogContext); const [speedtests] = useContext(SpeedtestContext); const config = useContext(ConfigContext)[0]; @@ -41,7 +41,7 @@ function LatestTestComponent() {

{t("latest.ping")}{t("latest.ping_unit")}

-

{latest.ping === -1 ? "-" : latest.ping}

+

{latest.ping === -1 ? "N/A" : latest.ping}

@@ -53,7 +53,7 @@ function LatestTestComponent() {

{t("latest.down")}{t("latest.speed_unit")}

-

{latest.download === -1 ? "-" : latest.download}

+

{latest.download === -1 ? "N/A" : latest.download}

@@ -67,7 +67,7 @@ function LatestTestComponent() {

{t("latest.up")}{t("latest.speed_unit")}

-

{latest.upload === -1 ? "-" : latest.upload}

+

{latest.upload === -1 ? "N/A" : latest.upload}

diff --git a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx index 7d935c3c..5fbf1450 100644 --- a/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx +++ b/client/src/pages/Home/components/Speedtest/SpeedtestComponent.jsx @@ -1,4 +1,4 @@ -import React, {useContext} from "react"; +import React, {useContext, useRef} from "react"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import { faArrowDown, faArrowUp, faClockRotateLeft, faClose, @@ -12,11 +12,15 @@ import {averageResultDialog, resultDialog} from "@/pages/Home/components/Speedte import {errors} from "@/pages/Home/components/Speedtest/utils/errors"; import {tooltips} from "@/pages/Home/components/Speedtest/utils/tooltips"; import {t} from "i18next"; +import {ConfigContext} from "@/common/contexts/Config"; function SpeedtestComponent(props) { const [setDialog] = useContext(DialogContext); + const [config] = useContext(ConfigContext); const updateTests = useContext(SpeedtestContext)[1]; + const ref = useRef(); + let errorMessage = t("test.unknown_error") + " " + props.error; let isAverage = props.type === "average"; @@ -33,9 +37,15 @@ function SpeedtestComponent(props) { description: errorMessage + ". " + t("test.recheck"), buttonText: t("dialog.okay"), unsetButton: t("test.delete"), - onClear: () => deleteRequest(`/speedtests/${props.id}`).then(updateTests) + onClear: () => deleteRequest(`/speedtests/${props.id}`).then(fadeOut) }); + const fadeOut = () => { + if (ref.current == null) return; + ref.current.classList.add("speedtest-hidden"); + setTimeout(() => updateTests(), 300); + } + const showInfoDialog = () => { if (props.type === "average") { setDialog({title: t("test.average.title"), buttonText: t("dialog.okay"), description: averageResultDialog(timeString, props)}); @@ -44,14 +54,14 @@ function SpeedtestComponent(props) { title: t("test.result.title"), description: resultDialog(props), buttonText: t("dialog.okay"), - unsetButton: t("test.delete"), - onClear: () => deleteRequest(`/speedtests/${props.id}`).then(updateTests) + unsetButton: !config.viewMode ? t("test.delete") : undefined, + onClear: () => deleteRequest(`/speedtests/${props.id}`).then(fadeOut) }); } } return ( -
+