Merge branch 'develop' into feat/super-simple-queue
This commit is contained in:
commit
4f186a6b49
189 files changed
+4473
-5400
No files matched your search
@@ -28,7 +28,8 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
|
||||
- [📦 Demo](#-demo)
|
||||
- [🔗 User's guide](#-users-guide)
|
||||
- [🛠️ Installation](#️-installation)
|
||||
- [🛠️ Installation](#️-installation)
|
||||
- [🚀 Deploying Checkmate with Helm](#-deploying-checkmate-with-helm)
|
||||
- [🏁 Translations](#-translations)
|
||||
- [🚀 Performance](#-performance)
|
||||
- [💚 Questions & Ideas](#-questions--ideas)
|
||||
@@ -50,7 +51,10 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
|
||||
|
||||
## 🛠️ 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) 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.
|
||||
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
|
||||
|
||||
@@ -68,14 +72,20 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
||||
|
||||
## 💚 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:
|
||||
|
||||
- [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
|
||||
@@ -130,7 +140,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
|
||||
|
||||
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!
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.DS_Store
|
||||
.git/
|
||||
.gitignore
|
||||
.bzr/
|
||||
.bzrignore
|
||||
.hg/
|
||||
.hgignore
|
||||
.svn/
|
||||
*.swp
|
||||
*.bak
|
||||
*.tmp
|
||||
*.orig
|
||||
*~
|
||||
.project
|
||||
.idea/
|
||||
*.tmproj
|
||||
.vscode/
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v2
|
||||
name: checkmate-chart
|
||||
description: A Helm chart for Checkmate App
|
||||
type: application
|
||||
version: 0.1.0
|
||||
appVersion: "2.3"
|
||||
@@ -0,0 +1,39 @@
|
||||
# Kubernetes Installation Guide for Checkmate
|
||||
|
||||
This guide walks you through deploying Checkmate on your Kubernetes cluster using Helm.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- A running Kubernetes cluster
|
||||
- Helm CLI installed and configured
|
||||
- `kubectl` configured to access your cluster
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Clone the repo and navigate to the Helm chart
|
||||
|
||||
```bash
|
||||
git clone https://github.com/bluewave-labs/checkmate.git
|
||||
cd checkmate/charts/helm/checkmate
|
||||
```
|
||||
|
||||
### 2. Customize values.yaml
|
||||
Edit `values.yaml` to update:
|
||||
- `client.ingress.host` and `server.ingress.host` with your domain names
|
||||
- `server.protocol` (usually http or https)
|
||||
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
|
||||
|
||||
### 3. Deploy the Helm chart
|
||||
```bash
|
||||
helm install checkmate ./charts/helm/checkmate
|
||||
```
|
||||
This will deploy the client, server, MongoDB, and Redis components.
|
||||
|
||||
### 4. Verify the deployment
|
||||
Check pods and services:
|
||||
```bash
|
||||
kubectl get pods
|
||||
kubectl get svc
|
||||
```
|
||||
|
||||
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
|
||||
@@ -0,0 +1,12 @@
|
||||
{{- if eq .Values.client.ingress.host "change_me" }}
|
||||
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if eq .Values.server.ingress.host "change_me" }}
|
||||
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- $protocol := .Values.server.protocol }}
|
||||
{{- if not (or (eq $protocol "http") (eq $protocol "https")) }}
|
||||
{{- fail "server.protocol must be either 'http' or 'https'" }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: client
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: client
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: client
|
||||
spec:
|
||||
containers:
|
||||
- name: client
|
||||
image: {{ .Values.client.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.client.port }}
|
||||
env:
|
||||
- name: UPTIME_APP_API_BASE_URL
|
||||
value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1"
|
||||
@@ -0,0 +1,20 @@
|
||||
{{- if .Values.client.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: client-ingress
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
spec:
|
||||
rules:
|
||||
- host: {{ .Values.client.ingress.host }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: client
|
||||
port:
|
||||
number: {{ .Values.client.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: client
|
||||
spec:
|
||||
selector:
|
||||
app: client
|
||||
ports:
|
||||
- port: {{ .Values.client.port }}
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: mongodb
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: mongodb
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: mongodb
|
||||
spec:
|
||||
containers:
|
||||
- name: mongodb
|
||||
image: {{ .Values.mongodb.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.mongodb.port }}
|
||||
command: ["mongod", "--quiet", "--bind_ip_all"]
|
||||
volumeMounts:
|
||||
- name: mongodb-data
|
||||
mountPath: /data/db
|
||||
volumes:
|
||||
- name: mongodb-data
|
||||
persistentVolumeClaim:
|
||||
claimName: mongodb-pvc
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: mongodb-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.mongodbSize }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: mongodb
|
||||
spec:
|
||||
selector:
|
||||
app: mongodb
|
||||
ports:
|
||||
- port: {{ .Values.mongodb.port }}
|
||||
@@ -0,0 +1,26 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: redis
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: redis
|
||||
spec:
|
||||
containers:
|
||||
- name: redis
|
||||
image: {{ .Values.redis.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.redis.port }}
|
||||
volumeMounts:
|
||||
- name: redis-data
|
||||
mountPath: /data
|
||||
volumes:
|
||||
- name: redis-data
|
||||
persistentVolumeClaim:
|
||||
claimName: redis-pvc
|
||||
@@ -0,0 +1,10 @@
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: redis-pvc
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
resources:
|
||||
requests:
|
||||
storage: {{ .Values.persistence.redisSize }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: redis
|
||||
spec:
|
||||
selector:
|
||||
app: redis
|
||||
ports:
|
||||
- port: {{ .Values.redis.port }}
|
||||
@@ -0,0 +1,19 @@
|
||||
{{- $secrets := .Values.secrets }}
|
||||
|
||||
{{- if or (not $secrets.JWT_SECRET) (eq $secrets.JWT_SECRET "change_me") }}
|
||||
{{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }}
|
||||
{{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }}
|
||||
{{- end }}
|
||||
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: checkmate-secrets
|
||||
type: Opaque
|
||||
stringData:
|
||||
{{- range $key, $value := $secrets }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,22 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: server
|
||||
spec:
|
||||
containers:
|
||||
- name: server
|
||||
image: {{ .Values.server.image }}
|
||||
ports:
|
||||
- containerPort: {{ .Values.server.port }}
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: checkmate-secrets
|
||||
@@ -0,0 +1,25 @@
|
||||
{{- if .Values.server.ingress.enabled }}
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
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 }}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: server
|
||||
port:
|
||||
number: {{ .Values.server.port }}
|
||||
{{- end }}
|
||||
@@ -0,0 +1,9 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: server
|
||||
spec:
|
||||
selector:
|
||||
app: server
|
||||
ports:
|
||||
- port: {{ .Values.server.port }}
|
||||
@@ -0,0 +1,41 @@
|
||||
client:
|
||||
image: ghcr.io/bluewave-labs/checkmate-client:v2.3
|
||||
port: 80
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate-backend:v2.3
|
||||
port: 52345
|
||||
protocol: change_me
|
||||
ingress:
|
||||
enabled: true
|
||||
host: change_me
|
||||
|
||||
redis:
|
||||
image: redis:7.2
|
||||
port: 6379
|
||||
|
||||
mongodb:
|
||||
image: mongo:7.0
|
||||
port: 27017
|
||||
|
||||
secrets:
|
||||
JWT_SECRET: change_me
|
||||
REFRESH_TOKEN_SECRET: change_me
|
||||
SYSTEM_EMAIL_ADDRESS: test@example.com
|
||||
SYSTEM_EMAIL_PASSWORD: change_me
|
||||
SYSTEM_EMAIL_HOST: smtp.example.com
|
||||
SYSTEM_EMAIL_PORT: "587"
|
||||
PAGESPEED_API_KEY: change_me
|
||||
DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db
|
||||
REDIS_HOST: redis
|
||||
REDIS_PORT: "6379"
|
||||
DB_TYPE: MongoDB
|
||||
TOKEN_TTL: 99d
|
||||
REFRESH_TOKEN_TTL: 99d
|
||||
|
||||
persistence:
|
||||
mongodbSize: 5Gi
|
||||
redisSize: 1Gi
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 };
|
||||
@@ -205,6 +205,10 @@ const useFetchStatsByMonitorId = ({
|
||||
const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
useEffect(() => {
|
||||
if (typeof monitorId === "undefined") {
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
const fetchMonitor = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
@@ -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 };
|
||||
@@ -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)}
|
||||
>
|
||||
|
||||
@@ -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,9 +0,0 @@
|
||||
.configure-pagespeed button {
|
||||
height: var(--env-var-height-2);
|
||||
}
|
||||
|
||||
.configure-pagespeed .field,
|
||||
.configure-pagespeed .section-disabled,
|
||||
.configure-pagespeed .select-wrapper {
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1,351 +0,0 @@
|
||||
// Components
|
||||
import { Box, Stack, Tooltip, Typography, Button } from "@mui/material";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import NotificationsConfig from "../../../Components/NotificationConfig";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useParams } from "react-router";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
|
||||
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
|
||||
import {
|
||||
useFetchMonitorById,
|
||||
useDeleteMonitor,
|
||||
useUpdateMonitor,
|
||||
usePauseMonitor,
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
const PageSpeedConfigure = () => {
|
||||
// Redux state
|
||||
|
||||
// Local state
|
||||
const [monitor, setMonitor] = useState({});
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
// Utils
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { monitorId } = useParams();
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
|
||||
|
||||
const [notifications, notificationsAreLoading, notificationsError] =
|
||||
useGetNotificationsByTeamId();
|
||||
|
||||
const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger });
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const [pauseMonitor, isPausing] = usePauseMonitor();
|
||||
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
|
||||
// Handlers
|
||||
const triggerUpdate = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const onChange = (event) => {
|
||||
let { value, name } = event.target;
|
||||
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
|
||||
const validation = monitorValidation.validate(
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => {
|
||||
const updatedErrors = { ...prev };
|
||||
|
||||
if (validation.error) updatedErrors[name] = validation.error.details[0].message;
|
||||
else delete updatedErrors[name];
|
||||
return updatedErrors;
|
||||
});
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
await pauseMonitor({ monitorId, triggerUpdate });
|
||||
};
|
||||
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
await updateMonitor({ monitor, redirect: "/pagespeed" });
|
||||
};
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
await deleteMonitor({ monitor, redirect: "/pagespeed" });
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack
|
||||
className="configure-pagespeed"
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
{Object.keys(monitor).length === 0 ? (
|
||||
<SkeletonLayout />
|
||||
) : (
|
||||
<>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "details", path: `/pagespeed/${monitorId}` },
|
||||
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
onSubmit={onSubmit}
|
||||
flex={1}
|
||||
gap={theme.spacing(10)}
|
||||
>
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="monitorName"
|
||||
>
|
||||
{monitor.name}
|
||||
</Typography>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: {
|
||||
offset: [0, -8],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: 4,
|
||||
height: 4,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
opacity: 0.8,
|
||||
left: -10,
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("editing")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isLoading}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{
|
||||
ml: theme.spacing(6),
|
||||
}}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</Box>
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("settingsGeneralSettings")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("pageSpeedConfigureSettingsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack
|
||||
gap={theme.spacing(20)}
|
||||
sx={{
|
||||
".MuiInputBase-root:has(> .Mui-disabled)": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
name="url"
|
||||
type="url"
|
||||
label={t("url")}
|
||||
placeholder="random.website.com"
|
||||
value={monitor?.url || ""}
|
||||
onChange={onChange}
|
||||
error={errors.url ? true : false}
|
||||
helperText={errors.url}
|
||||
disabled={true}
|
||||
/>
|
||||
<TextInput
|
||||
name="name"
|
||||
type="text"
|
||||
label={t("monitorDisplayName")}
|
||||
placeholder="Example monitor"
|
||||
isOptional={true}
|
||||
value={monitor?.name || ""}
|
||||
onChange={onChange}
|
||||
error={errors.name ? true : false}
|
||||
helperText={errors.name}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">{t("notificationConfig.title")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("notificationConfig.description")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<NotificationsConfig
|
||||
notifications={notifications}
|
||||
setMonitor={setMonitor}
|
||||
setNotifications={monitor.notifications}
|
||||
/>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("distributedUptimeCreateAdvancedSettings")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(20)}>
|
||||
<Select
|
||||
name="interval"
|
||||
label={t("checkFrequency")}
|
||||
items={frequencies}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
mt="auto"
|
||||
>
|
||||
<Button
|
||||
loading={isLoading || isDeleting || isUpdating || isPausing}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
sx={{ px: theme.spacing(12) }}
|
||||
>
|
||||
{t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</>
|
||||
)}
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
isLoading={isLoading || isDeleting || isUpdating || isPausing}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default PageSpeedConfigure;
|
||||
@@ -1,19 +1,20 @@
|
||||
//Components
|
||||
import Box from "@mui/material/Box";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import Button from "@mui/material/Button";
|
||||
import ButtonGroup from "@mui/material/ButtonGroup";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
// Components
|
||||
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material";
|
||||
import ConfigBox from "../../../Components/ConfigBox";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
import Select from "../../../Components/Inputs/Select";
|
||||
import TextInput from "../../../Components/Inputs/TextInput";
|
||||
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
|
||||
import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import PulseDot from "../../../Components/Animated/PulseDot";
|
||||
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
|
||||
import SkeletonLayout from "./skeleton";
|
||||
import NotificationsConfig from "../../../Components/NotificationConfig";
|
||||
import Dialog from "../../../Components/Dialog";
|
||||
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
|
||||
import Radio from "../../../Components/Inputs/Radio";
|
||||
|
||||
// Utils
|
||||
import { useState } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { parseDomainName } from "../../../Utils/monitorUtils";
|
||||
@@ -21,79 +22,130 @@ import { useTranslation } from "react-i18next";
|
||||
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { useCreateMonitor } from "../../../Hooks/monitorHooks";
|
||||
|
||||
const MS_PER_MINUTE = 60000;
|
||||
import { useParams } from "react-router";
|
||||
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
|
||||
import {
|
||||
useCreateMonitor,
|
||||
useFetchMonitorById,
|
||||
useDeleteMonitor,
|
||||
useUpdateMonitor,
|
||||
usePauseMonitor,
|
||||
} from "../../../Hooks/monitorHooks";
|
||||
|
||||
const CRUMBS = [
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "create", path: `/pagespeed/create` },
|
||||
];
|
||||
|
||||
const SELECT_VALUES = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
// State
|
||||
const [monitor, setMonitor] = useState({
|
||||
url: "",
|
||||
name: "",
|
||||
type: "pagespeed",
|
||||
notifications: [],
|
||||
interval: 3,
|
||||
});
|
||||
const PageSpeedSetup = () => {
|
||||
const { monitorId } = useParams();
|
||||
const isCreate = typeof monitorId === "undefined";
|
||||
const CRUMBS = [
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
...(isCreate
|
||||
? [{ name: "create", path: `/pagespeed/create` }]
|
||||
: [
|
||||
{ name: "details", path: `/pagespeed/${monitorId}` },
|
||||
{ name: "configure", path: `/pagespeed/configure/${monitorId}` },
|
||||
]),
|
||||
];
|
||||
|
||||
// States
|
||||
const [monitor, setMonitor] = useState(
|
||||
isCreate
|
||||
? {
|
||||
url: "",
|
||||
name: "",
|
||||
type: "pagespeed",
|
||||
notifications: [],
|
||||
interval: 180000,
|
||||
}
|
||||
: {}
|
||||
);
|
||||
const [https, setHttps] = useState(true);
|
||||
const [errors, setErrors] = useState({});
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [updateTrigger, setUpdateTrigger] = useState(false);
|
||||
|
||||
// Setup
|
||||
const { t } = useTranslation();
|
||||
const theme = useTheme();
|
||||
// Constants
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const FREQUENCIES = [
|
||||
{ _id: 3, name: t("time.threeMinutes") },
|
||||
{ _id: 5, name: t("time.fiveMinutes") },
|
||||
{ _id: 10, name: t("time.tenMinutes") },
|
||||
{ _id: 20, name: t("time.twentyMinutes") },
|
||||
{ _id: 60, name: t("time.oneHour") },
|
||||
{ _id: 1440, name: t("time.oneDay") },
|
||||
{ _id: 10080, name: t("time.oneWeek") },
|
||||
];
|
||||
|
||||
const { user } = useSelector((state) => state.auth);
|
||||
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
|
||||
const [notifications, notificationsAreLoading, notificationsError] =
|
||||
useGetNotificationsByTeamId();
|
||||
|
||||
// Hooks for API actions
|
||||
const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger });
|
||||
const [createMonitor, isCreating] = useCreateMonitor();
|
||||
const [deleteMonitor, isDeleting] = useDeleteMonitor();
|
||||
const [updateMonitor, isUpdating] = useUpdateMonitor();
|
||||
const [pauseMonitor, isPausing] = usePauseMonitor();
|
||||
|
||||
// Handlers
|
||||
const onSubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval * MS_PER_MINUTE,
|
||||
};
|
||||
if (isCreate) {
|
||||
let form = {
|
||||
url: `http${https ? "s" : ""}://` + monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval,
|
||||
};
|
||||
|
||||
const { error } = monitorValidation.validate(form, {
|
||||
abortEarly: false,
|
||||
});
|
||||
const { error } = monitorValidation.validate(form, { abortEarly: false });
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: t("checkFormError") });
|
||||
return;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Please check the form for errors." });
|
||||
return;
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
await createMonitor({ monitor: form, redirect: "/pagespeed" });
|
||||
} else {
|
||||
const monitorParams = {
|
||||
url: monitor.url,
|
||||
name: monitor.name === "" ? monitor.url : monitor.name,
|
||||
type: monitor.type,
|
||||
interval: monitor.interval,
|
||||
};
|
||||
const { error } = monitorValidation.validate(monitorParams, { abortEarly: false });
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: t("checkFormError") });
|
||||
return;
|
||||
}
|
||||
await updateMonitor({ monitor, redirect: "/pagespeed" });
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
|
||||
await createMonitor({ monitor: form, redirect: "/pagespeed" });
|
||||
};
|
||||
|
||||
const handleChange = (event) => {
|
||||
const { value, name } = event.target;
|
||||
let { value, name } = event.target;
|
||||
|
||||
if (name === "interval") {
|
||||
value = value * MS_PER_MINUTE;
|
||||
}
|
||||
|
||||
setMonitor({
|
||||
...monitor,
|
||||
[name]: value,
|
||||
@@ -103,7 +155,6 @@ const CreatePageSpeed = () => {
|
||||
{ [name]: value },
|
||||
{ abortEarly: false }
|
||||
);
|
||||
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
|
||||
@@ -111,59 +162,179 @@ const CreatePageSpeed = () => {
|
||||
};
|
||||
|
||||
const handleBlur = (event) => {
|
||||
const { name } = event.target;
|
||||
if (name === "url") {
|
||||
const { value } = event.target;
|
||||
if (monitor.name !== "") {
|
||||
return;
|
||||
}
|
||||
setMonitor((prev) => ({
|
||||
...prev,
|
||||
name: parseDomainName(value),
|
||||
}));
|
||||
const { name, value } = event.target;
|
||||
if (name === "url" && monitor.name === "") {
|
||||
setMonitor((prev) => ({ ...prev, name: parseDomainName(value) }));
|
||||
}
|
||||
};
|
||||
|
||||
const { t } = useTranslation();
|
||||
const triggerUpdate = () => {
|
||||
setUpdateTrigger(!updateTrigger);
|
||||
};
|
||||
|
||||
const handlePause = async () => {
|
||||
await pauseMonitor({ monitorId, triggerUpdate });
|
||||
};
|
||||
|
||||
const handleRemove = async (event) => {
|
||||
event.preventDefault();
|
||||
await deleteMonitor({ monitor, redirect: "/pagespeed" });
|
||||
};
|
||||
|
||||
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
|
||||
|
||||
if (Object.keys(monitor).length === 0) {
|
||||
return <SkeletonLayout />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.primary.contrastText,
|
||||
},
|
||||
"& h1": { color: theme.palette.primary.contrastText },
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs list={CRUMBS} />
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={onSubmit}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
<Stack
|
||||
direction="row"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
{t("createYour")}{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("pageSpeedMonitor")}
|
||||
</Typography>
|
||||
</Typography>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
color={
|
||||
!isCreate ? theme.palette.primary.contrastTextSecondary : undefined
|
||||
}
|
||||
>
|
||||
{!isCreate ? monitor.name : t("createYour") + " "}
|
||||
</Typography>
|
||||
{isCreate ? (
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.primary.contrastTextSecondary}
|
||||
>
|
||||
{t("pageSpeedMonitor")}
|
||||
</Typography>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
</Typography>
|
||||
{!isCreate && (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
height="fit-content"
|
||||
gap={theme.spacing(2)}
|
||||
>
|
||||
<Tooltip
|
||||
title={pagespeedStatusMsg[determineState(monitor)]}
|
||||
disableInteractive
|
||||
slotProps={{
|
||||
popper: {
|
||||
modifiers: [
|
||||
{
|
||||
name: "offset",
|
||||
options: { offset: [0, -8] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<PulseDot color={statusColor[determineState(monitor)]} />
|
||||
</Box>
|
||||
</Tooltip>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="monitorUrl"
|
||||
>
|
||||
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
|
||||
</Typography>
|
||||
<Typography
|
||||
position="relative"
|
||||
variant="body2"
|
||||
ml={theme.spacing(6)}
|
||||
mt={theme.spacing(1)}
|
||||
sx={{
|
||||
"&:before": {
|
||||
position: "absolute",
|
||||
content: `""`,
|
||||
width: theme.spacing(2),
|
||||
height: theme.spacing(2),
|
||||
borderRadius: "50%",
|
||||
backgroundColor: theme.palette.primary.contrastTextTertiary,
|
||||
opacity: 0.8,
|
||||
left: theme.spacing(-5),
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{t("editing")}
|
||||
</Typography>
|
||||
</Stack>
|
||||
)}
|
||||
</Box>
|
||||
{!isCreate && (
|
||||
<Box
|
||||
alignSelf="flex-end"
|
||||
ml="auto"
|
||||
>
|
||||
<Button
|
||||
onClick={handlePause}
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
sx={{
|
||||
pl: theme.spacing(4),
|
||||
pr: theme.spacing(6),
|
||||
"& svg": {
|
||||
mr: theme.spacing(2),
|
||||
"& path": {
|
||||
stroke: theme.palette.primary.contrastTextTertiary,
|
||||
strokeWidth: 0.1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{monitor?.isActive ? (
|
||||
<>
|
||||
<PauseCircleOutlineIcon />
|
||||
{t("pause")}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlayCircleOutlineRoundedIcon />
|
||||
{t("resume")}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
loading={isBusy}
|
||||
variant="contained"
|
||||
color="error"
|
||||
onClick={() => setIsOpen(true)}
|
||||
sx={{ ml: theme.spacing(6) }}
|
||||
>
|
||||
{t("remove")}
|
||||
</Button>
|
||||
</Box>
|
||||
)}
|
||||
</Stack>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
@@ -172,90 +343,101 @@ const CreatePageSpeed = () => {
|
||||
>
|
||||
{t("settingsGeneralSettings")}
|
||||
</Typography>
|
||||
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
|
||||
<Typography component="p">
|
||||
{t("pageSpeedConfigureSettingsDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Stack
|
||||
gap={!isCreate ? theme.spacing(20) : theme.spacing(15)}
|
||||
sx={{
|
||||
".MuiInputBase-root:has(> .Mui-disabled)": {
|
||||
backgroundColor: theme.palette.tertiary.main,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TextInput
|
||||
type={"url"}
|
||||
name="url"
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
startAdornment={<HttpAdornment https={https} />}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
label={!isCreate ? t("url") : t("urlMonitor")}
|
||||
startAdornment={isCreate ? <HttpAdornment https={https} /> : undefined}
|
||||
placeholder="random.website.com"
|
||||
value={monitor.url || ""}
|
||||
onChange={handleChange}
|
||||
onBlur={handleBlur}
|
||||
error={errors["url"] ? true : false}
|
||||
onBlur={isCreate ? handleBlur : undefined}
|
||||
error={!!errors["url"]}
|
||||
helperText={errors["url"]}
|
||||
disabled={!isCreate}
|
||||
/>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
name="name"
|
||||
label="Display name"
|
||||
label={t("monitorDisplayName")}
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
value={monitor.name || ""}
|
||||
onChange={handleChange}
|
||||
error={errors["name"] ? true : false}
|
||||
error={!!errors["name"]}
|
||||
helperText={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("distributedUptimeCreateChecks")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateChecksDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="PageSpeed"
|
||||
desc="Use the Lighthouse PageSpeed API to monitor your website"
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
{t("https")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group" // Why does this work?
|
||||
filled={(!https).toString()} // There's nothing in the docs about this either
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
{t("http")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
{isCreate && (
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
component="h2"
|
||||
variant="h2"
|
||||
>
|
||||
{t("distributedUptimeCreateChecks")}
|
||||
</Typography>
|
||||
<Typography component="p">
|
||||
{t("distributedUptimeCreateChecksDescription")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="PageSpeed"
|
||||
desc={t("pageSpeedLighthouseAPI")}
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
{t("https")}
|
||||
</Button>
|
||||
<Button
|
||||
variant="group" // Why does this work?
|
||||
filled={(!https).toString()} // There's nothing in the docs about this either
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
{t("http")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box>
|
||||
<Typography
|
||||
component="p"
|
||||
color={theme.palette.error.contrastText}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.contrastText}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
</ConfigBox>
|
||||
)}
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography
|
||||
@@ -269,6 +451,7 @@ const CreatePageSpeed = () => {
|
||||
<NotificationsConfig
|
||||
notifications={notifications}
|
||||
setMonitor={setMonitor}
|
||||
setNotifications={isCreate ? undefined : monitor.notifications}
|
||||
/>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
@@ -280,33 +463,47 @@ const CreatePageSpeed = () => {
|
||||
{t("distributedUptimeCreateAdvancedSettings")}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(isCreate ? 12 : 20)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
name="interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
label={t("checkFrequency")}
|
||||
value={monitor?.interval / MS_PER_MINUTE || 3}
|
||||
onChange={handleChange}
|
||||
items={SELECT_VALUES}
|
||||
items={FREQUENCIES}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
mt={isCreate ? undefined : "auto"}
|
||||
>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="contained"
|
||||
color="accent"
|
||||
loading={isBusy}
|
||||
disabled={!Object.values(errors).every((value) => value === undefined)}
|
||||
loading={isCreating}
|
||||
sx={isCreate ? undefined : { px: theme.spacing(12) }}
|
||||
>
|
||||
{t("createMonitor")}
|
||||
{isCreate ? t("createMonitor") : t("settingsSave")}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{!isCreate && (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
theme={theme}
|
||||
title={t("deleteDialogTitle")}
|
||||
description={t("deleteDialogDescription")}
|
||||
onCancel={() => setIsOpen(false)}
|
||||
confirmationButtonLabel={t("delete")}
|
||||
onConfirm={handleRemove}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
export default CreatePageSpeed;
|
||||
|
||||
export default PageSpeedSetup;
|
||||
File renamed without changes.
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -21,7 +21,7 @@ import MonitorCountHeader from "../../../Components/MonitorCountHeader";
|
||||
import { Stack, Box, Button, Typography } from "@mui/material";
|
||||
|
||||
// Utils
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect } from "react";
|
||||
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -146,6 +146,12 @@ const UptimeMonitors = () => {
|
||||
monitorUpdateTrigger,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isSearching) {
|
||||
setPage(undefined);
|
||||
}
|
||||
}, [isSearching]);
|
||||
|
||||
const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading;
|
||||
if (networkError) {
|
||||
return (
|
||||
@@ -168,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}
|
||||
/>
|
||||
|
||||
@@ -18,9 +18,8 @@ import UptimeConfigure from "../Pages/Uptime/Configure";
|
||||
|
||||
// PageSpeed
|
||||
import PageSpeed from "../Pages/PageSpeed/Monitors";
|
||||
import PageSpeedCreate from "../Pages/PageSpeed/Create";
|
||||
import PageSpeedDetails from "../Pages/PageSpeed/Details";
|
||||
import PageSpeedConfigure from "../Pages/PageSpeed/Configure";
|
||||
import PageSpeedCreate from "../Pages/PageSpeed/Create";
|
||||
|
||||
// Infrastructure
|
||||
import Infrastructure from "../Pages/Infrastructure/Monitors";
|
||||
@@ -43,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";
|
||||
@@ -107,7 +107,7 @@ const Routes = () => {
|
||||
/>
|
||||
<Route
|
||||
path="pagespeed/configure/:monitorId"
|
||||
element={<PageSpeedConfigure />}
|
||||
element={<PageSpeedCreate />}
|
||||
/>
|
||||
<Route
|
||||
path="infrastructure"
|
||||
@@ -188,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"
|
||||
|
||||
@@ -393,7 +393,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 +406,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`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -977,7 +977,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 +1091,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.";
|
||||
|
||||
@@ -128,48 +129,37 @@ const monitorValidation = joi.object({
|
||||
.string()
|
||||
.trim()
|
||||
.custom((value, helpers) => {
|
||||
// Regex from https://gist.github.com/dperini/729294
|
||||
var urlRegex = new RegExp(
|
||||
"^" +
|
||||
// protocol identifier (optional)
|
||||
// short syntax // still required
|
||||
"(?:(?:https?|ftp):\\/\\/)?" +
|
||||
// user:pass BasicAuth (optional)
|
||||
"(?:" +
|
||||
// IP address dotted notation octets
|
||||
// excludes loopback network 0.0.0.0
|
||||
// excludes reserved space >= 224.0.0.0
|
||||
// excludes network & broadcast addresses
|
||||
// (first & last IP address of each class)
|
||||
"(?:[1-9]\\d?|1\\d\\d|2[01]\\d|22[0-3])" +
|
||||
"(?:\\.(?:1?\\d{1,2}|2[0-4]\\d|25[0-5])){2}" +
|
||||
"(?:\\.(?:[1-9]\\d?|1\\d\\d|2[0-4]\\d|25[0-4]))" +
|
||||
"|" +
|
||||
// host & domain names, may end with dot
|
||||
// can be replaced by a shortest alternative
|
||||
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+
|
||||
"(?:" +
|
||||
"(?:" +
|
||||
"[a-z0-9\\u00a1-\\uffff]" +
|
||||
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
|
||||
")?" +
|
||||
"[a-z0-9\\u00a1-\\uffff]\\." +
|
||||
")+" +
|
||||
// TLD identifier name, may end with dot
|
||||
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
|
||||
")" +
|
||||
// port number (optional)
|
||||
"(?::\\d{2,5})?" +
|
||||
// resource path (optional)
|
||||
"(?:[/?#]\\S*)?" +
|
||||
"$",
|
||||
"i"
|
||||
);
|
||||
if (!urlRegex.test(value)) {
|
||||
// 1. Standard URLs: must have protocol and pass canParse()
|
||||
if (/^(https?:\/\/)/.test(value)) {
|
||||
if (
|
||||
typeof URL !== "undefined" &&
|
||||
typeof URL.canParse === "function" &&
|
||||
URL.canParse(value)
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
// else, it's a malformed URL with protocol
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
|
||||
return value;
|
||||
// 2. Docker/internal hostnames (no protocol)
|
||||
if (/^[a-zA-Z0-9]([a-zA-Z0-9.-]*[a-zA-Z0-9])?(:\d{1,5})?$/.test(value)) {
|
||||
if (value.includes("..")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
// Split by dot, check each label
|
||||
const labels = value.split(":")[0].split(".");
|
||||
for (const label of labels) {
|
||||
if (!label) return helpers.error("string.invalidUrl");
|
||||
if (label.startsWith("-") || label.endsWith("-")) {
|
||||
return helpers.error("string.invalidUrl");
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// 3. Everything else is invalid
|
||||
return helpers.error("string.invalidUrl");
|
||||
})
|
||||
.messages({
|
||||
"string.empty": "This field is required.",
|
||||
@@ -410,17 +400,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 +460,5 @@ export {
|
||||
statusPageValidation,
|
||||
logoImageValidation,
|
||||
notificationValidation,
|
||||
editUserValidation,
|
||||
};
|
||||
+138
-17
@@ -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": {
|
||||
@@ -209,7 +214,12 @@
|
||||
"validationFailed": "Validation failed"
|
||||
},
|
||||
"cancel": "Cancel",
|
||||
"checkFormError": "Please check the form for errors.",
|
||||
"checkFrequency": "Check frequency",
|
||||
"checkHooks": {
|
||||
"failureResolveAll": "Failed to resolve all incidents.",
|
||||
"failureResolveOne": "Failed to resolve incident."
|
||||
},
|
||||
"checkingEvery": "Checking every",
|
||||
"city": "CITY",
|
||||
"common": {
|
||||
@@ -243,6 +253,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",
|
||||
@@ -273,6 +285,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.",
|
||||
@@ -284,10 +297,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",
|
||||
@@ -297,6 +307,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",
|
||||
@@ -347,6 +378,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",
|
||||
@@ -377,6 +426,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.",
|
||||
@@ -391,24 +443,24 @@
|
||||
"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",
|
||||
"incidentsPageActionResolve": "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.",
|
||||
@@ -420,6 +472,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",
|
||||
@@ -438,7 +501,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": {
|
||||
@@ -450,6 +513,7 @@
|
||||
}
|
||||
},
|
||||
"tabs": {
|
||||
"diagnostics": "Diagnostics",
|
||||
"logs": "Server logs",
|
||||
"queue": "Job queue"
|
||||
},
|
||||
@@ -462,6 +526,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": "*************************************",
|
||||
@@ -493,8 +568,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",
|
||||
@@ -505,7 +580,6 @@
|
||||
"failureAddDemoMonitors": "Failed to add demo monitors",
|
||||
"successAddDemoMonitors": "Successfully added demo monitors"
|
||||
},
|
||||
|
||||
"monitorState": {
|
||||
"active": "Active",
|
||||
"paused": "Paused",
|
||||
@@ -522,10 +596,6 @@
|
||||
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
|
||||
"monitors": "monitors",
|
||||
"monitorsToApply": "Monitors to apply maintenance window to",
|
||||
"checkHooks": {
|
||||
"failureResolveOne": "Failed to resolve incident.",
|
||||
"failureResolveAll": "Failed to resolve all incidents."
|
||||
},
|
||||
"ms": "ms",
|
||||
"navControls": "Controls",
|
||||
"nextWindow": "Next window",
|
||||
@@ -564,7 +634,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,11 +680,23 @@
|
||||
"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.",
|
||||
"pageSpeedDetailsPerformanceReportCalculator": "See calculator",
|
||||
"pageSpeedLearnMoreLink": "Click here",
|
||||
"pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website",
|
||||
"pageSpeedMonitor": "PageSpeed monitor",
|
||||
"pageSpeedWarning": "Warning: You haven't added a Google PageSpeed API key yet. Without it, the PageSpeed monitor won't function.",
|
||||
"passwordPanel": {
|
||||
@@ -796,6 +879,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"
|
||||
},
|
||||
@@ -821,7 +912,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": {
|
||||
@@ -849,6 +949,15 @@
|
||||
},
|
||||
"testLocale": "testLocale",
|
||||
"testNotificationsDisabled": "There are no notifications setup for this monitor. You need to add one by clicking 'Configure' button",
|
||||
"time": {
|
||||
"fiveMinutes": "5 minutes",
|
||||
"oneDay": "1 day",
|
||||
"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",
|
||||
"title": "Title",
|
||||
@@ -866,6 +975,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,5 @@
|
||||
import { createAnnouncementValidation } from "../validation/joi.js";
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "announcementController";
|
||||
|
||||
@@ -28,16 +28,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 +43,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 +57,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,16 +1,20 @@
|
||||
import {
|
||||
registrationBodyValidation,
|
||||
loginValidation,
|
||||
editUserParamValidation,
|
||||
editUserBodyValidation,
|
||||
recoveryValidation,
|
||||
recoveryTokenValidation,
|
||||
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";
|
||||
import { asyncHandler, createAuthError, createError } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "authController";
|
||||
|
||||
class AuthController {
|
||||
@@ -32,15 +36,10 @@ class AuthController {
|
||||
* @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");
|
||||
}
|
||||
const tokenTTL = appSettings?.jwtTTL ?? "2h";
|
||||
const tokenSecret = appSettings?.jwtSecret;
|
||||
const payloadData = payload;
|
||||
return jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,19 +54,14 @@ class AuthController {
|
||||
* @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).
|
||||
*/
|
||||
registerUser = async (req, res, next) => {
|
||||
try {
|
||||
registerUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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 {
|
||||
|
||||
// Create a new user
|
||||
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);
|
||||
@@ -100,16 +94,14 @@ class AuthController {
|
||||
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,
|
||||
});
|
||||
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,
|
||||
@@ -123,10 +115,10 @@ class AuthController {
|
||||
msg: this.stringService.authCreateUser,
|
||||
data: { user: newUser, token: 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.
|
||||
@@ -140,18 +132,13 @@ class AuthController {
|
||||
* @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.
|
||||
*/
|
||||
loginUser = async (req, res, next) => {
|
||||
try {
|
||||
loginUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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
|
||||
@@ -160,10 +147,7 @@ class AuthController {
|
||||
// Compare password
|
||||
const match = await user.comparePassword(password);
|
||||
if (match !== true) {
|
||||
const error = new Error(this.stringService.authIncorrectPassword);
|
||||
error.status = 401;
|
||||
next(error);
|
||||
return;
|
||||
throw createAuthError(this.stringService.authIncorrectPassword);
|
||||
}
|
||||
|
||||
// Remove password from user object. Should this be abstracted to DB layer?
|
||||
@@ -184,11 +168,10 @@ class AuthController {
|
||||
token: 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.
|
||||
@@ -204,26 +187,10 @@ class AuthController {
|
||||
* @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).
|
||||
*/
|
||||
editUser = async (req, res, next) => {
|
||||
try {
|
||||
await editUserParamValidation.validateAsync(req.params);
|
||||
editUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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;
|
||||
}
|
||||
|
||||
try {
|
||||
// Change Password check
|
||||
if (req.body.password && req.body.newPassword) {
|
||||
// Get token from headers
|
||||
@@ -240,24 +207,21 @@ class AuthController {
|
||||
// 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;
|
||||
throw createError(this.stringService.authIncorrectPassword, 403);
|
||||
}
|
||||
// If a match, update the password
|
||||
req.body.password = req.body.newPassword;
|
||||
}
|
||||
|
||||
const updatedUser = await this.db.updateUser(req, res);
|
||||
const updatedUser = await this.db.updateUser({ userId: req?.user?._id, user: req.body, file: req.file });
|
||||
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.
|
||||
@@ -268,18 +232,17 @@ class AuthController {
|
||||
* @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.
|
||||
*/
|
||||
checkSuperadminExists = async (req, res, next) => {
|
||||
try {
|
||||
checkSuperadminExists = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const superAdminExists = await this.db.checkSuperadmin(req, res);
|
||||
|
||||
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.
|
||||
* @async
|
||||
@@ -291,16 +254,9 @@ class AuthController {
|
||||
* @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).
|
||||
*/
|
||||
requestRecovery = async (req, res, next) => {
|
||||
try {
|
||||
requestRecovery = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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);
|
||||
@@ -313,20 +269,16 @@ class AuthController {
|
||||
email,
|
||||
url,
|
||||
});
|
||||
const msgId = await this.emailService.sendEmail(
|
||||
email,
|
||||
"Checkmate Password Reset",
|
||||
html
|
||||
);
|
||||
const msgId = await this.emailService.sendEmail(email, "Checkmate Password Reset", html);
|
||||
|
||||
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.
|
||||
* @async
|
||||
@@ -338,25 +290,17 @@ class AuthController {
|
||||
* @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).
|
||||
*/
|
||||
validateRecovery = async (req, res, next) => {
|
||||
try {
|
||||
validateRecovery = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await recoveryTokenValidation.validateAsync(req.body);
|
||||
} catch (error) {
|
||||
const validationError = handleValidationError(error, SERVICE_NAME);
|
||||
next(validationError);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.db.validateRecoveryToken(req, res);
|
||||
|
||||
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.
|
||||
@@ -370,27 +314,20 @@ class AuthController {
|
||||
* @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).
|
||||
*/
|
||||
resetPassword = async (req, res, next) => {
|
||||
try {
|
||||
resetPassword = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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);
|
||||
|
||||
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.
|
||||
@@ -401,22 +338,36 @@ class AuthController {
|
||||
* @returns {Object} The response object with success status and message.
|
||||
* @throws {Error} If user validation fails or user is not found in the database.
|
||||
*/
|
||||
deleteUser = async (req, res, next) => {
|
||||
try {
|
||||
const token = getTokenFromHeaders(req.headers);
|
||||
const decodedToken = jwt.decode(token);
|
||||
const { email } = decodedToken;
|
||||
deleteUser = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const email = req?.user?.email;
|
||||
if (!email) {
|
||||
throw new Error("No email in request");
|
||||
}
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
const userId = req?.user?._id;
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
if (!userId) {
|
||||
throw new Error("No user ID in request");
|
||||
}
|
||||
|
||||
const roles = req.user.role;
|
||||
if (roles.includes("demo")) {
|
||||
throw new Error("Demo user cannot be deleted");
|
||||
}
|
||||
|
||||
// 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(),
|
||||
teamId: teamId,
|
||||
});
|
||||
|
||||
if (user.role.includes("superadmin")) {
|
||||
//2. Remove all jobs, delete checks and alerts
|
||||
if (roles.includes("superadmin")) {
|
||||
// 2. Remove all jobs, delete checks and alerts
|
||||
result?.monitors.length > 0 &&
|
||||
(await Promise.all(
|
||||
result.monitors.map(async (monitor) => {
|
||||
@@ -425,28 +376,74 @@ class AuthController {
|
||||
));
|
||||
}
|
||||
// 6. Delete the user by id
|
||||
await this.db.deleteUser(user._id);
|
||||
await this.db.deleteUser(userId);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authDeleteUser,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "deleteUserController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"deleteUser"
|
||||
);
|
||||
|
||||
getAllUsers = async (req, res, next) => {
|
||||
try {
|
||||
getAllUsers = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const allUsers = await this.db.getAllUsers(req, res);
|
||||
|
||||
return res.success({
|
||||
msg: this.stringService.authGetAllUsers,
|
||||
data: allUsers,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getAllUsersController"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getAllUsers"
|
||||
);
|
||||
|
||||
getUserById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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.db.getUserById(roles, userId);
|
||||
|
||||
return res.success({ msg: "ok", data: user });
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getUserById"
|
||||
);
|
||||
|
||||
editUserById = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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.db.editUserById(userId, user);
|
||||
return res.success({ msg: "ok" });
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"editUserById"
|
||||
);
|
||||
}
|
||||
|
||||
export default AuthController;
|
||||
@@ -1,6 +1,4 @@
|
||||
import {
|
||||
createCheckParamValidation,
|
||||
createCheckBodyValidation,
|
||||
getChecksParamValidation,
|
||||
getChecksQueryValidation,
|
||||
getTeamChecksParamValidation,
|
||||
@@ -12,9 +10,7 @@ import {
|
||||
ackAllChecksParamValidation,
|
||||
ackAllChecksBodyValidation,
|
||||
} from "../validation/joi.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import { handleValidationError, handleError } from "./controllerUtils.js";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "checkController";
|
||||
|
||||
@@ -25,41 +21,41 @@ class CheckController {
|
||||
this.stringService = stringService;
|
||||
}
|
||||
|
||||
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 {
|
||||
getChecksByMonitor = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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 monitorId = req?.params?.monitorId;
|
||||
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor ID in request");
|
||||
}
|
||||
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
const err = new Error("Monitor not found");
|
||||
err.status = 404;
|
||||
err.service = SERVICE_NAME;
|
||||
err.method = "getChecksByMonitor";
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const err = new Error("Unauthorized");
|
||||
err.status = 403;
|
||||
err.service = SERVICE_NAME;
|
||||
err.method = "getChecksByMonitor";
|
||||
throw err;
|
||||
}
|
||||
|
||||
let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = req.query;
|
||||
const result = await this.db.getChecksByMonitor({
|
||||
monitorId,
|
||||
type,
|
||||
@@ -76,22 +72,22 @@ class CheckController {
|
||||
msg: this.stringService.checkGet,
|
||||
data: result,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getChecksByMonitor"
|
||||
);
|
||||
|
||||
getChecksByTeam = async (req, res, next) => {
|
||||
try {
|
||||
getChecksByTeam = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await getTeamChecksParamValidation.validateAsync(req.params);
|
||||
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 teamId = req?.user?.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const checkData = await this.db.getChecksByTeam({
|
||||
sortOrder,
|
||||
@@ -106,36 +102,44 @@ class CheckController {
|
||||
msg: this.stringService.checkGet,
|
||||
data: checkData,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getTeamChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getChecksByTeam"
|
||||
);
|
||||
|
||||
getChecksSummaryByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
getChecksSummaryByTeamId = async (req, res, next) => {
|
||||
try {
|
||||
const { teamId } = req.user;
|
||||
const summary = await this.db.getChecksSummaryByTeamId({ 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 {
|
||||
ackCheck = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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 checkId = req?.params?.checkId;
|
||||
const ack = req?.body?.ack;
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!checkId) {
|
||||
throw new Error("No check ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const updatedCheck = await this.db.ackCheck(checkId, teamId, ack);
|
||||
|
||||
@@ -143,24 +147,45 @@ class CheckController {
|
||||
msg: this.stringService.checkUpdateStatus,
|
||||
data: updatedCheck,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "ackCheck"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"ackCheck"
|
||||
);
|
||||
|
||||
ackAllChecks = async (req, res, next) => {
|
||||
try {
|
||||
ackAllChecks = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
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 monitorId = req?.params?.monitorId;
|
||||
|
||||
const path = req?.params?.path;
|
||||
|
||||
const ack = req?.body?.ack;
|
||||
|
||||
if (path === "monitor") {
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
if (!monitor) {
|
||||
throw new Error("Monitor not found");
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const err = new Error("Unauthorized");
|
||||
err.status = 403;
|
||||
err.service = SERVICE_NAME;
|
||||
err.method = "ackAllChecks";
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedChecks = await this.db.ackAllChecks(monitorId, teamId, ack, path);
|
||||
|
||||
@@ -168,64 +193,81 @@ class CheckController {
|
||||
msg: this.stringService.checkUpdateStatus,
|
||||
data: updatedChecks,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "ackAllChecks"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"ackAllChecks"
|
||||
);
|
||||
|
||||
deleteChecks = async (req, res, next) => {
|
||||
try {
|
||||
deleteChecks = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await deleteChecksParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const monitorId = req?.params?.monitorId;
|
||||
const teamId = req?.user?.teamId;
|
||||
|
||||
if (!monitorId) {
|
||||
throw new Error("No monitor ID in request");
|
||||
}
|
||||
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const monitor = await this.db.getMonitorById(monitorId);
|
||||
|
||||
if (!monitor) {
|
||||
const err = new Error("Monitor not found");
|
||||
err.status = 404;
|
||||
err.service = SERVICE_NAME;
|
||||
err.method = "deleteChecks";
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!monitor.teamId.equals(teamId)) {
|
||||
const err = new Error("Unauthorized");
|
||||
err.status = 403;
|
||||
err.service = SERVICE_NAME;
|
||||
err.method = "deleteChecks";
|
||||
throw err;
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.deleteChecks(req.params.monitorId);
|
||||
|
||||
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 {
|
||||
deleteChecksByTeamId = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
await deleteChecksByTeamIdParamValidation.validateAsync(req.params);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const { teamId } = req.user;
|
||||
const teamId = req?.user?.teamId;
|
||||
if (!teamId) {
|
||||
throw new Error("No team ID in request");
|
||||
}
|
||||
|
||||
const deletedCount = await this.db.deleteChecksByTeamId(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;
|
||||
updateChecksTTL = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
const SECONDS_PER_DAY = 86400;
|
||||
|
||||
try {
|
||||
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);
|
||||
@@ -233,9 +275,9 @@ class CheckController {
|
||||
return res.success({
|
||||
msg: this.stringService.checkUpdateTTL,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "updateTTL"));
|
||||
}
|
||||
};
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"updateChecksTtl"
|
||||
);
|
||||
}
|
||||
export default CheckController;
|
||||
@@ -1,16 +1,4 @@
|
||||
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;
|
||||
};
|
||||
import { createServerError } from "../utils/errorUtils.js";
|
||||
|
||||
const fetchMonitorCertificate = async (sslChecker, monitor) => {
|
||||
const monitorUrl = new URL(monitor.url);
|
||||
@@ -18,9 +6,9 @@ const fetchMonitorCertificate = async (sslChecker, monitor) => {
|
||||
const cert = await sslChecker(hostname);
|
||||
// Throw an error if no cert or if cert.validTo is not present
|
||||
if (cert?.validTo === null || cert?.validTo === undefined) {
|
||||
throw new Error("Certificate not found");
|
||||
throw createServerError("Certificate not found");
|
||||
}
|
||||
return cert;
|
||||
};
|
||||
|
||||
export { handleValidationError, handleError, fetchMonitorCertificate };
|
||||
export { fetchMonitorCertificate };
|
||||
@@ -1,48 +1,91 @@
|
||||
import { handleError } from "./controllerUtils.js";
|
||||
import v8 from "v8";
|
||||
import os from "os";
|
||||
import { asyncHandler } from "../utils/errorUtils.js";
|
||||
|
||||
const SERVICE_NAME = "diagnosticController";
|
||||
|
||||
const obs = new PerformanceObserver((items) => {
|
||||
const entry = items.getEntries()[0];
|
||||
performance.clearMarks();
|
||||
});
|
||||
obs.observe({ entryTypes: ["measure"] });
|
||||
class DiagnosticController {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
this.getMonitorsByTeamIdExecutionStats =
|
||||
this.getMonitorsByTeamIdExecutionStats.bind(this);
|
||||
this.getDbStats = this.getDbStats.bind(this);
|
||||
}
|
||||
|
||||
async getMonitorsByTeamIdExecutionStats(req, res, next) {
|
||||
try {
|
||||
const data = await this.db.getMonitorsByTeamIdExecutionStats(req);
|
||||
return res.success({
|
||||
msg: "OK",
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getMonitorsByTeamIdExecutionStats"));
|
||||
}
|
||||
}
|
||||
getCPUUsage = async () => {
|
||||
const startUsage = process.cpuUsage();
|
||||
const timingPeriod = 1000; // measured in ms
|
||||
await new Promise((resolve) => setTimeout(resolve, timingPeriod));
|
||||
const endUsage = process.cpuUsage(startUsage);
|
||||
const cpuUsage = {
|
||||
userUsageMs: endUsage.user / 1000,
|
||||
systemUsageMs: endUsage.system / 1000,
|
||||
usagePercentage: ((endUsage.user + endUsage.system) / 1000 / timingPeriod) * 100,
|
||||
};
|
||||
return cpuUsage;
|
||||
};
|
||||
|
||||
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,
|
||||
getSystemStats = asyncHandler(
|
||||
async (req, res, next) => {
|
||||
// Memory Usage
|
||||
const totalMemory = os.totalmem();
|
||||
const freeMemory = os.freemem();
|
||||
|
||||
const osStats = {
|
||||
freeMemoryBytes: freeMemory, // bytes
|
||||
totalMemoryBytes: totalMemory, // bytes
|
||||
};
|
||||
|
||||
const used = process.memoryUsage();
|
||||
const memoryUsage = {};
|
||||
for (let key in used) {
|
||||
memoryUsage[`${key}Mb`] = Math.round((used[key] / 1024 / 1024) * 100) / 100; // MB
|
||||
}
|
||||
|
||||
// CPU Usage
|
||||
const cpuMetrics = await this.getCPUUsage();
|
||||
|
||||
// V8 Heap Statistics
|
||||
const heapStats = v8.getHeapStatistics();
|
||||
const v8Metrics = {
|
||||
totalHeapSizeBytes: heapStats.total_heap_size, // bytes
|
||||
usedHeapSizeBytes: heapStats.used_heap_size, // bytes
|
||||
heapSizeLimitBytes: heapStats.heap_size_limit, // bytes
|
||||
};
|
||||
|
||||
// Event Loop Delay
|
||||
let eventLoopDelay = 0;
|
||||
performance.mark("start");
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
performance.mark("end");
|
||||
performance.measure("eventLoopDelay", "start", "end");
|
||||
const entries = performance.getEntriesByName("eventLoopDelay");
|
||||
if (entries.length > 0) {
|
||||
eventLoopDelay = entries[0].duration;
|
||||
}
|
||||
|
||||
// Uptime
|
||||
const uptimeMs = process.uptime() * 1000; // ms
|
||||
|
||||
// Combine Metrics
|
||||
const diagnostics = {
|
||||
osStats,
|
||||
memoryUsage,
|
||||
cpuUsage: cpuMetrics,
|
||||
v8HeapStats: v8Metrics,
|
||||
eventLoopDelayMs: eventLoopDelay,
|
||||
uptimeMs,
|
||||
};
|
||||
|
||||
return res.success({
|
||||
msg: "OK",
|
||||
data: stats,
|
||||
data: diagnostics,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "getDbStats"));
|
||||
}
|
||||
}
|
||||
},
|
||||
SERVICE_NAME,
|
||||
"getSystemStats"
|
||||
);
|
||||
}
|
||||
export default DiagnosticController;
|
||||
Loaded 100 of 189 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user