From 2b4727db0d6bf17c152f0b39ae774996d590f7af Mon Sep 17 00:00:00 2001 From: Jeffer Marcelino Date: Sun, 29 Jun 2025 15:34:50 +0200 Subject: [PATCH 01/83] feat: add checkmate helm chart with basic validation --- charts/helm/checkmate/.helmignore | 17 ++++++++ charts/helm/checkmate/Chart.yaml | 6 +++ .../helm/checkmate/templates/_prechecks.yaml | 12 ++++++ .../templates/client-deployment.yaml | 22 ++++++++++ .../checkmate/templates/client-ingress.yaml | 20 +++++++++ .../checkmate/templates/client-service.yaml | 9 ++++ .../templates/mongodb-deployment.yaml | 27 ++++++++++++ .../helm/checkmate/templates/mongodb-pvc.yaml | 10 +++++ .../checkmate/templates/mongodb-service.yaml | 9 ++++ .../checkmate/templates/redis-deployment.yaml | 26 ++++++++++++ .../helm/checkmate/templates/redis-pvc.yaml | 10 +++++ .../checkmate/templates/redis-service.yaml | 9 ++++ charts/helm/checkmate/templates/secrets.yaml | 19 +++++++++ .../templates/server-deployment.yaml | 30 ++++++++++++++ .../checkmate/templates/server-ingress.yaml | 20 +++++++++ .../checkmate/templates/server-service.yaml | 9 ++++ charts/helm/checkmate/values.yaml | 41 +++++++++++++++++++ 17 files changed, 296 insertions(+) create mode 100644 charts/helm/checkmate/.helmignore create mode 100644 charts/helm/checkmate/Chart.yaml create mode 100644 charts/helm/checkmate/templates/_prechecks.yaml create mode 100644 charts/helm/checkmate/templates/client-deployment.yaml create mode 100644 charts/helm/checkmate/templates/client-ingress.yaml create mode 100644 charts/helm/checkmate/templates/client-service.yaml create mode 100644 charts/helm/checkmate/templates/mongodb-deployment.yaml create mode 100644 charts/helm/checkmate/templates/mongodb-pvc.yaml create mode 100644 charts/helm/checkmate/templates/mongodb-service.yaml create mode 100644 charts/helm/checkmate/templates/redis-deployment.yaml create mode 100644 charts/helm/checkmate/templates/redis-pvc.yaml create mode 100644 charts/helm/checkmate/templates/redis-service.yaml create mode 100644 charts/helm/checkmate/templates/secrets.yaml create mode 100644 charts/helm/checkmate/templates/server-deployment.yaml create mode 100644 charts/helm/checkmate/templates/server-ingress.yaml create mode 100644 charts/helm/checkmate/templates/server-service.yaml create mode 100644 charts/helm/checkmate/values.yaml diff --git a/charts/helm/checkmate/.helmignore b/charts/helm/checkmate/.helmignore new file mode 100644 index 000000000..21846e965 --- /dev/null +++ b/charts/helm/checkmate/.helmignore @@ -0,0 +1,17 @@ +.DS_Store +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +*.swp +*.bak +*.tmp +*.orig +*~ +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/helm/checkmate/Chart.yaml b/charts/helm/checkmate/Chart.yaml new file mode 100644 index 000000000..2ca35c1f6 --- /dev/null +++ b/charts/helm/checkmate/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: checkmate-chart +description: A Helm chart for Checkmate App +type: application +version: 0.1.0 +appVersion: "2.1.0" diff --git a/charts/helm/checkmate/templates/_prechecks.yaml b/charts/helm/checkmate/templates/_prechecks.yaml new file mode 100644 index 000000000..870ab5c77 --- /dev/null +++ b/charts/helm/checkmate/templates/_prechecks.yaml @@ -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 }} diff --git a/charts/helm/checkmate/templates/client-deployment.yaml b/charts/helm/checkmate/templates/client-deployment.yaml new file mode 100644 index 000000000..6c1f70fd7 --- /dev/null +++ b/charts/helm/checkmate/templates/client-deployment.yaml @@ -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" diff --git a/charts/helm/checkmate/templates/client-ingress.yaml b/charts/helm/checkmate/templates/client-ingress.yaml new file mode 100644 index 000000000..da059a863 --- /dev/null +++ b/charts/helm/checkmate/templates/client-ingress.yaml @@ -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 }} diff --git a/charts/helm/checkmate/templates/client-service.yaml b/charts/helm/checkmate/templates/client-service.yaml new file mode 100644 index 000000000..afb88188c --- /dev/null +++ b/charts/helm/checkmate/templates/client-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: client +spec: + selector: + app: client + ports: + - port: {{ .Values.client.port }} diff --git a/charts/helm/checkmate/templates/mongodb-deployment.yaml b/charts/helm/checkmate/templates/mongodb-deployment.yaml new file mode 100644 index 000000000..4c906fe09 --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-deployment.yaml @@ -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 \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-pvc.yaml b/charts/helm/checkmate/templates/mongodb-pvc.yaml new file mode 100644 index 000000000..435b662e6 --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: mongodb-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-service.yaml b/charts/helm/checkmate/templates/mongodb-service.yaml new file mode 100644 index 000000000..2911dc3eb --- /dev/null +++ b/charts/helm/checkmate/templates/mongodb-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: mongodb +spec: + selector: + app: mongodb + ports: + - port: {{ .Values.mongodb.port }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-deployment.yaml b/charts/helm/checkmate/templates/redis-deployment.yaml new file mode 100644 index 000000000..cc9ffdaf9 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-deployment.yaml @@ -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 diff --git a/charts/helm/checkmate/templates/redis-pvc.yaml b/charts/helm/checkmate/templates/redis-pvc.yaml new file mode 100644 index 000000000..2f0d81960 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-pvc.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: redis-pvc +spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.redisSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-service.yaml b/charts/helm/checkmate/templates/redis-service.yaml new file mode 100644 index 000000000..f29fa46de --- /dev/null +++ b/charts/helm/checkmate/templates/redis-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: redis +spec: + selector: + app: redis + ports: + - port: {{ .Values.redis.port }} diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml new file mode 100644 index 000000000..42e7c6047 --- /dev/null +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -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 }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml new file mode 100644 index 000000000..8548495dd --- /dev/null +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -0,0 +1,30 @@ +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 + volumeMounts: + - name: docker-sock + mountPath: /var/run/docker.sock + volumes: + - name: docker-sock + hostPath: + path: /var/run/docker.sock + type: Socket diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml new file mode 100644 index 000000000..acf64f95f --- /dev/null +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -0,0 +1,20 @@ +{{- if .Values.server.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: server-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: {{ .Values.server.ingress.host }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: server + port: + number: {{ .Values.server.port }} +{{- end }} diff --git a/charts/helm/checkmate/templates/server-service.yaml b/charts/helm/checkmate/templates/server-service.yaml new file mode 100644 index 000000000..64494ff90 --- /dev/null +++ b/charts/helm/checkmate/templates/server-service.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Service +metadata: + name: server +spec: + selector: + app: server + ports: + - port: {{ .Values.server.port }} diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml new file mode 100644 index 000000000..d73e17965 --- /dev/null +++ b/charts/helm/checkmate/values.yaml @@ -0,0 +1,41 @@ +client: + image: bluewaveuptime/uptime_client:latest + port: 80 + ingress: + enabled: true + host: checkmate-chart.local + +server: + image: bluewaveuptime/uptime_server:latest + port: 5000 + protocol: http + ingress: + enabled: true + host: server.checkmate-chart.local + +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 \ No newline at end of file From edd23469390c62128d6fce64c7341d81d605699b Mon Sep 17 00:00:00 2001 From: Jeffer Marcelino Date: Thu, 3 Jul 2025 18:06:47 +0200 Subject: [PATCH 02/83] fix: change hardcoded protocol and host to be change_me by default --- charts/helm/checkmate/values.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml index d73e17965..36ce3a246 100644 --- a/charts/helm/checkmate/values.yaml +++ b/charts/helm/checkmate/values.yaml @@ -3,15 +3,15 @@ client: port: 80 ingress: enabled: true - host: checkmate-chart.local + host: change_me server: image: bluewaveuptime/uptime_server:latest port: 5000 - protocol: http + protocol: change_me ingress: enabled: true - host: server.checkmate-chart.local + host: change_me redis: image: redis:7.2 From 6e5550ba32a301cfe5c3b690e8c94bb2842e5f5a Mon Sep 17 00:00:00 2001 From: Jeffer Marcelino Date: Thu, 3 Jul 2025 18:16:42 +0200 Subject: [PATCH 03/83] docs: add INSTALLATION.md with Helm-based Kubernetes deployment steps --- README.md | 9 +++++-- charts/helm/checkmate/INSTALLATION.md | 39 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 charts/helm/checkmate/INSTALLATION.md diff --git a/README.md b/README.md index c9c9073ad..5b99e67d4 100644 --- a/README.md +++ b/README.md @@ -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,11 @@ 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/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. 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). + +For detailed Kubernetes deployment instructions using Helm, see the [INSTALLATION.md](./charts/helm/checkmate/INSTALLATION.md) file. + +Alternatively, you can also use [Coolify](https://coolify.io/) or [Elestio](https://elest.io/open-source/checkmate) for a one-click Docker deployment. 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 diff --git a/charts/helm/checkmate/INSTALLATION.md b/charts/helm/checkmate/INSTALLATION.md new file mode 100644 index 000000000..9ca321ad3 --- /dev/null +++ b/charts/helm/checkmate/INSTALLATION.md @@ -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. From f00d7275e5b977b73762c9a7b8c1e0dc53036a86 Mon Sep 17 00:00:00 2001 From: Jesulayomy Date: Sat, 5 Jul 2025 18:26:34 -0400 Subject: [PATCH 04/83] [PageSpeed]: Monitor Config and Create pages consolidated into Setup Component --- client/src/Pages/PageSpeed/Setup/index.css | 9 + client/src/Pages/PageSpeed/Setup/index.jsx | 466 ++++++++++++++++++ client/src/Pages/PageSpeed/Setup/skeleton.jsx | 86 ++++ client/src/Routes/index.jsx | 5 +- 4 files changed, 564 insertions(+), 2 deletions(-) create mode 100644 client/src/Pages/PageSpeed/Setup/index.css create mode 100644 client/src/Pages/PageSpeed/Setup/index.jsx create mode 100644 client/src/Pages/PageSpeed/Setup/skeleton.jsx diff --git a/client/src/Pages/PageSpeed/Setup/index.css b/client/src/Pages/PageSpeed/Setup/index.css new file mode 100644 index 000000000..89a7c3466 --- /dev/null +++ b/client/src/Pages/PageSpeed/Setup/index.css @@ -0,0 +1,9 @@ +.configure-pagespeed button { + height: var(--env-var-height-2); +} + +.configure-pagespeed .field, +.configure-pagespeed .section-disabled, +.configure-pagespeed .select-wrapper { + flex: 1; +} diff --git a/client/src/Pages/PageSpeed/Setup/index.jsx b/client/src/Pages/PageSpeed/Setup/index.jsx new file mode 100644 index 000000000..4a653c58d --- /dev/null +++ b/client/src/Pages/PageSpeed/Setup/index.jsx @@ -0,0 +1,466 @@ +// Components +import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } 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"; +import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import Radio from "../../../Components/Inputs/Radio"; + + +// Utils +import { useState, useEffect } from "react"; +import { useSelector } from "react-redux"; +import { monitorValidation } from "../../../Validation/validation"; +import { parseDomainName } from "../../../Utils/monitorUtils"; +import { useTranslation } from "react-i18next"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import { useTheme } from "@emotion/react"; +import { createToast } from "../../../Utils/toastUtils"; + +import { useParams } from "react-router"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; +import { + useCreateMonitor, + useFetchMonitorById, + useDeleteMonitor, + useUpdateMonitor, + usePauseMonitor, +} from "../../../Hooks/monitorHooks"; + +const MS_PER_MINUTE = 60000; +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 PageSpeedSetup = ({ monitor }) => { + const isConfigure = monitor; + const { t } = useTranslation(); + const theme = useTheme(); + const { monitorId } = useParams(); + const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); + const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); + const [https, setHttps] = useState(true); + const [errors, setErrors] = useState({}); + const [isOpen, setIsOpen] = useState(false); + const [updateTrigger, setUpdateTrigger] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isUpdating, setIsUpdating] = useState(false); + const [isPausing, setIsPausing] = useState(false); + + // Monitor state + const [monitorData, setMonitorData] = useState({ + url: "", + name: "", + type: "pagespeed", + notifications: [], + interval: 3, + }); + + // Hooks for API actions + const [createMonitor, isCreating] = useCreateMonitor(); + const [fetchMonitor] = monitor + ? useFetchMonitorById({ monitorId, setMonitor: setMonitorData, updateTrigger }) + : [null]; + const [deleteMonitor] = useDeleteMonitor(); + const [updateMonitor] = useUpdateMonitor(); + const [pauseMonitor] = 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" }, + ]; + + // Fetch monitor if in configure mode + useEffect(() => { + if (monitor && monitorId && typeof fetchMonitor === 'function') { + setIsLoading(true); + fetchMonitor().finally(() => setIsLoading(false)); + } + }, [monitor, monitorId, updateTrigger, fetchMonitor]); + + // Handlers + + const handleChange = (event) => { + let { value, name } = event.target; + if (name === "interval") { + value = value * MS_PER_MINUTE; + } + setMonitorData((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 handleBlur = (event) => { + const { name, value } = event.target; + if (name === "url" && monitorData.name === "") { + setMonitorData((prev) => ({ ...prev, name: parseDomainName(value) })); + } + }; + + const onSubmit = async (event) => { + event.preventDefault(); + if (isConfigure) { + setIsUpdating(true); + await updateMonitor({ monitor: monitorData, redirect: "/pagespeed" }); + setIsUpdating(false); + } else { + let form = { + url: `http${https ? "s" : ""}://` + monitorData.url, + name: monitorData.name === "" ? monitorData.url : monitorData.name, + type: monitorData.type, + interval: monitorData.interval * MS_PER_MINUTE, + description: monitorData.name === "" ? monitorData.url : monitorData.name, + notifications: monitorData.notifications, + }; + 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: "Please check the form for errors." }); + return; + } + setIsLoading(true); + await createMonitor({ monitor: form, redirect: "/pagespeed" }); + setIsLoading(false); + } + }; + + const handlePause = async () => { + setIsPausing(true); + await pauseMonitor({ monitorId, triggerUpdate: () => setUpdateTrigger((u) => !u) }); + setIsPausing(false); + }; + + const handleRemove = async (event) => { + event.preventDefault(); + setIsDeleting(true); + await deleteMonitor({ monitor: monitorData, redirect: "/pagespeed" }); + setIsDeleting(false); + }; + + // Loading skeleton + if (isConfigure && (isLoading || !monitorData._id)) { + return ; + } + + return ( + + + + + + {isConfigure ? monitorData.name : t("createYour") + " "} + + { !isConfigure + ? ( + + {t("pageSpeedMonitor")} + + ) + : <> + } + + {isConfigure && ( + + + + + + + + + + {monitorData.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + + + + + + + )} + + + + {t("settingsGeneralSettings")} + + + {isConfigure ? t("pageSpeedConfigureSettingsDescription") : t("distributedUptimeCreateSelectURL")} + + + + : undefined} + placeholder={isConfigure ? "random.website.com" : "google.com"} + value={monitorData.url || ""} + onChange={handleChange} + onBlur={!isConfigure ? handleBlur : undefined} + error={!!errors["url"]} + helperText={errors["url"]} + disabled={isConfigure} + /> + + + + {!isConfigure && ( + + + + {t("distributedUptimeCreateChecks")} + + + {t("distributedUptimeCreateChecksDescription")} + + + + + + + + + + + {errors["type"] ? ( + + + {errors["type"]} + + + ) : null} + + + )} + + + + {t("notificationConfig.title")} + + {t("notificationConfig.description")} + + + + + + + {t("distributedUptimeCreateAdvancedSettings")} + + + + - - - - - - - - )} - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - isLoading={isLoading || isDeleting || isUpdating || isPausing} - /> - - ); -}; - -export default PageSpeedConfigure; diff --git a/client/src/Pages/PageSpeed/Configure/skeleton.jsx b/client/src/Pages/PageSpeed/Configure/skeleton.jsx deleted file mode 100644 index 347fcaa65..000000000 --- a/client/src/Pages/PageSpeed/Configure/skeleton.jsx +++ /dev/null @@ -1,86 +0,0 @@ -import { Box, Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -/** - * Renders a skeleton layout. - * - * @returns {JSX.Element} - */ -const SkeletonLayout = () => { - const theme = useTheme(); - - return ( - <> - - - - - - - - - - - - - - - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/PageSpeed/Create/index.css b/client/src/Pages/PageSpeed/Create/index.css deleted file mode 100644 index e69de29bb..000000000 diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx deleted file mode 100644 index 5cc4a1a54..000000000 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ /dev/null @@ -1,312 +0,0 @@ -//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"; -import ConfigBox from "../../../Components/ConfigBox"; -import Radio from "../../../Components/Inputs/Radio"; -import Select from "../../../Components/Inputs/Select"; -import NotificationsConfig from "../../../Components/NotificationConfig"; - -// Utils -import { useState } from "react"; -import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../Validation/validation"; -import { parseDomainName } from "../../../Utils/monitorUtils"; -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; - -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 [https, setHttps] = useState(true); - const [errors, setErrors] = useState({}); - const { user } = useSelector((state) => state.auth); - const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId(); - - // Setup - const theme = useTheme(); - const [createMonitor, isCreating] = useCreateMonitor(); - - // 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, - }; - - 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: "Please check the form for errors." }); - return; - } - - form = { - ...form, - description: form.name, - notifications: monitor.notifications, - }; - - await createMonitor({ monitor: form, redirect: "/pagespeed" }); - }; - - const handleChange = (event) => { - const { value, name } = event.target; - setMonitor({ - ...monitor, - [name]: value, - }); - - const { error } = monitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - 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 { t } = useTranslation(); - - return ( - - - - - - {t("createYour")}{" "} - - - {t("pageSpeedMonitor")} - - - - - - {t("settingsGeneralSettings")} - - {t("distributedUptimeCreateSelectURL")} - - - } - placeholder="google.com" - value={monitor.url} - onChange={handleChange} - onBlur={handleBlur} - error={errors["url"] ? true : false} - helperText={errors["url"]} - /> - - - - - - - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} - - - - - - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - + + + + - - )} - - - - {t("notificationConfig.title")} - - {t("notificationConfig.description")} - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - - - - - - - )} - {isConfigure && ( - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - isLoading={isLoading || isDeleting || isUpdating || isPausing} - /> - )} - - ); + > + : undefined + } + placeholder="random.website.com" + value={monitor.url || ""} + onChange={handleChange} + onBlur={!isConfigure ? handleBlur : undefined} + error={!!errors["url"]} + helperText={errors["url"]} + disabled={isConfigure} + /> + + + + {!isConfigure && ( + + + + {t("distributedUptimeCreateChecks")} + + + {t("distributedUptimeCreateChecksDescription")} + + + + + + + + + + + {errors["type"] ? ( + + + {errors["type"]} + + + ) : ( + "" + )} + + + )} + + + + {t("notificationConfig.title")} + + + {t("notificationConfig.description")} + + + + + + + + {t("distributedUptimeCreateAdvancedSettings")} + + + + { )} - {isConfigure && ( + {!isCreate && ( { onCancel={() => setIsOpen(false)} confirmationButtonLabel={t("delete")} onConfirm={handleRemove} - isLoading={isLoading || isDeleting || isUpdating || isPausing} /> )} From 1cbec8220dff3394293391d9eebc016f3824711f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 10 Jul 2025 16:08:04 -0700 Subject: [PATCH 11/83] add diagnostic page --- .../Components/Charts/CustomGauge/index.jsx | 4 +- client/src/Hooks/logHooks.js | 77 ++++++++++++++++++- client/src/Hooks/queueHooks.js | 57 -------------- .../Diagnostics/components/gauges/index.jsx | 74 ++++++++++++++++++ .../Diagnostics/components/gauges/utils.js | 4 + client/src/Pages/Logs/Diagnostics/index.jsx | 30 ++++++++ client/src/Pages/Logs/Logs/index.jsx | 2 +- client/src/Pages/Logs/Queue/index.jsx | 2 +- client/src/Pages/Logs/index.jsx | 5 +- client/src/Utils/NetworkService.js | 8 ++ client/src/locales/en.json | 6 +- server/controllers/diagnosticController.js | 73 ++++++++++++++++++ server/routes/diagnosticRoute.js | 1 + 13 files changed, 279 insertions(+), 64 deletions(-) delete mode 100644 client/src/Hooks/queueHooks.js create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx create mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/utils.js create mode 100644 client/src/Pages/Logs/Diagnostics/index.jsx diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 3c7e13610..359b5b328 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -54,8 +54,8 @@ 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; return ( { 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); + + useEffect(() => { + const fetchDiagnostics = async () => { + try { + setIsLoading(true); + const response = await networkService.getDiagnostics(); + setDiagnostics(response.data.data); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + fetchDiagnostics(); + }, []); + + return [diagnostics, isLoading, error]; +}; + +export { useFetchLogs, useFetchQueueData, useFlushQueue, useFetchDiagnostics }; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js deleted file mode 100644 index f7a25b3d3..000000000 --- a/client/src/Hooks/queueHooks.js +++ /dev/null @@ -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 }; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx new file mode 100644 index 000000000..21d9a35d8 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -0,0 +1,74 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +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"; + +const GaugeBox = ({ title, subtitle, children }) => { + const theme = useTheme(); + return ( + + {children} + + {title} + {subtitle} + + ); +}; + +const Gauges = ({ diagnostics }) => { + const heapTotalSize = getPercentage( + diagnostics?.v8HeapStats?.totalHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const heapUsedSize = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.heapSizeLimitMb + ); + + const actualHeapUsed = getPercentage( + diagnostics?.v8HeapStats?.usedHeapSizeMb, + diagnostics?.v8HeapStats?.totalHeapSizeMb + ); + const theme = useTheme(); + + return ( + + + + + + + + + + + + ); +}; + +Gauges.propTypes = { + diagnostics: PropTypes.object.isRequired, +}; + +export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js new file mode 100644 index 000000000..bdc570a3b --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js @@ -0,0 +1,4 @@ +export const getPercentage = (value, total) => { + if (!value || !total) return 0; + return (value / total) * 100; +}; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx new file mode 100644 index 000000000..9d7ee4639 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -0,0 +1,30 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Gauges from "./components/gauges"; + +import { useTheme } from "@emotion/react"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks"; + +const Diagnostics = () => { + // Local state + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const [diagnostics, isLoading, error] = useFetchDiagnostics(); + console.log(diagnostics); + // Setup + return ( + + + {t("diagnosticsPage.description")} + + + + ); +}; + +export default Diagnostics; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index 6c5e1a301..d8bd323a3 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -51,7 +51,7 @@ const Logs = () => { return ( - {t("logsPage.description")} + {t("logsPage.description")} { 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 = () => { > + {value === 0 && } {value === 1 && } + {value === 2 && } ); }; diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 7fba2ce18..895d90d30 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1091,6 +1091,14 @@ class NetworkService { }, }); } + + async getDiagnostics() { + return this.axiosInstance.get(`/diagnostic/system`, { + headers: { + "Content-Type": "application/json", + }, + }); + } } export default NetworkService; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ddc07f166..54f9600e2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -451,13 +451,17 @@ }, "tabs": { "logs": "Server logs", - "queue": "Job queue" + "queue": "Job queue", + "diagnostics": "Diagnostics" }, "title": "Logs", "toast": { "fetchLogsSuccess": "Logs fetched successfully" } }, + "diagnosticsPage": { + "description": "System diagnostics" + }, "low": "low", "maintenance": "maintenance", "maintenanceRepeat": "Maintenance Repeat", diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index 8fa5fa42b..c4223dc04 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -1,6 +1,14 @@ import { handleError } from "./controllerUtils.js"; +import v8 from "v8"; +import os from "os"; + 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; @@ -44,5 +52,70 @@ class DiagnosticController { next(handleError(error, SERVICE_NAME, "getDbStats")); } } + + async getSystemStats(req, res, next) { + try { + // Memory Usage + const totalMemory = os.totalmem(); + const freeMemory = os.freemem(); + + const osStats = { + freeMemoryMb: freeMemory / 1024 / 1024, // MB + totalMemoryMb: totalMemory / 1024 / 1024, // MB + }; + + 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 cpuUsage = process.cpuUsage(); + const cpuMetrics = { + userUsageMs: cpuUsage.user / 1000, // ms + systemUsageMs: cpuUsage.system / 1000, // ms + }; + + // V8 Heap Statistics + const heapStats = v8.getHeapStatistics(); + const v8Metrics = { + totalHeapSizeMb: heapStats.total_heap_size / 1024 / 1024, // MB + usedHeapSizeMb: heapStats.used_heap_size / 1024 / 1024, // MB + heapSizeLimitMb: heapStats.heap_size_limit / 1024 / 1024, // MB + }; + + // 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 uptime = process.uptime(); // seconds + + // Combine Metrics + const diagnostics = { + osStats, + memoryUsage, + cpuUsage: cpuMetrics, + v8HeapStats: v8Metrics, + eventLoopDelayMs: eventLoopDelay, + uptimeSeconds: uptime, + }; + + return res.success({ + msg: "OK", + data: diagnostics, + }); + } catch (error) { + next(handleError(error, SERVICE_NAME, "getMemoryUsage")); + } + } } export default DiagnosticController; diff --git a/server/routes/diagnosticRoute.js b/server/routes/diagnosticRoute.js index 616d01ccf..0d8f4df84 100755 --- a/server/routes/diagnosticRoute.js +++ b/server/routes/diagnosticRoute.js @@ -13,6 +13,7 @@ class DiagnosticRoutes { ); this.router.post("/db/stats", this.diagnosticController.getDbStats); + this.router.get("/system", this.diagnosticController.getSystemStats); } getRouter() { From 840069b93519116c593d502912b19f0bb5c01f29 Mon Sep 17 00:00:00 2001 From: Jesulayomy Date: Thu, 10 Jul 2025 21:53:33 -0400 Subject: [PATCH 12/83] [PageSpeed]: Implement conditional check for monitorId inside useFetchMonitorById hook --- client/src/Hooks/monitorHooks.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d59d88c74..b3210c1b4 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -205,18 +205,22 @@ const useFetchStatsByMonitorId = ({ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - const fetchMonitor = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorById({ monitorId: monitorId }); - setMonitor(res.data.data); - } catch (error) { - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); + if (monitorId !== undefined) { + const fetchMonitor = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorById({ monitorId: monitorId }); + setMonitor(res.data.data); + } catch (error) { + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + } else { + setIsLoading(false); + } }, [monitorId, setMonitor, updateTrigger]); return [isLoading]; }; From b7b768760825fa0b8240a8741ff2d63830c7ef98 Mon Sep 17 00:00:00 2001 From: Jesulayomy Date: Thu, 10 Jul 2025 21:54:27 -0400 Subject: [PATCH 13/83] [PageSpeed]: Loading states from hooks, unused classnames, conditional calling of hook removed --- client/src/Pages/PageSpeed/Create/index.jsx | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 8941d8c9a..7c6042fad 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -84,13 +84,11 @@ const PageSpeedSetup = () => { useGetNotificationsByTeamId(); // Hooks for API actions + const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger }); const [createMonitor, isCreating] = useCreateMonitor(); - const [isLoading] = isCreate - ? [false] - : useFetchMonitorById({ monitorId, setMonitor, updateTrigger }); - const [deleteMonitor] = useDeleteMonitor(); - const [updateMonitor] = useUpdateMonitor(); - const [pauseMonitor] = usePauseMonitor(); + const [deleteMonitor, isDeleting] = useDeleteMonitor(); + const [updateMonitor, isUpdating] = useUpdateMonitor(); + const [pauseMonitor, isPausing] = usePauseMonitor(); // Handlers const onSubmit = async (event) => { @@ -186,7 +184,6 @@ const PageSpeedSetup = () => { return ( { { > - - + ) : ( + <> )} - - - + + {!isCreate && ( + + + + + + - {t("settingsGeneralSettings")} + {monitor.url?.replace(/^https?:\/\//, "") || "..."} - - {t("pageSpeedConfigureSettingsDescription")} + + {t("editing")} - - + )} + + {!isCreate && ( + + + + + )} + + + + + {t("settingsGeneralSettings")} + + + {t("pageSpeedConfigureSettingsDescription")} + + + .Mui-disabled)": { + backgroundColor: theme.palette.tertiary.main, + }, + }} + > + : undefined} + placeholder="random.website.com" + value={monitor.url || ""} + onChange={handleChange} + onBlur={isCreate ? handleBlur : undefined} + error={!!errors["url"]} + helperText={errors["url"]} + disabled={!isCreate} + /> + + + + {isCreate && ( + + + + {t("distributedUptimeCreateChecks")} + + + {t("distributedUptimeCreateChecksDescription")} + + + + + + + + + - - {isCreate && ( - + {errors["type"] ? ( - {t("distributedUptimeCreateChecks")} - - - {t("distributedUptimeCreateChecksDescription")} + {errors["type"]} - - - - - - - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - )} - - - - {t("notificationConfig.title")} - - - {t("notificationConfig.description")} - - - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - - )} + + + + + {!isCreate && ( Date: Fri, 11 Jul 2025 13:40:29 -0400 Subject: [PATCH 18/83] [PageSpeed]: Use typeof operator for clarity, and to handle undeclared and undefined variables --- client/src/Hooks/monitorHooks.js | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index b3210c1b4..55c348626 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -205,22 +205,22 @@ const useFetchStatsByMonitorId = ({ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { - if (monitorId !== undefined) { - const fetchMonitor = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorById({ monitorId: monitorId }); - setMonitor(res.data.data); - } catch (error) { - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitor(); - } else { + if (typeof monitorId === "undefined") { setIsLoading(false); - } + return + }; + const fetchMonitor = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorById({ monitorId: monitorId }); + setMonitor(res.data.data); + } catch (error) { + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); }, [monitorId, setMonitor, updateTrigger]); return [isLoading]; }; From b1b4fd3f6406fa83468a8726bfe45a35e2e9a933 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 11 Jul 2025 11:23:36 -0700 Subject: [PATCH 19/83] add 1s cpu usage metric, return standard units of ms and bytes --- server/controllers/diagnosticController.js | 43 +++++++++++++++------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js index c4223dc04..4410a28a5 100755 --- a/server/controllers/diagnosticController.js +++ b/server/controllers/diagnosticController.js @@ -53,15 +53,34 @@ class DiagnosticController { } } - async getSystemStats(req, res, next) { + async getCPUUsage() { + try { + const startUsage = process.cpuUsage(); + await new Promise((resolve) => setTimeout(resolve, 1000)); + const endUsage = process.cpuUsage(startUsage); + const cpuUsage = { + userUsageMs: endUsage.user / 1000, + systemUsageMs: endUsage.system / 1000, + usagePercentage: ((endUsage.user + endUsage.system) / 1000 / 1000) * 100, + }; + return cpuUsage; + } catch (error) { + return { + userUsageMs: 0, + systemUsageMs: 0, + }; + } + } + + getSystemStats = async (req, res, next) => { try { // Memory Usage const totalMemory = os.totalmem(); const freeMemory = os.freemem(); const osStats = { - freeMemoryMb: freeMemory / 1024 / 1024, // MB - totalMemoryMb: totalMemory / 1024 / 1024, // MB + freeMemoryBytes: freeMemory, // bytes + totalMemoryBytes: totalMemory, // bytes }; const used = process.memoryUsage(); @@ -71,18 +90,14 @@ class DiagnosticController { } // CPU Usage - const cpuUsage = process.cpuUsage(); - const cpuMetrics = { - userUsageMs: cpuUsage.user / 1000, // ms - systemUsageMs: cpuUsage.system / 1000, // ms - }; + const cpuMetrics = await this.getCPUUsage(); // V8 Heap Statistics const heapStats = v8.getHeapStatistics(); const v8Metrics = { - totalHeapSizeMb: heapStats.total_heap_size / 1024 / 1024, // MB - usedHeapSizeMb: heapStats.used_heap_size / 1024 / 1024, // MB - heapSizeLimitMb: heapStats.heap_size_limit / 1024 / 1024, // MB + totalHeapSizeBytes: heapStats.total_heap_size, // bytes + usedHeapSizeBytes: heapStats.used_heap_size, // bytes + heapSizeLimitBytes: heapStats.heap_size_limit, // bytes }; // Event Loop Delay @@ -97,7 +112,7 @@ class DiagnosticController { } // Uptime - const uptime = process.uptime(); // seconds + const uptimeMs = process.uptime() * 1000; // ms // Combine Metrics const diagnostics = { @@ -106,7 +121,7 @@ class DiagnosticController { cpuUsage: cpuMetrics, v8HeapStats: v8Metrics, eventLoopDelayMs: eventLoopDelay, - uptimeSeconds: uptime, + uptimeMs, }; return res.success({ @@ -116,6 +131,6 @@ class DiagnosticController { } catch (error) { next(handleError(error, SERVICE_NAME, "getMemoryUsage")); } - } + }; } export default DiagnosticController; From 5b395b96cd5f7001dee9d62c5a20e03471b6993f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 11 Jul 2025 11:23:50 -0700 Subject: [PATCH 20/83] implemet stats --- .../Components/Charts/CustomGauge/index.jsx | 35 ++++++- .../Diagnostics/components/gauges/index.jsx | 73 ++++++++++---- .../Diagnostics/components/gauges/utils.js | 4 - .../Diagnostics/components/stats/index.jsx | 97 +++++++++++++++++++ client/src/Pages/Logs/Diagnostics/index.jsx | 20 +++- .../src/Pages/Logs/Diagnostics/utils/utils.js | 16 +++ client/src/Pages/Logs/Logs/index.jsx | 4 + .../Logs/Queue/components/Metrics/index.jsx | 27 +++--- client/src/Pages/Logs/Queue/index.jsx | 69 +++++++------ client/src/Utils/timeUtils.js | 4 + client/src/locales/en.json | 22 ++++- 11 files changed, 294 insertions(+), 77 deletions(-) delete mode 100644 client/src/Pages/Logs/Diagnostics/components/gauges/utils.js create mode 100644 client/src/Pages/Logs/Diagnostics/components/stats/index.jsx create mode 100644 client/src/Pages/Logs/Diagnostics/utils/utils.js diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/Charts/CustomGauge/index.jsx index 359b5b328..d9718d72c 100644 --- a/client/src/Components/Charts/CustomGauge/index.jsx +++ b/client/src/Components/Charts/CustomGauge/index.jsx @@ -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 * { +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( @@ -57,6 +69,20 @@ const CustomGauge = ({ progress = 0, radius = 70, strokeWidth = 15, threshold = ? theme.palette.error.lowContrast : theme.palette.accent.main; + if (isLoading) { + return ( + + + + ); + } + return ( - {`${progressWithinRange.toFixed(1)}%`} + {`${progressWithinRange.toFixed(precision)}${unit}`} ); @@ -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, }; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx index 21d9a35d8..e9f235112 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -1,12 +1,12 @@ import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; 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"; +import { getPercentage } from "../../utils/utils"; +import { useTranslation } from "react-i18next"; const GaugeBox = ({ title, subtitle, children }) => { const theme = useTheme(); @@ -14,6 +14,8 @@ const GaugeBox = ({ title, subtitle, children }) => { {children} @@ -23,52 +25,85 @@ const GaugeBox = ({ title, subtitle, children }) => { ); }; -const Gauges = ({ diagnostics }) => { +GaugeBox.propTypes = { + title: PropTypes.string.isRequired, + subtitle: PropTypes.string.isRequired, + children: PropTypes.node.isRequired, +}; + +const Gauges = ({ diagnostics, isLoading }) => { const heapTotalSize = getPercentage( - diagnostics?.v8HeapStats?.totalHeapSizeMb, - diagnostics?.v8HeapStats?.heapSizeLimitMb + diagnostics?.v8HeapStats?.totalHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes ); const heapUsedSize = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeMb, - diagnostics?.v8HeapStats?.heapSizeLimitMb + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.heapSizeLimitBytes ); const actualHeapUsed = getPercentage( - diagnostics?.v8HeapStats?.usedHeapSizeMb, - diagnostics?.v8HeapStats?.totalHeapSizeMb + diagnostics?.v8HeapStats?.usedHeapSizeBytes, + diagnostics?.v8HeapStats?.totalHeapSizeBytes ); + const theme = useTheme(); + const { t } = useTranslation(); return ( - + - + - + + + + ); }; Gauges.propTypes = { - diagnostics: PropTypes.object.isRequired, + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, }; export default Gauges; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js b/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js deleted file mode 100644 index bdc570a3b..000000000 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/utils.js +++ /dev/null @@ -1,4 +0,0 @@ -export const getPercentage = (value, total) => { - if (!value || !total) return 0; - return (value / total) * 100; -}; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx new file mode 100644 index 000000000..103d1c698 --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -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 ( + + {isLoading ? ( + + + + ) : ( + + + {title} + + + {value} {unit} + + + )} + + ); +}; + +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 ( + + + + + + + + + + + ); +}; + +Stats.propTypes = { + diagnostics: PropTypes.object, + isLoading: PropTypes.bool, +}; + +export default Stats; diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx index 9d7ee4639..e000c05bc 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -2,6 +2,8 @@ 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 { useTheme } from "@emotion/react"; import { useState } from "react"; @@ -15,14 +17,26 @@ const Diagnostics = () => { const theme = useTheme(); const { t } = useTranslation(); const [diagnostics, isLoading, error] = useFetchDiagnostics(); - console.log(diagnostics); // Setup return ( - {t("diagnosticsPage.description")} + {t("diagnosticsPage.diagnosticDescription")} - + + + + + ); }; diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js new file mode 100644 index 000000000..a3b75631f --- /dev/null +++ b/client/src/Pages/Logs/Diagnostics/utils/utils.js @@ -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`; +}; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index d8bd323a3..b65f9c0fd 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -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"; @@ -53,10 +54,13 @@ const Logs = () => { {t("logsPage.description")} + + {t("logsPage.logLevelSelect.title")} tz._id === form.timezone) || null} + inputValue={timezoneValue} + handleInputChange={(newVal) => setTimezoneValue(newVal)} + handleChange={(newValue) => { + setTimezoneValue(""); + handleFormChange({ + target: { name: "timezone", value: newValue?._id || "" }, + }); + }} + isAdorned={false} /> From a7e26d553ff0d1a41aa6f37f0dc64b130c5c6bac Mon Sep 17 00:00:00 2001 From: Yashaswee Kesharwani Date: Sat, 12 Jul 2025 23:22:42 +0530 Subject: [PATCH 22/83] chore: format code using npm run format --- client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index 5ed9bb122..de47cfc78 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -16,7 +16,6 @@ import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useState } from "react"; - const TabSettings = ({ isCreate, tabValue, From a7dd23141ccc022629c6cb73f39f287c126b10c9 Mon Sep 17 00:00:00 2001 From: Jesulayomy Date: Sat, 12 Jul 2025 14:26:17 -0400 Subject: [PATCH 23/83] [PageSpeed]: Formatting monitorHooks --- client/src/Hooks/monitorHooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 55c348626..4f0e9c049 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -207,8 +207,8 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { useEffect(() => { if (typeof monitorId === "undefined") { setIsLoading(false); - return - }; + return; + } const fetchMonitor = async () => { try { setIsLoading(true); From 39d666df6b51c7c779adc1f27b0af794ba8d4f92 Mon Sep 17 00:00:00 2001 From: shanika Jayawardane Date: Sat, 12 Jul 2025 15:13:24 -0600 Subject: [PATCH 24/83] add the checkboxes for dropdown list and remove searchbox list inside it --- .../Maintenance/CreateMaintenance/index.jsx | 33 +++++-------------- 1 file changed, 9 insertions(+), 24 deletions(-) diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 0be00e8da..19cb5b16d 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -222,23 +222,14 @@ const CreateMaintenance = () => { }; const handleMonitorsChange = (selected) => { - if (selected.some((m) => m._id === "__all__")) { - if (selectedMonitors.length !== monitors.length) { - setSelectedMonitors(monitors); - } else { - setSelectedMonitors([]); - } - } else { - // Ensure all are objects - const selectedObjs = selected - .map((sel) => - typeof sel === "string" ? monitors.find((m) => m._id === sel) : sel - ) - .filter(Boolean); - setSelectedMonitors(selectedObjs); - } + // Ensure all are objects + const selectedObjs = selected + .map((sel) => (typeof sel === "string" ? monitors.find((m) => m._id === sel) : sel)) + .filter(Boolean); + setSelectedMonitors(selectedObjs); + const { error } = maintenanceWindowValidation.validate( - { monitors: selected }, + { monitors: selectedObjs }, { abortEarly: false } ); setErrors((prev) => { @@ -246,8 +237,6 @@ const CreateMaintenance = () => { }); }; - const allSelected = selectedMonitors.length === monitors.length && monitors.length > 0; - const handleSubmit = async () => { if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; // Build timestamp for maintenance window from startDate and startTime @@ -303,10 +292,6 @@ const CreateMaintenance = () => { } }; - // Add Select All option to the dropdown - const selectAllOption = { _id: "__all__", name: t("selectAll") }; - const monitorOptions = [selectAllOption, ...monitors]; - return ( { id={"monitors"} label={t("addMonitors")} multiple={true} - isAdorned={false} - options={monitorOptions} + isAdorned={true} + options={monitors} filteredBy="name" secondaryLabel={"type"} inputValue={search} From 2bb78181163b4d445895632d8a3beb92827ecc42 Mon Sep 17 00:00:00 2001 From: Yashaswee Kesharwani Date: Sun, 13 Jul 2025 03:52:16 +0530 Subject: [PATCH 25/83] fix: replaced remaining Selects with Search in Settings Tab - optimised with useMemo and standardised the no unit found --- client/src/Components/Inputs/Search/index.jsx | 9 ++++- .../src/Pages/Settings/SettingsTimeZone.jsx | 40 +++++++++++++++---- .../Create/Components/Tabs/Settings.jsx | 36 ++++++++++++----- client/src/locales/en.json | 4 +- 4 files changed, 69 insertions(+), 20 deletions(-) diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx index 4624b2cba..f536bd5b5 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/Inputs/Search/index.jsx @@ -24,6 +24,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 */ @@ -68,6 +69,7 @@ const Search = ({ startAdornment, endAdornment, onBlur, + unit = "option", }) => { const theme = useTheme(); const { t } = useTranslation(); @@ -186,7 +188,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; }} diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx index 346e4e546..4b17f3f81 100644 --- a/client/src/Pages/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -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, timezones] + ); + + const handleTimezoneChange = useCallback( + (newValue) => { + setRawInput(""); + handleChange({ + target: { + name: "timezone", + value: newValue?._id ?? "", + }, + }); + }, + [handleChange] + ); + return ( @@ -28,12 +49,17 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => { -