Merge branch 'develop' into develop

This commit is contained in:
rkhatta1
2025-02-09 11:12:33 -07:00
committed by GitHub
44 changed files with 911 additions and 456 deletions

210
Client/package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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"

View File

@@ -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,

View File

@@ -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

View File

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

View File

@@ -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,
};

View File

@@ -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,

View File

@@ -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;

View 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;

View File

@@ -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;

View File

@@ -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,
}}
/>
);

View File

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

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -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 };

View File

@@ -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}

View File

@@ -79,7 +79,7 @@ const MonitorTable = ({ isLoading, monitors }) => {
},
},
onRowClick: (row) => {
navigate(`/distributed-uptime/${row.id}`);
navigate(`/distributed-uptime/${row._id}`);
},
}}
/>

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -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];

View File

@@ -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>

View File

@@ -0,0 +1,14 @@
import { Stack, Skeleton } from "@mui/material";
export const SkeletonLayout = () => {
return (
<Stack>
<Skeleton
variant="rectangular"
height={"90vh"}
/>
</Stack>
);
};
export default SkeletonLayout;

View File

@@ -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 };

View 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;

View File

@@ -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),
}}
/>
);

View File

@@ -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>

View File

@@ -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

View File

@@ -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) {

View File

@@ -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) {

View File

@@ -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();

View File

@@ -191,7 +191,7 @@ const UptimeDataTable = ({
},
},
onRowClick: (row) => {
navigate(`/uptime/${row.id}`);
navigate(`/uptime/${row._id}`);
},
emptyView: "No monitors found",
}}

View File

@@ -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="*"

View File

@@ -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}`,
},

View File

@@ -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,

View File

@@ -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;

View File

@@ -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")

View 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

View File

@@ -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 {

View File

@@ -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,
});

View File

@@ -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: [

View File

@@ -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,
};

View File

@@ -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"

View File

@@ -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() {

View File

@@ -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",

View File

@@ -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(),
});