Merge branch 'develop' of https://github.com/Owaiseimdad/Checkmate into develop
This commit is contained in:
commit
729f107af2
198 files changed
+6942
-6232
No files matched your search
+172
@@ -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://www.bestpractices.dev/projects/9901)
|
||||
[](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:
|
||||
|
||||

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

|
||||
|
||||
## 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>
|
||||
|
||||
[](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.
|
||||
@@ -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
|
||||
|
||||

|
||||
|
||||
## 💚 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:
|
||||
|
||||
[](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
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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;
|
||||
@@ -38,7 +38,3 @@
|
||||
background-size: cover;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.fallback__status > .MuiStack-root {
|
||||
margin-left: var(--env-var-spacing-2);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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" },
|
||||
}}
|
||||
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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 };
|
||||
@@ -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";
|
||||
}, []);
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
@@ -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;
|
||||
+3
-3
@@ -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";
|
||||
+5
-6
@@ -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;
|
||||
+19
-7
@@ -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>
|
||||
|
||||
@@ -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,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)}
|
||||
>
|
||||
|
||||
@@ -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`;
|
||||
};
|
||||
@@ -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;
|
||||
@@ -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,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"
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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"]}
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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 },
|
||||
];
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"printWidth": 90,
|
||||
"printWidth": 150,
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": false,
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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,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 };
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user