mirror of
https://github.com/bluewave-labs/Checkmate.git
synced 2026-02-05 00:28:30 -06:00
Merge branch 'develop' into develop
This commit is contained in:
210
Client/package-lock.json
generated
210
Client/package-lock.json
generated
@@ -37,7 +37,6 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"recharts": "2.15.1",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
@@ -2368,14 +2367,6 @@
|
||||
"@svgr/core": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@trysound/sax": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz",
|
||||
"integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==",
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
|
||||
@@ -2880,11 +2871,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/boolbase": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
@@ -3077,14 +3063,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||
@@ -3137,74 +3115,6 @@
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css-select": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
|
||||
"integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0",
|
||||
"css-what": "^6.1.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"domutils": "^3.0.1",
|
||||
"nth-check": "^2.0.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/css-tree": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
|
||||
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.30",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/css-what": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz",
|
||||
"integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
},
|
||||
"node_modules/csso": {
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz",
|
||||
"integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==",
|
||||
"dependencies": {
|
||||
"css-tree": "~2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csso/node_modules/css-tree": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz",
|
||||
"integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==",
|
||||
"dependencies": {
|
||||
"mdn-data": "2.0.28",
|
||||
"source-map-js": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/csso/node_modules/mdn-data": {
|
||||
"version": "2.0.28",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz",
|
||||
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
@@ -3529,57 +3439,6 @@
|
||||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/dom-serializer": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.2",
|
||||
"entities": "^4.2.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domelementtype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/fb55"
|
||||
}
|
||||
]
|
||||
},
|
||||
"node_modules/domhandler": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||
"dependencies": {
|
||||
"domelementtype": "^2.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/domutils": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||
"dependencies": {
|
||||
"dom-serializer": "^2.0.0",
|
||||
"domelementtype": "^2.3.0",
|
||||
"domhandler": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
@@ -5474,11 +5333,6 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mdn-data": {
|
||||
"version": "2.0.30",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
|
||||
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||
@@ -5600,17 +5454,6 @@
|
||||
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nth-check": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||
"dependencies": {
|
||||
"boolbase": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/nth-check?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -5938,9 +5781,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
|
||||
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
@@ -6222,19 +6065,6 @@
|
||||
"react-dom": ">=16.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-world-flags": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-world-flags/-/react-world-flags-1.6.0.tgz",
|
||||
"integrity": "sha512-eutSeAy5YKoVh14js/JUCSlA6EBk1n4k+bDaV+NkNB50VhnG+f4QDTpYycnTUTsZ5cqw/saPmk0Z4Fa0VVZ1Iw==",
|
||||
"dependencies": {
|
||||
"svg-country-flags": "^1.2.10",
|
||||
"svgo": "^3.0.2",
|
||||
"world-countries": "^5.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=0.14"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "2.15.1",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz",
|
||||
@@ -6862,41 +6692,12 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/svg-country-flags": {
|
||||
"version": "1.2.10",
|
||||
"resolved": "https://registry.npmjs.org/svg-country-flags/-/svg-country-flags-1.2.10.tgz",
|
||||
"integrity": "sha512-xrqwo0TYf/h2cfPvGpjdSuSguUbri4vNNizBnwzoZnX0xGo3O5nGJMlbYEp7NOYcnPGBm6LE2axqDWSB847bLw=="
|
||||
},
|
||||
"node_modules/svg-parser": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz",
|
||||
"integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svgo": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
|
||||
"integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
|
||||
"dependencies": {
|
||||
"@trysound/sax": "0.2.0",
|
||||
"commander": "^7.2.0",
|
||||
"css-select": "^5.1.0",
|
||||
"css-tree": "^2.3.1",
|
||||
"css-what": "^6.1.0",
|
||||
"csso": "^5.0.5",
|
||||
"picocolors": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"svgo": "bin/svgo"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/svgo"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -7330,11 +7131,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/world-countries": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/world-countries/-/world-countries-5.0.0.tgz",
|
||||
"integrity": "sha512-wAfOT9Y5i/xnxNOdKJKXdOCw9Q3yQLahBUeuRol+s+o20F6h2a4tLEbJ1lBCYwEQ30Sf9Meqeipk1gib3YwF5w=="
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
|
||||
@@ -40,7 +40,6 @@
|
||||
"react-router": "^6.23.0",
|
||||
"react-router-dom": "^6.23.1",
|
||||
"react-toastify": "^10.0.5",
|
||||
"react-world-flags": "^1.6.0",
|
||||
"recharts": "2.15.1",
|
||||
"redux-persist": "6.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
|
||||
@@ -73,9 +73,12 @@ const CustomToolTip = ({ active, payload, label, dateRange }) => {
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
Response time:
|
||||
</Typography>
|
||||
<Typography
|
||||
ml={theme.spacing(4)}
|
||||
component="span"
|
||||
>
|
||||
{Math.floor(responseTime)}
|
||||
<Typography
|
||||
component="span"
|
||||
|
||||
@@ -11,9 +11,12 @@ const Image = ({
|
||||
alt,
|
||||
width = "auto",
|
||||
height = "auto",
|
||||
minWidth = "auto",
|
||||
minHeight = "auto",
|
||||
maxWidth = "auto",
|
||||
maxHeight = "auto",
|
||||
base64,
|
||||
placeholder,
|
||||
sx,
|
||||
}) => {
|
||||
if (shouldRender === false) {
|
||||
@@ -28,11 +31,21 @@ const Image = ({
|
||||
src = `data:image/png;base64,${base64}`;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof src === "undefined" &&
|
||||
typeof base64 === "undefined" &&
|
||||
typeof placeholder !== "undefined"
|
||||
) {
|
||||
src = placeholder;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
component="img"
|
||||
src={src}
|
||||
alt={alt}
|
||||
minWidth={minWidth}
|
||||
minHeight={minHeight}
|
||||
maxWidth={maxWidth}
|
||||
maxHeight={maxHeight}
|
||||
width={width}
|
||||
@@ -48,6 +61,8 @@ Image.propTypes = {
|
||||
alt: PropTypes.string.isRequired,
|
||||
width: PropTypes.string,
|
||||
height: PropTypes.string,
|
||||
minWidth: PropTypes.string,
|
||||
minHeight: PropTypes.string,
|
||||
maxWidth: PropTypes.string,
|
||||
maxHeight: PropTypes.string,
|
||||
base64: PropTypes.string,
|
||||
|
||||
@@ -106,7 +106,7 @@ const ImageField = ({ id, src, loading, onChange, error, isRound = true, maxSize
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={theme.palette.primary.main}
|
||||
color="info"
|
||||
fontWeight={500}
|
||||
>
|
||||
Click to upload
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, MenuItem, Select, Stack } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import Flag from "react-world-flags";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { setLanguage } from "../Features/Settings/uiSlice";
|
||||
|
||||
@@ -81,55 +81,59 @@ const LanguageSelector = () => {
|
||||
},
|
||||
}}
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastText,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
{languages.map((lang) => {
|
||||
const flag = lang ? `fi fi-${lang}` : null;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={lang}
|
||||
value={lang}
|
||||
sx={{
|
||||
color: theme.palette.primary.contrastText,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
ml={0.5}
|
||||
>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& img": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 0.5,
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.primary.lowContrast,
|
||||
},
|
||||
}}
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
alignItems="center"
|
||||
ml={0.5}
|
||||
>
|
||||
<Flag code={lang.toUpperCase()} />
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ textTransform: "uppercase", fontSize: 10 }}
|
||||
>
|
||||
{lang}
|
||||
</Box>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
))}
|
||||
<Box
|
||||
component="span"
|
||||
sx={{
|
||||
width: 16,
|
||||
height: 12,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
"& img": {
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
borderRadius: 0.5,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{flag && <span className={flag} />}
|
||||
</Box>
|
||||
<Box
|
||||
component="span"
|
||||
sx={{ textTransform: "uppercase", fontSize: 10 }}
|
||||
>
|
||||
{lang}
|
||||
</Box>
|
||||
</Stack>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,12 @@ import { useNavigate } from "react-router-dom";
|
||||
import PropTypes from "prop-types";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
|
||||
const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => {
|
||||
const CreateMonitorHeader = ({
|
||||
isAdmin,
|
||||
label = "Create new",
|
||||
shouldRender = true,
|
||||
path,
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
if (!isAdmin) return null;
|
||||
if (!shouldRender) return <SkeletonLayout />;
|
||||
@@ -18,7 +23,7 @@ const CreateMonitorHeader = ({ isAdmin, shouldRender = true, path }) => {
|
||||
color="accent"
|
||||
onClick={() => navigate(path)}
|
||||
>
|
||||
Create new
|
||||
{label}
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
@@ -30,4 +35,5 @@ CreateMonitorHeader.propTypes = {
|
||||
isAdmin: PropTypes.bool.isRequired,
|
||||
shouldRender: PropTypes.bool,
|
||||
path: PropTypes.string.isRequired,
|
||||
label: PropTypes.string,
|
||||
};
|
||||
|
||||
@@ -22,10 +22,7 @@ const useMonitorUtils = () => {
|
||||
}
|
||||
|
||||
return {
|
||||
id: monitor._id,
|
||||
name: monitor.name,
|
||||
url: monitor.url,
|
||||
title: monitor.name,
|
||||
...monitor,
|
||||
percentage: uptimePercentage,
|
||||
percentageColor,
|
||||
monitor: monitor,
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
const VisuallyHiddenInput = ({ onChange }) => {
|
||||
return (
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={onChange}
|
||||
style={{
|
||||
clip: "rect(0 0 0 0)",
|
||||
clipPath: "inset(50%)",
|
||||
height: 1,
|
||||
overflow: "hidden",
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
whiteSpace: "nowrap",
|
||||
width: 1,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default VisuallyHiddenInput;
|
||||
217
Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
Normal file
217
Client/src/Pages/DistributedUptime/CreateStatus/index.jsx
Normal file
@@ -0,0 +1,217 @@
|
||||
// Components
|
||||
import { Stack, Typography, Button, Box } from "@mui/material";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import Checkbox from "../../../Components/Inputs/Checkbox";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
|
||||
import Image from "../../../Components/Image";
|
||||
import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
// Utils
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { statusPageValidation } from "../../../Validation/validation";
|
||||
import { buildErrors } from "../../../Validation/error";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const CreateStatus = () => {
|
||||
const theme = useTheme();
|
||||
const { monitorId } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const isCreate = location.pathname.startsWith("/distributed-uptime/status/create");
|
||||
const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
|
||||
const BREADCRUMBS = [
|
||||
{ name: "distributed uptime", path: "/distributed-uptime" },
|
||||
{ name: "details", path: `/distributed-uptime/${monitorId}` },
|
||||
{ name: "create status page", path: `` },
|
||||
];
|
||||
// Local state
|
||||
const [form, setForm] = useState({
|
||||
isPublished: false,
|
||||
url: Math.floor(Math.random() * 1000000).toFixed(0),
|
||||
logo: undefined,
|
||||
companyName: "",
|
||||
monitors: [monitorId],
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
|
||||
const handleFormChange = (e) => {
|
||||
const { name, value, checked, type } = e.target;
|
||||
|
||||
// Check for errors
|
||||
const { error } = statusPageValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => buildErrors(prev, name, error));
|
||||
|
||||
if (type === "checkbox") {
|
||||
setForm({ ...form, [name]: checked });
|
||||
return;
|
||||
}
|
||||
setForm({ ...form, [name]: value });
|
||||
};
|
||||
|
||||
const handleImageUpload = (e) => {
|
||||
const img = e.target?.files?.[0];
|
||||
setForm((prev) => ({
|
||||
...prev,
|
||||
logo: img,
|
||||
}));
|
||||
};
|
||||
const handleSubmit = async () => {
|
||||
let logoToSubmit = undefined;
|
||||
|
||||
// Handle image
|
||||
if (typeof form.logo !== "undefined") {
|
||||
logoToSubmit = {
|
||||
src: URL.createObjectURL(form.logo),
|
||||
name: form.logo.name,
|
||||
type: form.logo.type,
|
||||
size: form.logo.size,
|
||||
};
|
||||
}
|
||||
const formToSubmit = { ...form };
|
||||
if (typeof logoToSubmit !== "undefined") {
|
||||
formToSubmit.logo = logoToSubmit;
|
||||
}
|
||||
|
||||
// Validate
|
||||
const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
|
||||
if (typeof error === "undefined") {
|
||||
const success = await createStatusPage({ form: formToSubmit });
|
||||
if (success) {
|
||||
createToast({ body: "Status page created successfully" });
|
||||
navigate(`/distributed-uptime/status/${form.url}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newErrors = {};
|
||||
error?.details?.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors((prev) => ({ ...prev, ...newErrors }));
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<Typography variant="h1">
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
variant="h2"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
>
|
||||
status page
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Stack>
|
||||
<Typography component="h2">Access</Typography>
|
||||
<Typography component="p">
|
||||
If your status page is ready, you can mark it as published.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<Checkbox
|
||||
id="publish"
|
||||
name="isPublished"
|
||||
label={`Published and visible to the public`}
|
||||
isChecked={form.isPublished}
|
||||
onChange={handleFormChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Basic Information</Typography>
|
||||
<Typography component="p">
|
||||
Define company name and the subdomain that your status page points to.
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Stack gap={theme.spacing(18)}>
|
||||
<TextInput
|
||||
id="companyName"
|
||||
name="companyName"
|
||||
type="text"
|
||||
label="Company name"
|
||||
placeholder="Company name"
|
||||
value={form.companyName}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["companyName"]}
|
||||
error={errors["companyName"] ? true : false}
|
||||
/>
|
||||
<TextInput
|
||||
id="url"
|
||||
name="url"
|
||||
type="url"
|
||||
label="Your status page address"
|
||||
value={form.url}
|
||||
onChange={handleFormChange}
|
||||
helperText={errors["url"]}
|
||||
error={errors["url"] ? true : false}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="h2">Logo</Typography>
|
||||
<Typography component="p">Upload a logo for your status page </Typography>
|
||||
</Stack>
|
||||
<Stack
|
||||
gap={theme.spacing(18)}
|
||||
alignItems="center"
|
||||
>
|
||||
<Image
|
||||
src={form.logo ? URL.createObjectURL(form.logo) : undefined}
|
||||
alt="Logo"
|
||||
minWidth={"300px"}
|
||||
minHeight={"100px"}
|
||||
maxWidth={"300px"}
|
||||
maxHeight={"300px"}
|
||||
placeholder={LogoPlaceholder}
|
||||
/>
|
||||
<Box>
|
||||
<Button
|
||||
component="label"
|
||||
role={undefined}
|
||||
variant="contained"
|
||||
color="accent"
|
||||
tabIndex={-1}
|
||||
>
|
||||
Upload logo
|
||||
<VisuallyHiddenInput onChange={handleImageUpload} />
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="accent"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateStatus;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Stack, Typography, List, ListItem } from "@mui/material";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import PulseDot from "../../../../../Components/Animated/PulseDot";
|
||||
import "/node_modules/flag-icons/css/flag-icons.min.css";
|
||||
import "flag-icons/css/flag-icons.min.css";
|
||||
|
||||
const BASE_BOX_PADDING_VERTICAL = 16;
|
||||
const BASE_BOX_PADDING_HORIZONTAL = 8;
|
||||
|
||||
@@ -6,7 +6,7 @@ import maplibregl from "maplibre-gl";
|
||||
import { useSelector } from "react-redux";
|
||||
import buildStyle from "./buildStyle";
|
||||
|
||||
const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
|
||||
const DistributedUptimeMap = ({ width = "100%", checks }) => {
|
||||
const mapContainer = useRef(null);
|
||||
const map = useRef(null);
|
||||
const theme = useTheme();
|
||||
@@ -87,7 +87,6 @@ const DistributedUptimeMap = ({ width = "100%", height = "100%", checks }) => {
|
||||
ref={mapContainer}
|
||||
style={{
|
||||
width: width,
|
||||
height: height,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -24,73 +24,70 @@ const CustomToolTip = ({ active, payload, label }) => {
|
||||
? payload[0]?.payload?.originalAvgResponseTime
|
||||
: (payload[0]?.payload?.avgResponseTime ?? 0);
|
||||
return (
|
||||
<ChartBox
|
||||
icon={<ResponseTimeIcon />}
|
||||
header="Response Times"
|
||||
sx={{ padding: 0 }}
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
sx={{
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
className="area-tooltip"
|
||||
<Typography
|
||||
sx={{
|
||||
backgroundColor: theme.palette.background.main,
|
||||
border: 1,
|
||||
borderColor: theme.palette.primary.lowContrast,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
py: theme.spacing(2),
|
||||
px: theme.spacing(4),
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 12,
|
||||
fontWeight: 500,
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
|
||||
</Typography>
|
||||
<Box mt={theme.spacing(1)}>
|
||||
<Box
|
||||
display="inline-block"
|
||||
width={theme.spacing(4)}
|
||||
height={theme.spacing(4)}
|
||||
backgroundColor={theme.palette.primary.main}
|
||||
sx={{ borderRadius: "50%" }}
|
||||
/>
|
||||
<Stack
|
||||
display="inline-flex"
|
||||
direction="row"
|
||||
justifyContent="space-between"
|
||||
ml={theme.spacing(3)}
|
||||
sx={{
|
||||
"& span": {
|
||||
color: theme.palette.text.tertiary,
|
||||
fontSize: 11,
|
||||
fontWeight: 500,
|
||||
},
|
||||
}}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response time
|
||||
</Typography>
|
||||
<Typography
|
||||
ml={theme.spacing(4)}
|
||||
component="span"
|
||||
>
|
||||
{Math.floor(responseTime)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
Response Time
|
||||
</Typography>{" "}
|
||||
<Typography component="span">
|
||||
{Math.floor(responseTime)}
|
||||
<Typography
|
||||
component="span"
|
||||
sx={{ opacity: 0.8 }}
|
||||
>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
{" "}
|
||||
ms
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
{/* Display original value */}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</ChartBox>
|
||||
{/* Display original value */}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
@@ -143,75 +140,81 @@ const DistributedUptimeResponseChart = ({ checks }) => {
|
||||
|
||||
if (checks.length === 0) return null;
|
||||
return (
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
minWidth={25}
|
||||
height={220}
|
||||
<ChartBox
|
||||
icon={<ResponseTimeIcon />}
|
||||
header="Response Times"
|
||||
sx={{ padding: 0 }}
|
||||
>
|
||||
<AreaChart
|
||||
<ResponsiveContainer
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={checks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
minWidth={25}
|
||||
height={220}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.accent.darker}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.accent.main}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.primary.lowContrast }}
|
||||
content={<CustomToolTip />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.primary.accent}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
<AreaChart
|
||||
width="100%"
|
||||
height="100%"
|
||||
data={checks}
|
||||
margin={{
|
||||
top: 10,
|
||||
right: 0,
|
||||
left: 0,
|
||||
bottom: 0,
|
||||
}}
|
||||
onMouseMove={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<CartesianGrid
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
strokeWidth={1}
|
||||
strokeOpacity={1}
|
||||
fill="transparent"
|
||||
vertical={false}
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="colorUv"
|
||||
x1="0"
|
||||
y1="0"
|
||||
x2="0"
|
||||
y2="1"
|
||||
>
|
||||
<stop
|
||||
offset="0%"
|
||||
stopColor={theme.palette.accent.darker}
|
||||
stopOpacity={0.8}
|
||||
/>
|
||||
<stop
|
||||
offset="100%"
|
||||
stopColor={theme.palette.accent.main}
|
||||
stopOpacity={0}
|
||||
/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<XAxis
|
||||
stroke={theme.palette.primary.lowContrast}
|
||||
dataKey="_id"
|
||||
tick={<CustomTick />}
|
||||
minTickGap={0}
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
height={20}
|
||||
/>
|
||||
<Tooltip
|
||||
cursor={{ stroke: theme.palette.primary.lowContrast }}
|
||||
content={<CustomToolTip />}
|
||||
wrapperStyle={{ pointerEvents: "none" }}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="avgResponseTime"
|
||||
stroke={theme.palette.primary.accent}
|
||||
fill="url(#colorUv)"
|
||||
strokeWidth={isHovered ? 2.5 : 1.5}
|
||||
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</ChartBox>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -45,7 +45,7 @@ const getRandomDevice = () => {
|
||||
model: randomModel,
|
||||
};
|
||||
};
|
||||
const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
const useSubscribeToDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [connectionStatus, setConnectionStatus] = useState(undefined);
|
||||
const [retryCount, setRetryCount] = useState(0);
|
||||
@@ -58,6 +58,14 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
const prevDateRangeRef = useRef(dateRange);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof monitorId === "undefined") {
|
||||
return;
|
||||
}
|
||||
// If this page is public and not published, don't subscribe to details
|
||||
if (isPublic && isPublished === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const cleanup = networkService.subscribeToDistributedUptimeDetails({
|
||||
authToken,
|
||||
@@ -65,6 +73,9 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
dateRange: dateRange,
|
||||
normalize: true,
|
||||
onUpdate: (data) => {
|
||||
if (isLoading === true) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
if (networkError === true) {
|
||||
setNetworkError(false);
|
||||
}
|
||||
@@ -84,6 +95,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
setRetryCount(0); // Reset retry count on successful connection
|
||||
},
|
||||
onError: () => {
|
||||
setIsLoading(false);
|
||||
setNetworkError(true);
|
||||
setConnectionStatus("down");
|
||||
},
|
||||
@@ -91,8 +103,6 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
return cleanup;
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [
|
||||
authToken,
|
||||
@@ -102,6 +112,7 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
setConnectionStatus,
|
||||
networkError,
|
||||
devices,
|
||||
isLoading,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -116,4 +127,4 @@ const useSubscribeToDetails = ({ monitorId, dateRange }) => {
|
||||
return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
|
||||
};
|
||||
|
||||
export default useSubscribeToDetails;
|
||||
export { useSubscribeToDetails };
|
||||
|
||||
@@ -10,21 +10,23 @@ import StatBoxes from "./Components/StatBoxes";
|
||||
import MonitorHeader from "./Components/MonitorHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
|
||||
import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import useSubscribeToDetails from "./Hooks/useSubscribeToDetails";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSubscribeToDetails } from "./Hooks/useSubscribeToDetails";
|
||||
|
||||
const DistributedUptimeDetails = () => {
|
||||
const { monitorId } = useParams();
|
||||
|
||||
// Local State
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const isAdmin = useIsAdmin();
|
||||
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
|
||||
useSubscribeToDetails({ monitorId, dateRange });
|
||||
// Constants
|
||||
@@ -33,6 +35,10 @@ const DistributedUptimeDetails = () => {
|
||||
{ name: "Details", path: `/distributed-uptime/${monitorId}` },
|
||||
];
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
@@ -65,6 +71,11 @@ const DistributedUptimeDetails = () => {
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<MonitorCreateHeader
|
||||
label="Create status page"
|
||||
isAdmin={isAdmin}
|
||||
path={`/distributed-uptime/status/create/${monitorId}`}
|
||||
/>
|
||||
<MonitorHeader monitor={monitor} />
|
||||
<StatBoxes
|
||||
monitor={monitor}
|
||||
|
||||
@@ -79,7 +79,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/distributed-uptime/${row.id}`);
|
||||
navigate(`/distributed-uptime/${row._id}`);
|
||||
},
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -32,6 +32,10 @@ const useSubscribeToMonitors = () => {
|
||||
field: null,
|
||||
order: null,
|
||||
onUpdate: (data) => {
|
||||
if (isLoading === true) {
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
const res = data.monitors;
|
||||
const { monitors, filteredMonitors, summary } = res;
|
||||
const mappedMonitors = filteredMonitors.map((monitor) =>
|
||||
@@ -41,6 +45,9 @@ const useSubscribeToMonitors = () => {
|
||||
setMonitorsSummary(summary);
|
||||
setFilteredMonitors(mappedMonitors);
|
||||
},
|
||||
onError: () => {
|
||||
setIsLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
return cleanup;
|
||||
@@ -49,8 +56,6 @@ const useSubscribeToMonitors = () => {
|
||||
body: error.message,
|
||||
});
|
||||
setNetworkError(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authToken, user, getMonitorWithPercentage, theme]);
|
||||
return [isLoading, networkError, monitors, monitorsSummary, filteredMonitors];
|
||||
|
||||
@@ -10,6 +10,7 @@ import GenericFallback from "../../../Components/GenericFallback";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useSubscribeToMonitors } from "./Hooks/useSubscribeToMonitors";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
// Constants
|
||||
const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
|
||||
|
||||
@@ -21,6 +22,10 @@ const DistributedUptimeMonitors = () => {
|
||||
const [isLoading, networkError, monitors, monitorsSummary, filteredMonitors] =
|
||||
useSubscribeToMonitors();
|
||||
|
||||
if (isLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (networkError) {
|
||||
return (
|
||||
<GenericFallback>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { Stack, Skeleton } from "@mui/material";
|
||||
|
||||
export const SkeletonLayout = () => {
|
||||
return (
|
||||
<Stack>
|
||||
<Skeleton
|
||||
variant="rectangular"
|
||||
height={"90vh"}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default SkeletonLayout;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useSelector } from "react-redux";
|
||||
|
||||
const useStatusPageFetchByUrl = ({ url }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [networkError, setNetworkError] = useState(false);
|
||||
const [statusPage, setStatusPage] = useState(undefined);
|
||||
const [monitorId, setMonitorId] = useState(undefined);
|
||||
const [isPublished, setIsPublished] = useState(false);
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
useEffect(() => {
|
||||
const fetchStatusPageByUrl = async () => {
|
||||
try {
|
||||
const response = await networkService.getStatusPageByUrl({ authToken, url });
|
||||
if (!response?.data?.data) return;
|
||||
const statusPage = response.data.data;
|
||||
setStatusPage(statusPage);
|
||||
setMonitorId(statusPage?.monitors[0]);
|
||||
setIsPublished(statusPage?.isPublished);
|
||||
} catch (error) {
|
||||
setNetworkError(true);
|
||||
createToast({
|
||||
body: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
fetchStatusPageByUrl();
|
||||
}, [authToken, url]);
|
||||
|
||||
return [isLoading, networkError, statusPage, monitorId, isPublished];
|
||||
};
|
||||
|
||||
export { useStatusPageFetchByUrl };
|
||||
194
Client/src/Pages/DistributedUptime/Status/index.jsx
Normal file
194
Client/src/Pages/DistributedUptime/Status/index.jsx
Normal file
@@ -0,0 +1,194 @@
|
||||
//Components
|
||||
import DistributedUptimeMap from "../Details/Components/DistributedUptimeMap";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import { Stack, Typography } from "@mui/material";
|
||||
import DeviceTicker from "../Details/Components/DeviceTicker";
|
||||
import DistributedUptimeResponseChart from "../Details/Components/DistributedUptimeResponseChart";
|
||||
import NextExpectedCheck from "../Details/Components/NextExpectedCheck";
|
||||
import Footer from "../Details/Components/Footer";
|
||||
import StatBoxes from "../Details/Components/StatBoxes";
|
||||
import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader";
|
||||
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
|
||||
import GenericFallback from "../../../Components/GenericFallback";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import SkeletonLayout from "./Components/Skeleton";
|
||||
//Utils
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useSubscribeToDetails } from "../Details/Hooks/useSubscribeToDetails";
|
||||
import { useStatusPageFetchByUrl } from "./Hooks/useStatusPageFetchByUrl";
|
||||
import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
const DistributedUptimeStatus = () => {
|
||||
const { url } = useParams();
|
||||
const location = useLocation();
|
||||
const isPublic = location.pathname.startsWith("/distributed-uptime/status/public");
|
||||
|
||||
// Local State
|
||||
const [dateRange, setDateRange] = useState("day");
|
||||
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const navigate = useNavigate();
|
||||
const [
|
||||
statusPageIsLoading,
|
||||
statusPageNetworkError,
|
||||
statusPage,
|
||||
monitorId,
|
||||
isPublished,
|
||||
] = useStatusPageFetchByUrl({
|
||||
url,
|
||||
});
|
||||
|
||||
const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
|
||||
useSubscribeToDetails({ monitorId, dateRange, isPublic, isPublished });
|
||||
|
||||
const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
|
||||
navigate("/distributed-uptime");
|
||||
}, url);
|
||||
// Constants
|
||||
const BREADCRUMBS = [
|
||||
{ name: "Distributed Uptime", path: "/distributed-uptime" },
|
||||
{ name: "details", path: `/distributed-uptime/${monitorId}` },
|
||||
{ name: "status", path: `` },
|
||||
];
|
||||
|
||||
let sx = {};
|
||||
if (isPublic) {
|
||||
sx = {
|
||||
paddingTop: "10vh",
|
||||
paddingRight: "10vw",
|
||||
paddingBottom: "10vh",
|
||||
paddingLeft: "10vw",
|
||||
};
|
||||
}
|
||||
|
||||
// Done loading, a status page doesn't exist
|
||||
if (!statusPageIsLoading && typeof statusPage === "undefined") {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography
|
||||
variant="h1"
|
||||
marginY={theme.spacing(4)}
|
||||
color={theme.palette.primary.contrastTextTertiary}
|
||||
>
|
||||
A public status page is not set up.
|
||||
</Typography>
|
||||
<Typography>Please contact to your administrator</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
// Done loading, a status page exists but is not public
|
||||
if (!statusPageIsLoading && statusPage.isPublished === false) {
|
||||
return (
|
||||
<Stack sx={sx}>
|
||||
<GenericFallback>
|
||||
<Typography>This status page is not public.</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading || statusPageIsLoading) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
if (networkError || statusPageNetworkError) {
|
||||
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 (
|
||||
typeof statusPage === "undefined" ||
|
||||
typeof monitor === "undefined" ||
|
||||
monitor.totalChecks === 0
|
||||
) {
|
||||
return (
|
||||
<Stack gap={theme.spacing(10)}>
|
||||
<Breadcrumbs list={BREADCRUMBS} />
|
||||
<GenericFallback>
|
||||
<Typography>There is no check history for this monitor yet.</Typography>
|
||||
</GenericFallback>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="column"
|
||||
gap={theme.spacing(10)}
|
||||
sx={sx}
|
||||
>
|
||||
{!isPublic && <Breadcrumbs list={BREADCRUMBS} />}
|
||||
<ControlsHeader
|
||||
shouldShow={!isPublic}
|
||||
statusPage={statusPage}
|
||||
isDeleting={isDeleting}
|
||||
isDeleteOpen={isDeleteOpen}
|
||||
setIsDeleteOpen={setIsDeleteOpen}
|
||||
/>
|
||||
<StatBoxes
|
||||
monitor={monitor}
|
||||
lastUpdateTrigger={lastUpdateTrigger}
|
||||
/>
|
||||
<NextExpectedCheck
|
||||
lastUpdateTime={monitor?.timeSinceLastCheck ?? 0}
|
||||
interval={monitor?.interval ?? 0}
|
||||
trigger={lastUpdateTrigger}
|
||||
/>
|
||||
<MonitorTimeFrameHeader
|
||||
dateRange={dateRange}
|
||||
setDateRange={setDateRange}
|
||||
/>
|
||||
<DistributedUptimeResponseChart checks={monitor?.groupedChecks ?? []} />
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(8)}
|
||||
>
|
||||
<DistributedUptimeMap
|
||||
checks={monitor?.groupedMapChecks ?? []}
|
||||
height={"100%"}
|
||||
width={"100%"}
|
||||
/>
|
||||
<DeviceTicker
|
||||
width={"25vw"}
|
||||
data={monitor?.latestChecks ?? []}
|
||||
connectionStatus={connectionStatus}
|
||||
/>
|
||||
</Stack>
|
||||
<Footer />
|
||||
<Dialog
|
||||
// open={isOpen.deleteStats}
|
||||
title="Do you want to delete this status page?"
|
||||
onConfirm={() => {
|
||||
deleteStatusPage();
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setIsDeleteOpen(false);
|
||||
}}
|
||||
open={isDeleteOpen}
|
||||
confirmationButtonLabel="Yes, delete status page"
|
||||
description="Once deleted, your status page cannot be retrieved."
|
||||
isLoading={isDeleting || isLoading}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default DistributedUptimeStatus;
|
||||
@@ -126,7 +126,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
|
||||
transition: "background-color .3s ease",
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => openDetails(row.id),
|
||||
onRowClick: (row) => openDetails(row._id),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,9 +14,15 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting }) => {
|
||||
const location = useLocation();
|
||||
const currentPath = location.pathname;
|
||||
const navigate = useNavigate();
|
||||
|
||||
if (currentPath === "/status/public") {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (currentPath.startsWith("/distributed-uptime/status/public")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
@@ -80,7 +86,7 @@ const ControlsHeader = ({ statusPage, isDeleting, isDeleteOpen, setIsDeleteOpen
|
||||
<Image
|
||||
shouldRender={statusPage?.logo?.data ? true : false}
|
||||
alt={"Company logo"}
|
||||
maxWidth={"100px"}
|
||||
maxWidth={"300px"}
|
||||
base64={statusPage?.logo?.data}
|
||||
/>
|
||||
<Typography variant="h2">{statusPage?.companyName}</Typography>
|
||||
|
||||
@@ -34,7 +34,7 @@ const MonitorsList = ({ monitors = [] }) => {
|
||||
gap={theme.spacing(20)}
|
||||
>
|
||||
<Box flex={9}>
|
||||
<StatusPageBarChart checks={monitor.checks.slice().reverse()} />
|
||||
<StatusPageBarChart checks={monitor?.checks?.slice().reverse()} />
|
||||
</Box>
|
||||
<Box flex={1}>
|
||||
<StatusLabel
|
||||
|
||||
@@ -4,15 +4,14 @@ import { networkService } from "../../../../main";
|
||||
import { createToast } from "../../../../Utils/toastUtils";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
const useStatusPageDelete = (fetchStatusPage) => {
|
||||
const useStatusPageDelete = (fetchStatusPage, url = "/status/public") => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { authToken } = useSelector((state) => state.auth);
|
||||
|
||||
const deleteStatusPage = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await networkService.deleteStatusPage({ authToken });
|
||||
await networkService.deleteStatusPage({ authToken, url });
|
||||
fetchStatusPage?.();
|
||||
return true;
|
||||
} catch (error) {
|
||||
|
||||
@@ -18,11 +18,13 @@ const useStatusPageFetch = (isCreate = false) => {
|
||||
const response = await networkService.getStatusPage({ authToken });
|
||||
if (!response?.data?.data) return;
|
||||
const { statusPage, monitors } = response.data.data;
|
||||
|
||||
setStatusPage(statusPage);
|
||||
|
||||
const monitorsWithPercentage = monitors.map((monitor) =>
|
||||
getMonitorWithPercentage(monitor, theme)
|
||||
);
|
||||
|
||||
setMonitors(monitorsWithPercentage);
|
||||
} catch (error) {
|
||||
// If there is a 404, status page is not found
|
||||
@@ -35,7 +37,7 @@ const useStatusPageFetch = (isCreate = false) => {
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [authToken, theme]);
|
||||
}, [authToken, theme, getMonitorWithPercentage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isCreate === true) {
|
||||
|
||||
@@ -32,6 +32,7 @@ const PublicStatus = () => {
|
||||
const currentPath = location.pathname;
|
||||
let sx = { paddingLeft: theme.spacing(20), paddingRight: theme.spacing(20) };
|
||||
let link = undefined;
|
||||
|
||||
// Public status page
|
||||
if (currentPath === "/status/public") {
|
||||
sx = {
|
||||
@@ -139,7 +140,6 @@ const PublicStatus = () => {
|
||||
<MonitorsList monitors={monitors} />
|
||||
{link}
|
||||
<Dialog
|
||||
// open={isOpen.deleteStats}
|
||||
title="Do you want to delete this status page?"
|
||||
onConfirm={() => {
|
||||
deleteStatusPage();
|
||||
|
||||
@@ -191,7 +191,7 @@ const UptimeDataTable = ({
|
||||
},
|
||||
},
|
||||
onRowClick: (row) => {
|
||||
navigate(`/uptime/${row.id}`);
|
||||
navigate(`/uptime/${row._id}`);
|
||||
},
|
||||
emptyView: "No monitors found",
|
||||
}}
|
||||
|
||||
@@ -31,6 +31,8 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details";
|
||||
import DistributedUptimeMonitors from "../Pages/DistributedUptime/Monitors";
|
||||
import CreateDistributedUptime from "../Pages/DistributedUptime/Create";
|
||||
import DistributedUptimeDetails from "../Pages/DistributedUptime/Details";
|
||||
import DistributedUptimeStatus from "../Pages/DistributedUptime/Status";
|
||||
import CreateDistributedUptimeStatus from "../Pages/DistributedUptime/CreateStatus";
|
||||
|
||||
// Incidents
|
||||
import Incidents from "../Pages/Incidents";
|
||||
@@ -110,6 +112,23 @@ const Routes = () => {
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/status/create/:monitorId"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<CreateDistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/status/:url"
|
||||
element={
|
||||
<ProtectedDistributedUptimeRoute>
|
||||
<DistributedUptimeStatus />
|
||||
</ProtectedDistributedUptimeRoute>
|
||||
}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="pagespeed"
|
||||
element={<PageSpeed />}
|
||||
@@ -223,6 +242,10 @@ const Routes = () => {
|
||||
path="/status/public"
|
||||
element={<Status />}
|
||||
/>
|
||||
<Route
|
||||
path="/distributed-uptime/status/public/:url"
|
||||
element={<DistributedUptimeStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path="*"
|
||||
|
||||
@@ -990,19 +990,31 @@ class NetworkService {
|
||||
});
|
||||
}
|
||||
|
||||
async getStatusPageByUrl(config) {
|
||||
const { authToken, url } = config;
|
||||
return this.axiosInstance.get(`/status-page/${url}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createStatusPage(config) {
|
||||
const { authToken, user, form, isCreate } = config;
|
||||
const fd = new FormData();
|
||||
fd.append("isPublished", form.isPublished);
|
||||
fd.append("companyName", form.companyName);
|
||||
fd.append("url", form.url);
|
||||
fd.append("timezone", form.timezone);
|
||||
fd.append("color", form.color);
|
||||
fd.append("showCharts", form.showCharts);
|
||||
fd.append("showUptimePercentage", form.showUptimePercentage);
|
||||
form.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
});
|
||||
form.isPublished && fd.append("isPublished", form.isPublished);
|
||||
form.companyName && fd.append("companyName", form.companyName);
|
||||
form.url && fd.append("url", form.url);
|
||||
form.timezone && fd.append("timezone", form.timezone);
|
||||
form.color && fd.append("color", form.color);
|
||||
form.showCharts && fd.append("showCharts", form.showCharts);
|
||||
form.showUptimePercentage &&
|
||||
fd.append("showUptimePercentage", form.showUptimePercentage);
|
||||
form.monitors &&
|
||||
form.monitors.forEach((monitorId) => {
|
||||
fd.append("monitors[]", monitorId);
|
||||
});
|
||||
if (form?.logo?.src && form?.logo?.src !== "") {
|
||||
const imageResult = await axios.get(form.logo.src, {
|
||||
responseType: "blob",
|
||||
@@ -1029,8 +1041,10 @@ class NetworkService {
|
||||
}
|
||||
|
||||
async deleteStatusPage(config) {
|
||||
const { authToken } = config;
|
||||
return this.axiosInstance.delete(`/status-page`, {
|
||||
const { authToken, url } = config;
|
||||
const encodedUrl = encodeURIComponent(url);
|
||||
|
||||
return this.axiosInstance.delete(`/status-page/${encodedUrl}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
|
||||
@@ -128,6 +128,10 @@ const baseTheme = (palette) => ({
|
||||
"&:hover": {
|
||||
boxShadow: "none",
|
||||
},
|
||||
"&.Mui-disabled": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
"&.MuiLoadingButton-root": {
|
||||
"&:disabled": {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
import i18n from 'i18next';
|
||||
import { initReactI18next } from 'react-i18next';
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
|
||||
const primaryLanguage = 'gb';
|
||||
const primaryLanguage = "gb";
|
||||
|
||||
const translations = import.meta.glob('../locales/*.json', { eager: true });
|
||||
const translations = import.meta.glob("../locales/*.json", { eager: true });
|
||||
|
||||
const resources = {};
|
||||
Object.keys(translations).forEach((path) => {
|
||||
const langCode = path.match(/\/([^/]+)\.json$/)[1];
|
||||
resources[langCode] = {
|
||||
translation: translations[path].default || translations[path]
|
||||
};
|
||||
const langCode = path.match(/\/([^/]+)\.json$/)[1];
|
||||
resources[langCode] = {
|
||||
translation: translations[path].default || translations[path],
|
||||
};
|
||||
});
|
||||
|
||||
const savedLanguage = localStorage.getItem("language") || primaryLanguage;
|
||||
|
||||
i18n
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources,
|
||||
lng: savedLanguage,
|
||||
fallbackLng: primaryLanguage,
|
||||
debug: import.meta.env.MODE === 'development',
|
||||
ns: ['translation'],
|
||||
defaultNS: 'translation',
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: savedLanguage,
|
||||
fallbackLng: primaryLanguage,
|
||||
debug: import.meta.env.MODE === "development",
|
||||
ns: ["translation"],
|
||||
defaultNS: "translation",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
|
||||
i18n.on("languageChanged", (lng) => {
|
||||
localStorage.setItem("language", lng);
|
||||
localStorage.setItem("language", lng);
|
||||
});
|
||||
|
||||
export default i18n;
|
||||
|
||||
@@ -180,6 +180,8 @@ const imageValidation = joi.object({
|
||||
|
||||
const logoImageValidation = joi
|
||||
.object({
|
||||
src: joi.string(),
|
||||
name: joi.string(),
|
||||
type: joi
|
||||
.string()
|
||||
.valid("image/jpeg", "image/png")
|
||||
|
||||
5
Client/src/assets/Images/logo_placeholder.svg
Normal file
5
Client/src/assets/Images/logo_placeholder.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="300" height="100" viewBox="0 0 300 100">
|
||||
<rect width="100%" height="100%" fill="#DDDDDD" />
|
||||
<path fill="#999999"
|
||||
d="m76.795 37.635-7.38 12.4v7.84h-3.75v-7.84l-7.38-12.4h3.32q.49 0 .78.23.29.24.48.61l3.7 6.76q.32.6.57 1.13.24.53.44 1.07.18-.54.42-1.07.23-.53.54-1.13l3.68-6.76q.16-.31.47-.58.3-.26.78-.26zm7.88 5.65q1.6 0 2.91.52t2.24 1.47 1.43 2.32q.51 1.38.51 3.07 0 1.71-.51 3.08-.5 1.37-1.43 2.34-.93.96-2.24 1.48t-2.91.52q-1.61 0-2.92-.52-1.32-.52-2.25-1.48-.93-.97-1.44-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.44-2.32t2.25-1.47q1.31-.52 2.92-.52m0 12.14q1.8 0 2.66-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.66-1.22-1.82 0-2.69 1.22-.88 1.23-.88 3.54t.88 3.52q.87 1.21 2.69 1.21m18.65-11.91h3.46v14.36h-2.11q-.69 0-.87-.63l-.24-1.15q-.88.9-1.95 1.45-1.06.55-2.5.55-1.18 0-2.08-.4-.9-.39-1.52-1.12t-.93-1.73q-.32-1-.32-2.21v-9.12h3.46v9.12q0 1.32.61 2.04t1.83.72q.89 0 1.68-.4.78-.4 1.48-1.1zm9.93.86.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m14.24-7.3h3.45v20.8h-3.45zm13.27 6.21q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m14.98-5.08q.64 0 1.12-.18.47-.17.79-.48.31-.31.48-.74.16-.44.16-.95 0-1.07-.64-1.69t-1.91-.62q-1.28 0-1.91.62-.64.62-.64 1.69 0 .5.16.93.16.44.48.75.31.32.8.49.48.18 1.11.18m3.9 8.17q0-.42-.25-.68-.25-.27-.68-.42-.44-.14-1.02-.21t-1.23-.11l-1.34-.06q-.7-.03-1.36-.11-.57.32-.93.75-.35.44-.35 1.01 0 .38.19.71.18.33.6.57.41.23 1.07.37.66.13 1.61.13.96 0 1.66-.15.7-.14 1.16-.4.45-.26.66-.62t.21-.78m-.68-14.51h4.13v1.28q0 .62-.74.76l-1.29.24q.29.74.29 1.62 0 1.07-.42 1.93-.43.86-1.19 1.46-.75.6-1.78.93t-2.22.33q-.42 0-.81-.04-.4-.04-.77-.11-.68.4-.68.91 0 .43.4.63.4.21 1.06.29t1.5.1q.84.03 1.72.1t1.72.24q.84.18 1.5.55.66.38 1.06 1.03t.4 1.68q0 .95-.47 1.84-.47.9-1.36 1.6t-2.18 1.13q-1.3.42-2.95.42-1.63 0-2.83-.31-1.2-.32-2-.84-.8-.53-1.19-1.21-.39-.69-.39-1.43 0-1.01.61-1.69.6-.68 1.67-1.08-.58-.3-.91-.79-.34-.49-.34-1.28 0-.33.12-.67t.35-.68q.23-.33.58-.64.35-.3.83-.53-1.09-.59-1.72-1.57-.62-.98-.62-2.3 0-1.06.43-1.92.42-.86 1.19-1.47.76-.61 1.8-.93 1.05-.33 2.28-.33.92 0 1.73.19.82.19 1.49.56m12.67-.72q1.59 0 2.9.52t2.24 1.47 1.44 2.32q.5 1.38.5 3.07 0 1.71-.5 3.08-.51 1.37-1.44 2.34-.93.96-2.24 1.48t-2.9.52q-1.61 0-2.93-.52-1.31-.52-2.24-1.48-.94-.97-1.45-2.34t-.51-3.08q0-1.69.51-3.07.51-1.37 1.45-2.32.93-.95 2.24-1.47 1.32-.52 2.93-.52m0 12.14q1.79 0 2.65-1.2.86-1.21.86-3.53 0-2.33-.86-3.54-.86-1.22-2.65-1.22-1.82 0-2.7 1.22-.87 1.23-.87 3.54t.87 3.52q.88 1.21 2.7 1.21m19.99-18.35v7.98q.84-.79 1.85-1.28t2.36-.49q1.18 0 2.09.4t1.52 1.12.92 1.72q.32 1 .32 2.21v9.14h-3.46v-9.14q0-1.32-.6-2.04t-1.84-.72q-.89 0-1.68.41-.78.4-1.48 1.1v10.39h-3.46v-20.8zm15.06 11.97h6.53q0-.68-.19-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.09 1.11.4 1.91.3.81.81 1.33.5.53 1.2.79.69.26 1.53.26t1.45-.2 1.06-.43q.46-.24.8-.44.34-.19.66-.19.44 0 .65.32l.99 1.26q-.57.67-1.29 1.12-.71.46-1.49.73-.77.28-1.58.39-.8.11-1.56.11-1.5 0-2.79-.5-1.28-.49-2.24-1.47-.95-.97-1.49-2.41-.55-1.43-.55-3.32 0-1.47.48-2.77.47-1.29 1.36-2.25t2.17-1.52 2.89-.56q1.36 0 2.51.44 1.15.43 1.97 1.26.83.84 1.3 2.05t.47 2.76q0 .79-.17 1.06t-.65.27m6.46-6.75.21 1.63q.67-1.29 1.59-2.02.93-.74 2.19-.74.99 0 1.59.43l-.22 2.59q-.07.26-.2.36-.14.11-.36.11-.21 0-.62-.07-.42-.07-.81-.07-.57 0-1.02.16-.45.17-.8.49-.36.31-.63.76-.28.45-.52 1.02v8.85h-3.45v-14.36h2.03q.53 0 .74.18.21.19.28.68m10.12 4.67h6.52q0-.68-.18-1.27-.19-.6-.57-1.04-.38-.45-.96-.71t-1.35-.26q-1.5 0-2.36.85-.86.86-1.1 2.43m8.82 2.08h-8.89q.08 1.11.39 1.91.31.81.81 1.33.51.53 1.2.79t1.53.26 1.45-.2 1.07-.43q.45-.24.8-.44.34-.19.66-.19.43 0 .64.32l1 1.26q-.58.67-1.29 1.12-.71.46-1.49.73-.78.28-1.58.39-.81.11-1.56.11-1.5 0-2.79-.5-1.29-.49-2.24-1.47-.95-.97-1.5-2.41-.54-1.43-.54-3.32 0-1.47.47-2.77.48-1.29 1.37-2.25t2.17-1.52 2.89-.56q1.36 0 2.5.44 1.15.43 1.98 1.26.82.84 1.29 2.05t.47 2.76q0 .79-.17 1.06-.16.27-.64.27" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
@@ -1,7 +1,6 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import Monitor from "../db/models/Monitor.js";
|
||||
import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
|
||||
|
||||
const SERVICE_NAME = "DistributedUptimeQueueController";
|
||||
|
||||
class DistributedUptimeController {
|
||||
|
||||
@@ -62,7 +62,7 @@ class StatusPageController {
|
||||
try {
|
||||
const statusPage = await this.db.getStatusPage();
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_BY_URL,
|
||||
msg: successMessages.STATUS_PAGE,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -70,9 +70,28 @@ class StatusPageController {
|
||||
}
|
||||
};
|
||||
|
||||
getStatusPageByUrl = async (req, res, next) => {
|
||||
try {
|
||||
await getStatusPageParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const statusPage = await this.db.getStatusPageByUrl(req.params.url);
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_BY_URL,
|
||||
data: statusPage,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getStatusPageByUrl"));
|
||||
}
|
||||
};
|
||||
|
||||
deleteStatusPage = async (req, res, next) => {
|
||||
try {
|
||||
await this.db.deleteStatusPage();
|
||||
await this.db.deleteStatusPage(req.params.url);
|
||||
return res.success({
|
||||
msg: successMessages.STATUS_PAGE_DELETE,
|
||||
});
|
||||
|
||||
@@ -15,12 +15,11 @@ const StatusPageSchema = mongoose.Schema(
|
||||
},
|
||||
timezone: {
|
||||
type: String,
|
||||
required: true,
|
||||
default: "America/Toronto",
|
||||
required: false,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: false,
|
||||
default: "#4169E1",
|
||||
},
|
||||
monitors: [
|
||||
|
||||
@@ -47,10 +47,21 @@ const updateStatusPage = async (statusPageData, image) => {
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPageByUrl = async (url) => {
|
||||
try {
|
||||
const statusPage = await StatusPage.aggregate([{ $match: { url } }]);
|
||||
return statusPage[0];
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "getStatusPageByUrl";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusPage = async () => {
|
||||
try {
|
||||
const statusPageQuery = await StatusPage.aggregate([
|
||||
{ $limit: 1 },
|
||||
{ $match: { url: "/status/public" } },
|
||||
{
|
||||
$set: {
|
||||
originalMonitors: "$monitors",
|
||||
@@ -143,14 +154,20 @@ const getStatusPage = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const deleteStatusPage = async () => {
|
||||
const deleteStatusPage = async (url) => {
|
||||
try {
|
||||
await StatusPage.deleteOne({});
|
||||
await StatusPage.deleteOne({ url });
|
||||
} catch (error) {
|
||||
error.service = SERVICE_NAME;
|
||||
error.method = "createStatusPage";
|
||||
error.method = "deleteStatusPage";
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export { createStatusPage, updateStatusPage, getStatusPage, deleteStatusPage };
|
||||
export {
|
||||
createStatusPage,
|
||||
updateStatusPage,
|
||||
getStatusPage,
|
||||
getStatusPageByUrl,
|
||||
deleteStatusPage,
|
||||
};
|
||||
|
||||
6
Server/package-lock.json
generated
6
Server/package-lock.json
generated
@@ -6882,9 +6882,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
||||
"version": "3.5.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.0.tgz",
|
||||
"integrity": "sha512-quyMrVt6svPS7CjQ9gKb3GLEX/rl3BCL2oa/QkNcXv4YNVBC9olt3s+H7ukto06q7B1Qz46PbrKLO34PR6vXcA==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
|
||||
@@ -12,6 +12,7 @@ class StatusPageRoutes {
|
||||
|
||||
initRoutes() {
|
||||
this.router.get("/", this.statusPageController.getStatusPage);
|
||||
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
|
||||
this.router.post(
|
||||
"/",
|
||||
upload.single("logo"),
|
||||
@@ -24,7 +25,7 @@ class StatusPageRoutes {
|
||||
verifyJWT,
|
||||
this.statusPageController.updateStatusPage
|
||||
);
|
||||
this.router.delete("/", verifyJWT, this.statusPageController.deleteStatusPage);
|
||||
this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage);
|
||||
}
|
||||
|
||||
getRouter() {
|
||||
|
||||
@@ -135,6 +135,7 @@ const successMessages = {
|
||||
|
||||
// Status Page
|
||||
STATUS_PAGE_BY_URL: "Got status page by url successfully",
|
||||
STATUS_PAGE: "Got status page successfully",
|
||||
STATUS_PAGE_CREATE: "Status page created successfully",
|
||||
STATUS_PAGE_DELETE: "Status page deleted successfully",
|
||||
STATUS_PAGE_UPDATE: "Status page updated successfully",
|
||||
|
||||
@@ -421,8 +421,8 @@ const getStatusPageParamValidation = joi.object({
|
||||
const createStatusPageBodyValidation = joi.object({
|
||||
companyName: joi.string().required(),
|
||||
url: joi.string().required(),
|
||||
timezone: joi.string().required(),
|
||||
color: joi.string().required(),
|
||||
timezone: joi.string().optional(),
|
||||
color: joi.string().optional(),
|
||||
monitors: joi
|
||||
.array()
|
||||
.items(joi.string().pattern(/^[0-9a-fA-F]{24}$/))
|
||||
@@ -434,7 +434,7 @@ const createStatusPageBodyValidation = joi.object({
|
||||
"any.required": "Monitors are required",
|
||||
}),
|
||||
isPublished: joi.boolean(),
|
||||
showCharts: joi.boolean(),
|
||||
showCharts: joi.boolean().optional(),
|
||||
showUptimePercentage: joi.boolean(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user