Merge pull request #33 from gnmyt/features/update-time

📅 Integration der "Zeitraum festlegen" Funktion
This commit is contained in:
Mathias Wagner
2022-08-15 14:02:02 +02:00
committed by GitHub
8 changed files with 174 additions and 40 deletions

View File

@@ -3,7 +3,7 @@ import "./styles.sass";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {
faArrowDown,
faArrowUp, faClock, faClose, faFileExport,
faArrowUp, faCalendarDays, faClock, faClose, faFileExport,
faGear, faInfo,
faKey,
faPause,
@@ -14,6 +14,7 @@ import {
import {ConfigContext} from "@/common/contexts/Config";
import {StatusContext} from "@/common/contexts/Status";
import {DialogContext} from "@/common/contexts/Dialog";
import {SpeedtestContext} from "@/common/contexts/Speedtests";
let icon;
@@ -33,6 +34,7 @@ function DropdownComponent() {
const [config, reloadConfig] = useContext(ConfigContext);
const [status, updateStatus] = useContext(StatusContext);
const updateTests = useContext(SpeedtestContext)[1];
const [setDialog] = useContext(DialogContext);
let headers = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {};
@@ -236,6 +238,26 @@ function DropdownComponent() {
});
}
const updateTime = async () => {
toggleDropdown();
setDialog({
title: "Zeige Tests der letzten ...",
select: true,
selectOptions: {
1: "24 Stunden (Standard)",
2: "2 Tage (Insgesamt)",
3: "7 Tage (Durchschnitt)",
4: "30 Tage (Durchschnitt)"
},
value: localStorage.getItem("testTime") || 1,
onSuccess: value => {
localStorage.setItem("testTime", value);
updateTests();
showFeedback(undefined, false);
}
});
}
return (
<div className="dropdown dropdown-invisible" id="dropdown">
<div className="dropdown-content">
@@ -272,6 +294,10 @@ function DropdownComponent() {
<FontAwesomeIcon icon={faClock}/>
<h3>Häufigkeit einstellen</h3>
</div>
<div className="dropdown-item" onClick={updateTime}>
<FontAwesomeIcon icon={faCalendarDays} />
<h3>Zeitraum festlegen</h3>
</div>
<div className="dropdown-item" onClick={exportDialog}>
<FontAwesomeIcon icon={faFileExport}/>
<h3>Tests exportieren</h3>

View File

@@ -6,9 +6,24 @@ export const SpeedtestProvider = (props) => {
const [speedtests, setSpeedtests] = useState({});
const generatePath = (level) => {
switch (level) {
case 1:
return "?hours=24";
case 2:
return "?hours=48";
case 3:
return "/averages?days=7";
case 4:
return "/averages?days=30";
}
}
const updateTests = () => {
let passwordHeaders = localStorage.getItem("password") ? {password: localStorage.getItem("password")} : {}
fetch("/api/speedtests", {headers: passwordHeaders})
let testTime = localStorage.getItem("testTime") || 1;
fetch("/api/speedtests" + generatePath(parseInt(testTime)), {headers: passwordHeaders})
.then(res => res.json())
.then(tests => setSpeedtests(tests))
}

View File

@@ -21,7 +21,7 @@ function LatestTestComponent() {
useEffect(() => {
if (latest) setLatestTestTime(generateRelativeTime(latest.created));
const interval = setInterval(() => setLatestTestTime(generateRelativeTime(latest.created || 0)), 1000);
const interval = setInterval(() => setLatestTestTime(generateRelativeTime(latest ? latest.created : 0)), 1000);
return () => clearInterval(interval);
}, [latest]);

View File

@@ -22,6 +22,12 @@ const errors = {
"Could not retrieve or read configuration": "Die Konfigurationsdatei konnte nicht geladen werden",
}
const tooltips = {
custom: "Benutzerdefiniert",
average: "Durchschnitt",
auto: "Automatisiert",
}
function SpeedtestComponent(props) {
const [setDialog] = useContext(DialogContext);
@@ -31,40 +37,69 @@ function SpeedtestComponent(props) {
let errorMessage = "Unbekannter Fehler: " + props.error;
let timeString;
if (props.type === "average") {
timeString = String(props.time.getDate()).padStart(2, '0') + "." + String(props.time.getMonth() + 1).padStart(2, '0');
} else {
timeString = String(props.time.getHours()).padStart(2, '0') + ":" + String(props.time.getMinutes()).padStart(2, '0');
}
if (props.error) {
for (let errorsKey in errors)
if (props.error.includes(errorsKey)) errorMessage = errors[errorsKey];
}
const showErrorDialog = () => {
setDialog({
title: "Test fehlgeschlagen",
description: errorMessage + ". Bitte überprüfe weitestgehend, ob das öfters passiert.",
buttonText: "Okay",
unsetButton: true,
unsetButtonText: "Test löschen",
onClear: () => fetch("/api/speedtests/" + props.id, {headers: passwordHeaders, method: "DELETE"})
.then(updateTests)
});
}
const showInfoDialog = () => {
if (props.type === "average") {
setDialog({
title: "Durchschnittsgeschwindigkeit",
buttonText: "Okay",
description: <><span className="dialog-value">{props.amount}</span> Tests haben ergeben, dass am <span
className="dialog-value">{timeString}</span> eine durchschnittliche Downloadgeschwindigkeit von <span
className="dialog-value">{props.down} Mbit/s</span> und eine Upload-geschwindigkeit von <span
className="dialog-value">{props.up} Mbit/s</span> bestand. Die Tests dauerten im Durchschnitt <span
className="dialog-value">{props.duration} Sekunden</span>.</>
});
} else {
setDialog({
title: "Testergebnis",
description: <>Dieser Test erreichte eine maximale Downloadgeschwindigkeit von <span
className="dialog-value">{props.down} Mbit/s </span>
und eine maximale Uploadgeschwindigkeit von <span className="dialog-value">{props.up} Mbit/s</span>.
Er wurde <span className="dialog-value">{props.type === "custom"
? "von dir" : "automatisch"}</span> angelegt und hat <span
className="dialog-value">{props.duration} Sekunden</span> gedauert.</>,
buttonText: "Okay",
unsetButton: true,
unsetButtonText: "Test löschen",
onClear: () => fetch("/api/speedtests/" + props.id, {headers: passwordHeaders, method: "DELETE"})
.then(updateTests)
});
}
}
return (
<div className="speedtest">
<div className="date">
<div className="tooltip-element">
<FontAwesomeIcon icon={props.error ? faInfo : faClockRotateLeft}
className={"container-icon help-icon icon-" + (props.error ? "error" : "blue")}
onClick={props.error ? () => setDialog({
title: "Test fehlgeschlagen",
description: errorMessage + ". Bitte überprüfe weitestgehend, ob das öfters passiert.",
buttonText: "Okay",
unsetButton: true,
unsetButtonText: "Test löschen",
onClear: () => fetch("/api/speedtests/"+props.id, {headers: passwordHeaders, method: "DELETE"})
.then(updateTests)
}) : () => setDialog({
title: "Testergebnis",
description: <>Dieser Test erreichte eine maximale Downloadgeschwindigkeit von <span className="dialog-value">{props.down} Mbit/s </span>
und eine maximale Uploadgeschwindigkeit von <span className="dialog-value">{props.up} Mbit/s</span>. Er wurde <span className="dialog-value">{props.type === "custom"
? "von dir" : "automatisch"}</span> angelegt und hat <span className="dialog-value">{props.duration} Sekunden</span> gedauert.</>,
buttonText: "Okay",
unsetButton: true,
unsetButtonText: "Test löschen",
onClear: () => fetch("/api/speedtests/"+props.id, {headers: passwordHeaders, method: "DELETE"})
.then(updateTests)
})} />
<span className="tooltip">{props.type === "custom" ? "Benutzerdefiniert" :"Automatisiert"}</span>
onClick={props.error ? showErrorDialog : showInfoDialog}/>
<span className="tooltip">{tooltips[props.type]}</span>
</div>
<h2 className="date-text">Um {props.time}</h2>
<h2 className="date-text">{(props.type === "average" ? "Am " : "Um ") + timeString}</h2>
</div>
<div className="speedtest-row">
<FontAwesomeIcon icon={props.error ? faClose : faPingPongPaddleBall}

View File

@@ -8,23 +8,29 @@ function TestArea() {
const config = useContext(ConfigContext)[0];
const [speedtests] = useContext(SpeedtestContext);
if (Object.entries(config).length === 0) return (<></>)
if (Object.entries(config).length === 0) return (<></>);
return (
<div className="speedtest-area">
{speedtests.map ? speedtests.map(test => {
let date = new Date(Date.parse(test.created));
let timeString = String(date.getHours()).padStart(2, '0') + ":" + String(date.getMinutes()).padStart(2, '0');
return <Speedtest time={timeString}
let item = localStorage.getItem("testTime");
if ((item === "3" || item === "4") && test.type !== "average") return;
let id = (test.type === "average") ? date.getDate() + "-" + date.getMonth() : test.id;
return <Speedtest time={date}
ping={test.ping} pingLevel={getIconBySpeed(test.ping, config.ping, false)}
down={test.download} downLevel={getIconBySpeed(test.download, config.download, true)}
up={test.upload} upLevel={getIconBySpeed(test.upload, config.upload, true)}
error={test.error}
key={test.id}
key={id}
url={test.url}
type={test.type}
duration={test.time}
id={test.id}
amount={test.amount}
id={id}
/>
}) : ""}
</div>

View File

@@ -15,16 +15,63 @@ module.exports.get = async (id) => {
}
// Lists all speedtests from the database
module.exports.list = async () => {
let dbEntries = await tests.findAll({order: [["created", "DESC"]]});
let all = [];
module.exports.list = async (hours = 24) => {
let dbEntries = (await tests.findAll({order: [["created", "DESC"]]}))
.filter((entry) => new Date(entry.created) > new Date().getTime() - hours * 3600000);
for (let dbEntry of dbEntries)
if (dbEntry.error === null) delete dbEntry.error
return dbEntries;
}
// Lists all speedtests from the database grouped by days
module.exports.listByDays = async (days) => {
let dbEntries = (await tests.findAll({order: [["created", "DESC"]]})).filter((entry) => entry.error === null)
.filter((entry) => new Date(entry.created) > new Date().getTime() - days * 24 * 3600000);
let averages = {};
dbEntries.forEach((entry) => {
if (entry.error === null) delete entry.error
all.push(entry);
const day = new Date(entry.created).toLocaleDateString();
if (!averages[day]) averages[day] = [];
averages[day].push(entry);
});
return all;
return averages;
}
// Calculates the average speedtests and lists them
module.exports.listAverage = async (days) => {
const averages = await this.listByDays(days);
let result = [];
if (Object.keys(averages).length !== 0)
result.push(averages[Object.keys(averages)[0]][0]);
for (let day in averages) {
let avgNumbers = {ping: 0, down: 0, up: 0, time: 0};
let currentDay = averages[day];
currentDay.forEach((current) => {
avgNumbers.ping += current.ping;
avgNumbers.down += current.download;
avgNumbers.up += current.upload;
avgNumbers.time += current.time;
});
const created = new Date(currentDay[0].created);
result.push({
ping: Math.round(avgNumbers["ping"] / currentDay.length),
download: parseFloat((avgNumbers["down"] / currentDay.length).toFixed(2)),
upload: parseFloat((avgNumbers["up"] / currentDay.length).toFixed(2)),
time: Math.round(avgNumbers["time"] / currentDay.length),
type: "average",
amount: currentDay.length,
created: created.getFullYear() + "-" + (created.getMonth() + 1) + "-" + created.getDate()
});
}
return result;
}
// Gets the latest speedtest from the database
@@ -41,12 +88,12 @@ module.exports.delete = async (id) => {
return true;
}
// Removes speedtests older than 24 hours
// Removes speedtests older than 30 days
module.exports.removeOld = async () => {
await tests.destroy({
where: {
created: {
[Op.lte]: Sequelize.literal(`datetime('now', '-1 day')`)
[Op.lte]: Sequelize.literal(`datetime('now', '-30 days')`)
}
}
});

View File

@@ -4,7 +4,12 @@ const pauseController = require('../controller/pause');
// List all speedtests
app.get("/", async (req, res) => {
res.json(await tests.list());
res.json(await tests.list(req.query.hours || 24));
});
// List all speedtests by average
app.get("/averages", async (req, res) => {
res.json(await tests.listAverage(req.query.days || 7));
});
// Runs a speedtest

View File

@@ -50,7 +50,7 @@ module.exports.create = async (type = "auto", retried = false) => {
let upload = roundSpeed(test.upload.bytes, test.upload.elapsed);
let time = Math.round((test.download.elapsed + test.upload.elapsed) / 1000);
let testResult = await tests.create(ping, download, upload, time, type);
console.log(`Test #${testResult} was executed successfully. 🏓 ${ping}${download}${upload}`);
console.log(`Test #${testResult} was executed successfully in ${time}s. 🏓 ${ping}${download}${upload}`);
createRecommendations().then(() => "");
} catch (e) {
if (!retried) return this.create(type, true);