Merge remote-tracking branch 'upstream/develop' into feat/be/webhook-integrations

This commit is contained in:
Skorpios committed 2025-02-01 18:51:24 -08:00
commit 92d9573fcf
140 files changed
+4606 -4446

No files matched your search

+1
View File
@@ -0,0 +1 @@
package-lock=false
+83 -83
View File
@@ -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"
+7 -7
View File
@@ -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"
},
-360
View File
@@ -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 (
<Box sx={{ flexShrink: 0, ml: "24px" }}>
<Button
variant="group"
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
<LeftArrowDouble />
</Button>
<Button
variant="group"
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
<LeftArrow />
</Button>
<Button
variant="group"
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
<RightArrow />
</Button>
<Button
variant="group"
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
<RightArrowDouble />
</Button>
</Box>
);
};
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: <div>Data for Row 1 Col 1</div> },
* { id: 2, data: <div>Data for Row 1 Col 2</div> },
* { id: 3, data: <div>Data for Row 1 Col 3</div> },
* { id: 4, data: <div>Data for Row 1 Col 4</div> },
* ],
* },
* {
* id: 2,
* data: [
* { id: 5, data: <div>Data for Row 2 Col 1</div> },
* { id: 6, data: <div>Data for Row 2 Col 2</div> },
* { id: 7, data: <div>Data for Row 2 Col 3</div> },
* { id: 8, data: <div>Data for Row 2 Col 4</div> },
* ],
* },
* ],
* };
*
* <BasicTable data={data} rows={rows} paginated={true} />
*/
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 <div>No data</div>;
}
/**
* 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 (
<>
<TableContainer component={Paper}>
<Table
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(12),
},
"& th": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
/*
TODO all general styling for th should live here
*/
},
"& td": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
/*
TODO all general styling for td should live here
*/
},
}}
>
<TableHead>
<TableRow>
{data.cols.map((col) => (
<TableCell key={col.id}>{col.name}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{displayData.map((row) => {
return (
<TableRow
sx={{
cursor: row.handleClick ? "pointer" : "default",
"&:hover": {
filter: "brightness(.75)",
opacity: 0.75,
transition: "filter 0.3s ease, opacity 0.3s ease",
},
}}
key={row.id}
onClick={row.handleClick ? row.handleClick : null}
>
{row.data.map((cell) => {
return <TableCell key={cell.id}>{cell.data}</TableCell>;
})}
</TableRow>
);
})}
{displayData.length === 0 && (
<TableRow>
<TableCell
sx={{ textAlign: "center" }}
colSpan={data.cols.length}
>
{emptyMessage}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</TableContainer>
{paginated && (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(4)}
sx={{
"& p": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
>
<Typography
px={theme.spacing(2)}
fontSize={12}
sx={{ opacity: 0.7 }}
>
Showing {getRange()} of {data.rows.length} monitor(s)
</Typography>
<TablePagination
component="div"
count={data.rows.length}
page={page}
onPageChange={handleChangePage}
rowsPerPage={rowsPerPage}
rowsPerPageOptions={[5, 10, 15, 25]}
onRowsPerPageChange={handleChangeRowsPerPage}
ActionsComponent={TablePaginationActions}
labelRowsPerPage="Rows per page"
labelDisplayedRows={({ page, count }) =>
`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,
},
}}
/>
</Stack>
)}
</>
);
};
BasicTable.propTypes = {
data: PropTypes.object.isRequired,
paginated: PropTypes.bool,
reversed: PropTypes.bool,
rowPerPage: PropTypes.number,
table: PropTypes.string,
emptyMessage: PropTypes.string,
};
export default BasicTable;
@@ -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 (
<Stack
flex={1}
direction="row"
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: 2,
borderTopRightRadius: borderRadiusRight,
borderBottomRightRadius: borderRadiusRight,
}}
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
gap: theme.spacing(8),
height,
minWidth: 250,
"& 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,
},
},
"& tspan, & text": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& path": {
transition: "fill 300ms ease, stroke-width 400ms ease",
},
}}
>
<Stack
alignSelf="flex-start"
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
{Legend && Legend}
</Stack>
);
};
export default ChartBox;
ChartBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node.isRequired,
header: PropTypes.string.isRequired,
height: PropTypes.string,
};
@@ -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 (
<Stack
direction="column"
gap={theme.spacing(4)}
borderRadius={theme.spacing(8)}
sx={{
...sx,
"& label": { pl: theme.spacing(6) },
borderLeftStyle: "solid",
borderLeftWidth: 1,
borderLeftColor: theme.palette.primary.lowContrast,
backgroundColor: theme.palette.primary.main,
padding: theme.spacing(8),
background: `linear-gradient(325deg, ${theme.palette.tertiary.main} 20%, ${theme.palette.primary.main} 45%)`,
}}
>
<Stack
direction="row"
gap={theme.spacing(6)}
>
<IconBox>{icon}</IconBox>
<Typography component="h2">{header}</Typography>
</Stack>
{children}
</Stack>
);
};
LegendBox.propTypes = {
children: PropTypes.node,
icon: PropTypes.node,
header: PropTypes.string,
};
export default LegendBox;
@@ -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 (
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50rem",
fontSize: "12px",
fontWeight: 500,
}}
>
{count}
</Box>
);
};
CircularCount.propTypes = {
count: PropTypes.number,
};
export default CircularCount;
+23
View File
@@ -0,0 +1,23 @@
import PropTypes from "prop-types";
const Dot = ({ color = "gray", size = "4px" }) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
}}
/>
);
};
Dot.propTypes = {
color: PropTypes.string,
size: PropTypes.string,
};
export default Dot;
+16
View File
@@ -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);
}
+60 -47
View File
@@ -26,58 +26,71 @@ const Fallback = ({ title, checks, link = "/", isAdmin, vowelStart = false }) =>
const mode = useSelector((state) => state.ui.mode);
return (
<Stack
className={`fallback__${title.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
<Box
className="page-speed"
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
className={`fallback__${title?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={"300px"}
zIndex={1}
>
<Typography
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
>
Let's create your first {title}
</Button>
)}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate(link)}
>
Let's create your {title}
</Button>
)}
</Stack>
</Box>
);
};
@@ -0,0 +1,20 @@
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
const NetworkError = () => {
const theme = useTheme();
return (
<>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</>
);
};
export default NetworkError;
@@ -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 (
<Box
padding={theme.spacing(16)}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
position: "absolute",
top: "0",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "800px",
height: "100%",
maxHeight: "800px",
backgroundPosition: "center",
backgroundSize: "cover",
backgroundRepeat: "no-repeat",
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export default GenericFallback;
@@ -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,
@@ -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;
}
@@ -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 <SkeletonLayout />;
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
sx={{ ...sx }}
>
<Heading component="h2">{heading}</Heading>
<CircularCount count={monitorCount} />
{children}
</Stack>
);
};
MonitorCountHeader.propTypes = {
shouldRender: PropTypes.bool,
monitorCount: PropTypes.number,
heading: PropTypes.string,
children: PropTypes.node,
sx: PropTypes.object,
};
export default MonitorCountHeader;
@@ -0,0 +1,26 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(2)}
>
<Skeleton
variant="text"
width={100}
height={32}
/>
<Skeleton
variant="circular"
width={40}
height={40}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 <SkeletonLayout />;
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Button
variant="contained"
color="accent"
onClick={() => navigate(path)}
>
Create new
</Button>
</Stack>
);
};
export default CreateMonitorHeader;
CreateMonitorHeader.propTypes = {
isAdmin: PropTypes.bool.isRequired,
shouldRender: PropTypes.bool,
path: PropTypes.string.isRequired,
};
@@ -0,0 +1,19 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="end"
alignItems="center"
>
<Skeleton
variant="rectangular"
width={100}
height={36}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/${path}/configure/${monitorId}`)}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
/* Should always be contrastText for the button color */
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
);
};
ConfigButton.propTypes = {
shouldRender: PropTypes.bool,
monitorId: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
};
export default ConfigButton;
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(2)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography variant="h2">
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
<ConfigButton
path={path}
shouldRender={isAdmin}
monitorId={monitor?._id}
/>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
path: PropTypes.string.isRequired,
shouldRender: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
};
export default MonitorStatusHeader;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 <SkeletonLayout />;
}
let timeFramePicker = null;
if (hasDateRange) {
timeFramePicker = (
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
);
}
return (
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(4)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day" ? "24 hours" : dateRange === "week" ? "7 days" : "30 days"}.
</Typography>
{timeFramePicker}
</Stack>
);
};
MonitorTimeFrameHeader.propTypes = {
shouldRender: PropTypes.bool,
hasDateRange: PropTypes.bool,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default MonitorTimeFrameHeader;
@@ -0,0 +1,23 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
/>
</Stack>
);
};
export default SkeletonLayout;
+1 -1
View File
@@ -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
@@ -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 (
<SkeletonLayout
numBoxes={children?.length ?? 1}
flexWrap={flexWrap}
/>
);
}
return (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(8)}
>
{children}
</Stack>
);
};
StatusBoxes.propTypes = {
shouldRender: PropTypes.bool,
children: PropTypes.node,
};
export default StatusBoxes;
@@ -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 (
<Stack
direction="row"
flexWrap={flexWrap}
gap={theme.spacing(4)}
mt={theme.spacing(4)}
>
{Array.from({ length: numBoxes }).map((_, index) => {
const width = `${200 / numBoxes}%`;
return (
<Skeleton
variant="rounded"
width={width}
height={100}
key={index}
/>
);
})}
</Stack>
);
};
SkeletonLayout.propTypes = {
flexWrap: PropTypes.string,
numBoxes: PropTypes.number,
};
export default SkeletonLayout;
@@ -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,
}) {
+17 -3
View File
@@ -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 <SkeletonLayout />;
}
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,
}),
+14
View File
@@ -0,0 +1,14 @@
import { Skeleton } from "@mui/material";
const TableSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height="80%"
flex={1}
/>
);
};
export default TableSkeleton;
@@ -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 (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
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) => <HttpStatusLabel status={row.statusCode} />,
},
{ id: "message", content: "Message", render: (row) => row.message },
];
if (!shouldRender || isLoading) return <TableSkeleton />;
if (networkError) {
return (
<GenericFallback>
<NetworkError />
</GenericFallback>
);
}
if (!isLoading && typeof checksCount === "undefined") {
return <GenericFallback>No incidents recorded</GenericFallback>;
}
return (
<>
<Table
headers={headers}
data={checks}
/>
<Pagination
paginationLabel="incidents"
itemCount={checksCount}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
);
};
IncidentTable.propTypes = {
shouldRender: PropTypes.bool,
monitors: PropTypes.object,
selectedMonitor: PropTypes.string,
filter: PropTypes.string,
dateRange: PropTypes.string,
};
export default IncidentTable;
@@ -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 <SkeletonLayout />;
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Incidents for
</Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={(e) => setSelectedMonitor(e.target.value)}
items={monitorNames}
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
}}
/>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Filter by:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
<Button
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
</Button>
<Button
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
>
Down
</Button>
<Button
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
>
Cannot resolve
</Button>
</ButtonGroup>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Show:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
<Button
variant="group"
filled={(dateRange === "hour").toString()}
onClick={() => setDateRange("hour")}
>
Last hour
</Button>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Last day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Last week
</Button>
<Button
variant="group"
filled={(dateRange === "all").toString()}
onClick={() => setDateRange("all")}
>
All
</Button>
</ButtonGroup>
</Stack>
</Stack>
);
};
OptionsHeader.propTypes = {
shouldRender: PropTypes.bool,
selectedMonitor: PropTypes.string,
setSelectedMonitor: PropTypes.func,
monitors: PropTypes.object,
filter: PropTypes.string,
setFilter: PropTypes.func,
dateRange: PropTypes.string,
setDateRange: PropTypes.func,
};
export default OptionsHeader;
@@ -0,0 +1,11 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack>
<Skeleton height={40} />
</Stack>
);
};
export default SkeletonLayout;
@@ -0,0 +1,60 @@
import { useState, useEffect } from "react";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import { useSelector } from "react-redux";
const useChecksFetch = ({ selectedMonitor, filter, dateRange, page, rowsPerPage }) => {
//Redux
const { authToken, user } = useSelector((state) => state.auth);
//Local
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
useEffect(() => {
const fetchChecks = async () => {
try {
setIsLoading(true);
let res;
if (selectedMonitor === "0") {
res = await networkService.getChecksByTeam({
authToken: authToken,
status: false,
teamId: user.teamId,
sortOrder: "desc",
limit: null,
dateRange,
filter: filter,
page: page,
rowsPerPage: rowsPerPage,
});
} else {
res = await networkService.getChecksByMonitor({
authToken: authToken,
status: false,
monitorId: selectedMonitor,
sortOrder: "desc",
limit: null,
dateRange,
filter: filter,
page,
rowsPerPage,
});
}
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
setNetworkError(true);
createToast({ body: error.message });
} finally {
setIsLoading(false);
}
};
fetchChecks();
}, [authToken, user, dateRange, page, rowsPerPage, filter, selectedMonitor]);
return { isLoading, networkError, checks, checksCount };
};
export default useChecksFetch;
@@ -0,0 +1,51 @@
import { useState, useEffect } from "react";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
const useMonitorsFetch = ({ authToken, teamId }) => {
//Local state
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitors, setMonitors] = useState(undefined);
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken,
teamId,
limit: null,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
}
} catch (error) {
setNetworkError(true);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authToken, teamId]);
return { isLoading, monitors, networkError };
};
export { useMonitorsFetch };
@@ -1,32 +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 } from "@mui/material";
import PropTypes from "prop-types";
const Empty = ({ styles, mode }) => {
const theme = useTheme();
return (
<Box sx={{ ...styles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.primary.contrastTextSecondary}
>
No incidents recorded yet.
</Typography>
</Box>
);
};
Empty.propTypes = {
styles: PropTypes.object,
mode: PropTypes.string,
};
export { Empty };
@@ -1,21 +0,0 @@
import { Skeleton /* , Stack */ } from "@mui/material";
const IncidentSkeleton = () => {
return (
<>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
animation={"wave"}
variant="rounded"
width="100%"
height={100}
/>
</>
);
};
export { IncidentSkeleton };
@@ -1,185 +0,0 @@
import PropTypes from "prop-types";
import { Typography, Box } from "@mui/material";
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { networkService } from "../../../main";
import { StatusLabel } from "../../../Components/Label";
import { logger } from "../../../Utils/Logger";
import { useTheme } from "@emotion/react";
import { formatDateWithTz } from "../../../Utils/timeUtils";
import PlaceholderLight from "../../../assets/Images/data_placeholder.svg?react";
import PlaceholderDark from "../../../assets/Images/data_placeholder_dark.svg?react";
import { HttpStatusLabel } from "../../../Components/HttpStatusLabel";
import { Empty } from "./Empty/Empty";
import { IncidentSkeleton } from "./Skeleton/Skeleton";
import DataTable from "../../../Components/Table";
import Pagination from "../../../Components/Table/TablePagination";
const IncidentTable = ({ monitors, selectedMonitor, filter, dateRange }) => {
const uiTimezone = useSelector((state) => state.ui.timezone);
const theme = useTheme();
const { authToken, user } = useSelector((state) => state.auth);
const mode = useSelector((state) => state.ui.mode);
const [checks, setChecks] = useState([]);
const [checksCount, setChecksCount] = useState(0);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
const fetchPage = async () => {
if (!monitors || Object.keys(monitors).length === 0) {
return;
}
try {
setIsLoading(true);
let res;
if (selectedMonitor === "0") {
res = await networkService.getChecksByTeam({
authToken: authToken,
teamId: user.teamId,
sortOrder: "desc",
limit: null,
dateRange,
filter: filter,
page: page,
rowsPerPage: rowsPerPage,
});
} else {
res = await networkService.getChecksByMonitor({
authToken: authToken,
monitorId: selectedMonitor,
sortOrder: "desc",
limit: null,
dateRange,
filter: filter,
page,
rowsPerPage,
});
}
setChecks(res.data.data.checks);
setChecksCount(res.data.data.checksCount);
} catch (error) {
logger.error(error);
} finally {
setIsLoading(false);
}
};
fetchPage();
}, [authToken, user, monitors, selectedMonitor, filter, page, rowsPerPage, dateRange]);
const handlePageChange = (_, 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 (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
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) => <HttpStatusLabel status={row.statusCode} />,
},
{ id: "message", content: "Message", render: (row) => row.message },
];
let sharedStyles = {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(30),
};
const hasChecks = checks?.length === 0;
const noIncidentsRecordedYet = hasChecks && selectedMonitor === "0";
const noIncidentsForThatMonitor = hasChecks && selectedMonitor !== "0";
return (
<>
{isLoading ? (
<IncidentSkeleton />
) : noIncidentsRecordedYet ? (
<Empty
mode={mode}
styles={sharedStyles}
/>
) : noIncidentsForThatMonitor ? (
<Box sx={{ ...sharedStyles }}>
<Box
textAlign="center"
pb={theme.spacing(20)}
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
</Box>
<Typography
textAlign="center"
color={theme.palette.primary.contrastTextSecondary}
>
The monitor you have selected has no recorded incidents yet.
</Typography>
</Box>
) : (
<>
<DataTable
headers={headers}
data={checks}
/>
<Pagination
paginationLabel="incidents"
itemCount={checksCount}
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handlePageChange}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</>
)}
</>
);
};
IncidentTable.propTypes = {
monitors: PropTypes.object.isRequired,
selectedMonitor: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
dateRange: PropTypes.string.isRequired,
};
export default IncidentTable;
+50 -198
View File
@@ -1,211 +1,63 @@
import { useState, useEffect } from "react";
import { useSelector } from "react-redux";
import { ButtonGroup, Stack, Typography, Button } from "@mui/material";
import { useParams } from "react-router-dom";
// Components
import { Stack } from "@mui/material";
import Breadcrumbs from "../../Components/Breadcrumbs";
import { networkService } from "../../main";
//Utils
import { useTheme } from "@emotion/react";
import Select from "../../Components/Inputs/Select";
import IncidentTable from "./IncidentTable";
import SkeletonLayout from "./skeleton";
import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
import { useSelector } from "react-redux";
import OptionsHeader from "./Components/OptionsHeader";
import { useState } from "react";
import IncidentTable from "./Components/IncidentTable";
import GenericFallback from "../../Components/GenericFallback";
import NetworkError from "../../Components/GenericFallback/NetworkError";
//Constants
const BREADCRUMBS = [{ name: `Incidents`, path: "/incidents" }];
const Incidents = () => {
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const { monitorId } = useParams();
// Redux state
const { authToken, user } = useSelector((state) => state.auth);
const [monitors, setMonitors] = useState({});
// Local state
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [isLoading, setIsLoading] = useState(true);
const [filter, setFilter] = useState(undefined);
const [dateRange, setDateRange] = useState(undefined);
//Utils
const theme = useTheme();
// TODO do something with these filters
const [filter, setFilter] = useState("all");
const [dateRange, setDateRange] = useState("hour");
const { monitors, isLoading, networkError } = useMonitorsFetch({
authToken,
teamId: user.teamId,
});
useEffect(() => {
const fetchMonitors = async () => {
try {
setIsLoading(true);
const res = await networkService.getMonitorsByTeamId({
authToken: authState.authToken,
teamId: authState.user.teamId,
limit: null,
types: null,
status: null,
checkOrder: null,
normalize: null,
page: null,
rowsPerPage: null,
filter: null,
field: null,
order: null,
});
if (res?.data?.data?.monitors?.length > 0) {
const monitorLookup = res.data.data.monitors.reduce((acc, monitor) => {
acc[monitor._id] = monitor;
return acc;
}, {});
setMonitors(monitorLookup);
monitorId !== undefined && setSelectedMonitor(monitorId);
}
} catch (error) {
console.info(error);
} finally {
setIsLoading(false);
}
};
fetchMonitors();
}, [authState, monitorId]);
useEffect(() => {}, [monitors]);
const handleSelect = (event) => {
setSelectedMonitor(event.target.value);
};
const isActuallyLoading = isLoading && Object.keys(monitors)?.length === 0;
if (networkError) {
return (
<GenericFallback>
<NetworkError />
</GenericFallback>
);
}
return (
<Stack
className="incidents"
pt={theme.spacing(6)}
gap={theme.spacing(12)}
>
{isActuallyLoading ? (
<SkeletonLayout />
) : (
<>
<Stack
direction="row"
justifyContent="space-between"
gap={theme.spacing(6)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Incidents for
</Typography>
<Select
id="incidents-select-monitor"
placeholder="All servers"
value={selectedMonitor}
onChange={handleSelect}
items={Object.values(monitors)}
sx={{
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
}}
/>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Filter by:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
<Button
variant="group"
filled={(filter === "all").toString()}
onClick={() => setFilter("all")}
>
All
</Button>
<Button
variant="group"
filled={(filter === "down").toString()}
onClick={() => setFilter("down")}
>
Down
</Button>
<Button
variant="group"
filled={(filter === "resolve").toString()}
onClick={() => setFilter("resolve")}
>
Cannot resolve
</Button>
</ButtonGroup>
</Stack>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Typography
display="inline-block"
component="h1"
color={theme.palette.primary.contrastTextSecondary}
>
Show:
</Typography>
<ButtonGroup
sx={{
ml: "auto",
"& .MuiButtonBase-root, & .MuiButtonBase-root:hover": {
borderColor: theme.palette.primary.lowContrast,
},
}}
>
<Button
variant="group"
filled={(dateRange === "hour").toString()}
onClick={() => setDateRange("hour")}
>
Last hour
</Button>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Last day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Last week
</Button>
<Button
variant="group"
filled={(dateRange === "all").toString()}
onClick={() => setDateRange("all")}
>
All
</Button>
</ButtonGroup>
</Stack>
</Stack>
<IncidentTable
monitors={monitors}
selectedMonitor={selectedMonitor}
filter={filter}
dateRange={dateRange}
/>
</>
)}
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<OptionsHeader
shouldRender={!isLoading}
monitors={monitors}
selectedMonitor={selectedMonitor}
setSelectedMonitor={setSelectedMonitor}
filter={filter}
setFilter={setFilter}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<IncidentTable
shouldRender={!isLoading}
monitors={monitors}
selectedMonitor={selectedMonitor}
filter={filter}
dateRange={dateRange}
/>
</Stack>
);
};
-50
View File
@@ -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 (
<>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<Skeleton
variant="rounded"
width={150}
height={34}
/>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ ml: "auto" }}
/>
</Stack>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={100}
/>
</>
);
};
export default SkeletonLayout;
@@ -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,
@@ -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" },
@@ -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 (
<BaseContainer>
<Typography
component="h2"
padding={theme.spacing(8)}
>
{config.heading}
</Typography>
<AreaChart
height={getDimensions().areaChartHeight}
data={config.data}
dataKeys={config.dataKeys}
xKey="_id"
yDomain={config.yDomain}
customTooltip={config.toolTip}
xTick={config.xTick}
yTick={config.yTick}
strokeColor={config.strokeColor}
gradient={true}
gradientStartColor={config.gradientStartColor}
gradientEndColor="#ffffff"
/>
</BaseContainer>
);
};
export default InfraAreaChart;
@@ -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 <SkeletonLayout />;
}
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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.primary.main}
yKey={"avgMemoryUsage"}
yLabel={"Memory usage"}
dateRange={dateRange}
/>
),
},
{
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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"avgCpuUsage"}
yLabel={"CPU usage"}
dateRange={dateRange}
/>
),
},
{
type: "temperature",
data: temps,
dataKeys: tempKeys,
strokeColor: theme.palette.error.main,
gradientStartColor: theme.palette.error.main,
heading: "CPU Temperature",
yLabel: "Temperature",
xTick: <TzTick dateRange={dateRange} />,
yDomain: [
0,
Math.max(Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1, 200),
],
toolTip: (
<TemperatureTooltip
keys={tempKeys}
dotColor={theme.palette.error.main}
dateRange={dateRange}
/>
),
},
...(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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.warning.main}
yKey={`disks.usagePercent`}
yLabel={"Disc usage"}
yIdx={idx}
dateRange={dateRange}
/>
),
})) || []),
];
return (
<Stack
direction={"row"}
// height={chartContainerHeight} // FE team HELP! Possibly no longer needed?
gap={theme.spacing(8)} // FE team HELP!
flexWrap="wrap" // //FE team HELP! Better way to do this?
sx={{
"& > *": {
flexBasis: `calc(50% - ${theme.spacing(8)})`,
maxWidth: `calc(50% - ${theme.spacing(8)})`,
},
}}
>
{configs.map((config) => (
<InfraAreaChart
key={`${config.type}-${config.diskIndex ?? ""}`}
config={config}
/>
))}
</Stack>
);
};
export default AreaChartBoxes;
@@ -0,0 +1,25 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<Skeleton
height={"33vh"}
sx={{
flex: 1,
}}
/>
<Skeleton
height={"33vh"}
sx={{ flex: 1 }}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<Box
sx={{
padding: `${theme.spacing(getDimensions().baseBoxPaddingVertical)} ${theme.spacing(getDimensions().baseBoxPaddingHorizontal)}`,
minWidth: 200,
width: 225,
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
...sx,
}}
>
{children}
</Box>
);
};
BaseContainer.propTypes = {
children: PropTypes.node.isRequired,
sx: PropTypes.object,
};
export default BaseContainer;
@@ -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 (
<BaseContainer>
<Stack
direction="column"
gap={theme.spacing(2)}
alignItems="center"
>
<CustomGauge
progress={value}
radius={100}
color={theme.palette.primary.main}
/>
<Typography component="h2">{heading}</Typography>
<Box
sx={{
width: "100%",
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
}}
>
<Stack
justifyContent={"space-between"}
direction="row"
gap={theme.spacing(2)}
>
<Typography>{metricOne}</Typography>
<Typography>{valueOne}</Typography>
</Stack>
<Stack
justifyContent={"space-between"}
direction="row"
gap={theme.spacing(2)}
>
<Typography>{metricTwo}</Typography>
<Typography>{valueTwo}</Typography>
</Stack>
</Box>
</Stack>
</BaseContainer>
);
};
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;
@@ -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 <SkeletonLayout />;
}
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 (
<Stack
direction="row"
gap={theme.spacing(8)}
>
{gauges.map((gauge) => {
return (
<Gauge
key={`${gauge.type}-${gauge.diskIndex ?? ""}`}
value={gauge.value}
heading={gauge.heading}
metricOne={gauge.metricOne}
valueOne={gauge.valueOne}
metricTwo={gauge.metricTwo}
valueTwo={gauge.valueTwo}
/>
);
})}
</Stack>
);
};
export default Gauges;
@@ -0,0 +1,26 @@
import { Stack, Skeleton } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
{Array.from({ length: 3 }).map((_, idx) => {
return (
<Skeleton
key={`gauge-${idx}`}
variant="rectangular"
width={200}
height={200}
/>
);
})}
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<StatusBoxes
shouldRender={shouldRender}
flexWrap="wrap"
>
<StatBox
sx={statusStyles[determineState(monitor)]}
heading="Status"
subHeading={determineState(monitor)}
/>
<StatBox
heading="CPU (Physical)"
subHeading={
<>
{physicalCores}
<Typography component="span">cores</Typography>
</>
}
/>
<StatBox
key={2}
heading="CPU (Logical)"
subHeading={
<>
{logicalCores}
<Typography component="span">cores</Typography>
</>
}
/>
<StatBox
heading="CPU Frequency"
subHeading={
<>
{(cpuFrequency / 1000).toFixed(2)}
<Typography component="span">Ghz</Typography>
</>
}
/>
<StatBox
heading="Average CPU Temperature"
subHeading={
<>
{cpuTemperature.toFixed(2)}
<Typography component="span">C</Typography>
</>
}
/>
<StatBox
heading="Memory"
subHeading={formatBytes(memoryTotalBytes)}
/>
<StatBox
heading="Disk"
subHeading={formatBytes(diskTotalBytes)}
/>
<StatBox
heading="Uptime"
subHeading={
<>
{(uptimePercentage * 100).toFixed(2)}
<Typography component="span">%</Typography>
</>
}
/>
<StatBox
key={8}
heading="OS"
subHeading={osPlatform}
/>
</StatusBoxes>
);
};
export default InfraStatBoxes;
@@ -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 };
@@ -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 ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (typeof bytes !== "number")
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (bytes === 0)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
const GB = bytes / (1024 * 1024 * 1024);
const MB = bytes / (1024 * 1024);
if (GB >= 1) {
return (
<>
{Number(GB.toFixed(0))}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
} else {
return (
<>
{Number(MB.toFixed(0))}
<Typography component="span">MB</Typography>
</>
);
}
};
/**
* 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 };
@@ -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 (
<Box sx={{ ...styles, marginTop: theme.spacing(24) }}>
<Stack
direction="column"
gap={theme.spacing(8)}
alignItems="center"
>
{mode === "light" ? <PlaceholderLight /> : <PlaceholderDark />}
<Typography variant="h2">Your infrastructure dashboard will show here</Typography>
<Typography
textAlign="center"
color={theme.palette.primary.contrastTextSecondary}
>
Hang tight! Data is loading
</Typography>
</Stack>
</Box>
);
};
Empty.propTypes = {
styles: PropTypes.object,
mode: PropTypes.string,
};
export default Empty;
+84 -665
View File
@@ -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 ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (typeof bytes !== "number")
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
if (bytes === 0)
return (
<>
{0}
{space ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
// 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 ? " " : ""}
<Typography component="span">GB</Typography>
</>
);
} else {
return (
<>
{Number(MB.toFixed(0))}
<Typography component="span">MB</Typography>
</>
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
};
/**
* 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 (
<Box
sx={{
height: "100%",
padding: `${theme.spacing(BASE_BOX_PADDING_VERTICAL)} ${theme.spacing(BASE_BOX_PADDING_HORIZONTAL)}`,
minWidth: 200,
width: 225,
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
...sx,
}}
>
{children}
</Box>
);
};
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 (
<BaseBox>
<Stack
direction="column"
gap={theme.spacing(2)}
alignItems="center"
>
<CustomGauge
progress={value}
radius={100}
color={theme.palette.primary.main}
if (!isLoading && monitor?.stats?.checks?.length === 0) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader
path={"infrastructure"}
isAdmin={false}
shouldRender={!isLoading}
monitor={monitor}
/>
<Typography component="h2">{heading}</Typography>
<Box
sx={{
width: "100%",
borderTop: `1px solid ${theme.palette.primary.lowContrast}`,
}}
>
<Stack
justifyContent={"space-between"}
direction="row"
gap={theme.spacing(2)}
>
<Typography>{metricOne}</Typography>
<Typography>{valueOne}</Typography>
</Stack>
<Stack
justifyContent={"space-between"}
direction="row"
gap={theme.spacing(2)}
>
<Typography>{metricTwo}</Typography>
<Typography>{valueTwo}</Typography>
</Stack>
</Box>
<GenericFallback>
<Typography>No check history for htis monitor yet.</Typography>
</GenericFallback>
</Stack>
</BaseBox>
);
};
);
}
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}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 1,
heading: "CPU (Logical)",
subHeading: (
<>
{logicalCores}
<Typography component="span">cores</Typography>
</>
),
},
{
id: 2,
heading: "CPU Frequency",
subHeading: (
<>
{(cpuFrequency / 1000).toFixed(2)}
<Typography component="span">Ghz</Typography>
</>
),
},
{
id: 3,
heading: "Average CPU Temperature",
subHeading: (
<>
{cpuTemperature.toFixed(2)}
<Typography component="span">C</Typography>
</>
),
},
{
id: 4,
heading: "Memory",
subHeading: formatBytes(memoryTotalBytes),
},
{
id: 5,
heading: "Disk",
subHeading: formatBytes(diskTotalBytes),
},
{
id: 6,
heading: "Uptime",
subHeading: (
<>
{(uptime * 100).toFixed(2)}
<Typography component="span">%</Typography>
</>
),
},
{
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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.primary.main}
yKey={"avgMemoryUsage"}
yLabel={"Memory usage"}
dateRange={dateRange}
/>
),
},
{
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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.success.main}
yKey={"avgCpuUsage"}
yLabel={"CPU usage"}
dateRange={dateRange}
/>
),
},
{
type: "temperature",
data: temps,
dataKeys: tempKeys,
strokeColor: theme.palette.error.main,
gradientStartColor: theme.palette.error.main,
heading: "CPU Temperature",
yLabel: "Temperature",
xTick: <TzTick dateRange={dateRange} />,
yDomain: [
0,
Math.max(
Math.max(...temps.flatMap((t) => tempKeys.map((k) => t[k]))) * 1.1,
200
),
],
toolTip: (
<TemperatureTooltip
keys={tempKeys}
dotColor={theme.palette.error.main}
dateRange={dateRange}
/>
),
},
...(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: <PercentTick />,
xTick: <TzTick dateRange={dateRange} />,
toolTip: (
<InfrastructureTooltip
dotColor={theme.palette.warning.main}
yKey={`disks.usagePercent`}
yLabel={"Disc usage"}
yIdx={idx}
dateRange={dateRange}
/>
),
})) || []),
];
};
// 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 (
<Box>
<Breadcrumbs list={navList} />
{monitor?.stats.checks?.length > 0 ? (
<Stack
direction="column"
gap={theme.spacing(10)}
mt={theme.spacing(10)}
>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
<Typography
alignSelf="end"
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
<Typography alignSelf="end">{monitor.url || "..."}</Typography>
<Box sx={{ flexGrow: 1 }} />
<Typography alignSelf="end">
Checking every {formatDurationRounded(monitor?.interval)}
</Typography>
<Typography alignSelf="end">
Last checked {formatDurationSplit(lastChecked).time}{" "}
{formatDurationSplit(lastChecked).format} ago
</Typography>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
{statBoxConfigs.map((statBox) => (
<StatBox
key={statBox.id}
{...statBox}
/>
))}
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
{gaugeBoxConfigs.map((config) => {
return (
<GaugeBox
key={`${config.type}-${config.diskIndex ?? ""}`}
value={config.value}
heading={config.heading}
metricOne={config.metricOne}
valueOne={config.valueOne}
metricTwo={config.metricTwo}
valueTwo={config.valueTwo}
/>
);
})}
</Stack>
<Stack
direction="row"
justifyContent="space-between"
alignItems="flex-end"
gap={theme.spacing(8)}
mb={theme.spacing(8)}
>
<Typography variant="body2">
Showing statistics for past{" "}
{dateRange === "day"
? "24 hours"
: dateRange === "week"
? "7 days"
: "30 days"}
.
</Typography>
<ButtonGroup sx={{ height: 32 }}>
<Button
variant="group"
filled={(dateRange === "day").toString()}
onClick={() => setDateRange("day")}
>
Day
</Button>
<Button
variant="group"
filled={(dateRange === "week").toString()}
onClick={() => setDateRange("week")}
>
Week
</Button>
<Button
variant="group"
filled={(dateRange === "month").toString()}
onClick={() => setDateRange("month")}
>
Month
</Button>
</ButtonGroup>
</Stack>
<Stack
direction={"row"}
height={chartContainerHeight} // FE team HELP!
gap={theme.spacing(8)} // FE team HELP!
flexWrap="wrap" // //FE team HELP! Better way to do this?
sx={{
"& > *": {
flexBasis: `calc(50% - ${theme.spacing(8)})`,
maxWidth: `calc(50% - ${theme.spacing(8)})`,
},
}}
>
{areaChartConfigs.map((config) => {
return (
<BaseBox key={`${config.type}-${config.diskIndex ?? ""}`}>
<Typography
component="h2"
padding={theme.spacing(8)}
>
{config.heading}
</Typography>
<AreaChart
height={areaChartHeight}
data={config.data}
dataKeys={config.dataKeys}
xKey="_id"
yDomain={config.yDomain}
customTooltip={config.toolTip}
xTick={config.xTick}
yTick={config.yTick}
strokeColor={config.strokeColor}
gradient={true}
gradientStartColor={config.gradientStartColor}
gradientEndColor="#ffffff"
/>
</BaseBox>
);
})}
</Stack>
</Stack>
) : (
<Empty
styles={{
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(30),
}}
/>
)}
</Box>
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader
path={"infrastructure"}
isAdmin={false}
shouldRender={!isLoading}
monitor={monitor}
/>
<StatusBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<GaugeBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<MonitorTimeFrameHeader
shouldRender={!isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
<AreaChartBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
</Stack>
);
};
@@ -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) => (
<Host
title={row.name}
url={row.url}
percentage={row.uptimePercentage}
percentageColor={row.percentageColor}
/>
),
},
{
id: "status",
content: "Status",
render: (row) => (
<StatusLabel
status={row.status}
text={row.status}
/>
),
},
{
id: "frequency",
content: "Frequency",
render: (row) => (
<Stack
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
gap=".25rem"
>
<CPUChipIcon
width={20}
height={20}
/>
{row.processor}
</Stack>
),
},
{ id: "cpu", content: "CPU", render: (row) => <CustomGauge progress={row.cpu} /> },
{ id: "mem", content: "Mem", render: (row) => <CustomGauge progress={row.mem} /> },
{ id: "disk", content: "Disk", render: (row) => <CustomGauge progress={row.disk} /> },
{
id: "actions",
content: "Actions",
render: (row) => (
<InfrastructureMenu
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
),
},
];
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 (
<DataTable
shouldRender={shouldRender}
headers={headers}
data={data}
config={{
/* TODO this behavior seems to be repeated. Put it on the root table? */
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => openDetails(row.id),
}}
/>
);
};
export default MonitorsTable;
@@ -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
@@ -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 };
@@ -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 (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
vowelStart={true}
title="infrastructure monitor"
checks={[
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring",
]}
link="/infrastructure/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorCreateHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
path="/infrastructure/create"
/>
<MonitorCountHeader
shouldRender={!isLoading}
heading="Infrastructure monitors"
monitorCount={summary?.totalMonitors ?? 0}
/>
<MonitorsTable
shouldRender={!isLoading}
monitors={monitors}
isAdmin={isAdmin}
handleActionMenuDelete={handleActionMenuDelete}
/>
<Pagination
itemCount={summary?.totalMonitors}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
);
};
export default InfrastructureMonitors;
-298
View File
@@ -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) => (
<Host
title={row.name}
url={row.url}
percentage={row.uptimePercentage}
percentageColor={row.percentageColor}
/>
),
},
{
id: "status",
content: "Status",
render: (row) => (
<StatusLabel
status={row.status}
text={row.status}
/>
),
},
{
id: "frequency",
content: "Frequency",
render: (row) => (
<Stack
direction={"row"}
justifyContent={"center"}
alignItems={"center"}
gap=".25rem"
>
<CPUChipIcon
width={20}
height={20}
/>
{row.processor}
</Stack>
),
},
{ id: "cpu", content: "CPU", render: (row) => <CustomGauge progress={row.cpu} /> },
{ id: "mem", content: "Mem", render: (row) => <CustomGauge progress={row.mem} /> },
{ id: "disk", content: "Disk", render: (row) => <CustomGauge progress={row.disk} /> },
{
id: "actions",
content: "Actions",
render: (row) => (
<IconButton>
<InfrastructureMenu
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
/>
</IconButton>
),
},
];
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 (
<Box
className="infrastructure-monitor"
sx={{
':has(> [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 ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Stack gap={theme.spacing(8)}>
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
>
{isAdmin && (
<Button
variant="contained"
color="accent"
onClick={navigateToCreate}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Stack
sx={{
gap: "1rem",
}}
>
<Stack
direction="row"
sx={{
alignItems: "center",
gap: ".25rem",
flexWrap: "wrap",
}}
>
<Heading component="h2">Infrastructure monitors</Heading>
{/* TODO Same as the one in UptimaDataTable. Create component */}
<Box
component="span"
color={theme.palette.tertiary.contrastText}
border={2}
borderColor={theme.palette.accent.main}
backgroundColor={theme.palette.tertiary.main}
sx={{
padding: ".25em .75em",
borderRadius: "50%",
fontSize: "12px",
fontWeight: 500,
}}
>
{summary?.totalMonitors ?? 0}
</Box>
</Stack>
<DataTable
config={{
/* TODO this behavior seems to be repeated. Put it on the root table? */
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => openDetails(row.id),
}}
headers={headers}
data={monitorsAsRows}
/>
<Pagination
itemCount={summary?.totalMonitors ?? 0}
paginationLabel="monitors"
page={page}
rowsPerPage={rowsPerPage}
handleChangePage={handleChangePage}
handleChangeRowsPerPage={handleChangeRowsPerPage}
/>
</Stack>
</Stack>
) : (
<Fallback
vowelStart={true}
title="infrastructure monitor"
checks={[
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring",
]}
link="/infrastructure/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
}
export { Infrastructure };
@@ -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 (
<Stack gap={theme.spacing(2)}>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(12)}
>
<Box width="80%">
<Skeleton
variant="rounded"
width="25%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={19.5}
sx={{ mt: theme.spacing(2) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(12)}
>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
</Stack>
</Stack>
);
};
export default SkeletonLayout;
@@ -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";
@@ -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;
@@ -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 (
<LegendBox
icon={<MetricsIcon />}
header="Metrics"
>
<Box>
<Typography
fontSize={11}
fontWeight={500}
>
Shown
</Typography>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Checkbox
id="accessibility-toggle"
label="Accessibility"
isChecked={metrics.accessibility}
onChange={() => handleMetrics("accessibility")}
/>
<Divider />
<Checkbox
id="best-practices-toggle"
label="Best Practices"
isChecked={metrics.bestPractices}
onChange={() => handleMetrics("bestPractices")}
/>
<Divider />
<Checkbox
id="performance-toggle"
label="Performance"
isChecked={metrics.performance}
onChange={() => handleMetrics("performance")}
/>
<Divider />
<Checkbox
id="seo-toggle"
label="Search Engine Optimization"
isChecked={metrics.seo}
onChange={() => handleMetrics("seo")}
/>
<Divider />
</LegendBox>
);
};
export default AreaChartLegend;
@@ -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 (
<Box onMouseLeave={() => setExpand(false)}>
{expand ? (
@@ -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 (
<LegendBox
icon={<SpeedometerIcon />}
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 (
<Stack
flex={1}
key={`${key}-box`}
justifyContent="space-between"
direction="row"
gap={theme.spacing(4)}
p={theme.spacing(3)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={4}
>
<Box>
<Typography
fontSize={12}
fontWeight={500}
lineHeight={1}
mb={1}
textTransform="uppercase"
>
{audit.title}
</Typography>
<Typography
component="span"
fontSize={14}
fontWeight={500}
color={theme.palette.primary.contrastText}
>
{value}
<Typography
component="span"
variant="body2"
ml={2}
>
{unit}
</Typography>
</Typography>
</Box>
<Box
width={4}
backgroundColor={bg}
borderRadius={4}
/>
</Stack>
);
})}
</LegendBox>
);
};
PieChartLegend.propTypes = {
audits: PropTypes.object,
};
export default PieChartLegend;
@@ -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 <SkeletonLayout />;
}
const data = monitor?.checks ? [...monitor.checks].reverse() : [];
return (
<Stack
direction="row"
gap={theme.spacing(10)}
>
<ChartBox
justifyContent="flex-start"
icon={<ScoreIcon />}
header="Score history"
height="100%"
borderRadiusRight={16}
Legend={
<AreaChartLegend
metrics={metrics}
handleMetrics={handleMetrics}
/>
}
>
<AreaChart
data={data}
monitor={monitor}
metrics={metrics}
/>
</ChartBox>
</Stack>
);
};
PageSpeedAreaChart.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
metrics: PropTypes.object,
handleMetrics: PropTypes.func,
};
export default PageSpeedAreaChart;
@@ -0,0 +1,17 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
@@ -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 (
<StatusBoxes shouldRender={shouldRender}>
<StatBox
heading="checks since"
subHeading={
<>
{uptimeDuration}
<Typography component="span">{uptimeUnits}</Typography>
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">ago</Typography>
</>
}
/>
</StatusBoxes>
);
};
export default PageSpeedStatusBoxes;
@@ -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 <SkeletonLayout />;
}
return (
<ChartBox
icon={<PerformanceIcon />}
header="Performance report"
Legend={<PieChartLegend audits={audits} />}
borderRadiusRight={16}
>
<PieChart audits={audits} />
<Typography
variant="body1"
mt="auto"
>
Values are estimated and may vary.{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
See calculator
</Typography>
</Typography>
</ChartBox>
);
};
export default PerformanceReport;
@@ -0,0 +1,17 @@
import { Box, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Box
height={"100%"}
width={"100%"}
>
<Skeleton
height={"100%"}
width={"100%"}
/>
</Box>
);
};
export default SkeletonLayout;
@@ -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 };
@@ -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;
}
+85 -420
View File
@@ -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}
<Typography component="span">{format}</Typography>
</>
);
};
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 (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
// Empty view, displayed when loading is complete and there are no checks
if (!isLoading && monitor?.checks?.length === 0) {
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader
path={"pagespeed"}
isAdmin={isAdmin}
monitor={monitor}
/>
<GenericFallback>
<Typography>There is no check history for this monitor yet.</Typography>
</GenericFallback>
</Stack>
);
}
return (
<Stack
className="page-speed-details"
gap={theme.spacing(10)}
>
{loading ? (
<SkeletonLayout />
) : (
<>
<Breadcrumbs
list={[
{ name: "pagespeed", path: "/pagespeed" },
{ name: "details", path: `/pagespeed/${monitorId}` },
]}
/>
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Typography
component="h1"
variant="h1"
>
{monitor.name}
</Typography>
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -8],
},
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="h2"
>
{monitor?.url}
</Typography>
<Typography
position="relative"
variant="body2"
mt={theme.spacing(1)}
ml={theme.spacing(6)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: 4,
height: 4,
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: -9,
top: "50%",
transform: "translateY(-50%)",
},
}}
>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Box>
{isAdmin && (
<Button
variant="contained"
color="secondary"
onClick={() => navigate(`/pagespeed/configure/${monitorId}`)}
sx={{
ml: "auto",
alignSelf: "flex-end",
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
}}
>
<SettingsIcon />
Configure
</Button>
)}
</Stack>
<Stack
direction="row"
gap={theme.spacing(8)}
>
<StatBox
heading="checks since"
subHeading={
<>
{splitDuration(monitor?.uptimeDuration)}
<Typography component="span">ago</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{splitDuration(monitor?.lastChecked)}
<Typography component="span">ago</Typography>
</>
}
/>
</Stack>
<Box>
<Typography
variant="body2"
my={theme.spacing(8)}
>
Showing statistics for past 24 hours.
</Typography>
<ChartBox sx={{ gridTemplateColumns: "75% 25%" }}>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<ScoreIcon />
</IconBox>
<Typography component="h2">Score history</Typography>
</Stack>
<Box>
<PagespeedDetailsAreaChart
data={data}
interval={monitor?.interval}
metrics={metrics}
/>
</Box>
<Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<MetricsIcon />
</IconBox>
<Typography component="h2">Metrics</Typography>
</Stack>
<Stack
gap={theme.spacing(4)}
mt={theme.spacing(16)}
sx={{
"& label": { pl: theme.spacing(6) },
}}
>
<Box>
<Typography
fontSize={11}
fontWeight={500}
>
Shown
</Typography>
<Divider sx={{ mt: theme.spacing(2) }} />
</Box>
<Checkbox
id="accessibility-toggle"
label="Accessibility"
isChecked={metrics.accessibility}
onChange={() => handleMetrics("accessibility")}
/>
<Divider />
<Checkbox
id="best-practices-toggle"
label="Best Practices"
isChecked={metrics.bestPractices}
onChange={() => handleMetrics("bestPractices")}
/>
<Divider />
<Checkbox
id="performance-toggle"
label="Performance"
isChecked={metrics.performance}
onChange={() => handleMetrics("performance")}
/>
<Divider />
<Checkbox
id="seo-toggle"
label="Search Engine Optimization"
isChecked={metrics.seo}
onChange={() => handleMetrics("seo")}
/>
<Divider />
</Stack>
</Box>
</ChartBox>
</Box>
<ChartBox
flex={1}
/* TODO apply 1fr 1fr for columns, and auto 1fr for Rows */
sx={{ gridTemplateColumns: "50% 50%", gridTemplateRows: "15% 85%" }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<PerformanceIcon />
</IconBox>
<Typography component="h2">Performance report</Typography>
</Stack>
<Stack
alignItems="center"
textAlign="center"
minWidth="300px"
flex={1}
pt={theme.spacing(6)}
pb={theme.spacing(12)}
>
<PieChart audits={audits} />
<Typography
variant="body1"
mt="auto"
>
Values are estimated and may vary.{" "}
<Typography
component="span"
fontSize="inherit"
sx={{
color: theme.palette.primary.main,
fontWeight: 500,
textDecoration: "underline",
textUnderlineOffset: 2,
transition: "all 200ms",
cursor: "pointer",
"&:hover": {
textUnderlineOffset: 4,
},
}}
>
See calculator
</Typography>
</Typography>
</Stack>
<Box
px={theme.spacing(20)}
py={theme.spacing(8)}
height="100%"
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(6)}
>
<IconBox>
<SpedometerIcon />
</IconBox>
<Typography component="h2">Performance Metrics</Typography>
</Stack>
<Stack
flexWrap="wrap"
mt={theme.spacing(4)}
gap={theme.spacing(4)}
>
{Object.keys(audits).map((key) => {
if (key === "_id") return;
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<MonitorStatusHeader
path={"pagespeed"}
isAdmin={isAdmin}
shouldRender={!isLoading}
monitor={monitor}
/>
<PageSpeedStatusBoxes
shouldRender={!isLoading}
monitor={monitor}
/>
<MonitorTimeFrameHeader
shouldRender={!isLoading}
dateRange={"day"}
hasDateRange={false}
/>
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 (
<Stack
key={`${key}-box`}
justifyContent="space-between"
direction="row"
gap={theme.spacing(4)}
p={theme.spacing(3)}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={4}
>
<Box>
<Typography
fontSize={12}
fontWeight={500}
lineHeight={1}
mb={1}
textTransform="uppercase"
>
{audit.title}
</Typography>
<Typography
component="span"
fontSize={14}
fontWeight={500}
color={theme.palette.primary.contrastText}
>
{value}
<Typography
component="span"
variant="body2"
ml={2}
>
{unit}
</Typography>
</Typography>
</Box>
<Box
width={4}
backgroundColor={bg}
borderRadius={4}
/>
</Stack>
);
})}
</Stack>
</Box>
</ChartBox>
</>
)}
<PageSpeedAreaChart
shouldRender={!isLoading}
monitor={monitor}
metrics={metrics}
handleMetrics={handleMetrics}
/>
<PerformanceReport
shouldRender={!isLoading}
audits={audits}
/>
</Stack>
);
};
PageSpeedDetails.propTypes = {
isAdmin: PropTypes.bool,
push: PropTypes.func,
};
export default PageSpeedDetails;
@@ -1,93 +0,0 @@
import { Box, Skeleton, Stack } from "@mui/material";
/**
* Renders a skeleton layout.
*
* @returns {JSX.Element}
*/
const SkeletonLayout = () => {
return (
<>
<Skeleton
variant="rounded"
width="15%"
height={34}
/>
<Stack
direction="row"
gap={4}
>
<Skeleton
variant="circular"
style={{ minWidth: 24, minHeight: 24 }}
/>
<Box width="85%">
<Skeleton
variant="rounded"
width="50%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={18}
sx={{ mt: 4 }}
/>
</Box>
<Skeleton
variant="rounded"
width="15%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
gap={20}
flexWrap="wrap"
>
<Skeleton
variant="rounded"
width="30%"
height={90}
sx={{ flex: 1 }}
/>
<Skeleton
variant="rounded"
width="30%"
height={90}
sx={{ flex: 1 }}
/>
<Skeleton
variant="rounded"
width="30%"
height={90}
sx={{ flex: 1 }}
/>
</Stack>
<Skeleton
variant="rounded"
width="25%"
height={24}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="25%"
height={24}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
</>
);
};
export default SkeletonLayout;
@@ -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",
},
}));
@@ -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
@@ -0,0 +1,20 @@
import { Grid, Grid2 } from "@mui/material";
import Card from "../Card";
const MonitorGrid = ({ shouldRender, monitors }) => {
return (
<Grid
container
spacing={12}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id}
/>
))}
</Grid>
);
};
export default MonitorGrid;
@@ -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;
@@ -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 (
<GenericFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
</Typography>
<Typography>Please check your connection</Typography>
</GenericFallback>
);
}
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
);
}
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
<CreateMonitorHeader
isAdmin={isAdmin}
shouldRender={!isLoading}
path="/pagespeed/create"
/>
<MonitorCountHeader
shouldRender={!isLoading}
monitorCount={summary?.totalMonitors}
heading="PageSpeed monitors"
sx={{ mb: theme.spacing(8) }}
/>
<MonitorGrid
size={6}
shouldRender={!isLoading}
monitors={monitors}
/>
</Stack>
);
};
export default PageSpeed;
-8
View File
@@ -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;
}
-147
View File
@@ -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 (
<Box
className="page-speed"
sx={{
':has(> [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 ? (
<SkeletonLayout />
) : monitors?.length !== 0 ? (
<Stack gap={theme.spacing(8)}>
<Box>
<Breadcrumbs list={[{ name: `pagespeed`, path: "/pagespeed" }]} />
<Stack
direction="row"
justifyContent="end"
alignItems="center"
mt={theme.spacing(5)}
>
{isAdmin && (
<Button
variant="contained"
color="accent"
onClick={() => navigate("/pagespeed/create")}
sx={{ fontWeight: 500, whiteSpace: "nowrap" }}
>
Create new
</Button>
)}
</Stack>
</Box>
<Stack
direction="row"
sx={{
alignItems: "center",
gap: ".25rem",
flexWrap: "wrap",
}}
>
<Heading component="h2">PageSpeed monitors</Heading>
{/* TODO Correct the class current-monitors-counter, there are some unnecessary things there */}
<Box
component="span"
className="current-monitors-counter"
color={theme.palette.primary.contrastText}
border={1}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.tertiary.main}
>
{summary?.totalMonitors ?? 0}
</Box>
</Stack>
<Grid
container
spacing={theme.spacing(12)}
>
{monitors?.map((monitor) => (
<Card
monitor={monitor}
key={monitor._id}
/>
))}
</Grid>
</Stack>
) : (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
link="/pagespeed/create"
isAdmin={isAdmin}
/>
)}
</Box>
);
};
PageSpeed.propTypes = {
isAdmin: PropTypes.bool,
};
export default PageSpeed;
-73
View File
@@ -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 (
<Stack gap={theme.spacing(2)}>
<Stack
direction="row"
justifyContent="space-between"
mb={theme.spacing(12)}
>
<Box width="80%">
<Skeleton
variant="rounded"
width="25%"
height={24}
/>
<Skeleton
variant="rounded"
width="50%"
height={19.5}
sx={{ mt: theme.spacing(2) }}
/>
</Box>
<Skeleton
variant="rounded"
width="20%"
height={34}
sx={{ alignSelf: "flex-end" }}
/>
</Stack>
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(12)}
>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
<Skeleton
variant="rounded"
width="100%"
height={120}
sx={{ flex: "35%" }}
/>
</Stack>
</Stack>
);
};
export default SkeletonLayout;
+1 -1
View File
@@ -347,7 +347,7 @@ const Configure = () => {
<TextInput
type={monitor?.type === "http" ? "url" : "text"}
https={protocol === "https"}
startAdornment={<HttpAdornment https={protocol === "https"} />}
startAdornment={monitor?.type === "http" && <HttpAdornment https={protocol === "https"} />}
id="monitor-url"
label="URL to monitor"
placeholder="google.com"
@@ -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
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of monitor.
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
startAdornment={
monitor.type === "http" ? <HttpAdornment https={https} /> : 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"]}
/>
<TextInput
type="number"
id="monitor-port"
label="Port to monitor"
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Checks to perform</Typography>
@@ -363,6 +322,52 @@ const CreateMonitor = () => {
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">General settings</Typography>
<Typography component="p">
Here you can select the URL of the host, together with the type of monitor.
</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={monitor.type === "http" ? "url" : "text"}
id="monitor-url"
startAdornment={
monitor.type === "http" ? <HttpAdornment https={https} /> : 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"]}
/>
<TextInput
type="number"
id="monitor-port"
label="Port to monitor"
placeholder="5173"
value={monitor.port}
onChange={(event) => handleChange(event, "port")}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={monitorTypeMaps[monitor.type].namePlaceholder || ""}
value={monitor.name}
onChange={(event) => handleChange(event, "name")}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">Incident notifications</Typography>
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
<ChartBox
icon={<UptimeIcon />}
header="Uptime"
>
<Stack
width={"100%"}
justifyContent="space-between"
direction="row"
>
<Box position="relative">
<Typography>Total Checks</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? hoveredUptimeData.totalChecks
: (monitor?.groupedUpChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredUptimeData !== null && hoveredUptimeData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
<Box>
<Typography>
{hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"}
</Typography>
<Typography component="span">
{hoveredUptimeData !== null
? Math.floor(hoveredUptimeData?.avgResponseTime ?? 0)
: Math.floor(
((monitor?.upChecks?.totalChecks ?? 0) /
(monitor?.totalChecks ?? 1)) *
100
)}
<Typography component="span">
{hoveredUptimeData !== null ? " ms" : " %"}
</Typography>
</Typography>
</Box>
</Stack>
<UpBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredUptimeData}
/>
</ChartBox>
<ChartBox
icon={<IncidentsIcon />}
header="Incidents"
>
<Stack width={"100%"}>
<Box position="relative">
<Typography component="span">
{hoveredIncidentsData !== null
? hoveredIncidentsData.totalChecks
: (monitor?.groupedDownChecks?.reduce((count, checkGroup) => {
return count + checkGroup.totalChecks;
}, 0) ?? 0)}
</Typography>
{hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && (
<Typography
component="h5"
position="absolute"
top="100%"
fontSize={11}
color={theme.palette.primary.contrastTextTertiary}
>
{formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)}
</Typography>
)}
</Box>
</Stack>
<DownBarChart
monitor={monitor}
type={dateRange}
onBarHover={setHoveredIncidentsData}
/>
</ChartBox>
<ChartBox
icon={<AverageResponseIcon />}
header="Average Response Time"
>
<ResponseGaugeChart avgResponseTime={monitor?.avgResponseTime ?? 0} />
</ChartBox>
</Stack>
);
};
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,
};
@@ -0,0 +1,30 @@
import { Skeleton, Stack } from "@mui/material";
import { useTheme } from "@emotion/react";
const SkeletonLayout = () => {
const theme = useTheme();
return (
<Stack
direction="row"
gap={theme.spacing(8)}
>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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,
};
@@ -19,7 +19,7 @@ const DownBarChart = memo(({ monitor, type, onBarHover }) => {
<BarChart
width="100%"
height="100%"
data={monitor.groupedDownChecks}
data={monitor?.groupedDownChecks}
onMouseEnter={() => {
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 (
<Cell
key={`cell-${entry.time}`}
@@ -0,0 +1,31 @@
import ChartBox from "../../../../../Components/Charts/ChartBox";
import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart";
import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
import SkeletonLayout from "./ResponseTimeChartSkeleton";
import PropTypes from "prop-types";
const ResponseTImeChart = ({ shouldRender = true, monitor, dateRange }) => {
if (!shouldRender) {
return <SkeletonLayout />;
}
return (
<ChartBox
icon={<ResponseTimeIcon />}
header="Response Times"
>
<MonitorDetailsAreaChart
checks={monitor?.groupedChecks ?? []}
dateRange={dateRange}
/>
</ChartBox>
);
};
ResponseTImeChart.propTypes = {
shouldRender: PropTypes.bool,
monitor: PropTypes.object,
dateRange: PropTypes.string,
};
export default ResponseTImeChart;
@@ -0,0 +1,12 @@
import { Skeleton } from "@mui/material";
const ResponseTimeChartSkeleton = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default ResponseTimeChartSkeleton;
@@ -28,7 +28,7 @@ const UpBarChart = memo(({ monitor, type, onBarHover }) => {
<BarChart
width="100%"
height="100%"
data={monitor.groupedUpChecks}
data={monitor?.groupedUpChecks}
onMouseEnter={() => {
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 (
<Cell
@@ -0,0 +1,89 @@
import ChartBox from "../../../../../Components/Charts/ChartBox";
import PropTypes from "prop-types";
import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react";
import Table from "../../../../../Components/Table";
import TablePagination from "../../../../../Components/Table/TablePagination";
import { StatusLabel } from "../../../../../Components/Label";
import { formatDateWithTz } from "../../../../../Utils/timeUtils";
import SkeletonLayout from "./skeleton";
const ResponseTable = ({
shouldRender = true,
checks = [],
checksCount,
uiTimezone,
page,
setPage,
rowsPerPage,
setRowsPerPage,
}) => {
if (!shouldRender) {
return <SkeletonLayout />;
}
const headers = [
{
id: "status",
content: "Status",
render: (row) => {
const status = row.status === true ? "up" : "down";
return (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
);
},
},
{
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 (
<ChartBox
icon={<HistoryIcon />}
header="Response Times"
height="100%"
>
<Table
headers={headers}
data={checks}
/>
<TablePagination
page={page}
handleChangePage={setPage}
rowsPerPage={rowsPerPage}
handleChangeRowsPerPage={setRowsPerPage}
itemCount={checksCount}
/>
</ChartBox>
);
};
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;
@@ -0,0 +1,13 @@
import { Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Skeleton
variant="rounded"
width="100%"
height={300}
/>
);
};
export default SkeletonLayout;
@@ -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 (
<StatusBoxes shouldRender={shouldRender}>
<StatBox
gradient={true}
status={determineState(monitor)}
heading={"active for"}
subHeading={
<>
{streakTime}
<Typography component="span">{streakUnits}</Typography>
</>
}
/>
<StatBox
heading="last check"
subHeading={
<>
{lastCheckTime}
<Typography component="span">{lastCheckUnits}</Typography>
<Typography component="span">{"ago"}</Typography>
</>
}
/>
<StatBox
heading="last response time"
subHeading={
<>
{monitor?.latestResponseTime}
<Typography component="span">{"ms"}</Typography>
</>
}
/>
<StatBox
heading="certificate expiry"
subHeading={
<Typography
component="span"
fontSize={13}
color={theme.palette.primary.contrastText}
>
{certificateExpiry}
</Typography>
}
/>
</StatusBoxes>
);
};
export default UptimeStatusBoxes;
@@ -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;
@@ -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;
@@ -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;
Loaded 100 of 140 files, more files were not shown because too many files have changed in this diff. Show more