diff --git a/.npmrc b/.npmrc new file mode 100644 index 000000000..43c97e719 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/Client/package-lock.json b/Client/package-lock.json index abbc57323..72bd51752 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -11,13 +11,13 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.4.1", - "@mui/lab": "6.0.0-beta.24", - "@mui/material": "6.4.1", + "@mui/icons-material": "6.4.2", + "@mui/lab": "6.0.0-beta.25", + "@mui/material": "6.4.2", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.24.1", - "@mui/x-date-pickers": "7.24.1", - "@reduxjs/toolkit": "2.5.0", + "@mui/x-data-grid": "7.25.0", + "@mui/x-date-pickers": "7.25.0", + "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", @@ -28,7 +28,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.15.0", + "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, @@ -1103,9 +1103,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.1.tgz", - "integrity": "sha512-SfDLWMV5b5oXgDf3NTa2hCTPC1d2defhDH2WgFKmAiejC4mSfXYbyi+AFCLzpizauXhgBm8OaZy9BHKnrSpahQ==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.2.tgz", + "integrity": "sha512-Qmod9fHsFWrtLxdSkZ4iDLRz2AUKt3C2ZEimuY+qKlQGVKJDNS5DuSlNOAgqfHFDq8mzB17ATN6HFcThwJlvUw==", "license": "MIT", "funding": { "type": "opencollective", @@ -1113,9 +1113,9 @@ } }, "node_modules/@mui/icons-material": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.1.tgz", - "integrity": "sha512-wsxFcUTQxt4s+7Bg4GgobqRjyaHLmZGNOs+HJpbwrwmLbT6mhIJxhpqsKzzWq9aDY8xIe7HCjhpH7XI5UD6teA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.2.tgz", + "integrity": "sha512-uwsH1KRmxkJwK3NZyo1xL9pEduL16ftCnzYBYjd6nPNtm05QAoIc0aqedS9tqDV+Ab3q5C04HHOVsMDDv1EBpg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0" @@ -1128,7 +1128,7 @@ "url": "https://opencollective.com/mui-org" }, "peerDependencies": { - "@mui/material": "^6.4.1", + "@mui/material": "^6.4.2", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0" }, @@ -1139,16 +1139,16 @@ } }, "node_modules/@mui/lab": { - "version": "6.0.0-beta.24", - "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.24.tgz", - "integrity": "sha512-Off0rx2065TfGu5Z8E4OduNiVAqE9p9A8N1268hoXOcx7HzARxJsDFLRKvWgpPMqmGEvR8DVaHkcF2N90UB15A==", + "version": "6.0.0-beta.25", + "resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-beta.25.tgz", + "integrity": "sha512-Dkh+gFp30Mbqg9A6VY1fOqP3amsgylYZLKb7fbO80GMecSfx+tb+HU5IJghYrhiR6JaY3nEK1AOxMO0kPydaUw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/base": "5.0.0-beta.69", - "@mui/system": "^6.4.1", + "@mui/system": "^6.4.2", "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.1", + "@mui/utils": "^6.4.2", "clsx": "^2.1.1", "prop-types": "^15.8.1" }, @@ -1162,8 +1162,8 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material": "^6.4.1", - "@mui/material-pigment-css": "^6.4.1", + "@mui/material": "^6.4.2", + "@mui/material-pigment-css": "^6.4.2", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1184,16 +1184,16 @@ } }, "node_modules/@mui/material": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.1.tgz", - "integrity": "sha512-MFBfia6UiKxyoLeGkAh8M15bkeDmfnsUTMRJd/vTQue6YQ8AQ6lw9HqDthyYghzDEWIvZO/lQQzLrZE8XwNJLA==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.2.tgz", + "integrity": "sha512-9jKr53KbAJyyBRx8LRmX7ATXHlGtxVQdPgm1uyXMoEPMVkSJW1yO3vFgfYoDbGx4ZHcCNuWa4FkFIPWVt9fghA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.1", - "@mui/system": "^6.4.1", + "@mui/core-downloads-tracker": "^6.4.2", + "@mui/system": "^6.4.2", "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.1", + "@mui/utils": "^6.4.2", "@popperjs/core": "^2.11.8", "@types/react-transition-group": "^4.4.12", "clsx": "^2.1.1", @@ -1212,7 +1212,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.1", + "@mui/material-pigment-css": "^6.4.2", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1239,13 +1239,13 @@ "license": "MIT" }, "node_modules/@mui/private-theming": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.1.tgz", - "integrity": "sha512-DcT7mwK89owwgcEuiE7w458te4CIjHbYWW6Kn6PiR6eLtxBsoBYphA968uqsQAOBQDpbYxvkuFLwhgk4bxoN/Q==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.2.tgz", + "integrity": "sha512-2CkQT0gNlogM50qGTBJgWA7hPPx4AeH8RE2xJa+PHtIOowiVPX52ZsQ0e7Ho18DAqEbkngQ6Uju037ER+TCY5A==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/utils": "^6.4.1", + "@mui/utils": "^6.4.2", "prop-types": "^15.8.1" }, "engines": { @@ -1266,9 +1266,9 @@ } }, "node_modules/@mui/styled-engine": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.0.tgz", - "integrity": "sha512-ek/ZrDujrger12P6o4luQIfRd2IziH7jQod2WMbLqGE03Iy0zUwYmckRTVhRQTLPNccpD8KXGcALJF+uaUQlbg==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.2.tgz", + "integrity": "sha512-cgjQK2bkllSYoWUBv93ALhCPJ0NhfO3NctsBf13/b4XSeQVfKPBAnR+P9mNpdFMa5a5RWwtWuBD3cZ5vktsN+g==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1300,16 +1300,16 @@ } }, "node_modules/@mui/system": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.1.tgz", - "integrity": "sha512-rgQzgcsHCTtzF9MZ+sL0tOhf2ZBLazpjrujClcb4Siju5lTrK0xX4PsiropActzCemNfM+mOu+0jezAVnfRK8g==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.2.tgz", + "integrity": "sha512-wQbaPCtsxNsM5nR+NZpkFJBKVKH03GQnAjlkKENM8JQqGdWcRyM3f4fJZgzzNdIFpSQw4wpAQKnhfHkjf3d6yQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/private-theming": "^6.4.1", - "@mui/styled-engine": "^6.4.0", + "@mui/private-theming": "^6.4.2", + "@mui/styled-engine": "^6.4.2", "@mui/types": "^7.2.21", - "@mui/utils": "^6.4.1", + "@mui/utils": "^6.4.2", "clsx": "^2.1.1", "csstype": "^3.1.3", "prop-types": "^15.8.1" @@ -1354,9 +1354,9 @@ } }, "node_modules/@mui/utils": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.1.tgz", - "integrity": "sha512-iQUDUeYh87SvR4lVojaRaYnQix8BbRV51MxaV6MBmqthecQoxwSbS5e2wnbDJUeFxY2ppV505CiqPLtd0OWkqw==", + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.2.tgz", + "integrity": "sha512-5NkhzlJkmR5+5RSs/Irqin1GPy2Z8vbLk/UzQrH9FEAnm6OA9SvuXjzgklxUs7N65VwEkGpKK1jMZ5K84hRdzQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1390,15 +1390,15 @@ "license": "MIT" }, "node_modules/@mui/x-charts": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.24.1.tgz", - "integrity": "sha512-OdTS/nXaANPe4AoUFIDD4LlID8kK/00q+uqVOCkVClEvFQeAkj3pBaghdS4hY7rVqsCgsm+yOStQVJa9G2MR+Q==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.25.0.tgz", + "integrity": "sha512-+DhnojHrVTt8RsTgq8AztzdFpW1kzOgiBdo0Pkl0DyxVdaKELC5QaetFwim9nIxT2zmU/RmiBcOU+qbqmQpFNA==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", "@mui/x-charts-vendor": "7.20.0", - "@mui/x-internals": "7.24.1", + "@mui/x-internals": "7.25.0", "@react-spring/rafz": "^9.7.5", "@react-spring/web": "^9.7.5", "clsx": "^2.1.1", @@ -1448,14 +1448,14 @@ } }, "node_modules/@mui/x-data-grid": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.24.1.tgz", - "integrity": "sha512-4sYTbMwsDotuTd2Cwa2JGTPXPWQs8RGJvocAKnIsNOzNdZNMrikE//HO35snriK8s4dauAApY7RVbeisjpVT+A==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.25.0.tgz", + "integrity": "sha512-e9ZLbCgnDiADFiDyXo91ucZFHEMkKBNpwpkaTq5KohzefJfMpMQjTEbJeueSfBG2G1Q1Am2TPeBqrNeReIA7RQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.24.1", + "@mui/x-internals": "7.25.0", "clsx": "^2.1.1", "prop-types": "^15.8.1", "reselect": "^5.1.1" @@ -1485,14 +1485,14 @@ } }, "node_modules/@mui/x-date-pickers": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.24.1.tgz", - "integrity": "sha512-ykQugMQHuQKBk3kViW/r0PpubtHQOlrd54bgbdafgTMCeM2VpXvv4zimzOu5IGnM6wEN8hupC7EXZbkrT6x46w==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.25.0.tgz", + "integrity": "sha512-t62OSFAKwj7KYQ8KcwTuKj6OgDuLQPSe4QUJcKDzD9rEhRIJVRUw2x27gBSdcls4l0PTrba19TghvDxCZprriw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", "@mui/utils": "^5.16.6 || ^6.0.0", - "@mui/x-internals": "7.24.1", + "@mui/x-internals": "7.25.0", "@types/react-transition-group": "^4.4.11", "clsx": "^2.1.1", "prop-types": "^15.8.1", @@ -1551,9 +1551,9 @@ } }, "node_modules/@mui/x-internals": { - "version": "7.24.1", - "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.24.1.tgz", - "integrity": "sha512-9BvJzpLJnS9BDphvkiv6v0QOLxbnu8jhwcexFjtCQ2ZyxtVuVsWzGZ2npT9sGOil7+eaFDmWnJtea/tgrPvSwQ==", + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.25.0.tgz", + "integrity": "sha512-tBUN54YznAkmtCIRAOl35Kgl0MjFDIjUbzIrbWRgVSIR3QJ8bXnVSkiRBi+P91SZEl9+ZW0rDj+osq7xFJV0kg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.25.7", @@ -1691,9 +1691,9 @@ } }, "node_modules/@reduxjs/toolkit": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.0.tgz", - "integrity": "sha512-awNe2oTodsZ6LmRqmkFhtb/KH03hUhxOamEQy411m3Njj3BbFvoBovxo4Q1cBWnV1ErprVj9MlF0UPXkng0eyg==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.5.1.tgz", + "integrity": "sha512-UHhy3p0oUpdhnSxyDjaRDYaw8Xra75UiLbCiRozVPHjfDwNYkh0TsVm/1OmTW8Md+iDAJmYPWUKMvsMc2GtpNg==", "license": "MIT", "dependencies": { "immer": "^10.0.3", @@ -1715,9 +1715,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.21.1", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.21.1.tgz", - "integrity": "sha512-KeBYSwohb8g4/wCcnksvKTYlg69O62sQeLynn2YE+5z7JWEj95if27kclW9QqbrlsQ2DINI8fjbV3zyuKfwjKg==", + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.22.0.tgz", + "integrity": "sha512-MBOl8MeOzpK0HQQQshKB7pABXbmyHizdTpqnrIseTbsv0nAepwC2ENZa1aaBExNQcpLoXmWthhak8SABLzvGPw==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -5447,12 +5447,12 @@ } }, "node_modules/react-router": { - "version": "6.28.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.28.2.tgz", - "integrity": "sha512-BgFY7+wEGVjHCiqaj2XiUBQ1kkzfg6UoKYwEe0wv+FF+HNPCxtS/MVPvLAPH++EsuCMReZl9RYVGqcHLk5ms3A==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.29.0.tgz", + "integrity": "sha512-DXZJoE0q+KyeVw75Ck6GkPxFak63C4fGqZGNijnWgzB/HzSP1ZfTlBj5COaGWwhrMQ/R8bXiq5Ooy4KG+ReyjQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.21.1" + "@remix-run/router": "1.22.0" }, "engines": { "node": ">=14.0.0" @@ -5462,13 +5462,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.28.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.28.2.tgz", - "integrity": "sha512-O81EWqNJWqvlN/a7eTudAdQm0TbI7hw+WIi7OwwMcTn5JMyZ0ibTFNGz+t+Lju0df4LcqowCegcrK22lB1q9Kw==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.29.0.tgz", + "integrity": "sha512-pkEbJPATRJ2iotK+wUwHfy0xs2T59YPEN8BQxVCPeBZvK7kfPESRc/nyxzdcxR17hXgUPYx2whMwl+eo9cUdnQ==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.21.1", - "react-router": "6.28.2" + "@remix-run/router": "1.22.0", + "react-router": "6.29.0" }, "engines": { "node": ">=14.0.0" @@ -5479,9 +5479,9 @@ } }, "node_modules/react-smooth": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.1.tgz", - "integrity": "sha512-OE4hm7XqR0jNOq3Qmk9mFLyd6p2+j6bvbPJ7qlB7+oo0eNcL2l7WQzG6MBnT3EXY6xzkLMUBec3AfewJdA0J8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", "license": "MIT", "dependencies": { "fast-equals": "^5.0.1", @@ -5489,8 +5489,8 @@ "react-transition-group": "^4.4.5" }, "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/react-toastify": { @@ -5523,16 +5523,16 @@ } }, "node_modules/recharts": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", - "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", "license": "MIT", "dependencies": { "clsx": "^2.0.0", "eventemitter3": "^4.0.1", "lodash": "^4.17.21", "react-is": "^18.3.1", - "react-smooth": "^4.0.0", + "react-smooth": "^4.0.4", "recharts-scale": "^0.4.4", "tiny-invariant": "^1.3.1", "victory-vendor": "^36.6.8" diff --git a/Client/package.json b/Client/package.json index 006bdd081..441b79824 100644 --- a/Client/package.json +++ b/Client/package.json @@ -14,13 +14,13 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@fontsource/roboto": "^5.0.13", - "@mui/icons-material": "6.4.1", - "@mui/lab": "6.0.0-beta.24", - "@mui/material": "6.4.1", + "@mui/icons-material": "6.4.2", + "@mui/lab": "6.0.0-beta.25", + "@mui/material": "6.4.2", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.24.1", - "@mui/x-date-pickers": "7.24.1", - "@reduxjs/toolkit": "2.5.0", + "@mui/x-data-grid": "7.25.0", + "@mui/x-date-pickers": "7.25.0", + "@reduxjs/toolkit": "2.5.1", "axios": "^1.7.4", "dayjs": "1.11.13", "joi": "17.13.3", @@ -31,7 +31,7 @@ "react-router": "^6.23.0", "react-router-dom": "^6.23.1", "react-toastify": "^10.0.5", - "recharts": "2.15.0", + "recharts": "2.15.1", "redux-persist": "6.0.0", "vite-plugin-svgr": "^4.2.0" }, diff --git a/Client/src/Components/BasicTable/index.jsx b/Client/src/Components/BasicTable/index.jsx deleted file mode 100644 index b93c584da..000000000 --- a/Client/src/Components/BasicTable/index.jsx +++ /dev/null @@ -1,360 +0,0 @@ -/* TODO - -Should we delete this? -*/ - -import PropTypes from "prop-types"; -import { useState, useEffect } from "react"; -import { useTheme } from "@emotion/react"; -import { - TableContainer, - Paper, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - TablePagination, - Box, - Typography, - Stack, - Button, -} from "@mui/material"; -import { useDispatch, useSelector } from "react-redux"; -import { setRowsPerPage } from "../../Features/UI/uiSlice"; -import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react"; -import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; -import LeftArrow from "../../assets/icons/left-arrow.svg?react"; -import RightArrow from "../../assets/icons/right-arrow.svg?react"; -import SelectorVertical from "../../assets/icons/selector-vertical.svg?react"; -import "./index.css"; -/** - * Component for pagination actions (first, previous, next, last). - * - * @component - * @param {Object} props - * @param {number} props.count - Total number of items. - * @param {number} props.page - Current page number. - * @param {number} props.rowsPerPage - Number of rows per page. - * @param {function} props.onPageChange - Callback function to handle page change. - * - * @returns {JSX.Element} Pagination actions component. - */ -const TablePaginationActions = (props) => { - const { count, page, rowsPerPage, onPageChange } = props; - - const handleFirstPageButtonClick = (event) => { - onPageChange(event, 0); - }; - const handleBackButtonClick = (event) => { - onPageChange(event, page - 1); - }; - const handleNextButtonClick = (event) => { - onPageChange(event, page + 1); - }; - const handleLastPageButtonClick = (event) => { - onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); - }; - - return ( - - - - - - - ); -}; - -TablePaginationActions.propTypes = { - count: PropTypes.number.isRequired, - page: PropTypes.number.isRequired, - rowsPerPage: PropTypes.number.isRequired, - onPageChange: PropTypes.func.isRequired, -}; - -/** - * BasicTable Component - * Renders a table with optional pagination. - * - * @component - * @param {Object} props - Component props. - * @param {Object} props.data - Data for the table including columns and rows. - * @param {Array} props.data.cols - Array of objects for column headers. - * @param {number} props.data.cols[].id - Unique identifier for the column. - * @param {string} props.data.cols[].name - Name of the column to display as header. - * @param {Array} props.data.rows - Array of row objects. - * @param {number} props.data.rows[].id - Unique identifier for the row. - * @param {Array} props.data.rows[].data - Array of cell data objects for the row. - * @param {number} props.data.rows[].data[].id - Unique identifier for the cell. - * @param {JSX.Element} props.data.rows[].data[].data - The content to display in the cell. - * @param {function} props.data.rows.data.handleClick - Function to call when the row is clicked. - * @param {boolean} [props.paginated=false] - Flag to enable pagination. - * @param {boolean} [props.reversed=false] - Flag to enable reverse order. - * @param {number} props.rowsPerPage- Number of rows per page (table). - * @param {string} props.emptyMessage - Message to display when there is no data. - * @example - * const data = { - * cols: [ - * { id: 1, name: "First Col" }, - * { id: 2, name: "Second Col" }, - * { id: 3, name: "Third Col" }, - * { id: 4, name: "Fourth Col" }, - * ], - * rows: [ - * { - * id: 1, - * data: [ - * { id: 1, data:
Data for Row 1 Col 1
}, - * { id: 2, data:
Data for Row 1 Col 2
}, - * { id: 3, data:
Data for Row 1 Col 3
}, - * { id: 4, data:
Data for Row 1 Col 4
}, - * ], - * }, - * { - * id: 2, - * data: [ - * { id: 5, data:
Data for Row 2 Col 1
}, - * { id: 6, data:
Data for Row 2 Col 2
}, - * { id: 7, data:
Data for Row 2 Col 3
}, - * { id: 8, data:
Data for Row 2 Col 4
}, - * ], - * }, - * ], - * }; - * - * - */ - -const BasicTable = ({ data, paginated, reversed, table, emptyMessage = "No data" }) => { - const DEFAULT_ROWS_PER_PAGE = 5; - const theme = useTheme(); - const dispatch = useDispatch(); - const uiState = useSelector((state) => state.ui); - let rowsPerPage = uiState?.[table]?.rowsPerPage ?? DEFAULT_ROWS_PER_PAGE; - const [page, setPage] = useState(0); - - useEffect(() => { - setPage(0); - }, [data]); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: table, - }) - ); - setPage(0); - }; - - let displayData = []; - - if (data && data.rows) { - let rows = reversed ? [...data.rows].reverse() : data.rows; - displayData = paginated - ? rows.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) - : rows; - } - - if (!data || !data.cols || !data.rows) { - return
No data
; - } - - /** - * Helper function to calculate the range of displayed rows. - * @returns {string} - */ - const getRange = () => { - let start = page * rowsPerPage + 1; - let end = Math.min(page * rowsPerPage + rowsPerPage, data.rows.length); - return `${start} - ${end}`; - }; - - return ( - <> - - - - - {data.cols.map((col) => ( - {col.name} - ))} - - - - {displayData.map((row) => { - return ( - - {row.data.map((cell) => { - return {cell.data}; - })} - - ); - })} - {displayData.length === 0 && ( - - - {emptyMessage} - - - )} - -
-
- {paginated && ( - - - Showing {getRange()} of {data.rows.length} monitor(s) - - - `Page ${page + 1} of ${Math.max(0, Math.ceil(count / rowsPerPage))}` - } - slotProps={{ - select: { - MenuProps: { - keepMounted: true, - PaperProps: { - className: "pagination-dropdown", - sx: { - mt: 0, - mb: theme.spacing(2), - }, - }, - transformOrigin: { vertical: "bottom", horizontal: "left" }, - anchorOrigin: { vertical: "top", horizontal: "left" }, - sx: { mt: theme.spacing(-2) }, - }, - inputProps: { id: "pagination-dropdown" }, - IconComponent: SelectorVertical, - sx: { - ml: theme.spacing(4), - mr: theme.spacing(12), - minWidth: theme.spacing(20), - textAlign: "left", - "&.Mui-focused > div": { - backgroundColor: theme.palette.primary.main, - }, - }, - }, - }} - sx={{ - mt: theme.spacing(6), - color: theme.palette.primary.contrastTextSecondary, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - strokeWidth: 1.3, - }, - "& .MuiSelect-select": { - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - }, - }} - /> - - )} - - ); -}; - -BasicTable.propTypes = { - data: PropTypes.object.isRequired, - paginated: PropTypes.bool, - reversed: PropTypes.bool, - rowPerPage: PropTypes.number, - table: PropTypes.string, - emptyMessage: PropTypes.string, -}; - -export default BasicTable; diff --git a/Client/src/Components/Charts/ChartBox/index.jsx b/Client/src/Components/Charts/ChartBox/index.jsx new file mode 100644 index 000000000..eed3efd0d --- /dev/null +++ b/Client/src/Components/Charts/ChartBox/index.jsx @@ -0,0 +1,90 @@ +import { Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import IconBox from "../../IconBox"; +import PropTypes from "prop-types"; + +const ChartBox = ({ + children, + icon, + header, + height = "300px", + justifyContent = "space-between", + Legend, + borderRadiusRight = 4, +}) => { + const theme = useTheme(); + return ( + + span": { + color: theme.palette.primary.contrastText, + fontSize: 20, + "& span": { + opacity: 0.8, + marginLeft: 2, + fontSize: 15, + }, + }, + + "& tspan, & text": { + fill: theme.palette.primary.contrastTextTertiary, + }, + "& path": { + transition: "fill 300ms ease, stroke-width 400ms ease", + }, + }} + > + + {icon} + {header} + + {children} + + {Legend && Legend} + + ); +}; + +export default ChartBox; + +ChartBox.propTypes = { + children: PropTypes.node, + icon: PropTypes.node.isRequired, + header: PropTypes.string.isRequired, + height: PropTypes.string, +}; diff --git a/Client/src/Components/Charts/LegendBox/index.jsx b/Client/src/Components/Charts/LegendBox/index.jsx new file mode 100644 index 000000000..ac14669e3 --- /dev/null +++ b/Client/src/Components/Charts/LegendBox/index.jsx @@ -0,0 +1,42 @@ +import { Stack, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import IconBox from "../../IconBox"; +import PropTypes from "prop-types"; + +const LegendBox = ({ children, icon, header, sx }) => { + const theme = useTheme(); + return ( + + + {icon} + {header} + + {children} + + ); +}; + +LegendBox.propTypes = { + children: PropTypes.node, + icon: PropTypes.node, + header: PropTypes.string, +}; + +export default LegendBox; diff --git a/Client/src/Components/CircularCount/index.jsx b/Client/src/Components/CircularCount/index.jsx new file mode 100644 index 000000000..8350a8c44 --- /dev/null +++ b/Client/src/Components/CircularCount/index.jsx @@ -0,0 +1,29 @@ +import { Box } from "@mui/material"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +const CircularCount = ({ count }) => { + const theme = useTheme(); + return ( + + {count} + + ); +}; + +CircularCount.propTypes = { + count: PropTypes.number, +}; + +export default CircularCount; diff --git a/Client/src/Components/Dot/index.jsx b/Client/src/Components/Dot/index.jsx new file mode 100644 index 000000000..0fd51730f --- /dev/null +++ b/Client/src/Components/Dot/index.jsx @@ -0,0 +1,23 @@ +import PropTypes from "prop-types"; + +const Dot = ({ color = "gray", size = "4px" }) => { + return ( + + ); +}; + +Dot.propTypes = { + color: PropTypes.string, + size: PropTypes.string, +}; + +export default Dot; diff --git a/Client/src/Components/Fallback/index.css b/Client/src/Components/Fallback/index.css index 9b4d6f653..cad46300b 100644 --- a/Client/src/Components/Fallback/index.css +++ b/Client/src/Components/Fallback/index.css @@ -19,6 +19,22 @@ font-size: var(--env-var-font-size-medium); } +[class*="fallback__"] .background-pattern-svg { + position: absolute; + top: 0; + left: 50%; + transform: translate(-50%, -50%); + z-index: 0; + + width: 100%; + max-width: 800px; + height: 100%; + max-height: 800px; + + background-position: center; + background-size: cover; + background-repeat: no-repeat; +} .fallback__status > .MuiStack-root { margin-left: var(--env-var-spacing-2); } diff --git a/Client/src/Components/Fallback/index.jsx b/Client/src/Components/Fallback/index.jsx index 61840452f..36c0d3eb1 100644 --- a/Client/src/Components/Fallback/index.jsx +++ b/Client/src/Components/Fallback/index.jsx @@ -26,58 +26,71 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) => const mode = useSelector((state) => state.ui.mode); return ( - - {mode === "light" ? ( - - ) : ( - - )} - - - - + ) : ( + + )} + - {vowelStart ? "An" : "A"} {title} is used to: - - {checks.map((check, index) => ( - - ))} + + + + + {vowelStart ? "An" : "A"} {title} is used to: + + {checks?.map((check, index) => ( + + ))} + + {/* TODO - display a different fallback if user is not an admin*/} + {isAdmin && ( + + )} - {/* TODO - display a different fallback if user is not an admin*/} - {isAdmin && ( - - )} - + ); }; diff --git a/Client/src/Components/GenericFallback/NetworkError.jsx b/Client/src/Components/GenericFallback/NetworkError.jsx new file mode 100644 index 000000000..b26037a57 --- /dev/null +++ b/Client/src/Components/GenericFallback/NetworkError.jsx @@ -0,0 +1,20 @@ +import { Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const NetworkError = () => { + const theme = useTheme(); + return ( + <> + + Network error + + Please check your connection + + ); +}; + +export default NetworkError; diff --git a/Client/src/Components/GenericFallback/index.jsx b/Client/src/Components/GenericFallback/index.jsx new file mode 100644 index 000000000..af9ed2ea4 --- /dev/null +++ b/Client/src/Components/GenericFallback/index.jsx @@ -0,0 +1,78 @@ +import { useTheme } from "@emotion/react"; +import { Box, Stack } from "@mui/material"; +import Skeleton from "../../assets/Images/create-placeholder.svg?react"; +import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; +import Background from "../../assets/Images/background-grid.svg?react"; +import { useSelector } from "react-redux"; + +/** + * Fallback component to display a fallback UI for network errors + * + * @returns {JSX.Element} The rendered fallback UI. + */ + +const GenericFallback = ({ children }) => { + const theme = useTheme(); + const mode = useSelector((state) => state.ui.mode); + + return ( + + + {mode === "light" ? ( + + ) : ( + + )} + + + + + {children} + + + + ); +}; + +export default GenericFallback; diff --git a/Client/src/Components/Inputs/Search/index.jsx b/Client/src/Components/Inputs/Search/index.jsx index 1dff6b532..95930b0ec 100644 --- a/Client/src/Components/Inputs/Search/index.jsx +++ b/Client/src/Components/Inputs/Search/index.jsx @@ -139,7 +139,9 @@ const Search = ({ } return filtered; }} - getOptionKey={(option) => option.id} + getOptionKey={(option) => { + return option._id; + }} renderOption={(props, option) => { const { key, ...optionProps } = props; return ( @@ -196,6 +198,7 @@ const Search = ({ }; Search.propTypes = { + label: PropTypes.string, id: PropTypes.string, multiple: PropTypes.bool, options: PropTypes.array.isRequired, diff --git a/Client/src/Components/Layouts/HomeLayout/index.css b/Client/src/Components/Layouts/HomeLayout/index.css index e08904f8e..ef6f38abd 100644 --- a/Client/src/Components/Layouts/HomeLayout/index.css +++ b/Client/src/Components/Layouts/HomeLayout/index.css @@ -26,20 +26,3 @@ min-height: calc(100vh - var(--env-var-spacing-2) * 2); flex: 1; } - -.home-layout > div:has(> [class*="fallback__"]) .background-pattern-svg { - position: absolute; - top: 0; - left: 50%; - transform: translate(-50%, -50%); - z-index: 0; - - width: 100%; - max-width: 800px; - height: 100%; - max-height: 800px; - - background-position: center; - background-size: cover; - background-repeat: no-repeat; -} diff --git a/Client/src/Components/MonitorCountHeader/index.jsx b/Client/src/Components/MonitorCountHeader/index.jsx new file mode 100644 index 000000000..f7c31a5a0 --- /dev/null +++ b/Client/src/Components/MonitorCountHeader/index.jsx @@ -0,0 +1,40 @@ +import { Stack } from "@mui/material"; +import { Heading } from "../Heading"; +import CircularCount from "../CircularCount"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const MonitorCountHeader = ({ + shouldRender = true, + monitorCount, + heading, + sx, + children, +}) => { + const theme = useTheme(); + if (!shouldRender) return ; + + return ( + + {heading} + + {children} + + ); +}; + +MonitorCountHeader.propTypes = { + shouldRender: PropTypes.bool, + monitorCount: PropTypes.number, + heading: PropTypes.string, + children: PropTypes.node, + sx: PropTypes.object, +}; + +export default MonitorCountHeader; diff --git a/Client/src/Components/MonitorCountHeader/skeleton.jsx b/Client/src/Components/MonitorCountHeader/skeleton.jsx new file mode 100644 index 000000000..9b4971c32 --- /dev/null +++ b/Client/src/Components/MonitorCountHeader/skeleton.jsx @@ -0,0 +1,26 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/MonitorCreateHeader/index.jsx b/Client/src/Components/MonitorCreateHeader/index.jsx new file mode 100644 index 000000000..01a102b7d --- /dev/null +++ b/Client/src/Components/MonitorCreateHeader/index.jsx @@ -0,0 +1,33 @@ +import { Stack, Button } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import PropTypes from "prop-types"; +import SkeletonLayout from "./skeleton"; + +const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => { + const navigate = useNavigate(); + if (!isAdmin) return null; + if (!shouldRender) return ; + return ( + + + + ); +}; + +export default CreateMonitorHeader; + +CreateMonitorHeader.propTypes = { + isAdmin: PropTypes.bool.isRequired, + shouldRender: PropTypes.bool, + path: PropTypes.string.isRequired, +}; diff --git a/Client/src/Components/MonitorCreateHeader/skeleton.jsx b/Client/src/Components/MonitorCreateHeader/skeleton.jsx new file mode 100644 index 000000000..7583f7849 --- /dev/null +++ b/Client/src/Components/MonitorCreateHeader/skeleton.jsx @@ -0,0 +1,19 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx b/Client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx new file mode 100644 index 000000000..c966abf33 --- /dev/null +++ b/Client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx @@ -0,0 +1,41 @@ +import { Button, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; +import PropTypes from "prop-types"; +const ConfigButton = ({ shouldRender = true, monitorId, path }) => { + const theme = useTheme(); + const navigate = useNavigate(); + + if (!shouldRender) return null; + + return ( + + + + ); +}; + +ConfigButton.propTypes = { + shouldRender: PropTypes.bool, + monitorId: PropTypes.string.isRequired, + path: PropTypes.string.isRequired, +}; + +export default ConfigButton; diff --git a/Client/src/Components/MonitorStatusHeader/index.jsx b/Client/src/Components/MonitorStatusHeader/index.jsx new file mode 100644 index 000000000..cbbba52b2 --- /dev/null +++ b/Client/src/Components/MonitorStatusHeader/index.jsx @@ -0,0 +1,56 @@ +import { Stack, Typography } from "@mui/material"; +import PulseDot from "../Animated/PulseDot"; +import Dot from "../Dot"; +import { useTheme } from "@emotion/react"; +import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; +import { formatDurationRounded } from "../../Utils/timeUtils"; +import ConfigButton from "./ConfigButton"; +import SkeletonLayout from "./skeleton"; +import PropTypes from "prop-types"; + +const MonitorStatusHeader = ({ path, shouldRender = true, isAdmin, monitor }) => { + const theme = useTheme(); + const { statusColor, statusMsg, determineState } = useUtils(); + if (!shouldRender) { + return ; + } + + return ( + + + {monitor?.name} + + + + {monitor?.url?.replace(/^https?:\/\//, "") || "..."} + + + + Checking every {formatDurationRounded(monitor?.interval)}. + + + + + + ); +}; + +MonitorStatusHeader.propTypes = { + path: PropTypes.string.isRequired, + shouldRender: PropTypes.bool, + isAdmin: PropTypes.bool, + monitor: PropTypes.object, +}; + +export default MonitorStatusHeader; diff --git a/Client/src/Components/MonitorStatusHeader/skeleton.jsx b/Client/src/Components/MonitorStatusHeader/skeleton.jsx new file mode 100644 index 000000000..64dc0547f --- /dev/null +++ b/Client/src/Components/MonitorStatusHeader/skeleton.jsx @@ -0,0 +1,23 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/MonitorTimeFrameHeader/index.jsx b/Client/src/Components/MonitorTimeFrameHeader/index.jsx new file mode 100644 index 000000000..e43d63494 --- /dev/null +++ b/Client/src/Components/MonitorTimeFrameHeader/index.jsx @@ -0,0 +1,72 @@ +import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; +import PropTypes from "prop-types"; + +const MonitorTimeFrameHeader = ({ + shouldRender = true, + hasDateRange = true, + dateRange, + setDateRange, +}) => { + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + let timeFramePicker = null; + + if (hasDateRange) { + timeFramePicker = ( + + + + + + ); + } + + return ( + + + Showing statistics for past{" "} + {dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}. + + {timeFramePicker} + + ); +}; + +MonitorTimeFrameHeader.propTypes = { + shouldRender: PropTypes.bool, + hasDateRange: PropTypes.bool, + dateRange: PropTypes.string, + setDateRange: PropTypes.func, +}; + +export default MonitorTimeFrameHeader; diff --git a/Client/src/Components/MonitorTimeFrameHeader/skeleton.jsx b/Client/src/Components/MonitorTimeFrameHeader/skeleton.jsx new file mode 100644 index 000000000..d7c395d54 --- /dev/null +++ b/Client/src/Components/MonitorTimeFrameHeader/skeleton.jsx @@ -0,0 +1,23 @@ +import { Stack, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/StatBox/index.jsx b/Client/src/Components/StatBox/index.jsx index 953d378ec..4b1fd2454 100644 --- a/Client/src/Components/StatBox/index.jsx +++ b/Client/src/Components/StatBox/index.jsx @@ -1,7 +1,7 @@ import { Box, Typography } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import useUtils from "../../Pages/Uptime/utils"; +import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; /** * StatBox Component diff --git a/Client/src/Components/StatusBoxes/index.jsx b/Client/src/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..a2c627d19 --- /dev/null +++ b/Client/src/Components/StatusBoxes/index.jsx @@ -0,0 +1,34 @@ +// Components +import { Stack } from "@mui/material"; +import SkeletonLayout from "./skeleton"; +// Utils +import { useTheme } from "@mui/material/styles"; +import PropTypes from "prop-types"; +const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => { + const theme = useTheme(); + if (!shouldRender) { + return ( + + ); + } + + return ( + + {children} + + ); +}; + +StatusBoxes.propTypes = { + shouldRender: PropTypes.bool, + children: PropTypes.node, +}; + +export default StatusBoxes; diff --git a/Client/src/Components/StatusBoxes/skeleton.jsx b/Client/src/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..bc8b18251 --- /dev/null +++ b/Client/src/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,33 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +const SkeletonLayout = ({ numBoxes, flexWrap }) => { + const theme = useTheme(); + return ( + + {Array.from({ length: numBoxes }).map((_, index) => { + const width = `${200 / numBoxes}%`; + return ( + + ); + })} + + ); +}; + +SkeletonLayout.propTypes = { + flexWrap: PropTypes.string, + numBoxes: PropTypes.number, +}; + +export default SkeletonLayout; diff --git a/Client/src/Components/Table/TablePagination/index.jsx b/Client/src/Components/Table/TablePagination/index.jsx index 61e89232f..8427f4270 100644 --- a/Client/src/Components/Table/TablePagination/index.jsx +++ b/Client/src/Components/Table/TablePagination/index.jsx @@ -5,9 +5,10 @@ import { TablePaginationActions } from "./Actions"; import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"; Pagination.propTypes = { - itemCount: PropTypes.number.isRequired, // Total number of items for pagination. - page: PropTypes.number.isRequired, // Current page index. - rowsPerPage: PropTypes.number.isRequired, // Number of rows displayed per page. + paginationLabel: PropTypes.string, // Label for the pagination. + itemCount: PropTypes.number, // Total number of items for pagination. + page: PropTypes.number, // Current page index. + rowsPerPage: PropTypes.number, // Number of rows displayed per page. handleChangePage: PropTypes.func.isRequired, // Function to handle page changes. handleChangeRowsPerPage: PropTypes.func, // Function to handle changes in rows per page. }; @@ -28,9 +29,9 @@ const ROWS_PER_PAGE_OPTIONS = [5, 10, 15, 25]; */ function Pagination({ paginationLabel, - itemCount, - page, - rowsPerPage, + itemCount = 0, + page = 0, + rowsPerPage = 5, handleChangePage, handleChangeRowsPerPage, }) { diff --git a/Client/src/Components/Table/index.jsx b/Client/src/Components/Table/index.jsx index 7a7048ab8..b716038e9 100644 --- a/Client/src/Components/Table/index.jsx +++ b/Client/src/Components/Table/index.jsx @@ -7,6 +7,7 @@ import { TableHead, TableRow, } from "@mui/material"; +import SkeletonLayout from "./skeleton"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; @@ -32,8 +33,20 @@ import { useTheme } from "@emotion/react"; * @returns {JSX.Element} The rendered table component. */ -const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { +const DataTable = ({ + shouldRender = true, + headers = [], + data = [], + config = { + emptyView: "No data", + onRowClick: () => {}, + }, +}) => { const theme = useTheme(); + if (!shouldRender) { + return ; + } + if ((headers?.length ?? 0) === 0) { return "No data"; } @@ -110,6 +123,7 @@ const DataTable = ({ headers, data, config = { emptyView: "No data" } }) => { }; DataTable.propTypes = { + shouldRender: PropTypes.bool, headers: PropTypes.arrayOf( PropTypes.shape({ id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, @@ -117,9 +131,9 @@ DataTable.propTypes = { render: PropTypes.func.isRequired, }) ).isRequired, - data: PropTypes.array.isRequired, + data: PropTypes.array, config: PropTypes.shape({ - onRowClick: PropTypes.func.isRequired, + onRowClick: PropTypes.func, rowSX: PropTypes.object, emptyView: PropTypes.node, }), diff --git a/Client/src/Components/Table/skeleton.jsx b/Client/src/Components/Table/skeleton.jsx new file mode 100644 index 000000000..062d39156 --- /dev/null +++ b/Client/src/Components/Table/skeleton.jsx @@ -0,0 +1,14 @@ +import { Skeleton } from "@mui/material"; + +const TableSkeleton = () => { + return ( + + ); +}; + +export default TableSkeleton; diff --git a/Client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/Client/src/Pages/Incidents/Components/IncidentTable/index.jsx new file mode 100644 index 000000000..ba7a03e05 --- /dev/null +++ b/Client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -0,0 +1,126 @@ +//Components +import Table from "../../../../Components/Table"; +import TableSkeleton from "../../../../Components/Table/skeleton"; +import Pagination from "../../../../Components/Table/TablePagination"; +import { StatusLabel } from "../../../../Components/Label"; +import { HttpStatusLabel } from "../../../../Components/HttpStatusLabel"; +import GenericFallback from "../../../../Components/GenericFallback"; +import NetworkError from "../../../../Components/GenericFallback/NetworkError"; + +//Utils +import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import useChecksFetch from "../../Hooks/useChecksFetch"; +import PropTypes from "prop-types"; + +const IncidentTable = ({ + shouldRender, + monitors, + selectedMonitor, + filter, + dateRange, +}) => { + //Redux state + const uiTimezone = useSelector((state) => state.ui.timezone); + + //Local state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const { isLoading, networkError, checks, checksCount } = useChecksFetch({ + selectedMonitor, + filter, + dateRange, + page, + rowsPerPage, + }); + + //Handlers + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + const headers = [ + { + id: "monitorName", + content: "Monitor Name", + render: (row) => monitors[row.monitorId]?.name ?? "N/A", + }, + { + id: "status", + content: "Status", + render: (row) => { + const status = row.status === true ? "up" : "down"; + return ( + + ); + }, + }, + { + id: "dateTime", + content: "Date & Time", + render: (row) => { + const formattedDate = formatDateWithTz( + row.createdAt, + "YYYY-MM-DD HH:mm:ss A", + uiTimezone + ); + return formattedDate; + }, + }, + { + id: "statusCode", + content: "Status Code", + render: (row) => , + }, + { id: "message", content: "Message", render: (row) => row.message }, + ]; + + if (!shouldRender || isLoading) return ; + + if (networkError) { + return ( + + + + ); + } + + if (!isLoading && typeof checksCount === "undefined") { + return No incidents recorded; + } + + return ( + <> + + + + ); +}; + +IncidentTable.propTypes = { + shouldRender: PropTypes.bool, + monitors: PropTypes.object, + selectedMonitor: PropTypes.string, + filter: PropTypes.string, + dateRange: PropTypes.string, +}; +export default IncidentTable; diff --git a/Client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/Client/src/Pages/Incidents/Components/OptionsHeader/index.jsx new file mode 100644 index 000000000..967acfed5 --- /dev/null +++ b/Client/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -0,0 +1,162 @@ +// Components +import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; +import Select from "../../../../Components/Inputs/Select"; +import PropTypes from "prop-types"; + +//Utils +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const OptionsHeader = ({ + shouldRender, + selectedMonitor = 0, + setSelectedMonitor, + monitors, + filter = "all", + setFilter, + dateRange = "hour", + setDateRange, +}) => { + const theme = useTheme(); + const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + + if (!shouldRender) return ; + + return ( + + + + Incidents for + + - - - - Filter by: - - - - - - - - - - Show: - - - - - - - - - - - - )} + + + + ); }; diff --git a/Client/src/Pages/Incidents/skeleton.jsx b/Client/src/Pages/Incidents/skeleton.jsx deleted file mode 100644 index 51848a108..000000000 --- a/Client/src/Pages/Incidents/skeleton.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx similarity index 95% rename from Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx rename to Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx index 3220582a8..d91e899c5 100644 --- a/Client/src/Pages/Infrastructure/CreateMonitor/CustomThreshold/index.jsx +++ b/Client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import Checkbox from "../../../../Components/Inputs/Checkbox"; +import TextInput from "../../../../../Components/Inputs/TextInput"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; @@ -117,7 +117,7 @@ CustomThreshold.propTypes = { fieldName: PropTypes.string, fieldValue: PropTypes.string.isRequired, onFieldChange: PropTypes.func.isRequired, - onFieldBlur: PropTypes.func.isRequired, + onFieldBlur: PropTypes.func, alertUnit: PropTypes.string.isRequired, infrastructureMonitor: PropTypes.object.isRequired, errors: PropTypes.object.isRequired, diff --git a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx b/Client/src/Pages/Infrastructure/Create/index.jsx similarity index 99% rename from Client/src/Pages/Infrastructure/CreateMonitor/index.jsx rename to Client/src/Pages/Infrastructure/Create/index.jsx index c51b56360..464bdeceb 100644 --- a/Client/src/Pages/Infrastructure/CreateMonitor/index.jsx +++ b/Client/src/Pages/Infrastructure/Create/index.jsx @@ -22,7 +22,7 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; import { createToast } from "../../../Utils/toastUtils"; import Checkbox from "../../../Components/Inputs/Checkbox"; import Select from "../../../Components/Inputs/Select"; -import { CustomThreshold } from "./CustomThreshold"; +import { CustomThreshold } from "./Components/CustomThreshold"; const SELECT_VALUES = [ { _id: 0.25, name: "15 seconds" }, diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx new file mode 100644 index 000000000..9544bcf2a --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx @@ -0,0 +1,37 @@ +// Components +import { Typography } from "@mui/material"; +import BaseContainer from "../BaseContainer"; +import AreaChart from "../../../../../Components/Charts/AreaChart"; +// Utils +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +const InfraAreaChart = ({ config }) => { + const theme = useTheme(); + const { getDimensions } = useHardwareUtils(); + return ( + + + {config.heading} + + + + ); +}; + +export default InfraAreaChart; diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx new file mode 100644 index 000000000..4052b11b5 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx @@ -0,0 +1,138 @@ +// Components +import { Stack } from "@mui/material"; +import InfraAreaChart from "./InfraAreaChart"; +import SkeletonLayout from "./skeleton"; + +// Utils +import { + PercentTick, + TzTick, + InfrastructureTooltip, + TemperatureTooltip, +} from "../../../../../Components/Charts/Utils/chartUtils"; +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => { + const theme = useTheme(); + const { buildTemps } = useHardwareUtils(); + + if (!shouldRender) { + return ; + } + + const { stats } = monitor ?? {}; + const { checks } = stats; + + let latestCheck = checks[0]; + const { temps, tempKeys } = buildTemps(checks); + + const configs = [ + { + type: "memory", + data: checks, + dataKeys: ["avgMemoryUsage"], + heading: "Memory usage", + strokeColor: theme.palette.accent.main, // CAIO_REVIEW + gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW + yLabel: "Memory usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + }, + { + type: "cpu", + data: checks, + dataKeys: ["avgCpuUsage"], + heading: "CPU usage", + strokeColor: theme.palette.success.main, + gradientStartColor: theme.palette.success.main, + yLabel: "CPU usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + }, + { + type: "temperature", + data: temps, + dataKeys: tempKeys, + strokeColor: theme.palette.error.main, + gradientStartColor: theme.palette.error.main, + heading: "CPU Temperature", + yLabel: "Temperature", + xTick: , + yDomain: [ + 0, + Math.max(Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, 200), + ], + toolTip: ( + + ), + }, + ...(latestCheck?.disks?.map((disk, idx) => ({ + type: "disk", + data: checks, + diskIndex: idx, + dataKeys: [`disks[${idx}].usagePercent`], + heading: `Disk${idx} usage`, + strokeColor: theme.palette.warning.main, + gradientStartColor: theme.palette.warning.main, + yLabel: "Disk Usage", + yDomain: [0, 1], + yTick: , + xTick: , + toolTip: ( + + ), + })) || []), + ]; + + return ( + *": { + flexBasis: `calc(50% - ${theme.spacing(8)})`, + maxWidth: `calc(50% - ${theme.spacing(8)})`, + }, + }} + > + {configs.map((config) => ( + + ))} + + ); +}; + +export default AreaChartBoxes; diff --git a/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx new file mode 100644 index 000000000..f787b88ec --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx @@ -0,0 +1,25 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx new file mode 100644 index 000000000..246769751 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx @@ -0,0 +1,43 @@ +/** + * Renders a base box with consistent styling + * @param {Object} props - Component properties + * @param {React.ReactNode} props.children - Child components to render inside the box + * @param {Object} props.sx - Additional styling for the box + * @returns {React.ReactElement} Styled box component + */ + +// Components +import { Box } from "@mui/material"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import PropTypes from "prop-types"; + +const BaseContainer = ({ children, sx = {} }) => { + const theme = useTheme(); + const { getDimensions } = useHardwareUtils(); + return ( + + {children} + + ); +}; + +BaseContainer.propTypes = { + children: PropTypes.node.isRequired, + sx: PropTypes.object, +}; + +export default BaseContainer; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx new file mode 100644 index 000000000..b1202682d --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx @@ -0,0 +1,62 @@ +// Components +import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import BaseContainer from "../BaseContainer"; +import { Stack, Typography, Box } from "@mui/material"; +// Utils +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; + +const Gauge = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { + const theme = useTheme(); + + return ( + + + + {heading} + + + {metricOne} + {valueOne} + + + {metricTwo} + {valueTwo} + + + + + ); +}; + +Gauge.propTypes = { + value: PropTypes.number, + heading: PropTypes.string, + metricOne: PropTypes.string, + valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), + metricTwo: PropTypes.string, + valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.node]), +}; + +export default Gauge; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx new file mode 100644 index 000000000..1d2e044ce --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -0,0 +1,80 @@ +// Components +import { Stack } from "@mui/material"; +import Gauge from "./Gauge"; +import SkeletonLayout from "./skeleton"; + +// Utils +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useTheme } from "@emotion/react"; + +const Gauges = ({ shouldRender, monitor }) => { + const { decimalToPercentage, formatBytes } = useHardwareUtils(); + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + const { stats } = monitor ?? {}; + let latestCheck = stats?.aggregateData?.latestCheck; + const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; + const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; + const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; + const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0; + const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0; + const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; + + const gauges = [ + { + type: "memory", + value: decimalToPercentage(memoryUsagePercent), + heading: "Memory usage", + metricOne: "Used", + valueOne: formatBytes(memoryUsedBytes, true), + metricTwo: "Total", + valueTwo: formatBytes(memoryTotalBytes, true), + }, + { + type: "cpu", + value: decimalToPercentage(cpuUsagePercent), + heading: "CPU usage", + metricOne: "Cores", + valueOne: cpuPhysicalCores ?? 0, + metricTwo: "Frequency", + valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`, + }, + ...(latestCheck?.disk ?? []).map((disk, idx) => ({ + type: "disk", + diskIndex: idx, + value: decimalToPercentage(disk.usage_percent), + heading: `Disk${idx} usage`, + metricOne: "Used", + valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true), + metricTwo: "Total", + valueTwo: formatBytes(disk.total_bytes, true), + })), + ]; + + return ( + + {gauges.map((gauge) => { + return ( + + ); + })} + + ); +}; + +export default Gauges; diff --git a/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx new file mode 100644 index 000000000..347a52901 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx @@ -0,0 +1,26 @@ +import { Stack, Skeleton } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + + return ( + + {Array.from({ length: 3 }).map((_, idx) => { + return ( + + ); + })} + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..aa7ab97ae --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -0,0 +1,109 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; + +//Utils +import useUtils from "../../../../../Pages/Uptime/Monitors/Hooks/useUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; + +const InfraStatBoxes = ({ shouldRender, monitor }) => { + // Utils + const { formatBytes } = useHardwareUtils(); + const { statusStyles, determineState } = useUtils(); + + const { stats, uptimePercentage } = monitor ?? {}; + const latestCheck = stats?.aggregateData?.latestCheck; + + // Get data from latest check + const physicalCores = latestCheck?.cpu?.physical_core ?? 0; + const logicalCores = latestCheck?.cpu?.logical_core ?? 0; + const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; + const cpuTemperature = + latestCheck?.cpu?.temperature?.length > 0 + ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / + latestCheck.cpu.temperature.length + : 0; + const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; + const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; + const os = latestCheck?.host?.os ?? undefined; + const platform = latestCheck?.host?.platform ?? undefined; + const osPlatform = + typeof os === "undefined" && typeof platform === "undefined" + ? undefined + : `${os} ${platform}`; + + return ( + + + + {physicalCores} + cores + + } + /> + + {logicalCores} + cores + + } + /> + + {(cpuFrequency / 1000).toFixed(2)} + Ghz + + } + /> + + {cpuTemperature.toFixed(2)} + C + + } + /> + + + + {(uptimePercentage * 100).toFixed(2)} + % + + } + /> + + + ); +}; + +export default InfraStatBoxes; diff --git a/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx new file mode 100644 index 000000000..49972debb --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx @@ -0,0 +1,38 @@ +import { useEffect, useState } from "react"; +import { useSelector } from "react-redux"; +import { networkService } from "../../../../main"; + +const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitor, setMonitor] = useState(undefined); + + const { authToken } = useSelector((state) => state.auth); + + useEffect(() => { + const fetchData = async () => { + try { + const response = await networkService.getHardwareDetailsByMonitorId({ + authToken: authToken, + monitorId: monitorId, + dateRange: dateRange, + }); + response.data.data; + setMonitor(response.data.data); + } catch (error) { + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchData(); + }, [monitorId, dateRange, authToken]); + + return { + isLoading, + networkError, + monitor, + }; +}; + +export { useHardwareMonitorsFetch }; diff --git a/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx new file mode 100644 index 000000000..bc540dfe3 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -0,0 +1,137 @@ +import { Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +// Constants +const BASE_BOX_PADDING_VERTICAL = 4; +const BASE_BOX_PADDING_HORIZONTAL = 8; +const TYPOGRAPHY_PADDING = 8; +const CHART_CONTAINER_HEIGHT = 300; + +const useHardwareUtils = () => { + const theme = useTheme(); + + const getDimensions = () => { + const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; + const totalChartContainerPadding = + parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; + return { + baseBoxPaddingVertical: BASE_BOX_PADDING_VERTICAL, + baseBoxPaddingHorizontal: BASE_BOX_PADDING_HORIZONTAL, + totalContainerPadding: parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2, + areaChartHeight: + CHART_CONTAINER_HEIGHT - totalChartContainerPadding - totalTypographyPadding, + }; + }; + + const formatBytes = (bytes, space = false) => { + if (bytes === undefined || bytes === null) + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + if (typeof bytes !== "number") + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + if (bytes === 0) + return ( + <> + {0} + {space ? " " : ""} + GB + + ); + + const GB = bytes / (1024 * 1024 * 1024); + const MB = bytes / (1024 * 1024); + + if (GB >= 1) { + return ( + <> + {Number(GB.toFixed(0))} + {space ? " " : ""} + GB + + ); + } else { + return ( + <> + {Number(MB.toFixed(0))} + MB + + ); + } + }; + + /** + * Converts a decimal value to a percentage + * + * @function decimalToPercentage + * @param {number} value - Decimal value to convert + * @returns {number} Percentage representation + * + * @example + * decimalToPercentage(0.75) // Returns 75 + * decimalToPercentage(null) // Returns 0 + */ + const decimalToPercentage = (value) => { + if (value === null || value === undefined) return 0; + return value * 100; + }; + + const buildTemps = (checks) => { + let numCores = 1; + if (checks === null) return { temps: [], tempKeys: [] }; + + for (const check of checks) { + if (check?.avgTemperature?.length > numCores) { + numCores = check.avgTemperature.length; + break; + } + } + const temps = checks.map((check) => { + // If there's no data, set the temperature to 0 + if ( + check?.avgTemperature?.length === 0 || + check?.avgTemperature === undefined || + check?.avgTemperature === null + ) { + check.avgTemperature = Array(numCores).fill(0); + } + const res = check?.avgTemperature?.reduce( + (acc, cur, idx) => { + acc[`core${idx + 1}`] = cur; + return acc; + }, + { + _id: check._id, + } + ); + return res; + }); + if (temps.length === 0 || !temps[0]) { + return { temps: [], tempKeys: [] }; + } + + return { + tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), + temps, + }; + }; + + return { + formatBytes, + decimalToPercentage, + buildTemps, + getDimensions, + }; +}; + +export { useHardwareUtils }; diff --git a/Client/src/Pages/Infrastructure/Details/empty.jsx b/Client/src/Pages/Infrastructure/Details/empty.jsx deleted file mode 100644 index ee3cdb4df..000000000 --- a/Client/src/Pages/Infrastructure/Details/empty.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import { useTheme } from "@emotion/react"; -import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; -import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; -import { Box, Typography, Stack } from "@mui/material"; -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -const Empty = ({ styles }) => { - const theme = useTheme(); - const mode = useSelector((state) => state.ui.mode); - return ( - - - {mode === "light" ? : } - - Your infrastructure dashboard will show here - - Hang tight! Data is loading - - - - ); -}; - -Empty.propTypes = { - styles: PropTypes.object, - mode: PropTypes.string, -}; - -export default Empty; diff --git a/Client/src/Pages/Infrastructure/Details/index.jsx b/Client/src/Pages/Infrastructure/Details/index.jsx index acd1bafb1..f686fa9ff 100644 --- a/Client/src/Pages/Infrastructure/Details/index.jsx +++ b/Client/src/Pages/Infrastructure/Details/index.jsx @@ -1,681 +1,100 @@ -import { useParams } from "react-router-dom"; -import { useEffect, useState } from "react"; +// Components +import { Stack, Typography } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { Button, ButtonGroup, Stack, Box, Typography } from "@mui/material"; +import MonitorStatusHeader from "../../../Components/MonitorStatusHeader"; +import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; +import StatusBoxes from "./Components/StatusBoxes"; +import GaugeBoxes from "./Components/GaugeBoxes"; +import AreaChartBoxes from "./Components/AreaChartBoxes"; +import GenericFallback from "../../../Components/GenericFallback"; + +// Utils import { useTheme } from "@emotion/react"; -import CustomGauge from "../../../Components/Charts/CustomGauge"; -import AreaChart from "../../../Components/Charts/AreaChart"; -import { useSelector } from "react-redux"; -import { networkService } from "../../../main"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import useUtils from "../../Uptime/utils"; -import { useNavigate } from "react-router-dom"; -import Empty from "./empty"; -import { logger } from "../../../Utils/Logger"; -import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils"; -import { TzTick, PercentTick } from "../../../Components/Charts/Utils/chartUtils"; -import { - InfrastructureTooltip, - TemperatureTooltip, -} from "../../../Components/Charts/Utils/chartUtils"; -import PropTypes from "prop-types"; -import StatBox from "../../../Components/StatBox"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useHardwareMonitorsFetch } from "./Hooks/useHardwareMonitorsFetch"; +import { useState } from "react"; +import { useParams } from "react-router-dom"; -const BASE_BOX_PADDING_VERTICAL = 4; -const BASE_BOX_PADDING_HORIZONTAL = 8; -const TYPOGRAPHY_PADDING = 8; -/** - * Converts bytes to gigabytes - * @param {number} bytes - Number of bytes to convert - * @returns {number} Converted value in gigabytes - */ -const formatBytes = (bytes, space = false) => { - if (bytes === undefined || bytes === null) - return ( - <> - {0} - {space ? " " : ""} - GB - - ); - if (typeof bytes !== "number") - return ( - <> - {0} - {space ? " " : ""} - GB - - ); - if (bytes === 0) - return ( - <> - {0} - {space ? " " : ""} - GB - - ); +// Constants +const BREADCRUMBS = [ + { name: "infrastructure monitors", path: "/infrastructure" }, + { name: "details", path: "" }, +]; +const InfrastructureDetails = () => { + // Redux state - const GB = bytes / (1024 * 1024 * 1024); - const MB = bytes / (1024 * 1024); + // Local state + const [dateRange, setDateRange] = useState("day"); - if (GB >= 1) { + // Utils + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { monitorId } = useParams(); + + const { isLoading, networkError, monitor } = useHardwareMonitorsFetch({ + monitorId, + dateRange, + }); + + if (networkError === true) { return ( - <> - {Number(GB.toFixed(0))} - {space ? " " : ""} - GB - - ); - } else { - return ( - <> - {Number(MB.toFixed(0))} - MB - + + + Network error + + Please check your connection + ); } -}; -/** - * Converts a decimal value to a percentage - * - * @function decimalToPercentage - * @param {number} value - Decimal value to convert - * @returns {number} Percentage representation - * - * @example - * decimalToPercentage(0.75) // Returns 75 - * decimalToPercentage(null) // Returns 0 - */ -const decimalToPercentage = (value) => { - if (value === null || value === undefined) return 0; - return value * 100; -}; - -/** - * Renders a base box with consistent styling - * @param {Object} props - Component properties - * @param {React.ReactNode} props.children - Child components to render inside the box - * @param {Object} props.sx - Additional styling for the box - * @returns {React.ReactElement} Styled box component - */ -const BaseBox = ({ children, sx = {} }) => { - const theme = useTheme(); - return ( - - {children} - - ); -}; - -BaseBox.propTypes = { - children: PropTypes.node.isRequired, - sx: PropTypes.object, -}; - -/** - * Renders a gauge box with usage visualization - * @param {Object} props - Component properties - * @param {number} props.value - Percentage value for gauge - * @param {string} props.heading - Box heading - * @param {string} props.metricOne - First metric label - * @param {string} props.valueOne - First metric value - * @param {string} props.metricTwo - Second metric label - * @param {string} props.valueTwo - Second metric value - * @returns {React.ReactElement} Gauge box component - */ -const GaugeBox = ({ value, heading, metricOne, valueOne, metricTwo, valueTwo }) => { - const theme = useTheme(); - - return ( - - - + + - {heading} - - - {metricOne} - {valueOne} - - - {metricTwo} - {valueTwo} - - + + No check history for htis monitor yet. + - - ); -}; + ); + } -GaugeBox.propTypes = { - value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - heading: PropTypes.string.isRequired, - metricOne: PropTypes.string.isRequired, - valueOne: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]) - .isRequired, - metricTwo: PropTypes.string.isRequired, - valueTwo: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.element]) - .isRequired, -}; - -/** - * Renders the infrastructure details page - * @returns {React.ReactElement} Infrastructure details page component - */ -const InfrastructureDetails = () => { - const navigate = useNavigate(); - const theme = useTheme(); - const { monitorId } = useParams(); - const navList = [ - { name: "infrastructure monitors", path: "/infrastructure" }, - { name: "details", path: `/infrastructure/${monitorId}` }, - ]; - const [monitor, setMonitor] = useState(null); - const { authToken } = useSelector((state) => state.auth); - const [dateRange, setDateRange] = useState("day"); - const { statusColor, statusStyles, determineState } = useUtils(); - // These calculations are needed because ResponsiveContainer - // doesn't take padding of parent/siblings into account - // when calculating height. - const chartContainerHeight = 300; - const totalChartContainerPadding = - parseInt(theme.spacing(BASE_BOX_PADDING_VERTICAL), 10) * 2; - const totalTypographyPadding = parseInt(theme.spacing(TYPOGRAPHY_PADDING), 10) * 2; - const areaChartHeight = - (chartContainerHeight - totalChartContainerPadding - totalTypographyPadding) * 0.95; - // end height calculations - - const buildStatBoxes = (stats, uptime) => { - if (Object.keys(stats).length === 0) return []; - let latestCheck = stats?.aggregateData?.latestCheck ?? null; - if (latestCheck === null) return []; - - // Extract values from latest check - const physicalCores = latestCheck?.cpu?.physical_core ?? 0; - const logicalCores = latestCheck?.cpu?.logical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - const cpuTemperature = - latestCheck?.cpu?.temperature?.length > 0 - ? latestCheck.cpu.temperature.reduce((acc, curr) => acc + curr, 0) / - latestCheck.cpu.temperature.length - : 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const diskTotalBytes = latestCheck?.disk[0]?.total_bytes ?? 0; - const os = latestCheck?.host?.os ?? null; - const platform = latestCheck?.host?.platform ?? null; - const osPlatform = os === null && platform === null ? null : `${os} ${platform}`; - return [ - { - id: 7, - sx: statusStyles[determineState(monitor)], - heading: "Status", - subHeading: monitor?.status === true ? "Active" : "Inactive", - }, - { - id: 0, - heading: "CPU (Physical)", - subHeading: ( - <> - {physicalCores} - cores - - ), - }, - { - id: 1, - heading: "CPU (Logical)", - subHeading: ( - <> - {logicalCores} - cores - - ), - }, - { - id: 2, - heading: "CPU Frequency", - subHeading: ( - <> - {(cpuFrequency / 1000).toFixed(2)} - Ghz - - ), - }, - { - id: 3, - heading: "Average CPU Temperature", - subHeading: ( - <> - {cpuTemperature.toFixed(2)} - C - - ), - }, - { - id: 4, - heading: "Memory", - subHeading: formatBytes(memoryTotalBytes), - }, - { - id: 5, - heading: "Disk", - subHeading: formatBytes(diskTotalBytes), - }, - { - id: 6, - heading: "Uptime", - subHeading: ( - <> - {(uptime * 100).toFixed(2)} - % - - ), - }, - - { - id: 8, - heading: "OS", - subHeading: osPlatform, - }, - ]; - }; - - const buildGaugeBoxConfigs = (stats) => { - if (Object.keys(stats).length === 0) return []; - - let latestCheck = stats?.aggregateData?.latestCheck ?? null; - if (latestCheck === null) return []; - - // Extract values from latest check - const memoryUsagePercent = latestCheck?.memory?.usage_percent ?? 0; - const memoryUsedBytes = latestCheck?.memory?.used_bytes ?? 0; - const memoryTotalBytes = latestCheck?.memory?.total_bytes ?? 0; - const cpuUsagePercent = latestCheck?.cpu?.usage_percent ?? 0; - const cpuPhysicalCores = latestCheck?.cpu?.physical_core ?? 0; - const cpuFrequency = latestCheck?.cpu?.frequency ?? 0; - return [ - { - type: "memory", - value: decimalToPercentage(memoryUsagePercent), - heading: "Memory usage", - metricOne: "Used", - valueOne: formatBytes(memoryUsedBytes, true), - metricTwo: "Total", - valueTwo: formatBytes(memoryTotalBytes, true), - }, - { - type: "cpu", - value: decimalToPercentage(cpuUsagePercent), - heading: "CPU usage", - metricOne: "Cores", - valueOne: cpuPhysicalCores ?? 0, - metricTwo: "Frequency", - valueTwo: `${(cpuFrequency / 1000).toFixed(2)} Ghz`, - }, - ...(latestCheck?.disk ?? []).map((disk, idx) => ({ - type: "disk", - diskIndex: idx, - value: decimalToPercentage(disk.usage_percent), - heading: `Disk${idx} usage`, - metricOne: "Used", - valueOne: formatBytes(disk.total_bytes - disk.free_bytes, true), - metricTwo: "Total", - valueTwo: formatBytes(disk.total_bytes, true), - })), - ]; - }; - - const buildTemps = (checks) => { - let numCores = 1; - if (checks === null) return { temps: [], tempKeys: [] }; - - for (const check of checks) { - if (check?.avgTemperature?.length > numCores) { - numCores = check.avgTemperature.length; - break; - } - } - const temps = checks.map((check) => { - // If there's no data, set the temperature to 0 - if ( - check?.avgTemperature?.length === 0 || - check?.avgTemperature === undefined || - check?.avgTemperature === null - ) { - check.avgTemperature = Array(numCores).fill(0); - } - const res = check?.avgTemperature?.reduce( - (acc, cur, idx) => { - acc[`core${idx + 1}`] = cur; - return acc; - }, - { - _id: check._id, - } - ); - return res; - }); - if (temps.length === 0 || !temps[0]) { - return { temps: [], tempKeys: [] }; - } - - return { - tempKeys: Object.keys(temps[0] || {}).filter((key) => key !== "_id"), - temps, - }; - }; - - const buildAreaChartConfigs = (checks) => { - let latestCheck = checks[0] ?? null; - if (latestCheck === null) return []; - const { temps, tempKeys } = buildTemps(checks); - return [ - { - type: "memory", - data: checks, - dataKeys: ["avgMemoryUsage"], - heading: "Memory usage", - strokeColor: theme.palette.accent.main, // CAIO_REVIEW - gradientStartColor: theme.palette.accent.main, // CAIO_REVIEW - yLabel: "Memory usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "cpu", - data: checks, - dataKeys: ["avgCpuUsage"], - heading: "CPU usage", - strokeColor: theme.palette.success.main, - gradientStartColor: theme.palette.success.main, - yLabel: "CPU usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - }, - { - type: "temperature", - data: temps, - dataKeys: tempKeys, - strokeColor: theme.palette.error.main, - gradientStartColor: theme.palette.error.main, - heading: "CPU Temperature", - yLabel: "Temperature", - xTick: , - yDomain: [ - 0, - Math.max( - Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, - 200 - ), - ], - toolTip: ( - - ), - }, - ...(latestCheck?.disks?.map((disk, idx) => ({ - type: "disk", - data: checks, - diskIndex: idx, - dataKeys: [`disks[${idx}].usagePercent`], - heading: `Disk${idx} usage`, - strokeColor: theme.palette.warning.main, - gradientStartColor: theme.palette.warning.main, - yLabel: "Disk Usage", - yDomain: [0, 1], - yTick: , - xTick: , - toolTip: ( - - ), - })) || []), - ]; - }; - - // Fetch data - useEffect(() => { - const fetchData = async () => { - try { - const response = await networkService.getHardwareDetailsByMonitorId({ - authToken: authToken, - monitorId: monitorId, - dateRange: dateRange, - }); - response.data.data; - setMonitor(response.data.data); - } catch (error) { - navigate("/not-found", { replace: true }); - logger.error(error); - } - }; - fetchData(); - }, [authToken, monitorId, dateRange, navigate]); - - const statBoxConfigs = buildStatBoxes( - monitor?.stats ?? {}, - monitor?.uptimePercentage ?? "Unknown" - ); - const gaugeBoxConfigs = buildGaugeBoxConfigs(monitor?.stats ?? {}); - const areaChartConfigs = buildAreaChartConfigs(monitor?.stats?.checks ?? []); - const lastChecked = - Date.now() - new Date(monitor?.stats?.aggregateData?.latestCheck?.createdAt); return ( - - - {monitor?.stats.checks?.length > 0 ? ( - - - - - - - {monitor.name} - - {monitor.url || "..."} - - - Checking every {formatDurationRounded(monitor?.interval)} - - - Last checked {formatDurationSplit(lastChecked).time}{" "} - {formatDurationSplit(lastChecked).format} ago - - - - - {statBoxConfigs.map((statBox) => ( - - ))} - - - - {gaugeBoxConfigs.map((config) => { - return ( - - ); - })} - - - - Showing statistics for past{" "} - {dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . - - - - - - - - *": { - flexBasis: `calc(50% - ${theme.spacing(8)})`, - maxWidth: `calc(50% - ${theme.spacing(8)})`, - }, - }} - > - {areaChartConfigs.map((config) => { - return ( - - - {config.heading} - - - - ); - })} - - - ) : ( - - )} - + + + + + + + + ); }; diff --git a/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx new file mode 100644 index 000000000..f44b844a7 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -0,0 +1,134 @@ +// Components +import DataTable from "../../../../../Components/Table"; +import Host from "../../../../Uptime/Monitors/Components/Host"; +import { StatusLabel } from "../../../../../Components/Label"; +import { Stack } from "@mui/material"; +import { InfrastructureMenu } from "../MonitorsTableMenu"; +// Assets +import CPUChipIcon from "../../../../../assets/icons/cpu-chip.svg?react"; +import CustomGauge from "../../../../../Components/Charts/CustomGauge"; + +// Utils +import { useTheme } from "@emotion/react"; +import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils"; +import { useNavigate } from "react-router-dom"; + +const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete }) => { + // Utils + const theme = useTheme(); + const { determineState } = useUtils(); + const navigate = useNavigate(); + + // Handlers + const openDetails = (id) => { + navigate(`/infrastructure/${id}`); + }; + const headers = [ + { + id: "host", + content: "Host", + render: (row) => ( + + ), + }, + { + id: "status", + content: "Status", + render: (row) => ( + + ), + }, + { + id: "frequency", + content: "Frequency", + render: (row) => ( + + + {row.processor} + + ), + }, + { id: "cpu", content: "CPU", render: (row) => }, + { id: "mem", content: "Mem", render: (row) => }, + { id: "disk", content: "Disk", render: (row) => }, + { + id: "actions", + content: "Actions", + render: (row) => ( + + ), + }, + ]; + + const data = monitors?.map((monitor) => { + const processor = + ((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; + const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100; + const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100; + const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100; + const status = determineState(monitor); + const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100) + .toFixed(2) + .toString(); + const percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : theme.palette.success.main; + + return { + id: monitor._id, + name: monitor.name, + url: monitor.url, + processor, + cpu, + mem, + disk, + status, + uptimePercentage, + percentageColor, + }; + }); + + return ( + openDetails(row.id), + }} + /> + ); +}; + +export default MonitorsTable; diff --git a/Client/src/Pages/Infrastructure/components/Menu/index.jsx b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx similarity index 93% rename from Client/src/Pages/Infrastructure/components/Menu/index.jsx rename to Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx index f036b9d86..39a9d18cb 100644 --- a/Client/src/Pages/Infrastructure/components/Menu/index.jsx +++ b/Client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx @@ -4,12 +4,12 @@ import { useRef, useState } from "react"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import Settings from "../../../../assets/icons/settings-bold.svg?react"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import Dialog from "../../../../Components/Dialog"; -import { networkService } from "../../../../Utils/NetworkService.js"; +import Dialog from "../../../../../Components/Dialog"; +import { networkService } from "../../../../../Utils/NetworkService.js"; /** * InfrastructureMenu Component diff --git a/Client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..8e316b728 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx @@ -0,0 +1,45 @@ +import { useState, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useMonitorFetch = ({ page, rowsPerPage, updateTrigger }) => { + // Redux state + const { authToken, user } = useSelector((state) => state.auth); + + // Local state + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); + + useEffect(() => { + const fetchMonitors = async () => { + try { + const response = await networkService.getMonitorsByTeamId({ + authToken, + teamId: user.teamId, + limit: 1, + types: ["hardware"], + page: page, + rowsPerPage: rowsPerPage, + }); + setMonitors(response?.data?.data?.filteredMonitors ?? []); + setSummary(response?.data?.data?.summary ?? {}); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchMonitors(); + }, [page, rowsPerPage, authToken, user.teamId, updateTrigger]); + + return { monitors, summary, isLoading, networkError }; +}; + +export { useMonitorFetch }; diff --git a/Client/src/Pages/Infrastructure/Monitors/index.jsx b/Client/src/Pages/Infrastructure/Monitors/index.jsx new file mode 100644 index 000000000..00eb48330 --- /dev/null +++ b/Client/src/Pages/Infrastructure/Monitors/index.jsx @@ -0,0 +1,109 @@ +// Components +import { Stack, Typography } from "@mui/material"; +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import MonitorCountHeader from "../../../Components/MonitorCountHeader"; +import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; +import MonitorsTable from "./Components/MonitorsTable"; +import Pagination from "../../..//Components/Table/TablePagination"; +import GenericFallback from "../../../Components/GenericFallback"; +import Fallback from "../../../Components/Fallback"; +// Utils +import { useTheme } from "@emotion/react"; +import { useMonitorFetch } from "./Hooks/useMonitorFetch"; +import { useState } from "react"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +// Constants +const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; + +const InfrastructureMonitors = () => { + // Redux state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + const [updateTrigger, setUpdateTrigger] = useState(false); + + // Utils + const theme = useTheme(); + const isAdmin = useIsAdmin(); + + // Handlers + const handleActionMenuDelete = () => { + setUpdateTrigger(!updateTrigger); + }; + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + const { monitors, summary, isLoading, networkError } = useMonitorFetch({ + page, + rowsPerPage, + updateTrigger, + }); + + if (networkError === true) { + return ( + + + Network error + + Please check your connection + + ); + } + + if (!isLoading && monitors?.length === 0) { + return ( + + ); + } + + return ( + + + + + + + + ); +}; + +export default InfrastructureMonitors; diff --git a/Client/src/Pages/Infrastructure/index.jsx b/Client/src/Pages/Infrastructure/index.jsx deleted file mode 100644 index fbfae6a3c..000000000 --- a/Client/src/Pages/Infrastructure/index.jsx +++ /dev/null @@ -1,298 +0,0 @@ -import { useEffect, useState, useCallback } from "react"; -import { useNavigate } from "react-router-dom"; -import { /* useDispatch, */ useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import useUtils from "../Uptime/utils.jsx"; -import { jwtDecode } from "jwt-decode"; -import SkeletonLayout from "./skeleton"; -import Fallback from "../../Components/Fallback"; -// import GearIcon from "../../Assets/icons/settings-bold.svg?react"; -import CPUChipIcon from "../../assets/icons/cpu-chip.svg?react"; -import DataTable from "../../Components/Table"; -import { Box, Button, IconButton, Stack } from "@mui/material"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import { StatusLabel } from "../../Components/Label"; -import { Heading } from "../../Components/Heading"; -import Pagination from "../../Components/Table/TablePagination/index.jsx"; -// import { getInfrastructureMonitorsByTeamId } from "../../Features/InfrastructureMonitors/infrastructureMonitorsSlice"; -import { networkService } from "../../Utils/NetworkService.js"; -import CustomGauge from "../../Components/Charts/CustomGauge/index.jsx"; -import Host from "../Uptime/Home/host.jsx"; -import { useIsAdmin } from "../../Hooks/useIsAdmin.js"; -import { InfrastructureMenu } from "./components/Menu"; - -const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; - -/** - * This is the Infrastructure monitoring page. This is a work in progress - * - * @param - Define params. - * @returns {JSX.Element} The infrastructure monitoring page. - */ - -function Infrastructure() { - /* Adding this custom hook so we can avoid using the HOC approach that can lower performance (we are calling the admin logic N times on initializing the project. using a custom hook will cal it ass needed ) */ - const isAdmin = useIsAdmin(); - const theme = useTheme(); - const [isLoading, setIsLoading] = useState(true); - - const navigate = useNavigate(); - const navigateToCreate = () => navigate("/infrastructure/create"); - - const [page, setPage] = useState(0); - /* TODO refactor this, so it is not aware of the MUI implementation. First argument only exists because of MUI. This should require onlu the new page. Adapting for MUI should happen inside of table pagination component */ - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const [rowsPerPage, setRowsPerPage] = useState(5); - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value)); - setPage(0); - }; - const [monitors, setMonitors] = useState([]); - const [summary, setSummary] = useState({}); - - const { authToken } = useSelector((state) => state.auth); - const user = jwtDecode(authToken); - - const fetchMonitors = useCallback(async () => { - try { - setIsLoading(true); - const response = await networkService.getMonitorsByTeamId({ - authToken, - teamId: user.teamId, - limit: 1, - types: ["hardware"], - page: page, - rowsPerPage: rowsPerPage, - }); - setMonitors(response?.data?.data?.filteredMonitors ?? []); - setSummary(response?.data?.data?.summary ?? {}); - } catch (error) { - console.error(error); - } finally { - setIsLoading(false); - } - }, [page, rowsPerPage, authToken, user.teamId]); - - useEffect(() => { - fetchMonitors(); - }, [fetchMonitors]); - - const { determineState } = useUtils(); - // do it here - function openDetails(id) { - navigate(`/infrastructure/${id}`); - } - function handleActionMenuDelete() { - fetchMonitors(); - } - - const headers = [ - { - id: "host", - content: "Host", - render: (row) => ( - - ), - }, - { - id: "status", - content: "Status", - render: (row) => ( - - ), - }, - { - id: "frequency", - content: "Frequency", - render: (row) => ( - - - {row.processor} - - ), - }, - { id: "cpu", content: "CPU", render: (row) => }, - { id: "mem", content: "Mem", render: (row) => }, - { id: "disk", content: "Disk", render: (row) => }, - { - id: "actions", - content: "Actions", - render: (row) => ( - - - - ), - }, - ]; - - const monitorsAsRows = monitors.map((monitor) => { - const processor = - ((monitor.checks[0]?.cpu?.frequency ?? 0) / 1000).toFixed(2) + " GHz"; - const cpu = (monitor?.checks[0]?.cpu.usage_percent ?? 0) * 100; - const mem = (monitor?.checks[0]?.memory.usage_percent ?? 0) * 100; - const disk = (monitor?.checks[0]?.disk[0]?.usage_percent ?? 0) * 100; - const status = determineState(monitor); - const uptimePercentage = ((monitor?.uptimePercentage ?? 0) * 100) - .toFixed(2) - .toString(); - const percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.percentage.uptimeFair - : monitor.uptimePercentage < 0.75 - ? theme.palette.percentage.uptimeGood - : theme.palette.success.lowContrast; - return { - id: monitor._id, - name: monitor.name, - url: monitor.url, - processor, - cpu, - mem, - disk, - status, - uptimePercentage, - percentageColor, - }; - }); - - let isActuallyLoading = isLoading && monitors?.length === 0; - return ( - [class*="fallback__"])': { - position: "relative", - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - borderStyle: "dashed", - backgroundColor: theme.palette.primary.main, - overflow: "hidden", - }, - }} - > - {isActuallyLoading ? ( - - ) : monitors?.length !== 0 ? ( - - - - - {isAdmin && ( - - )} - - - - - Infrastructure monitors - {/* TODO Same as the one in UptimaDataTable. Create component */} - - {summary?.totalMonitors ?? 0} - - - - openDetails(row.id), - }} - headers={headers} - data={monitorsAsRows} - /> - - - - - ) : ( - - )} - - ); -} - -export { Infrastructure }; diff --git a/Client/src/Pages/Infrastructure/skeleton.jsx b/Client/src/Pages/Infrastructure/skeleton.jsx deleted file mode 100644 index b6d29dc38..000000000 --- a/Client/src/Pages/Infrastructure/skeleton.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/PageSpeed/Configure/index.jsx b/Client/src/Pages/PageSpeed/Configure/index.jsx index f7829ef35..a76f496cb 100644 --- a/Client/src/Pages/PageSpeed/Configure/index.jsx +++ b/Client/src/Pages/PageSpeed/Configure/index.jsx @@ -23,7 +23,7 @@ import PulseDot from "../../../Components/Animated/PulseDot"; import LoadingButton from "@mui/lab/LoadingButton"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton"; -import useUtils from "../../Uptime/utils"; +import useUtils from "../../Uptime/Monitors/Hooks/useUtils"; import "./index.css"; import Dialog from "../../../Components/Dialog"; diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.css b/Client/src/Pages/PageSpeed/Create/index.css similarity index 100% rename from Client/src/Pages/PageSpeed/CreatePageSpeed/index.css rename to Client/src/Pages/PageSpeed/Create/index.css diff --git a/Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx b/Client/src/Pages/PageSpeed/Create/index.jsx similarity index 100% rename from Client/src/Pages/PageSpeed/CreatePageSpeed/index.jsx rename to Client/src/Pages/PageSpeed/Create/index.jsx diff --git a/Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx b/Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx similarity index 95% rename from Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx rename to Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx index a854eb094..40fec0ab6 100644 --- a/Client/src/Pages/PageSpeed/Details/Charts/AreaChart.jsx +++ b/Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx @@ -11,7 +11,7 @@ import { import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; import { useSelector } from "react-redux"; const config = { @@ -211,10 +211,13 @@ CustomTick.propTypes = { * @returns {JSX.Element} The area chart component. */ -const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => { +const PageSpeedAreaChart = ({ data, monitor, metrics }) => { const theme = useTheme(); const [isHovered, setIsHovered] = useState(false); - const memoizedData = useMemo(() => processDataWithGaps(data, interval), [data[0]]); + const memoizedData = useMemo( + () => processDataWithGaps(data, monitor.interval), + [data[0]] + ); const filteredConfig = Object.keys(config).reduce((result, key) => { if (metrics[key]) { @@ -310,7 +313,7 @@ const PagespeedDetailsAreaChart = ({ data, interval, metrics }) => { ); }; -PagespeedDetailsAreaChart.propTypes = { +PageSpeedAreaChart.propTypes = { data: PropTypes.arrayOf( PropTypes.shape({ createdAt: PropTypes.string.isRequired, @@ -320,8 +323,8 @@ PagespeedDetailsAreaChart.propTypes = { seo: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, }) ).isRequired, - interval: PropTypes.number.isRequired, + monitor: PropTypes.object.isRequired, metrics: PropTypes.object, }; -export default PagespeedDetailsAreaChart; +export default PageSpeedAreaChart; diff --git a/Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx new file mode 100644 index 000000000..1d3d5ddef --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx @@ -0,0 +1,56 @@ +import { Box, Typography, Divider } from "@mui/material"; +import Checkbox from "../../../../../Components/Inputs/Checkbox"; +import MetricsIcon from "../../../../../assets/icons/ruler-icon.svg?react"; +import LegendBox from "../../../../../Components/Charts/LegendBox"; + +import { useTheme } from "@emotion/react"; + +const AreaChartLegend = ({ metrics, handleMetrics }) => { + const theme = useTheme(); + return ( + } + header="Metrics" + > + + + Shown + + + + handleMetrics("accessibility")} + /> + + handleMetrics("bestPractices")} + /> + + handleMetrics("performance")} + /> + + handleMetrics("seo")} + /> + + + ); +}; + +export default AreaChartLegend; diff --git a/Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx b/Client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx similarity index 99% rename from Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx rename to Client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx index 193841e7e..c9204c38a 100644 --- a/Client/src/Pages/PageSpeed/Details/Charts/PieChart.jsx +++ b/Client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx @@ -115,6 +115,8 @@ const weights = { const PieChart = ({ audits }) => { const theme = useTheme(); + const [highlightedItem, setHighLightedItem] = useState(null); + const [expand, setExpand] = useState(false); /** * Retrieves color properties based on the performance value. @@ -162,6 +164,8 @@ const PieChart = ({ audits }) => { */ let performance = 0; const getPieData = (audits) => { + if (typeof audits === "undefined") return undefined; + let data = []; let startAngle = 0; const padding = 3; // padding between arcs @@ -216,8 +220,6 @@ const PieChart = ({ audits }) => { const pieData = getPieData(audits); const colorMap = getColors(performance); - const [highlightedItem, setHighLightedItem] = useState(null); - const [expand, setExpand] = useState(false); return ( setExpand(false)}> {expand ? ( diff --git a/Client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/Client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx new file mode 100644 index 000000000..010716b88 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx @@ -0,0 +1,96 @@ +import { Stack, Box, Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import LegendBox from "../../../../../Components/Charts/LegendBox"; +import SpeedometerIcon from "../../../../../assets/icons/speedometer-icon.svg?react"; +import PropTypes from "prop-types"; + +const PieChartLegend = ({ audits }) => { + const theme = useTheme(); + + return ( + } + header="Performance metrics" + sx={{ flex: 1 }} + > + {typeof audits !== "undefined" && + Object.keys(audits).map((key) => { + if (key === "_id") return; + + let audit = audits[key]; + let score = audit.score * 100; + let bg = + score >= 90 + ? theme.palette.success.main + : score >= 50 + ? theme.palette.warning.main + : score >= 0 + ? theme.palette.error.main + : theme.palette.tertiary.main; + + // Find the position where the number ends and the unit begins + const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); + let value; + let unit; + if (match) { + value = match[1]; + match[2] === "s" ? (unit = "seconds") : (unit = match[2]); + } else { + value = audit.displayValue; + } + + return ( + + + + {audit.title} + + + {value} + + {unit} + + + + + + ); + })} + + ); +}; + +PieChartLegend.propTypes = { + audits: PropTypes.object, +}; + +export default PieChartLegend; diff --git a/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx new file mode 100644 index 000000000..582648e23 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx @@ -0,0 +1,55 @@ +import ChartBox from "../../../../../Components/Charts/ChartBox"; +import AreaChart from "../Charts/AreaChart"; +import AreaChartLegend from "../Charts/AreaChartLegend"; +import SkeletonLayout from "./skeleton"; +import ScoreIcon from "../../../../../assets/icons/monitor-graph-line.svg?react"; +import { Stack } from "@mui/material"; +import PropTypes from "prop-types"; + +import { useTheme } from "@emotion/react"; + +const PageSpeedAreaChart = ({ shouldRender, monitor, metrics, handleMetrics }) => { + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + const data = monitor?.checks ? [...monitor.checks].reverse() : []; + + return ( + + } + header="Score history" + height="100%" + borderRadiusRight={16} + Legend={ + + } + > + + + + ); +}; + +PageSpeedAreaChart.propTypes = { + shouldRender: PropTypes.bool, + monitor: PropTypes.object, + metrics: PropTypes.object, + handleMetrics: PropTypes.func, +}; + +export default PageSpeedAreaChart; diff --git a/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx new file mode 100644 index 000000000..b793908d4 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx @@ -0,0 +1,17 @@ +import { Box, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx new file mode 100644 index 000000000..5bce18df6 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -0,0 +1,41 @@ +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { Typography } from "@mui/material"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; + +const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { + const { time: uptimeDuration, units: uptimeUnits } = getHumanReadableDuration( + monitor?.uptimeDuration + ); + + const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration( + monitor?.lastChecked + ); + + return ( + + + {uptimeDuration} + {uptimeUnits} + ago + + } + /> + + {lastCheckTime} + {lastCheckUnits} + ago + + } + /> + + ); +}; + +export default PageSpeedStatusBoxes; diff --git a/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx new file mode 100644 index 000000000..cd220a73c --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx @@ -0,0 +1,51 @@ +import ChartBox from "../../../../../Components/Charts/ChartBox"; +import PerformanceIcon from "../../../../../assets/icons/performance-report.svg?react"; +import PieChart from "../Charts/PieChart"; +import { Typography } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PieChartLegend from "../Charts/PieChartLegend"; +import SkeletonLayout from "./skeleton"; + +const PerformanceReport = ({ shouldRender, audits }) => { + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + return ( + } + header="Performance report" + Legend={} + borderRadiusRight={16} + > + + + Values are estimated and may vary.{" "} + + See calculator + + + + ); +}; + +export default PerformanceReport; diff --git a/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx new file mode 100644 index 000000000..b793908d4 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx @@ -0,0 +1,17 @@ +import { Box, Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx b/Client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..1d15f4a42 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx @@ -0,0 +1,40 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useNavigate } from "react-router-dom"; +const useMonitorFetch = ({ authToken, monitorId }) => { + const navigate = useNavigate(); + + const [monitor, setMonitor] = useState(undefined); + const [audits, setAudits] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + useEffect(() => { + const fetchMonitor = async () => { + try { + const res = await networkService.getStatsByMonitorId({ + authToken: authToken, + monitorId: monitorId, + sortOrder: "desc", + limit: 50, + dateRange: "day", + numToDisplay: null, + normalize: null, + }); + setMonitor(res?.data?.data ?? undefined); + setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + + fetchMonitor(); + }, [authToken, monitorId, navigate]); + + return { monitor, audits, isLoading }; +}; + +export { useMonitorFetch }; diff --git a/Client/src/Pages/PageSpeed/Details/index.css b/Client/src/Pages/PageSpeed/Details/index.css deleted file mode 100644 index 0d0699b3d..000000000 --- a/Client/src/Pages/PageSpeed/Details/index.css +++ /dev/null @@ -1,18 +0,0 @@ -.page-speed-details button.MuiButtonBase-root { - min-height: 34px; -} - -.page-speed-details .MuiPieArcLabel-root { - font-size: var(--env-var-font-size-small-plus); - transition: fill 300ms ease; -} -.page-speed-details .MuiPieArcLabel-faded { - fill: rgba(0, 0, 0, 0.3); -} -.page-speed-details .pie-label, -.page-speed-details .pie-value-label { - transition: all 400ms ease; -} -.page-speed-details .MuiPieArc-root { - stroke: inherit; -} diff --git a/Client/src/Pages/PageSpeed/Details/index.jsx b/Client/src/Pages/PageSpeed/Details/index.jsx index c5badbab8..b40a4aca0 100644 --- a/Client/src/Pages/PageSpeed/Details/index.jsx +++ b/Client/src/Pages/PageSpeed/Details/index.jsx @@ -1,75 +1,36 @@ -import PropTypes from "prop-types"; -import { Box, Button, Divider, Stack, Tooltip, Typography } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { formatDurationRounded, formatDurationSplit } from "../../../Utils/timeUtils"; -import { ChartBox } from "./styled"; -import { logger } from "../../../Utils/Logger"; -import { networkService } from "../../../main"; -import SkeletonLayout from "./skeleton"; -import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; -import SpedometerIcon from "../../../assets/icons/spedometer-icon.svg?react"; -import MetricsIcon from "../../../assets/icons/ruler-icon.svg?react"; -import ScoreIcon from "../../../assets/icons/monitor-graph-line.svg?react"; -import PerformanceIcon from "../../../assets/icons/performance-report.svg?react"; +// Components +import { Stack, Typography } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import PagespeedDetailsAreaChart from "./Charts/AreaChart"; -import Checkbox from "../../../Components/Inputs/Checkbox"; -import PieChart from "./Charts/PieChart"; -import useUtils from "../../Uptime/utils"; -import "./index.css"; +import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; +import MonitorStatusHeader from "../../../Components/MonitorStatusHeader"; +import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes"; +import PageSpeedAreaChart from "./Components/PageSpeedAreaChart"; +import PerformanceReport from "./Components/PerformanceReport"; +import GenericFallback from "../../../Components/GenericFallback"; +// Utils +import { useTheme } from "@emotion/react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import StatBox from "../../../Components/StatBox"; -import IconBox from "../../../Components/IconBox"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useMonitorFetch } from "./Hooks/useMonitorFetch"; +import { useState } from "react"; +// Constants +const BREADCRUMBS = [ + { name: "pagespeed", path: "/pagespeed" }, + { name: "details", path: `` }, + // { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed? +]; const PageSpeedDetails = () => { const theme = useTheme(); - const navigate = useNavigate(); const isAdmin = useIsAdmin(); - const { statusColor, pagespeedStatusMsg, determineState } = useUtils(); - const [monitor, setMonitor] = useState({}); - const [audits, setAudits] = useState({}); const { monitorId } = useParams(); const { authToken } = useSelector((state) => state.auth); - useEffect(() => { - const fetchMonitor = async () => { - try { - const res = await networkService.getStatsByMonitorId({ - authToken: authToken, - monitorId: monitorId, - sortOrder: "desc", - limit: 50, - dateRange: "day", - numToDisplay: null, - normalize: null, - }); - setMonitor(res?.data?.data ?? {}); - setAudits(res?.data?.data?.checks?.[0]?.audits ?? {}); - } catch (error) { - logger.error(logger); - navigate("/not-found", { replace: true }); - } - }; - - fetchMonitor(); - }, [monitorId, authToken, navigate]); - - let loading = Object.keys(monitor).length === 0; - const data = monitor?.checks ? [...monitor.checks].reverse() : []; - - const splitDuration = (duration) => { - const { time, format } = formatDurationSplit(duration); - return ( - <> - {time} - {format} - - ); - }; + const { monitor, audits, isLoading, networkError } = useMonitorFetch({ + authToken, + monitorId, + }); const [metrics, setMetrics] = useState({ accessibility: true, @@ -77,371 +38,75 @@ const PageSpeedDetails = () => { performance: true, seo: true, }); + + // Handlers const handleMetrics = (id) => { setMetrics((prev) => ({ ...prev, [id]: !prev[id] })); }; + if (networkError === true) { + return ( + + + Network error + + Please check your connection + + ); + } + + // Empty view, displayed when loading is complete and there are no checks + if (!isLoading && monitor?.checks?.length === 0) { + return ( + + + + + There is no check history for this monitor yet. + + + ); + } + return ( - - {loading ? ( - - ) : ( - <> - - - - - {monitor.name} - - - - - - - - - {monitor?.url} - - - Checking every {formatDurationRounded(monitor?.interval)}. - - - - {isAdmin && ( - - )} - - - - {splitDuration(monitor?.uptimeDuration)} - ago - - } - /> - - {splitDuration(monitor?.lastChecked)} - ago - - } - /> - - - - Showing statistics for past 24 hours. - - - - - - - Score history - - - - - - - - - - Metrics - - - - - Shown - - - - handleMetrics("accessibility")} - /> - - handleMetrics("bestPractices")} - /> - - handleMetrics("performance")} - /> - - handleMetrics("seo")} - /> - - - - - - - - - - - Performance report - - - - - Values are estimated and may vary.{" "} - - See calculator - - - - - - - - - Performance Metrics - - - {Object.keys(audits).map((key) => { - if (key === "_id") return; + + + + + - let audit = audits[key]; - let score = audit.score * 100; - let bg = - score >= 90 - ? theme.palette.success.main - : score >= 50 - ? theme.palette.warning.main - : score >= 0 - ? theme.palette.error.contrastText - : theme.palette.tertiary.main; - - // Find the position where the number ends and the unit begins - const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); - let value; - let unit; - if (match) { - value = match[1]; - match[2] === "s" ? (unit = "seconds") : (unit = match[2]); - } else { - value = audit.displayValue; - } - - return ( - - - - {audit.title} - - - {value} - - {unit} - - - - - - ); - })} - - - - - )} + + ); }; -PageSpeedDetails.propTypes = { - isAdmin: PropTypes.bool, - push: PropTypes.func, -}; - export default PageSpeedDetails; diff --git a/Client/src/Pages/PageSpeed/Details/skeleton.jsx b/Client/src/Pages/PageSpeed/Details/skeleton.jsx deleted file mode 100644 index 1d55a5039..000000000 --- a/Client/src/Pages/PageSpeed/Details/skeleton.jsx +++ /dev/null @@ -1,93 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - return ( - <> - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/PageSpeed/Details/styled.jsx b/Client/src/Pages/PageSpeed/Details/styled.jsx deleted file mode 100644 index 291d0c6f2..000000000 --- a/Client/src/Pages/PageSpeed/Details/styled.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Stack, styled } from "@mui/material"; - -export const ChartBox = styled(Stack)(({ theme }) => ({ - display: "grid", - minHeight: 300, - minWidth: 250, - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: 4, - borderTopRightRadius: 16, - borderBottomRightRadius: 16, - backgroundColor: theme.palette.primary.main, - "& h2": { - color: theme.palette.primary.contrastTextSecondary, - fontSize: 15, - fontWeight: 500, - }, - "& p": { color: theme.palette.primary.contrastTextTertiary }, - "& > :nth-of-type(1)": { - gridColumn: 1, - gridRow: 1, - height: "fit-content", - paddingTop: theme.spacing(8), - paddingLeft: theme.spacing(8), - }, - "& > :nth-of-type(2)": { gridColumn: 1, gridRow: 2 }, - "& > :nth-of-type(3)": { - gridColumn: 2, - gridRow: "span 2", - padding: theme.spacing(8), - borderLeft: 1, - borderLeftStyle: "solid", - borderLeftColor: theme.palette.primary.lowContrast, - borderRadius: 16, - backgroundColor: theme.palette.primary.main, - background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`, - }, - "& path": { - transition: "stroke-width 400ms ease", - }, -})); diff --git a/Client/src/Pages/PageSpeed/card.jsx b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx similarity index 94% rename from Client/src/Pages/PageSpeed/card.jsx rename to Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index f5b0d7110..42abca268 100644 --- a/Client/src/Pages/PageSpeed/card.jsx +++ b/Client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -1,15 +1,16 @@ import PropTypes from "prop-types"; -import PageSpeedIcon from "../../assets/icons/page-speed.svg?react"; -import { StatusLabel } from "../../Components/Label"; +// import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react"; +import PageSpeedIcon from "../../../../../assets/icons/page-speed.svg?react"; +import { StatusLabel } from "../../../../../Components/Label"; import { Box, Grid, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "../../Utils/timeUtils"; -import useUtils from "../Uptime/utils"; +import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils"; +import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils"; import { useState } from "react"; -import IconBox from "../../Components/IconBox"; +import IconBox from "../../../../../Components/IconBox"; /** * CustomToolTip displays a tooltip with formatted date and score information. * @param {Object} props diff --git a/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx new file mode 100644 index 000000000..6266ebdc9 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -0,0 +1,20 @@ +import { Grid, Grid2 } from "@mui/material"; +import Card from "../Card"; + +const MonitorGrid = ({ shouldRender, monitors }) => { + return ( + + {monitors?.map((monitor) => ( + + ))} + + ); +}; + +export default MonitorGrid; diff --git a/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx b/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx new file mode 100644 index 000000000..72bfbfc35 --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; + +const useMonitorsFetch = ({ authToken, teamId }) => { + const [isLoading, setIsLoading] = useState(true); + const [monitors, setMonitors] = useState([]); + const [summary, setSummary] = useState({}); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken: authToken, + teamId: teamId, + limit: 10, + types: ["pagespeed"], + page: null, + rowsPerPage: null, + filter: null, + field: null, + order: null, + }); + if (res?.data?.data?.filteredMonitors) { + setMonitors(res.data.data.filteredMonitors); + setSummary(res.data.data.summary); + } + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + + fetchMonitors(); + }, [authToken, teamId]); + return { isLoading, monitors, summary, networkError }; +}; + +export default useMonitorsFetch; diff --git a/Client/src/Pages/PageSpeed/Monitors/index.jsx b/Client/src/Pages/PageSpeed/Monitors/index.jsx new file mode 100644 index 000000000..ea3ed93af --- /dev/null +++ b/Client/src/Pages/PageSpeed/Monitors/index.jsx @@ -0,0 +1,82 @@ +// Components +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import { Stack, Typography } from "@mui/material"; +import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; +import MonitorCountHeader from "../../../Components/MonitorCountHeader"; +import MonitorGrid from "./Components/MonitorGrid"; +import Fallback from "../../../Components/Fallback"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useSelector } from "react-redux"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import useMonitorsFetch from "./Hooks/useMonitorsFetch"; +import GenericFallback from "../../../Components/GenericFallback"; + +// Constants +const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; + +const PageSpeed = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const { user, authToken } = useSelector((state) => state.auth); + + const { isLoading, monitors, summary, networkError } = useMonitorsFetch({ + authToken: authToken, + teamId: user.teamId, + }); + + if (networkError === true) { + return ( + + + Network error + + Please check your connection + + ); + } + + if (!isLoading && monitors?.length === 0) { + return ( + + ); + } + + return ( + + + + + + + ); +}; + +export default PageSpeed; diff --git a/Client/src/Pages/PageSpeed/index.css b/Client/src/Pages/PageSpeed/index.css deleted file mode 100644 index 3741a6f0f..000000000 --- a/Client/src/Pages/PageSpeed/index.css +++ /dev/null @@ -1,8 +0,0 @@ -.page-speed .label { - padding: 7px; - height: 24px; - font-size: var(--env-var-font-size-small); -} -.page-speed:not(:has([class*="fallback__"])) button { - min-height: 34px; -} diff --git a/Client/src/Pages/PageSpeed/index.jsx b/Client/src/Pages/PageSpeed/index.jsx deleted file mode 100644 index c15ed5296..000000000 --- a/Client/src/Pages/PageSpeed/index.jsx +++ /dev/null @@ -1,147 +0,0 @@ -import { Box, Button, Grid, Stack } from "@mui/material"; -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useSelector } from "react-redux"; -import Fallback from "../../Components/Fallback"; -import "./index.css"; -import { useNavigate } from "react-router"; -import PropTypes from "prop-types"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import SkeletonLayout from "./skeleton"; -import Card from "./card"; -import { networkService } from "../../main"; -import { Heading } from "../../Components/Heading"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; -const PageSpeed = () => { - const theme = useTheme(); - const navigate = useNavigate(); - const isAdmin = useIsAdmin(); - const { user, authToken } = useSelector((state) => state.auth); - const [isLoading, setIsLoading] = useState(true); - const [monitors, setMonitors] = useState([]); - const [summary, setSummary] = useState({}); - - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsByTeamId({ - authToken: authToken, - teamId: user.teamId, - limit: 10, - types: ["pagespeed"], - page: null, - rowsPerPage: null, - filter: null, - field: null, - order: null, - }); - if (res?.data?.data?.filteredMonitors) { - setMonitors(res.data.data.filteredMonitors); - setSummary(res.data.data.summary); - } - } catch (error) { - console.log(error); - } finally { - setIsLoading(false); - } - }; - - fetchMonitors(); - }, [authToken, user.teamId]); - - // will show skeletons only on initial load - // since monitor state is being added to redux persist, there's no reason to display skeletons on every render - let isActuallyLoading = isLoading && monitors?.length === 0; - return ( - [class*="fallback__"])': { - position: "relative", - border: 1, - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - borderStyle: "dashed", - backgroundColor: theme.palette.primary.main, - overflow: "hidden", - }, - }} - > - {isActuallyLoading ? ( - - ) : monitors?.length !== 0 ? ( - - - - - {isAdmin && ( - - )} - - - - PageSpeed monitors - {/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */} - - {summary?.totalMonitors ?? 0} - - - - {monitors?.map((monitor) => ( - - ))} - - - ) : ( - - )} - - ); -}; -PageSpeed.propTypes = { - isAdmin: PropTypes.bool, -}; - -export default PageSpeed; diff --git a/Client/src/Pages/PageSpeed/skeleton.jsx b/Client/src/Pages/PageSpeed/skeleton.jsx deleted file mode 100644 index b6d29dc38..000000000 --- a/Client/src/Pages/PageSpeed/skeleton.jsx +++ /dev/null @@ -1,73 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Configure/index.jsx b/Client/src/Pages/Uptime/Configure/index.jsx index bdb90891b..4f281a763 100644 --- a/Client/src/Pages/Uptime/Configure/index.jsx +++ b/Client/src/Pages/Uptime/Configure/index.jsx @@ -347,7 +347,7 @@ const Configure = () => { } + startAdornment={monitor?.type === "http" && } id="monitor-url" label="URL to monitor" placeholder="google.com" diff --git a/Client/src/Pages/Uptime/CreateUptime/index.css b/Client/src/Pages/Uptime/Create/index.css similarity index 100% rename from Client/src/Pages/Uptime/CreateUptime/index.css rename to Client/src/Pages/Uptime/Create/index.css diff --git a/Client/src/Pages/Uptime/CreateUptime/index.jsx b/Client/src/Pages/Uptime/Create/index.jsx similarity index 97% rename from Client/src/Pages/Uptime/CreateUptime/index.jsx rename to Client/src/Pages/Uptime/Create/index.jsx index 762d8e764..a0fcce43b 100644 --- a/Client/src/Pages/Uptime/CreateUptime/index.jsx +++ b/Client/src/Pages/Uptime/Create/index.jsx @@ -88,7 +88,7 @@ const CreateMonitor = () => { ? `http${https ? "s" : ""}://` + monitor.url : monitor.url, port: monitor.type === "port" ? monitor.port : undefined, - name: monitor.name === "" ? monitor.url : monitor.name, + name: monitor.name || monitor.url.substring(0, 50), type: monitor.type, interval: monitor.interval * MS_PER_MINUTE, }; @@ -122,7 +122,7 @@ const CreateMonitor = () => { form = { ...form, - description: form.name, + description: monitor.name || monitor.url, teamId: user.teamId, userId: user._id, notifications: monitor.notifications, @@ -138,17 +138,23 @@ const CreateMonitor = () => { const handleChange = (event, formName) => { const { value } = event.target; - setMonitor({ + + const newMonitor = { ...monitor, [formName]: value, - }); + }; + if (formName === 'type') { + newMonitor.url = ''; + } + setMonitor(newMonitor); const { error } = monitorValidation.validate( - { [formName]: value }, + { type: monitor.type, [formName]: value }, { abortEarly: false } ); setErrors((prev) => ({ ...prev, + url: undefined, ...(error ? { [formName]: error.details[0].message } : { [formName]: undefined }), })); }; @@ -235,53 +241,6 @@ const CreateMonitor = () => { monitor - - - - General settings - - Here you can select the URL of the host, together with the type of monitor. - - - - : null - } - label={monitorTypeMaps[monitor.type].label || "URL to monitor"} - https={https} - placeholder={monitorTypeMaps[monitor.type].placeholder || ""} - value={monitor.url} - onChange={(event) => handleChange(event, "url")} - error={errors["url"] ? true : false} - helperText={errors["url"]} - /> - handleChange(event, "port")} - error={errors["port"] ? true : false} - helperText={errors["port"]} - hidden={monitor.type !== "port"} - /> - handleChange(event, "name")} - error={errors["name"] ? true : false} - helperText={errors["name"]} - /> - - Checks to perform @@ -363,6 +322,52 @@ const CreateMonitor = () => { )} + + + General settings + + Here you can select the URL of the host, together with the type of monitor. + + + + : null + } + label={monitorTypeMaps[monitor.type].label || "URL to monitor"} + https={https} + placeholder={monitorTypeMaps[monitor.type].placeholder || ""} + value={monitor.url} + onChange={(event) => handleChange(event, "url")} + error={errors["url"] ? true : false} + helperText={errors["url"]} + /> + handleChange(event, "port")} + error={errors["port"] ? true : false} + helperText={errors["port"]} + hidden={monitor.type !== "port"} + /> + handleChange(event, "name")} + error={errors["name"] ? true : false} + helperText={errors["name"]} + /> + + Incident notifications diff --git a/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx new file mode 100644 index 000000000..77fffd497 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -0,0 +1,147 @@ +// Components +import { Stack, Typography, Box } from "@mui/material"; +import ChartBox from "../../../../../Components/Charts/ChartBox"; +import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react"; +import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react"; +import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react"; +import UpBarChart from "../Charts/UpBarChart"; +import DownBarChart from "../Charts/DownBarChart"; +import ResponseGaugeChart from "../Charts/ResponseGaugeChart"; +import SkeletonLayout from "./skeleton"; +// Utils +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; + +const ChartBoxes = ({ + shouldRender = true, + monitor, + dateRange, + uiTimezone, + dateFormat, + hoveredUptimeData, + setHoveredUptimeData, + hoveredIncidentsData, + setHoveredIncidentsData, +}) => { + const theme = useTheme(); + + if (!shouldRender) { + return ; + } + + return ( + + } + header="Uptime" + > + + + Total Checks + + {hoveredUptimeData !== null + ? hoveredUptimeData.totalChecks + : (monitor?.groupedUpChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} + + {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( + + {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)} + + )} + + + + {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"} + + + {hoveredUptimeData !== null + ? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0) + : Math.floor( + ((monitor?.upChecks?.totalChecks ?? 0) / + (monitor?.totalChecks ?? 1)) * + 100 + )} + + {hoveredUptimeData !== null ? " ms" : " %"} + + + + + + + } + header="Incidents" + > + + + + {hoveredIncidentsData !== null + ? hoveredIncidentsData.totalChecks + : (monitor?.groupedDownChecks?.reduce((count, checkGroup) => { + return count + checkGroup.totalChecks; + }, 0) ?? 0)} + + {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( + + {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)} + + )} + + + + + } + header="Average Response Time" + > + + + + ); +}; + +export default ChartBoxes; + +ChartBoxes.propTypes = { + shouldRender: PropTypes.bool, + monitor: PropTypes.object, + dateRange: PropTypes.string.isRequired, + uiTimezone: PropTypes.string.isRequired, + dateFormat: PropTypes.string.isRequired, + hoveredUptimeData: PropTypes.object, + setHoveredUptimeData: PropTypes.func, + hoveredIncidentsData: PropTypes.object, + setHoveredIncidentsData: PropTypes.func, +}; diff --git a/Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx new file mode 100644 index 000000000..750b3b20a --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx @@ -0,0 +1,30 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx similarity index 85% rename from Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx index 10494830f..8df33953f 100644 --- a/Client/src/Pages/Uptime/Details/Charts/CustomLabels.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils"; const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -34,8 +34,8 @@ CustomLabels.propTypes = { x: PropTypes.number.isRequired, width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - firstDataPoint: PropTypes.object.isRequired, - lastDataPoint: PropTypes.object.isRequired, + firstDataPoint: PropTypes.object, + lastDataPoint: PropTypes.object, type: PropTypes.string.isRequired, }; diff --git a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx similarity index 89% rename from Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx index ae2adda0f..5d6c48b58 100644 --- a/Client/src/Pages/Uptime/Details/Charts/DownBarChart.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx @@ -19,7 +19,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => { { setChartHovered(true); onBarHover({ time: null, totalChecks: 0 }); @@ -40,9 +40,9 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => { y={0} width="100%" height="100%" - firstDataPoint={monitor.groupedDownChecks?.[0] ?? {}} + firstDataPoint={monitor?.groupedDownChecks?.[0] ?? {}} lastDataPoint={ - monitor.groupedDownChecks?.[monitor.groupedDownChecks.length - 1] ?? {} + monitor?.groupedDownChecks?.[monitor?.groupedDownChecks?.length - 1] ?? {} } type={type} /> @@ -53,7 +53,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => { maxBarSize={7} background={{ fill: "transparent" }} > - {monitor.groupedDownChecks.map((entry, index) => { + {monitor?.groupedDownChecks?.map((entry, index) => { return ( { + if (!shouldRender) { + return ; + } + + return ( + } + header="Response Times" + > + + + ); +}; + +ResponseTImeChart.propTypes = { + shouldRender: PropTypes.bool, + monitor: PropTypes.object, + dateRange: PropTypes.string, +}; + +export default ResponseTImeChart; diff --git a/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx new file mode 100644 index 000000000..a70757e36 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx @@ -0,0 +1,12 @@ +import { Skeleton } from "@mui/material"; +const ResponseTimeChartSkeleton = () => { + return ( + + ); +}; + +export default ResponseTimeChartSkeleton; diff --git a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx b/Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx similarity index 90% rename from Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx rename to Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx index 223dc8eef..1df79c26e 100644 --- a/Client/src/Pages/Uptime/Details/Charts/UpBarChart.jsx +++ b/Client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx @@ -28,7 +28,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => { { setChartHovered(true); onBarHover({ time: null, totalChecks: 0, avgResponseTime: 0 }); @@ -49,8 +49,10 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => { y={0} width="100%" height="100%" - firstDataPoint={monitor.groupedUpChecks[0]} - lastDataPoint={monitor.groupedUpChecks[monitor.groupedUpChecks.length - 1]} + firstDataPoint={monitor?.groupedUpChecks?.[0]} + lastDataPoint={ + monitor?.groupedUpChecks?.[monitor?.groupedUpChecks?.length - 1] + } type={type} /> } @@ -60,7 +62,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => { maxBarSize={7} background={{ fill: "transparent" }} > - {monitor.groupedUpChecks.map((entry, index) => { + {monitor?.groupedUpChecks?.map((entry, index) => { const themeColor = getThemeColor(entry.avgResponseTime); return ( { + if (!shouldRender) { + return ; + } + + const headers = [ + { + id: "status", + content: "Status", + render: (row) => { + const status = row.status === true ? "up" : "down"; + + return ( + + ); + }, + }, + { + id: "date", + content: "Date & Time", + render: (row) => + formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone), + }, + { + id: "statusCode", + content: "Status code", + render: (row) => (row.statusCode ? row.statusCode : "N/A"), + }, + { + id: "message", + content: "Message", + render: (row) => row.message, + }, + ]; + + return ( + } + header="Response Times" + height="100%" + > +
+ + + ); +}; + +ResponseTable.propTypes = { + shouldRender: PropTypes.bool, + checks: PropTypes.array, + checksCount: PropTypes.number, + uiTimezone: PropTypes.string.isRequired, + page: PropTypes.number.isRequired, + setPage: PropTypes.func.isRequired, + rowsPerPage: PropTypes.number.isRequired, + setRowsPerPage: PropTypes.func.isRequired, +}; + +export default ResponseTable; diff --git a/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx new file mode 100644 index 000000000..2665ffaf1 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx @@ -0,0 +1,13 @@ +import { Skeleton } from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/Client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx new file mode 100644 index 000000000..5597ce2ac --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx @@ -0,0 +1,67 @@ +import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../Components/StatBox"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { useTheme } from "@mui/material/styles"; +import { Typography } from "@mui/material"; +import useUtils from "../../../Monitors/Hooks/useUtils"; + +const UptimeStatusBoxes = ({ shouldRender, monitor, certificateExpiry }) => { + const theme = useTheme(); + const { determineState } = useUtils(); + + const { time: streakTime, units: streakUnits } = getHumanReadableDuration( + monitor?.uptimeStreak + ); + + const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration( + monitor?.timeSinceLastCheck + ); + return ( + + + {streakTime} + {streakUnits} + + } + /> + + {lastCheckTime} + {lastCheckUnits} + {"ago"} + + } + /> + + {monitor?.latestResponseTime} + {"ms"} + + } + /> + + {certificateExpiry} + + } + /> + + ); +}; + +export default UptimeStatusBoxes; diff --git a/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx new file mode 100644 index 000000000..6a820ff6f --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -0,0 +1,46 @@ +import { logger } from "../../../../Utils/Logger"; +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { formatDateWithTz } from "../../../../Utils/timeUtils"; + +const useCertificateFetch = ({ + monitor, + authToken, + monitorId, + certificateDateFormat, + uiTimezone, +}) => { + const [certificateExpiry, setCertificateExpiry] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + const fetchCertificate = async () => { + if (monitor?.type !== "http") { + return; + } + + try { + setIsLoading(true); + const res = await networkService.getCertificateExpiry({ + authToken: authToken, + monitorId: monitorId, + }); + if (res?.data?.data?.certificateDate) { + const date = res.data.data.certificateDate; + setCertificateExpiry( + formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" + ); + } + } catch (error) { + setCertificateExpiry("N/A"); + logger.error(error); + } finally { + setIsLoading(false); + } + }; + fetchCertificate(); + }, [authToken, monitorId, certificateDateFormat, uiTimezone, monitor]); + return [certificateExpiry, isLoading]; +}; + +export default useCertificateFetch; diff --git a/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx new file mode 100644 index 000000000..8f57f63c0 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { useEffect } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +export const useChecksFetch = ({ + authToken, + monitorId, + dateRange, + page, + rowsPerPage, +}) => { + const [checks, setChecks] = useState(undefined); + const [checksCount, setChecksCount] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchChecks = async () => { + try { + setIsLoading(true); + const res = await networkService.getChecksByMonitor({ + authToken: authToken, + monitorId: monitorId, + sortOrder: "desc", + limit: null, + dateRange: dateRange, + filter: null, + page: page, + rowsPerPage: rowsPerPage, + }); + setChecks(res.data.data.checks); + setChecksCount(res.data.data.checksCount); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchChecks(); + }, [authToken, monitorId, dateRange, page, rowsPerPage]); + + return [checks, checksCount, isLoading, networkError]; +}; + +export default useChecksFetch; diff --git a/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx b/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx new file mode 100644 index 000000000..06d833d13 --- /dev/null +++ b/Client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx @@ -0,0 +1,34 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { useNavigate } from "react-router-dom"; +import { createToast } from "../../../../Utils/toastUtils"; + +export const useMonitorFetch = ({ authToken, monitorId, dateRange }) => { + const [networkError, setNetworkError] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [monitor, setMonitor] = useState(undefined); + const navigate = useNavigate(); + + useEffect(() => { + const fetchMonitors = async () => { + try { + const res = await networkService.getUptimeDetailsById({ + authToken: authToken, + monitorId: monitorId, + dateRange: dateRange, + normalize: true, + }); + setMonitor(res?.data?.data ?? {}); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [authToken, dateRange, monitorId, navigate]); + return [monitor, isLoading, networkError]; +}; + +export default useMonitorFetch; diff --git a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx b/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx deleted file mode 100644 index 93e9532c5..000000000 --- a/Client/src/Pages/Uptime/Details/PaginationTable/index.jsx +++ /dev/null @@ -1,163 +0,0 @@ -import PropTypes from "prop-types"; -import { - TableContainer, - Table, - TableHead, - TableRow, - TableCell, - TableBody, - PaginationItem, - Pagination, - Paper, -} from "@mui/material"; - -import { useState, useEffect } from "react"; -import { useSelector } from "react-redux"; -import { networkService } from "../../../../main"; -import { StatusLabel } from "../../../../Components/Label"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import ArrowForwardRoundedIcon from "@mui/icons-material/ArrowForwardRounded"; -import { logger } from "../../../../Utils/Logger"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; -import { useTheme } from "@emotion/react"; -const PaginationTable = ({ monitorId, dateRange }) => { - const theme = useTheme(); - const { authToken } = useSelector((state) => state.auth); - const [checks, setChecks] = useState([]); - const [checksCount, setChecksCount] = useState(0); - const [paginationController, setPaginationController] = useState({ - page: 0, - rowsPerPage: 5, - }); - const uiTimezone = useSelector((state) => state.ui.timezone); - - useEffect(() => { - setPaginationController((prevPaginationController) => ({ - ...prevPaginationController, - page: 0, - })); - }, [dateRange]); - - useEffect(() => { - const fetchPage = async () => { - try { - const res = await networkService.getChecksByMonitor({ - authToken: authToken, - monitorId: monitorId, - sortOrder: "desc", - limit: null, - dateRange: dateRange, - filter: null, - page: paginationController.page, - rowsPerPage: paginationController.rowsPerPage, - }); - setChecks(res.data.data.checks); - setChecksCount(res.data.data.checksCount); - } catch (error) { - logger.error(error); - } - }; - fetchPage(); - }, [ - authToken, - monitorId, - dateRange, - paginationController.page, - paginationController.rowsPerPage, - ]); - - const handlePageChange = (_, newPage) => { - setPaginationController({ - ...paginationController, - page: newPage - 1, // 0-indexed - }); - }; - - let paginationComponent = <>; - if (checksCount > paginationController.rowsPerPage) { - paginationComponent = ( - ( - - )} - /> - ); - } - - return ( - <> - -
- - - Status - Date & Time - Status Code - Message - - - - {checks.map((check) => { - const status = check.status === true ? "up" : "down"; - - return ( - - - - - - {formatDateWithTz( - check.createdAt, - "ddd, MMMM D, YYYY, HH:mm A", - uiTimezone - )} - - {check.statusCode ? check.statusCode : "N/A"} - {check.message} - - ); - })} - -
- - {paginationComponent} - - ); -}; - -PaginationTable.propTypes = { - monitorId: PropTypes.string.isRequired, - dateRange: PropTypes.string.isRequired, -}; - -export default PaginationTable; diff --git a/Client/src/Pages/Uptime/Details/index.css b/Client/src/Pages/Uptime/Details/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/Client/src/Pages/Uptime/Details/index.jsx b/Client/src/Pages/Uptime/Details/index.jsx index d5be671d3..3063ba27a 100644 --- a/Client/src/Pages/Uptime/Details/index.jsx +++ b/Client/src/Pages/Uptime/Details/index.jsx @@ -1,476 +1,164 @@ -import PropTypes from "prop-types"; -import { useEffect, useState, useCallback } from "react"; -import { Box, Button, Stack, Tooltip, Typography, useTheme } from "@mui/material"; -import { useSelector } from "react-redux"; -import { useNavigate, useParams } from "react-router-dom"; -import { networkService } from "../../../main"; -import { logger } from "../../../Utils/Logger"; -import MonitorDetailsAreaChart from "../../../Components/Charts/MonitorDetailsAreaChart"; -import ButtonGroup from "@mui/material/ButtonGroup"; -import SettingsIcon from "../../../assets/icons/settings-bold.svg?react"; -import UptimeIcon from "../../../assets/icons/uptime-icon.svg?react"; -import ResponseTimeIcon from "../../../assets/icons/response-time-icon.svg?react"; -import AverageResponseIcon from "../../../assets/icons/average-response-icon.svg?react"; -import IncidentsIcon from "../../../assets/icons/incidents.svg?react"; -import HistoryIcon from "../../../assets/icons/history-icon.svg?react"; -import PaginationTable from "./PaginationTable"; +// Components import Breadcrumbs from "../../../Components/Breadcrumbs"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import { ChartBox } from "./styled"; -import SkeletonLayout from "./skeleton"; -import "./index.css"; -import useUtils from "../utils"; -import { formatDateWithTz, formatDurationSplit } from "../../../Utils/timeUtils"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import IconBox from "../../../Components/IconBox"; -import StatBox from "../../../Components/StatBox"; -import UpBarChart from "./Charts/UpBarChart"; -import DownBarChart from "./Charts/DownBarChart"; -import ResponseGaugeChart from "./Charts/ResponseGaugeChart"; -/** - * Details page component displaying monitor details and related information. - * @component - */ -const DetailsPage = () => { - const theme = useTheme(); - const { statusColor, statusMsg, determineState } = useUtils(); - const isAdmin = useIsAdmin(); - const [monitor, setMonitor] = useState({}); - const { monitorId } = useParams(); - const { authToken } = useSelector((state) => state.auth); - const [dateRange, setDateRange] = useState("day"); - const [certificateExpiry, setCertificateExpiry] = useState("N/A"); - const navigate = useNavigate(); +import MonitorStatusHeader from "../../../Components/MonitorStatusHeader"; +import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; +import ChartBoxes from "./Components/ChartBoxes"; +import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; +import ResponseTable from "./Components/ResponseTable"; +import UptimeStatusBoxes from "./Components/UptimeStatusBoxes"; +import GenericFallback from "../../../Components/GenericFallback"; +// MUI Components +import { Stack, Typography } from "@mui/material"; - const certificateDateFormat = "MMM D, YYYY h A"; - const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; +// Utils +import { useState } from "react"; +import { useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import { useTheme } from "@emotion/react"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import useMonitorFetch from "./Hooks/useMonitorFetch"; +import useCertificateFetch from "./Hooks/useCertificateFetch"; +import useChecksFetch from "./Hooks/useChecksFetch"; + +// Constants +const BREADCRUMBS = [ + { name: "uptime", path: "/uptime" }, + { name: "details", path: "" }, + // { name: "details", path: `/uptime/${monitorId}` }, Is this needed? We can't click on this anywy +]; + +const certificateDateFormat = "MMM D, YYYY h A"; + +const UptimeDetails = () => { + // Redux state + const { authToken } = useSelector((state) => state.auth); const uiTimezone = useSelector((state) => state.ui.timezone); - const fetchMonitor = useCallback(async () => { - try { - const res = await networkService.getUptimeDetailsById({ - authToken: authToken, - monitorId: monitorId, - dateRange: dateRange, - normalize: true, - }); - setMonitor(res?.data?.data ?? {}); - } catch (error) { - logger.error(error); - navigate("/not-found", { replace: true }); - } - }, [authToken, monitorId, navigate, dateRange]); - - useEffect(() => { - fetchMonitor(); - }, [fetchMonitor]); - - useEffect(() => { - const fetchCertificate = async () => { - if (monitor?.type !== "http") { - return; - } - try { - const res = await networkService.getCertificateExpiry({ - authToken: authToken, - monitorId: monitorId, - }); - if (res?.data?.data?.certificateDate) { - const date = res.data.data.certificateDate; - setCertificateExpiry( - formatDateWithTz(date, certificateDateFormat, uiTimezone) ?? "N/A" - ); - } - } catch (error) { - setCertificateExpiry("N/A"); - console.error(error); - } - }; - fetchCertificate(); - }, [authToken, monitorId, monitor, uiTimezone, dateFormat]); - - const splitDuration = (duration) => { - const { time, format } = formatDurationSplit(duration); - return ( - <> - {time} - {format} - - ); - }; - - let loading = Object.keys(monitor).length === 0; - + // Local state + const [dateRange, setDateRange] = useState("day"); const [hoveredUptimeData, setHoveredUptimeData] = useState(null); const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(5); + + // Utils + const dateFormat = dateRange === "day" ? "MMM D, h A" : "MMM D"; + const { monitorId } = useParams(); + const theme = useTheme(); + const isAdmin = useIsAdmin(); + + const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({ + authToken, + monitorId, + dateRange, + }); + + const [certificateExpiry, certificateIsLoading] = useCertificateFetch({ + monitor, + authToken, + monitorId, + certificateDateFormat, + uiTimezone, + }); + + const [checks, checksCount, checksAreLoading, checksNetworkError] = useChecksFetch({ + authToken, + monitorId, + dateRange, + page, + rowsPerPage, + }); + + // Handlers + const handlePageChange = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + if (monitorNetworkError || checksNetworkError) { + return ( + + + Network error + + Please check your connection + + ); + } + + // Empty view, displayed when loading is complete and there are no checks + if (!monitorIsLoading && !checksAreLoading && checksCount === 0) { + return ( + + + + + There is no check history for this monitor yet. + + + ); + } - const BREADCRUMBS = [ - { name: "uptime", path: "/uptime" }, - { name: "details", path: `/uptime/${monitorId}` }, - ]; return ( - - {loading ? ( - - ) : ( - <> - - - - - - {monitor.name} - - - {/* TODO there is a tooltip at BarChart component. Wrap the Tooltip on our own component */} - - - - - - - {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {/* Checking every {formatDurationRounded(monitor?.interval)}. */} - - - - - {isAdmin && ( - - )} - - - - - - - {monitor?.latestResponseTime} - {"ms"} - - } - /> - - {certificateExpiry} - - } - /> - - - - - Showing statistics for past{" "} - {dateRange === "day" - ? "24 hours" - : dateRange === "week" - ? "7 days" - : "30 days"} - . - - - - - - - - - - - - - - Uptime - - - - Total Checks - - {hoveredUptimeData !== null - ? hoveredUptimeData.totalChecks - : (monitor?.groupedUpChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( - - {formatDateWithTz( - hoveredUptimeData._id, - dateFormat, - uiTimezone - )} - - )} - - - - {hoveredUptimeData !== null - ? "Avg Response Time" - : "Uptime Percentage"} - - - {hoveredUptimeData !== null - ? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0) - : Math.floor( - ((monitor?.upChecks?.totalChecks ?? 0) / - (monitor?.totalChecks ?? 1)) * - 100 - )} - - {hoveredUptimeData !== null ? " ms" : " %"} - - - - - - - - - - - - Incidents - - - Total Incidents - - {hoveredIncidentsData !== null - ? hoveredIncidentsData.totalChecks - : (monitor?.groupedDownChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredIncidentsData !== null && - hoveredIncidentsData.time !== null && ( - - {formatDateWithTz( - hoveredIncidentsData._id, - dateFormat, - uiTimezone - )} - - )} - - - - - - - - - Average Response Time - - - - - - - - - Response Times - - - - - - - - - - History - - - - - - - - - )} - + + + + + + + + + ); }; -DetailsPage.propTypes = { - isAdmin: PropTypes.bool, -}; -export default DetailsPage; +export default UptimeDetails; diff --git a/Client/src/Pages/Uptime/Details/skeleton.jsx b/Client/src/Pages/Uptime/Details/skeleton.jsx deleted file mode 100644 index 0bdc03ca8..000000000 --- a/Client/src/Pages/Uptime/Details/skeleton.jsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Box, Skeleton, Stack, useTheme } from "@mui/material"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Details/styled.jsx b/Client/src/Pages/Uptime/Details/styled.jsx deleted file mode 100644 index 164fee457..000000000 --- a/Client/src/Pages/Uptime/Details/styled.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Stack, styled } from "@mui/material"; - -export const ChartBox = styled(Stack)(({ theme }) => ({ - flex: "1 30%", - gap: theme.spacing(8), - height: 300, - minWidth: 250, - padding: theme.spacing(8), - border: 1, - borderStyle: "solid", - borderColor: theme.palette.primary.lowContrast, - borderRadius: 4, - backgroundColor: theme.palette.primary.main, - "& h2": { - color: theme.palette.primary.contrastTextSecondary, - fontSize: 15, - fontWeight: 500, - }, - "& .MuiBox-root:not(.area-tooltip) p": { - color: theme.palette.primary.contrastTextTertiary, - fontSize: 13, - }, - "& .MuiBox-root > span": { - color: theme.palette.primary.contrastText, - fontSize: 20, - "& span": { - opacity: 0.8, - marginLeft: 2, - fontSize: 15, - }, - }, - "& .MuiStack-root": { - flexDirection: "row", - gap: theme.spacing(6), - }, - "& .MuiStack-root:first-of-type": { - alignItems: "center", - }, - "& tspan, & text": { - fill: theme.palette.primary.contrastTextTertiary, - }, - "& path": { - transition: "fill 300ms ease, stroke-width 400ms ease", - }, -})); diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx b/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx deleted file mode 100644 index c17f6d74b..000000000 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/Skeleton/index.jsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Skeleton } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -const ROWS_NUMBER = 7; -const ROWS_ARRAY = Array.from({ length: ROWS_NUMBER }, (_, i) => i); - -const TableSkeleton = () => { - /* TODO Skeleton does not follow light and dark theme */ - - const headers = [ - { - id: "name", - - content: "Host", - - render: () => , - }, - { - id: "status", - content: "Status", - render: () => , - }, - { - id: "responseTime", - content: "Response Time", - render: () => , - }, - { - id: "type", - content: "Type", - render: () => , - }, - { - id: "actions", - content: "Actions", - render: () => , - }, - ]; - - return ( - - ); -}; - -export { TableSkeleton }; diff --git a/Client/src/Pages/Uptime/Home/fallback.jsx b/Client/src/Pages/Uptime/Home/fallback.jsx deleted file mode 100644 index 2606ca95f..000000000 --- a/Client/src/Pages/Uptime/Home/fallback.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react"; -import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react"; -import PropTypes from "prop-types"; - -const Fallback = ({ isAdmin }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const mode = useSelector((state) => state.ui.mode); - - return ( - - - {mode === "light" ? : } - - - No monitors found to display - - - It looks like you don’t have any monitors set up yet. - - {isAdmin && ( - - )} - - ); -}; - -Fallback.propTypes = { - isAdmin: PropTypes.bool, -}; - -export default Fallback; diff --git a/Client/src/Pages/Uptime/Home/index.css b/Client/src/Pages/Uptime/Home/index.css deleted file mode 100644 index adb1d8674..000000000 --- a/Client/src/Pages/Uptime/Home/index.css +++ /dev/null @@ -1,6 +0,0 @@ -/* TODO take these from here and declare using emotion. Plus, the values should live in the theme */ -.monitors .MuiStack-root > button:not(.MuiIconButton-root) { - font-size: var(--env-var-font-size-medium); - height: var(--env-var-height-2); - min-width: fit-content; -} diff --git a/Client/src/Pages/Uptime/Home/index.jsx b/Client/src/Pages/Uptime/Home/index.jsx deleted file mode 100644 index 446702ebb..000000000 --- a/Client/src/Pages/Uptime/Home/index.jsx +++ /dev/null @@ -1,244 +0,0 @@ -// Components -import { Box, Stack, Button } from "@mui/material"; -import Greeting from "../../../Utils/greeting"; -import SkeletonLayout from "./skeleton"; -import Fallback from "./fallback"; -import StatusBox from "./StatusBox"; -import UptimeDataTable from "./UptimeDataTable"; -import Pagination from "../../../Components/Table/TablePagination"; - -// Utils -import { useTheme } from "@emotion/react"; -import { useEffect, useState, useCallback, useMemo } from "react"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useSelector, useDispatch } from "react-redux"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import useDebounce from "../../../Utils/debounce"; -import { networkService } from "../../../main"; - -const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; - -const UptimeMonitors = () => { - // Redux state - const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage); - // Local state - const [sort, setSort] = useState({}); - const [search, setSearch] = useState(""); - const [page, setPage] = useState(0); - const [isSearching, setIsSearching] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); - const [monitors, setMonitors] = useState([]); - const [filteredMonitors, setFilteredMonitors] = useState([]); - const [monitorsSummary, setMonitorsSummary] = useState({}); - - // Utils - const debouncedFilter = useDebounce(search, 500); - const dispatch = useDispatch(); - const theme = useTheme(); - const navigate = useNavigate(); - const isAdmin = useIsAdmin(); - - const authState = useSelector((state) => state.auth); - - const fetchParams = useMemo( - () => ({ - authToken: authState.authToken, - teamId: authState.user.teamId, - sort: { field: sort.field, order: sort.order }, - filter: debouncedFilter, - page, - rowsPerPage, - }), - [authState.authToken, authState.user.teamId, sort, debouncedFilter, page, rowsPerPage] - ); - - const getMonitorWithPercentage = useCallback((monitor, theme) => { - let uptimePercentage = ""; - let percentageColor = ""; - - if (monitor.uptimePercentage !== undefined) { - uptimePercentage = - monitor.uptimePercentage === 0 - ? "0" - : (monitor.uptimePercentage * 100).toFixed(2); - - percentageColor = - monitor.uptimePercentage < 0.25 - ? theme.palette.error.main - : monitor.uptimePercentage < 0.5 - ? theme.palette.warning.main - : monitor.uptimePercentage < 0.75 - ? theme.palette.success.main - : theme.palette.success.main; - } - - return { - id: monitor._id, - name: monitor.name, - url: monitor.url, - title: monitor.name, - percentage: uptimePercentage, - percentageColor, - monitor: monitor, - }; - }, []); - - const fetchMonitors = useCallback(async () => { - try { - setIsLoading(true); - const config = fetchParams; - const res = await networkService.getMonitorsByTeamId({ - authToken: config.authToken, - teamId: config.teamId, - limit: 25, - types: ["http", "ping", "docker", "port"], - page: config.page, - rowsPerPage: config.rowsPerPage, - filter: config.filter, - field: config.sort.field, - order: config.sort.order, - }); - const { monitors, filteredMonitors, summary } = res.data.data; - const mappedMonitors = filteredMonitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(monitors); - setFilteredMonitors(mappedMonitors); - setMonitorsSummary(summary); - } catch (error) { - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - setIsSearching(false); - } - }, [fetchParams, getMonitorWithPercentage, theme]); - - useEffect(() => { - fetchMonitors(); - }, [fetchMonitors, monitorUpdateTrigger]); - - const handleChangePage = (event, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - dispatch( - setRowsPerPage({ - value: parseInt(event.target.value, 10), - table: "monitors", - }) - ); - setPage(0); - }; - - const triggerUpdate = useCallback(() => { - setMonitorUpdateTrigger((prev) => !prev); - }, []); - const totalMonitors = monitorsSummary?.totalMonitors ?? 0; - const hasMonitors = totalMonitors > 0; - const canAddMonitor = isAdmin && hasMonitors; - - return ( - - - - - {canAddMonitor && ( - <> - - - )} - - - - { - <> - {!isLoading && !hasMonitors && } - {isLoading ? ( - - ) : ( - hasMonitors && ( - <> - - - - - - - - - ) - )} - - } - - ); -}; - -export default UptimeMonitors; diff --git a/Client/src/Pages/Uptime/Home/actionsMenu.jsx b/Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx similarity index 94% rename from Client/src/Pages/Uptime/Home/actionsMenu.jsx rename to Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx index 493c5f25e..f0561d6e6 100644 --- a/Client/src/Pages/Uptime/Home/actionsMenu.jsx +++ b/Client/src/Pages/Uptime/Monitors/Components/ActionsMenu/index.jsx @@ -2,16 +2,16 @@ import { useState } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; +import { createToast } from "../../../../../Utils/toastUtils"; +import { logger } from "../../../../../Utils/Logger"; import { IconButton, Menu, MenuItem } from "@mui/material"; import { deleteUptimeMonitor, pauseUptimeMonitor, -} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Settings from "../../../assets/icons/settings-bold.svg?react"; +} from "../../../../../Features/UptimeMonitors/uptimeMonitorsSlice"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import Dialog from "../../../Components/Dialog"; +import Dialog from "../../../../../Components/Dialog"; const ActionsMenu = ({ monitor, diff --git a/Client/src/Pages/Uptime/Home/host.jsx b/Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx similarity index 52% rename from Client/src/Pages/Uptime/Home/host.jsx rename to Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx index ec78875f4..c2f082f3a 100644 --- a/Client/src/Pages/Uptime/Home/host.jsx +++ b/Client/src/Pages/Uptime/Monitors/Components/Host/index.jsx @@ -1,5 +1,7 @@ -import { Box, Typography } from "@mui/material"; +import { Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import Dot from "../../../../../Components/Dot"; /** * Host component. * This subcomponent receives a params object and displays the host details. @@ -13,44 +15,33 @@ import PropTypes from "prop-types"; * @returns {React.ElementType} Returns a div element with the host details. */ const Host = ({ url, title, percentageColor, percentage }) => { - const noTitle = title === undefined || title === url; + const theme = useTheme(); return ( - - + {title} - - {percentageColor && percentage && ( - - {percentage}% - - )} - {!noTitle && {url}} - + {percentageColor && percentage && ( + <> + + + {percentage}% + + + )} + + {url} + ); }; diff --git a/Client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx new file mode 100644 index 000000000..a649473af --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx @@ -0,0 +1,46 @@ +import { CircularProgress, Box } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import PropTypes from "prop-types"; +const LoadingSpinner = ({ shouldRender }) => { + const theme = useTheme(); + if (shouldRender === false) { + return; + } + + return ( + <> + + + + + + ); +}; + +LoadingSpinner.propTypes = { + shouldRender: PropTypes.bool, +}; + +export default LoadingSpinner; diff --git a/Client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx new file mode 100644 index 000000000..749f921a8 --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import Search from "../../../../../Components/Inputs/Search"; +import { Box } from "@mui/material"; +import useDebounce from "../../Hooks/useDebounce"; +import { useEffect, useRef } from "react"; +import PropTypes from "prop-types"; + +const SearchComponent = ({ monitors = [], onSearchChange, setIsSearching }) => { + const isFirstRender = useRef(true); + const [localSearch, setLocalSearch] = useState(""); + const debouncedSearch = useDebounce(localSearch, 500); + useEffect(() => { + if (isFirstRender.current === true) { + isFirstRender.current = false; + return; + } + onSearchChange(debouncedSearch); + setIsSearching(false); + }, [debouncedSearch, onSearchChange, setIsSearching]); + + const handleSearch = (value) => { + setLocalSearch(value); + setIsSearching(true); + }; + + return ( + + + + ); +}; + +SearchComponent.propTypes = { + monitors: PropTypes.array, + onSearchChange: PropTypes.func, + setIsSearching: PropTypes.func, +}; + +export default SearchComponent; diff --git a/Client/src/Pages/Uptime/Home/skeleton.jsx b/Client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx similarity index 100% rename from Client/src/Pages/Uptime/Home/skeleton.jsx rename to Client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx diff --git a/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx new file mode 100644 index 000000000..aa7b7ee0d --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx @@ -0,0 +1,36 @@ +import PropTypes from "prop-types"; +import { Stack } from "@mui/material"; +import StatusBox from "./statusBox"; +import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton"; + +const StatusBoxes = ({ shouldRender, monitorsSummary }) => { + const theme = useTheme(); + if (!shouldRender) return ; + return ( + + + + + + ); +}; + +StatusBoxes.propTypes = { + monitorsSummary: PropTypes.object, +}; + +export default StatusBoxes; diff --git a/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx new file mode 100644 index 000000000..df6e08ccd --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx @@ -0,0 +1,31 @@ +import { Skeleton, Stack } from "@mui/material"; +import { useTheme } from "@emotion/react"; + +const SkeletonLayout = () => { + const theme = useTheme(); + return ( + + + + + + ); +}; + +export default SkeletonLayout; diff --git a/Client/src/Pages/Uptime/Home/StatusBox.jsx b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx similarity index 60% rename from Client/src/Pages/Uptime/Home/StatusBox.jsx rename to Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index d04ebdf5f..b157857ea 100644 --- a/Client/src/Pages/Uptime/Home/StatusBox.jsx +++ b/Client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../assets/icons/clock-snooze.svg?react"; +import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react"; +import Background from "../../../../../assets/Images/background-grid.svg?react"; +import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react"; const StatusBox = ({ title, value }) => { const theme = useTheme(); @@ -52,47 +52,48 @@ const StatusBox = ({ title, value }) => { overflow="hidden" > - - {title} - - {icon} - - {value} - - + - # - + + {title} + + {icon} + + + {value} + + + # + + ); diff --git a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx similarity index 52% rename from Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx rename to Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 6961c6d5c..2824f80d6 100644 --- a/Client/src/Pages/Uptime/Home/UptimeDataTable/index.jsx +++ b/Client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,59 +1,22 @@ // Components -import { Box, Stack, CircularProgress } from "@mui/material"; -import Search from "../../../../Components/Inputs/Search"; -import { Heading } from "../../../../Components/Heading"; -import DataTable from "../../../../Components/Table"; +import { Box, Stack } from "@mui/material"; +import DataTable from "../../../../../Components/Table"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../host"; -import { StatusLabel } from "../../../../Components/Label"; -import BarChart from "../../../../Components/Charts/BarChart"; -import ActionsMenu from "../actionsMenu"; +import Host from "../Host"; +import { StatusLabel } from "../../../../../Components/Label"; +import BarChart from "../../../../../Components/Charts/BarChart"; +import ActionsMenu from "../ActionsMenu"; + +import LoadingSpinner from "../LoadingSpinner"; +import TableSkeleton from "../../../../../Components/Table/skeleton"; // Utils import { useTheme } from "@emotion/react"; -import useUtils from "../../utils"; -import { useState, memo, useCallback } from "react"; +import useUtils from "../../Hooks/useUtils"; import { useNavigate } from "react-router-dom"; -import "../index.css"; import PropTypes from "prop-types"; -const SearchComponent = memo( - ({ monitors, debouncedSearch, onSearchChange, setIsSearching }) => { - const [localSearch, setLocalSearch] = useState(debouncedSearch); - const handleSearch = useCallback( - (value) => { - setIsSearching(true); - setLocalSearch(value); - onSearchChange(value); - }, - [onSearchChange, setIsSearching] - ); - - return ( - - - - ); - } -); -SearchComponent.displayName = "SearchComponent"; -SearchComponent.propTypes = { - monitors: PropTypes.array, - debouncedSearch: PropTypes.string, - onSearchChange: PropTypes.func, - setIsSearching: PropTypes.func, -}; - /** * UptimeDataTable displays a table of uptime monitors with sorting, searching, and action capabilities * @param {Object} props - Component props @@ -79,37 +42,33 @@ SearchComponent.propTypes = { * @param {string} props.search - Current search query * @param {Function} props.setSearch - Callback to update search query * @param {boolean} props.isSearching - Whether a search is in progress - * @param {Function} props.setIsSearching - Callback to update search state * @param {Function} props.setIsLoading - Callback to update loading state * @param {Function} props.triggerUpdate - Callback to trigger a data refresh * @returns {JSX.Element} Rendered component */ const UptimeDataTable = ({ isAdmin, - isLoading, - monitors, + isSearching, + setIsLoading, filteredMonitors, - monitorCount, sort, setSort, - debouncedSearch, - setSearch, - isSearching, - setIsSearching, - setIsLoading, triggerUpdate, + monitorsAreLoading, }) => { - const { determineState } = useUtils(); - - const theme = useTheme(); + // Utils const navigate = useNavigate(); + const { determineState } = useUtils(); + const theme = useTheme(); + // Local state + // Handlers const handleSort = (field) => { let order = ""; - if (sort.field !== field) { + if (sort?.field !== field) { order = "desc"; } else { - order = sort.order === "asc" ? "desc" : "asc"; + order = sort?.order === "asc" ? "desc" : "asc"; } setSort({ field, order }); }; @@ -128,10 +87,10 @@ const UptimeDataTable = ({ - {sort.order === "asc" ? ( + {sort?.order === "asc" ? ( ) : ( @@ -164,10 +123,10 @@ const UptimeDataTable = ({ - {sort.order === "asc" ? ( + {sort?.order === "asc" ? ( ) : ( @@ -195,7 +154,9 @@ const UptimeDataTable = ({ id: "type", content: "Type", render: (row) => ( - {row.monitor.type} + + {row.monitor.type === "http" ? "HTTP(s)" : row.monitor.type} + ), }, { @@ -213,113 +174,52 @@ const UptimeDataTable = ({ }, ]; - return ( - - - Uptime monitors - {/* TODO Same as the one in Infrastructure. Create component */} - - {monitorCount} - + if (monitorsAreLoading) { + return ; + } - - - - {(isSearching || isLoading) && ( - <> - - - - - - )} - + + { - navigate(`/uptime/${row.id}`); - }, - emptyView: "No monitors found", - }} - /> - + }, + onRowClick: (row) => { + navigate(`/uptime/${row.id}`); + }, + emptyView: "No monitors found", + }} + /> ); }; -const MemoizedUptimeDataTable = memo(UptimeDataTable); -export default MemoizedUptimeDataTable; - UptimeDataTable.propTypes = { + isSearching: PropTypes.bool, + setSort: PropTypes.func, + setSearch: PropTypes.func, + setIsLoading: PropTypes.func, + triggerUpdate: PropTypes.func, + debouncedSearch: PropTypes.string, + onSearchChange: PropTypes.func, isAdmin: PropTypes.bool, isLoading: PropTypes.bool, monitors: PropTypes.array, filteredMonitors: PropTypes.array, monitorCount: PropTypes.number, + monitorsAreLoading: PropTypes.bool, sort: PropTypes.shape({ field: PropTypes.string, order: PropTypes.oneOf(["asc", "desc"]), }), - setSort: PropTypes.func, - debouncedSearch: PropTypes.string, - setSearch: PropTypes.func, - isSearching: PropTypes.bool, - setIsSearching: PropTypes.func, - setIsLoading: PropTypes.func, - triggerUpdate: PropTypes.func, }; + +export default UptimeDataTable; diff --git a/Client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx new file mode 100644 index 000000000..11d639f14 --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx @@ -0,0 +1,18 @@ +import { useState, useEffect } from "react"; + +const useDebounce = (value, delay) => { + const [debouncedValue, setDebouncedValue] = useState(value); + + useEffect(() => { + const handler = setTimeout(() => { + setDebouncedValue(value); + }, delay); + + return () => { + clearTimeout(handler); + }; + }, [value, delay]); + return debouncedValue; +}; + +export default useDebounce; diff --git a/Client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx new file mode 100644 index 000000000..ec768e9f8 --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx @@ -0,0 +1,106 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../../../../main"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useTheme } from "@emotion/react"; +const getMonitorWithPercentage = (monitor, theme) => { + let uptimePercentage = ""; + let percentageColor = ""; + + if (monitor.uptimePercentage !== undefined) { + uptimePercentage = + monitor.uptimePercentage === 0 ? "0" : (monitor.uptimePercentage * 100).toFixed(2); + + percentageColor = + monitor.uptimePercentage < 0.25 + ? theme.palette.error.main + : monitor.uptimePercentage < 0.5 + ? theme.palette.warning.main + : monitor.uptimePercentage < 0.75 + ? theme.palette.success.main + : theme.palette.success.main; + } + + return { + id: monitor._id, + name: monitor.name, + url: monitor.url, + title: monitor.name, + percentage: uptimePercentage, + percentageColor, + monitor: monitor, + }; +}; + +export const useMonitorFetch = ({ + authToken, + teamId, + limit, + page, + rowsPerPage, + filter, + field, + order, + triggerUpdate, +}) => { + const [monitorsAreLoading, setMonitorsAreLoading] = useState(false); + const [monitors, setMonitors] = useState(undefined); + const [filteredMonitors, setFilteredMonitors] = useState(undefined); + const [monitorsSummary, setMonitorsSummary] = useState(undefined); + const [networkError, setNetworkError] = useState(false); + + const theme = useTheme(); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setMonitorsAreLoading(true); + const res = await networkService.getMonitorsByTeamId({ + authToken, + teamId, + limit, + types: ["http", "ping", "docker", "port"], + page, + rowsPerPage, + filter, + field, + order, + }); + const { monitors, filteredMonitors, summary } = res.data.data; + const mappedMonitors = filteredMonitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(monitors); + setFilteredMonitors(mappedMonitors); + setMonitorsSummary(summary); + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setMonitorsAreLoading(false); + } + }; + fetchMonitors(); + }, [ + authToken, + teamId, + limit, + field, + filter, + order, + page, + rowsPerPage, + theme, + triggerUpdate, + ]); + return { + monitors, + filteredMonitors, + monitorsSummary, + monitorsAreLoading, + networkError, + }; +}; + +export default useMonitorFetch; diff --git a/Client/src/Pages/Uptime/utils.jsx b/Client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx similarity index 98% rename from Client/src/Pages/Uptime/utils.jsx rename to Client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx index 0e5fc62ec..ff3288cdf 100644 --- a/Client/src/Pages/Uptime/utils.jsx +++ b/Client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx @@ -2,6 +2,7 @@ import { useTheme } from "@mui/material"; const useUtils = () => { const determineState = (monitor) => { + if (typeof monitor === "undefined") return "pending"; if (monitor.isActive === false) return "paused"; if (monitor?.status === undefined) return "pending"; return monitor?.status == true ? "up" : "down"; diff --git a/Client/src/Pages/Uptime/Monitors/index.jsx b/Client/src/Pages/Uptime/Monitors/index.jsx new file mode 100644 index 000000000..0784b46a8 --- /dev/null +++ b/Client/src/Pages/Uptime/Monitors/index.jsx @@ -0,0 +1,193 @@ +// Components +import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Greeting from "../../../Utils/greeting"; +import StatusBoxes from "./Components/StatusBoxes"; +import UptimeDataTable from "./Components/UptimeDataTable"; +import Pagination from "../../../Components/Table/TablePagination"; +import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; +import Fallback from "../../../Components/Fallback"; +import GenericFallback from "../../../Components/GenericFallback"; +import SearchComponent from "./Components/SearchComponent"; + +import MonitorCountHeader from "../../../Components/MonitorCountHeader"; + +// MUI Components +import { Stack, Box, Button, Typography } from "@mui/material"; + +// Utils +import { useState, useCallback } from "react"; +import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useTheme } from "@emotion/react"; +import { useNavigate } from "react-router-dom"; +import useMonitorsFetch from "./Hooks/useMonitorsFetch"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import PropTypes from "prop-types"; + +const BREADCRUMBS = [{ name: `Uptime`, path: "/uptime" }]; + +const CreateMonitorButton = ({ shouldRender }) => { + // Utils + const navigate = useNavigate(); + if (shouldRender === false) { + return; + } + + return ( + + + + ); +}; + +CreateMonitorButton.propTypes = { + shouldRender: PropTypes.bool, +}; + +const UptimeMonitors = () => { + // Redux state + const { authToken, user } = useSelector((state) => state.auth); + const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage); + + // Local state + const [search, setSearch] = useState(undefined); + const [page, setPage] = useState(undefined); + const [sort, setSort] = useState(undefined); + const [isSearching, setIsSearching] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [monitorUpdateTrigger, setMonitorUpdateTrigger] = useState(false); + + // Utils + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const dispatch = useDispatch(); + + // Handlers + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + dispatch( + setRowsPerPage({ + value: parseInt(event.target.value, 10), + table: "monitors", + }) + ); + setPage(0); + }; + + const triggerUpdate = useCallback(() => { + setMonitorUpdateTrigger((prev) => !prev); + }, []); + + const teamId = user.teamId; + const { + monitorsAreLoading, + monitors, + filteredMonitors, + monitorsSummary, + networkError, + } = useMonitorsFetch({ + authToken, + teamId, + limit: 25, + page: page, + rowsPerPage: rowsPerPage, + filter: search, + field: sort?.field, + order: sort?.order, + triggerUpdate: monitorUpdateTrigger, + }); + const totalMonitors = monitorsSummary?.totalMonitors ?? 0; + + if (networkError) { + return ( + + + Network error + + Please check your connection + + ); + } + if (!isLoading && !monitorsAreLoading && totalMonitors === 0) { + return ( + + ); + } + + return ( + + + + + + + + 0 && !monitorsAreLoading} + monitorCount={totalMonitors} + heading={"Uptime monitors"} + > + + + + + + ); +}; + +export default UptimeMonitors; diff --git a/Client/src/Routes/index.jsx b/Client/src/Routes/index.jsx index b5f4dd376..555c335f7 100644 --- a/Client/src/Routes/index.jsx +++ b/Client/src/Routes/index.jsx @@ -1,35 +1,49 @@ import { Navigate, Route, Routes as LibRoutes } from "react-router"; import HomeLayout from "../Components/Layouts/HomeLayout"; -import { Infrastructure } from "../Pages/Infrastructure"; -import InfrastructureDetails from "../Pages/Infrastructure/Details"; import NotFound from "../Pages/NotFound"; -import Login from "../Pages/Auth/Login/Login"; -import Register from "../Pages/Auth/Register/Register"; -import Account from "../Pages/Account"; -import Monitors from "../Pages/Uptime/Home"; -import CreateMonitor from "../Pages/Uptime/CreateUptime"; -import CreateInfrastructureMonitor from "../Pages/Infrastructure/CreateMonitor"; -import Incidents from "../Pages/Incidents"; -import Status from "../Pages/Status"; -import Integrations from "../Pages/Integrations"; -import Settings from "../Pages/Settings"; -import ForgotPassword from "../Pages/Auth/ForgotPassword"; -import CheckEmail from "../Pages/Auth/CheckEmail"; -import SetNewPassword from "../Pages/Auth/SetNewPassword"; -import NewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; -import ProtectedRoute from "../Components/ProtectedRoute"; -import Details from "../Pages/Uptime/Details"; -import Maintenance from "../Pages/Maintenance"; -import Configure from "../Pages/Uptime/Configure"; -import PageSpeed from "../Pages/PageSpeed"; -import CreatePageSpeed from "../Pages/PageSpeed/CreatePageSpeed"; -import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance"; + +// Auth +import AuthLogin from "../Pages/Auth/Login/Login"; +import AuthRegister from "../Pages/Auth/Register/Register"; +import AuthForgotPassword from "../Pages/Auth/ForgotPassword"; +import AuthCheckEmail from "../Pages/Auth/CheckEmail"; +import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; +import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; + +// Uptime +import Uptime from "../Pages/Uptime/Monitors"; +import UptimeDetails from "../Pages/Uptime/Details"; +import UptimeCreate from "../Pages/Uptime/Create"; +import UptimeConfigure from "../Pages/Uptime/Configure"; + +// PageSpeed +import PageSpeed from "../Pages/PageSpeed/Monitors"; +import PageSpeedCreate from "../Pages/PageSpeed/Create"; import PageSpeedDetails from "../Pages/PageSpeed/Details"; import PageSpeedConfigure from "../Pages/PageSpeed/Configure"; + +// Infrastructure +import Infrastructure from "../Pages/Infrastructure/Monitors"; +import InfrastructureCreate from "../Pages/Infrastructure/Create"; +import InfrastructureDetails from "../Pages/Infrastructure/Details"; + +import Incidents from "../Pages/Incidents"; + +import Status from "../Pages/Status"; +import Integrations from "../Pages/Integrations"; + +// Settings +import Account from "../Pages/Account"; +import Settings from "../Pages/Settings"; + +import Maintenance from "../Pages/Maintenance"; + +import ProtectedRoute from "../Components/ProtectedRoute"; +import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance"; import withAdminCheck from "../Components/HOC/withAdminCheck"; const Routes = () => { - const AdminCheckedRegister = withAdminCheck(Register); + const AdminCheckedRegister = withAdminCheck(AuthRegister); return ( { /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> { /> } + element={} /> { /> } + element={} /> { } + element={} /> { } + element={} /> } + element={} /> } + element={} /> } + element={} /> } + element={} /> { const dispatch = useDispatch(); const navigate = useNavigate(); - const networkService = new NetworkService(store, dispatch, navigate); - setNetworkService(networkService); + if (!networkService) { + setNetworkService(new NetworkService(store, dispatch, navigate)); + } return children; }; diff --git a/Client/src/Utils/timeUtils.js b/Client/src/Utils/timeUtils.js index 1a84d0ea5..2fd6c6654 100644 --- a/Client/src/Utils/timeUtils.js +++ b/Client/src/Utils/timeUtils.js @@ -1,4 +1,5 @@ import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; import utc from "dayjs/plugin/utc"; import timezone from "dayjs/plugin/timezone"; import customParseFormat from "dayjs/plugin/customParseFormat"; @@ -6,6 +7,7 @@ import customParseFormat from "dayjs/plugin/customParseFormat"; dayjs.extend(utc); dayjs.extend(timezone); dayjs.extend(customParseFormat); +dayjs.extend(duration); export const MS_PER_SECOND = 1000; export const MS_PER_MINUTE = 60 * MS_PER_SECOND; @@ -75,6 +77,23 @@ export const formatDurationSplit = (ms) => { : { time: 0, format: "seconds" }; }; +export const getHumanReadableDuration = (ms) => { + const durationObj = dayjs.duration(ms); + if (durationObj.asDays() >= 1) { + const days = Math.floor(durationObj.asDays()); + return { time: days, units: days === 1 ? "day" : "days" }; + } else if (durationObj.asHours() >= 1) { + const hours = Math.floor(durationObj.asHours()); + return { time: hours, units: hours === 1 ? "hour" : "hours" }; + } else if (durationObj.asMinutes() >= 1) { + const minutes = Math.floor(durationObj.asMinutes()); + return { time: minutes, units: minutes === 1 ? "minute" : "minutes" }; + } else { + const seconds = Math.floor(durationObj.asSeconds()); + return { time: seconds, units: seconds === 1 ? "second" : "seconds" }; + } +}; + export const formatDate = (date, customOptions) => { const options = { year: "numeric", diff --git a/Client/src/Utils/toastUtils.jsx b/Client/src/Utils/toastUtils.jsx index 5920d06a7..dbec9117e 100644 --- a/Client/src/Utils/toastUtils.jsx +++ b/Client/src/Utils/toastUtils.jsx @@ -12,7 +12,7 @@ import Alert from "../Components/Alert"; */ export const createToast = ({ - variant = "secondary", + variant = "info", title, body, hasIcon = false, diff --git a/Client/src/Validation/validation.js b/Client/src/Validation/validation.js index e40a9c174..ec9245cdb 100644 --- a/Client/src/Validation/validation.js +++ b/Client/src/Validation/validation.js @@ -91,13 +91,17 @@ const credentials = joi.object({ }); const monitorValidation = joi.object({ - url: joi - .string() - .trim() - .custom((value, helpers) => { - // Regex from https://gist.github.com/dperini/729294 - var urlRegex = new RegExp( - "^" + + url: joi.when('type', { + is: 'docker', + then: joi.string() + .trim().regex(/^[a-z0-9]{64}$/), + otherwise: joi + .string() + .trim() + .custom((value, helpers) => { + // Regex from https://gist.github.com/dperini/729294 + var urlRegex = new RegExp( + "^" + // protocol identifier (optional) // short syntax // still required "(?:(?:https?|ftp):\\/\\/)?" + @@ -130,18 +134,20 @@ const monitorValidation = joi.object({ // resource path (optional) "(?:[/?#]\\S*)?" + "$", - "i" - ); - if (!urlRegex.test(value)) { - return helpers.error("string.invalidUrl"); - } + "i" + ); + if (!urlRegex.test(value)) { + return helpers.error("string.invalidUrl"); + } - return value; - }) + return value; + }) + }) .messages({ "string.empty": "This field is required.", "string.uri": "The URL you provided is not valid.", "string.invalidUrl": "Please enter a valid URL with optional port", + "string.pattern.base": "Please enter a valid container ID.", }), port: joi.number(), name: joi.string().trim().max(50).allow("").messages({ diff --git a/Client/src/assets/icons/spedometer-icon.svg b/Client/src/assets/icons/speedometer-icon.svg similarity index 100% rename from Client/src/assets/icons/spedometer-icon.svg rename to Client/src/assets/icons/speedometer-icon.svg diff --git a/README.md b/README.md index c37adfc6a..3dd59ef8f 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,13 @@ If you have any questions, suggestions or comments, please use our [Discord chan - Docker monitoring - Ping monitoring - SSL monitoring +- Port monitoring - Incidents at a glance - E-mail notifications - Scheduled maintenance **Short term roadmap:** -- Port monitoring (**complete**, waiting to be deployed to stable version) https://github.com/bluewave-labs/Checkmate/issues/1476 - Global (distributed) uptime checking on Solana network (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1593 - Status pages (**in progress**) https://github.com/bluewave-labs/Checkmate/issues/1131 - Translations (i18n) (**in progress**) diff --git a/Server/controllers/distributedUptimeController.js b/Server/controllers/distributedUptimeController.js index ce4ee2df5..5215c2cce 100644 --- a/Server/controllers/distributedUptimeController.js +++ b/Server/controllers/distributedUptimeController.js @@ -8,7 +8,6 @@ class DistributedUptimeController { async resultsCallback(req, res, next) { try { - console.log(req.body); res.status(200).json({ message: "OK" }); } catch (error) { throw handleError(error, SERVICE_NAME, "resultsCallback"); diff --git a/Server/controllers/monitorController.js b/Server/controllers/monitorController.js index 86c0fb840..ccd8ec26f 100644 --- a/Server/controllers/monitorController.js +++ b/Server/controllers/monitorController.js @@ -436,7 +436,7 @@ class MonitorController { monitor.isActive = !monitor.isActive; monitor.status = undefined; monitor.save(); - return res.ssuccess({ + return res.success({ msg: monitor.isActive ? successMessages.MONITOR_RESUME : successMessages.MONITOR_PAUSE, diff --git a/Server/db/mongo/modules/checkModule.js b/Server/db/mongo/modules/checkModule.js index 7965e7c30..6450b78a2 100644 --- a/Server/db/mongo/modules/checkModule.js +++ b/Server/db/mongo/modules/checkModule.js @@ -70,13 +70,14 @@ const createCheck = async (checkData) => { const getChecksByMonitor = async (req) => { try { const { monitorId } = req.params; - let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query; + let { sortOrder, dateRange, filter, page, rowsPerPage, status } = req.query; + status = typeof status !== "undefined" ? false : undefined; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); // Match const matchStage = { monitorId: ObjectId.createFromHexString(monitorId), - status: false, + ...(typeof status !== "undefined" && { status }), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -111,7 +112,6 @@ const getChecksByMonitor = async (req) => { if (page && rowsPerPage) { skip = page * rowsPerPage; } - const checks = await Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, @@ -123,8 +123,12 @@ const getChecksByMonitor = async (req) => { }, { $project: { - checksCount: { $arrayElemAt: ["$summary.checksCount", 0] }, - checks: "$checks", + checksCount: { + $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], + }, + checks: { + $ifNull: ["$checks", []], + }, }, }, ]); diff --git a/Server/db/mongo/modules/monitorModule.js b/Server/db/mongo/modules/monitorModule.js index 4c60e0f22..af4ea7e9f 100644 --- a/Server/db/mongo/modules/monitorModule.js +++ b/Server/db/mongo/modules/monitorModule.js @@ -340,6 +340,7 @@ const getUptimeDetailsById = async (req) => { }; const dateString = formatLookup[dateRange]; + const results = await Check.aggregate( buildUptimeDetailsPipeline(monitor, dates, dateString) ); @@ -497,7 +498,6 @@ const getMonitorById = async (monitorId) => { const getMonitorsByTeamId = async (req) => { let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - limit = parseInt(limit); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); @@ -512,7 +512,6 @@ const getMonitorsByTeamId = async (req) => { } const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; const results = await Monitor.aggregate([ { $match: matchStage }, @@ -839,3 +838,22 @@ export { groupChecksByTime, calculateGroupStats, }; + +// limit 25 +// page 1 +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 25 +// sort { name: 1 } +// filteredMonitors [] + +// limit 25 +// page NaN +// rowsPerPage 25 +// filter undefined +// field name +// order asc +// skip 0 +// sort { name: 1 } diff --git a/Server/db/mongo/modules/monitorModuleQueries.js b/Server/db/mongo/modules/monitorModuleQueries.js index cdbcd51d8..094e1f932 100644 --- a/Server/db/mongo/modules/monitorModuleQueries.js +++ b/Server/db/mongo/modules/monitorModuleQueries.js @@ -218,18 +218,20 @@ const buildUptimeDetailsPipeline = (monitor, dates, dateString) => { { $project: { uptimeStreak: { - $cond: [ - { $eq: [{ $size: { $first: "$uptimeStreak.streak.checks" } }, 0] }, - 0, - { - $subtract: [ - new Date(), + $let: { + vars: { + checks: { $ifNull: [{ $first: "$uptimeStreak.streak.checks" }, []] }, + }, + in: { + $cond: [ + { $eq: [{ $size: "$$checks" }, 0] }, + 0, { - $last: { $first: "$uptimeStreak.streak.checks.createdAt" }, + $subtract: [new Date(), { $last: "$$checks.createdAt" }], }, ], }, - ], + }, }, avgResponseTime: { $arrayElemAt: ["$aggregateData.avgResponseTime", 0], diff --git a/Server/package-lock.json b/Server/package-lock.json index d5a5ea747..0c7b32f8c 100644 --- a/Server/package-lock.json +++ b/Server/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.37.0", + "bullmq": "5.39.1", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", @@ -1748,9 +1748,9 @@ } }, "node_modules/bullmq": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.37.0.tgz", - "integrity": "sha512-h/wf979+9uROyYpB8oTE44Py6JERCluCSd+ZFpCZlPsYh+wxAkqrfHsHHHKBgsNJp9odWLIY4SG+280EXzXiCQ==", + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.39.1.tgz", + "integrity": "sha512-z6ovgX3mMly0L9F3dStCuiMQOk3bqiwqgr83mbjRlX5yGRcStwP9kZsrO0a5VrjPuU0oUgwmWXv37jcSbGaERQ==", "license": "MIT", "dependencies": { "cron-parser": "^4.9.0", @@ -5571,9 +5571,9 @@ } }, "node_modules/mongoose": { - "version": "8.9.5", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.5.tgz", - "integrity": "sha512-SPhOrgBm0nKV3b+IIHGqpUTOmgVL5Z3OO9AwkFEmvOZznXTvplbomstCnPOGAyungtRXE5pJTgKpKcZTdjeESg==", + "version": "8.9.6", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.6.tgz", + "integrity": "sha512-ipLvXwNPVuuuq5H3lnSD0lpaRH3DlCoC6emnMVJvweTwxU29uxDJWxMsNpERDQt8JMvYF1HGVuTK+Id2BlQLCA==", "license": "MIT", "dependencies": { "bson": "^6.10.1", diff --git a/Server/package.json b/Server/package.json index 9d92a2010..0b826fd39 100644 --- a/Server/package.json +++ b/Server/package.json @@ -18,7 +18,7 @@ "dependencies": { "axios": "^1.7.2", "bcrypt": "5.1.1", - "bullmq": "5.37.0", + "bullmq": "5.39.1", "cors": "^2.8.5", "dockerode": "4.0.4", "dotenv": "^16.4.5", diff --git a/Server/templates/addReview.mjml b/Server/templates/addReview.mjml new file mode 100644 index 000000000..22adbe8b0 --- /dev/null +++ b/Server/templates/addReview.mjml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + Message from Checkmate Service + + + + + + +

Hello {{name}}!

+

+We hope you’re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we truly appreciate having you as part of our community. +

+

+If you’re happy with Checkmate, we’d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and supports our ongoing improvements. +

+G2 Link: TBD +

+Thank you for taking the time to share your thoughts - we greatly appreciate it! +

+Checkmate Team +
+
+ + + +

This email was sent by Checkmate.

+
+
+
+
+
diff --git a/Server/validation/joi.js b/Server/validation/joi.js index 8f366a113..9f20eb0d1 100644 --- a/Server/validation/joi.js +++ b/Server/validation/joi.js @@ -292,6 +292,7 @@ const getChecksQueryValidation = joi.object({ filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), + status: joi.boolean(), }); const getTeamChecksParamValidation = joi.object({ @@ -305,6 +306,7 @@ const getTeamChecksQueryValidation = joi.object({ filter: joi.string().valid("all", "down", "resolve"), page: joi.number(), rowsPerPage: joi.number(), + status: joi.boolean(), }); const deleteChecksParamValidation = joi.object({ diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index f71a78c1f..000000000 --- a/package-lock.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "bluewave-uptime", - "lockfileVersion": 3, - "requires": true, - "packages": {} -}