diff --git a/client/public/locales/de.json b/client/public/locales/de.json index 62b33c2c..eb68ba0b 100644 --- a/client/public/locales/de.json +++ b/client/public/locales/de.json @@ -104,7 +104,7 @@ "view_title": "Ansicht wechseln" }, "header": { - "title": "Netzwerkanalyse", + "title": "MySpeed", "running_tooltip": "Speedtest läuft", "start_tooltip": "Speedtest starten", "new_update": "Update verfügbar", @@ -237,5 +237,34 @@ "down": "Download-Werte", "up": "Upload-Werte" } + }, + "nodes": { + "add": "Server hinzufügen", + "create": "Hinzufügen", + "this_server": "Dieser Server", + "created": "Der Server wurde erfolgreich hinzugefügt", + "password_required": "Für diese Node ist ein Passwort erforderlich", + "update_password": "Bitte aktualisiere das Passwort dieser Node", + "password_outdated": "Passwort veraltet", + "password_updated": "Das Passwort wurde erfolgreich aktualisiert", + "placeholder": { + "name": "MySpeed Instanz", + "url": "https://dein-server.de" + }, + "group": { + "name": "Servername", + "url": "Serveradresse" + }, + "delete": { + "title": "Server löschen", + "description": "Der Server {{name}} (#{{id}}) wird gelöscht. Diese Aktion kann nicht rückgängig gemacht werden. Möchtest du fortfahren?", + "yes": "Ja, löschen", + "success": "Der Server wurde erfolgreich gelöscht" + }, + "messages": { + "not_reachable": "Server ist nicht erreichbar", + "password_changed": "Passwort wurde geändert", + "tests_pending": "Testergebnisse ausstehend" + } } } diff --git a/client/public/locales/en.json b/client/public/locales/en.json index e342de07..c246b682 100644 --- a/client/public/locales/en.json +++ b/client/public/locales/en.json @@ -104,7 +104,7 @@ "view_title": "Switch view" }, "header": { - "title": "Network Analysis", + "title": "MySpeed", "running_tooltip": "Speedtest running", "start_tooltip": "Start speedtest", "new_update": "Update available", @@ -237,5 +237,34 @@ "down": "Download values", "up": "Upload values" } + }, + "nodes": { + "add": "Add server", + "create": "Add", + "this_server": "This server", + "created": "The server has been successfully added", + "password_required": "This node requires a password", + "update_password": "Please update the password of this node", + "password_outdated": "Password outdated", + "password_updated": "The password has been successfully updated", + "placeholder": { + "name": "MySpeed instance", + "url": "https://your-server.com" + }, + "group": { + "name": "Server name", + "url": "Server address" + }, + "delete": { + "title": "Delete server", + "description": "The server {{name}} (#{{id}}) will be deleted. This action cannot be undone. Do you want to continue?", + "yes": "Yes, delete", + "success": "The server has been deleted successfully" + }, + "messages": { + "not_reachable": "Server is not reachable", + "password_changed": "Password changed", + "tests_pending": "Test results pending" + } } } diff --git a/client/src/App.jsx b/client/src/App.jsx index 08749b3b..338e5228 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -14,6 +14,8 @@ import {ViewContext, ViewProvider} from "@/common/contexts/View"; import Statistics from "@/pages/Statistics"; import {t} from "i18next"; import {ToastNotificationProvider} from "@/common/contexts/ToastNotification"; +import Nodes from "@/pages/Nodes"; +import {NodeProvider} from "@/common/contexts/Node"; const MainContent = () => { const [view] = useContext(ViewContext); @@ -30,6 +32,8 @@ const App = () => { const [translationsLoaded, setTranslationsLoaded] = useState(false); const [translationError, setTranslationError] = useState(false); + const [showNodePage, setShowNodePage] = useState(false); + i18n.on("initialized", () => setTranslationsLoaded(true)); i18n.on("failedLoading", () => setTranslationError(true)); @@ -37,20 +41,24 @@ const App = () => { <> {!translationsLoaded && !translationError && } {translationError && } - {translationsLoaded && !translationError && + - - - - - - - - - - + + + {translationsLoaded && !translationError && showNodePage && + } + {translationsLoaded && !translationError && !showNodePage && + + + + + + + } + + - } + ); } diff --git a/client/src/common/components/Dropdown/DropdownComponent.jsx b/client/src/common/components/Dropdown/DropdownComponent.jsx index 7033f5f5..8cc7621c 100644 --- a/client/src/common/components/Dropdown/DropdownComponent.jsx +++ b/client/src/common/components/Dropdown/DropdownComponent.jsx @@ -25,7 +25,7 @@ import {ConfigContext} from "@/common/contexts/Config"; import {StatusContext} from "@/common/contexts/Status"; import {InputDialogContext} from "@/common/contexts/InputDialog"; import {SpeedtestContext} from "@/common/contexts/Speedtests"; -import {downloadRequest, jsonRequest, patchRequest, postRequest} from "@/common/utils/RequestUtil"; +import {baseRequest, downloadRequest, jsonRequest, patchRequest, postRequest} from "@/common/utils/RequestUtil"; import {creditsInfo, healthChecksInfo, recommendationsInfo} from "@/common/components/Dropdown/utils/infos"; import { exportOptions, languageOptions, levelOptions, @@ -36,6 +36,7 @@ import {changeLanguage, t} from "i18next"; import ViewDialog from "@/common/components/ViewDialog"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {ToastNotificationContext} from "@/common/contexts/ToastNotification"; +import {NodeContext} from "@/common/contexts/Node"; let icon; @@ -56,6 +57,7 @@ export const toggleDropdown = (setIcon) => { function DropdownComponent() { const [config, reloadConfig] = useContext(ConfigContext); const [status, updateStatus] = useContext(StatusContext); + const currentNode = useContext(NodeContext)[2]; const updateTests = useContext(SpeedtestContext)[1]; const updateToast = useContext(ToastNotificationContext); const [setDialog] = useContext(InputDialogContext); @@ -153,7 +155,10 @@ function DropdownComponent() { .then(() => localStorage.removeItem("password")), onSuccess: (value) => patchRequest("/config/password", {value}) .then(() => showFeedback(undefined, false)) - .then(() => localStorage.setItem("password", value)) + .then(() => { + currentNode !== 0 ? baseRequest("/nodes/" + currentNode + "/password", "PATCH", + {password: value}) : localStorage.setItem("password", value); + }) }) } diff --git a/client/src/common/components/Header/HeaderComponent.jsx b/client/src/common/components/Header/HeaderComponent.jsx index f80ab56f..d99d168b 100644 --- a/client/src/common/components/Header/HeaderComponent.jsx +++ b/client/src/common/components/Header/HeaderComponent.jsx @@ -1,6 +1,12 @@ import "./styles.sass"; import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; -import {faCircleArrowUp, faGaugeHigh, faGear, faLock} from "@fortawesome/free-solid-svg-icons"; +import { + faCircleArrowUp, + faGaugeHigh, + faGear, + faLock, + faServer +} from "@fortawesome/free-solid-svg-icons"; import {useContext, useEffect, useState} from "react"; import DropdownComponent, {toggleDropdown} from "../Dropdown/DropdownComponent"; import {InputDialogContext} from "@/common/contexts/InputDialog"; @@ -11,11 +17,12 @@ import {updateInfo} from "@/common/components/Header/utils/infos"; import {t} from "i18next"; import {ConfigContext} from "@/common/contexts/Config"; import {SpeedtestDialog} from "@/common/components/SpeedtestDialog"; -import {ViewContext} from "@/common/contexts/View"; -import {Trans} from "react-i18next"; -import {PROJECT_URL} from "@/index"; +import {NodeContext} from "@/common/contexts/Node"; + +function HeaderComponent(props) { + const nodes = useContext(NodeContext)[0]; + const currentNode = useContext(NodeContext)[2]; -function HeaderComponent() { const [setDialog] = useContext(InputDialogContext); const [icon, setIcon] = useState(faGear); const [status, updateStatus] = useContext(StatusContext); @@ -23,7 +30,6 @@ function HeaderComponent() { const updateTests = useContext(SpeedtestContext)[1]; const [config, reloadConfig, checkConfig] = useContext(ConfigContext); const [updateAvailable, setUpdateAvailable] = useState(""); - const [view] = useContext(ViewContext); function switchDropdown() { toggleDropdown(setIcon); @@ -76,15 +82,17 @@ function HeaderComponent() { if (!config.viewMode) updateVersion(); }, [config]); + const getNodeName = () => + currentNode === "0" ? t("header.title") : nodes?.find(node => node.id === currentNode)?.name || t("header.title"); + + if (Object.keys(config).length === 0) return <>; + return (
-

{t("header.title")} {view === 1 && setDialog({ - title: t("header.beta.title"), - description: }}>header.beta.description, - buttonText: t("dialog.okay") - })}>BETA}

+ {config.viewMode ?

{t("header.title")}

:

props.showNodePage(true)} className="h2-click"> {getNodeName()}

} +
{updateAvailable ?