From bdadc0e908acc73a3f09ad621dbd7255bdc753ab Mon Sep 17 00:00:00 2001
From: LeoMartinDev <7238941+LeoMartinDev@users.noreply.github.com>
Date: Mon, 6 Dec 2021 10:31:30 +0100
Subject: [PATCH] Update panels structure
---
.env | 2 +-
client/App.jsx | 40 +++-
client/api.js | 33 +++-
client/modules/panels/DailyAveragePanel.jsx | 30 +++
client/modules/panels/NetworkSpeedPanel.jsx | 181 ++++++++++++++++++
client/modules/panels/NetworkSpeedsPanel.jsx | 41 ----
client/modules/panels/Panel.jsx | 17 +-
client/pages/Panels.jsx | 18 +-
...211117192523_create_network_stats_table.js | 2 +-
package-lock.json | 42 +++-
package.json | 6 +-
src/database.js | 1 +
.../{stats => networkStats}/NetworkStats.js | 2 +-
.../networkStats/networkStats.constants.js | 5 +
.../networkStats.model.js} | 0
.../networkStats/networkStats.repository.js | 48 +++++
.../networkStats.usecases.js} | 17 +-
.../speedTest.service.js | 0
.../networkSpeedPanel.constants.js | 12 ++
.../networkSpeedPanel.model.js | 102 ++++++++++
.../networkSpeedPanel.usecase.js | 14 ++
src/modules/panels/panels.routes.js | 55 ++++++
src/modules/stats/networkStats.repository.js | 29 ---
src/modules/stats/stats.routes.js | 31 ---
src/routes.js | 4 +-
25 files changed, 585 insertions(+), 147 deletions(-)
create mode 100644 client/modules/panels/DailyAveragePanel.jsx
create mode 100644 client/modules/panels/NetworkSpeedPanel.jsx
delete mode 100644 client/modules/panels/NetworkSpeedsPanel.jsx
rename src/modules/{stats => networkStats}/NetworkStats.js (94%)
create mode 100644 src/modules/networkStats/networkStats.constants.js
rename src/modules/{stats/stats.models.js => networkStats/networkStats.model.js} (100%)
create mode 100644 src/modules/networkStats/networkStats.repository.js
rename src/modules/{stats/stats.usecases.js => networkStats/networkStats.usecases.js} (53%)
rename src/modules/{stats => networkStats}/speedTest.service.js (100%)
create mode 100644 src/modules/panels/networkSpeedPanel/networkSpeedPanel.constants.js
create mode 100644 src/modules/panels/networkSpeedPanel/networkSpeedPanel.model.js
create mode 100644 src/modules/panels/networkSpeedPanel/networkSpeedPanel.usecase.js
create mode 100644 src/modules/panels/panels.routes.js
delete mode 100644 src/modules/stats/networkStats.repository.js
delete mode 100644 src/modules/stats/stats.routes.js
diff --git a/.env b/.env
index 2e70385..e6fa6a4 100644
--- a/.env
+++ b/.env
@@ -1,5 +1,5 @@
LOGGER_LEVEL=debug
-MONITORING_INTERVAL=60000 # 1 minute
+MONITORING_INTERVAL=120000 # 2 minutes
DATABASE_TYPE=sqlite
# DATABASE_HOST=
diff --git a/client/App.jsx b/client/App.jsx
index 7ae1192..00fb3c7 100644
--- a/client/App.jsx
+++ b/client/App.jsx
@@ -1,11 +1,10 @@
import * as React from 'react';
-import { createTheme, CssBaseline, ThemeProvider, Typography, useMediaQuery } from '@mui/material';
-import { Box } from '@mui/system';
+import { createTheme, CssBaseline, ThemeProvider, useMediaQuery } from '@mui/material';
import PanelsPage from './pages/Panels';
-import { getStats } from './api';
export function App() {
+ let timeoutId;
const prefersDarkMode = useMediaQuery('(prefers-color-scheme: dark)');
const theme = React.useMemo(
@@ -18,19 +17,42 @@ export function App() {
[prefersDarkMode],
);
- const [stats, setStats] = React.useState([]);
+ const [state, setState] = React.useState({
+ stats: [],
+ units: {},
+ });
- React.useEffect(async () => {
- const response = await getStats();
+ // const fetchStats = async () => {
+ // const { stats, units } = await getStats();
- setStats(response);
- }, []);
+ // setState((prevState) => ({
+ // ...prevState,
+ // stats,
+ // units,
+ // }));
+ // };
+
+ // const pollStats = async () => {
+ // await fetchStats();
+
+ // timeoutId = setTimeout(async () => {
+ // pollStats();
+ // }, 120000);
+ // };
+
+ // React.useEffect(async () => {
+ // // pollStats();
+
+ // return () => {
+ // clearTimeout(timeoutId);
+ // };
+ // }, []);
return (
-
+
);
diff --git a/client/api.js b/client/api.js
index 8c198d7..be917fa 100644
--- a/client/api.js
+++ b/client/api.js
@@ -1,16 +1,31 @@
+import useSWR from "swr";
+
+export {
+ useNetworkSpeedPanelData,
+};
+
const BASE_URL = 'http://localhost:3000/api';
-async function request({ path }) {
- const fetchResult = await fetch(`${BASE_URL}/${path}`);
- const response = await fetchResult.json();
+const fetcher = async (url) => {
+ const response = await fetch(`${BASE_URL}/${url}`);
+
+ if (!response.ok) {
+ const error = new Error('An error occurred while fetching the data.')
- if (fetchResult.ok) {
- return response;
+ error.info = await response.json()
+ error.status = response.status
+ throw error
}
- throw response;
-}
+ return response.json()
+};
+
+function useNetworkSpeedPanelData(options) {
+ const { data, error } = useSWR(`panels/network-speed`, fetcher, options)
-export function getStats() {
- return request({ path: 'stats'});
+ return {
+ data,
+ error,
+ isLoading: !error && !data,
+ };
}
diff --git a/client/modules/panels/DailyAveragePanel.jsx b/client/modules/panels/DailyAveragePanel.jsx
new file mode 100644
index 0000000..b40e498
--- /dev/null
+++ b/client/modules/panels/DailyAveragePanel.jsx
@@ -0,0 +1,30 @@
+import dayjs from 'dayjs';
+import { Typography } from '@mui/material';
+import AvTimer from '@mui/icons-material/AvTimer';
+import Upload from '@mui/icons-material/Upload';
+import Download from '@mui/icons-material/Download';
+
+import Panel from "./Panel";
+import { Box } from '@mui/system';
+
+const getMeanMetric = ({ last24HoursStats, metricName }) =>
+ _.chain(last24HoursStats).meanBy(`metrics.${metricName}`).round(2).value() || undefined;
+
+export default function NetworkSpeedsPanel({ stats, units }) {
+ const last24HoursStats = _.filter(stats, ({ date }) =>
+ dayjs(date).isAfter(dayjs().subtract(24, 'hours')));
+
+ const meanPing = getMeanMetric({ last24HoursStats, metricName: 'ping' });
+ const downloadPing = getMeanMetric({ last24HoursStats, metricName: 'download' });
+ const uploadPing = getMeanMetric({ last24HoursStats, metricName: 'upload' });
+
+ return (
+
+
+ Ping: {meanPing} {units?.ping}
+ Download speed: {downloadPing} {units?.download}
+ Upload speed: {uploadPing} {units?.upload}
+
+
+ );
+}
\ No newline at end of file
diff --git a/client/modules/panels/NetworkSpeedPanel.jsx b/client/modules/panels/NetworkSpeedPanel.jsx
new file mode 100644
index 0000000..7d41dc6
--- /dev/null
+++ b/client/modules/panels/NetworkSpeedPanel.jsx
@@ -0,0 +1,181 @@
+// import get from 'lodash/get';
+// import capitalize from 'lodash/capitalize';
+// import map from 'lodash/map';
+// import { useEffect, useState } from "react";
+// import { Line } from "react-chartjs-2";
+// import { ToggleButton, ToggleButtonGroup } from '@mui/material';
+
+// import { useNetworkSpeedPanelData } from "../../api";
+// import Panel from "./Panel";
+
+// export default function NetworkSpeedPanelContainer() {
+// const {
+// data,
+// error,
+// isLoading,
+// } = useNetworkSpeedPanelData();
+
+// return ;
+// }
+
+// function PeriodTypeToggleButton({ onChange, periodTypes, periodType }) {
+// const formatPeriodType = ({ periodType }) => capitalize(periodType);
+
+// return (
+//
+// {map(periodTypes, (periodType) => {formatPeriodType({ periodType })})}
+//
+// );
+// }
+
+// function NetworkSpeedPanel({ isLoading, data, fetchData }) {
+// const [state, setState] = useState({
+// labels: undefined,
+// stats: undefined,
+// metricsLabels: undefined,
+// periodTypes: undefined,
+// periodType: undefined,
+// chartjsData: undefined,
+// });
+
+// const getDataset = ({ metricName, color, stats, metricsLabels }) => ({
+// label: metricsLabels?.[metricName],
+// data: stats?.[metricName],
+// fill: false,
+// borderColor: color,
+// tension: 0.1,
+// });
+
+// useEffect(() => {
+// if (!data) {
+// return;
+// }
+
+// const labels = get(data, 'periods');
+// const stats = get(data, 'data');
+// const metricsLabels = get(data, 'metricsLabels');
+
+// setState({
+// labels,
+// stats,
+// metricsLabels,
+// periodTypes: get(data, 'periodTypes'),
+// periodType: get(data, 'periodType'),
+// chartjsData: {
+// labels,
+// datasets: [
+// getDataset({ metricName: 'download', color: '#4caf50', stats, metricsLabels }),
+// getDataset({ metricName: 'upload', color: '#f50057', stats, metricsLabels }),
+// getDataset({ metricName: 'ping', color: '#2196f3', stats, metricsLabels }),
+// ],
+// },
+// });
+// }, [isLoading]);
+
+// const onPeriodTypeChange = (_event, newPeriodType) => {
+// setState((prevState) => ({ ...prevState, periodType: newPeriodType }));
+
+// fetchData({ periodType: newPeriodType });
+// };
+
+// return (
+//
+// }
+// >
+// {state.chartjsData ? : null}
+//
+// );
+// }
+
+import get from 'lodash/get';
+import capitalize from 'lodash/capitalize';
+import map from 'lodash/map';
+import { useEffect, useState } from "react";
+import { Line } from "react-chartjs-2";
+import { ToggleButton, ToggleButtonGroup } from '@mui/material';
+
+import { useNetworkSpeedPanelData } from "../../api";
+import Panel from "./Panel";
+
+function PeriodTypeToggleButton({ onChange, periodTypes, periodType }) {
+ const formatPeriodType = ({ periodType }) => capitalize(periodType);
+
+ return (
+
+ {map(periodTypes, (periodType) => {formatPeriodType({ periodType })})}
+
+ );
+}
+
+export default function NetworkSpeedPanel() {
+ const {
+ data,
+ error,
+ isLoading,
+ } = useNetworkSpeedPanelData({
+ refreshInterval: 60 * 1000,
+ });
+
+ const getDataset = ({ metricName, color, stats, metricsLabels }) => ({
+ label: metricsLabels?.[metricName],
+ data: stats?.[metricName],
+ fill: false,
+ borderColor: color,
+ tension: 0.1,
+ });
+
+ const labels = get(data, 'periods');
+ const stats = get(data, 'data');
+ const metricsLabels = get(data, 'metricsLabels');
+ const periodTypes = get(data, 'periodTypes');
+ const [periodType, setPeriodType] = useState(get(data, 'periodType'));
+ const chartjsData = {
+ labels,
+ datasets: [
+ getDataset({ metricName: 'download', color: '#4caf50', stats, metricsLabels }),
+ getDataset({ metricName: 'upload', color: '#f50057', stats, metricsLabels }),
+ getDataset({ metricName: 'ping', color: '#2196f3', stats, metricsLabels }),
+ ],
+ };
+
+ const onPeriodTypeChange = (_event, newPeriodType) => {
+ setPeriodType(newPeriodType);
+ };
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+ }
+ >
+
+
+ );
+}
diff --git a/client/modules/panels/NetworkSpeedsPanel.jsx b/client/modules/panels/NetworkSpeedsPanel.jsx
deleted file mode 100644
index dd67af9..0000000
--- a/client/modules/panels/NetworkSpeedsPanel.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Line } from "react-chartjs-2";
-import Panel from "./Panel";
-
-export default function NetworkSpeedsPanel({ stats }) {
- const statsByDate = _.orderBy(stats, 'asc');
- const metricsNames = ['download', 'upload'];
-
- const dates = _.map(statsByDate, 'date');
-
- const download = {
- label: 'Download',
- data: _.map(statsByDate, 'metrics.download'),
- fill: false,
- borderColor: '#4caf50',
- tension: 0.1,
- };
-
- const upload = {
- label: 'Upload',
- data: _.map(statsByDate, 'metrics.upload'),
- fill: false,
- borderColor: '#f50057',
- tension: 0.1,
- };
-
- const data = {
- labels: dates,
- datasets: [
- download,
- upload,
- ],
- };
-
- return (
-
-
-
- );
-}
\ No newline at end of file
diff --git a/client/modules/panels/Panel.jsx b/client/modules/panels/Panel.jsx
index 99d4c5b..353769f 100644
--- a/client/modules/panels/Panel.jsx
+++ b/client/modules/panels/Panel.jsx
@@ -1,13 +1,23 @@
-import { AppBar, Card, CardContent, Divider, Toolbar, Typography } from "@mui/material";
+import { Card, CardContent, Skeleton, Typography } from "@mui/material";
import { Box } from "@mui/system";
-export default function Panel({ title, children }) {
+function Content({ isLoading, children }) {
+ if (isLoading) {
+ return ;
+ }
+
+ return children;
+}
+
+export default function Panel({ title, children, isLoading, header }) {
return (
`1px solid ${theme.palette.divider}`,
bgcolor: 'background.paper',
color: 'text.secondary',
@@ -22,9 +32,10 @@ export default function Panel({ title, children }) {
}}
>
{title}
+ {header ? {header} : null}
- {children}
+ {children}
diff --git a/client/pages/Panels.jsx b/client/pages/Panels.jsx
index 21b367a..a3a87c2 100644
--- a/client/pages/Panels.jsx
+++ b/client/pages/Panels.jsx
@@ -1,13 +1,21 @@
import { Line } from 'react-chartjs-2';
import _ from 'lodash';
-import { Container } from '@mui/material';
-import NetworkSpeedChart from '../modules/panels/NetworkSpeedsPanel';
+import { Container, Grid } from '@mui/material';
+import NetworkSpeedPanel from '../modules/panels/NetworkSpeedPanel';
+import DailyAveragePanel from '../modules/panels/DailyAveragePanel';
-export default function PanelsPage({ stats }) {
+export default function PanelsPage({ stats, units }) {
return (
-
-
+
+
+
+
+
+
+ {/* */}
+
+
);
}
\ No newline at end of file
diff --git a/migrations/20211117192523_create_network_stats_table.js b/migrations/20211117192523_create_network_stats_table.js
index b149bc7..dbbdc6a 100644
--- a/migrations/20211117192523_create_network_stats_table.js
+++ b/migrations/20211117192523_create_network_stats_table.js
@@ -12,7 +12,7 @@ module.exports = {
t.float('upload').notNullable();
t.float('download').notNullable();
t.float('ping').notNullable();
- t.datetime('created_at').defaultTo(knex.fn.now());
+ t.timestamps(true, true);
});
},
down: (knex) =>{
diff --git a/package-lock.json b/package-lock.json
index e6cb81d..f394c76 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,11 +11,12 @@
"dependencies": {
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
- "@mui/icons-material": "^5.1.1",
+ "@mui/icons-material": "^5.2.0",
"@mui/material": "^5.1.1",
"chart.js": "^3.6.0",
"convict": "^6.2.1",
"convict-format-with-validator": "^6.2.0",
+ "dayjs": "^1.10.7",
"dotenv": "^10.0.0",
"fastify": "^3.24.0",
"fastify-knexjs": "^1.4.0",
@@ -29,7 +30,8 @@
"react": "^17.0.2",
"react-chartjs-2": "^3.3.0",
"react-dom": "^17.0.2",
- "sqlite3": "^5.0.2"
+ "sqlite3": "^5.0.2",
+ "swr": "^1.1.0"
},
"devDependencies": {
"@babel/preset-react": "^7.16.0",
@@ -2039,9 +2041,9 @@
}
},
"node_modules/@mui/icons-material": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.1.tgz",
- "integrity": "sha512-tLM1/QhVAgcetEscZa8BlM1IRRaoNxjhFzQOIs5wAuuVhHSrB8zZCKugpZVIZ1nKyQqLgVEa9TbtWpo5jLrnRQ==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.0.tgz",
+ "integrity": "sha512-NvyrVaGKpP4R1yFw8BCnE0QcsQ67RtpgxPr4FtH8q60MDYPuPVczLOn5Ash5CFavoDWur/NfM/4DpT54yf3InA==",
"dependencies": {
"@babel/runtime": "^7.16.3"
},
@@ -4665,6 +4667,11 @@
"node": "*"
}
},
+ "node_modules/dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -10880,6 +10887,14 @@
"node": ">=10.13.0"
}
},
+ "node_modules/swr": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-1.1.0.tgz",
+ "integrity": "sha512-MFL3mkl752Uap81nLA1tEu7vQmikPamSziW+6dBidYKAo4oLOlUx/x5GZy4ZCkCwfZe2uedylkz1UMGnatUX4g==",
+ "peerDependencies": {
+ "react": "^16.11.0 || ^17.0.0 || ^18.0.0"
+ }
+ },
"node_modules/tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
@@ -13269,9 +13284,9 @@
}
},
"@mui/icons-material": {
- "version": "5.1.1",
- "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.1.1.tgz",
- "integrity": "sha512-tLM1/QhVAgcetEscZa8BlM1IRRaoNxjhFzQOIs5wAuuVhHSrB8zZCKugpZVIZ1nKyQqLgVEa9TbtWpo5jLrnRQ==",
+ "version": "5.2.0",
+ "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.2.0.tgz",
+ "integrity": "sha512-NvyrVaGKpP4R1yFw8BCnE0QcsQ67RtpgxPr4FtH8q60MDYPuPVczLOn5Ash5CFavoDWur/NfM/4DpT54yf3InA==",
"requires": {
"@babel/runtime": "^7.16.3"
}
@@ -15323,6 +15338,11 @@
"integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
"dev": true
},
+ "dayjs": {
+ "version": "1.10.7",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.10.7.tgz",
+ "integrity": "sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig=="
+ },
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -20076,6 +20096,12 @@
"stable": "^0.1.8"
}
},
+ "swr": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/swr/-/swr-1.1.0.tgz",
+ "integrity": "sha512-MFL3mkl752Uap81nLA1tEu7vQmikPamSziW+6dBidYKAo4oLOlUx/x5GZy4ZCkCwfZe2uedylkz1UMGnatUX4g==",
+ "requires": {}
+ },
"tapable": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz",
diff --git a/package.json b/package.json
index 2e8e678..c49480c 100644
--- a/package.json
+++ b/package.json
@@ -17,11 +17,12 @@
"dependencies": {
"@emotion/react": "^11.6.0",
"@emotion/styled": "^11.6.0",
- "@mui/icons-material": "^5.1.1",
+ "@mui/icons-material": "^5.2.0",
"@mui/material": "^5.1.1",
"chart.js": "^3.6.0",
"convict": "^6.2.1",
"convict-format-with-validator": "^6.2.0",
+ "dayjs": "^1.10.7",
"dotenv": "^10.0.0",
"fastify": "^3.24.0",
"fastify-knexjs": "^1.4.0",
@@ -35,7 +36,8 @@
"react": "^17.0.2",
"react-chartjs-2": "^3.3.0",
"react-dom": "^17.0.2",
- "sqlite3": "^5.0.2"
+ "sqlite3": "^5.0.2",
+ "swr": "^1.1.0"
},
"devDependencies": {
"@babel/preset-react": "^7.16.0",
diff --git a/src/database.js b/src/database.js
index 7f4343c..a620c13 100644
--- a/src/database.js
+++ b/src/database.js
@@ -18,6 +18,7 @@ function getDatabaseConfig({ database }) {
client: DATABASES_CLIENTS[database.type],
connection: _.pick(database, ['host', 'port', 'user', 'password', 'database']),
useNullAsDefault: true,
+ timezone: '+00:00',
};
if (database.type !== 'sqlite') {
diff --git a/src/modules/stats/NetworkStats.js b/src/modules/networkStats/NetworkStats.js
similarity index 94%
rename from src/modules/stats/NetworkStats.js
rename to src/modules/networkStats/NetworkStats.js
index 9689b05..9a20914 100644
--- a/src/modules/stats/NetworkStats.js
+++ b/src/modules/networkStats/NetworkStats.js
@@ -2,7 +2,7 @@ const { EventEmitter } = require('events');
const _ = require('lodash');
const runSpeedTest = require('./speedTest.service');
-const { formatBandwidthToMbps } = require('./stats.models');
+const { formatBandwidthToMbps } = require('./networkStats.model');
const DEFAULT_OPTIONS = {
interval: 30 * 1000,
diff --git a/src/modules/networkStats/networkStats.constants.js b/src/modules/networkStats/networkStats.constants.js
new file mode 100644
index 0000000..8ce5bc7
--- /dev/null
+++ b/src/modules/networkStats/networkStats.constants.js
@@ -0,0 +1,5 @@
+const METRICS = ['ping', 'download', 'upload'];
+
+module.exports = {
+ METRICS,
+};
diff --git a/src/modules/stats/stats.models.js b/src/modules/networkStats/networkStats.model.js
similarity index 100%
rename from src/modules/stats/stats.models.js
rename to src/modules/networkStats/networkStats.model.js
diff --git a/src/modules/networkStats/networkStats.repository.js b/src/modules/networkStats/networkStats.repository.js
new file mode 100644
index 0000000..7b2f0cd
--- /dev/null
+++ b/src/modules/networkStats/networkStats.repository.js
@@ -0,0 +1,48 @@
+const dayjs = require('dayjs');
+const _ = require('lodash');
+
+dayjs.extend(require('dayjs/plugin/utc'));
+
+const DATE_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS';
+
+const formatOneNetworkStat = ({ id, download, upload, ping, created_at }) => ({
+ id,
+ metrics: {
+ download,
+ upload,
+ ping,
+ },
+ date: created_at,
+});
+
+module.exports = ({ knex }) => {
+ const NetworkStats = () => knex('network_stats');
+
+ return {
+ async find({ startDate, endDate } = {}) {
+ const query = NetworkStats();
+
+ if (startDate) {
+ query.where('created_at', '>', dayjs(startDate).utc().format(DATE_FORMAT));
+ }
+
+ if (endDate) {
+ query.where('created_at', '<', dayjs(endDate).utc().format(DATE_FORMAT));
+ }
+
+ const networkStats = await query;
+
+ return _.map(networkStats, formatOneNetworkStat);
+ },
+ async findOneById({ id }) {
+ const networkStat = await NetworkStats().select().where({ id }).first();
+
+ return formatOneNetworkStat(networkStat);
+ },
+ async insertOne({ data }) {
+ const [id] = await knex.insert(data).into('network_stats', ['id']);
+
+ return this.findOneById({ id });
+ },
+ };
+};
diff --git a/src/modules/stats/stats.usecases.js b/src/modules/networkStats/networkStats.usecases.js
similarity index 53%
rename from src/modules/stats/stats.usecases.js
rename to src/modules/networkStats/networkStats.usecases.js
index 4d3b76e..cdfa03b 100644
--- a/src/modules/stats/stats.usecases.js
+++ b/src/modules/networkStats/networkStats.usecases.js
@@ -6,17 +6,24 @@ module.exports = {
};
function monitorNetworkStats({ options, onData, onWarning }) {
- const networkStats = new NetworkStats({ options });
+ const stats = new NetworkStats({ options });
- networkStats.on('data', onData);
+ stats.on('data', onData);
- networkStats.on('warn', onWarning);
+ stats.on('warn', onWarning);
- networkStats.startMonitoring();
+ stats.startMonitoring();
}
async function getStats({ networkStatsRepository }) {
const stats = await networkStatsRepository.find();
- return stats;
+ return {
+ stats,
+ units: {
+ ping: 'ms',
+ upload: 'mbps',
+ download: 'mbps',
+ },
+ };
}
\ No newline at end of file
diff --git a/src/modules/stats/speedTest.service.js b/src/modules/networkStats/speedTest.service.js
similarity index 100%
rename from src/modules/stats/speedTest.service.js
rename to src/modules/networkStats/speedTest.service.js
diff --git a/src/modules/panels/networkSpeedPanel/networkSpeedPanel.constants.js b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.constants.js
new file mode 100644
index 0000000..d2ceef1
--- /dev/null
+++ b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.constants.js
@@ -0,0 +1,12 @@
+const PERIOD_TYPES = {
+ HOUR: 'hour',
+ DAY: 'day',
+ WEEK: 'week',
+};
+
+const DEFAULT_PERIOD_TYPE = PERIOD_TYPES.HOUR;
+
+module.exports = {
+ PERIOD_TYPES,
+ DEFAULT_PERIOD_TYPE,
+};
\ No newline at end of file
diff --git a/src/modules/panels/networkSpeedPanel/networkSpeedPanel.model.js b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.model.js
new file mode 100644
index 0000000..3e6b537
--- /dev/null
+++ b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.model.js
@@ -0,0 +1,102 @@
+const dayjs = require('dayjs');
+const _ = require('lodash');
+
+const { METRICS } = require('../../networkStats/networkStats.constants');
+const { PERIOD_TYPES, DEFAULT_PERIOD_TYPE } = require('./networkSpeedPanel.constants');
+
+dayjs.extend(require('dayjs/plugin/utc'));
+
+module.exports = {
+ getNetworkStatsPeriod,
+ getNetworkStatsByPeriod,
+ formatNetworkStats,
+};
+
+function getNetworkStatsPeriod({ periodType, now = new Date() }) {
+ const periodTypeToStartDate = {
+ [PERIOD_TYPES.HOUR]: dayjs(now).utc().subtract(1, 'hour').toDate(),
+ [PERIOD_TYPES.DAY]: dayjs(now).utc().subtract(1, 'day').toDate(),
+ [PERIOD_TYPES.WEEK]: dayjs(now).utc().subtract(1, 'week').toDate(),
+ };
+
+ const startDate = periodTypeToStartDate[periodType] || periodTypeToStartDate[DEFAULT_PERIOD_TYPE];
+
+ return {
+ startDate,
+ endDate: now,
+ };
+}
+
+function getNetworkStatsByPeriod({ networkStats, periodType }) {
+ const periodTypeGroupFn = {
+ [PERIOD_TYPES.HOUR]: ({ date }) => dayjs(date).utc().format('YYYY-MM-DD HH:mm'),
+ [PERIOD_TYPES.DAY]: ({ date }) => dayjs(date).utc().format('YYYY-MM-DD HH'),
+ [PERIOD_TYPES.WEEK]: ({ date }) => `${dayjs(date).utc().format('YYYY-MM-DD')}}`,
+ };
+
+ const groupFn = periodTypeGroupFn[periodType] || periodTypeGroupFn[DEFAULT_PERIOD_TYPE];
+
+ return _.groupBy(networkStats, groupFn);
+}
+
+function getPerMetricNetworkStats({ networkStatsByPeriod }) {
+ return _.chain(networkStatsByPeriod)
+ .map((networkStats, period) =>
+ _.map(networkStats, ({ metrics }) =>
+ _.map(metrics, (value, metric) => ({
+ value,
+ metric,
+ })),
+ ))
+ .flattenDeep()
+ .groupBy('metric')
+ .mapValues((networkStats) => _.map(networkStats, 'value'))
+ .value();
+}
+
+function getPeriodsLabels({ networkStatsByPeriod, periodType }) {
+ const periodTextFormatFn = {
+ [PERIOD_TYPES.HOUR]: (period) => dayjs(period, 'YYYY-MM-DD HH:mm').utc().format('HH:mm'),
+ [PERIOD_TYPES.DAY]: (period) => dayjs(period, 'YYYY-MM-DD HH').utc().format('MM-DD HH:mm'),
+ [PERIOD_TYPES.WEEK]: (period) => dayjs(period, 'YYYY-MM-DD').utc().format('MM-DD'),
+ };
+
+ const textFormatFn = periodTextFormatFn[periodType] || periodTextFormatFn[DEFAULT_PERIOD_TYPE];
+
+ const periods = _.keys(networkStatsByPeriod);
+
+ return _.map(periods, textFormatFn);
+}
+
+function getMetricsLabels({ metrics }) {
+ const UNITS = {
+ ping: 'ms',
+ upload: 'mbps',
+ download: 'mbps',
+ };
+
+ return _.chain(metrics)
+ .map((metric) => ({ metric, label: `${_.capitalize(metric)} (${UNITS[metric]})` }))
+ .keyBy('metric')
+ .mapValues('label')
+ .value();
+}
+
+function formatNetworkStats({ networkStats, periodType = DEFAULT_PERIOD_TYPE }) {
+ const networkStatsByPeriod = getNetworkStatsByPeriod({ networkStats, periodType });
+
+ const perMetricNetworkStats = getPerMetricNetworkStats({ networkStatsByPeriod });
+ const metrics = METRICS;
+ const metricsLabels = getMetricsLabels({ metrics });
+ const periods = getPeriodsLabels({ networkStatsByPeriod, periodType });
+ const periodTypes = _.values(PERIOD_TYPES);
+
+ return {
+ metrics,
+ metricsLabels,
+ periods,
+ periodType,
+ periodTypes,
+ data: perMetricNetworkStats,
+ };
+}
diff --git a/src/modules/panels/networkSpeedPanel/networkSpeedPanel.usecase.js b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.usecase.js
new file mode 100644
index 0000000..1e14bb8
--- /dev/null
+++ b/src/modules/panels/networkSpeedPanel/networkSpeedPanel.usecase.js
@@ -0,0 +1,14 @@
+const model = require('./networkSpeedPanel.model');
+
+module.exports = async function getNetworkSpeedPanelData({
+ networkStatsRepository,
+ periodType,
+}) {
+ const networkStatsPeriod = model.getNetworkStatsPeriod({ periodType });
+
+ const networkStats = await networkStatsRepository.find({
+ ...networkStatsPeriod,
+ });
+
+ return model.formatNetworkStats({ networkStats, periodType });
+}
diff --git a/src/modules/panels/panels.routes.js b/src/modules/panels/panels.routes.js
new file mode 100644
index 0000000..6a79217
--- /dev/null
+++ b/src/modules/panels/panels.routes.js
@@ -0,0 +1,55 @@
+const _ = require('lodash');
+
+const config = require('../../../config');
+
+const { PERIOD_TYPES, DEFAULT_PERIOD_TYPE } = require('./networkSpeedPanel/networkSpeedPanel.constants');
+const getNetworkStatsRepository = require('../networkStats/networkStats.repository');
+const { monitorNetworkStats } = require('../networkStats/networkStats.usecases');
+
+const getNetworkSpeedPanelData = require('./networkSpeedPanel/networkSpeedPanel.usecase');
+
+module.exports = async (instance) => {
+ const networkStatsRepository = getNetworkStatsRepository({ knex: instance.knex });
+
+ monitorNetworkStats({
+ options: {
+ interval: config.get('monitoring.interval'),
+ },
+ onData: (data) => {
+ instance.log.debug(data, 'New network stats');
+ networkStatsRepository.insertOne({ data });
+ },
+ onWarning: instance.log.warn,
+ });
+
+ instance.route(getSpeedsPanelRoute({ instance, networkStatsRepository }));
+ // instance.route(getAveragePanelRoute({ instance, networkStatsRepository }));
+};
+
+function getSpeedsPanelRoute({ networkStatsRepository }) {
+ return {
+ method: 'GET',
+ url: '/network-speed',
+ schema: {
+ querystring: {
+ periodType: { type: 'string', enum: _.values(PERIOD_TYPES), default: DEFAULT_PERIOD_TYPE },
+ },
+ },
+ handler: async (request) => {
+ const { query } = request;
+ const { periodType } = query;
+
+ return getNetworkSpeedPanelData({ networkStatsRepository, periodType });
+ },
+ };
+}
+
+// function getAveragePanelRoute({ networkStatsRepository }) {
+// return {
+// method: 'GET',
+// url: '/average',
+// handler: async () => {
+// return getStats({ networkStatsRepository });
+// },
+// };
+// }
diff --git a/src/modules/stats/networkStats.repository.js b/src/modules/stats/networkStats.repository.js
deleted file mode 100644
index 21331f7..0000000
--- a/src/modules/stats/networkStats.repository.js
+++ /dev/null
@@ -1,29 +0,0 @@
-const _ = require('lodash');
-
-const formatOneNetworkStat = ({ id, download, upload, ping, created_at }) => ({
- id,
- metrics: {
- download,
- upload,
- ping,
- },
- date: created_at,
-});
-
-module.exports = ({ knex }) => ({
- async find() {
- const stats = await knex.select().from('network_stats');
-
- return _.map(stats, formatOneNetworkStat);
- },
- async findOneById({ id }) {
- const stat = await knex.select().from('network_stats').where({ id });
-
- return formatOneNetworkStat(stat);
- },
- async insertOne({ data }) {
- const [id] = await knex.insert(data).into('network_stats', ['id']);
-
- return this.findOneById({ id });
- },
-});
diff --git a/src/modules/stats/stats.routes.js b/src/modules/stats/stats.routes.js
deleted file mode 100644
index eb59151..0000000
--- a/src/modules/stats/stats.routes.js
+++ /dev/null
@@ -1,31 +0,0 @@
-const config = require('../../../config');
-
-const getNetworkStatsRepository = require('./networkStats.repository');
-const { monitorNetworkStats, getStats } = require('./stats.usecases');
-
-module.exports = async (instance) => {
- const networkStatsRepository = getNetworkStatsRepository({ knex: instance.knex });
-
- // monitorNetworkStats({
- // options: {
- // interval: config.get('monitoring.interval'),
- // },
- // onData: (data) => {
- // instance.log.debug(data, 'New network stats');
- // networkStatsRepository.insertOne({ data });
- // },
- // onWarning: instance.log.warn,
- // });
-
- instance.route(getStatsRoute({ instance, networkStatsRepository }));
-};
-
-function getStatsRoute({ networkStatsRepository }) {
- return {
- method: 'GET',
- url: '/',
- handler: async () => {
- return getStats({ networkStatsRepository });
- },
- };
-}
diff --git a/src/routes.js b/src/routes.js
index 9e385b6..129a3ca 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -1,5 +1,5 @@
-const statsRoute = require('./modules/stats/stats.routes');
+const panelsRoutes = require('./modules/panels/panels.routes');
module.exports = async (instance) => {
- instance.register(statsRoute, { prefix: '/stats' });
+ instance.register(panelsRoutes, { prefix: '/panels' });
}