diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml
index d3f34635c..056e2c586 100644
--- a/.github/workflows/check-format.yml
+++ b/.github/workflows/check-format.yml
@@ -16,7 +16,7 @@ jobs:
- name: Install client dependencies
working-directory: client
- run: npm ci
+ run: npm install
- name: Check client formatting
working-directory: client
@@ -34,7 +34,7 @@ jobs:
- name: Install server dependencies
working-directory: server
- run: npm ci
+ run: npm install
- name: Check server formatting
working-directory: server
diff --git a/client/src/Components/Charts/StatusPageBarChart/index.jsx b/client/src/Components/Charts/StatusPageBarChart/index.jsx
index e8a34806d..eab0c7d1d 100644
--- a/client/src/Components/Charts/StatusPageBarChart/index.jsx
+++ b/client/src/Components/Charts/StatusPageBarChart/index.jsx
@@ -1,7 +1,7 @@
import { useTheme } from "@emotion/react";
import { Box, Stack, Tooltip, Typography } from "@mui/material";
import { formatDateWithTz } from "../../../Utils/timeUtils";
-import { useEffect, useState } from "react";
+import { useEffect, useState, forwardRef } from "react";
import { useSelector } from "react-redux";
import PropTypes from "prop-types";
@@ -17,23 +17,29 @@ import PropTypes from "prop-types";
* @returns {JSX.Element} The Bar component.
*/
-const Bar = ({ width, height, backgroundColor, borderRadius, children }) => {
- const theme = useTheme();
+const Bar = forwardRef(
+ ({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => {
+ const theme = useTheme();
- return (
-
- {children}
-
- );
-};
+ return (
+
+ {children}
+
+ );
+ }
+);
+
+Bar.displayName = "Bar";
Bar.propTypes = {
width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
diff --git a/client/src/Components/Check/Check.jsx b/client/src/Components/Check/Check.jsx
index 7f37a4dc1..02b9fd78a 100644
--- a/client/src/Components/Check/Check.jsx
+++ b/client/src/Components/Check/Check.jsx
@@ -65,7 +65,7 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
};
Check.propTypes = {
- text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired,
+ text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
noHighlightText: PropTypes.string,
variant: PropTypes.oneOf(["info", "error", "success"]),
outlined: PropTypes.bool,
diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/Dialog/genericDialog.jsx
index b2cbe1299..a13eba849 100644
--- a/client/src/Components/Dialog/genericDialog.jsx
+++ b/client/src/Components/Dialog/genericDialog.jsx
@@ -2,7 +2,7 @@ import { useId } from "react";
import PropTypes from "prop-types";
import { Modal, Stack, Typography } from "@mui/material";
-const GenericDialog = ({ title, description, open, onClose, theme, children }) => {
+const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => {
const titleId = useId();
const descriptionId = useId();
const ariaDescribedBy = description?.length > 0 ? descriptionId : "";
@@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
>
{title}
@@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) =
{description}
@@ -64,6 +67,7 @@ GenericDialog.propTypes = {
theme: PropTypes.object.isRequired,
children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node])
.isRequired,
+ width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
};
export { GenericDialog };
diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js
index 705906efa..fb577272a 100644
--- a/client/src/Hooks/inviteHooks.js
+++ b/client/src/Hooks/inviteHooks.js
@@ -14,25 +14,25 @@ const useGetInviteToken = () => {
const clearToken = () => {
setToken(undefined);
};
-
+ const fetchToken = async (email, role) => {
+ const response = await networkService.requestInvitationToken({ email, role });
+ const token = response?.data?.data?.token;
+ if (typeof token === "undefined") {
+ throw new Error(t("inviteNoTokenFound"));
+ }
+ return token;
+ };
const getInviteToken = async ({ email, role }) => {
try {
- const response = await networkService.requestInvitationToken({
- email,
- role,
- });
- const token = response?.data?.data?.token;
- if (typeof token === "undefined") {
- throw new Error(t("inviteNoTokenFound"));
- }
-
+ setIsLoading(true);
+ const token = await fetchToken(email, role);
let inviteLink = token;
if (typeof CLIENT_HOST !== "undefined") {
inviteLink = `${CLIENT_HOST}/register/${token}`;
}
-
setToken(inviteLink);
+ return token;
} catch (error) {
setError(error);
} finally {
@@ -40,7 +40,26 @@ const useGetInviteToken = () => {
}
};
- return [getInviteToken, clearToken, isLoading, error, token];
+ const addTeamMember = async (formData, role) => {
+ try {
+ setIsLoading(true);
+ const token = await fetchToken(formData.email, role);
+ const toSubmit = {
+ ...formData,
+ inviteToken: token,
+ };
+ delete toSubmit.confirm;
+ const responseRegister = await networkService.registerUser(toSubmit);
+ return responseRegister;
+ } catch (error) {
+ setError(error);
+ throw error;
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [getInviteToken, clearToken, isLoading, error, token, addTeamMember];
};
export { useGetInviteToken };
diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx
new file mode 100644
index 000000000..ab1ba599d
--- /dev/null
+++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import Button from "@mui/material/Button";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import { useTheme } from "@emotion/react";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import { useTranslation } from "react-i18next";
+import Proptypes from "prop-types";
+
+const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ return (
+ <>
+ }
+ onClick={handleClick}
+ >
+ {t("teamPanel.addTeamMember.addMemberMenu")}
+
+
+ >
+ );
+};
+
+AddMemberMenu.propTypes = {
+ handleInviteOpen: Proptypes.func.isRequired,
+ handleIsRegisterOpen: Proptypes.func.isRequired,
+};
+
+export default AddMemberMenu;
diff --git a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
new file mode 100644
index 000000000..e42c504b2
--- /dev/null
+++ b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx
@@ -0,0 +1,48 @@
+import { useState } from "react";
+import { newOrChangedCredentials } from "../../../../../Validation/validation";
+import { useTranslation } from "react-i18next";
+const useAddTeamMember = () => {
+ const { t } = useTranslation();
+ const [errors, setErrors] = useState({});
+
+ const clearErrors = () => setErrors({});
+
+ const validateFields = (name, value, formData) => {
+ const { error } = newOrChangedCredentials.validate(
+ { [name]: value },
+ { abortEarly: false, context: { password: formData.password } }
+ );
+
+ setErrors((prev) => ({
+ ...prev,
+ [name]: error?.details?.[0]?.message || "",
+ }));
+ };
+
+ const validateForm = (formData, role) => {
+ const { error } = newOrChangedCredentials.validate(formData, {
+ abortEarly: false,
+ context: { password: formData.password },
+ });
+ const formErrors = {};
+ if (error) {
+ for (const err of error.details) {
+ formErrors[err.path[0]] = err.message;
+ }
+ }
+ if (!role[0] || role.length === 0) {
+ formErrors.role = t(
+ "teamPanel.registerTeamMember.auth.common.inputs.role.errors.empty"
+ );
+ }
+ if (Object.keys(formErrors).length > 0) {
+ setErrors(formErrors);
+ return false;
+ }
+ setErrors({});
+ return true;
+ };
+
+ return { errors, setErrors, clearErrors, validateFields, validateForm };
+};
+export default useAddTeamMember;
diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx
new file mode 100644
index 000000000..2df8fb226
--- /dev/null
+++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx
@@ -0,0 +1,213 @@
+import { Button, Stack } from "@mui/material";
+import { GenericDialog } from "../../../../Components/Dialog/genericDialog";
+import TextInput from "../../../../Components/Inputs/TextInput";
+import Select from "../../../../Components/Inputs/Select";
+import { useGetInviteToken } from "../../../../Hooks/inviteHooks";
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../../../Utils/toastUtils";
+import { useState } from "react";
+import PasswordTooltip from "../../../Auth/components/PasswordTooltip";
+import useAddTeamMember from "./hooks/useAddTeamMember";
+import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback";
+import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
+import PropTypes from "prop-types";
+
+const INITIAL_FORM_STATE = {
+ firstName: "",
+ lastName: "",
+ email: "",
+ password: "",
+ confirm: "",
+ teamId: "",
+};
+
+const INITIAL_ROLE_STATE = ["user"];
+const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { errors, setErrors, clearErrors, validateFields, validateForm } =
+ useAddTeamMember();
+ const { feedback, handlePasswordFeedback } = usePasswordFeedback();
+ const [getInviteToken, clearToken, isLoading, error, token, addTeamMember] =
+ useGetInviteToken();
+ const [form, setForm] = useState(INITIAL_FORM_STATE);
+ const [role, setRole] = useState(INITIAL_ROLE_STATE);
+ const [isLoadingSubmit, setIsLoadingSubmit] = useState(false);
+ const closeAddMemberModal = () => {
+ handleIsRegisterOpen(false);
+ setForm(INITIAL_FORM_STATE);
+ setRole(INITIAL_ROLE_STATE);
+ clearErrors();
+ clearToken();
+ };
+
+ const onChange = (e) => {
+ let { name, value } = e.target;
+ if (name === "email") value = value.toLowerCase();
+ const updatedForm = { ...form, [name]: value };
+ validateFields(name, value, updatedForm);
+ setForm(updatedForm);
+
+ if (name === "password" || name === "confirm") {
+ handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors);
+ }
+ };
+
+ const onsubmitAddMember = async (event) => {
+ event.preventDefault();
+ if (!validateForm(form, role)) return;
+ try {
+ setIsLoadingSubmit(true);
+ await addTeamMember(form, role);
+ createToast({
+ body: t("teamPanel.registerToast.success"),
+ });
+ onMemberAdded();
+ closeAddMemberModal();
+ } catch (error) {
+ const errorMsg = error.response?.data?.msg || error.message || "unknownError";
+ createToast({
+ type: "error",
+ body: t(errorMsg),
+ });
+ } finally {
+ setIsLoadingSubmit(false);
+ }
+ };
+ const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : "");
+ return (
+ <>
+
+
+
+
+
+
+
+
+ >
+ );
+};
+AddTeamMember.propTypes = {
+ handleIsRegisterOpen: PropTypes.func.isRequired,
+ isRegisterOpen: PropTypes.bool.isRequired,
+ onMemberAdded: PropTypes.func.isRequired,
+};
+
+export default AddTeamMember;
diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx
index 95ece219f..fc1768642 100644
--- a/client/src/Pages/Account/components/TeamPanel.jsx
+++ b/client/src/Pages/Account/components/TeamPanel.jsx
@@ -9,10 +9,12 @@ import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../../Components/Inputs/Select";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
+import AddTeamMember from "../components/AddTeamMember";
import DataTable from "../../../Components/Table";
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
import { useNavigate } from "react-router-dom";
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
+import AddMemberMenu from "./AddMemberMenu";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -65,7 +67,10 @@ const TeamPanel = () => {
render: (row) => row.role,
},
];
-
+ const [refreshTrigger, setRefreshTrigger] = useState(false);
+ const refreshTeamList = () => {
+ setRefreshTrigger((prev) => !prev);
+ };
useEffect(() => {
const fetchTeam = async () => {
try {
@@ -79,7 +84,7 @@ const TeamPanel = () => {
};
fetchTeam();
- }, []);
+ }, [refreshTrigger]);
useEffect(() => {
const ROLE_MAP = {
@@ -109,6 +114,10 @@ const TeamPanel = () => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
const [isOpen, setIsOpen] = useState(false);
+ const [isRegisterOpen, setIsRegisterOpen] = useState(false);
+ const handleIsRegisterOpen = (open) => {
+ setIsRegisterOpen(open);
+ };
const handleChange = (event) => {
const { value } = event.target;
@@ -138,7 +147,6 @@ const TeamPanel = () => {
const handleGetToken = async () => {
await getInviteToken({ email: toInvite.email, role: toInvite.role });
};
-
const handleInviteMember = async () => {
if (!toInvite.email) {
setErrors((prev) => ({ ...prev, email: "Email is required." }));
@@ -239,13 +247,16 @@ const TeamPanel = () => {
-
+
+
+ setIsOpen(true)}
+ handleIsRegisterOpen={handleIsRegisterOpen}
+ />
{
}}
/>
-
{
},
}}
>
- {children}
+ {children}
);
};
PasswordTooltip.propTypes = {
feedback: PropTypes.shape({
- length: PropTypes.string.isRequired,
+ length: PropTypes.string,
special: PropTypes.string,
number: PropTypes.string,
uppercase: PropTypes.string,
diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
new file mode 100644
index 000000000..4159e6166
--- /dev/null
+++ b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx
@@ -0,0 +1,71 @@
+import { useState } from "react";
+import { newOrChangedCredentials } from "../../../Validation/validation";
+
+const usePasswordFeedback = () => {
+ const [feedback, setFeedback] = useState({});
+ const getFeedbackStatus = (form, errors, field, criteria) => {
+ const fieldErrors = errors?.[field];
+ const isFieldEmpty = form?.[field]?.length === 0;
+ const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
+ const isCorrect = !isFieldEmpty && !hasError;
+
+ if (isCorrect) {
+ return "success";
+ } else if (hasError) {
+ return "error";
+ } else {
+ return "info";
+ }
+ };
+
+ const handlePasswordFeedback = (updatedForm, name, value, form, errors, setErrors) => {
+ const validateValue = { [name]: value };
+ const validateOptions = { abortEarly: false, context: { password: form.password } };
+ if (name === "password" && form.confirm.length > 0) {
+ validateValue.confirm = form.confirm;
+ validateOptions.context = { password: value };
+ } else if (name === "confirm") {
+ validateValue.password = form.password;
+ }
+ const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
+
+ const pwdErrors = error?.details.map((error) => ({
+ path: error.path[0],
+ type: error.type,
+ }));
+
+ const errorsByPath =
+ pwdErrors &&
+ pwdErrors.reduce((acc, { path, type }) => {
+ if (!acc[path]) {
+ acc[path] = [];
+ }
+ acc[path].push(type);
+ return acc;
+ }, {});
+
+ const oldErrors = { ...errors };
+ if (name === "password") {
+ oldErrors.password = undefined;
+ } else if (name === "confirm") {
+ oldErrors.confirm = undefined;
+ }
+ const newErrors = { ...oldErrors, ...errorsByPath };
+
+ setErrors(newErrors);
+
+ const newFeedback = {
+ length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
+ special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
+ number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
+ uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
+ lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
+ confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
+ };
+
+ setFeedback(newFeedback);
+ };
+ return { feedback, handlePasswordFeedback, getFeedbackStatus };
+};
+
+export default usePasswordFeedback;
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
index 1097cf1da..545974c57 100644
--- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
+++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
@@ -11,7 +11,12 @@ import PropTypes from "prop-types";
import { useSelector } from "react-redux";
-const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => {
+const MonitorsList = ({
+ isLoading = false,
+ shouldRender = true,
+ monitors = [],
+ statusPage = {},
+}) => {
const theme = useTheme();
const { determineState } = useMonitorUtils();
@@ -40,10 +45,12 @@ const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] })
alignItems="center"
gap={theme.spacing(20)}
>
-
-
-
-
+ {statusPage.showCharts !== false && (
+
+
+
+ )}
+
{
/>
{t("statusPageStatusServiceStatus")}
-
+
{link}
);
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 350d2f82d..7ddcd7e3c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -223,7 +223,7 @@ const Routes = () => {
}
+ element={}
/>
=18.18.0"
}
},
- "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": {
- "version": "0.3.1",
- "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz",
- "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==",
- "dev": true,
- "license": "Apache-2.0",
- "engines": {
- "node": ">=18.18"
- },
- "funding": {
- "type": "github",
- "url": "https://github.com/sponsors/nzakas"
- }
- },
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
@@ -1237,6 +1223,17 @@
"node": ">=14.16"
}
},
+ "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==",
+ "license": "ISC",
+ "optional": true,
+ "peer": true,
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
@@ -1273,6 +1270,12 @@
"undici-types": "~7.10.0"
}
},
+ "node_modules/@types/relateurl": {
+ "version": "0.2.33",
+ "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz",
+ "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==",
+ "license": "MIT"
+ },
"node_modules/@types/triple-beam": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz",
@@ -2502,12 +2505,14 @@
}
},
"node_modules/css-tree": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
- "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "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==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
- "mdn-data": "2.12.2",
+ "mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
@@ -3015,9 +3020,9 @@
"license": "MIT"
},
"node_modules/electron-to-chromium": {
- "version": "1.5.212",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.212.tgz",
- "integrity": "sha512-gE7ErIzSW+d8jALWMcOIgf+IB6lpfsg6NwOhPVwKzDtN2qcBix47vlin4yzSregYDxTCXOUqAZjVY/Z3naS7ww==",
+ "version": "1.5.213",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz",
+ "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==",
"license": "ISC"
},
"node_modules/emitter-component": {
@@ -4290,11 +4295,12 @@
"license": "MIT"
},
"node_modules/htmlnano": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.2.tgz",
- "integrity": "sha512-8Fst+0bhAfU362S6oHVb4wtJj/UYEFr0qiCLAEi8zioqmp1JYBQx5crZAADlFVX0Ly/6s/IQz6G7PL9/hgoJaQ==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.3.tgz",
+ "integrity": "sha512-mzTUHhxdfDw80X36rv5qFvjvor7r0uCwXI5mzo8CW61tImbe5Jpvl3JzPAetVh54wUYVuoa8x3qw8LFn8B3gHQ==",
"license": "MIT",
"dependencies": {
+ "@types/relateurl": "^0.2.33",
"cosmiconfig": "^9.0.0",
"posthtml": "^0.16.5"
},
@@ -5257,10 +5263,12 @@
}
},
"node_modules/mdn-data": {
- "version": "2.12.2",
- "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
- "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
- "license": "CC0-1.0"
+ "version": "2.0.30",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "license": "CC0-1.0",
+ "optional": true,
+ "peer": true
},
"node_modules/media-typer": {
"version": "0.3.0",
@@ -6198,13 +6206,14 @@
}
},
"node_modules/nise/node_modules/path-to-regexp": {
- "version": "8.2.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz",
- "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
"dev": true,
"license": "MIT",
- "engines": {
- "node": ">=16"
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
}
},
"node_modules/node-abort-controller": {
@@ -7136,6 +7145,59 @@
"postcss": "^8.4.32"
}
},
+ "node_modules/postcss-svgo/node_modules/commander": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
+ "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/css-tree": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz",
+ "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==",
+ "license": "MIT",
+ "dependencies": {
+ "mdn-data": "2.12.2",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "node_modules/postcss-svgo/node_modules/mdn-data": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz",
+ "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==",
+ "license": "CC0-1.0"
+ },
+ "node_modules/postcss-svgo/node_modules/svgo": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
+ "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
+ "license": "MIT",
+ "dependencies": {
+ "commander": "^11.1.0",
+ "css-select": "^5.1.0",
+ "css-tree": "^3.0.1",
+ "css-what": "^6.1.0",
+ "csso": "^5.0.5",
+ "picocolors": "^1.1.1",
+ "sax": "^1.4.1"
+ },
+ "bin": {
+ "svgo": "bin/svgo.js"
+ },
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/svgo"
+ }
+ },
"node_modules/postcss-unique-selectors": {
"version": "7.0.4",
"resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz",
@@ -8245,24 +8307,26 @@
}
},
"node_modules/svgo": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz",
- "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==",
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",
+ "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"dependencies": {
- "commander": "^11.1.0",
+ "@trysound/sax": "0.2.0",
+ "commander": "^7.2.0",
"css-select": "^5.1.0",
- "css-tree": "^3.0.1",
+ "css-tree": "^2.3.1",
"css-what": "^6.1.0",
"csso": "^5.0.5",
- "picocolors": "^1.1.1",
- "sax": "^1.4.1"
+ "picocolors": "^1.0.0"
},
"bin": {
- "svgo": "bin/svgo.js"
+ "svgo": "bin/svgo"
},
"engines": {
- "node": ">=16"
+ "node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
@@ -8270,18 +8334,20 @@
}
},
"node_modules/svgo/node_modules/commander": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz",
- "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==",
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz",
+ "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
"license": "MIT",
+ "optional": true,
+ "peer": true,
"engines": {
- "node": ">=16"
+ "node": ">= 10"
}
},
"node_modules/swagger-ui-dist": {
- "version": "5.28.0",
- "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.0.tgz",
- "integrity": "sha512-I9ibQtr77BPzT28WFWMVktzQOtWzoSS2J99L0Att8gDar1atl1YTRI7NUFSr4kj8VvWICgylanYHIoHjITc7iA==",
+ "version": "5.28.1",
+ "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz",
+ "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==",
"license": "Apache-2.0",
"dependencies": {
"@scarf/scarf": "=1.4.0"
diff --git a/server/src/db/mongo/modules/statusPageModule.js b/server/src/db/mongo/modules/statusPageModule.js
index 10710d0f7..aadde0c81 100755
--- a/server/src/db/mongo/modules/statusPageModule.js
+++ b/server/src/db/mongo/modules/statusPageModule.js
@@ -162,6 +162,14 @@ class StatusPageModule {
as: "monitors.checks",
},
},
+ {
+ $lookup: {
+ from: "monitorstats",
+ localField: "monitors._id",
+ foreignField: "monitorId",
+ as: "monitors.stats",
+ },
+ },
{
$addFields: {
"monitors.orderIndex": {
@@ -181,6 +189,9 @@ class StatusPageModule {
},
},
},
+ "monitors.uptimePercentage": {
+ $arrayElemAt: ["$monitors.stats.uptimePercentage", 0],
+ },
},
},
{ $match: { "monitors.orderIndex": { $ne: -1 } } },