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' }); }