From fcc14a4dc0b171bd0cfba08d03e2e1b1368884ad Mon Sep 17 00:00:00 2001 From: ksjaay Date: Tue, 24 Dec 2024 01:56:12 +0000 Subject: [PATCH] Adds support for cloning, and pausing monitors --- app/components/home/monitor/layout/card.jsx | 5 +- app/components/icons/index.jsx | 6 + app/components/monitor/menu.jsx | 118 +++++++++++++++++--- app/components/monitor/menu.scss | 21 ++++ app/pages/home.jsx | 5 + docs/api/monitor.md | 10 +- package-lock.json | 4 +- package.json | 2 +- server/cache/index.js | 7 ++ server/class/certificate.js | 2 +- server/class/monitor.js | 6 +- server/class/notification.js | 4 +- server/database/queries/monitor.js | 11 ++ server/database/sqlite/setup.js | 1 + server/middleware/monitor/pause.js | 34 ++++++ server/routes/monitor.js | 2 + test/server/classes/monitor.test.js | 3 + 17 files changed, 212 insertions(+), 29 deletions(-) create mode 100644 server/middleware/monitor/pause.js diff --git a/app/components/home/monitor/layout/card.jsx b/app/components/home/monitor/layout/card.jsx index 7820bd4..3f3fd90 100644 --- a/app/components/home/monitor/layout/card.jsx +++ b/app/components/home/monitor/layout/card.jsx @@ -39,7 +39,10 @@ const MonitorCard = ({ monitor = {} }) => {
{uptimePercentage}%
-
+

Status

diff --git a/app/components/icons/index.jsx b/app/components/icons/index.jsx index 68ddae7..695a8f9 100644 --- a/app/components/icons/index.jsx +++ b/app/components/icons/index.jsx @@ -10,6 +10,7 @@ import { FaPlus, FaSignOutAlt, FaUsers, + FaClone, } from 'react-icons/fa'; import { FiLayout } from 'react-icons/fi'; import { HiStatusOffline, HiStatusOnline } from 'react-icons/hi'; @@ -23,6 +24,8 @@ import { FaBars, FaTrashCan, FaFilter, + FaPause, + FaPlay, } from 'react-icons/fa6'; import { IoArrowBack, IoColorPalette, IoGrid, IoReload } from 'react-icons/io5'; import { RiStackFill } from 'react-icons/ri'; @@ -37,10 +40,13 @@ export { FaChevronRight, FaChevronUp, FaCircleCheck, + FaClone, FaCog, FaEllipsisVertical, FaFilter, FaHome, + FaPause, + FaPlay, FaPlus, FaSignOutAlt, FaTrashCan, diff --git a/app/components/monitor/menu.jsx b/app/components/monitor/menu.jsx index 134b50a..2ee65c8 100644 --- a/app/components/monitor/menu.jsx +++ b/app/components/monitor/menu.jsx @@ -10,18 +10,23 @@ import { useNavigate } from 'react-router-dom'; import Button from '../ui/button'; import useContextStore from '../../context'; import MonitorModal from '../modal/monitor/delete'; -import { createGetRequest } from '../../services/axios'; +import { createGetRequest, createPostRequest } from '../../services/axios'; import MonitorConfigureModal from '../modal/monitor/configure'; -import { FaTrashCan, MdEdit } from '../icons'; +import { FaClone, FaEllipsisVertical, FaTrashCan, MdEdit } from '../icons'; +import { FaPlay, FaPause } from '../icons'; +import Dropdown from '../ui/dropdown'; +import useDropdown from '../../hooks/useDropdown'; const MonitorMenu = ({ name = 'Unknown', monitorId }) => { const { modalStore: { openModal, closeModal }, - globalStore: { getMonitor, editMonitor, removeMonitor }, + globalStore: { addMonitor, getMonitor, editMonitor, removeMonitor }, userStore: { user }, } = useContextStore(); + const { toggleDropdown, dropdownIsOpen } = useDropdown(); const navigate = useNavigate(); + const monitor = getMonitor(monitorId); const isEditor = user.permission <= 3; const handleConfirm = async () => { @@ -37,9 +42,38 @@ const MonitorMenu = ({ name = 'Unknown', monitorId }) => { navigate('/'); }; - const handleEdit = () => { - const monitor = getMonitor(monitorId); + const handlePause = async () => { + try { + await createPostRequest('/api/monitor/pause', { + monitorId, + pause: !monitor.paused, + }); + + editMonitor({ ...monitor, paused: !monitor.paused }); + toast.success( + monitor.paused + ? 'Monitor resumed successfully!' + : 'Monitor paused successfully!' + ); + } catch (error) { + console.log(error); + toast.error('Error occurred while pausing monitor!'); + } + }; + + const handleClone = () => { + openModal( + , + false + ); + }; + + const handleEdit = () => { openModal( { ); }; + const options = [ + { + value: 'Clone', + icon: , + onClick: handleClone, + id: 'monitor-pause-button', + }, + { + value: 'Edit', + icon: , + onClick: handleEdit, + id: 'monitor-edit-button', + }, + { + value: 'Delete', + icon: , + onClick: handleDelete, + id: 'monitor-delete-button', + }, + { + value: monitor.paused ? 'Resume' : 'Pause', + icon: monitor.paused ? ( + + ) : ( + + ), + onClick: handlePause, + id: 'monitor-clone-button', + }, + ]; + return (
@@ -70,20 +135,37 @@ const MonitorMenu = ({ name = 'Unknown', monitorId }) => { {/* */} {isEditor && ( <> - - + ))} + + - Delete - + + + + + {options.map((option) => ( + + {option.icon} + {option.value} + + ))} + + )}
diff --git a/app/components/monitor/menu.scss b/app/components/monitor/menu.scss index 02114ba..7f3c4c9 100644 --- a/app/components/monitor/menu.scss +++ b/app/components/monitor/menu.scss @@ -1,3 +1,5 @@ +@use '../../styles/breakpoints.scss' as *; + .monitor-view-menu-container { display: flex; gap: 10px; @@ -13,3 +15,22 @@ font-size: var(--font-2xl); font-weight: bold; } + +.monitor-view-menu-container .dropdown { + display: none; +} + +@include mobile { + .monitor-view-menu-container { + justify-content: center; + align-items: center; + } + + .monitor-view-menu-container .button { + display: none; + } + + .monitor-view-menu-container .dropdown { + display: block; + } +} diff --git a/app/pages/home.jsx b/app/pages/home.jsx index e44c979..e4d0b72 100644 --- a/app/pages/home.jsx +++ b/app/pages/home.jsx @@ -56,6 +56,11 @@ const Home = () => { return true; }) + .sort((a, b) => { + if (a.paused && !b.paused) return 1; + if (!a.paused && b.paused) return -1; + return a.name.localeCompare(b.name); + }) .map((monitor, index) => { if (layout === 'list') { return ( diff --git a/docs/api/monitor.md b/docs/api/monitor.md index fa935f3..3e9ae1d 100644 --- a/docs/api/monitor.md +++ b/docs/api/monitor.md @@ -61,6 +61,7 @@ There are various restrictions applied to the monitor data. The following are so | uptimePercentage | number | Uptime percentage for the monitor over the last 24 hours | | averageHeartbeatLatency | number | Average latency for the monitor over the last 24 hours | | showFilters | boolean | Used to check if hourly heartbeats are available | +| paused | boolean | Boolean if the monitor is paused | ### Example Partial Monitor @@ -83,7 +84,8 @@ There are various restrictions applied to the monitor data. The following are so "port": null, "uptimePercentage": 83, "averageHeartbeatLatency": 38, - "showFilters": true + "showFilters": true, + "paused": false } ``` @@ -104,7 +106,8 @@ There are various restrictions applied to the monitor data. The following are so "port": 2308, "uptimePercentage": 83, "averageHeartbeatLatency": 38, - "showFilters": false + "showFilters": false, + "paused": false } ``` @@ -130,6 +133,7 @@ There are various restrictions applied to the monitor data. The following are so | uptimePercentage | number | Uptime percentage for the monitor over the last 24 hours | | averageHeartbeatLatency | number | Average latency for the monitor over the last 24 hours | | showFilters | boolean | Used to check if hourly heartbeats are available | +| paused | boolean | Boolean if the monitor is paused | | heartbeats | Array<[Heartbeat](#heartbeat-structure)> | Array of monitor heartbeats | | cert | [Certificate](#certificate-structure) | Information about the certificate | @@ -155,6 +159,7 @@ There are various restrictions applied to the monitor data. The following are so "uptimePercentage": 83, "averageHeartbeatLatency": 38, "showFilters": true, + "paused": false, "heartbeats": [ { "id": 38, @@ -204,6 +209,7 @@ There are various restrictions applied to the monitor data. The following are so "uptimePercentage": 83, "averageHeartbeatLatency": 38, "showFilters": false, + "paused": false, "heartbeats": [ { "id": 38, diff --git a/package-lock.json b/package-lock.json index bae46bd..0c7ba3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "lunalytics", - "version": "0.7.0", + "version": "0.7.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "lunalytics", - "version": "0.7.0", + "version": "0.7.2", "license": "SEE LICENSE IN LICENSE", "dependencies": { "axios": "^1.6.2", diff --git a/package.json b/package.json index 97ad8a7..2c964e3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "lunalytics", - "version": "0.7.0", + "version": "0.7.2", "description": "Open source Node.js server/website monitoring tool", "private": true, "author": "KSJaay ", diff --git a/server/cache/index.js b/server/cache/index.js index b11db99..4b049f2 100644 --- a/server/cache/index.js +++ b/server/cache/index.js @@ -58,6 +58,8 @@ class Master { return; } + if (monitor.paused) return; + if (this.timeouts.has(monitorId)) { clearTimeout(this.timeouts.get(monitorId)); } @@ -146,6 +148,11 @@ class Master { }); } } + + removeMonitor(monitorId) { + clearTimeout(this.timeouts.get(monitorId)); + this.timeouts.delete(monitorId); + } } const cache = new Master(); diff --git a/server/class/certificate.js b/server/class/certificate.js index 3f7783e..c13b42f 100644 --- a/server/class/certificate.js +++ b/server/class/certificate.js @@ -7,7 +7,7 @@ const parseJson = (str) => { }; const cleanCertificate = (certificate) => ({ - isValid: certificate.isValid, + isValid: certificate.isValid == '1', issuer: parseJson(certificate.issuer), validFrom: certificate.validFrom, validTill: certificate.validTill, diff --git a/server/class/monitor.js b/server/class/monitor.js index 8c2459f..e71b545 100644 --- a/server/class/monitor.js +++ b/server/class/monitor.js @@ -26,7 +26,8 @@ export const cleanPartialMonitor = (monitor) => ({ notificationType: monitor.notificationType, uptimePercentage: monitor.uptimePercentage, averageHeartbeatLatency: monitor.averageHeartbeatLatency, - showFilters: monitor.showFilters || false, + showFilters: monitor.showFilters == '1', + paused: monitor.paused == '1', }); export const cleanMonitor = ({ heartbeats = [], cert, ...monitor }) => ({ @@ -47,7 +48,8 @@ export const cleanMonitor = ({ heartbeats = [], cert, ...monitor }) => ({ notificationType: monitor.notificationType, uptimePercentage: monitor.uptimePercentage, averageHeartbeatLatency: monitor.averageHeartbeatLatency, - showFilters: monitor.showFilters || false, + showFilters: monitor.showFilters == '1', + paused: monitor.paused == '1', cert: !cert?.isValid ? cert : cleanCertificate(cert), heartbeats, }); diff --git a/server/class/notification.js b/server/class/notification.js index 624e388..de91319 100644 --- a/server/class/notification.js +++ b/server/class/notification.js @@ -21,7 +21,7 @@ export const cleanNotification = (notification) => ({ token: notification.token, email: notification.email, friendlyName: notification.friendlyName, - isEnabled: notification.isEnabled ? true : false, + isEnabled: notification.isEnabled == '1', data: typeof notification.data === 'string' ? parseJson(notification.data) @@ -35,6 +35,6 @@ export const stringifyNotification = (notification) => ({ token: notification.token, email: notification.email, friendlyName: notification.friendlyName, - isEnabled: notification.isEnabled ? true : false, + isEnabled: notification.isEnabled == '1', data: stringifyJson(notification.data), }); diff --git a/server/database/queries/monitor.js b/server/database/queries/monitor.js index eb0fbde..7920612 100644 --- a/server/database/queries/monitor.js +++ b/server/database/queries/monitor.js @@ -114,6 +114,16 @@ const deleteMonitor = async (monitorId) => { return true; }; +const pauseMonitor = async (monitorId, paused) => { + const monitor = await SQLite.client('monitor').where({ monitorId }).first(); + + if (!monitor) { + throw new UnprocessableError('Monitor does not exist'); + } + + await SQLite.client('monitor').where({ monitorId }).update({ paused }); +}; + export { createMonitor, monitorExists, @@ -123,4 +133,5 @@ export { deleteMonitor, fetchUptimePercentage, fetchMonitorUptime, + pauseMonitor, }; diff --git a/server/database/sqlite/setup.js b/server/database/sqlite/setup.js index 7c40a53..f21110c 100644 --- a/server/database/sqlite/setup.js +++ b/server/database/sqlite/setup.js @@ -118,6 +118,7 @@ export class SQLite { table.string('notificationId').defaultTo(null); table.string('notificationType').defaultTo('All'); table.string('email').notNullable(); + table.boolean('paused').defaultTo(false); table.index('monitorId'); }); diff --git a/server/middleware/monitor/pause.js b/server/middleware/monitor/pause.js new file mode 100644 index 0000000..ff9d90f --- /dev/null +++ b/server/middleware/monitor/pause.js @@ -0,0 +1,34 @@ +import cache from '../../cache/index.js'; +import { pauseMonitor } from '../../database/queries/monitor.js'; +import { handleError } from '../../utils/errors.js'; + +const isTruthy = (value) => value == true || value == 'true'; +const isFalsy = (value) => value == false || value == 'false'; + +const monitorPause = async (request, response) => { + try { + const { monitorId, pause } = request.body; + + if (!monitorId) { + throw new Error('No monitorId provided'); + } + + if (!isTruthy(pause) && !isFalsy(pause)) { + throw new Error('Pause should be a boolean value'); + } + + await pauseMonitor(monitorId, isTruthy(pause)); + + if (isTruthy(pause)) { + cache.removeMonitor(monitorId); + } else { + await cache.checkStatus(monitorId); + } + + return response.sendStatus(200); + } catch (error) { + handleError(error, response); + } +}; + +export default monitorPause; diff --git a/server/routes/monitor.js b/server/routes/monitor.js index b055f57..f64a9a6 100644 --- a/server/routes/monitor.js +++ b/server/routes/monitor.js @@ -9,11 +9,13 @@ import monitorDelete from '../middleware/monitor/delete.js'; import fetchMonitorUsingId from '../middleware/monitor/id.js'; import hasEditorPermissions from '../middleware/user/hasEditor.js'; import fetchMonitorStatus from '../middleware/monitor/status.js'; +import monitorPause from '../middleware/monitor/pause.js'; router.post('/add', hasEditorPermissions, monitorAdd); router.post('/edit', hasEditorPermissions, monitorEdit); router.get('/delete', hasEditorPermissions, monitorDelete); router.get('/status', fetchMonitorStatus); router.get('/id', fetchMonitorUsingId); +router.post('/pause', hasEditorPermissions, monitorPause); export default router; diff --git a/test/server/classes/monitor.test.js b/test/server/classes/monitor.test.js index a6981f9..10cda40 100644 --- a/test/server/classes/monitor.test.js +++ b/test/server/classes/monitor.test.js @@ -24,6 +24,7 @@ describe('Monitor - Class', () => { uptimePercentage: 100, averageHeartbeatLatency: 820, showFilters: false, + paused: false, }; const certificate = { @@ -56,6 +57,7 @@ describe('Monitor - Class', () => { uptimePercentage: 100, averageHeartbeatLatency: 820, showFilters: false, + paused: false, }); }); @@ -85,6 +87,7 @@ describe('Monitor - Class', () => { uptimePercentage: 100, averageHeartbeatLatency: 820, showFilters: false, + paused: false, cert: { isValid: true, issuer: { C: 'US', O: "Let's Encrypt", CN: 'R3' },