Merge branch 'develop' of https://github.com/Owaiseimdad/Checkmate into develop

This commit is contained in:
Owaise Imdad committed 2025-07-27 03:47:49 +05:30
commit 729f107af2
198 files changed
+6942 -6232

No files matched your search

+172
View File
@@ -0,0 +1,172 @@
<p align=center> <a href="https://trendshift.io/repositories/12443" target="_blank"><img src="https://trendshift.io/api/badge/repositories/12443" alt="bluewave-labs%2Fcheckmate | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a></p>
![](https://img.shields.io/github/license/bluewave-labs/checkmate)
![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate)
![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate)
![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate)
![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues/bluewave-labs/checkmate)
![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate)
[![OpenSSF Mejores Prácticas](https://www.bestpractices.dev/projects/9901/badge)](https://www.bestpractices.dev/projects/9901)
[![Pregunta en DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate)
<h1 align="center"><a href="https://bluewavelabs.ca" target="_blank">Checkmate</a></h1>
<p align="center"><strong>Una aplicación de código abierto para monitoreo de infraestructura y tiempo de actividad</strong></p>
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
Este repositorio contiene tanto el frontend como el backend de Checkmate, una herramienta de monitoreo de código abierto y autoalojada para rastrear hardware de servidores, tiempo de actividad, tiempos de respuesta e incidentes en tiempo real con visualizaciones atractivas. Checkmate revisa regularmente si un servidor o sitio web es accesible y funciona de manera óptima, proporcionando alertas e informes en tiempo real sobre la disponibilidad, el tiempo de inactividad y el tiempo de respuesta de los servicios monitoreados.
Checkmate también tiene un agente llamado [Capture](https://github.com/bluewave-labs/capture), para recuperar datos de servidores remotos. Aunque Capture no es obligatorio para ejecutar Checkmate, proporciona información adicional sobre el estado de la CPU, RAM, disco y temperatura de tus servidores.
Checkmate ha sido probado con más de 1000 monitores activos sin problemas ni cuellos de botella de rendimiento.
**Si deseas patrocinar una función, [visita este enlace](https://checkmate.so/sponsored-features).**
## 📚 Tabla de contenidos
- [📦 Demo](#demo)
- [🔗 Guía del usuario](#guía-del-usuario)
- [🛠️ Instalación](#instalación)
- [🏁 Traducciones](#traducciones)
- [🚀 Rendimiento](#rendimiento)
- [💚 Preguntas e ideas](#preguntas-e-ideas)
- [🧩 Características](#características)
- [🏗️ Capturas de pantalla](#capturas-de-pantalla)
- [🏗️ Tecnologías](#tecnologías)
- [🔗 Enlaces útiles](#enlaces-útiles)
- [🤝 Contribuciones](#contribuciones)
- [💰 Patrocinadores](#patrocinadores)
...
**[Texto truncado para mantener la longitud del mensaje manejable]**
## Demo
Puedes ver la última versión de [Checkmate](https://checkmate-demo.bluewavelabs.ca/) en acción. El usuario es uptimedemo@demo.com y la contraseña es Demouser1! (ten en cuenta que actualizamos el servidor de demostración de vez en cuando, así que si no funciona para ti, por favor contáctanos en el canal de Discusiones).
## Guía del usuario
Las instrucciones de uso se pueden encontrar [aquí](https://docs.checkmate.so/checkmate-2.1). Todavía está en desarrollo y parte de la información puede estar desactualizada ya que continuamente añadimos funciones cada semana. ¡Ten por seguro que estamos haciendo lo mejor posible! :)
## Instalación
Consulta las instrucciones de instalación en el [portal de documentación de Checkmate](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternativamente, también puedes usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) o [Pikapods](https://www.pikapods.com/) para desplegar rápidamente una instancia de Checkmate. Si deseas monitorear tu infraestructura de servidores, necesitarás el agente [Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture también contiene las instrucciones de instalación.
## Traducciones
Si deseas usar Checkmate en tu idioma, por favor [ve a esta página](https://poeditor.com/join/project/lRUoGZFCsJ) y regístrate para el idioma al que te gustaría traducir Checkmate.
## Rendimiento
Gracias a extensas optimizaciones, Checkmate opera con un uso de memoria excepcionalmente bajo, requiriendo recursos mínimos de memoria y CPU. Aquí está el uso de memoria de una instancia de Node.js ejecutándose en un servidor que monitorea 323 servidores cada minuto:
![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9)
También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servidor (398Mb y 15Mb) para la misma cantidad de servidores:
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## Preguntas e Ideas
Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
## Características
- Completamente de código abierto, desplegable en tus propios servidores o dispositivos personales (por ejemplo, Raspberry Pi 4 o 5)
- Monitoreo de sitios web
- Monitoreo de velocidad de carga
- Monitoreo de infraestructura (memoria, uso de disco, rendimiento de CPU, etc) - requiere el agente [Capture](https://github.com/bluewave-labs/capture)
- Monitoreo de Docker
- Monitoreo con ping
- Monitoreo de certificados SSL
- Monitoreo de puertos
- Incidentes en una sola vista
- Páginas de estado
- Notificaciones por correo, Webhooks, Discord, Telegram, Slack
- Mantenimiento programado
- Monitoreo de consultas JSON
- Soporte para múltiples idiomas
**Hoja de ruta a corto plazo:** ([Milestone 2.2](https://github.com/bluewave-labs/Checkmate/milestone/8))
- Mejores notificaciones
- Monitoreo de red
- ...y algunas funciones más
## Capturas de pantalla
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
## Tecnologías
- [ReactJs](https://react.dev/)
- [MUI (framework de React)](https://mui.com/)
- [Node.js](https://nodejs.org/en)
- [MongoDB](https://mongodb.com)
- [Recharts](https://recharts.org)
- ¡Y muchos otros componentes de código abierto!
## Enlaces útiles
- Si deseas apoyarnos, por favor considera darle una ⭐ y haz clic en "watch".
- ¿Tienes una pregunta o sugerencia para la hoja de ruta? Revisa nuestro [canal de Discord](https://discord.gg/NAb6H3UTjK) o el foro de [Discusiones](https://github.com/bluewave-labs/checkmate/discussions).
- ¿Quieres saber cuándo hay una nueva versión? Usa [Newreleases](https://newreleases.io/), un servicio gratuito para seguir lanzamientos.
- Mira un video de instalación y uso de Checkmate [aquí](https://www.youtube.com/watch?v=GfFOc0xHIwY)
## Contribuciones
Somos [Alex](http://github.com/ajhollid) (líder de equipo), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) y [Mert](https://github.com/mertssmnoglu), ayudando a personas y empresas a monitorear su infraestructura y servidores.
Nos enorgullecemos de construir conexiones fuertes con contribuyentes de todos los niveles. A pesar de ser un proyecto joven, Checkmate ya ha ganado más de 7000 estrellas y ha atraído a más de 90 contribuyentes de todo el mundo.
Nuestro repositorio ha sido marcado con estrella por empleados de **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes y NEC**, ¡así que no te detengas — participa, contribuye y aprende con nosotros!
Cómo contribuir:
0. Dale una estrella al repositorio :)
1. Revisa la [guía para contribuidores](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Se anima a los nuevos a revisar las etiquetas `good-first-issue`.
2. Consulta la [estructura del proyecto](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) y la [visión general](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview).
3. Lee una estructura detallada de [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) si deseas profundizar en la arquitectura.
4. Abre un issue si crees que has encontrado un error.
5. Revisa los issues con la etiqueta `good-first-issue` si eres nuevo.
6. Haz un pull request para añadir nuevas funciones, mejoras o correcciones.
<a href="https://github.com/bluewave-labs/checkmate/graphs/contributors">
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Historial de estrellas](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
## Patrocinadores
Gracias a [Gitbook](https://gitbook.io/) por darnos una cuenta gratuita para su plataforma de documentación, y a [Poeditor](https://poeditor.com/) por proporcionarnos una cuenta gratuita para servicios de traducción. Si deseas patrocinar Checkmate, por favor envía un correo a hello@bluewavelabs.ca
Si deseas patrocinar una función, [visita esta página](https://checkmate.so/sponsored-features).
También puedes revisar otros proyectos de BlueWave orientados a desarrolladores y contribuidores:
- [VerifyWise](https://github.com/bluewave-labs/verifywise), la primera plataforma de gobernanza de IA de código abierto.
- [DataRoom](https://github.com/bluewave-labs/bluewave-dataroom), una aplicación de intercambio de archivos seguro, también conocida como dataroom.
- [Guidefox](https://github.com/bluewave-labs/guidefox), una app que ayuda a nuevos usuarios a aprender a usar tu producto mediante pistas, tours, popups y banners.
+31 -26
View File
@@ -26,41 +26,40 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
## 📚 Table of contents
- [📦 Demo](#-demo)
- [🔗 User's guide](#-users-guide)
- [🛠️ Installation](#️-installation)
- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm)
- [🏁 Translations](#-translations)
- [🚀 Performance](#-performance)
- [💚 Questions & Ideas](#-questions--ideas)
- [🧩 Features](#-features)
- [🏗️ Screenshots](#-screenshots)
- [🏗️ Tech stack](#-tech-stack)
- [📦 Demo](#demo)
- [🔗 User's guide](#users-guide)
- [🛠️ Installation](#installation)
- [🏁 Translations](#translations)
- [🚀 Performance](#performance)
- [💚 Questions & Ideas](#questions--ideas)
- [🧩 Features](#features)
- [🏗️ Screenshots](#screenshots)
- [🏗️ Tech stack](#tech-stack)
- [🔗 A few links](#a-few-links)
- [🤝 Contributing](#-contributing)
- [💰 Our sponsors](#-our-sponsors)
- [🤝 Contributing](#contributing)
- [💰 Our sponsors](#our-sponsors)
## 📦 Demo
## Demo
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is uptimedemo@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
## 🔗 User's guide
## User's guide
Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1). It's still WIP and some of the information there might be outdated as we continuously add features weekly. Rest assured, we are doing our best! :)
## 🛠️ Installation
## Installation
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
## 🏁 Translations
## Translations
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
## 🚀 Performance
## Performance
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
@@ -70,16 +69,22 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751)
## 💚 Questions & Ideas
## Questions & Ideas
If you have any questions, suggestions or comments, please use our [Discord channel](https://discord.gg/NAb6H3UTjK). We've also launched our [Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) page! Feel free to ask questions or share your ideas—we'd love to hear from you!
If you have any questions, suggestions or comments, you have several options:
## 🧩 Features
- [Discord channel](https://discord.gg/NAb6H3UTjK)
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions)
- [Reddit group](https://www.reddit.com/r/CheckmateMonitoring/)
Feel free to ask questions or share your ideas - we'd love to hear from you!
## Features
- Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5)
- Website monitoring
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture)
- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
- Docker monitoring
- Ping monitoring
- SSL monitoring
@@ -97,7 +102,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Network monitoring
- ..and a few more features
## 🏗️ Screenshots
## Screenshots
<p>
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
@@ -114,7 +119,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🏗️ Tech stack
## Tech stack
- [ReactJs](https://react.dev/)
- [MUI (React framework)](https://mui.com/)
@@ -130,11 +135,11 @@ If you have any questions, suggestions or comments, please use our [Discord chan
- Need a ping when there's a new release? Use [Newreleases](https://newreleases.io/), a free service to track releases.
- Watch a Checkmate [installation and usage video](https://www.youtube.com/watch?v=GfFOc0xHIwY)
## 🤝 Contributing
## Contributing
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe.
We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 7000+ stars and attracted 90+ contributors from around the globe.
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
@@ -154,7 +159,7 @@ Here's how you can contribute:
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
## 💰 Our sponsors
## Our sponsors
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
+1 -1
View File
@@ -14,6 +14,6 @@ metadata:
name: checkmate-secrets
type: Opaque
stringData:
{{- range $key, $value := := $secrets }}
{{- range $key, $value := $secrets }}
{{ $key }}: {{ $value | quote }}
{{- end }}
@@ -20,11 +20,3 @@ spec:
envFrom:
- secretRef:
name: checkmate-secrets
volumeMounts:
- name: docker-sock
mountPath: /var/run/docker.sock
volumes:
- name: docker-sock
hostPath:
path: /var/run/docker.sock
type: Socket
@@ -5,6 +5,11 @@ metadata:
name: server-ingress
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
nginx.ingress.kubernetes.io/enable-cors: "true"
nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}"
nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS"
nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization"
nginx.ingress.kubernetes.io/cors-allow-credentials: "true"
spec:
rules:
- host: {{ .Values.server.ingress.host }}
@@ -1,8 +1,10 @@
import { useTheme } from "@emotion/react";
import { useEffect, useState, useMemo } from "react";
import Stack from "@mui/material/Stack";
import { Box, Typography } from "@mui/material";
import PropTypes from "prop-types";
import "./index.css";
import CircularProgress from "@mui/material/CircularProgress";
const MINIMUM_VALUE = 0;
const MAXIMUM_VALUE = 100;
@@ -16,6 +18,8 @@ const MAXIMUM_VALUE = 100;
* @param {number} [props.radius=60] - Radius of the gauge circle
* @param {number} [props.strokeWidth=15] - Width of the gauge stroke
* @param {number} [props.threshold=50] - Threshold for color change
* @param {number} [props.precision=1] - Precision of the progress percentage
* @param {string} [props.unit="%"] - Unit of progress
*
* @example
* <CustomGauge
@@ -27,7 +31,15 @@ const MAXIMUM_VALUE = 100;
*
* @returns {React.ReactElement} Rendered CustomGauge component
*/
const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = 50 }) => {
const CustomGauge = ({
isLoading = false,
progress = 0,
radius = 70,
strokeWidth = 15,
threshold = 50,
precision = 1,
unit = "%",
}) => {
const theme = useTheme();
// Calculate the length of the stroke for the circle
const { circumference, totalSize, strokeLength } = useMemo(
@@ -54,8 +66,22 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
const fillColor =
progressWithinRange > threshold
? theme.palette.error.lowContrast // CAIO_REVIEW
: theme.palette.accent.main; // CAIO_REVIEW
? theme.palette.error.lowContrast
: theme.palette.accent.main;
if (isLoading) {
return (
<Stack
className="radial-chart"
width={radius}
height={radius}
alignItems="center"
justifyContent="center"
>
<CircularProgress color="accent" />
</Stack>
);
}
return (
<Box
@@ -71,7 +97,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
>
<circle
className="radial-chart-base"
stroke={theme.palette.secondary.light} // CAIO_REVIEW
stroke={theme.palette.secondary.light}
strokeWidth={strokeWidth}
fill="none"
cx={totalSize / 2} // Center the circle
@@ -103,7 +129,7 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
fill: theme.typography.h2.color,
}}
>
{`${progressWithinRange.toFixed(1)}%`}
{`${progressWithinRange.toFixed(precision)}${unit}`}
</Typography>
</Box>
);
@@ -112,8 +138,11 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold =
export default CustomGauge;
CustomGauge.propTypes = {
isLoading: PropTypes.bool,
progress: PropTypes.number,
radius: PropTypes.number,
strokeWidth: PropTypes.number,
threshold: PropTypes.number,
precision: PropTypes.number,
unit: PropTypes.string,
};
+3 -4
View File
@@ -4,15 +4,14 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
display: "flex",
flexDirection: "row",
justifyContent: "space-between",
gap: theme.spacing(20),
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.spacing(2),
"& > *": {
paddingTop: theme.spacing(12),
paddingBottom: theme.spacing(18),
paddingTop: theme.spacing(15),
paddingBottom: theme.spacing(15),
},
"& > div:first-of-type": {
flex: 0.7,
@@ -25,7 +24,7 @@ const ConfigBox = styled(Stack)(({ theme }) => ({
"& > div:last-of-type": {
flex: 1,
paddingRight: theme.spacing(20),
paddingLeft: theme.spacing(18),
paddingLeft: theme.spacing(20),
},
"& h1, & h2": {
color: theme.palette.primary.contrastTextSecondary,
@@ -0,0 +1,43 @@
import { Button, Stack } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import PropTypes from "prop-types";
const FallbackActionButtons = ({ link, type }) => {
const theme = useTheme();
const navigate = useNavigate();
const { t } = useTranslation();
return (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<Button
variant="contained"
color="accent"
onClick={() => navigate(link)}
>
{t(`${type}.fallback.actionButton`)}
</Button>
{type === "uptimeMonitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
</Stack>
);
};
FallbackActionButtons.propTypes = {
title: PropTypes.string.isRequired,
link: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackActionButtons;
@@ -0,0 +1,31 @@
import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box";
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
import { useSelector } from "react-redux";
const FallbackBackground = () => {
const theme = useTheme();
const mode = useSelector((state) => state.ui.mode);
return (
<>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
</>
);
};
export default FallbackBackground;
@@ -0,0 +1,35 @@
import PropTypes from "prop-types";
import { useTheme } from "@mui/material/styles";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Check from "../Check/Check";
const FallbackCheckList = ({ checks, type }) => {
const theme = useTheme();
return (
<Box
sx={{
display: "flex",
flexWrap: "wrap",
gap: theme.spacing(2),
alignItems: "flex-start",
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{checks?.map((check, index) => (
<Check
text={check}
key={`${type.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Box>
);
};
FallbackCheckList.propTypes = {
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
};
export default FallbackCheckList;
@@ -0,0 +1,44 @@
import { useTheme } from "@emotion/react";
import { Box, Stack } from "@mui/material";
import PropTypes from "prop-types";
const FallbackContainer = ({ children, type }) => {
const theme = useTheme();
return (
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: "fit-content",
minHeight: "60vh",
width: {
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
},
padding: `${theme.spacing(20)} ${theme.spacing(10)}`,
}}
>
<Stack
className={`fallback__${type?.trim().split(" ")[0]}`}
alignItems="center"
gap={theme.spacing(20)}
>
{children}
</Stack>
</Box>
);
};
FallbackContainer.propTypes = {
children: PropTypes.node,
type: PropTypes.string,
};
export default FallbackContainer;
@@ -0,0 +1,69 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import Link from "@mui/material/Link";
import { Link as RouterLink } from "react-router-dom";
import { useTranslation } from "react-i18next";
import Alert from "../Alert";
import PropTypes from "prop-types";
const renderWarningMessage = (t) => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
const FallbackPageSpeedWarning = ({ settingsData }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage(t)}
/>
)}
</Box>
</Box>
);
};
FallbackPageSpeedWarning.propTypes = {
settingsData: PropTypes.shape({
pagespeedKeySet: PropTypes.bool,
}),
};
export default FallbackPageSpeedWarning;
@@ -0,0 +1,21 @@
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
import PropTypes from "prop-types";
const FallbackTitle = ({ title }) => {
const theme = useTheme();
return (
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
);
};
FallbackTitle.propTypes = {
title: PropTypes.string.isRequired,
};
export default FallbackTitle;
-4
View File
@@ -38,7 +38,3 @@
background-size: cover;
background-repeat: no-repeat;
}
.fallback__status > .MuiStack-root {
margin-left: var(--env-var-spacing-2);
}
+34 -174
View File
@@ -1,72 +1,26 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, Stack, Typography, Link } from "@mui/material";
import { Link as RouterLink } from "react-router-dom";
import Skeleton from "../../assets/Images/create-placeholder.svg?react";
import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import Check from "../Check/Check";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import Alert from "../Alert";
import { useTranslation } from "react-i18next";
import { Box, Stack } from "@mui/material";
import "./index.css";
import { useFetchSettings } from "../../Hooks/settingsHooks";
import { useState } from "react";
import FallbackTitle from "./FallbackTitle";
import FallbackCheckList from "./FallbackCheckList";
import FallbackActionButtons from "./FallBackActionButtons";
import FallbackBackground from "./FallbackBackground";
import FallbackContainer from "./FallbackContainer";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
*
* @param {Object} props - The component props.
* @param {string} props.title - The title to be displayed in the fallback UI.
* @param {string} props.type - The type of the fallback (e.g., "pageSpeed", "notifications").
* @param {Array<string>} props.checks - An array of strings representing the checks to display.
* @param {string} [props.link="/"] - The link to navigate to.
* @param {boolean} [props.vowelStart=false] - Whether the title starts with a vowel.
* @param {boolean} [props.showPageSpeedWarning=false] - Whether to show the PageSpeed API warning.
* @returns {JSX.Element} The rendered fallback UI.
*/
const Fallback = ({
title,
checks,
link = "/",
isAdmin,
vowelStart = false,
showPageSpeedWarning = false,
}) => {
const Fallback = ({ title, checks, link = "/", isAdmin, type, children }) => {
const theme = useTheme();
const navigate = useNavigate();
const mode = useSelector((state) => state.ui.mode);
const { t } = useTranslation();
const [settingsData, setSettingsData] = useState(undefined);
const [isLoading, error] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
// Custom warning message with clickable link
const renderWarningMessage = () => {
return (
<>
{t("pageSpeedWarning")}{" "}
<Link
component={RouterLink}
to="/settings"
sx={{
textDecoration: "underline",
color: "inherit",
fontWeight: "inherit",
"&:hover": {
textDecoration: "underline",
},
}}
>
{t("pageSpeedLearnMoreLink")}
</Link>{" "}
{t("pageSpeedAddApiKey")}
</>
);
};
return (
<Box
position="relative"
@@ -78,138 +32,44 @@ const Fallback = ({
alignItems: "center",
}}
>
<Box
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
backgroundColor={theme.palette.primary.main}
overflow="hidden"
sx={{
display: "flex",
borderStyle: "dashed",
height: {
sm: "55vh",
md: "50vh",
lg: "70vh",
xl: "58vh",
},
width: {
sm: "90%",
md: "70%",
lg: "42%",
},
minHeight: {
sm: theme.spacing(280),
},
padding: theme.spacing(10),
}}
>
<FallbackContainer type={type}>
<FallbackBackground />
<Stack
className={`fallback__${title?.trim().split(" ")[0]}`}
gap={theme.spacing(5)}
zIndex={1}
alignItems="center"
gap={theme.spacing(20)}
>
{mode === "light" ? (
<Skeleton style={{ zIndex: 1 }} />
) : (
<SkeletonDark style={{ zIndex: 1 }} />
)}
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
gap={theme.spacing(4)}
maxWidth={theme.spacing(180)}
zIndex={1}
>
<Typography
alignSelf="center"
component="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{vowelStart ? "An" : "A"} {title} is used to:
</Typography>
{checks?.map((check, index) => (
<Check
text={check}
key={`${title.trim().split(" ")[0]}-${index}`}
outlined={true}
/>
))}
</Stack>
{/* TODO - display a different fallback if user is not an admin*/}
{isAdmin && (
<Stack gap={theme.spacing(10)}>
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center", mb: "2px" }}
onClick={() => navigate(link)}
>
Let's create your first {title}
</Button>
{/* Bulk create of uptime monitors */}
{title === "uptime monitor" && (
<Button
variant="contained"
color="accent"
sx={{ alignSelf: "center" }}
onClick={() => navigate("/uptime/bulk-import")}
>
{t("bulkImport.fallbackPage")}
</Button>
)}
{/* Warning box for PageSpeed monitor */}
{title === "pagespeed monitor" && showPageSpeedWarning && (
<Box sx={{ width: "80%", maxWidth: "600px", zIndex: 1 }}>
<Box
sx={{
"& .alert.row-stack": {
backgroundColor: theme.palette.warningSecondary.main,
borderColor: theme.palette.warningSecondary.lowContrast,
"& .MuiTypography-root": {
color: theme.palette.warningSecondary.contrastText,
},
"& .MuiBox-root > svg": {
color: theme.palette.warningSecondary.contrastText,
},
},
}}
>
{settingsData?.pagespeedKeySet === false && (
<Alert
variant="warning"
hasIcon={true}
body={renderWarningMessage()}
/>
)}
</Box>
</Box>
)}
</Stack>
)}
<FallbackTitle title={title} />
<FallbackCheckList
checks={checks}
title={title}
type={type}
/>
</Stack>
</Box>
{isAdmin && (
<Stack
gap={theme.spacing(10)}
alignItems="center"
>
<FallbackActionButtons
title={title}
link={link}
type={type}
/>
{children}
</Stack>
)}
</FallbackContainer>
</Box>
);
};
Fallback.propTypes = {
title: PropTypes.string.isRequired,
checks: PropTypes.arrayOf(PropTypes.string).isRequired,
link: PropTypes.string,
isAdmin: PropTypes.bool,
vowelStart: PropTypes.bool,
showPageSpeedWarning: PropTypes.bool,
type: PropTypes.string.isRequired,
children: PropTypes.node,
};
export default Fallback;
@@ -0,0 +1,51 @@
import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
const DEFAULT_GAP = 6;
const FieldWrapper = ({
label,
children,
gap,
labelMb,
labelFontWeight = 500,
labelVariant = "h3",
labelSx = {},
sx = {},
}) => {
const theme = useTheme();
return (
<Stack
gap={gap ?? theme.spacing(DEFAULT_GAP)}
sx={sx}
>
{label && (
<Typography
component={labelVariant}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={labelFontWeight}
sx={{
...(labelMb !== undefined && { mb: theme.spacing(labelMb) }),
...labelSx,
}}
>
{label}
</Typography>
)}
{children}
</Stack>
);
};
FieldWrapper.propTypes = {
label: PropTypes.node,
children: PropTypes.node.isRequired,
gap: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]),
labelMb: PropTypes.number,
labelFontWeight: PropTypes.number,
labelVariant: PropTypes.string,
labelSx: PropTypes.object,
sx: PropTypes.object,
};
export default FieldWrapper;
+19 -2
View File
@@ -24,7 +24,17 @@ import "./index.css";
* @returns {JSX.Element} - The rendered Radio component.
*/
const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
const Radio = ({
name,
checked,
value,
id,
size,
title,
desc,
onChange,
labelSpacing,
}) => {
const theme = useTheme();
return (
@@ -53,7 +63,14 @@ const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
onChange={onChange}
label={
<>
<Typography component="p">{title}</Typography>
<Typography
component="p"
mb={
labelSpacing !== undefined ? theme.spacing(labelSpacing) : theme.spacing(2)
}
>
{title}
</Typography>
<Typography
component="h6"
mt={theme.spacing(1)}
+31 -13
View File
@@ -12,6 +12,7 @@ import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import FieldWrapper from "../FieldWrapper";
/**
* Search component using Material UI's Autocomplete.
@@ -24,6 +25,7 @@ import { useTranslation } from "react-i18next";
* @param {Function} props.handleChange - Function to call when the input changes
* @param {Function} Prop.onBlur - Function to call when the input is blured
* @param {Object} props.sx - Additional styles to apply to the component
* @param {string} props.unit - Label to identify type of options
* @returns {JSX.Element} The rendered Search component
*/
@@ -49,7 +51,7 @@ const SearchAdornment = () => {
);
};
//TODO keep search state inside of component
//TODO keep search state inside of component.
const Search = ({
label,
id,
@@ -68,6 +70,14 @@ const Search = ({
startAdornment,
endAdornment,
onBlur,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
unit = "option",
maxWidth = "100%",
}) => {
const theme = useTheme();
const { t } = useTranslation();
@@ -139,15 +149,17 @@ const Search = ({
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
renderInput={(params) => (
<Stack>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
>
{label}
</Typography>
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
>
<TextField
{...params}
error={Boolean(error)}
@@ -175,7 +187,7 @@ const Search = ({
{error}
</Typography>
)}
</Stack>
</FieldWrapper>
)}
filterOptions={(options, { inputValue }) => {
if (inputValue.trim() === "" && multiple && isAdorned) {
@@ -186,7 +198,12 @@ const Search = ({
);
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
return [
{
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
noOptions: true,
},
];
}
return filtered;
}}
@@ -281,7 +298,7 @@ Search.propTypes = {
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
value: PropTypes.array,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
@@ -292,6 +309,7 @@ Search.propTypes = {
startAdornment: PropTypes.object,
endAdornment: PropTypes.object,
onBlur: PropTypes.func,
unit: PropTypes.string,
};
export default Search;
+26 -15
View File
@@ -2,6 +2,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import FieldWrapper from "../FieldWrapper";
import "./index.css";
@@ -50,8 +51,14 @@ const Select = ({
onBlur,
sx,
name = "",
labelControlSpacing = 2,
labelControlSpacing = 6,
maxWidth,
//FieldWrapper's props
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
fieldWrapperSx = {},
}) => {
const theme = useTheme();
const itemStyles = {
@@ -69,20 +76,17 @@ const Select = ({
};
return (
<Stack
gap={theme.spacing(labelControlSpacing)}
className="select-wrapper"
<FieldWrapper
label={label}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={labelControlSpacing}
sx={{
...fieldWrapperSx,
}}
>
{label && (
<Typography
component="h3"
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
fontSize={13}
>
{label}
</Typography>
)}
<MuiSelect
className="select-component"
value={value}
@@ -107,6 +111,13 @@ const Select = ({
"& svg path": {
fill: theme.palette.primary.contrastTextTertiary,
},
"& .MuiSelect-select": {
padding: "0 13px",
minHeight: "34px",
display: "flex",
alignItems: "center",
lineHeight: 1,
},
...sx,
}}
renderValue={(selected) => {
@@ -151,7 +162,7 @@ const Select = ({
</MenuItem>
))}
</MuiSelect>
</Stack>
</FieldWrapper>
);
};
@@ -2,6 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { forwardRef, useState, cloneElement } from "react";
import PropTypes from "prop-types";
import FieldWrapper from "../FieldWrapper";
const getSx = (theme, type, maxWidth) => {
const sx = {
@@ -85,31 +86,43 @@ const TextInput = forwardRef(
marginLeft,
disabled = false,
hidden = false,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
sx = {},
},
ref
) => {
const [fieldType, setFieldType] = useState(type);
const theme = useTheme();
const labelContent = label && (
<>
{label}
{isRequired && <Required />}
{isOptional && <Optional optionalLabel={optionalLabel} />}
</>
);
return (
<Stack
flex={flex}
display={hidden ? "none" : ""}
marginTop={marginTop}
marginRight={marginRight}
marginBottom={marginBottom}
marginLeft={marginLeft}
<FieldWrapper
label={labelContent}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
flex,
display: hidden ? "none" : "",
mt: marginTop,
mr: marginRight,
mb: marginBottom,
ml: marginLeft,
...sx,
}}
>
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
color={theme.palette.primary.contrastTextSecondary}
fontWeight={500}
mb={theme.spacing(2)}
>
{label}
{isRequired && <Required />}
{isOptional && <Optional optionalLabel={optionalLabel} />}
</Typography>
<TextField
id={id}
name={name}
@@ -132,7 +145,7 @@ const TextInput = forwardRef(
}}
disabled={disabled}
/>
</Stack>
</FieldWrapper>
);
}
);
+9 -4
View File
@@ -6,6 +6,13 @@ import { useSelector } from "react-redux";
import { useDispatch } from "react-redux";
import { setLanguage } from "../Features/UI/uiSlice";
const langMap = {
cs: "cz",
ja: "jp",
uk: "ua",
vi: "vn",
};
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
@@ -28,14 +35,12 @@ const LanguageSelector = () => {
{languages.map((lang) => {
let parsedLang = lang === "en" ? "gb" : lang;
// Fix for Czech
if (parsedLang === "cs") {
parsedLang = "cz";
}
if (parsedLang.includes("-")) {
parsedLang = parsedLang.split("-")[1].toLowerCase();
}
parsedLang = langMap[parsedLang] || parsedLang;
const flag = parsedLang ? `fi fi-${parsedLang}` : null;
return (
@@ -10,7 +10,18 @@ import { useState, useEffect } from "react";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
const NotificationConfig = ({
notifications,
setMonitor,
setNotifications,
//FieldWrapper's props
gap,
labelMb,
labelFontWeight,
labelVariant,
labelSx = {},
sx = {},
}) => {
// Local state
const [notificationsSearch, setNotificationsSearch] = useState("");
const [selectedNotifications, setSelectedNotifications] = useState([]);
@@ -66,6 +77,14 @@ const NotificationConfig = ({ notifications, setMonitor, setNotifications }) =>
handleChange={(value) => {
handleSearch(value);
}}
labelMb={labelMb}
labelVariant={labelVariant}
labelFontWeight={labelFontWeight}
labelSx={labelSx}
gap={gap}
sx={{
...sx,
}}
/>
<Stack
flex={1}
+1
View File
@@ -16,6 +16,7 @@ const CustomTabList = ({ value, onChange, children, ...props }) => {
return (
<Box
sx={{
marginBottom: theme.spacing(12),
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
"& .MuiTabs-root": { height: "fit-content", minHeight: "0" },
}}
+34
View File
@@ -0,0 +1,34 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const ToastBody = ({ body }) => {
const theme = useTheme();
if (Array.isArray(body)) {
return (
<Stack gap={theme.spacing(2)}>
{body.map((item, idx) => (
<Typography
key={`item-${idx}`}
color={theme.palette.secondary.contrastText}
>
{item}
</Typography>
))}
</Stack>
);
} else if (typeof body === "string") {
return <Typography color={theme.palette.secondary.contrastText}>{body}</Typography>;
}
return null;
};
ToastBody.propTypes = {
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
};
export default ToastBody;
+96
View File
@@ -0,0 +1,96 @@
import Stack from "@mui/material/Stack";
import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ToastBody from "./body";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const icons = {
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
};
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
const theme = useTheme();
const icon = icons[variant];
return (
<Stack
gap={theme.spacing(2)}
paddingTop={theme.spacing(4)}
paddingRight={theme.spacing(8)}
paddingBottom={theme.spacing(4)}
paddingLeft={theme.spacing(8)}
backgroundColor={theme.palette.alert.main}
border={`solid 1px ${theme.palette.alert.contrastText}`}
borderRadius={theme.shape.borderRadius}
>
<Stack
direction="row"
gap={theme.spacing(8)}
justifyContent="space-between"
alignItems="center"
>
{hasIcon && icon}
{title && (
<Typography
fontWeight="700"
color={theme.palette.secondary.contrastText}
>
{title}
</Typography>
)}
{title && (
<IconButton onClick={onClick}>
<CloseIcon />
</IconButton>
)}
</Stack>
<Stack
direction="row"
gap={theme.spacing(2)}
alignItems="center"
>
<ToastBody body={body} />
{!title && (
<IconButton onClick={onClick}>
<CloseIcon />
</IconButton>
)}
</Stack>
{hasDismiss && (
<Button
variant="text"
color="info"
onClick={onClick}
sx={{
fontWeight: "600",
width: "fit-content",
}}
>
Dismiss
</Button>
)}
</Stack>
);
};
export default Toast;
Toast.propTypes = {
variant: PropTypes.string.isRequired,
title: PropTypes.string,
body: PropTypes.oneOfType([PropTypes.string, PropTypes.array]),
hasDismiss: PropTypes.bool,
hasIcon: PropTypes.bool,
onClick: PropTypes.func,
};
+1 -5
View File
@@ -44,7 +44,6 @@ export const login = createAsyncThunk("auth/login", async (form, thunkApi) => {
export const update = createAsyncThunk("auth/update", async (data, thunkApi) => {
const { localData: form } = data;
const user = thunkApi.getState().auth.user;
try {
const fd = new FormData();
form.firstName && fd.append("firstName", form.firstName);
@@ -60,7 +59,6 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
form.deleteProfileImage && fd.append("deleteProfileImage", form.deleteProfileImage);
const res = await networkService.updateUser({
userId: user._id,
form: fd,
});
return res.data;
@@ -77,10 +75,8 @@ export const update = createAsyncThunk("auth/update", async (data, thunkApi) =>
});
export const deleteUser = createAsyncThunk("auth/delete", async (_, thunkApi) => {
const user = thunkApi.getState().auth.user;
try {
const res = await networkService.deleteUser({ userId: user._id });
const res = await networkService.deleteUser();
return res.data;
} catch (error) {
if (error.response && error.response.data) {
+14 -7
View File
@@ -185,23 +185,30 @@ const useResolveIncident = () => {
return [resolveIncident, isLoading];
};
const useAckAllChecks = () => {
const useAcknowledgeChecks = () => {
const [isLoading, setIsLoading] = useState(false);
const { t } = useTranslation();
const ackAllChecks = async (setUpdateTrigger) => {
const acknowledge = async (setUpdateTrigger, monitorId = null) => {
setIsLoading(true);
try {
setIsLoading(true);
await networkService.updateAllChecksStatus({ ack: true });
if (monitorId) {
await networkService.updateMonitorChecksStatus({ monitorId, ack: true });
} else {
await networkService.updateAllChecksStatus({ ack: true });
}
setUpdateTrigger((prev) => !prev);
} catch (error) {
createToast({ body: t("checkHooks.failureResolveAll") });
const toastMessage = monitorId
? t("checkHooks.failureResolveMonitor")
: t("checkHooks.failureResolveAll");
createToast({ body: toastMessage });
} finally {
setIsLoading(false);
}
};
return [ackAllChecks, isLoading];
return { acknowledge, isLoading };
};
export {
@@ -209,5 +216,5 @@ export {
useFetchChecksTeam,
useFetchChecksSummaryByTeamId,
useResolveIncident,
useAckAllChecks,
useAcknowledgeChecks,
};
+80 -1
View File
@@ -32,4 +32,83 @@ const useFetchLogs = () => {
return [logs, isLoading, error];
};
export { useFetchLogs };
const useFetchQueueData = (trigger) => {
const [jobs, setJobs] = useState(undefined);
const [metrics, setMetrics] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchJobs = async () => {
try {
setIsLoading(true);
const response = await networkService.getQueueData();
if (response.status === 200) {
setJobs(response.data.data.jobs);
setMetrics(response.data.data.metrics);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchJobs();
}, [trigger]);
return [jobs, metrics, isLoading, error];
};
const useFlushQueue = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const flushQueue = async (trigger, setTrigger) => {
try {
setIsLoading(true);
await networkService.flushQueue();
createToast({
body: "Queue flushed",
});
} catch (error) {
setError(error);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setTrigger(!trigger);
}
};
return [flushQueue, isLoading, error];
};
const useFetchDiagnostics = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const [diagnostics, setDiagnostics] = useState(undefined);
const fetchDiagnostics = async () => {
try {
setIsLoading(true);
const response = await networkService.getDiagnostics();
setDiagnostics(response.data.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchDiagnostics();
}, []);
return [diagnostics, fetchDiagnostics, isLoading, error];
};
export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics };
-57
View File
@@ -1,57 +0,0 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
const useFetchQueueData = (trigger) => {
const [jobs, setJobs] = useState(undefined);
const [metrics, setMetrics] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchJobs = async () => {
try {
setIsLoading(true);
const response = await networkService.getQueueData();
if (response.status === 200) {
setJobs(response.data.data.jobs);
setMetrics(response.data.data.metrics);
}
} catch (error) {
setError(error);
} finally {
setIsLoading(false);
}
};
fetchJobs();
}, [trigger]);
return [jobs, metrics, isLoading, error];
};
const useFlushQueue = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const flushQueue = async (trigger, setTrigger) => {
try {
setIsLoading(true);
await networkService.flushQueue();
createToast({
body: "Queue flushed",
});
} catch (error) {
setError(error);
createToast({
body: error.message,
});
} finally {
setIsLoading(false);
setTrigger(!trigger);
}
};
return [flushQueue, isLoading, error];
};
export { useFetchQueueData, useFlushQueue };
+7 -1
View File
@@ -8,4 +8,10 @@ const useIsAdmin = () => {
return isAdmin;
};
export { useIsAdmin };
const useIsSuperAdmin = () => {
const { user } = useSelector((state) => state.auth);
const isSuperAdmin = user?.role?.includes("superadmin");
return isSuperAdmin;
};
export { useIsAdmin, useIsSuperAdmin };
+7 -7
View File
@@ -6,18 +6,18 @@ const useMonitorUtils = () => {
let uptimePercentage = "";
let percentageColor = "";
if (monitor.uptimePercentage !== undefined) {
if (monitor?.uptimePercentage !== undefined) {
uptimePercentage =
monitor.uptimePercentage === 0
monitor?.uptimePercentage === 0
? "0"
: (monitor.uptimePercentage * 100).toFixed(2);
: (monitor?.uptimePercentage * 100).toFixed(2);
percentageColor =
monitor.uptimePercentage < 0.25
monitor?.uptimePercentage < 0.25
? theme.palette.error.main
: monitor.uptimePercentage < 0.5
: monitor?.uptimePercentage < 0.5
? theme.palette.warning.main
: monitor.uptimePercentage < 0.75
: monitor?.uptimePercentage < 0.75
? theme.palette.success.main
: theme.palette.success.main;
}
@@ -32,7 +32,7 @@ const useMonitorUtils = () => {
const determineState = useCallback((monitor) => {
if (typeof monitor === "undefined") return "pending";
if (monitor.isActive === false) return "paused";
if (monitor?.isActive === false) return "paused";
if (monitor?.status === undefined) return "pending";
return monitor?.status == true ? "up" : "down";
}, []);
+61
View File
@@ -0,0 +1,61 @@
import { useEffect, useState, useCallback } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
export const useGetUser = (userId) => {
const [user, setUser] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const fetchUser = useCallback(async () => {
try {
setIsLoading(true);
const response = await networkService.getUserById({ userId });
setUser(response?.data?.data);
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
}, [userId]);
useEffect(() => {
if (userId) {
fetchUser();
}
}, [userId, fetchUser]);
return [user, isLoading, error];
};
export const useEditUser = (userId) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
const editUser = useCallback(
async (user) => {
try {
setIsLoading(true);
await networkService.editUser({ userId, user });
createToast({
body: t("editUserPage.toast.successUserUpdate"),
});
} catch (error) {
createToast({
body: error.message,
});
setError(error);
} finally {
setIsLoading(false);
}
},
[userId]
);
return [editUser, isLoading, error];
};
@@ -0,0 +1,102 @@
import { useState, useEffect } from "react";
import { ROLES } from "../../../../Utils/roleUtils";
import { editUserValidation } from "../../../../Validation/validation";
import Joi from "joi";
import { createToast } from "../../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
export const useEditUserForm = (user) => {
const [searchInput, setSearchInput] = useState("");
const [form, setForm] = useState({
firstName: "",
lastName: "",
email: "",
role: [],
});
// Effect to set user
useEffect(() => {
if (user) {
setForm({
firstName: user?.firstName,
lastName: user?.lastName,
email: user?.email,
role: user?.role,
});
}
}, [user]);
const handleRoleChange = (value) => {
const hasSuperAdmin = form.role.includes(ROLES.SUPERADMIN);
const newRoles = value.map((item) => item.role);
if (hasSuperAdmin && !newRoles.includes(ROLES.SUPERADMIN)) {
newRoles.push(ROLES.SUPERADMIN);
}
setForm({
...form,
role: newRoles,
});
};
const handleDeleteRole = (role) => {
if (role === ROLES.SUPERADMIN) return;
setForm({ ...form, role: form?.role?.filter((r) => r !== role) });
};
const handleSearchInput = (value) => {
setSearchInput(value);
};
return [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
];
};
export const useValidateEditUserForm = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
const validateForm = (form) => {
const { error } = editUserValidation.validate(form, {
abortEarly: false,
});
const errs = error?.details;
const errsObj = errs?.reduce((acc, curr) => {
acc[curr.path[0]] = curr.message;
return acc;
}, {});
setErrors(errsObj);
if (errs?.length > 0) {
createToast({
variant: "warning",
hasIcon: true,
title: t("editUserPage.toast.validationErrors"),
body: errs.map((err) => t(err.message)),
});
return false;
}
return true;
};
const validateField = (name, value) => {
const fieldSchema = Joi.object({
[name]: editUserValidation.extract(name),
});
const { error } = fieldSchema.validate({ [name]: value });
setErrors((prev) => {
const prevErrors = { ...prev };
if (error) prevErrors[name] = error.details[0].message;
else delete prevErrors[name];
return prevErrors;
});
};
return [errors, validateForm, validateField];
};
+122
View File
@@ -0,0 +1,122 @@
// Components
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import Search from "../../../Components/Inputs/Search";
import Button from "@mui/material/Button";
import RoleTable from "../components/RoleTable";
// Utils
import { useParams } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useGetUser, useEditUser } from "../../../Hooks/userHooks";
import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils";
import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser";
const EditUser = () => {
const { userId } = useParams();
const theme = useTheme();
const { t } = useTranslation();
const BREADCRUMBS = [
{ name: t("menu.team"), path: "/account/team" },
{ name: t("editUserPage.title"), path: "" },
];
const [user, isLoading, error] = useGetUser(userId);
const [editUser, isSaving, saveError] = useEditUser(userId);
const [
form,
setForm,
handleRoleChange,
handleDeleteRole,
searchInput,
handleSearchInput,
] = useEditUserForm(user);
const [errors, validateForm, validateField] = useValidateEditUserForm();
const onChange = (e) => {
const name = e.target.name;
const value = e.target.value;
validateField(name, value);
setForm({ ...form, [name]: value });
};
const handleSubmit = (e) => {
e.preventDefault();
const valid = validateForm(form);
if (valid) {
editUser(form);
}
};
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Typography variant="h2">{t("editUserPage.title")}</Typography>
<Stack
component="form"
onSubmit={handleSubmit}
gap={theme.spacing(12)}
maxWidth="50%"
>
<TextInput
name="firstName"
label={t("editUserPage.form.firstName")}
value={form?.firstName}
onChange={onChange}
error={errors?.firstName ? true : false}
helperText={t(errors?.firstName)}
/>
<TextInput
name="lastName"
label={t("editUserPage.form.lastName")}
value={form?.lastName}
onChange={onChange}
error={errors?.lastName ? true : false}
helperText={t(errors?.lastName)}
/>
<TextInput
name="email"
label={t("editUserPage.form.email")}
value={form?.email}
disabled={true}
error={errors?.email ? true : false}
helperText={t(errors?.email)}
/>
<Search
label={t("editUserPage.form.role")}
filteredBy="role"
inputValue={searchInput}
handleInputChange={handleSearchInput}
value={
form?.role
?.filter((role) => role !== ROLES.SUPERADMIN)
.map((role) => ({ role, _id: role })) || []
}
options={EDITABLE_ROLES}
multiple={true}
handleChange={handleRoleChange}
/>
<RoleTable
roles={form?.role}
handleDeleteRole={handleDeleteRole}
/>
<Box>
<Button
type="submit"
variant="contained"
color="accent"
loading={isLoading || isSaving}
>
{t("editUserPage.form.save")}
</Button>
</Box>
</Stack>
</Stack>
);
};
export default EditUser;
@@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
import TextInput from "../../Inputs/TextInput";
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import TextInput from "../../../Components/Inputs/TextInput";
import { newOrChangedCredentials } from "../../../Validation/validation";
import Alert from "../../Alert";
import Alert from "../../../Components/Alert";
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
import { createToast } from "../../../Utils/toastUtils";
@@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react";
import { useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import Avatar from "../../Avatar";
import TextInput from "../../Inputs/TextInput";
import ImageUpload from "../../Inputs/ImageUpload";
import Avatar from "../../../Components/Avatar";
import TextInput from "../../../Components/Inputs/TextInput";
import ImageUpload from "../../../Components/Inputs/ImageUpload";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { GenericDialog } from "../../Dialog/genericDialog";
import Dialog from "../../Dialog";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
import Dialog from "../../../Components/Dialog";
import { useTranslation } from "react-i18next";
/**
@@ -191,7 +191,6 @@ const ProfilePanel = () => {
>
<Stack
component="form"
className="edit-profile-form"
noValidate
spellCheck="false"
gap={SPACING_GAP}
@@ -0,0 +1,42 @@
import Typography from "@mui/material/Typography";
import DataTable from "../../../../Components/Table";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import { useTranslation } from "react-i18next";
import { ROLES } from "../../../../Utils/roleUtils";
const RoleTable = ({ roles, handleDeleteRole }) => {
const { t } = useTranslation();
const HEADERS = [
{
id: "name",
content: <Typography>{t("editUserPage.table.roleHeader")}</Typography>,
render: (row) => {
return row;
},
},
{
id: "delete",
content: <Typography>{t("editUserPage.table.actionHeader")}</Typography>,
render: (row) => {
if (row === ROLES.SUPERADMIN) return null;
return (
<DeleteOutlineRoundedIcon
onClick={() => {
handleDeleteRole(row);
}}
sx={{ cursor: "pointer" }}
/>
);
},
},
];
return (
<DataTable
headers={HEADERS}
data={roles}
/>
);
};
export default RoleTable;
@@ -3,14 +3,16 @@ import TabPanel from "@mui/lab/TabPanel";
import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import TextInput from "../../../Components/Inputs/TextInput";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../Inputs/Select";
import { GenericDialog } from "../../Dialog/genericDialog";
import DataTable from "../../Table/";
import Select from "../../../Components/Inputs/Select";
import { GenericDialog } from "../../../Components/Dialog/genericDialog";
import DataTable from "../../../Components/Table";
import { useGetInviteToken } from "../../../Hooks/inviteHooks";
import { useNavigate } from "react-router-dom";
import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin";
/**
* TeamPanel component manages the organization and team members,
* providing functionalities like renaming the organization, managing team members,
@@ -34,8 +36,9 @@ const TeamPanel = () => {
const [isDisabled, setIsDisabled] = useState(true);
const [errors, setErrors] = useState({});
const [isSendingInvite, setIsSendingInvite] = useState(false);
const navigate = useNavigate();
const [getInviteToken, clearToken, isLoading, error, token] = useGetInviteToken();
const isSuperAdmin = useIsSuperAdmin();
const headers = [
{
@@ -184,7 +187,6 @@ const TeamPanel = () => {
return (
<TabPanel
className="team-panel table-container"
value="team"
sx={{
"& h1": {
@@ -249,7 +251,17 @@ const TeamPanel = () => {
<DataTable
headers={headers}
data={data}
config={{ emptyView: t("teamPanel.noMembers") }}
config={{
emptyView: t("teamPanel.noMembers"),
rowSX: {
cursor: isSuperAdmin ? "pointer" : "default",
},
onRowClick: (row) => {
if (isSuperAdmin) {
navigate(`/account/team/${row.id}`);
}
},
}}
/>
</Stack>
-38
View File
@@ -1,38 +0,0 @@
.account h1.MuiTypography-root,
.account p.MuiTypography-root,
.account input,
.account button,
.account td,
.account .MuiSelect-select {
font-size: var(--env-var-font-size-medium);
}
.account h1.MuiTypography-root {
font-weight: 600;
}
.account .MuiTabPanel-root {
padding: 0;
margin-top: 50px;
}
.account button:not(.MuiIconButton-root) {
min-height: 34px;
}
.account .field {
flex: 1;
}
#modal-delete-account,
#modal-edit-org-name,
#modal-invite-member {
font-size: var(--env-var-font-size-large);
}
.account .MuiStack-root:has(span.MuiTypography-root.input-error) {
position: relative;
}
.account:not(:has(#modal-invite-member)) span.MuiTypography-root.input-error {
position: absolute;
top: 100%;
}
.account .MuiTableBody-root .MuiTableCell-root {
padding: var(--env-var-spacing-1-plus) var(--env-var-spacing-2);
}
+5 -5
View File
@@ -5,11 +5,10 @@ import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
import CustomTabList from "../../Components/Tab";
import TabContext from "@mui/lab/TabContext";
import ProfilePanel from "../../Components/TabPanels/Account/ProfilePanel";
import PasswordPanel from "../../Components/TabPanels/Account/PasswordPanel";
import TeamPanel from "../../Components/TabPanels/Account/TeamPanel";
import ProfilePanel from "./components/ProfilePanel";
import PasswordPanel from "./components/PasswordPanel";
import TeamPanel from "./components/TeamPanel";
import { useTranslation } from "react-i18next";
import "./index.css";
/**
* Account component renders a settings page with tabs for Profile, Password, and Team settings.
@@ -33,7 +32,9 @@ const Account = ({ open = "profile" }) => {
{ name: t("menu.password"), value: "password" },
{ name: t("menu.team"), value: "team" },
];
const hideTeams = !requiredRoles.some((role) => user.role.includes(role));
if (hideTeams) {
tabList = [
{ name: t("menu.profile"), value: "profile" },
@@ -63,7 +64,6 @@ const Account = ({ open = "profile" }) => {
return (
<Box
className="account"
px={theme.spacing(20)}
py={theme.spacing(12)}
>
+10 -7
View File
@@ -11,7 +11,7 @@ import { Box, Button } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks";
import { useAckAllChecks } from "../../Hooks/checkHooks";
import { useAcknowledgeChecks } from "../../Hooks/checkHooks";
import { useState, useEffect } from "react";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
@@ -34,7 +34,7 @@ const Incidents = () => {
const [updateTrigger, setUpdateTrigger] = useState(false);
//Hooks
const [ackAllChecks, ackAllLoading] = useAckAllChecks();
const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks();
//Utils
const theme = useTheme();
@@ -62,8 +62,9 @@ const Incidents = () => {
setMonitorLookup(monitorLookup);
}, [monitors]);
const handleAckAllChecks = () => {
ackAllChecks(setUpdateTrigger);
const handleAcknowledge = () => {
const monitorId = selectedMonitor === "0" ? null : selectedMonitor;
acknowledge(setUpdateTrigger, monitorId);
};
if (networkError || networkErrorSummary) {
@@ -81,10 +82,12 @@ const Incidents = () => {
<Button
variant="contained"
color="accent"
onClick={handleAckAllChecks}
disabled={ackAllLoading}
onClick={handleAcknowledge}
disabled={isLoadingAcknowledge}
>
{t("incidentsPageActionResolve")}
{selectedMonitor === "0"
? t("incidentsPageActionResolveAll")
: t("incidentsPageActionResolveMonitor")}
</Button>
</Box>
<StatusBoxes
@@ -62,14 +62,15 @@ export const CustomThreshold = ({
const theme = useTheme();
return (
<Stack
direction={"row"}
sx={{
width: "50%",
justifyContent: "space-between",
flexWrap: "wrap",
}}
direction={{ sm: "column", md: "row" }}
spacing={theme.spacing(2)}
>
<Box>
<Box
sx={{
width: { md: "45%", lg: "25%", xl: "20%" },
}}
justifyContent="flex-start"
>
<Checkbox
id={checkboxId}
name={checkboxName}
@@ -81,8 +82,10 @@ export const CustomThreshold = ({
<Stack
direction={"row"}
sx={{
justifyContent: "flex-end",
justifyContent: "flex-start",
}}
alignItems="center"
spacing={theme.spacing(4)}
>
<TextInput
maxWidth="var(--env-var-width-4)"
@@ -25,6 +25,7 @@ import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../../Components/Inputs/Select";
import { CustomThreshold } from "./Components/CustomThreshold";
import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
const SELECT_VALUES = [
{ _id: 0.25, name: "15 seconds" },
@@ -283,7 +284,7 @@ const CreateInfrastructureMonitor = () => {
/>
</Typography>
</Stack>
<Stack gap={theme.spacing(15)}>
<Stack gap={theme.spacing(8)}>
<TextInput
type="url"
id="url"
@@ -299,8 +300,10 @@ const CreateInfrastructureMonitor = () => {
disabled={!isCreate}
/>
{isCreate && (
<Box>
<Typography component="p">{t("infrastructureProtocol")}</Typography>
<FieldWrapper
label={t("infrastructureProtocol")}
labelVariant="p"
>
<ButtonGroup>
<Button
variant="group"
@@ -317,7 +320,7 @@ const CreateInfrastructureMonitor = () => {
{t("http")}
</Button>
</ButtonGroup>
</Box>
</FieldWrapper>
)}
<TextInput
type="text"
@@ -2,18 +2,19 @@
import { Stack } from "@mui/material";
import Gauge from "./Gauge";
import SkeletonLayout from "./skeleton";
import PropTypes from "prop-types";
// Utils
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
const Gauges = ({ shouldRender, monitor }) => {
const Gauges = ({ isLoading = false, monitor }) => {
const { decimalToPercentage, formatBytes } = useHardwareUtils();
const theme = useTheme();
const { t } = useTranslation();
if (!shouldRender) {
if (isLoading) {
return <SkeletonLayout />;
}
@@ -60,6 +61,7 @@ const Gauges = ({ shouldRender, monitor }) => {
return (
<Stack
direction="row"
flexWrap="wrap"
gap={theme.spacing(8)}
>
{gauges.map((gauge) => {
@@ -79,4 +81,9 @@ const Gauges = ({ shouldRender, monitor }) => {
);
};
Gauges.propTypes = {
isLoading: PropTypes.bool,
monitor: PropTypes.object,
};
export default Gauges;
@@ -92,11 +92,11 @@ const InfrastructureDetails = () => {
monitor={monitor}
/>
<GaugeBoxes
shouldRender={!isLoading}
isLoading={isLoading}
monitor={monitor}
/>
<MonitorTimeFrameHeader
shouldRender={!isLoading}
isLoading={isLoading}
dateRange={dateRange}
setDateRange={setDateRange}
/>
@@ -11,7 +11,7 @@ import Filter from "./Components/Filters";
import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
// Utils
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
@@ -58,6 +58,12 @@ const InfrastructureMonitors = () => {
setPage(0);
};
useEffect(() => {
if (isSearching) {
setPage(0);
}
}, [isSearching]);
const handleReset = () => {
setSelectedStatus(undefined);
setToFilterStatus(undefined);
@@ -93,13 +99,9 @@ const InfrastructureMonitors = () => {
if (!isLoading && typeof summary?.totalMonitors === "undefined") {
return (
<Fallback
vowelStart={true}
title="infrastructure monitor"
checks={[
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring",
]}
type="infrastructureMonitor"
title={t("infrastructureMonitor.fallback.title")}
checks={t("infrastructureMonitor.fallback.checks", { returnObjects: true })}
link="/infrastructure/create"
isAdmin={isAdmin}
/>
@@ -0,0 +1,109 @@
import Stack from "@mui/material/Stack";
import Gauge from "../../../../../Components/Charts/CustomGauge";
import Typography from "@mui/material/Typography";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { getPercentage } from "../../utils/utils";
import { useTranslation } from "react-i18next";
const GaugeBox = ({ title, subtitle, children }) => {
const theme = useTheme();
return (
<Stack
alignItems="center"
p={theme.spacing(2)}
maxWidth={150}
width={150}
>
{children}
<Typography variant="h2">{title}</Typography>
<Typography variant="body2">{subtitle}</Typography>
</Stack>
);
};
GaugeBox.propTypes = {
title: PropTypes.string.isRequired,
subtitle: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
};
const Gauges = ({ diagnostics, isLoading }) => {
const heapTotalSize = getPercentage(
diagnostics?.v8HeapStats?.totalHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const heapUsedSize = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.heapSizeLimitBytes
);
const actualHeapUsed = getPercentage(
diagnostics?.v8HeapStats?.usedHeapSizeBytes,
diagnostics?.v8HeapStats?.totalHeapSizeBytes
);
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
spacing={theme.spacing(4)}
flexWrap="wrap"
>
<GaugeBox
title={t("diagnosticsPage.gauges.heapAllocationTitle")}
subtitle={t("diagnosticsPage.gauges.heapAllocationSubtitle")}
>
<Gauge
isLoading={isLoading}
radius={100}
progress={heapTotalSize}
/>
</GaugeBox>
<GaugeBox
title={t("diagnosticsPage.gauges.heapUsageTitle")}
subtitle={t("diagnosticsPage.gauges.heapUsageSubtitle")}
>
<Gauge
isLoading={isLoading}
radius={100}
progress={heapUsedSize}
/>
</GaugeBox>
<GaugeBox
title={t("diagnosticsPage.gauges.heapUtilizationTitle")}
subtitle={t("diagnosticsPage.gauges.heapUtilizationSubtitle")}
>
<Gauge
isLoading={isLoading}
radius={100}
progress={actualHeapUsed}
/>
</GaugeBox>
<GaugeBox
title={t("diagnosticsPage.gauges.instantCpuUsageTitle")}
subtitle={t("diagnosticsPage.gauges.instantCpuUsageSubtitle")}
>
<Gauge
isLoading={isLoading}
radius={100}
progress={diagnostics?.cpuUsage?.usagePercentage}
precision={2}
/>
</GaugeBox>
</Stack>
);
};
Gauges.propTypes = {
diagnostics: PropTypes.object,
isLoading: PropTypes.bool,
};
export default Gauges;
@@ -0,0 +1,97 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Card from "@mui/material/Card";
import CardContent from "@mui/material/CardContent";
import CircularProgress from "@mui/material/CircularProgress";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import { formatBytes } from "../../utils/utils";
import { useTranslation } from "react-i18next";
const StatsCard = ({ title, value, unit = "", isLoading }) => {
const theme = useTheme();
return (
<Card sx={{ width: 150, maxWidth: 150, height: 80, maxHeight: 80 }}>
{isLoading ? (
<Stack
alignItems="center"
justifyContent="center"
height={80}
maxHeight={80}
>
<CircularProgress color="accent" />
</Stack>
) : (
<CardContent>
<Typography
variant="body1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Typography variant="body1">
{value} {unit}
</Typography>
</CardContent>
)}
</Card>
);
};
StatsCard.propTypes = {
title: PropTypes.string,
value: PropTypes.string,
unit: PropTypes.string,
isLoading: PropTypes.bool,
};
const Stats = ({ diagnostics, isLoading }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
direction="row"
gap={theme.spacing(4)}
flexWrap="wrap"
>
<StatsCard
title={t("diagnosticsPage.stats.eventLoopDelayTitle")}
value={getHumanReadableDuration(diagnostics?.eventLoopDelayMs)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.uptimeTitle")}
value={getHumanReadableDuration(diagnostics?.uptimeMs)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.usedHeapSizeTitle")}
value={formatBytes(diagnostics?.v8HeapStats?.usedHeapSizeBytes)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.totalHeapSizeTitle")}
value={formatBytes(diagnostics?.v8HeapStats?.totalHeapSizeBytes)}
isLoading={isLoading}
/>
<StatsCard
title={t("diagnosticsPage.stats.osMemoryLimitTitle")}
value={formatBytes(diagnostics?.osStats?.totalMemoryBytes)}
isLoading={isLoading}
/>
</Stack>
);
};
Stats.propTypes = {
diagnostics: PropTypes.object,
isLoading: PropTypes.bool,
};
export default Stats;
@@ -0,0 +1,54 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import Gauges from "./components/gauges";
import Stats from "./components/stats";
import Divider from "@mui/material/Divider";
import Button from "@mui/material/Button";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useFetchDiagnostics } from "../../../Hooks/logHooks";
const Diagnostics = () => {
// Local state
// Hooks
const theme = useTheme();
const { t } = useTranslation();
const [diagnostics, fetchDiagnostics, isLoading, error] = useFetchDiagnostics();
// Setup
return (
<Stack gap={theme.spacing(4)}>
<Box>
<Typography variant="h2">{t("diagnosticsPage.diagnosticDescription")}</Typography>
</Box>
<Divider color={theme.palette.accent.main} />
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(10)}
>
<Gauges
diagnostics={diagnostics}
isLoading={isLoading}
/>
<Stats
diagnostics={diagnostics}
isLoading={isLoading}
/>
<Box>
<Button
variant="contained"
color="accent"
onClick={fetchDiagnostics}
loading={isLoading}
>
Fetch Diagnostics
</Button>
</Box>
</Stack>
</Stack>
);
};
export default Diagnostics;
@@ -0,0 +1,16 @@
export const getPercentage = (value, total) => {
if (!value || !total) return 0;
return (value / total) * 100;
};
export const formatBytes = (bytes) => {
if (!bytes) return "N/A";
if (bytes === 0) return "0 Bytes";
if (bytes >= 1024 * 1024 * 1024) {
return `${(bytes / (1024 * 1024 * 1024)).toFixed(2)} GB`;
}
return `${(bytes / (1024 * 1024)).toFixed(2)} MB`;
};
+5 -1
View File
@@ -2,6 +2,7 @@ import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "../../../Components/Inputs/Select";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import { useFetchLogs } from "../../../Hooks/logHooks";
import { useTheme } from "@emotion/react";
@@ -51,12 +52,15 @@ const Logs = () => {
return (
<Stack gap={theme.spacing(4)}>
<Box>
<Typography variant="body">{t("logsPage.description")}</Typography>
<Typography variant="h2">{t("logsPage.description")}</Typography>
</Box>
<Divider color={theme.palette.accent.main} />
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
mt={theme.spacing(10)}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
@@ -1,8 +1,6 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import StatBox from "../../../../../Components/StatBox";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -24,20 +22,17 @@ const Metrics = ({ metrics = {} }) => {
});
return (
<Stack gap={theme.spacing(2)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
</Stack>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
);
};
export default Metrics;
+40 -31
View File
@@ -5,10 +5,12 @@ import Metrics from "./components/Metrics";
import FailedJobTable from "./components/FailedJobTable";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
// Utils
import { useState } from "react";
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -26,39 +28,46 @@ const QueueDetails = () => {
if (error || flushError) return <div>Error: {error.message}</div>;
return (
<Stack gap={theme.spacing(20)}>
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
variant="contained"
color="accent"
sx={{
position: "sticky",
bottom: 0,
zIndex: 1000,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(4),
border: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: theme.spacing(2),
}}
<Stack gap={theme.spacing(4)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<Divider color={theme.palette.accent.main} />
<Stack
gap={theme.spacing(20)}
mt={theme.spacing(10)}
>
<Button
onClick={() => {
setTrigger(!trigger);
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
variant="contained"
color="accent"
sx={{
position: "sticky",
bottom: 0,
zIndex: 1000,
backgroundColor: theme.palette.primary.main,
p: theme.spacing(4),
border: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: theme.spacing(2),
}}
loading={isLoading}
>
{t("queuePage.refreshButton")}
</Button>
<Button
onClick={() => flushQueue(trigger, setTrigger)}
loading={isFlushing}
>
{t("queuePage.flushButton")}
</Button>
</ButtonGroup>
<Button
onClick={() => {
setTrigger(!trigger);
}}
loading={isLoading}
>
{t("queuePage.refreshButton")}
</Button>
<Button
onClick={() => flushQueue(trigger, setTrigger)}
loading={isFlushing}
>
{t("queuePage.flushButton")}
</Button>
</ButtonGroup>
</Stack>
</Stack>
);
};
+4 -1
View File
@@ -4,6 +4,7 @@ import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Queue from "./Queue";
import LogsComponent from "./Logs";
import Diagnostics from "./Diagnostics";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
@@ -14,7 +15,7 @@ const Logs = () => {
const theme = useTheme();
// Local state
const [value, setValue] = useState(0);
const [value, setValue] = useState(2);
// Handlers
const handleChange = (event, newValue) => {
@@ -31,9 +32,11 @@ const Logs = () => {
>
<Tab label={t("logsPage.tabs.logs")} />
<Tab label={t("logsPage.tabs.queue")} />
<Tab label={t("logsPage.tabs.diagnostics")} />
</Tabs>
{value === 0 && <LogsComponent />}
{value === 1 && <Queue />}
{value === 2 && <Diagnostics />}
</Stack>
);
};
@@ -0,0 +1,69 @@
// Components
import { Stack, Typography } from "@mui/material";
import DeleteIcon from "@mui/icons-material/Delete";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const MonitorListItem = ({ monitor, onDelete }) => {
const theme = useTheme();
return (
<Stack
direction={"row"}
alignItems={"center"}
gap={theme.spacing(4)}
width="100%"
>
<Typography flexGrow={1}>{monitor.name}</Typography>
<DeleteIcon
sx={{ cursor: "pointer" }}
onClick={() => onDelete(monitor)}
/>
</Stack>
);
};
MonitorListItem.propTypes = {
monitor: PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
}).isRequired,
onDelete: PropTypes.func.isRequired,
};
const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => {
const onDelete = (monitorToDelete) => {
const newMonitors = selectedMonitors.filter(
(monitor) => monitor._id !== monitorToDelete._id
);
setSelectedMonitors(newMonitors);
};
const theme = useTheme();
return (
<Stack
gap={theme.spacing(6)}
width="100%"
>
{selectedMonitors?.map((monitor) => (
<MonitorListItem
key={monitor._id}
monitor={monitor}
onDelete={onDelete}
/>
))}
</Stack>
);
};
MonitorList.propTypes = {
selectedMonitors: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.string.isRequired,
name: PropTypes.string.isRequired,
})
).isRequired,
setSelectedMonitors: PropTypes.func.isRequired,
};
export default MonitorList;
@@ -9,6 +9,8 @@ import { DatePicker } from "@mui/x-date-pickers/DatePicker";
import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import MonitorList from "./Components/MonitorList";
import Checkbox from "../../../Components/Inputs/Checkbox";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
@@ -212,6 +214,17 @@ const CreateMaintenance = () => {
});
};
const handleMonitorsChange = (selected) => {
setForm((prev) => ({ ...prev, monitors: selected }));
const { error } = maintenanceWindowValidation.validate(
{ monitors: selected },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleSubmit = async () => {
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
// Build timestamp for maintenance window from startDate and startTime
@@ -525,19 +538,25 @@ const CreateMaintenance = () => {
id={"monitors"}
label={t("addMonitors")}
multiple={true}
isAdorned={false}
options={monitors ? monitors : []}
isAdorned={true}
options={monitors}
filteredBy="name"
secondaryLabel={"type"}
inputValue={search}
value={form.monitors}
handleInputChange={handleSearch}
handleChange={handleSelectMonitors}
handleInputChange={setSearch}
handleChange={handleMonitorsChange}
error={errors["monitors"]}
disabled={maintenanceWindowId !== undefined}
/>
<MonitorList
selectedMonitors={form.monitors}
setSelectedMonitors={(monitors) =>
setForm((prev) => ({ ...prev, monitors }))
}
/>
</Stack>
</ConfigBox>
<Box
ml="auto"
display="inline-block"
+3 -6
View File
@@ -68,12 +68,9 @@ const Maintenance = () => {
if (isDataFetched && maintenanceWindows.length === 0) {
return (
<Fallback
title="maintenance window"
checks={[
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows",
]}
type="maintenanceWindow"
title={t("maintenanceWindow.fallback.title")}
checks={t("maintenanceWindow.fallback.checks", { returnObjects: true })}
link="/maintenance/create"
isAdmin={isAdmin}
/>
+29 -12
View File
@@ -65,12 +65,19 @@ const CreateNotifications = () => {
const [isOpen, setIsOpen] = useState(false);
const getNotificationTypeValue = (typeId) => {
return NOTIFICATION_TYPES.find((type) => type._id === typeId)?.value || "email";
};
const extractError = (error, field) =>
error?.details.find((d) => d.path.includes(field))?.message;
// handlers
const onSubmit = (e) => {
e.preventDefault();
const form = {
...notification,
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
type: getNotificationTypeValue(notification.type),
};
let error = null;
@@ -84,7 +91,7 @@ const CreateNotifications = () => {
});
console.log(JSON.stringify(newErrors));
console.log(JSON.stringify(form, null, 2));
createToast({ body: "Please check the form for errors." });
createToast({ body: Object.values(newErrors)[0] });
setErrors(newErrors);
return;
}
@@ -98,22 +105,32 @@ const CreateNotifications = () => {
const onChange = (e) => {
const { name, value } = e.target;
let rawNotification = { ...notification, [name]: value };
let newNotification = {
...rawNotification,
type: getNotificationTypeValue(rawNotification.type),
};
const newNotification = { ...notification, [name]: value };
const { error } = notificationValidation.validate(newNotification, {
abortEarly: false,
});
let validationError = { ...errors };
const { error } = notificationValidation.extract(name).validate(value);
setErrors((prev) => ({
...prev,
[name]: error?.message,
}));
if (name === "type") {
validationError["type"] = extractError(error, "type");
validationError["address"] = extractError(error, "address");
} else {
validationError[name] = extractError(error, name);
}
setNotification(newNotification);
setNotification(rawNotification);
setErrors(validationError);
};
const onTestNotification = () => {
const form = {
...notification,
type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
type: getNotificationTypeValue(notification.type),
};
let error = null;
@@ -125,7 +142,7 @@ const CreateNotifications = () => {
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
createToast({ body: "Please check the form for errors." });
createToast({ body: Object.values(newErrors)[0] });
setErrors(newErrors);
return;
}
@@ -139,7 +156,7 @@ const CreateNotifications = () => {
}
};
const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
const type = getNotificationTypeValue(notification.type);
return (
<Stack gap={theme.spacing(10)}>
<Breadcrumbs list={BREADCRUMBS} />
+1 -1
View File
@@ -79,7 +79,7 @@ const Notifications = () => {
if (notifications?.length === 0) {
return (
<Fallback
vowelStart={false}
type="notifications"
title={t("notifications.fallback.title")}
checks={t("notifications.fallback.checks", { returnObjects: true })}
link="/notifications/create"
+18 -8
View File
@@ -1,4 +1,5 @@
// Components
import { useState } from "react";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import { Stack, Typography } from "@mui/material";
import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
@@ -6,12 +7,14 @@ import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
import Fallback from "../../../Components/Fallback";
import GenericFallback from "../../../Components/GenericFallback";
import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
import { useFetchSettings } from "../../../Hooks/settingsHooks";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
const TYPES = ["pagespeed"];
@@ -30,6 +33,13 @@ const PageSpeed = () => {
order: null,
});
const [settingsData, setSettingsData] = useState(undefined);
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
setIsApiKeySet: () => {},
setIsEmailPasswordSet: () => {},
});
if (networkError === true) {
return (
<GenericFallback>
@@ -48,16 +58,16 @@ const PageSpeed = () => {
if (!isLoading && monitors?.length === 0) {
return (
<Fallback
title="pagespeed monitor"
checks={[
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved",
]}
type="pageSpeed"
title={t("pageSpeed.fallback.title")}
checks={t("pageSpeed.fallback.checks", { returnObjects: true })}
link="/pagespeed/create"
isAdmin={isAdmin}
// showPageSpeedWarning={isAdmin && !pagespeedApiKey}
/>
>
{isAdmin && settingsData && !settingsData.pagespeedApiKey && (
<FallbackPageSpeedWarning settingsData={settingsData} />
)}
</Fallback>
);
}
+7 -7
View File
@@ -117,8 +117,8 @@ const SettingsEmail = ({
<Box>
<Stack gap={theme.spacing(10)}>
<Box>
<Typography>{t("settingsPage.emailSettings.labelHost")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelHost")}
name="systemEmailHost"
placeholder="smtp.gmail.com"
value={systemEmailHost}
@@ -126,8 +126,8 @@ const SettingsEmail = ({
/>
</Box>
<Box>
<Typography>{t("settingsPage.emailSettings.labelPort")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelPort")}
name="systemEmailPort"
placeholder="425"
type="number"
@@ -136,8 +136,8 @@ const SettingsEmail = ({
/>
</Box>
<Box>
<Typography>{t("settingsPage.emailSettings.labelUser")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelUser")}
name="systemEmailUser"
placeholder={t("settingsPage.emailSettings.placeholderUser")}
value={systemEmailUser}
@@ -145,8 +145,8 @@ const SettingsEmail = ({
/>
</Box>
<Box>
<Typography>{t("settingsPage.emailSettings.labelAddress")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelAddress")}
name="systemEmailAddress"
placeholder="uptime@bluewavelabs.ca"
value={systemEmailAddress}
@@ -155,8 +155,8 @@ const SettingsEmail = ({
</Box>
{(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
<Box>
<Typography>{t("settingsPage.emailSettings.labelPassword")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelPassword")}
name="systemEmailPassword"
type="password"
placeholder="123 456 789 101112"
@@ -188,8 +188,8 @@ const SettingsEmail = ({
</Box>
)}
<Box>
<Typography>{t("settingsPage.emailSettings.labelTLSServername")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelTLSServername")}
name="systemEmailTLSServername"
placeholder="bluewavelabs.ca"
value={systemEmailTLSServername}
@@ -197,8 +197,8 @@ const SettingsEmail = ({
/>
</Box>
<Box>
<Typography>{t("settingsPage.emailSettings.labelConnectionHost")}</Typography>
<TextInput
label={t("settingsPage.emailSettings.labelConnectionHost")}
name="systemEmailConnectionHost"
placeholder="bluewavelabs.ca"
value={systemEmailConnectionHost}
+33 -7
View File
@@ -2,16 +2,37 @@ import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
import Select from "../../Components/Inputs/Select";
import Search from "../../Components/Inputs/Search";
import timezones from "../../Utils/timezones.json";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
import { useCallback, useMemo, useState } from "react";
const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
const theme = useTheme();
const { t } = useTranslation();
const [rawInput, setRawInput] = useState("");
const selectedTimezone = useMemo(
() => timezones.find((tz) => tz._id === timezone) ?? null,
[timezone]
);
const handleTimezoneChange = useCallback(
(newValue) => {
setRawInput("");
handleChange({
target: {
name: "timezone",
value: newValue?._id ?? "",
},
});
},
[handleChange]
);
return (
<ConfigBox>
<Box>
@@ -28,12 +49,17 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
label={t("settingsPage.timezoneSettings.label")}
name="timezone"
value={timezone}
onChange={handleChange}
items={timezones}
<Search
id="timezone"
label={t("settingsDisplayTimezone")}
options={timezones}
filteredBy="name"
value={selectedTimezone}
inputValue={rawInput}
handleInputChange={(val) => setRawInput(val)}
handleChange={handleTimezoneChange}
isAdorned={false}
unit="timezone"
/>
</Stack>
</ConfigBox>
@@ -4,7 +4,7 @@ import { TabPanel } from "@mui/lab";
import ConfigBox from "../../../../../Components/ConfigBox";
import Checkbox from "../../../../../Components/Inputs/Checkbox";
import TextInput from "../../../../../Components/Inputs/TextInput";
import Select from "../../../../../Components/Inputs/Select";
import Search from "../../../../../Components/Inputs/Search";
import ImageUpload from "../../../../../Components/Inputs/ImageUpload";
import ColorPicker from "../../../../../Components/Inputs/ColorPicker";
import Progress from "../Progress";
@@ -14,6 +14,7 @@ import { useTheme } from "@emotion/react";
import timezones from "../../../../../Utils/timezones.json";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useMemo, useState, useCallback } from "react";
const TabSettings = ({
isCreate,
@@ -28,6 +29,25 @@ const TabSettings = ({
// Utils
const theme = useTheme();
const { t } = useTranslation();
const [rawInput, setRawInput] = useState("");
const selectedTimezone = useMemo(
() => timezones.find((tz) => tz._id === form.timezone) ?? null,
[form.timezone]
);
const handleTimezoneChange = useCallback(
(newValue) => {
setRawInput("");
handleFormChange({
target: {
name: "timezone",
value: newValue?._id ?? "",
},
});
},
[handleFormChange]
);
return (
<TabPanel value={tabValue}>
@@ -101,13 +121,17 @@ const TabSettings = ({
</Typography>
</Stack>
<Stack gap={theme.spacing(6)}>
<Select
<Search
id="timezone"
name="timezone"
label={t("settingsDisplayTimezone")}
items={timezones}
value={form.timezone}
onChange={handleFormChange}
options={timezones}
filteredBy="name"
value={selectedTimezone}
inputValue={rawInput}
handleInputChange={(newVal) => setRawInput(newVal)}
handleChange={handleTimezoneChange}
isAdorned={false}
unit="timezone"
/>
</Stack>
</ConfigBox>
@@ -42,12 +42,9 @@ const StatusPages = () => {
if (!isLoading && typeof statusPages !== "undefined" && statusPages.length === 0) {
return (
<Fallback
title="status page"
checks={[
"Monitor and display the health of your services in real time",
"Track multiple services and share their status",
"Keep users informed about outages and performance",
]}
title={t("statusPage.fallback.title")}
checks={t("statusPage.fallback.checks", { returnObjects: true })}
type="statusPage"
link="/status/uptime/create"
isAdmin={isAdmin}
/>
+24 -24
View File
@@ -163,18 +163,18 @@ const Configure = () => {
e.preventDefault();
const toSubmit = {
_id: form._id,
url: form.url,
name: form.name,
type: form.type,
matchMethod: form.matchMethod,
expectedValue: form.expectedValue,
jsonPath: form.jsonPath,
interval: form.interval,
teamId: form.teamId,
userId: form.userId,
port: form.port,
ignoreTlsErrors: form.ignoreTlsErrors,
_id: form?._id,
url: form?.url,
name: form?.name,
type: form?.type,
matchMethod: form?.matchMethod,
expectedValue: form?.expectedValue,
jsonPath: form?.jsonPath,
interval: form?.interval,
teamId: form?.teamId,
userId: form?.userId,
port: form?.port,
ignoreTlsErrors: form?.ignoreTlsErrors,
};
if (!useAdvancedMatching) {
@@ -197,7 +197,7 @@ const Configure = () => {
return;
}
toSubmit.notifications = form.notifications;
toSubmit.notifications = form?.notifications;
await updateMonitor({ monitor: toSubmit, redirect: "/uptime" });
};
@@ -235,7 +235,7 @@ const Configure = () => {
component="h1"
variant="monitorName"
>
{form.name}
{form?.name}
</Typography>
<Stack
direction="row"
@@ -267,7 +267,7 @@ const Configure = () => {
component="h2"
variant="monitorUrl"
>
{form.url?.replace(/^https?:\/\//, "") || "..."}
{form?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
@@ -352,11 +352,11 @@ const Configure = () => {
type="number"
label={t("portToMonitor")}
placeholder="5173"
value={form.port || ""}
value={form?.port || ""}
onChange={onChange}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={form.type !== "port"}
hidden={form?.type !== "port"}
/>
<TextInput
name="name"
@@ -379,7 +379,7 @@ const Configure = () => {
<NotificationsConfig
notifications={notifications}
setMonitor={setForm}
setNotifications={form.notifications}
setNotifications={form?.notifications}
/>
</ConfigBox>
<ConfigBox>
@@ -398,7 +398,7 @@ const Configure = () => {
control={
<Switch
name="ignoreTlsErrors"
checked={form.ignoreTlsErrors ?? false}
checked={form?.ignoreTlsErrors ?? false}
onChange={onChange}
sx={{ mr: theme.spacing(2) }}
/>
@@ -430,12 +430,12 @@ const Configure = () => {
isChecked={useAdvancedMatching}
onChange={onChange}
/>
{form.type === "http" && useAdvancedMatching && (
{form?.type === "http" && useAdvancedMatching && (
<>
<Select
name="matchMethod"
label={t("matchMethod")}
value={form.matchMethod || "equal"}
value={form?.matchMethod || "equal"}
onChange={onChange}
items={matchMethodOptions}
/>
@@ -445,8 +445,8 @@ const Configure = () => {
name="expectedValue"
label={t("expectedValue")}
isOptional={true}
placeholder={expectedValuePlaceholders[form.matchMethod || "equal"]}
value={form.expectedValue}
placeholder={expectedValuePlaceholders[form?.matchMethod || "equal"]}
value={form?.expectedValue}
onChange={onChange}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
@@ -466,7 +466,7 @@ const Configure = () => {
label="JSON Path"
isOptional={true}
placeholder="data.status"
value={form.jsonPath}
value={form?.jsonPath}
onChange={onChange}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
+15 -3
View File
@@ -17,12 +17,13 @@ import Checkbox from "../../../Components/Inputs/Checkbox";
// Utils
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { monitorValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import { useCreateMonitor } from "../../../Hooks/monitorHooks";
import { useCreateMonitor, useFetchMonitorById } from "../../../Hooks/monitorHooks";
import { useParams } from "react-router-dom";
const CreateMonitor = () => {
// Local state
@@ -42,12 +43,23 @@ const CreateMonitor = () => {
});
// Setup
const MS_PER_MINUTE = 60000;
const theme = useTheme();
const { t } = useTranslation();
const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId();
const [createMonitor, isCreating] = useCreateMonitor();
const { monitorId } = useParams();
const formatAndSet = (monitor) => {
monitor.interval = monitor.interval / MS_PER_MINUTE;
setMonitor(monitor);
};
const [isLoading] = useFetchMonitorById({
monitorId,
setMonitor: formatAndSet,
updateTrigger: true,
});
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
+3 -8
View File
@@ -174,14 +174,9 @@ const UptimeMonitors = () => {
) {
return (
<Fallback
vowelStart={true}
title="uptime monitor"
checks={[
"Check if websites or servers are online & responsive",
"Alert teams about downtime or performance issues",
"Monitor HTTP endpoints, pings, containers & ports",
"Track historical uptime and reliability trends",
]}
type="uptimeMonitor"
title={t("uptimeMonitor.fallback.title")}
checks={t("uptimeMonitor.fallback.checks", { returnObjects: true })}
link="/uptime/create"
isAdmin={isAdmin}
/>
+9
View File
@@ -42,6 +42,7 @@ import CreateNotifications from "../Pages/Notifications/create";
// Settings
import Account from "../Pages/Account";
import EditUser from "../Pages/Account/EditUser";
import Settings from "../Pages/Settings";
import Maintenance from "../Pages/Maintenance";
@@ -187,6 +188,14 @@ const Routes = () => {
path="account/team"
element={<Account open={"team"} />}
/>
<Route
path="account/team/:userId"
element={
<RoleProtectedRoute roles={["superadmin"]}>
<EditUser />
</RoleProtectedRoute>
}
/>
<Route
path="logs"
+59 -28
View File
@@ -111,30 +111,6 @@ class NetworkService {
return this.axiosInstance.post(`/monitors`, monitor);
}
/**
*
* ************************************
* Check the endpoint resolution
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
*/
async checkEndpointResolution(config) {
const { monitorURL } = config;
const params = new URLSearchParams();
if (monitorURL) params.append("monitorURL", monitorURL);
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
headers: {
"Content-Type": "application/json",
},
});
}
/**
* ************************************
* Get all uptime monitors for a Team
@@ -393,7 +369,7 @@ class NetworkService {
*
*/
async updateUser(config) {
return this.axiosInstance.put(`/auth/user/${config.userId}`, config.form);
return this.axiosInstance.put(`/auth/user`, config.form);
}
/**
@@ -406,8 +382,8 @@ class NetworkService {
* @param {string} config.userId - The ID of the user to be deleted.
*
**/
async deleteUser(config) {
return this.axiosInstance.delete(`/auth/user/${config.userId}`);
async deleteUser() {
return this.axiosInstance.delete(`/auth/user`);
}
/**
@@ -631,6 +607,35 @@ class NetworkService {
});
}
/**
* ************************************
* Update the status of all checks for a given monitor
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {string} config.monitorId - The ID of the monitor.
* @param {boolean} config.ack - The acknowledgment to update the checks to.
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
*
*/
async updateMonitorChecksStatus(config) {
return this.axiosInstance.put(`/checks/monitor/${config.monitorId}`, {
ack: config.ack,
});
}
/**
* ************************************
* Update the status of all checks for a given team
* ************************************
*
* @async
* @param {Object} config - The configuration object.
* @param {boolean} config.ack - The acknowledgment to update the checks to.
* @returns {Promise<AxiosResponse>} The response from the axios PUT request.
*
*/
async updateAllChecksStatus(config) {
return this.axiosInstance.put(`/checks/team/`, {
ack: config.ack,
@@ -977,7 +982,7 @@ class NetworkService {
});
}
return this.axiosInstance.get(`/monitors/summary/team?${params.toString()}`, {
return this.axiosInstance.get(`/monitors/team/summary?${params.toString()}`, {
headers: {
"Content-Type": "application/json",
},
@@ -1091,6 +1096,32 @@ class NetworkService {
},
});
}
async getDiagnostics() {
return this.axiosInstance.get(`/diagnostic/system`, {
headers: {
"Content-Type": "application/json",
},
});
}
async getUserById(config) {
const userId = config?.userId;
return this.axiosInstance.get(`auth/users/${userId}`, {
headers: {
"Content-Type": "application/json",
},
});
}
async editUser(config) {
const { userId, user } = config;
return this.axiosInstance.put(`auth/users/${userId}`, user, {
headers: {
"Content-Type": "application/json",
},
});
}
}
export default NetworkService;
+5
View File
@@ -553,6 +553,11 @@ const baseTheme = (palette) => ({
"&:hover .MuiOutlinedInput-notchedOutline": {
borderColor: theme.palette.primary.lowContrast,
},
padding: "0 13px",
minHeight: "34px",
display: "flex",
alignItems: "center",
lineHeight: 1,
}),
},
},
+13
View File
@@ -0,0 +1,13 @@
export const ROLES = {
SUPERADMIN: "superadmin",
ADMIN: "admin",
USER: "user",
DEMO: "demo",
};
export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO];
export const EDITABLE_ROLES = [
{ role: ROLES.ADMIN, _id: ROLES.ADMIN },
{ role: ROLES.USER, _id: ROLES.USER },
];
+4
View File
@@ -85,6 +85,7 @@ export const getHumanReadableDuration = (ms) => {
hours: durationObj.hours(),
minutes: durationObj.minutes(),
seconds: durationObj.seconds(),
milliseconds: durationObj.milliseconds(),
};
const result = [];
@@ -101,6 +102,9 @@ export const getHumanReadableDuration = (ms) => {
if (result.length < 2 && parts.seconds > 0) {
result.push(`${parts.seconds}s`);
}
if (result.length < 2 && parts.milliseconds > 0 && parts.seconds < 1) {
result.push(`${parts.milliseconds.toFixed(2)}ms`);
}
if (result.length === 0) {
// fallback for durations < 1s
+6 -4
View File
@@ -1,7 +1,6 @@
import PropTypes from "prop-types";
import { toast, Slide } from "react-toastify";
import Alert from "../Components/Alert";
import Toast from "../Components/Toast";
/**
* @param {object} props
* @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error").
@@ -15,6 +14,7 @@ export const createToast = ({
variant = "info",
title,
body,
hasDismiss = false,
hasIcon = false,
config = {},
}) => {
@@ -29,13 +29,14 @@ export const createToast = ({
toast(
({ closeToast }) => (
<Alert
<Toast
variant={variant}
title={title}
body={body}
isToast={true}
hasIcon={hasIcon}
onClick={closeToast}
hasDismiss={hasDismiss}
hasIcon={hasIcon}
/>
),
toastConfig
@@ -47,5 +48,6 @@ createToast.propTypes = {
title: PropTypes.string,
body: PropTypes.string.isRequired,
hasIcon: PropTypes.bool,
hasDismiss: PropTypes.bool,
config: PropTypes.object,
};
+47 -8
View File
@@ -1,5 +1,6 @@
import joi from "joi";
import dayjs from "dayjs";
import { ROLES } from "../Utils/roleUtils";
const THRESHOLD_COMMON_BASE_MSG = "Threshold must be a number.";
@@ -410,17 +411,54 @@ const notificationValidation = joi.object({
"string.empty": "Notification name is required",
"any.required": "Notification name is required",
}),
address: joi.string().required().messages({
"string.empty": "This field cannot be empty",
"string.base": "This field must be a string",
"any.required": "This field is required",
}),
type: joi.string().required().messages({
"string.empty": "This field is required",
"any.required": "This field is required",
type: joi
.string()
.valid("email", "webhook", "slack", "discord", "pager_duty")
.required()
.messages({
"string.empty": "Notification type is required",
"any.required": "Notification type is required",
"any.only": "Notification type must be email, webhook, or pager_duty",
}),
address: joi.when("type", {
is: "email",
then: joi
.string()
.email({ tlds: { allow: false } })
.required()
.messages({
"string.empty": "E-mail address cannot be empty",
"any.required": "E-mail address is required",
"string.email": "Please enter a valid e-mail address",
}),
otherwise: joi.string().uri().required().messages({
"string.empty": "Webhook URL cannot be empty",
"any.required": "Webhook URL is required",
"string.uri": "Please enter a valid Webhook URL",
}),
}),
});
const editUserValidation = joi.object({
firstName: nameSchema,
lastName: lastnameSchema,
role: joi
.array()
.items(joi.string().valid(...Object.values(ROLES)))
.min(1)
.messages({
"array.min": "auth.common.fields.role.errors.min",
}),
email: joi
.string()
.required()
.trim()
.email({ tlds: { allow: false } })
.lowercase(),
});
export {
newOrChangedCredentials,
loginCredentials,
@@ -433,4 +471,5 @@ export {
statusPageValidation,
logoImageValidation,
notificationValidation,
editUserValidation,
};
+132 -19
View File
@@ -32,6 +32,11 @@
"errors": {
"incorrect": "The password you provided does not match our records"
}
},
"role": {
"errors": {
"min": "At least one role is required"
}
}
},
"inputs": {
@@ -212,8 +217,9 @@
"checkFormError": "Please check the form for errors.",
"checkFrequency": "Check frequency",
"checkHooks": {
"failureResolveOne": "Failed to resolve incident.",
"failureResolveAll": "Failed to resolve all incidents."
"failureResolveAll": "Failed to resolve all incidents.",
"failureResolveMonitor": "Failed to resolve monitor incidents.",
"failureResolveOne": "Failed to resolve incident."
},
"checkingEvery": "Checking every",
"city": "CITY",
@@ -248,6 +254,8 @@
"createMonitor": "Create monitor",
"createNew": "Create new",
"createNotifications": {
"dialogDeleteConfirm": "Delete",
"dialogDeleteTitle": "Are you sure you want to delete this notification?",
"discordSettings": {
"description": "Configure your Discord webhook here",
"title": "Discord",
@@ -278,6 +286,7 @@
"webhookLabel": "Slack webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
"testNotification": "Test notification",
"title": "Create notification channel",
"typeSettings": {
"description": "Select the type of notification channel you want to create.",
@@ -289,10 +298,7 @@
"title": "Webhook",
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
},
"testNotification": "Test notification",
"dialogDeleteTitle": "Are you sure you want to delete this notification?",
"dialogDeleteConfirm": "Delete"
}
},
"createYour": "Create your",
"date&Time": "Date & Time",
@@ -302,6 +308,27 @@
"deleteStatusPage": "Do you want to delete this status page?",
"deleteStatusPageConfirm": "Yes, delete status page",
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
"diagnosticsPage": {
"diagnosticDescription": "System diagnostics",
"gauges": {
"heapAllocationSubtitle": "% of available memory",
"heapAllocationTitle": "Heap allocation",
"heapUsageSubtitle": "% of available memory",
"heapUsageTitle": "Heap usage",
"heapUtilizationSubtitle": "% of allocated",
"heapUtilizationTitle": "Heap utilization",
"instantCpuUsageSubtitle": "% of 1s used by CPU",
"instantCpuUsageTitle": "Instant CPU usage"
},
"stats": {
"eventLoopDelayTitle": "Event loop delay",
"osMemoryLimitTitle": "OS Memory Limit",
"totalHeapSizeTitle": "Total heap size",
"uptimeTitle": "Uptime",
"usedHeapSizeTitle": "Used heap size"
},
"statsDescription": "System statistics"
},
"disk": "Disk",
"diskUsage": "Disk Usage",
"displayName": "Display name",
@@ -352,6 +379,24 @@
"duration": "Duration",
"edit": "Edit",
"editMaintenance": "Edit maintenance",
"editUserPage": {
"form": {
"email": "Email",
"firstName": "First name",
"lastName": "Last name",
"role": "Roles",
"save": "Save"
},
"table": {
"actionHeader": "Action",
"roleHeader": "Role"
},
"title": "Edit user",
"toast": {
"successUserUpdate": "User updated successfully",
"validationErrors": "Validation errors"
}
},
"editing": "Editing...",
"emailSent": "Email sent successfully",
"errorInvalidFieldId": "Invalid field ID provided",
@@ -382,6 +427,9 @@
"friendlyNameInput": "Friendly name",
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
"gb": "GB",
"general": {
"noOptionsFound": "No {{unit}} found"
},
"greeting": {
"append": "The afternoon is your playground—let's make it epic!",
"overview": "Here's an overview of your {{type}} monitors.",
@@ -396,24 +444,25 @@
"incidentsOptionsHeader": "Incidents for:",
"incidentsOptionsHeaderFilterAll": "All",
"incidentsOptionsHeaderFilterBy": "Filter by:",
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
"incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve",
"incidentsTableResolvedAt": "Resolved at",
"incidentsOptionsHeaderFilterDown": "Down",
"incidentsTableActionResolve": "Resolve",
"incidentsPageActionResolve": "Resolve all incidents",
"incidentsOptionsHeaderFilterResolved": "Resolved",
"incidentsOptionsHeaderLastDay": "Last day",
"incidentsOptionsHeaderLastHour": "Last hour",
"incidentsOptionsHeaderLastWeek": "Last week",
"incidentsOptionsHeaderShow": "Show:",
"incidentsOptionsHeaderTotalIncidents": "Total Incidents",
"incidentsOptionsPlaceholderAllServers": "All servers",
"incidentsPageActionResolveMonitor": "Resolve monitor incidents",
"incidentsPageActionResolveAll": "Resolve all incidents",
"incidentsPageTitle": "Incidents",
"incidentsTableActionResolve": "Resolve",
"incidentsTableDateTime": "Date & Time",
"incidentsTableMessage": "Message",
"incidentsTableMonitorName": "Monitor Name",
"incidentsTableNoIncidents": "No incidents recorded",
"incidentsTablePaginationLabel": "incidents",
"incidentsTableResolvedAt": "Resolved at",
"incidentsTableStatus": "Status",
"incidentsTableStatusCode": "Status Code",
"infrastructureAlertNotificationDescription": "Send a notification to user(s) when thresholds exceed a specified percentage.",
@@ -425,6 +474,17 @@
"infrastructureDisplayNameLabel": "Display name",
"infrastructureEditMonitor": "Save Infrastructure Monitor",
"infrastructureEditYour": "Edit your",
"infrastructureMonitor": {
"fallback": {
"checks": [
"Track the performance of your servers",
"Identify bottlenecks and optimize usage",
"Ensure reliability with real-time monitoring"
],
"title": "An infrastructure monitor is used to:",
"actionButton": "Let's create your first infrastructure monitor!"
}
},
"infrastructureMonitorCreated": "Infrastructure monitor created successfully!",
"infrastructureMonitorUpdated": "Infrastructure monitor updated successfully!",
"infrastructureProtocol": "Protocol",
@@ -443,7 +503,7 @@
"inviteNoTokenFound": "No invite token found",
"loginHere": "Login here",
"logsPage": {
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
"description": "System logs - last 1000 lines",
"logLevelSelect": {
"title": "Log level",
"values": {
@@ -455,6 +515,7 @@
}
},
"tabs": {
"diagnostics": "Diagnostics",
"logs": "Server logs",
"queue": "Job queue"
},
@@ -467,6 +528,17 @@
"maintenance": "maintenance",
"maintenanceRepeat": "Maintenance Repeat",
"maintenanceTableActionMenuDialogTitle": "Do you really want to remove this maintenance window?",
"maintenanceWindow": {
"fallback": {
"checks": [
"Mark your maintenance periods",
"Eliminate any misunderstandings",
"Stop sending alerts in maintenance windows"
],
"title": "A maintenance window is used to:",
"actionButton": "Let's create your first maintenance window!"
}
},
"maintenanceWindowDescription": "Your pings won't be sent during this time frame",
"maintenanceWindowName": "Maintenance Window Name",
"maskedPageSpeedKeyPlaceholder": "*************************************",
@@ -498,8 +570,8 @@
"message": "Message",
"monitor": "monitor",
"monitorActions": {
"deleteSuccess": "Monitor deleted successfully",
"deleteFailed": "Failed to delete monitor",
"deleteSuccess": "Monitor deleted successfully",
"details": "Details",
"export": "Export Monitors",
"import": "Import Monitors",
@@ -564,7 +636,8 @@
"Let engineers know when incidents happen",
"Keep administrators informed of system changes"
],
"title": "notification channel"
"title": "A notification channel is used to:",
"actionButton": "Let's create your first notification channel!"
},
"fetch": {
"failed": "Failed to fetch notifications",
@@ -609,6 +682,17 @@
"notifySMS": "Notify via SMS (coming soon)",
"now": "Now",
"os": "OS",
"pageSpeed": {
"fallback": {
"checks": [
"Report on the user experience of a page",
"Help analyze webpage speed",
"Give suggestions on how the page can be improved"
],
"title": "A PageSpeed monitor is used to:",
"actionButton": "Let's create your first PageSpeed monitor!"
}
},
"pageSpeedAddApiKey": "to add your API key.",
"pageSpeedConfigureSettingsDescription": "Here you can select the URL of the host, together with the type of monitor.",
"pageSpeedDetailsPerformanceReport": "Values are estimated and may vary.",
@@ -797,6 +881,14 @@
"pending": "Pending...",
"up": "Your site is up."
},
"statusPage": {
"contents": "Contents",
"createSuccess": "Status page created successfully",
"deleteFailed": "Failed to delete status page",
"deleteSuccess": "Status page deleted successfully",
"generalSettings": "General settings",
"updateSuccess": "Status page updated successfully"
},
"statusPageCreate": {
"buttonSave": "Save"
},
@@ -822,7 +914,16 @@
"createSuccess": "Status page created successfully",
"updateSuccess": "Status page updated successfully",
"generalSettings": "General settings",
"contents": "Contents"
"contents": "Contents",
"fallback": {
"checks": [
"Monitor and display the health of your services in real time",
"Track multiple services and share their status",
"Keep users informed about outages and performance"
],
"title": "A status page is used to:",
"actionButton": "Let's create your first status page!"
}
},
"submit": "Submit",
"teamPanel": {
@@ -851,13 +952,13 @@
"testLocale": "testLocale",
"testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button",
"time": {
"threeMinutes": "3 minutes",
"fiveMinutes": "5 minutes",
"tenMinutes": "10 minutes",
"twentyMinutes": "20 minutes",
"oneHour": "1 hour",
"oneDay": "1 day",
"oneWeek": "1 week"
"oneHour": "1 hour",
"oneWeek": "1 week",
"tenMinutes": "10 minutes",
"threeMinutes": "3 minutes",
"twentyMinutes": "20 minutes"
},
"timeZoneInfo": "All dates and times are in GMT+0 time zone.",
"timezone": "Timezone",
@@ -876,6 +977,18 @@
"ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard."
},
"uptimeMonitor": {
"fallback": {
"checks": [
"Check if websites or servers are online & responsive",
"Alert teams about downtime or performance issues",
"Monitor HTTP endpoints, pings, containers & ports",
"Track historical uptime and reliability trends"
],
"title": "An uptime monitor is used to:",
"actionButton": "Let's create your first uptime monitor!"
}
},
"url": "URL",
"urlMonitor": "URL to monitor",
"used": "Used",
-836
View File
@@ -1,836 +0,0 @@
{
"submit": "Lähetä",
"title": "Otsikko",
"distributedStatusHeaderText": "Reaaliaikainen kattavuus oikeilla laitteilla",
"distributedStatusSubHeaderText": "Miljoonien laitteiden voimin ympäri maailman näet järjestelmän suorituskyvyn maailman alueittain, maittain tai kaupungeittain",
"settingsDisabled": "Poistettu käytöstä",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
"starPromptTitle": "",
"starPromptDescription": "",
"https": "HTTPS",
"http": "HTTP",
"monitor": "Seurain",
"aboutus": "Tietoja meistä",
"now": "Nyt",
"delete": "Poista",
"configure": "Määritä",
"responseTime": "Vasteaika",
"ms": "ms",
"bar": "Palkki",
"area": "Alue",
"country": "MAA",
"city": "KAUPUNKI",
"response": "Vastaus",
"monitorStatusUp": "Valvontakohde {name} ({url}) on nyt toiminnassa ja vastaa",
"monitorStatusDown": "Valvontakohde {name} ({url}) ei ole toiminnassa, eikä vastaa",
"webhookSendSuccess": "Webhook-ilmoitus lähetettiin onnistuneesti",
"webhookSendError": "Virhe lähetettäessä webhook-ilmoitusta palveluun {platform}",
"webhookUnsupportedPlatform": "Ei tuettu palvelu: {platform}",
"distributedRightCategoryTitle": "Valvonta",
"distributedStatusServerMonitors": "Palvelin valvonta",
"distributedStatusServerMonitorsDescription": "Valvo palvelimien tilaa",
"distributedUptimeCreateSelectURL": "Tässä voit valita isäntäkohteen URL-osoitteen ja valvontatyypin.",
"distributedUptimeCreateChecks": "Suoritettavat tarkistukset",
"distributedUptimeCreateChecksDescription": "Voit aina lisätä tai poistaa tarkistuksia sivuston lisäämisen jälkeen.",
"distributedUptimeCreateIncidentNotification": "Häiriöilmoitukset",
"distributedUptimeCreateIncidentDescription": "Jos ilmenee häiriö, ilmoita käyttäjille.",
"distributedUptimeCreateAdvancedSettings": "Lisäasetukset",
"distributedUptimeDetailsNoMonitorHistory": "Tälle valvontakohteelle ei ole vielä tarkistushistoriaa.",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Viimeksi päivitetty",
"notifications": {
"enableNotifications": "",
"testNotification": "Testi-ilmoitus",
"addOrEditNotifications": "Lisää tai muokkaa ilmoituksia",
"slack": {
"label": "Slack",
"description": "",
"webhookLabel": "Webhookin URL",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"discord": {
"label": "Discord",
"description": "",
"webhookLabel": "Discord Webhookin URL-osoite",
"webhookPlaceholder": "https://discord.com/api/webhooks/...",
"webhookRequired": ""
},
"telegram": {
"label": "Telegram",
"description": "",
"tokenLabel": "",
"tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chatIdLabel": "",
"chatIdPlaceholder": "-1001234567890",
"fieldsRequired": ""
},
"webhook": {
"label": "Webhookit",
"description": "",
"urlLabel": "Webhookin URL",
"urlPlaceholder": "https://your-server.com/webhook",
"urlRequired": ""
},
"testNotificationDevelop": "Testi-ilmoitus 2",
"integrationButton": "",
"testSuccess": "Testi ilmoituksen lähetys onnistui!",
"testFailed": "Testi ilmoituksen lähetys epäonnistui",
"unsupportedType": "",
"networkError": "",
"fallback": {
"title": "",
"checks": [""]
},
"createButton": "",
"createTitle": "",
"create": {
"success": "",
"failed": ""
},
"fetch": {
"success": "",
"failed": ""
},
"delete": {
"success": "",
"failed": ""
},
"edit": {
"success": "",
"failed": ""
},
"test": {
"success": "",
"failed": ""
}
},
"testLocale": "",
"add": "Lisää",
"monitors": "Seuraimet",
"distributedUptimeStatusCreateStatusPage": "",
"distributedUptimeStatusCreateStatusPageAccess": "",
"distributedUptimeStatusCreateStatusPageReady": "",
"distributedUptimeStatusBasicInfoHeader": "",
"distributedUptimeStatusBasicInfoDescription": "",
"distributedUptimeStatusLogoHeader": "Logo",
"distributedUptimeStatusLogoDescription": "",
"distributedUptimeStatusLogoUploadButton": "",
"distributedUptimeStatusStandardMonitorsHeader": "",
"distributedUptimeStatusStandardMonitorsDescription": "",
"distributedUptimeStatusCreateYour": "",
"distributedUptimeStatusEditYour": "",
"distributedUptimeStatusPublishedLabel": "",
"distributedUptimeStatusCompanyNameLabel": "Yrityksen nimi",
"distributedUptimeStatusPageAddressLabel": "",
"distributedUptimeStatus30Days": "30 päivää",
"distributedUptimeStatus60Days": "60 päivää",
"distributedUptimeStatus90Days": "90 päivää",
"distributedUptimeStatusPageNotSetUp": "",
"distributedUptimeStatusContactAdmin": "Ota yhteys järjestelmävalvojaasi",
"distributedUptimeStatusPageNotPublic": "",
"distributedUptimeStatusPageDeleteDialog": "",
"distributedUptimeStatusPageDeleteConfirm": "",
"distributedUptimeStatusPageDeleteDescription": "",
"distributedUptimeStatusDevices": "",
"distributedUptimeStatusUpt": "",
"distributedUptimeStatusUptBurned": "",
"distributedUptimeStatusUptLogo": "UPT logo",
"incidentsTableNoIncidents": "",
"incidentsTablePaginationLabel": "",
"incidentsTableMonitorName": "",
"incidentsTableStatus": "Tila",
"incidentsTableDateTime": "Päivämäärä ja aika",
"incidentsTableStatusCode": "",
"incidentsTableMessage": "Viesti",
"incidentsOptionsHeader": "",
"incidentsOptionsHeaderFilterBy": "",
"incidentsOptionsHeaderFilterAll": "Kaikki",
"incidentsOptionsHeaderFilterDown": "Alhaalla",
"incidentsOptionsHeaderFilterCannotResolve": "",
"incidentsOptionsHeaderShow": "Näytä:",
"incidentsOptionsHeaderLastHour": "Viime tunti",
"incidentsOptionsHeaderLastDay": "Viime päivä",
"incidentsOptionsHeaderLastWeek": "Viime viikko",
"incidentsOptionsPlaceholderAllServers": "Kaikki palvelimet",
"infrastructureCreateYour": "",
"infrastructureCreateGeneralSettingsDescription": "",
"infrastructureServerRequirement": "",
"infrastructureCustomizeAlerts": "",
"infrastructureAlertNotificationDescription": "",
"infrastructureCreateMonitor": "",
"infrastructureProtocol": "Protokolla",
"infrastructureServerUrlLabel": "Palvelimen URL-osoite",
"infrastructureDisplayNameLabel": "",
"infrastructureAuthorizationSecretLabel": "",
"gb": "",
"mb": "",
"mem": "",
"memoryUsage": "Muistinkäyttö",
"cpu": "",
"cpuUsage": "",
"cpuTemperature": "",
"diskUsage": "",
"used": "",
"total": "",
"cores": "",
"frequency": "",
"status": "Tila",
"cpuPhysical": "",
"cpuLogical": "",
"cpuFrequency": "",
"avgCpuTemperature": "",
"memory": "Muisti",
"disk": "",
"uptime": "",
"os": "",
"host": "",
"actions": "",
"integrations": "",
"integrationsPrism": "",
"integrationsSlack": "Slack",
"integrationsSlackInfo": "",
"integrationsDiscord": "",
"integrationsDiscordInfo": "",
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "",
"commonSave": "Tallenna",
"createYour": "",
"createMonitor": "",
"pause": "",
"resume": "",
"editing": "",
"url": "",
"access": "",
"timezone": "Aikavyöhyke",
"features": "",
"administrator": "",
"loginHere": "",
"displayName": "",
"urlMonitor": "",
"portToMonitor": "",
"websiteMonitoring": "",
"websiteMonitoringDescription": "",
"pingMonitoring": "",
"pingMonitoringDescription": "",
"dockerContainerMonitoring": "",
"dockerContainerMonitoringDescription": "",
"portMonitoring": "",
"portMonitoringDescription": "",
"createMaintenanceWindow": "",
"createMaintenance": "",
"editMaintenance": "",
"maintenanceWindowName": "",
"friendlyNameInput": "",
"friendlyNamePlaceholder": "",
"maintenanceRepeat": "",
"maintenance": "",
"duration": "",
"addMonitors": "",
"window": "",
"cancel": "Peruuttaa",
"message": "Viesti",
"low": "",
"high": "",
"statusCode": "Tilakoodi",
"date&Time": "Päivämäärä ja aika",
"type": "",
"statusPageName": "",
"publicURL": "Julkinen URL-osoite",
"repeat": "",
"edit": "Muokkaa",
"createA": "",
"remove": "",
"maintenanceWindowDescription": "",
"startTime": "",
"timeZoneInfo": "",
"monitorsToApply": "",
"nextWindow": "",
"notFoundButton": "",
"pageSpeedConfigureSettingsDescription": "",
"monitorDisplayName": "",
"whenNewIncident": "",
"notifySMS": "Ilmoita tekstiviestillä (tulossa pian)",
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
"deleteDialogDescription": "",
"pageSpeedMonitor": "",
"shown": "",
"ago": "sitten",
"companyName": "Yrityksen nimi",
"pageSpeedDetailsPerformanceReport": "",
"pageSpeedDetailsPerformanceReportCalculator": "",
"checkingEvery": "",
"statusPageCreateSettings": "",
"basicInformation": "",
"statusPageCreateBasicInfoDescription": "",
"statusPageCreateSelectTimeZoneDescription": "",
"statusPageCreateAppearanceDescription": "",
"statusPageCreateSettingsCheckboxLabel": "",
"statusPageCreateBasicInfoStatusPageAddress": "",
"statusPageCreateTabsContent": "",
"statusPageCreateTabsContentDescription": "",
"statusPageCreateTabsContentFeaturesDescription": "",
"showCharts": "Näytä kaaviot",
"showUptimePercentage": "",
"removeLogo": "Poista logo",
"statusPageStatus": "",
"statusPageStatusContactAdmin": "Ota yhteyttä järjestelmänvalvojaan",
"statusPageStatusNotPublic": "",
"statusPageStatusNoPage": "",
"statusPageStatusServiceStatus": "",
"deleteStatusPage": "",
"deleteStatusPageConfirm": "",
"deleteStatusPageDescription": "",
"uptimeCreate": "",
"uptimeCreateJsonPath": "",
"uptimeCreateJsonPathQuery": "",
"maintenanceTableActionMenuDialogTitle": "",
"infrastructureEditYour": "Muokkaa omaa",
"infrastructureEditMonitor": "",
"infrastructureMonitorCreated": "",
"infrastructureMonitorUpdated": "",
"errorInvalidTypeId": "",
"errorInvalidFieldId": "",
"inviteNoTokenFound": "",
"pageSpeedWarning": "",
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": "",
"update": "Päivitä",
"invalidFileFormat": "",
"invalidFileSize": "Tiedoston koko on liian suuri!",
"ClickUpload": "",
"DragandDrop": "raahaa ja pudota",
"MaxSize": "Enimmäiskoko",
"SupportedFormats": "Tuetut muodot",
"FirstName": "Etunimi",
"LastName": "Sukunimi",
"EmailDescriptionText": "Tämä on nykyinen sähköpostiosoitteesi — sitä ei voi vaihtaa.",
"YourPhoto": "Profiilikuva",
"PhotoDescriptionText": "",
"save": "Tallenna",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?",
"bulkImport": {
"title": "",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "Valitse tiedosto",
"parsingFailed": "",
"uploadSuccess": "",
"validationFailed": "",
"noFileSelected": "Ei valittua tiedostoa",
"fallbackPage": ""
},
"DeleteAccountTitle": "Poista tili",
"DeleteAccountButton": "Poista tili",
"publicLink": "Julkinen linkki",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"reset": "Palauta",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
"ignoreTLSErrorDescription": "",
"createNew": "Luo uusi",
"greeting": {
"prepend": "Hei",
"append": "",
"overview": ""
},
"roles": {
"superAdmin": "Pääylläpitäjä",
"admin": "Ylläpitäjä",
"teamMember": "Tiimin jäsen",
"demoUser": "Demokäyttäjä"
},
"teamPanel": {
"teamMembers": "Tiimin jäsenet",
"filter": {
"all": "Kaikki",
"member": "Jäsen"
},
"inviteTeamMember": "",
"inviteNewTeamMember": "",
"inviteDescription": "",
"email": "Sähköposti",
"selectRole": "Valitse rooli",
"inviteLink": "Kutsulinkki",
"cancel": "Peruuttaa",
"noMembers": "",
"getToken": "Hae tunniste",
"emailToken": "Sähköpostitunnus",
"table": {
"name": "Nimi",
"email": "Sähköposti",
"role": "Rooli",
"created": "Luotu"
}
},
"monitorState": {
"paused": "Tauota",
"resumed": "Jatka",
"active": ""
},
"menu": {
"uptime": "",
"pagespeed": "",
"infrastructure": "Infrastruktuuri",
"incidents": "",
"statusPages": "Tilasivut",
"maintenance": "Huolto",
"integrations": "",
"settings": "Asetukset",
"support": "Tuki",
"discussions": "Keskustelut",
"docs": "Dokumentit",
"changelog": "Muutosloki",
"profile": "Profiili",
"password": "Salasana",
"team": "Tiimi",
"logOut": "Kirjaudu ulos",
"notifications": "",
"logs": ""
},
"settingsEmailUser": "",
"state": "Tila",
"statusBreadCrumbsStatusPages": "Tilasivut",
"statusBreadCrumbsDetails": "Tiedot",
"commonSaving": "Tallennetaan...",
"navControls": "Ohjaimet",
"incidentsPageTitle": "Häiriöt",
"passwordPanel": {
"passwordChangedSuccess": "Salasanasi vaihdettiin onnistuneesti.",
"passwordInputIncorrect": "",
"currentPassword": "Nykyinen salasana",
"enterCurrentPassword": "Kirjoita nykyinen salasanasi",
"newPassword": "Uusi salasana",
"enterNewPassword": "Kirjoita uusi salasana",
"confirmNewPassword": "Vahvista uusi salasana",
"passwordRequirements": "Uuden salasanan on oltava vähintään 8 merkkiä pitkä ja sen on sisältävä vähintään yksi iso kirjain, yksi pieni kirjain, yksi numero sekä yksi erikoismerkki.",
"saving": "Tallennetaan..."
},
"emailSent": "Sähköpostin lähetys onnistui",
"failedToSendEmail": "Sähköpostin lähetys epäonnistui",
"settingsTestEmailSuccess": "Testisähköpostin lähetys onnistui",
"settingsTestEmailFailed": "Testisähköpostin lähetys epäonnistui",
"settingsTestEmailFailedWithReason": "Testisähköpostin lähetys epäonnistui: {{reason}}",
"settingsTestEmailUnknownError": "Tuntematon virhe",
"statusMsg": {
"paused": "Valvonta on keskeytetty.",
"up": "Sivusi on ylhäällä.",
"down": "Sivusi on alhaalla.",
"pending": "Odottaa..."
},
"uptimeGeneralInstructions": {
"http": "",
"ping": "",
"docker": "",
"port": ""
},
"common": {
"appName": "",
"monitoringAgentName": "",
"buttons": {
"toggleTheme": ""
},
"toasts": {
"networkError": "",
"checkConnection": "",
"unknownError": ""
}
},
"auth": {
"common": {
"navigation": {
"continue": "",
"back": ""
},
"inputs": {
"email": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"invalid": ""
}
},
"password": {
"label": "",
"rules": {
"length": {
"beginning": "",
"highlighted": ""
},
"special": {
"beginning": "",
"highlighted": ""
},
"number": {
"beginning": "",
"highlighted": ""
},
"uppercase": {
"beginning": "",
"highlighted": ""
},
"lowercase": {
"beginning": "",
"highlighted": ""
},
"match": {
"beginning": "",
"highlighted": ""
}
},
"errors": {
"empty": "",
"length": "",
"uppercase": "",
"lowercase": "",
"number": "",
"special": "",
"incorrect": ""
}
},
"passwordConfirm": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"different": ""
}
},
"firstName": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"length": "",
"pattern": ""
}
},
"lastName": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"length": "",
"pattern": ""
}
}
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
"heading": "",
"subheadings": {
"stepOne": "",
"stepTwo": ""
},
"links": {
"forgotPassword": "",
"register": "",
"forgotPasswordLink": "",
"registerLink": ""
},
"toasts": {
"success": "",
"incorrectPassword": ""
},
"errors": {
"password": {
"incorrect": ""
}
}
},
"registration": {
"heading": {
"superAdmin": "",
"user": ""
},
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": ""
},
"description": {
"superAdmin": "",
"user": ""
},
"gettingStartedButton": {
"superAdmin": "",
"user": ""
},
"termsAndPolicies": "",
"links": {
"login": ""
},
"toasts": {
"success": ""
}
},
"forgotPassword": {
"heading": "",
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": "",
"stepFour": ""
},
"buttons": {
"openEmail": "",
"resetPassword": ""
},
"imageAlts": {
"passwordKey": "",
"email": "",
"lock": "",
"passwordConfirm": ""
},
"links": {
"login": "",
"resend": ""
},
"toasts": {
"sent": "",
"emailNotFound": "",
"redirect": "",
"success": "",
"error": ""
}
}
},
"errorPages": {
"serverUnreachable": {
"toasts": {
"reconnected": "",
"stillUnreachable": ""
},
"alertBox": "",
"description": "",
"retryButton": {
"default": "",
"processing": ""
}
}
},
"createNotifications": {
"title": "",
"nameSettings": {
"title": "",
"description": "",
"nameLabel": "",
"namePlaceholder": ""
},
"typeSettings": {
"title": "",
"description": "",
"typeLabel": ""
},
"emailSettings": {
"title": "",
"description": "",
"emailLabel": "",
"emailPlaceholder": ""
},
"slackSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"pagerdutySettings": {
"title": "",
"description": "",
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
}
},
"notificationConfig": {
"title": "",
"description": ""
},
"monitorStatus": {
"checkingEvery": "",
"withCaptureAgent": "",
"up": "",
"down": "",
"paused": ""
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
"title": "",
"description": "",
"tabs": {
"queue": "",
"logs": ""
},
"toast": {
"fetchLogsSuccess": ""
},
"logLevelSelect": {
"title": "",
"values": {
"all": "",
"info": "",
"warn": "",
"error": "",
"debug": ""
}
}
},
"queuePage": {
"title": "",
"refreshButton": "",
"flushButton": "",
"jobTable": {
"title": "",
"idHeader": "",
"urlHeader": "",
"typeHeader": "",
"activeHeader": "",
"lockedAtHeader": "",
"runCountHeader": "",
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
"lastRunTookHeader": ""
},
"metricsTable": {
"title": "",
"metricHeader": "",
"valueHeader": ""
},
"failedJobTable": {
"title": "",
"monitorIdHeader": "",
"monitorUrlHeader": "",
"failCountHeader": "",
"failedAtHeader": "",
"failReasonHeader": ""
}
},
"export": {
"title": "",
"success": "",
"failed": ""
},
"monitorActions": {
"title": "",
"import": "",
"export": ""
},
"settingsPage": {
"aboutSettings": {
"labelDevelopedBy": "",
"labelVersion": "",
"title": ""
},
"demoMonitorsSettings": {
"buttonAddMonitors": "",
"description": "",
"title": ""
},
"emailSettings": {
"buttonSendTestEmail": "",
"description": "",
"descriptionTransport": "",
"labelAddress": "",
"labelConnectionHost": "",
"labelHost": "",
"labelIgnoreTLS": "",
"labelPassword": "",
"labelPasswordSet": "",
"labelPool": "",
"labelPort": "",
"labelRejectUnauthorized": "",
"labelRequireTLS": "",
"labelSecure": "",
"labelTLSServername": "",
"labelUser": "",
"linkTransport": "",
"placeholderUser": "",
"title": ""
},
"pageSpeedSettings": {
"description": "",
"labelApiKeySet": "",
"labelApiKey": "",
"title": ""
},
"saveButtonLabel": "",
"statsSettings": {
"clearAllStatsButton": "",
"clearAllStatsDescription": "",
"clearAllStatsDialogConfirm": "",
"clearAllStatsDialogDescription": "",
"clearAllStatsDialogTitle": "",
"description": "",
"labelTTL": "",
"labelTTLOptional": "",
"title": ""
},
"systemResetSettings": {
"buttonRemoveAllMonitors": "",
"description": "",
"dialogConfirm": "",
"dialogDescription": "",
"dialogTitle": "",
"title": ""
},
"timezoneSettings": {
"description": "",
"label": "",
"title": ""
},
"title": "",
"uiSettings": {
"description": "",
"labelLanguage": "",
"labelTheme": "",
"title": ""
},
"urlSettings": {
"description": "",
"label": "",
"selectDisabled": "",
"selectEnabled": "",
"title": ""
}
}
}
-836
View File
@@ -1,836 +0,0 @@
{
"submit": "Envoyer",
"title": "Titre",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsDisabled": "Désactivé",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
"starPromptTitle": "",
"starPromptDescription": "",
"https": "",
"http": "",
"monitor": "",
"aboutus": "A propos de nous",
"now": "",
"delete": "Supprimer",
"configure": "",
"responseTime": "",
"ms": "",
"bar": "",
"area": "Zone",
"country": "PAYS",
"city": "VILLE",
"response": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
"webhookSendError": "",
"webhookUnsupportedPlatform": "",
"distributedRightCategoryTitle": "Moniteur",
"distributedStatusServerMonitors": "",
"distributedStatusServerMonitorsDescription": "",
"distributedUptimeCreateSelectURL": "",
"distributedUptimeCreateChecks": "",
"distributedUptimeCreateChecksDescription": "",
"distributedUptimeCreateIncidentNotification": "Notifications d'incidents",
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "Paramètres avancés",
"distributedUptimeDetailsNoMonitorHistory": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
"enableNotifications": "",
"testNotification": "Notification de test",
"addOrEditNotifications": "Ajouter ou éditer des notifications",
"slack": {
"label": "",
"description": "",
"webhookLabel": "URL du webhook",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"discord": {
"label": "",
"description": "",
"webhookLabel": "URL du webhook Discord",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"telegram": {
"label": "",
"description": "",
"tokenLabel": "",
"tokenPlaceholder": "",
"chatIdLabel": "Votre Chat ID",
"chatIdPlaceholder": "-1001234567890",
"fieldsRequired": ""
},
"webhook": {
"label": "",
"description": "",
"urlLabel": "URL du webhook",
"urlPlaceholder": "https://votre-serveur.fr/webhook",
"urlRequired": "L'URL du webhook est nécessaire"
},
"testNotificationDevelop": "Notification de test 2",
"integrationButton": "Intégration de notification",
"testSuccess": "La notification de test a été envoyée avec succès !",
"testFailed": "Échec de l'envoi de la notification de test",
"unsupportedType": "",
"networkError": "",
"fallback": {
"title": "",
"checks": [""]
},
"createButton": "",
"createTitle": "",
"create": {
"success": "",
"failed": ""
},
"fetch": {
"success": "",
"failed": ""
},
"delete": {
"success": "",
"failed": ""
},
"edit": {
"success": "",
"failed": ""
},
"test": {
"success": "",
"failed": ""
}
},
"testLocale": "",
"add": "Ajouter",
"monitors": "",
"distributedUptimeStatusCreateStatusPage": "page de statut",
"distributedUptimeStatusCreateStatusPageAccess": "Accès",
"distributedUptimeStatusCreateStatusPageReady": "",
"distributedUptimeStatusBasicInfoHeader": "",
"distributedUptimeStatusBasicInfoDescription": "",
"distributedUptimeStatusLogoHeader": "Logo",
"distributedUptimeStatusLogoDescription": "",
"distributedUptimeStatusLogoUploadButton": "Téléverser un logo",
"distributedUptimeStatusStandardMonitorsHeader": "",
"distributedUptimeStatusStandardMonitorsDescription": "",
"distributedUptimeStatusCreateYour": "",
"distributedUptimeStatusEditYour": "",
"distributedUptimeStatusPublishedLabel": "",
"distributedUptimeStatusCompanyNameLabel": "Nom de l'entreprise",
"distributedUptimeStatusPageAddressLabel": "Adresse de votre page de statut",
"distributedUptimeStatus30Days": "30 jours",
"distributedUptimeStatus60Days": "60 jours",
"distributedUptimeStatus90Days": "90 jours",
"distributedUptimeStatusPageNotSetUp": "",
"distributedUptimeStatusContactAdmin": "Veuillez contacter votre administrateur",
"distributedUptimeStatusPageNotPublic": "Cette page de statut n'est pas publique.",
"distributedUptimeStatusPageDeleteDialog": "Voulez-vous supprimer cette page de statut ?",
"distributedUptimeStatusPageDeleteConfirm": "Oui, supprimer la page de statut",
"distributedUptimeStatusPageDeleteDescription": "Supprimer votre page de statut est irréversible.",
"distributedUptimeStatusDevices": "",
"distributedUptimeStatusUpt": "",
"distributedUptimeStatusUptBurned": "",
"distributedUptimeStatusUptLogo": "",
"incidentsTableNoIncidents": "",
"incidentsTablePaginationLabel": "incidents",
"incidentsTableMonitorName": "",
"incidentsTableStatus": "",
"incidentsTableDateTime": "Date et heure",
"incidentsTableStatusCode": "",
"incidentsTableMessage": "Message",
"incidentsOptionsHeader": "",
"incidentsOptionsHeaderFilterBy": "",
"incidentsOptionsHeaderFilterAll": "",
"incidentsOptionsHeaderFilterDown": "",
"incidentsOptionsHeaderFilterCannotResolve": "",
"incidentsOptionsHeaderShow": "",
"incidentsOptionsHeaderLastHour": "Dernière heure",
"incidentsOptionsHeaderLastDay": "Dernier jour",
"incidentsOptionsHeaderLastWeek": "Dernière semaine",
"incidentsOptionsPlaceholderAllServers": "",
"infrastructureCreateYour": "",
"infrastructureCreateGeneralSettingsDescription": "",
"infrastructureServerRequirement": "",
"infrastructureCustomizeAlerts": "",
"infrastructureAlertNotificationDescription": "",
"infrastructureCreateMonitor": "",
"infrastructureProtocol": "Protocole",
"infrastructureServerUrlLabel": "URL du serveur",
"infrastructureDisplayNameLabel": "",
"infrastructureAuthorizationSecretLabel": "",
"gb": "",
"mb": "",
"mem": "",
"memoryUsage": "",
"cpu": "CPU",
"cpuUsage": "",
"cpuTemperature": "",
"diskUsage": "Utilisation du disque",
"used": "",
"total": "",
"cores": "",
"frequency": "",
"status": "",
"cpuPhysical": "",
"cpuLogical": "",
"cpuFrequency": "Fréquence CPU",
"avgCpuTemperature": "Température moyenne du CPU",
"memory": "",
"disk": "Disque",
"uptime": "",
"os": "",
"host": "",
"actions": "Actions",
"integrations": "Intégrations",
"integrationsPrism": "Connecter Prism à votre service préféré.",
"integrationsSlack": "Slack",
"integrationsSlackInfo": "Connecter à Slack et voir les incidents dans un canal",
"integrationsDiscord": "Discord",
"integrationsDiscordInfo": "Connecter à Discord et voir les incidents dans un canal",
"integrationsZapier": "Zapier",
"integrationsZapierInfo": "Envoyer tous les incidents à Zapier",
"commonSave": "Sauvegarder",
"createYour": "Créez votre",
"createMonitor": "",
"pause": "",
"resume": "",
"editing": "",
"url": "URL",
"access": "Accès",
"timezone": "Fuseau horaire",
"features": "",
"administrator": "Administrateur ?",
"loginHere": "",
"displayName": "Nom d'affichage",
"urlMonitor": "URL à suivre",
"portToMonitor": "",
"websiteMonitoring": "",
"websiteMonitoringDescription": "",
"pingMonitoring": "",
"pingMonitoringDescription": "",
"dockerContainerMonitoring": "",
"dockerContainerMonitoringDescription": "",
"portMonitoring": "",
"portMonitoringDescription": "",
"createMaintenanceWindow": "",
"createMaintenance": "Créer une maintenance",
"editMaintenance": "",
"maintenanceWindowName": "",
"friendlyNameInput": "",
"friendlyNamePlaceholder": "",
"maintenanceRepeat": "",
"maintenance": "",
"duration": "",
"addMonitors": "Ajouter des moniteurs",
"window": "",
"cancel": "Annuler",
"message": "",
"low": "",
"high": "",
"statusCode": "",
"date&Time": "Date et heure",
"type": "",
"statusPageName": "",
"publicURL": "",
"repeat": "",
"edit": "",
"createA": "Créer un",
"remove": "",
"maintenanceWindowDescription": "",
"startTime": "",
"timeZoneInfo": "",
"monitorsToApply": "",
"nextWindow": "",
"notFoundButton": "",
"pageSpeedConfigureSettingsDescription": "",
"monitorDisplayName": "",
"whenNewIncident": "Lors d'un nouvel incident,",
"notifySMS": "",
"notifyEmails": "",
"seperateEmails": "",
"checkFrequency": "Vérifier la fréquence",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
"deleteDialogDescription": "",
"pageSpeedMonitor": "",
"shown": "",
"ago": "depuis",
"companyName": "",
"pageSpeedDetailsPerformanceReport": "",
"pageSpeedDetailsPerformanceReportCalculator": "",
"checkingEvery": "Vérification tous les",
"statusPageCreateSettings": "",
"basicInformation": "",
"statusPageCreateBasicInfoDescription": "",
"statusPageCreateSelectTimeZoneDescription": "",
"statusPageCreateAppearanceDescription": "",
"statusPageCreateSettingsCheckboxLabel": "",
"statusPageCreateBasicInfoStatusPageAddress": "",
"statusPageCreateTabsContent": "",
"statusPageCreateTabsContentDescription": "",
"statusPageCreateTabsContentFeaturesDescription": "",
"showCharts": "",
"showUptimePercentage": "",
"removeLogo": "",
"statusPageStatus": "",
"statusPageStatusContactAdmin": "",
"statusPageStatusNotPublic": "",
"statusPageStatusNoPage": "",
"statusPageStatusServiceStatus": "",
"deleteStatusPage": "",
"deleteStatusPageConfirm": "Oui, supprimer la page de statut",
"deleteStatusPageDescription": "",
"uptimeCreate": "",
"uptimeCreateJsonPath": "",
"uptimeCreateJsonPathQuery": "",
"maintenanceTableActionMenuDialogTitle": "",
"infrastructureEditYour": "",
"infrastructureEditMonitor": "",
"infrastructureMonitorCreated": "",
"infrastructureMonitorUpdated": "",
"errorInvalidTypeId": "",
"errorInvalidFieldId": "",
"inviteNoTokenFound": "",
"pageSpeedWarning": "",
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": "",
"update": "",
"invalidFileFormat": "Format de fichier non supporté !",
"invalidFileSize": "Le fichier est trop volumineux !",
"ClickUpload": "Cliquez pour téléverser",
"DragandDrop": "",
"MaxSize": "",
"SupportedFormats": "",
"FirstName": "",
"LastName": "",
"EmailDescriptionText": "",
"YourPhoto": "Photo de profil",
"PhotoDescriptionText": "",
"save": "",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Vous supprimez vraiment ce compte ?",
"bulkImport": {
"title": "",
"selectFileTips": "",
"selectFileDescription": "",
"selectFile": "",
"parsingFailed": "",
"uploadSuccess": "",
"validationFailed": "Echec de la validation",
"noFileSelected": "",
"fallbackPage": ""
},
"DeleteAccountTitle": "Supprimer le compte",
"DeleteAccountButton": "Supprimer le compte",
"publicLink": "",
"maskedPageSpeedKeyPlaceholder": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
"ignoreTLSErrorDescription": "",
"createNew": "Créer un nouveau",
"greeting": {
"prepend": "",
"append": "L'après-midi est votre libre, rendons-le épique !",
"overview": ""
},
"roles": {
"superAdmin": "",
"admin": "Admin",
"teamMember": "",
"demoUser": ""
},
"teamPanel": {
"teamMembers": "Membres de l'équipe",
"filter": {
"all": "Tous",
"member": ""
},
"inviteTeamMember": "",
"inviteNewTeamMember": "",
"inviteDescription": "",
"email": "Email",
"selectRole": "",
"inviteLink": "Lien d'invitation",
"cancel": "Annuler",
"noMembers": "",
"getToken": "",
"emailToken": "",
"table": {
"name": "",
"email": "Email",
"role": "",
"created": "Créée"
}
},
"monitorState": {
"paused": "",
"resumed": "",
"active": "Actif"
},
"menu": {
"uptime": "",
"pagespeed": "",
"infrastructure": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
"integrations": "Intégrations",
"settings": "",
"support": "",
"discussions": "Discussions",
"docs": "",
"changelog": "Changelog",
"profile": "",
"password": "",
"team": "",
"logOut": "Déconnexion",
"notifications": "",
"logs": ""
},
"settingsEmailUser": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
"commonSaving": "Sauvegarde...",
"navControls": "",
"incidentsPageTitle": "Incidents",
"passwordPanel": {
"passwordChangedSuccess": "Votre mot de passe a été modifié avec succès.",
"passwordInputIncorrect": "La saisie de votre mot de passe est incorrecte.",
"currentPassword": "Mot de passe actuel",
"enterCurrentPassword": "Entrez votre mot de passe actuel",
"newPassword": "Nouveau mot de passe",
"enterNewPassword": "Entrez votre nouveau mot de passe",
"confirmNewPassword": "Confirmer le nouveau mot de passe",
"passwordRequirements": "Le nouveau mot de passe doit contenir au moins 8 caractères et au moins une lettre majuscule, une lettre minuscule, un chiffre et un caractère spécial.",
"saving": "Sauvegarde..."
},
"emailSent": "",
"failedToSendEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "Erreur inconnue",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
"uptimeGeneralInstructions": {
"http": "",
"ping": "",
"docker": "",
"port": ""
},
"common": {
"appName": "",
"monitoringAgentName": "",
"buttons": {
"toggleTheme": ""
},
"toasts": {
"networkError": "",
"checkConnection": "",
"unknownError": ""
}
},
"auth": {
"common": {
"navigation": {
"continue": "",
"back": ""
},
"inputs": {
"email": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"invalid": ""
}
},
"password": {
"label": "",
"rules": {
"length": {
"beginning": "",
"highlighted": ""
},
"special": {
"beginning": "",
"highlighted": ""
},
"number": {
"beginning": "",
"highlighted": ""
},
"uppercase": {
"beginning": "",
"highlighted": ""
},
"lowercase": {
"beginning": "",
"highlighted": ""
},
"match": {
"beginning": "",
"highlighted": ""
}
},
"errors": {
"empty": "",
"length": "",
"uppercase": "",
"lowercase": "",
"number": "",
"special": "",
"incorrect": ""
}
},
"passwordConfirm": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"different": ""
}
},
"firstName": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"length": "",
"pattern": ""
}
},
"lastName": {
"label": "",
"placeholder": "",
"errors": {
"empty": "",
"length": "",
"pattern": ""
}
}
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
"heading": "",
"subheadings": {
"stepOne": "",
"stepTwo": ""
},
"links": {
"forgotPassword": "",
"register": "",
"forgotPasswordLink": "",
"registerLink": ""
},
"toasts": {
"success": "",
"incorrectPassword": ""
},
"errors": {
"password": {
"incorrect": ""
}
}
},
"registration": {
"heading": {
"superAdmin": "",
"user": ""
},
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": ""
},
"description": {
"superAdmin": "",
"user": ""
},
"gettingStartedButton": {
"superAdmin": "",
"user": ""
},
"termsAndPolicies": "",
"links": {
"login": ""
},
"toasts": {
"success": ""
}
},
"forgotPassword": {
"heading": "",
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": "",
"stepFour": ""
},
"buttons": {
"openEmail": "",
"resetPassword": ""
},
"imageAlts": {
"passwordKey": "",
"email": "",
"lock": "",
"passwordConfirm": ""
},
"links": {
"login": "",
"resend": ""
},
"toasts": {
"sent": "",
"emailNotFound": "",
"redirect": "",
"success": "",
"error": ""
}
}
},
"errorPages": {
"serverUnreachable": {
"toasts": {
"reconnected": "",
"stillUnreachable": ""
},
"alertBox": "",
"description": "",
"retryButton": {
"default": "",
"processing": ""
}
}
},
"createNotifications": {
"title": "",
"nameSettings": {
"title": "",
"description": "",
"nameLabel": "",
"namePlaceholder": ""
},
"typeSettings": {
"title": "",
"description": "",
"typeLabel": ""
},
"emailSettings": {
"title": "",
"description": "",
"emailLabel": "",
"emailPlaceholder": ""
},
"slackSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"pagerdutySettings": {
"title": "",
"description": "",
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
}
},
"notificationConfig": {
"title": "",
"description": ""
},
"monitorStatus": {
"checkingEvery": "",
"withCaptureAgent": "",
"up": "",
"down": "",
"paused": ""
},
"advancedMatching": "",
"sendTestNotifications": "",
"testNotificationsDisabled": "",
"selectAll": "",
"showAdminLoginLink": "",
"logsPage": {
"title": "",
"description": "",
"tabs": {
"queue": "",
"logs": ""
},
"toast": {
"fetchLogsSuccess": ""
},
"logLevelSelect": {
"title": "",
"values": {
"all": "",
"info": "",
"warn": "",
"error": "",
"debug": ""
}
}
},
"queuePage": {
"title": "",
"refreshButton": "",
"flushButton": "",
"jobTable": {
"title": "",
"idHeader": "",
"urlHeader": "",
"typeHeader": "",
"activeHeader": "",
"lockedAtHeader": "",
"runCountHeader": "",
"failCountHeader": "",
"lastRunHeader": "",
"lastFinishedAtHeader": "",
"lastRunTookHeader": ""
},
"metricsTable": {
"title": "",
"metricHeader": "",
"valueHeader": ""
},
"failedJobTable": {
"title": "",
"monitorIdHeader": "",
"monitorUrlHeader": "",
"failCountHeader": "",
"failedAtHeader": "",
"failReasonHeader": ""
}
},
"export": {
"title": "",
"success": "",
"failed": ""
},
"monitorActions": {
"title": "",
"import": "",
"export": ""
},
"settingsPage": {
"aboutSettings": {
"labelDevelopedBy": "",
"labelVersion": "",
"title": ""
},
"demoMonitorsSettings": {
"buttonAddMonitors": "",
"description": "",
"title": ""
},
"emailSettings": {
"buttonSendTestEmail": "",
"description": "",
"descriptionTransport": "",
"labelAddress": "",
"labelConnectionHost": "",
"labelHost": "",
"labelIgnoreTLS": "",
"labelPassword": "",
"labelPasswordSet": "",
"labelPool": "",
"labelPort": "",
"labelRejectUnauthorized": "",
"labelRequireTLS": "",
"labelSecure": "",
"labelTLSServername": "",
"labelUser": "",
"linkTransport": "",
"placeholderUser": "",
"title": ""
},
"pageSpeedSettings": {
"description": "",
"labelApiKeySet": "",
"labelApiKey": "",
"title": ""
},
"saveButtonLabel": "",
"statsSettings": {
"clearAllStatsButton": "",
"clearAllStatsDescription": "",
"clearAllStatsDialogConfirm": "",
"clearAllStatsDialogDescription": "",
"clearAllStatsDialogTitle": "",
"description": "",
"labelTTL": "",
"labelTTLOptional": "",
"title": ""
},
"systemResetSettings": {
"buttonRemoveAllMonitors": "",
"description": "",
"dialogConfirm": "",
"dialogDescription": "",
"dialogTitle": "",
"title": ""
},
"timezoneSettings": {
"description": "",
"label": "",
"title": ""
},
"title": "",
"uiSettings": {
"description": "",
"labelLanguage": "",
"labelTheme": "",
"title": ""
},
"urlSettings": {
"description": "",
"label": "",
"selectDisabled": "",
"selectEnabled": "",
"title": ""
}
}
}
File renamed without changes.
@@ -1,6 +1,6 @@
{
"submit": "Enviar",
"title": "Titulo",
"submit": "",
"title": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsDisabled": "",
@@ -1,11 +1,11 @@
{
"submit": "Odeslat",
"submit": "",
"title": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsDisabled": "Vypnuto",
"settingsSuccessSaved": "Nastavení bylo úspěšně uloženo",
"settingsFailedToSave": "Nepodařilo se uložit nastavení",
"settingsDisabled": "",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
"settingsFailedToAddDemoMonitors": "",
@@ -438,99 +438,99 @@
"port": ""
},
"common": {
"appName": "Checkmate",
"monitoringAgentName": "Capture",
"appName": "",
"monitoringAgentName": "",
"buttons": {
"toggleTheme": "Přepnout mezi světlým a tmavým motivem"
"toggleTheme": ""
},
"toasts": {
"networkError": "Chyba připojení k síti",
"checkConnection": "Zkontrolujte prosím své připojení k síti",
"unknownError": "Nastala neznámá chyba"
"networkError": "",
"checkConnection": "",
"unknownError": ""
}
},
"auth": {
"common": {
"navigation": {
"continue": "Pokračovat",
"back": "Zpět"
"continue": "",
"back": ""
},
"inputs": {
"email": {
"label": "E-mail",
"placeholder": "jan.novak@domena.cz",
"label": "",
"placeholder": "",
"errors": {
"empty": "Zadejte prosím e-mailovou adresu",
"invalid": "Překontrolujte si prosím správnost zadané e-mailové adresy"
"empty": "",
"invalid": ""
}
},
"password": {
"label": "Heslo",
"label": "",
"rules": {
"length": {
"beginning": "Heslo musí být dlouhé alespoň",
"highlighted": "8 znaků"
"beginning": "",
"highlighted": ""
},
"special": {
"beginning": "Heslo musí obsahovat alespoň",
"highlighted": "1 speciální znak"
"beginning": "",
"highlighted": ""
},
"number": {
"beginning": "Heslo musí obsahovat alespoň",
"highlighted": "1 číslo"
"beginning": "",
"highlighted": ""
},
"uppercase": {
"beginning": "Heslo musí obsahovat alespoň",
"highlighted": "1 velké písmeno"
"beginning": "",
"highlighted": ""
},
"lowercase": {
"beginning": "Heslo musí obsahovat alespoň",
"highlighted": "1 malé písmeno"
"beginning": "",
"highlighted": ""
},
"match": {
"beginning": "Obě hesla se",
"highlighted": "musí shodovat"
"beginning": "",
"highlighted": ""
}
},
"errors": {
"empty": "Zadejte prosím heslo",
"length": "Heslo musí být dlouhé alespoň 8 znaků",
"uppercase": "Heslo musí obsahovat alespoň 1 velké písmeno",
"lowercase": "Heslo musí obsahovat alespoň 1 malé písmeno",
"number": "Heslo musí obsahovat alespoň 1 číslo",
"special": "Heslo musí obsahovat alespoň 1 speciální znak",
"incorrect": "Zadané heslo neodpovídá tomu, co bylo nastaveno"
"empty": "",
"length": "",
"uppercase": "",
"lowercase": "",
"number": "",
"special": "",
"incorrect": ""
}
},
"passwordConfirm": {
"label": "Potvrzení hesla",
"placeholder": "Pro ověření správnosti zadejte heslo ještě jednou",
"label": "",
"placeholder": "",
"errors": {
"empty": "Zadejte prosím své heslo ještě jednou, aby mohla být potvrzena jeho správnost (odhalí překlepy)",
"different": "Zadaná hesla se liší, takže je nejspíše jedno z nich špatně zapsané"
"empty": "",
"different": ""
}
},
"firstName": {
"label": "Jméno",
"placeholder": "Jan",
"label": "",
"placeholder": "",
"errors": {
"empty": "Zadejte prosím své křestní jméno",
"length": "Jméno se musí vejít do 50 znaků",
"pattern": "Součástí jména mohou být pouze písmena, mezery apostrofy nebo spojovníky"
"empty": "",
"length": "",
"pattern": ""
}
},
"lastName": {
"label": "Příjmení",
"placeholder": "Novák",
"label": "",
"placeholder": "",
"errors": {
"empty": "Zadejte prosím své příjmení",
"length": "Příjmení se musí vejít do 50 znaků",
"pattern": "Součástí příjmení mohou být pouze písmena, mezery apostrofy nebo spojovníky"
"empty": "",
"length": "",
"pattern": ""
}
}
},
"errors": {
"validation": "Nastala chyba při ověřování dat."
"validation": ""
},
"fields": {
"password": {
@@ -541,20 +541,20 @@
}
},
"login": {
"heading": "Přihlášení",
"heading": "",
"subheadings": {
"stepOne": "Zadejte svůj e-mail",
"stepTwo": "Zadejte své heslo"
"stepOne": "",
"stepTwo": ""
},
"links": {
"forgotPassword": "Zapomněli jste heslo? <a>Obnovte si ho</a>",
"register": "Ještě nemáte účet? <a>Zaregistrujte se</a>",
"forgotPassword": "",
"register": "",
"forgotPasswordLink": "",
"registerLink": ""
},
"toasts": {
"success": "Vítejte zpět",
"incorrectPassword": "Nesprávné heslo"
"success": "",
"incorrectPassword": ""
},
"errors": {
"password": {
@@ -564,72 +564,72 @@
},
"registration": {
"heading": {
"superAdmin": "Vytvoření superadministrátora",
"user": "Registrace"
"superAdmin": "",
"user": ""
},
"subheadings": {
"stepOne": "Zadejte své osobní údaje",
"stepTwo": "Zadejte svůj e-mail",
"stepThree": "Nastavte si heslo"
"stepOne": "",
"stepTwo": "",
"stepThree": ""
},
"description": {
"superAdmin": "Pro začátek vytvořte účet superadministrátora",
"user": "Zaregistrujte se jako uživatel a požádejte svého superadministrátora, aby vám přidělil oprávnění k hlídačům"
"superAdmin": "",
"user": ""
},
"gettingStartedButton": {
"superAdmin": "Vytvořit účet superadministrátora",
"user": "Zaregistrovat se jako běžný uživatel"
"superAdmin": "",
"user": ""
},
"termsAndPolicies": "Vytvořením účtu souhlasíte s našimi <a1>Podmínkami použití</a1> (anglicky) a <a2>Podmínkami zpracování osobních údajů</a2> (anglicky).",
"termsAndPolicies": "",
"links": {
"login": "Máte již vytvořený účet? <a>Přihlašte se</a>"
"login": ""
},
"toasts": {
"success": "Vítejte! Váš účet byl úspěšně vytvořen."
"success": ""
}
},
"forgotPassword": {
"heading": "Zapomenuté heslo",
"heading": "",
"subheadings": {
"stepOne": "Nebojte se, zašleme vám pokyny pro obnovení hesla.",
"stepTwo": "Odkaz pro obnovení hesla byl odeslán na <email/>",
"stepThree": "Nové heslo se musí lišit od těch, které jste již použili.",
"stepFour": "Heslo bylo úspěšně obnoveno. Klepněte na tlačítko a dojde k čarovnému přihlášení."
"stepOne": "",
"stepTwo": "",
"stepThree": "",
"stepFour": ""
},
"buttons": {
"openEmail": "Otevřít e-mailovou aplikaci",
"resetPassword": "Obnovit heslo"
"openEmail": "",
"resetPassword": ""
},
"imageAlts": {
"passwordKey": "Ikonka přístupového klíče",
"email": "Ikonka e-mailu",
"lock": "Ikonka zámku",
"passwordConfirm": "Ikonka potvrzeného hesla"
"passwordKey": "",
"email": "",
"lock": "",
"passwordConfirm": ""
},
"links": {
"login": "Vrátit se k <a>Přihlášení</a>",
"resend": "Nedorazil vám e-mail? <a>Klepněte pro opětovné zaslání</a>"
"login": "",
"resend": ""
},
"toasts": {
"sent": "Instrukce vám dorazí na <email/>.",
"emailNotFound": "Nepodařilo se nalézt e-mailovou adresu.",
"redirect": "Přesměrování proběhne za <seconds/>...",
"success": "Heslo bylo úspěšně obnoveno.",
"error": "Obnova hesla se nepodařila. Zkuste to prosím znovu nebo kontaktujte podporu."
"sent": "",
"emailNotFound": "",
"redirect": "",
"success": "",
"error": ""
}
}
},
"errorPages": {
"serverUnreachable": {
"toasts": {
"reconnected": "Podařilo se úspěšně obnovit spojení se serverem",
"stillUnreachable": "Server je stále nedosažitelný. Zkuste to prosím později."
"reconnected": "",
"stillUnreachable": ""
},
"alertBox": "Chyba připojení k serveru",
"description": "Nepodařilo se připojit k serveru. Zkontrolujte si prosím internetové připojení a pokud problém přetrvává, ověřte konfiguraci aplikace Checkmate.",
"alertBox": "",
"description": "",
"retryButton": {
"default": "Zkusit se znovu připojit",
"processing": "Probíhá připojování…"
"default": "",
"processing": ""
}
}
},
+1 -1
View File
@@ -1,5 +1,5 @@
{
"printWidth": 90,
"printWidth": 150,
"useTabs": true,
"tabWidth": 2,
"singleQuote": false,
+16 -24
View File
@@ -1,5 +1,4 @@
import { createAnnouncementValidation } from "../validation/joi.js";
import { handleError } from "./controllerUtils.js";
const SERVICE_NAME = "announcementController";
@@ -10,10 +9,9 @@ const SERVICE_NAME = "announcementController";
* @class AnnouncementController
*/
class AnnouncementController {
constructor(db, stringService) {
this.db = db;
this.stringService = stringService;
class AnnouncementController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
this.createAnnouncement = this.createAnnouncement.bind(this);
this.getAnnouncement = this.getAnnouncement.bind(this);
}
@@ -28,16 +26,10 @@ class AnnouncementController {
*
* @returns {Promise<void>} A promise that resolves once the response is sent.
*/
createAnnouncement = async (req, res, next) => {
try {
createAnnouncement = asyncHandler(
async (req, res, next) => {
await createAnnouncementValidation.validateAsync(req.body);
} catch (error) {
return next(handleError(error, SERVICE_NAME)); // Handle Joi validation errors
}
const { title, message } = req.body;
try {
const { title, message } = req.body;
const announcementData = {
title: title.trim(),
message: message.trim(),
@@ -49,10 +41,10 @@ class AnnouncementController {
msg: this.stringService.createAnnouncement,
data: newAnnouncement,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createAnnouncement"));
}
};
},
SERVICE_NAME,
"createAnnouncement"
);
/**
* Handles retrieving announcements with pagination.
@@ -63,17 +55,17 @@ class AnnouncementController {
* - `msg`: A message about the success of the request.
* @param {Function} next - The next middleware function in the stack for error handling.
*/
getAnnouncement = async (req, res, next) => {
try {
getAnnouncement = asyncHandler(
async (req, res, next) => {
const allAnnouncements = await this.db.getAnnouncements();
return res.success({
msg: this.stringService.getAnnouncement,
data: allAnnouncements,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAnnouncement"));
}
};
},
SERVICE_NAME,
"getAnnouncement"
);
}
export default AnnouncementController;
+361 -357
View File
@@ -1,452 +1,456 @@
import BaseController from "./baseController.js";
import {
registrationBodyValidation,
loginValidation,
editUserParamValidation,
editUserBodyValidation,
recoveryValidation,
recoveryTokenValidation,
recoveryTokenBodyValidation,
newPasswordValidation,
getUserByIdParamValidation,
editUserByIdParamValidation,
editUserByIdBodyValidation,
editSuperadminUserByIdBodyValidation,
} from "../validation/joi.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import crypto from "crypto";
import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "authController";
class AuthController {
constructor({ db, settingsService, emailService, jobQueue, stringService, logger }) {
this.db = db;
/**
* Authentication Controller
*
* Handles all authentication-related HTTP requests including user registration,
* login, password recovery, and user management operations.
*
* @class AuthController
* @description Manages user authentication and authorization operations
*/
class AuthController extends BaseController {
/**
* Creates an instance of AuthController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.emailService - Service for email operations
* @param {Object} dependencies.jobQueue - Service for job queue operations
* @param {Object} dependencies.userService - User business logic service
*/
constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.logger = logger;
this.userService = userService;
}
/**
* Creates and returns JWT token with an arbitrary payload
* @function
* @param {Object} payload
* @param {Object} appSettings
* @returns {String}
* @throws {Error}
*/
issueToken = (payload, appSettings) => {
try {
const tokenTTL = appSettings?.jwtTTL ?? "2h";
const tokenSecret = appSettings?.jwtSecret;
const payloadData = payload;
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
} catch (error) {
throw handleError(error, SERVICE_NAME, "issueToken");
}
};
/**
* Registers a new user. If the user is the first account, a JWT secret is created. If not, an invite token is required.
* Registers a new user in the system.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.inviteToken - The invite token for registration.
* @property {Object} req.file - The file object for the user's profile image.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the user, the created user data, and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function registerUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user registration data
* @param {string} req.body.firstName - User's first name
* @param {string} req.body.lastName - User's last name
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists)
* @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin)
* @param {Array<string>} [req.body.role] - User roles (auto-assigned if superadmin)
* @param {Object} [req.file] - Profile image file uploaded via multer
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 409 - Conflict if user already exists
* @example
* // Register first user (becomes superadmin)
* POST /auth/register
* {
* "firstName": "John",
* "lastName": "Doe",
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*
* // Register subsequent user (requires invite token)
* POST /auth/register
* {
* "firstName": "Jane",
* "lastName": "Smith",
* "email": "jane@example.com",
* "password": "SecurePass123!",
* "inviteToken": "abc123..."
* }
*/
registerUser = async (req, res, next) => {
try {
registerUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await registrationBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// Create a new user
try {
const user = req.body;
// If superAdmin exists, a token should be attached to all further register requests
const superAdminExists = await this.db.checkSuperadmin(req, res);
if (superAdminExists) {
const invitedUser = await this.db.getInviteTokenAndDelete(user.inviteToken);
user.role = invitedUser.role;
user.teamId = invitedUser.teamId;
} else {
// This is the first account, create JWT secret to use if one is not supplied by env
const jwtSecret = crypto.randomBytes(64).toString("hex");
await this.db.updateAppSettings({ jwtSecret });
}
const newUser = await this.db.insertUser({ ...req.body }, req.file);
this.logger.info({
message: this.stringService.authCreateUser,
service: SERVICE_NAME,
details: newUser._id,
});
const userForToken = { ...newUser._doc };
delete userForToken.profileImage;
delete userForToken.avatarImage;
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userForToken, appSettings);
try {
const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
name: newUser.firstName,
});
this.emailService
.sendEmail(newUser.email, "Welcome to Uptime Monitor", html)
.catch((error) => {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
});
} catch (error) {
this.logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "registerUser",
stack: error.stack,
});
}
const { user, token } = await this.userService.registerUser(req.body, req.file);
res.success({
msg: this.stringService.authCreateUser,
data: { user: newUser, token: token },
data: { user, token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
},
SERVICE_NAME,
"registerUser"
);
/**
* Logs in a user by validating the user's credentials and issuing a JWT token.
* Authenticates a user and returns a JWT token.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user.
* @property {string} req.body.password - The password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the login of the user, the user data (without password and avatar image), and a JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or the password is incorrect.
* @function loginUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing login credentials
* @param {string} req.body.email - User's email address (will be converted to lowercase)
* @param {string} req.body.password - User's password
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 401 - Unauthorized if credentials are incorrect
* @example
* POST /auth/login
* {
* "email": "john@example.com",
* "password": "SecurePass123!"
* }
*/
loginUser = async (req, res, next) => {
try {
loginUser = this.asyncHandler(
async (req, res) => {
if (req.body?.email) {
req.body.email = req.body.email?.toLowerCase();
}
await loginValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const { email, password } = req.body;
// Check if user exists
const user = await this.db.getUserByEmail(email);
// Compare password
const match = await user.comparePassword(password);
if (match !== true) {
const error = new Error(this.stringService.authIncorrectPassword);
error.status = 401;
next(error);
return;
}
// Remove password from user object. Should this be abstracted to DB layer?
const userWithoutPassword = { ...user._doc };
delete userWithoutPassword.password;
delete userWithoutPassword.avatarImage;
// Happy path, return token
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(userWithoutPassword, appSettings);
// reset avatar image
userWithoutPassword.avatarImage = user.avatarImage;
const { user, token } = await this.userService.loginUser(req.body.email, req.body.password);
return res.success({
msg: this.stringService.authLoginUser,
data: {
user: userWithoutPassword,
token: token,
user,
token,
},
});
} catch (error) {
error.status = 401;
next(handleError(error, SERVICE_NAME, "loginUser"));
}
};
},
SERVICE_NAME,
"loginUser"
);
/**
* Edits a user's information. If the user wants to change their password, the current password is checked before updating to the new password.
* Updates the current user's profile information.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.userId - The ID of the user to be edited.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.password - The current password of the user.
* @property {string} req.body.newPassword - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the update of the user, and the updated user data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422), the user is unauthorized (401), or the password is incorrect (403).
* @function editUser
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {string} [req.body.password] - Current password (required for password change)
* @param {string} [req.body.newPassword] - New password (required for password change)
* @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image
* @param {Object} [req.file] - New profile image file
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with updated user data
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 403 - Forbidden if current password is incorrect
* @example
* PUT /auth/user
* {
* "firstName": "John Updated",
* "lastName": "Doe Updated"
* }
*
* // Change password
* PUT /auth/user
* {
* "password": "OldPass123!",
* "newPassword": "NewPass123!"
* }
*/
editUser = async (req, res, next) => {
try {
await editUserParamValidation.validateAsync(req.params);
editUser = this.asyncHandler(
async (req, res) => {
await editUserBodyValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
// TODO is this neccessary any longer? Verify ownership middleware should handle this
if (req.params.userId !== req.user._id.toString()) {
const error = new Error(this.stringService.unauthorized);
error.status = 401;
error.service = SERVICE_NAME;
next(error);
return;
}
const updatedUser = await this.userService.editUser(req.body, req.file, req.user);
try {
// Change Password check
if (req.body.password && req.body.newPassword) {
// Get token from headers
const token = getTokenFromHeaders(req.headers);
// Get email from token
const { jwtSecret } = this.settingsService.getSettings();
const { email } = jwt.verify(token, jwtSecret);
// Add user email to body for DB operation
req.body.email = email;
// Get user
const user = await this.db.getUserByEmail(email);
// Compare passwords
const match = await user.comparePassword(req.body.password);
// If not a match, throw a 403
// 403 instead of 401 to avoid triggering axios interceptor
if (!match) {
const error = new Error(this.stringService.authIncorrectPassword);
error.status = 403;
next(error);
return;
}
// If a match, update the password
req.body.password = req.body.newPassword;
}
const updatedUser = await this.db.updateUser(req, res);
res.success({
msg: this.stringService.authUpdateUser,
data: updatedUser,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "userEditController"));
}
};
},
SERVICE_NAME,
"editUser"
);
/**
* Checks if a superadmin account exists in the database.
* Checks if a superadmin account exists in the system.
*
* @async
* @param {Object} req - The Express request object.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the existence of a superadmin, and a boolean indicating the existence of a superadmin.
* @throws {Error} If there is an error during the process.
* @function checkSuperadminExists
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with boolean indicating superadmin existence
* @example
* GET /auth/users/superadmin
* // Response: { "data": true } or { "data": false }
*/
checkSuperadminExists = async (req, res, next) => {
try {
const superAdminExists = await this.db.checkSuperadmin(req, res);
checkSuperadminExists = this.asyncHandler(
async (req, res) => {
const superAdminExists = await this.userService.checkSuperadminExists();
return res.success({
msg: this.stringService.authAdminExists,
data: superAdminExists,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkSuperadminController"));
}
};
},
SERVICE_NAME,
"checkSuperadminExists"
);
/**
* Requests a recovery token for a user. The user's email is validated and a recovery token is created and sent via email.
* Initiates password recovery process by sending a recovery email.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user requesting recovery.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the recovery token, and the message ID of the sent email.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function requestRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing email
* @param {string} req.body.email - Email address for password recovery
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with message ID
* @throws {Error} 422 - Validation error if email is invalid
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* POST /auth/recovery/request
* {
* "email": "john@example.com"
* }
*/
requestRecovery = async (req, res, next) => {
try {
requestRecovery = this.asyncHandler(
async (req, res) => {
await recoveryValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const { email } = req.body;
const user = await this.db.getUserByEmail(email);
const recoveryToken = await this.db.requestRecoveryToken(req, res);
const name = user.firstName;
const { clientHost } = this.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
const html = await this.emailService.buildEmail("passwordResetTemplate", {
name,
email,
url,
});
const msgId = await this.emailService.sendEmail(
email,
"Checkmate Password Reset",
html
);
const email = req?.body?.email;
const msgId = await this.userService.requestRecovery(email);
return res.success({
msg: this.stringService.authCreateRecoveryToken,
data: msgId,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
}
};
},
SERVICE_NAME,
"requestRecovery"
);
/**
* Validates a recovery token. The recovery token is validated and if valid, a success message is returned.
* Validates a password recovery token.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token to be validated.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the validation of the recovery token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function validateRecovery
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing recovery token
* @param {string} req.body.recoveryToken - Recovery token to validate
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response if token is valid
* @throws {Error} 422 - Validation error if token format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/validate
* {
* "recoveryToken": "abc123..."
* }
*/
validateRecovery = async (req, res, next) => {
try {
await recoveryTokenValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
await this.db.validateRecoveryToken(req, res);
validateRecovery = this.asyncHandler(
async (req, res) => {
await recoveryTokenBodyValidation.validateAsync(req.body);
await this.userService.validateRecovery(req.body.recoveryToken);
return res.success({
msg: this.stringService.authVerifyRecoveryToken,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "validateRecoveryTokenController"));
}
};
},
SERVICE_NAME,
"validateRecovery"
);
/**
* Resets a user's password. The new password is validated and if valid, the user's password is updated in the database and a new JWT token is issued.
* Resets user password using a valid recovery token.
*
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.token - The recovery token.
* @property {string} req.body.password - The new password of the user.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the reset of the password, the updated user data (without password and avatar image), and a new JWT token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* @function resetPassword
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing new password and recovery token
* @param {string} req.body.password - New password
* @param {string} req.body.recoveryToken - Valid recovery token
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data and JWT token
* @throws {Error} 422 - Validation error if password format is invalid
* @throws {Error} 400 - Bad request if token is invalid or expired
* @example
* POST /auth/recovery/reset
* {
* "password": "NewSecurePass123!",
* "recoveryToken": "abc123..."
* }
*/
resetPassword = async (req, res, next) => {
try {
resetPassword = this.asyncHandler(
async (req, res) => {
await newPasswordValidation.validateAsync(req.body);
} catch (error) {
const validationError = handleValidationError(error, SERVICE_NAME);
next(validationError);
return;
}
try {
const user = await this.db.resetPassword(req, res);
const appSettings = await this.settingsService.getSettings();
const token = this.issueToken(user._doc, appSettings);
const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken);
return res.success({
msg: this.stringService.authResetPassword,
data: { user, token },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "resetPasswordController"));
}
};
},
SERVICE_NAME,
"resetPassword"
);
/**
* Deletes a user and all associated monitors, checks, and alerts.
* Deletes the current user's account and associated data.
*
* @param {Object} req - The request object.
* @param {Object} res - The response object.
* @param {Function} next - The next middleware function.
* @returns {Object} The response object with success status and message.
* @throws {Error} If user validation fails or user is not found in the database.
* @async
* @function deleteUser
* @param {Object} req - Express request object
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - User ID
* @param {string} req.user.email - User email
* @param {string} req.user.teamId - User's team ID
* @param {Array<string>} req.user.role - User roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user deletion
* @throws {Error} 400 - Bad request if user is demo user
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* DELETE /auth/user
* // Requires JWT authentication
*/
deleteUser = async (req, res, next) => {
try {
const token = getTokenFromHeaders(req.headers);
const decodedToken = jwt.decode(token);
const { email } = decodedToken;
// Check if the user exists
const user = await this.db.getUserByEmail(email);
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.db.getMonitorsByTeamId({
teamId: user.teamId.toString(),
});
if (user.role.includes("superadmin")) {
//2. Remove all jobs, delete checks and alerts
result?.monitors.length > 0 &&
(await Promise.all(
result.monitors.map(async (monitor) => {
await this.jobQueue.deleteJob(monitor);
})
));
}
// 6. Delete the user by id
await this.db.deleteUser(user._id);
deleteUser = this.asyncHandler(
async (req, res) => {
await this.userService.deleteUser(req.user);
return res.success({
msg: this.stringService.authDeleteUser,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteUserController"));
}
};
getAllUsers = async (req, res, next) => {
try {
const allUsers = await this.db.getAllUsers(req, res);
},
SERVICE_NAME,
"deleteUser"
);
/**
* Retrieves all users in the system (admin/superadmin only).
*
* @async
* @function getAllUsers
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with array of users
* @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role
* @example
* GET /auth/users
* // Requires JWT authentication with admin/superadmin role
*/
getAllUsers = this.asyncHandler(
async (req, res) => {
const allUsers = await this.userService.getAllUsers();
return res.success({
msg: this.stringService.authGetAllUsers,
data: allUsers,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
}
};
},
SERVICE_NAME,
"getAllUsers"
);
/**
* Retrieves a specific user by ID (superadmin only).
*
* @async
* @function getUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to retrieve
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with user data
* @throws {Error} 422 - Validation error if userId is invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* GET /auth/users/507f1f77bcf86cd799439011
* // Requires JWT authentication with superadmin role
*/
getUserById = this.asyncHandler(
async (req, res) => {
await getUserByIdParamValidation.validateAsync(req.params);
const userId = req?.params?.userId;
const roles = req?.user?.role;
if (!userId) {
throw new Error("No user ID in request");
}
if (!roles || roles.length === 0) {
throw new Error("No roles in request");
}
const user = await this.userService.getUserById(roles, userId);
return res.success({ msg: "ok", data: user });
},
SERVICE_NAME,
"getUserById"
);
/**
* Updates a specific user by ID (superadmin only).
*
* @async
* @function editUserById
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.userId - ID of the user to update
* @param {Object} req.body - Request body containing user update data
* @param {string} [req.body.firstName] - Updated first name
* @param {string} [req.body.lastName] - Updated last name
* @param {Array<string>} [req.body.role] - Updated user roles
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user._id - Current user's ID
* @param {Array<string>} req.user.role - Current user's roles
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming user update
* @throws {Error} 422 - Validation error if parameters or body are invalid
* @throws {Error} 403 - Forbidden if user doesn't have superadmin role
* @throws {Error} 404 - Not found if user doesn't exist
* @example
* PUT /auth/users/507f1f77bcf86cd799439011
* {
* "firstName": "Updated Name",
* "role": ["admin"]
* }
* // Requires JWT authentication with superadmin role
*/
editUserById = this.asyncHandler(
async (req, res) => {
const roles = req?.user?.role;
if (!roles.includes("superadmin")) {
throw createError("Unauthorized", 403);
}
const userId = req.params.userId;
const user = { ...req.body };
await editUserByIdParamValidation.validateAsync(req.params);
// If this is superadmin self edit, allow "superadmin" role
if (userId === req.user._id) {
await editSuperadminUserByIdBodyValidation.validateAsync(req.body);
} else {
await editUserByIdBodyValidation.validateAsync(req.body);
}
await this.userService.editUserById(userId, user);
return res.success({ msg: "ok" });
},
SERVICE_NAME,
"editUserById"
);
}
export default AuthController;
+83
View File
@@ -0,0 +1,83 @@
import { AppError } from "../service/infrastructure/errorService.js";
export const createCommonDependencies = (serviceRegistry, dbServiceName, loggerServiceName, errorServiceName, stringServiceName) => {
return {
db: serviceRegistry.get(dbServiceName),
errorService: serviceRegistry.get(errorServiceName),
logger: serviceRegistry.get(loggerServiceName),
stringService: serviceRegistry.get(stringServiceName),
};
};
class BaseController {
constructor({ db, logger, errorService, ...additionalDependencies }) {
this.db = db;
this.logger = logger;
this.errorService = errorService;
Object.assign(this, additionalDependencies);
this.asyncHandler = (fn, serviceName, methodName) => {
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (error) {
// Handle validation errors
if (error.isJoi) {
const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName);
return next(validationError);
}
if (error.name === "ValidationError") {
const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName);
return next(validationError);
}
if (error.name === "CastError") {
const notFoundError = this.errorService.createNotFoundError(
"Invalid resource identifier",
{ field: error.path, value: error.value },
serviceName,
methodName
);
return next(notFoundError);
}
if (error.code === "11000") {
const conflictError = this.errorService.createConflictError("Resource already exists", {
originalError: error.message,
code: error.code,
});
conflictError.service = serviceName;
conflictError.method = methodName;
return next(conflictError);
}
if (error instanceof AppError) {
error.service = error.service || serviceName;
error.method = error.method || methodName;
return next(error);
}
if (error.status) {
const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, {
originalError: error.message,
stack: error.stack,
});
return next(appError);
}
// For unknown errors, create a server error
const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", {
originalError: error.message,
stack: error.stack,
});
appError.service = serviceName;
appError.method = methodName;
appError.stack = error.stack; // Preserve original stack
return next(appError);
}
};
};
}
}
export default BaseController;
+274 -160
View File
@@ -1,9 +1,7 @@
import BaseController from "./baseController.js";
import {
createCheckParamValidation,
createCheckBodyValidation,
getChecksParamValidation,
getChecksQueryValidation,
getTeamChecksParamValidation,
getTeamChecksQueryValidation,
deleteChecksParamValidation,
deleteChecksByTeamIdParamValidation,
@@ -12,230 +10,346 @@ import {
ackAllChecksParamValidation,
ackAllChecksBodyValidation,
} from "../validation/joi.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "checkController";
class CheckController {
constructor(db, settingsService, stringService) {
this.db = db;
/**
* Check Controller
*
* Handles all check-related HTTP requests including retrieving checks,
* acknowledging checks, deleting checks, and managing check TTL settings.
*
* @class CheckController
* @description Manages check operations and monitoring data
*/
class CheckController extends BaseController {
/**
* Creates an instance of CheckController.
*
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.settingsService - Service for application settings
* @param {Object} dependencies.checkService - Check business logic service
*/
constructor(commonDependencies, { settingsService, checkService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.checkService = checkService;
}
createCheck = async (req, res, next) => {
try {
await createCheckParamValidation.validateAsync(req.params);
await createCheckBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const checkData = { ...req.body };
const check = await this.db.createCheck(checkData);
return res.success({
msg: this.stringService.checkCreate,
data: check,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createCheck"));
}
};
getChecksByMonitor = async (req, res, next) => {
try {
/**
* Retrieves checks for a specific monitor with filtering and pagination.
*
* @async
* @function getChecksByMonitor
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.monitorId - ID of the monitor to get checks for
* @param {Object} req.query - Query parameters for filtering and pagination
* @param {string} [req.query.type] - Type of checks to filter by
* @param {string} [req.query.sortOrder] - Sort order (asc/desc)
* @param {string} [req.query.dateRange] - Date range filter
* @param {string} [req.query.filter] - General filter string
* @param {boolean} [req.query.ack] - Filter by acknowledgment status
* @param {number} [req.query.page] - Page number for pagination
* @param {number} [req.query.rowsPerPage] - Number of rows per page
* @param {string} [req.query.status] - Filter by check status
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with checks data
* @throws {Error} 422 - Validation error if parameters are invalid
* @throws {Error} 404 - Not found if monitor doesn't exist
* @throws {Error} 403 - Forbidden if user doesn't have access to monitor
* @example
* GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down
* // Requires JWT authentication
*/
getChecksByMonitor = this.asyncHandler(
async (req, res) => {
await getChecksParamValidation.validateAsync(req.params);
await getChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorId } = req.params;
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } =
req.query;
const result = await this.db.getChecksByMonitor({
monitorId,
type,
sortOrder,
dateRange,
filter,
ack,
page,
rowsPerPage,
status,
const result = await this.checkService.getChecksByMonitor({
monitorId: req?.params?.monitorId,
query: req?.query,
teamId: req?.user?.teamId,
});
return res.success({
msg: this.stringService.checkGet,
data: result,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecks"));
}
};
},
SERVICE_NAME,
"getChecksByMonitor"
);
getChecksByTeam = async (req, res, next) => {
try {
await getTeamChecksParamValidation.validateAsync(req.params);
/**
* Retrieves all checks for the current user's team with filtering and pagination.
*
* @async
* @function getChecksByTeam
* @param {Object} req - Express request object
* @param {Object} req.query - Query parameters for filtering and pagination
* @param {string} [req.query.sortOrder] - Sort order (asc/desc)
* @param {string} [req.query.dateRange] - Date range filter
* @param {string} [req.query.filter] - General filter string
* @param {boolean} [req.query.ack] - Filter by acknowledgment status
* @param {number} [req.query.page] - Page number for pagination
* @param {number} [req.query.rowsPerPage] - Number of rows per page
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with checks data
* @throws {Error} 422 - Validation error if query parameters are invalid
* @example
* GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false
* // Requires JWT authentication
*/
getChecksByTeam = this.asyncHandler(
async (req, res) => {
await getTeamChecksQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = req.query;
const { teamId } = req.user;
const checkData = await this.db.getChecksByTeam({
sortOrder,
dateRange,
filter,
ack,
page,
rowsPerPage,
teamId,
const checkData = await this.checkService.getChecksByTeam({
teamId: req?.user?.teamId,
query: req?.query,
});
return res.success({
msg: this.stringService.checkGet,
data: checkData,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
}
};
},
SERVICE_NAME,
"getChecksByTeam"
);
getChecksSummaryByTeamId = async (req, res, next) => {
try {
const { teamId } = req.user;
const summary = await this.db.getChecksSummaryByTeamId({ teamId });
/**
* Retrieves a summary of checks for the current user's team.
*
* @async
* @function getChecksSummaryByTeamId
* @param {Object} req - Express request object
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with checks summary
* @example
* GET /checks/summary
* // Requires JWT authentication
* // Response includes counts by status, time ranges, etc.
*/
getChecksSummaryByTeamId = this.asyncHandler(
async (req, res) => {
const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId });
return res.success({
msg: this.stringService.checkGetSummary,
data: summary,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getChecksSummaryByTeamId"));
}
};
},
SERVICE_NAME,
"getChecksSummaryByTeamId"
);
ackCheck = async (req, res, next) => {
try {
/**
* Acknowledges a specific check by ID.
*
* @async
* @function ackCheck
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.checkId - ID of the check to acknowledge
* @param {Object} req.body - Request body containing acknowledgment data
* @param {boolean} req.body.ack - Acknowledgment status (true/false)
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with updated check data
* @throws {Error} 422 - Validation error if request body is invalid
* @throws {Error} 404 - Not found if check doesn't exist
* @throws {Error} 403 - Forbidden if user doesn't have access to check
* @example
* PUT /checks/507f1f77bcf86cd799439011/ack
* {
* "ack": true
* }
* // Requires JWT authentication
*/
ackCheck = this.asyncHandler(
async (req, res) => {
await ackCheckBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { checkId } = req.params;
const { ack } = req.body;
const { teamId } = req.user;
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
const updatedCheck = await this.checkService.ackCheck({
checkId: req?.params?.checkId,
teamId: req?.user?.teamId,
ack: req?.body?.ack,
});
return res.success({
msg: this.stringService.checkUpdateStatus,
data: updatedCheck,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "ackCheck"));
}
};
},
SERVICE_NAME,
"ackCheck"
);
ackAllChecks = async (req, res, next) => {
try {
/**
* Acknowledges all checks for a specific monitor or path.
*
* @async
* @function ackAllChecks
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.monitorId - ID of the monitor
* @param {string} req.params.path - Path for acknowledgment (e.g., "monitor")
* @param {Object} req.body - Request body containing acknowledgment data
* @param {boolean} req.body.ack - Acknowledgment status (true/false)
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with updated checks data
* @throws {Error} 422 - Validation error if parameters or body are invalid
* @throws {Error} 404 - Not found if monitor doesn't exist
* @throws {Error} 403 - Forbidden if user doesn't have access to monitor
* @example
* PUT /checks/monitor/507f1f77bcf86cd799439011/ack
* {
* "ack": true
* }
* // Requires JWT authentication
*/
ackAllChecks = this.asyncHandler(
async (req, res) => {
await ackAllChecksParamValidation.validateAsync(req.params);
await ackAllChecksBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorId, path } = req.params;
const { ack } = req.body;
const { teamId } = req.user;
const teamId = req?.user?.teamId;
if (!teamId) {
throw new Error("No team ID in request");
}
const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
const updatedChecks = await this.checkService.ackAllChecks({
monitorId: req?.params?.monitorId,
path: req?.params?.path,
teamId: req?.user?.teamId,
ack: req?.body?.ack,
});
return res.success({
msg: this.stringService.checkUpdateStatus,
data: updatedChecks,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "ackAllChecks"));
}
};
},
SERVICE_NAME,
"ackAllChecks"
);
deleteChecks = async (req, res, next) => {
try {
/**
* Deletes all checks for a specific monitor.
*
* @async
* @function deleteChecks
* @param {Object} req - Express request object
* @param {Object} req.params - URL parameters
* @param {string} req.params.monitorId - ID of the monitor whose checks to delete
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with deletion count
* @throws {Error} 422 - Validation error if monitorId is invalid
* @throws {Error} 404 - Not found if monitor doesn't exist
* @throws {Error} 403 - Forbidden if user doesn't have access to monitor
* @example
* DELETE /checks/monitor/507f1f77bcf86cd799439011
* // Requires JWT authentication
* // Response: { "data": { "deletedCount": 150 } }
*/
deleteChecks = this.asyncHandler(
async (req, res) => {
await deleteChecksParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const deletedCount = await this.db.deleteChecks(req.params.monitorId);
const deletedCount = await this.checkService.deleteChecks({
monitorId: req.params.monitorId,
teamId: req?.user?.teamId,
});
return res.success({
msg: this.stringService.checkDelete,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecks"));
}
};
},
SERVICE_NAME,
"deleteChecks"
);
deleteChecksByTeamId = async (req, res, next) => {
try {
/**
* Deletes all checks for the current user's team.
*
* @async
* @function deleteChecksByTeamId
* @param {Object} req - Express request object
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with deletion count
* @throws {Error} 422 - Validation error if parameters are invalid
* @example
* DELETE /checks/team
* // Requires JWT authentication
* // Response: { "data": { "deletedCount": 1250 } }
*/
deleteChecksByTeamId = this.asyncHandler(
async (req, res) => {
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { teamId } = req.user;
const deletedCount = await this.db.deleteChecksByTeamId(teamId);
const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId });
return res.success({
msg: this.stringService.checkDelete,
data: { deletedCount },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteChecksByTeamId"));
}
};
},
SERVICE_NAME,
"deleteChecksByTeamId"
);
updateChecksTTL = async (req, res, next) => {
const SECONDS_PER_DAY = 86400;
try {
/**
* Updates the TTL (Time To Live) setting for checks in the current user's team.
*
* @async
* @function updateChecksTTL
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing TTL data
* @param {number} req.body.ttl - TTL value in days
* @param {Object} req.user - Current authenticated user (from JWT)
* @param {string} req.user.teamId - User's team ID
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response confirming TTL update
* @throws {Error} 422 - Validation error if TTL value is invalid
* @example
* PUT /checks/ttl
* {
* "ttl": 30
* }
* // Requires JWT authentication
* // Sets check TTL to 30 days
*/
updateChecksTTL = this.asyncHandler(
async (req, res) => {
await updateChecksTTLBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
// Get user's teamId
const { teamId } = req.user;
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await this.db.updateChecksTTL(teamId, ttl);
await this.checkService.updateChecksTTL({
teamId: req?.user?.teamId,
ttl: req?.body?.ttl,
});
return res.success({
msg: this.stringService.checkUpdateTTL,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateTTL"));
}
};
},
SERVICE_NAME,
"updateChecksTtl"
);
}
export default CheckController;
+1 -15
View File
@@ -1,17 +1,3 @@
const handleValidationError = (error, serviceName) => {
error.status = 422;
error.service = serviceName;
error.message = error.details?.[0]?.message || error.message || "Validation Error";
return error;
};
const handleError = (error, serviceName, method, status = 500) => {
error.status === undefined ? (error.status = status) : null;
error.service === undefined ? (error.service = serviceName) : null;
error.method === undefined ? (error.method = method) : null;
return error;
};
const fetchMonitorCertificate = async (sslChecker, monitor) => {
const monitorUrl = new URL(monitor.url);
const hostname = monitorUrl.hostname;
@@ -23,4 +9,4 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => {
return cert;
};
export { handleValidationError, handleError, fetchMonitorCertificate };
export { fetchMonitorCertificate };
+49 -40
View File
@@ -1,48 +1,57 @@
import { handleError } from "./controllerUtils.js";
const SERVICE_NAME = "diagnosticController";
class DiagnosticController {
constructor(db) {
this.db = db;
this.getMonitorsByTeamIdExecutionStats =
this.getMonitorsByTeamIdExecutionStats.bind(this);
this.getDbStats = this.getDbStats.bind(this);
import BaseController from "./baseController.js";
/**
* Diagnostic Controller
*
* Handles system diagnostic and monitoring requests including system statistics,
* performance metrics, and health checks.
*
* @class DiagnosticController
* @description Manages system diagnostics and performance monitoring
*/
class DiagnosticController extends BaseController {
/**
* Creates an instance of DiagnosticController.
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies - The dependencies required by the controller
* @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring
*/
constructor(commonDependencies, { diagnosticService }) {
super(commonDependencies);
this.diagnosticService = diagnosticService;
}
async getMonitorsByTeamIdExecutionStats(req, res, next) {
try {
const data = await this.db.getMonitorsByTeamIdExecutionStats(req);
/**
* Retrieves comprehensive system statistics and performance metrics.
*
* @async
* @function getSystemStats
* @param {Object} req - Express request object
* @param {Object} res - Express response object
* @returns {Promise<Object>} Success response with system diagnostics data
* @description Returns system performance metrics, memory usage, CPU statistics,
* and other diagnostic information useful for monitoring system health.
* @example
* GET /diagnostics/stats
* // Response includes:
* // - Memory usage (heap, external, arrayBuffers)
* // - CPU usage statistics
* // - System uptime
* // - Performance metrics
* // - Database connection status
* // - Active processes/connections
*/
getSystemStats = this.asyncHandler(
async (req, res) => {
const diagnostics = await this.diagnosticService.getSystemStats();
return res.success({
msg: "OK",
data,
data: diagnostics,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamIdExecutionStats"));
}
}
async getDbStats(req, res, next) {
try {
const { methodName, args = [] } = req.body;
if (!methodName || !this.db[methodName]) {
return res.error({
msg: "Invalid method name or method doesn't exist",
status: 400,
});
}
const explainMethod = await this.db[methodName].apply(this.db, args);
const stats = {
methodName,
timestamp: new Date(),
explain: explainMethod,
};
return res.success({
msg: "OK",
data: stats,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getDbStats"));
}
}
},
SERVICE_NAME,
"getSystemStats"
);
}
export default DiagnosticController;
+71 -104
View File
@@ -1,129 +1,96 @@
import {
inviteRoleValidation,
inviteBodyValidation,
inviteVerificationBodyValidation,
} from "../validation/joi.js";
import logger from "../utils/logger.js";
import jwt from "jsonwebtoken";
import { handleError, handleValidationError } from "./controllerUtils.js";
import { getTokenFromHeaders } from "../utils/utils.js";
import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "inviteController";
class InviteController {
constructor(db, settingsService, emailService, stringService) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.stringService = stringService;
/**
* Controller for handling user invitation operations
* Manages invite token generation, email sending, and token verification
*/
class InviteController extends BaseController {
/**
* Creates a new InviteController instance
* @param {Object} commonDependencies - Common dependencies injected into the controller
* @param {Object} dependencies.inviteService - Service for invite-related operations
*/
constructor(commonDependencies, { inviteService }) {
super(commonDependencies);
this.inviteService = inviteService;
}
/**
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.email - The email of the user to be invited.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the sending of the invitation, and the invitation token.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
* Generates an invite token for a user invitation
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing invite details
* @param {Object} req.user - Authenticated user object
* @param {string} req.user.teamId - Team ID of the authenticated user
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with invite token data
*/
getInviteToken = async (req, res, next) => {
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
const { role, teamId } = jwt.decode(token);
req.body.teamId = teamId;
try {
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const inviteToken = await this.db.requestInviteToken({ ...req.body });
getInviteToken = this.asyncHandler(
async (req, res) => {
const invite = req.body;
const teamId = req?.user?.teamId;
invite.teamId = teamId;
await inviteBodyValidation.validateAsync(invite);
const inviteToken = await this.inviteService.getInviteToken({ invite, teamId });
return res.success({
msg: this.stringService.inviteIssued,
data: inviteToken,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteController"));
}
};
},
SERVICE_NAME,
"getInviteToken"
);
sendInviteEmail = async (req, res, next) => {
try {
// Only admins can invite
const token = getTokenFromHeaders(req.headers);
const { role, firstname, teamId } = jwt.decode(token);
req.body.teamId = teamId;
try {
await inviteRoleValidation.validateAsync({ roles: role });
await inviteBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const inviteToken = await this.db.requestInviteToken({ ...req.body });
const { clientHost } = this.settingsService.getSettings();
try {
const html = await this.emailService.buildEmail("employeeActivationTemplate", {
name: firstname,
link: `${clientHost}/register/${inviteToken.token}`,
});
const result = await this.emailService.sendEmail(
req.body.email,
"Welcome to Uptime Monitor",
html
);
if (!result) {
return res.error({
msg: "Failed to send invite e-mail... Please verify your settings.",
});
}
} catch (error) {
logger.warn({
message: error.message,
service: SERVICE_NAME,
method: "sendInviteEmail",
stack: error.stack,
});
}
/**
* Sends an invitation email to a user
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing invite details
* @param {Object} req.user - Authenticated user object
* @param {string} req.user.teamId - Team ID of the authenticated user
* @param {string} req.user.firstName - First name of the authenticated user
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with invite token data
*/
sendInviteEmail = this.asyncHandler(
async (req, res) => {
const inviteRequest = req.body;
inviteRequest.teamId = req?.user?.teamId;
await inviteBodyValidation.validateAsync(inviteRequest);
const inviteToken = await this.inviteService.sendInviteEmail({
inviteRequest,
firstName: req?.user?.firstName,
});
return res.success({
msg: this.stringService.inviteIssued,
data: inviteToken,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteController"));
}
};
},
SERVICE_NAME,
"sendInviteEmail"
);
inviteVerifyController = async (req, res, next) => {
try {
/**
* Verifies an invite token and returns invite details
* @param {Object} req - Express request object
* @param {Object} req.body - Request body containing the invite token
* @param {string} req.body.token - The invite token to verify
* @param {Object} res - Express response object
* @returns {Promise<Object>} Response with verified invite data
*/
verifyInviteToken = this.asyncHandler(
async (req, res) => {
await inviteVerificationBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const invite = await this.db.getInviteToken(req.body.token);
const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token });
return res.success({
msg: this.stringService.inviteVerified,
data: invite,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "inviteVerifyController"));
}
};
},
SERVICE_NAME,
"verifyInviteToken"
);
}
export default InviteController;
+11 -13
View File
@@ -1,23 +1,21 @@
import { handleError } from "./controllerUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "LogController";
const SERVICE_NAME = "JobQueueController";
class LogController {
constructor(logger) {
this.logger = logger;
class LogController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
}
getLogs = async (req, res, next) => {
try {
getLogs = this.asyncHandler(
async (req, res) => {
const logs = await this.logger.getLogs();
res.success({
msg: "Logs fetched successfully",
data: logs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getLogs"));
return;
}
};
},
SERVICE_NAME,
"getLogs"
);
}
export default LogController;
@@ -7,151 +7,135 @@ import {
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "../validation/joi.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "maintenanceWindowController";
class MaintenanceWindowController {
constructor(db, settingsService, stringService) {
this.db = db;
class MaintenanceWindowController extends BaseController {
constructor(commonDependencies, { settingsService, maintenanceWindowService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.maintenanceWindowService = maintenanceWindowService;
}
createMaintenanceWindows = async (req, res, next) => {
try {
createMaintenanceWindows = this.asyncHandler(
async (req, res) => {
await createMaintenanceWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { teamId } = req.user;
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return this.db.createMaintenanceWindow({
teamId,
monitorId,
name: req.body.name,
active: req.body.active ? req.body.active : true,
repeat: req.body.repeat,
start: req.body.start,
end: req.body.end,
});
});
await Promise.all(dbTransactions);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body });
return res.success({
msg: this.stringService.maintenanceWindowCreate,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMaintenanceWindow"));
}
};
},
SERVICE_NAME,
"createMaintenanceWindows"
);
getMaintenanceWindowById = async (req, res, next) => {
try {
getMaintenanceWindowById = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindow = await this.db.getMaintenanceWindowById(req.params.id);
const teamId = req.user.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId });
return res.success({
msg: this.stringService.maintenanceWindowGetById,
data: maintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowById"));
}
};
},
SERVICE_NAME,
"getMaintenanceWindowById"
);
getMaintenanceWindowsByTeamId = async (req, res, next) => {
try {
getMaintenanceWindowsByTeamId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { teamId } = req.user;
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query });
return res.success({
msg: this.stringService.maintenanceWindowGetByTeam,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByUserId"));
}
};
},
SERVICE_NAME,
"getMaintenanceWindowsByTeamId"
);
getMaintenanceWindowsByMonitorId = async (req, res, next) => {
try {
getMaintenanceWindowsByMonitorId = this.asyncHandler(
async (req, res) => {
await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(
req.params.monitorId
);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId });
return res.success({
msg: this.stringService.maintenanceWindowGetByUser,
data: maintenanceWindows,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMaintenanceWindowsByMonitorId"));
}
};
},
SERVICE_NAME,
"getMaintenanceWindowsByMonitorId"
);
deleteMaintenanceWindow = async (req, res, next) => {
try {
deleteMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
await this.db.deleteMaintenanceWindowById(req.params.id);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId });
return res.success({
msg: this.stringService.maintenanceWindowDelete,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMaintenanceWindow"));
}
};
},
SERVICE_NAME,
"deleteMaintenanceWindow"
);
editMaintenanceWindow = async (req, res, next) => {
try {
editMaintenanceWindow = this.asyncHandler(
async (req, res) => {
await editMaintenanceWindowByIdParamValidation.validateAsync(req.params);
await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const editedMaintenanceWindow = await this.db.editMaintenanceWindowById(
req.params.id,
req.body
);
const teamId = req.user.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId });
return res.success({
msg: this.stringService.maintenanceWindowEdit,
data: editedMaintenanceWindow,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMaintenanceWindow"));
}
};
},
SERVICE_NAME,
"editMaintenanceWindow"
);
}
export default MaintenanceWindowController;
+238 -536
View File
@@ -4,8 +4,6 @@ import {
getMonitorsByTeamIdParamValidation,
getMonitorsByTeamIdQueryValidation,
createMonitorBodyValidation,
createMonitorsBodyValidation,
getMonitorURLByQueryValidation,
editMonitorBodyValidation,
pauseMonitorParamValidation,
getMonitorStatsByIdParamValidation,
@@ -15,106 +13,80 @@ import {
getHardwareDetailsByIdQueryValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
import logger from "../utils/logger.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
import { fetchMonitorCertificate } from "./controllerUtils.js";
import BaseController from "./baseController.js";
class MonitorController {
constructor(db, settingsService, jobQueue, stringService, emailService) {
this.db = db;
const SERVICE_NAME = "monitorController";
class MonitorController extends BaseController {
constructor(commonDependencies, { settingsService, jobQueue, emailService, monitorService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.jobQueue = jobQueue;
this.stringService = stringService;
this.emailService = emailService;
this.monitorService = monitorService;
}
/**
* Returns all monitors
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getAllMonitors = async (req, res, next) => {
try {
const monitors = await this.db.getAllMonitors();
async verifyTeamAccess(teamId, monitorId) {
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
}
getAllMonitors = this.asyncHandler(
async (req, res) => {
const monitors = await this.monitorService.getAllMonitors();
return res.success({
msg: this.stringService.monitorGetAll,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitors"));
}
};
},
SERVICE_NAME,
"getAllMonitors"
);
/**
* Returns all monitors with uptime stats for 1,7,30, and 90 days
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getAllMonitorsWithUptimeStats = async (req, res, next) => {
try {
const monitors = await this.db.getAllMonitorsWithUptimeStats();
return res.success({
msg: this.stringService.monitorGetAll,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMonitorsWithUptimeStats"));
}
};
getUptimeDetailsById = this.asyncHandler(
async (req, res) => {
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
const normalize = req?.query?.normalize;
getUptimeDetailsById = async (req, res, next) => {
try {
const { monitorId } = req.params;
const { dateRange, normalize } = req.query;
const teamId = req?.user?.teamId;
const data = await this.db.getUptimeDetailsById({
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const data = await this.monitorService.getUptimeDetailsById({
teamId,
monitorId,
dateRange,
normalize,
});
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data,
data: data,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorDetailsById"));
}
};
},
SERVICE_NAME,
"getUptimeDetailsById"
);
/**
* Returns monitor stats for monitor with matching ID
* @async
* @param {Express.Request} req
* @param {Express.Response} res
* @param {function} next
* @returns {Promise<Express.Response>}
* @throws {Error}
*/
getMonitorStatsById = async (req, res, next) => {
try {
getMonitorStatsById = this.asyncHandler(
async (req, res) => {
await getMonitorStatsByIdParamValidation.validateAsync(req.params);
await getMonitorStatsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
const { monitorId } = req.params;
const monitorId = req?.params?.monitorId;
const monitorStats = await this.db.getMonitorStatsById({
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitorStats = await this.monitorService.getMonitorStatsById({
teamId,
monitorId,
limit,
sortOrder,
@@ -122,14 +94,15 @@ class MonitorController {
numToDisplay,
normalize,
});
return res.success({
msg: this.stringService.monitorStatsById,
data: monitorStats,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorStatsById"));
}
};
},
SERVICE_NAME,
"getMonitorStatsById"
);
/**
* Get hardware details for a specific monitor by ID
@@ -140,35 +113,37 @@ class MonitorController {
* @returns {Promise<Express.Response>}
* @throws {Error} - Throws error if monitor not found or other database errors
*/
getHardwareDetailsById = async (req, res, next) => {
try {
getHardwareDetailsById = this.asyncHandler(
async (req, res) => {
await getHardwareDetailsByIdParamValidation.validateAsync(req.params);
await getHardwareDetailsByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorId } = req.params;
const { dateRange } = req.query;
const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
const monitorId = req?.params?.monitorId;
const dateRange = req?.query?.dateRange;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getHardwareDetailsById({
teamId,
monitorId,
dateRange,
});
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getHardwareDetailsById"));
}
};
},
SERVICE_NAME,
"getHardwareDetailsById"
);
getMonitorCertificate = async (req, res, next, fetchMonitorCertificate) => {
try {
getMonitorCertificate = this.asyncHandler(
async (req, res) => {
await getCertificateParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
const { monitorId } = req.params;
const monitor = await this.db.getMonitorById(monitorId);
const certificate = await fetchMonitorCertificate(sslChecker, monitor);
@@ -179,497 +154,249 @@ class MonitorController {
certificateDate: new Date(certificate.validTo),
},
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorCertificate"));
}
};
},
SERVICE_NAME,
"getMonitorCertificate"
);
/**
* Retrieves a monitor by its ID.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be retrieved.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the retrieved monitor data.
* @throws {Error} If there is an error during the process, especially if the monitor is not found (404) or if there is a validation error (422).
*/
getMonitorById = async (req, res, next) => {
try {
getMonitorById = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await getMonitorByIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitor = await this.db.getMonitorById(req.params.monitorId);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId });
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorById"));
}
};
},
SERVICE_NAME,
"getMonitorById"
);
/**
* Creates a new monitor and adds it to the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications associated with the monitor.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the creation of the monitor, and the created monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
createMonitor = async (req, res, next) => {
try {
createMonitor = this.asyncHandler(
async (req, res) => {
await createMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { _id, teamId } = req.user;
const monitor = await this.db.createMonitor({
body: req.body,
teamId,
userId: _id,
});
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body });
// Add monitor to job queue
this.jobQueue.addJob(monitor._id, monitor);
return res.success({
msg: this.stringService.monitorCreate,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createMonitor"));
}
};
},
SERVICE_NAME,
"createMonitor"
);
/**
* Creates bulk monitors and adds them to the job queue after parsing CSV.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.file - The uploaded CSV file.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and message.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
createBulkMonitors = async (req, res, next) => {
try {
const { parse } = pkg;
// validate the file
createBulkMonitors = this.asyncHandler(
async (req, res) => {
if (!req.file) {
throw new Error("No file uploaded");
throw this.errorService.createBadRequestError("No file uploaded");
}
// Check if the file is a CSV
if (!req.file.mimetype.includes("csv")) {
throw new Error("File is not a CSV");
throw this.errorService.createBadRequestError("File is not a CSV");
}
// Validate if the file is empty
if (req.file.size === 0) {
throw new Error("File is empty");
throw this.errorService.createBadRequestError("File is empty");
}
const { _id, teamId } = req.user;
const userId = req?.user?._id;
const teamId = req?.user?.teamId;
if (!_id || !teamId) {
throw new Error("Missing userId or teamId");
if (!userId || !teamId) {
throw this.errorService.createBadRequestError("Missing userId or teamId");
}
// Get file buffer from memory and convert to string
const fileData = req.file.buffer.toString("utf-8");
const fileData = req?.file?.buffer?.toString("utf-8");
if (!fileData) {
throw this.errorService.createBadRequestError("Cannot get file from buffer");
}
// Parse the CSV data
parse(fileData, {
header: true,
skipEmptyLines: true,
transform: (value, header) => {
if (value === "") return undefined; // Empty fields become undefined
const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId });
// Handle 'port' and 'interval' fields, check if they're valid numbers
if (["port", "interval"].includes(header)) {
const num = parseInt(value, 10);
if (isNaN(num)) {
throw new Error(`${header} should be a valid number, got: ${value}`);
}
return num;
}
return value;
},
complete: async ({ data, errors }) => {
try {
if (errors.length > 0) {
throw new Error("Error parsing CSV");
}
if (!data || data.length === 0) {
throw new Error("CSV file contains no data rows");
}
const enrichedData = data.map((monitor) => ({
userId: _id,
teamId,
...monitor,
description: monitor.description || monitor.name || monitor.url,
name: monitor.name || monitor.url,
type: monitor.type || "http",
}));
await createMonitorsBodyValidation.validateAsync(enrichedData);
try {
const monitors = await this.db.createBulkMonitors(enrichedData);
await Promise.all(
monitors.map(async (monitor, index) => {
this.jobQueue.addJob(monitor._id, monitor);
})
);
return res.success({
msg: this.stringService.bulkMonitorsCreate,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
}
} catch (error) {
next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
}
},
});
} catch (error) {
return next(handleError(error, SERVICE_NAME, "createBulkMonitors"));
}
};
/**
* Checks if the endpoint can be resolved
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.query - The query parameters of the request.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message, and the resolution result.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
checkEndpointResolution = async (req, res, next) => {
try {
await getMonitorURLByQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { monitorURL } = req.query;
const parsedUrl = new URL(monitorURL);
const response = await axios.get(parsedUrl, {
timeout: 5000,
validateStatus: () => true,
});
return res.success({
status: response.status,
msg: response.statusText,
msg: this.stringService.bulkMonitorsCreate,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
}
};
},
SERVICE_NAME,
"createBulkMonitors"
);
/**
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be deleted.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and a message indicating the deletion of the monitor.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422) or an error in deleting associated records.
*/
deleteMonitor = async (req, res, next) => {
try {
deleteMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const monitorId = req.params.monitorId;
const monitor = await this.db.deleteMonitor({ monitorId });
await this.jobQueue.deleteJob(monitor);
await this.db.deleteStatusPagesByMonitorId(monitor._id);
return res.success({ msg: this.stringService.monitorDelete });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
}
};
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId });
return res.success({ msg: this.stringService.monitorDelete, data: deletedMonitor });
},
SERVICE_NAME,
"deleteMonitor"
);
deleteAllMonitors = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const deletedCount = await this.monitorService.deleteAllMonitors({ teamId });
/**
* Deletes all monitors associated with a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next
* @returns {Object} The response object with a success status and a message indicating the number of deleted monitors.
* @throws {Error} If there is an error during the deletion process.
*/
deleteAllMonitors = async (req, res, next) => {
try {
const { teamId } = req.user;
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
await Promise.all(
monitors.map(async (monitor) => {
try {
await this.jobQueue.deleteJob(monitor);
await this.db.deleteChecks(monitor._id);
await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
await this.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error({
message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
service: SERVICE_NAME,
method: "deleteAllMonitors",
stack: error.stack,
});
}
})
);
return res.success({ msg: `Deleted ${deletedCount} monitors` });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteAllMonitors"));
}
};
},
SERVICE_NAME,
"deleteAllMonitors"
);
/**
* Edits a monitor by its ID, updates its notifications, and updates its job in the job queue.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be edited.
* @property {Object} req.body - The body of the request.
* @property {Array} req.body.notifications - The notifications to be associated with the monitor.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the editing of the monitor, and the edited monitor data.
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
*/
editMonitor = async (req, res, next) => {
try {
editMonitor = this.asyncHandler(
async (req, res) => {
await getMonitorByIdParamValidation.validateAsync(req.params);
await editMonitorBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
const monitorId = req?.params?.monitorId;
try {
const { monitorId } = req.params;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
await this.jobQueue.updateJob(editedMonitor);
const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body });
return res.success({
msg: this.stringService.monitorEdit,
data: editedMonitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editMonitor"));
}
};
},
SERVICE_NAME,
"editMonitor"
);
/**
* Pauses or resumes a monitor based on its current state.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.params - The parameters of the request.
* @property {string} req.params.monitorId - The ID of the monitor to be paused or resumed.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the new state of the monitor, and the updated monitor data.
* @throws {Error} If there is an error during the process.
*/
pauseMonitor = async (req, res, next) => {
try {
pauseMonitor = this.asyncHandler(
async (req, res) => {
await pauseMonitorParamValidation.validateAsync(req.params);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
const monitorId = req.params.monitorId;
const monitor = await this.db.pauseMonitor({ monitorId });
monitor.isActive === true
? await this.jobQueue.resumeJob(monitor._id, monitor)
: await this.jobQueue.pauseJob(monitor);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId });
return res.success({
msg: monitor.isActive
? this.stringService.monitorResume
: this.stringService.monitorPause,
msg: monitor.isActive ? this.stringService.monitorResume : this.stringService.monitorPause,
data: monitor,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "pauseMonitor"));
}
};
},
SERVICE_NAME,
"pauseMonitor"
);
/**
* Adds demo monitors for a team.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.headers - The headers of the request.
* @property {string} req.headers.authorization - The authorization header containing the JWT token.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status, a message indicating the addition of demo monitors, and the number of demo monitors added.
* @throws {Error} If there is an error during the process.
*/
addDemoMonitors = async (req, res, next) => {
try {
addDemoMonitors = this.asyncHandler(
async (req, res) => {
const { _id, teamId } = req.user;
const demoMonitors = await this.db.addDemoMonitors(_id, teamId);
await Promise.all(
demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))
);
const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId });
return res.success({
msg: this.stringService.monitorDemoAdded,
data: demoMonitors.length,
data: demoMonitors?.length ?? 0,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addDemoMonitors"));
}
};
},
SERVICE_NAME,
"addDemoMonitors"
);
/**
* Sends a test email to verify email delivery functionality.
* @async
* @param {Object} req - The Express request object.
* @property {Object} req.body - The body of the request.
* @property {string} req.body.to - The email address to send the test email to.
* @param {Object} res - The Express response object.
* @param {function} next - The next middleware function.
* @returns {Object} The response object with a success status and the email delivery message ID.
* @throws {Error} If there is an error while sending the test email.
*/
sendTestEmail = async (req, res, next) => {
try {
sendTestEmail = this.asyncHandler(
async (req, res) => {
const { to } = req.body;
if (!to || typeof to !== "string") {
throw new Error(this.stringService.errorForValidEmailAddress);
}
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
const html = await this.emailService.buildEmail("testEmailTemplate", context);
const messageId = await this.emailService.sendEmail(to, subject, html);
if (!messageId) {
return res.error({
msg: "Failed to send test email.",
});
throw this.errorService.createBadRequestError(this.stringService.errorForValidEmailAddress);
}
const messageId = await this.monitorService.sendTestEmail({ to });
return res.success({
msg: this.stringService.sendTestEmail,
data: { messageId },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "sendTestEmail"));
}
};
},
SERVICE_NAME,
"sendTestEmail"
);
getMonitorsByTeamId = async (req, res, next) => {
try {
getMonitorsByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
}
try {
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const teamId = req.user.teamId;
const teamId = req?.user?.teamId;
const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order });
const monitors = await this.db.getMonitorsByTeamId({
limit,
type,
page,
rowsPerPage,
filter,
field,
order,
teamId,
});
return res.success({
msg: this.stringService.monitorGetByTeamId,
data: monitors,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamId"));
}
};
},
SERVICE_NAME,
"getMonitorsByTeamId"
);
getMonitorsAndSummaryByTeamId = async (req, res, next) => {
try {
getMonitorsAndSummaryByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
return next(handleValidationError(error, SERVICE_NAME));
}
try {
const { explain } = req;
const { type } = req.query;
const { teamId } = req.user;
const explain = req?.query?.explain;
const type = req?.query?.type;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain });
const result = await this.db.getMonitorsAndSummaryByTeamId({
type,
explain,
teamId,
});
return res.success({
msg: "OK", // TODO
data: result,
});
} catch (error) {
return next(handleError(error, SERVICE_NAME, "getMonitorsAndSummaryByTeamId"));
}
};
},
SERVICE_NAME,
"getMonitorsAndSummaryByTeamId"
);
getMonitorsWithChecksByTeamId = async (req, res, next) => {
try {
getMonitorsWithChecksByTeamId = this.asyncHandler(
async (req, res) => {
await getMonitorsByTeamIdParamValidation.validateAsync(req.params);
await getMonitorsByTeamIdQueryValidation.validateAsync(req.query);
} catch (error) {
return next(handleValidationError(error, SERVICE_NAME));
}
try {
const { explain } = req;
const explain = req?.query?.explain;
let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
const { teamId } = req.user;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const result = await this.db.getMonitorsWithChecksByTeamId({
const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({
teamId,
limit,
type,
page,
@@ -677,51 +404,26 @@ class MonitorController {
filter,
field,
order,
teamId,
explain,
});
return res.success({
msg: "OK",
data: result,
data: monitors,
});
} catch (error) {
return next(handleError(error, SERVICE_NAME, "getMonitorsWithChecksByTeamId"));
}
};
},
SERVICE_NAME,
"getMonitorsWithChecksByTeamId"
);
seedDb = async (req, res, next) => {
try {
const { _id, teamId } = req.user;
await seedDb(_id, teamId);
res.success({ msg: "Database seeded" });
} catch (error) {
next(handleError(error, SERVICE_NAME, "seedDb"));
}
};
exportMonitorsToCSV = async (req, res, next) => {
try {
const { teamId } = req.user;
const monitors = await this.db.getMonitorsByTeamId({ teamId });
if (!monitors || monitors.length === 0) {
return res.success({
msg: this.stringService.noMonitorsFound,
data: null,
});
exportMonitorsToCSV = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const csvData = monitors?.filteredMonitors?.map((monitor) => ({
name: monitor.name,
description: monitor.description,
type: monitor.type,
url: monitor.url,
interval: monitor.interval,
port: monitor.port,
ignoreTlsErrors: monitor.ignoreTlsErrors,
isActive: monitor.isActive,
}));
const csv = pkg.unparse(csvData);
const csv = await this.monitorService.exportMonitorsToCSV({ teamId });
return res.file({
data: csv,
@@ -730,10 +432,10 @@ class MonitorController {
"Content-Disposition": "attachment; filename=monitors.csv",
},
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV"));
}
};
},
SERVICE_NAME,
"exportMonitorsToCSV"
);
}
export default MonitorController;
+112 -84
View File
@@ -1,147 +1,175 @@
import {
triggerNotificationBodyValidation,
createNotificationBodyValidation,
} from "../validation/joi.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
import { createNotificationBodyValidation } from "../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "NotificationController";
const NOTIFICATION_TYPES = {
WEBHOOK: "webhook",
TELEGRAM: "telegram",
};
const PLATFORMS = {
SLACK: "slack",
DISCORD: "discord",
TELEGRAM: "telegram",
};
class NotificationController {
constructor({ notificationService, stringService, statusService, db }) {
class NotificationController extends BaseController {
constructor(commonDependencies, { notificationService, statusService }) {
super(commonDependencies);
this.notificationService = notificationService;
this.stringService = stringService;
this.statusService = statusService;
this.db = db;
}
testNotification = async (req, res, next) => {
try {
testNotification = this.asyncHandler(
async (req, res) => {
const notification = req.body;
const success = await this.notificationService.sendTestNotification(notification);
if (!success) {
return res.error({
msg: "Sending notification failed",
status: 400,
});
throw this.errorService.createServerError("Sending notification failed");
}
return res.success({
msg: "Notification sent successfully",
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "testWebhook"));
}
};
},
SERVICE_NAME,
"testNotification"
);
createNotification = async (req, res, next) => {
try {
createNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const body = req.body;
const { _id, teamId } = req.user;
body.userId = _id;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const userId = req?.user?._id;
if (!userId) {
throw this.errorService.createBadRequestError("User ID is required");
}
body.userId = userId;
body.teamId = teamId;
const notification = await this.db.createNotification(body);
return res.success({
msg: "Notification created successfully",
data: notification,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createNotification"));
}
};
},
SERVICE_NAME,
"createNotification"
);
getNotificationsByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notifications = await this.db.getNotificationsByTeamId(teamId);
getNotificationsByTeamId = async (req, res, next) => {
try {
const notifications = await this.db.getNotificationsByTeamId(req.user.teamId);
return res.success({
msg: "Notifications fetched successfully",
data: notifications,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId"));
}
};
},
SERVICE_NAME,
"getNotificationsByTeamId"
);
deleteNotification = this.asyncHandler(
async (req, res) => {
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
deleteNotification = async (req, res, next) => {
try {
await this.db.deleteNotificationById(req.params.id);
return res.success({
msg: "Notification deleted successfully",
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteNotification"));
}
};
},
SERVICE_NAME,
"deleteNotification"
);
getNotificationById = async (req, res, next) => {
try {
getNotificationById = this.asyncHandler(
async (req, res) => {
const notification = await this.db.getNotificationById(req.params.id);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
if (!notification.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
return res.success({
msg: "Notification fetched successfully",
data: notification,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getNotificationById"));
}
};
},
SERVICE_NAME,
"getNotificationById"
);
editNotification = async (req, res, next) => {
try {
editNotification = this.asyncHandler(
async (req, res) => {
await createNotificationBodyValidation.validateAsync(req.body, {
abortEarly: false,
});
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const notification = await this.db.editNotification(req.params.id, req.body);
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
const notification = await this.db.getNotificationById(req.params.id);
if (!notification.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
const editedNotification = await this.db.editNotification(req.params.id, req.body);
return res.success({
msg: "Notification updated successfully",
data: notification,
data: editedNotification,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "editNotification"));
}
};
},
SERVICE_NAME,
"editNotification"
);
testAllNotifications = this.asyncHandler(
async (req, res) => {
const monitorId = req.body.monitorId;
const teamId = req?.user?.teamId;
if (!teamId) {
throw this.errorService.createBadRequestError("Team ID is required");
}
testAllNotifications = async (req, res, next) => {
try {
const { monitorId } = req.body;
const monitor = await this.db.getMonitorById(monitorId);
if (!monitor.teamId.equals(teamId)) {
throw this.errorService.createAuthorizationError();
}
const notifications = monitor.notifications;
if (notifications.length === 0) throw new Error("No notifications");
if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications");
const result = await this.notificationService.testAllNotifications(notifications);
if (!result) throw new Error("Failed to send all notifications");
if (!result) throw this.errorService.createServerError("Failed to send all notifications");
return res.success({
msg: "All notifications sent successfully",
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "testAllNotifications"));
}
};
},
SERVICE_NAME,
"testAllNotifications"
);
}
export default NotificationController;
+40 -47
View File
@@ -1,89 +1,82 @@
import { handleError } from "./controllerUtils.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "JobQueueController";
class JobQueueController {
constructor(jobQueue, stringService) {
class JobQueueController extends BaseController {
constructor(commonDependencies, { jobQueue }) {
super(commonDependencies);
this.jobQueue = jobQueue;
this.stringService = stringService;
}
getMetrics = async (req, res, next) => {
try {
getMetrics = this.asyncHandler(
async (req, res) => {
const metrics = await this.jobQueue.getMetrics();
res.success({
msg: this.stringService.queueGetMetrics,
data: metrics,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
}
};
},
SERVICE_NAME,
"getMetrics"
);
getJobs = async (req, res, next) => {
try {
getJobs = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
return res.success({
msg: this.stringService.queueGetJobs,
data: jobs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
}
};
},
SERVICE_NAME,
"getJobs"
);
getAllMetrics = async (req, res, next) => {
try {
getAllMetrics = this.asyncHandler(
async (req, res) => {
const jobs = await this.jobQueue.getJobs();
const metrics = await this.jobQueue.getMetrics();
return res.success({
msg: this.stringService.queueGetAllMetrics,
data: { jobs, metrics },
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getAllMetrics"));
return;
}
};
},
SERVICE_NAME,
"getAllMetrics"
);
addJob = async (req, res, next) => {
try {
addJob = this.asyncHandler(
async (req, res) => {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.success({
msg: this.stringService.queueAddJob,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addJob"));
return;
}
};
},
SERVICE_NAME,
"addJob"
);
flushQueue = async (req, res, next) => {
try {
flushQueue = this.asyncHandler(
async (req, res) => {
const result = await this.jobQueue.flushQueues();
return res.success({
msg: this.stringService.jobQueueFlush,
data: result,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "flushQueue"));
return;
}
};
},
SERVICE_NAME,
"flushQueue"
);
checkQueueHealth = async (req, res, next) => {
try {
checkQueueHealth = this.asyncHandler(
async (req, res) => {
const stuckQueues = await this.jobQueue.checkQueueHealth();
return res.success({
msg: this.stringService.queueHealthCheck,
data: stuckQueues,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "checkQueueHealth"));
return;
}
};
},
SERVICE_NAME,
"checkQueueHealth"
);
}
export default JobQueueController;
+30 -39
View File
@@ -1,13 +1,13 @@
import { updateAppSettingsBodyValidation } from "../validation/joi.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
import { sendTestEmailBodyValidation } from "../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "SettingsController";
class SettingsController {
constructor({ db, settingsService, stringService, emailService }) {
this.db = db;
class SettingsController extends BaseController {
constructor(commonDependencies, { settingsService, emailService }) {
super(commonDependencies);
this.settingsService = settingsService;
this.stringService = stringService;
this.emailService = emailService;
}
@@ -32,45 +32,39 @@ class SettingsController {
return returnSettings;
};
getAppSettings = async (req, res, next) => {
const dbSettings = await this.settingsService.getDBSettings();
getAppSettings = this.asyncHandler(
async (req, res) => {
const dbSettings = await this.settingsService.getDBSettings();
const returnSettings = this.buildAppSettings(dbSettings);
return res.success({
msg: this.stringService.getAppSettings,
data: returnSettings,
});
};
const returnSettings = this.buildAppSettings(dbSettings);
return res.success({
msg: this.stringService.getAppSettings,
data: returnSettings,
});
},
SERVICE_NAME,
"getAppSettings"
);
updateAppSettings = async (req, res, next) => {
try {
updateAppSettings = this.asyncHandler(
async (req, res) => {
await updateAppSettingsBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const updatedSettings = await this.db.updateAppSettings(req.body);
const returnSettings = this.buildAppSettings(updatedSettings);
return res.success({
msg: this.stringService.updateAppSettings,
data: returnSettings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
}
};
},
SERVICE_NAME,
"updateAppSettings"
);
sendTestEmail = async (req, res, next) => {
try {
sendTestEmail = this.asyncHandler(
async (req, res) => {
await sendTestEmailBodyValidation.validateAsync(req.body);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const {
to,
systemEmailHost,
@@ -107,20 +101,17 @@ class SettingsController {
});
if (!messageId) {
return res.error({
msg: "Failed to send test email.",
});
throw this.errorService.createServerError("Failed to send test email.");
}
return res.success({
msg: this.stringService.sendTestEmail,
data: { messageId },
});
} catch (error) {
next(handleError(error, SERVICE_NAME));
return;
}
};
},
SERVICE_NAME,
"sendTestEmail"
);
}
export default SettingsController;
+42 -65
View File
@@ -1,29 +1,18 @@
import { handleError, handleValidationError } from "./controllerUtils.js";
import {
createStatusPageBodyValidation,
getStatusPageParamValidation,
getStatusPageQueryValidation,
imageValidation,
} from "../validation/joi.js";
import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js";
import BaseController from "./baseController.js";
const SERVICE_NAME = "statusPageController";
class StatusPageController {
constructor(db, stringService) {
this.db = db;
this.stringService = stringService;
class StatusPageController extends BaseController {
constructor(commonDependencies) {
super(commonDependencies);
}
createStatusPage = async (req, res, next) => {
try {
createStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const { _id, teamId } = req.user;
const statusPage = await this.db.createStatusPage({
statusPageData: req.body,
@@ -35,70 +24,58 @@ class StatusPageController {
msg: this.stringService.statusPageCreate,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "createStatusPage"));
}
};
},
SERVICE_NAME,
"createStatusPage"
);
updateStatusPage = async (req, res, next) => {
try {
updateStatusPage = this.asyncHandler(
async (req, res) => {
await createStatusPageBodyValidation.validateAsync(req.body);
await imageValidation.validateAsync(req.file);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.updateStatusPage(req.body, req.file);
if (statusPage === null) {
const error = new Error(this.stringService.statusPageNotFound);
error.status = 404;
throw error;
throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound);
}
return res.success({
msg: this.stringService.statusPageUpdate,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateStatusPage"));
}
};
},
SERVICE_NAME,
"updateStatusPage"
);
getStatusPage = async (req, res, next) => {
try {
getStatusPage = this.asyncHandler(
async (req, res) => {
const statusPage = await this.db.getStatusPage();
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPage"));
}
};
},
SERVICE_NAME,
"getStatusPage"
);
getStatusPageByUrl = async (req, res, next) => {
try {
getStatusPageByUrl = this.asyncHandler(
async (req, res) => {
await getStatusPageParamValidation.validateAsync(req.params);
await getStatusPageQueryValidation.validateAsync(req.query);
} catch (error) {
next(handleValidationError(error, SERVICE_NAME));
return;
}
try {
const statusPage = await this.db.getStatusPageByUrl(req.params.url, req.query.type);
return res.success({
msg: this.stringService.statusPageByUrl,
data: statusPage,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPageByUrl"));
}
};
},
SERVICE_NAME,
"getStatusPageByUrl"
);
getStatusPagesByTeamId = async (req, res, next) => {
try {
getStatusPagesByTeamId = this.asyncHandler(
async (req, res) => {
const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
@@ -106,21 +83,21 @@ class StatusPageController {
msg: this.stringService.statusPageByTeamId,
data: statusPages,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getStatusPageByTeamId"));
}
};
},
SERVICE_NAME,
"getStatusPagesByTeamId"
);
deleteStatusPage = async (req, res, next) => {
try {
deleteStatusPage = this.asyncHandler(
async (req, res) => {
await this.db.deleteStatusPage(req.params.url);
return res.success({
msg: this.stringService.statusPageDelete,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteStatusPage"));
}
};
},
SERVICE_NAME,
"deleteStatusPage"
);
}
export default StatusPageController;
+9 -8
View File
@@ -126,18 +126,22 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
try {
const doc = await this.model.findOne(this.getFilter());
if (doc.type === "pagespeed") {
if (!doc) {
throw new Error("Monitor not found");
}
if (doc?.type === "pagespeed") {
await PageSpeedCheck.deleteMany({ monitorId: doc._id });
} else if (doc.type === "hardware") {
} else if (doc?.type === "hardware") {
await HardwareCheck.deleteMany({ monitorId: doc._id });
} else {
await Check.deleteMany({ monitorId: doc._id });
}
// Deal with status pages
await StatusPage.updateMany({ monitors: doc._id }, { $pull: { monitors: doc._id } });
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
await MonitorStats.deleteMany({ monitorId: doc._id.toString() });
await MonitorStats.deleteMany({ monitorId: doc?._id.toString() });
next();
} catch (error) {
next(error);
@@ -156,10 +160,7 @@ MonitorSchema.pre("deleteMany", async function (next) {
} else {
await Check.deleteMany({ monitorId: monitor._id });
}
await StatusPage.updateMany(
{ monitors: monitor._id },
{ $pull: { monitors: monitor._id } }
);
await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } });
await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
}
next();
+1 -9
View File
@@ -89,8 +89,7 @@ class MongoDB {
connect = async () => {
try {
const connectionString =
this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
const connectionString = this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
await mongoose.connect(connectionString);
// If there are no AppSettings, create one
await AppSettings.findOneAndUpdate(
@@ -139,13 +138,6 @@ class MongoDB {
});
}
};
checkSuperadmin = async (req, res) => {
const superAdmin = await UserModel.findOne({ role: "superadmin" });
if (superAdmin !== null) {
return true;
}
return false;
};
}
export default MongoDB;
+7 -40
View File
@@ -58,26 +58,13 @@ const createChecks = async (checks) => {
* @returns {Promise<Array<Check>>}
* @throws {Error}
*/
const getChecksByMonitor = async ({
monitorId,
type,
sortOrder,
dateRange,
filter,
ack,
page,
rowsPerPage,
status,
}) => {
const getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => {
try {
status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage =
ack === "true"
? { ack: true }
: { $or: [{ ack: false }, { ack: { $exists: false } }] };
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
// Match
const matchStage = {
@@ -158,23 +145,12 @@ const getChecksByMonitor = async ({
}
};
const getChecksByTeam = async ({
sortOrder,
dateRange,
filter,
ack,
page,
rowsPerPage,
teamId,
}) => {
const getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => {
try {
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
const ackStage =
ack === "true"
? { ack: true }
: { $or: [{ ack: false }, { ack: { $exists: false } }] };
const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] };
const matchStage = {
teamId: new ObjectId(teamId),
@@ -264,11 +240,7 @@ const getChecksByTeam = async ({
*/
const ackCheck = async (checkId, teamId, ack) => {
try {
const updatedCheck = await Check.findOneAndUpdate(
{ _id: checkId, teamId: teamId },
{ $set: { ack, ackAt: new Date() } },
{ new: true }
);
const updatedCheck = await Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true });
if (!updatedCheck) {
throw new Error("Check not found");
@@ -293,10 +265,7 @@ const ackCheck = async (checkId, teamId, ack) => {
*/
const ackAllChecks = async (monitorId, teamId, ack, path) => {
try {
const updatedChecks = await Check.updateMany(
path === "monitor" ? { monitorId } : { teamId },
{ $set: { ack, ackAt: new Date() } }
);
const updatedChecks = await Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } });
return updatedChecks.modifiedCount;
} catch (error) {
error.service = SERVICE_NAME;
@@ -317,9 +286,7 @@ const getChecksSummaryByTeamId = async ({ teamId }) => {
const matchStage = {
teamId: new ObjectId(teamId),
};
const checks = await Check.aggregate(
buildChecksSummaryByTeamIdPipeline({ matchStage })
);
const checks = await Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage }));
return checks[0].summary;
} catch (error) {
error.service = SERVICE_NAME;
+2 -10
View File
@@ -10,20 +10,12 @@ const buildChecksSummaryByTeamIdPipeline = ({ matchStage }) => {
totalChecks: { $sum: { $cond: [{ $eq: ["$status", false] }, 1, 0] } },
resolvedChecks: {
$sum: {
$cond: [
{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] },
1,
0,
],
$cond: [{ $and: [{ $eq: ["$ack", true] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
downChecks: {
$sum: {
$cond: [
{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] },
1,
0,
],
$cond: [{ $and: [{ $eq: ["$ack", false] }, { $eq: ["$status", false] }] }, 1, 0],
},
},
cannotResolveChecks: {
+3 -11
View File
@@ -2,11 +2,7 @@ import Monitor from "../../models/Monitor.js";
import { ObjectId } from "mongodb";
const SERVICE_NAME = "diagnosticModule";
import {
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
} from "./monitorModuleQueries.js";
import { buildMonitorSummaryByTeamIdPipeline, buildMonitorsByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline } from "./monitorModuleQueries.js";
const getMonitorsByTeamIdExecutionStats = async (req) => {
try {
@@ -24,13 +20,9 @@ const getMonitorsByTeamIdExecutionStats = async (req) => {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
const summary = await Monitor.aggregate(
buildMonitorSummaryByTeamIdPipeline({ matchStage })
).explain("executionStats");
const summary = await Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })).explain("executionStats");
const monitors = await Monitor.aggregate(
buildMonitorsByTeamIdPipeline({ matchStage, field, order })
).explain("executionStats");
const monitors = await Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })).explain("executionStats");
const filteredMonitors = await Monitor.aggregate(
buildFilteredMonitorsByTeamIdPipeline({
Loaded 100 of 198 files, more files were not shown because too many files have changed in this diff. Show more