From 76d69005aadd332fb9daca99b2e8c269b3cf4e29 Mon Sep 17 00:00:00 2001
From: Thirukumaran-T
Date: Sat, 24 May 2025 18:33:05 +0530
Subject: [PATCH 001/319] REFACTOR: added sticky behaviour to the save button
at the end
---
client/src/Pages/Settings/index.jsx | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx
index 66662dc58..5c58ce223 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/Settings/index.jsx
@@ -189,6 +189,20 @@ const Settings = () => {
+
+
+
+
+
## 🏗️ Tech stack
From 1f9fbf71155f2d22a481d20fa23eaa0b0ed83b7c Mon Sep 17 00:00:00 2001
From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com>
Date: Wed, 4 Jun 2025 09:09:54 -0400
Subject: [PATCH 018/319] Update PULLREQUESTS.md
---
PULLREQUESTS.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/PULLREQUESTS.md b/PULLREQUESTS.md
index 725a175b7..d1ee8eea9 100644
--- a/PULLREQUESTS.md
+++ b/PULLREQUESTS.md
@@ -27,6 +27,10 @@ Process. If more revisions are required after the second review we’re looking
If PRs are small and manageable it is far more likely that a dev will catch bugs during the review process. If our eyes glaze over at line 400 of a 700 line PR since we’ve reached our cognitive limit we’re not going to likely miss bugs in the last 300 lines of code.
-### Bonus Topic: Keep PRs focused
+### Format your PRs for better readability
+
+Ensure you execute `npm run format` before submitting your pull requests in both the client and server directories. This command automatically applies our formatting structure, making the code easier to follow and review.
+
+### Keep PRs focused
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature.
From 9591ba3dc4c2f84138cacb0ff54cef9bc728a0b7 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 5 Jun 2025 10:44:27 +0800
Subject: [PATCH 019/319] version tagging images
---
.../workflows/deploy-images-on-release.yml | 149 ++++++++++++++++++
.github/workflows/deploy-images.yml | 133 ++++++++++++++++
.github/workflows/distribution-deploy.yml | 80 ----------
.../distribution-mono-deploy-arm.yml | 44 ------
.../workflows/distribution-mono-deploy.yml | 30 ----
docker/dist-mono/docker-compose.yaml | 6 +-
docker/dist/docker-compose.yaml | 8 +-
7 files changed, 289 insertions(+), 161 deletions(-)
create mode 100644 .github/workflows/deploy-images-on-release.yml
create mode 100644 .github/workflows/deploy-images.yml
delete mode 100644 .github/workflows/distribution-deploy.yml
delete mode 100644 .github/workflows/distribution-mono-deploy-arm.yml
delete mode 100644 .github/workflows/distribution-mono-deploy.yml
diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml
new file mode 100644
index 000000000..521acab61
--- /dev/null
+++ b/.github/workflows/deploy-images-on-release.yml
@@ -0,0 +1,149 @@
+name: Distribution deploy
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+jobs:
+ docker-build-and-push-client:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - name: Extract version from tag
+ id: extract_tag
+ run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Client Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \
+ -f ./docker/dist/client.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Client Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }}
+
+ docker-build-and-push-server:
+ needs: docker-build-and-push-client
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Extract version
+ id: extract_tag
+ run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Server Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \
+ -f ./docker/dist/server.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Server Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }}
+
+ - name: Build Mongo Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \
+ -f ./docker/dist/mongoDB.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push MongoDB Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }}
+
+ - name: Build Redis Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \
+ -f ./docker/dist/redis.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Redis Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }}
+
+ docker-build-and-push-server-mono-multiarch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Extract version
+ id: extract_tag
+ run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push multi-arch Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./docker/dist-arm/server.Dockerfile
+ push: true
+ tags: |
+ ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }}
+ platforms: linux/amd64,linux/arm64
+ labels: |
+ org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
+
+ docker-build-and-push-server-mono:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Extract version
+ id: extract_tag
+ run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Server Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \
+ -f ./docker/dist-mono/server.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Server Docker image
+ run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }}
diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml
new file mode 100644
index 000000000..bab2199a2
--- /dev/null
+++ b/.github/workflows/deploy-images.yml
@@ -0,0 +1,133 @@
+name: Distribution deploy
+
+on:
+ push:
+ branches: ["master"]
+ workflow_dispatch:
+jobs:
+ docker-build-and-push-client:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Client Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-client:latest \
+ -f ./docker/dist/client.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Client Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-client:latest
+
+ docker-build-and-push-server:
+ needs: docker-build-and-push-client
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Server Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-backend:latest \
+ -f ./docker/dist/server.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Server Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-backend:latest
+
+ - name: Build Mongo Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-mongo:latest \
+ -f ./docker/dist/mongoDB.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push MongoDB Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-mongo:latest
+
+ - name: Build Redis Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-redis:latest \
+ -f ./docker/dist/redis.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Redis Docker image
+ run: |
+ docker push ghcr.io/bluewave-labs/checkmate-redis:latest
+
+ docker-build-and-push-server-mono-multiarch:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build and push multi-arch Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./docker/dist-arm/server.Dockerfile
+ push: true
+ tags: |
+ ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
+ platforms: linux/amd64,linux/arm64
+ labels: |
+ org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
+
+ docker-build-and-push-server-mono:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Build Server Docker image
+ run: |
+ docker build \
+ -t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \
+ -f ./docker/dist-mono/server.Dockerfile \
+ --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ .
+
+ - name: Push Server Docker image
+ run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest
diff --git a/.github/workflows/distribution-deploy.yml b/.github/workflows/distribution-deploy.yml
deleted file mode 100644
index d4bcfc8b7..000000000
--- a/.github/workflows/distribution-deploy.yml
+++ /dev/null
@@ -1,80 +0,0 @@
-name: Distribution deploy
-
-on:
- push:
- branches: ["master"]
- workflow_dispatch:
-jobs:
- docker-build-and-push-client:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Build Client Docker image
- run: |
- docker build \
- -t ghcr.io/bluewave-labs/checkmate:frontend-dist \
- -f ./docker/dist/client.Dockerfile \
- --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- .
-
- - name: Push Client Docker image
- run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist
-
- docker-build-and-push-server:
- needs: docker-build-and-push-client
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Build Server Docker image
- run: |
- docker build \
- -t ghcr.io/bluewave-labs/checkmate:backend-dist \
- -f ./docker/dist/server.Dockerfile \
- --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- .
-
- - name: Push Server Docker image
- run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist
-
- - name: Build Mongo Docker image
- run: |
- docker build \
- -t ghcr.io/bluewave-labs/checkmate:mongo-dist \
- -f ./docker/dist/mongoDB.Dockerfile \
- --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- .
-
- - name: Push MongoDB Docker image
- run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist
-
- - name: Build Redis Docker image
- run: |
- docker build \
- -t ghcr.io/bluewave-labs/checkmate:redis-dist \
- -f ./docker/dist/redis.Dockerfile \
- --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- .
-
- - name: Push Redis Docker image
- run: docker push ghcr.io/bluewave-labs/checkmate:redis-dist
diff --git a/.github/workflows/distribution-mono-deploy-arm.yml b/.github/workflows/distribution-mono-deploy-arm.yml
deleted file mode 100644
index 345cab560..000000000
--- a/.github/workflows/distribution-mono-deploy-arm.yml
+++ /dev/null
@@ -1,44 +0,0 @@
-name: Distribution deploy - Monolithic Multiarch
-
-on:
- push:
- branches: ["master"]
- workflow_dispatch:
-jobs:
- docker-build-and-push-server:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Build and push multi-arch Docker image
- uses: docker/build-push-action@v5
- with:
- context: .
- file: ./docker/dist-arm/server.Dockerfile
- push: true
- tags: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
- platforms: linux/amd64,linux/arm64
- labels: |
- org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate
-
- # - name: Build Server Docker image
- # run: |
- # docker build \
- # -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch \
- # -f ./docker/dist-arm/server.Dockerfile \
- # --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- # .
-
- # - name: Push Server Docker image
- # run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
diff --git a/.github/workflows/distribution-mono-deploy.yml b/.github/workflows/distribution-mono-deploy.yml
deleted file mode 100644
index 7dafbfc21..000000000
--- a/.github/workflows/distribution-mono-deploy.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Distribution deploy - Monolithic
-
-on:
- push:
- branches: ["master"]
- workflow_dispatch:
-jobs:
- docker-build-and-push-server:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout code
- uses: actions/checkout@v4
-
- - name: Log in to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Build Server Docker image
- run: |
- docker build \
- -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono \
- -f ./docker/dist-mono/server.Dockerfile \
- --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
- .
-
- - name: Push Server Docker image
- run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono
diff --git a/docker/dist-mono/docker-compose.yaml b/docker/dist-mono/docker-compose.yaml
index 9b3364975..fd5d54ddc 100755
--- a/docker/dist-mono/docker-compose.yaml
+++ b/docker/dist-mono/docker-compose.yaml
@@ -1,6 +1,6 @@
services:
server:
- image: ghcr.io/bluewave-labs/checkmate:backend-dist-mono
+ image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest
restart: always
ports:
- "52345:52345"
@@ -15,7 +15,7 @@ services:
- redis
- mongodb
redis:
- image: ghcr.io/bluewave-labs/checkmate:redis-dist
+ image: ghcr.io/bluewave-labs/checkmate-redis:latest
restart: always
volumes:
- ./redis/data:/data
@@ -26,7 +26,7 @@ services:
retries: 5
start_period: 5s
mongodb:
- image: ghcr.io/bluewave-labs/checkmate:mongo-dist
+ image: ghcr.io/bluewave-labs/checkmate-mongo:latest
restart: always
command: ["mongod", "--quiet", "--replSet", "rs0", "--bind_ip_all"]
volumes:
diff --git a/docker/dist/docker-compose.yaml b/docker/dist/docker-compose.yaml
index a5da9d021..a0d13d061 100755
--- a/docker/dist/docker-compose.yaml
+++ b/docker/dist/docker-compose.yaml
@@ -1,6 +1,6 @@
services:
client:
- image: ghcr.io/bluewave-labs/checkmate:frontend-dist
+ image: ghcr.io/bluewave-labs/checkmate-frontend:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1"
@@ -11,7 +11,7 @@ services:
depends_on:
- server
server:
- image: ghcr.io/bluewave-labs/checkmate:backend-dist
+ image: ghcr.io/bluewave-labs/checkmate-backend:latest
restart: always
ports:
- "52345:52345"
@@ -26,7 +26,7 @@ services:
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
redis:
- image: ghcr.io/bluewave-labs/checkmate:redis-dist
+ image: ghcr.io/bluewave-labs/checkmate-redis:latest
restart: always
volumes:
- ./redis/data:/data
@@ -37,7 +37,7 @@ services:
retries: 5
start_period: 5s
mongodb:
- image: ghcr.io/bluewave-labs/checkmate:mongo-dist
+ image: ghcr.io/bluewave-labs/checkmate-mongo:latest
restart: always
volumes:
- ./mongo/data:/data/db
From a101bd479a95376a65af122334112c5d775f8112 Mon Sep 17 00:00:00 2001
From: Thirukumaran-T
Date: Thu, 5 Jun 2025 12:00:16 +0530
Subject: [PATCH 020/319] CHORE: added border styles to the save button
container
---
client/src/Pages/Settings/index.jsx | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx
index 6c3048c48..67f82afbe 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/Settings/index.jsx
@@ -202,6 +202,10 @@ const Settings = () => {
pr: theme.spacing(15),
pl: theme.spacing(5),
pt: theme.spacing(4),
+ border: 1,
+ borderStyle: "solid",
+ borderColor: theme.palette.primary.lowContrast,
+ borderRadius: theme.spacing(2)
}}
>
Date: Fri, 6 Jun 2025 07:44:19 +0800
Subject: [PATCH 021/319] change workflow names
---
.github/workflows/deploy-images-on-release.yml | 2 +-
.github/workflows/deploy-images.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml
index 521acab61..f1d0dd7ce 100644
--- a/.github/workflows/deploy-images-on-release.yml
+++ b/.github/workflows/deploy-images-on-release.yml
@@ -1,4 +1,4 @@
-name: Distribution deploy
+name: Deploy images on release
on:
push:
diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml
index bab2199a2..d3d70b90b 100644
--- a/.github/workflows/deploy-images.yml
+++ b/.github/workflows/deploy-images.yml
@@ -1,4 +1,4 @@
-name: Distribution deploy
+name: Deploy images
on:
push:
From 9a22d4a971876d8d2ff7a3fe8297ef7b83ecb4f3 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Fri, 6 Jun 2025 14:05:59 +0800
Subject: [PATCH 022/319] initial Pulse implementation
---
server/controllers/monitorController.js | 4 +-
server/controllers/queueController.js | 12 --
server/index.js | 37 ++--
server/package-lock.json | 98 +++++++++-
server/package.json | 1 +
server/routes/queueRoute.js | 6 -
server/service/JobQueue/JobQueue.js | 9 +-
server/service/JobQueue/JobQueueHelper.js | 12 +-
server/service/PulseQueue/PulseQueue.js | 171 ++++++++++++++++++
server/service/PulseQueue/PulseQueueHelper.js | 106 +++++++++++
server/service/networkService.js | 57 +++---
server/service/statusService.js | 14 +-
12 files changed, 455 insertions(+), 72 deletions(-)
create mode 100644 server/service/PulseQueue/PulseQueue.js
create mode 100644 server/service/PulseQueue/PulseQueueHelper.js
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index e8eaeb106..5f0e22ecf 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -595,8 +595,8 @@ class MonitorController {
{ new: true }
);
monitor.isActive === true
- ? await this.jobQueue.addJob(monitor._id, monitor)
- : await this.jobQueue.deleteJob(monitor);
+ ? await this.jobQueue.resumeJob(monitor._id, monitor)
+ : await this.jobQueue.pauseJob(monitor);
return res.success({
msg: monitor.isActive
diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js
index be3d764e7..b1a7badee 100755
--- a/server/controllers/queueController.js
+++ b/server/controllers/queueController.js
@@ -46,18 +46,6 @@ class JobQueueController {
}
};
- obliterateQueue = async (req, res, next) => {
- try {
- await this.jobQueue.obliterate();
- return res.success({
- msg: this.stringService.queueObliterate,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "obliterateQueue"));
- return;
- }
- };
-
flushQueue = async (req, res, next) => {
try {
const result = await this.jobQueue.flushQueues();
diff --git a/server/index.js b/server/index.js
index eea5b015a..81970ae7a 100755
--- a/server/index.js
+++ b/server/index.js
@@ -50,6 +50,9 @@ import JobQueue from "./service/JobQueue/JobQueue.js";
import JobQueueHelper from "./service/JobQueue/JobQueueHelper.js";
import { Queue, Worker } from "bullmq";
+import PulseQueue from "./service/PulseQueue/PulseQueue.js";
+import PulseQueueHelper from "./service/PulseQueue/PulseQueueHelper.js";
+
//Network service and dependencies
import NetworkService from "./service/networkService.js";
import axios from "axios";
@@ -183,25 +186,35 @@ const startApp = async () => {
const redisService = new RedisService({ Redis: IORedis, logger });
- const jobQueueHelper = new JobQueueHelper({
- redisService,
- Queue,
- Worker,
- logger,
+ // const jobQueueHelper = new JobQueueHelper({
+ // redisService,
+ // Queue,
+ // Worker,
+ // logger,
+ // db,
+ // networkService,
+ // statusService,
+ // notificationService,
+ // });
+ // const jobQueue = await JobQueue.create({
+ // db,
+ // jobQueueHelper,
+ // logger,
+ // stringService,
+ // });
+
+ const pulseQueueHelper = new PulseQueueHelper({
db,
+ logger,
networkService,
statusService,
notificationService,
});
- const jobQueue = await JobQueue.create({
- db,
- jobQueueHelper,
- logger,
- stringService,
- });
+ const pulseQueue = new PulseQueue({ appSettings, db, pulseQueueHelper, logger });
// Register services
- ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
+ // ServiceRegistry.register(JobQueue.SERVICE_NAME, jobQueue);
+ ServiceRegistry.register(JobQueue.SERVICE_NAME, pulseQueue);
ServiceRegistry.register(MongoDB.SERVICE_NAME, db);
ServiceRegistry.register(SettingsService.SERVICE_NAME, settingsService);
ServiceRegistry.register(EmailService.SERVICE_NAME, emailService);
diff --git a/server/package-lock.json b/server/package-lock.json
index 7bf4bfc78..3fd675af3 100755
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@pulsecron/pulse": "1.6.8",
"axios": "^1.7.2",
"bcryptjs": "3.0.2",
"bullmq": "5.41.2",
@@ -1012,6 +1013,37 @@
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==",
"license": "BSD-3-Clause"
},
+ "node_modules/@pulsecron/pulse": {
+ "version": "1.6.8",
+ "resolved": "https://registry.npmjs.org/@pulsecron/pulse/-/pulse-1.6.8.tgz",
+ "integrity": "sha512-fg9sT/pfpZTlUYr/ktDu6om6XEm2zmulcZ8TP4JZa+Dmol7/35mMKzDwT1H5a0AUcxiFIRfB4KOpkJ+BI2fgmg==",
+ "license": "MIT",
+ "dependencies": {
+ "cron-parser": "^4.9.0",
+ "date.js": "~0.3.3",
+ "debug": "~4.3.4",
+ "human-interval": "~2.0.1",
+ "moment-timezone": "^0.5.45",
+ "mongodb": "^6.5.0"
+ }
+ },
+ "node_modules/@pulsecron/pulse/node_modules/debug": {
+ "version": "4.3.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
+ "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@scarf/scarf": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz",
@@ -2394,6 +2426,30 @@
"integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==",
"license": "CC0-1.0"
},
+ "node_modules/date.js": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/date.js/-/date.js-0.3.3.tgz",
+ "integrity": "sha512-HgigOS3h3k6HnW011nAb43c5xx5rBXk8P2v/WIT9Zv4koIaVXiH2BURguI78VVp+5Qc076T7OR378JViCnZtBw==",
+ "license": "MIT",
+ "dependencies": {
+ "debug": "~3.1.0"
+ }
+ },
+ "node_modules/date.js/node_modules/debug": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz",
+ "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/date.js/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@@ -3664,6 +3720,15 @@
"node": ">= 6"
}
},
+ "node_modules/human-interval": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/human-interval/-/human-interval-2.0.1.tgz",
+ "integrity": "sha512-r4Aotzf+OtKIGQCB3odUowy4GfUDTy3aTWTfLd7ZF2gBCy3XW3v/dJLRefZnOFFnjqs5B1TypvS8WarpBkYUNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "numbered": "^1.1.0"
+ }
+ },
"node_modules/iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -4986,6 +5051,27 @@
"url": "https://github.com/chalk/supports-color?sponsor=1"
}
},
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moment-timezone": {
+ "version": "0.5.48",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/mongodb": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.13.0.tgz",
@@ -5385,6 +5471,12 @@
"url": "https://github.com/fb55/nth-check?sponsor=1"
}
},
+ "node_modules/numbered": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/numbered/-/numbered-1.1.0.tgz",
+ "integrity": "sha512-pv/ue2Odr7IfYOO0byC1KgBI10wo5YDauLhxY6/saNzAdAs0r1SotGCPzzCLNPL0xtrAwWRialLu23AAu9xO1g==",
+ "license": "MIT"
+ },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -7099,9 +7191,9 @@
}
},
"node_modules/tar-fs": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz",
- "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==",
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+ "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
diff --git a/server/package.json b/server/package.json
index 097cf4443..51e7cd218 100755
--- a/server/package.json
+++ b/server/package.json
@@ -16,6 +16,7 @@
"author": "",
"license": "ISC",
"dependencies": {
+ "@pulsecron/pulse": "1.6.8",
"axios": "^1.7.2",
"bcryptjs": "3.0.2",
"bullmq": "5.41.2",
diff --git a/server/routes/queueRoute.js b/server/routes/queueRoute.js
index 38ac32a26..dc9b40917 100755
--- a/server/routes/queueRoute.js
+++ b/server/routes/queueRoute.js
@@ -25,12 +25,6 @@ class QueueRoutes {
this.queueController.addJob
);
- this.router.post(
- "/obliterate",
- isAllowed(["admin", "superadmin"]),
- this.queueController.obliterateQueue
- );
-
this.router.post(
"/flush",
isAllowed(["admin", "superadmin"]),
diff --git a/server/service/JobQueue/JobQueue.js b/server/service/JobQueue/JobQueue.js
index 32d531809..9349eba7e 100644
--- a/server/service/JobQueue/JobQueue.js
+++ b/server/service/JobQueue/JobQueue.js
@@ -51,7 +51,6 @@ class JobQueue {
}
})
);
-
this.healthCheckInterval = setInterval(async () => {
try {
const queueHealthChecks = await this.checkQueueHealth();
@@ -116,6 +115,14 @@ class JobQueue {
});
}
+ pauseJob = async (monitor) => {
+ this.deleteJob(monitor);
+ };
+
+ resumeJob = async (monitor) => {
+ this.addJob(monitor._id, monitor);
+ };
+
async addJob(jobName, monitor) {
this.logger.info({
message: `Adding job ${monitor?.url ?? "No URL"}`,
diff --git a/server/service/JobQueue/JobQueueHelper.js b/server/service/JobQueue/JobQueueHelper.js
index de1e00fb8..9e08e2cf1 100644
--- a/server/service/JobQueue/JobQueueHelper.js
+++ b/server/service/JobQueue/JobQueueHelper.js
@@ -254,7 +254,8 @@ class JobQueueHelper {
// Get the current status
await job.updateProgress(30);
- const networkResponse = await this.networkService.getStatus(job);
+ const monitor = job.data;
+ const networkResponse = await this.networkService.getStatus(monitor);
if (
job.data.type === "distributed_http" ||
job.data.type === "distributed_test"
@@ -271,14 +272,17 @@ class JobQueueHelper {
// Handle status change
await job.updateProgress(60);
- const { monitor, statusChanged, prevStatus } =
- await this.statusService.updateStatus(networkResponse);
+ const {
+ monitor: updatedMonitor,
+ statusChanged,
+ prevStatus,
+ } = await this.statusService.updateStatus(networkResponse);
// Handle notifications
await job.updateProgress(80);
this.notificationService
.handleNotifications({
...networkResponse,
- monitor,
+ monitor: updatedMonitor,
prevStatus,
statusChanged,
})
diff --git a/server/service/PulseQueue/PulseQueue.js b/server/service/PulseQueue/PulseQueue.js
new file mode 100644
index 000000000..63e363b71
--- /dev/null
+++ b/server/service/PulseQueue/PulseQueue.js
@@ -0,0 +1,171 @@
+import { Pulse } from "@pulsecron/pulse";
+import { ObjectId } from "mongodb";
+
+const SERVICE_NAME = "PulseQueue";
+class PulseQueue {
+ static SERVICE_NAME = SERVICE_NAME;
+
+ constructor({ appSettings, db, pulseQueueHelper, logger }) {
+ this.db = db;
+ this.appSettings = appSettings;
+ this.pulseQueueHelper = pulseQueueHelper;
+ this.logger = logger;
+ this.init();
+ }
+
+ // ****************************************
+ // Core methods
+ // ****************************************
+ init = async () => {
+ try {
+ const mongoConnectionString =
+ this.appSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db";
+ this.pulse = new Pulse({ db: { address: mongoConnectionString } });
+ await this.pulse.start();
+ this.pulse.define("monitor-job", this.pulseQueueHelper.getMonitorJob(), {});
+
+ const monitors = await this.db.getAllMonitors();
+ for (const monitor of monitors) {
+ await this.addJob(monitor._id, monitor);
+ }
+ return true;
+ } catch (error) {
+ this.logger.error({
+ message: "Failed to initialize PulseQueue",
+ service: SERVICE_NAME,
+ method: "init",
+ details: error,
+ });
+ return false;
+ }
+ };
+
+ addJob = async (monitorId, monitor) => {
+ this.logger.debug({
+ message: `Adding job ${monitor?.url ?? "No URL"}`,
+ service: SERVICE_NAME,
+ method: "addJob",
+ });
+ const intervalInSeconds = monitor.interval / 1000;
+ const job = this.pulse.create("monitor-job", {
+ monitor,
+ });
+ job.unique({ "data.monitor._id": monitor._id });
+ job.attrs.jobId = monitorId.toString();
+ job.repeatEvery(`${intervalInSeconds} seconds`);
+ await job.save();
+ };
+
+ deleteJob = async (monitor) => {
+ this.logger.debug({
+ message: `Deleting job ${monitor?.url ?? "No URL"}`,
+ service: SERVICE_NAME,
+ method: "deleteJob",
+ });
+ const result = await this.pulse.cancel({
+ "data.monitor._id": monitor._id,
+ });
+ console.log(result);
+ };
+
+ pauseJob = async (monitor) => {
+ const result = await this.pulse.disable({
+ "data.monitor._id": monitor._id,
+ });
+
+ if (result.length < 1) {
+ throw new Error("Failed to pause monitor");
+ }
+
+ this.logger.debug({
+ message: `Paused monitor ${monitor._id}`,
+ service: SERVICE_NAME,
+ method: "pauseJob",
+ });
+ };
+
+ resumeJob = async (monitor) => {
+ const result = await this.pulse.enable({
+ "data.monitor._id": monitor._id,
+ });
+
+ if (result.length < 1) {
+ throw new Error("Failed to resume monitor");
+ }
+
+ this.logger.debug({
+ message: `Resumed monitor ${monitor._id}`,
+ service: SERVICE_NAME,
+ method: "resumeJob",
+ });
+ };
+
+ shutdown = async () => {
+ this.logger.info({
+ message: "Shutting down JobQueue",
+ service: SERVICE_NAME,
+ method: "shutdown",
+ });
+ await this.pulse.stop();
+ };
+
+ // ****************************************
+ // Diagnostic methods
+ // ****************************************
+
+ getMetrics = async () => {
+ const jobs = await this.pulse.jobs();
+ const metrics = jobs.reduce(
+ (acc, job) => {
+ acc.jobs++;
+ if (job.attrs.failCount > 0 && job.attrs.failedAt >= job.attrs.lastFinishedAt) {
+ acc.failingJobs++;
+ }
+ if (job.attrs.lockedAt) {
+ acc.activeJobs++;
+ }
+ if (job.attrs.failCount > 0) {
+ acc.jobsWithFailures.push({
+ monitorId: job.attrs.data.monitor._id,
+ monitorUrl: job.attrs.data.monitor.url,
+ failCount: job.attrs.failCount,
+ failReason: job.attrs.failReason,
+ });
+ }
+ return acc;
+ },
+ { jobs: 0, activeJobs: 0, failingJobs: 0, jobsWithFailures: [] }
+ );
+ return metrics;
+ };
+
+ getJobs = async () => {
+ const jobs = await this.pulse.jobs();
+ return jobs.map((job) => {
+ return {
+ monitorId: job.attrs.data.monitor._id,
+ monitorUrl: job.attrs.data.monitor.url,
+ lockedAt: job.attrs.lockedAt,
+ runCount: job.attrs.runCount || 0,
+ failCount: job.attrs.failCount || 0,
+ failReason: job.attrs.failReason,
+ };
+ });
+ };
+
+ flushQueues = async () => {
+ const cancelRes = await this.pulse.cancel();
+ await this.pulse.stop();
+ const initRes = await this.init();
+ return {
+ flushedJobs: cancelRes,
+ initSuccess: initRes,
+ };
+ };
+
+ obliterate = async () => {
+ await this.flushQueues();
+ };
+}
+
+export default PulseQueue;
diff --git a/server/service/PulseQueue/PulseQueueHelper.js b/server/service/PulseQueue/PulseQueueHelper.js
new file mode 100644
index 000000000..b1454ce26
--- /dev/null
+++ b/server/service/PulseQueue/PulseQueueHelper.js
@@ -0,0 +1,106 @@
+const SERVICE_NAME = "PulseQueueHelper";
+
+class PulseQueueHelper {
+ constructor({ db, logger, networkService, statusService, notificationService }) {
+ this.db = db;
+ this.logger = logger;
+ this.networkService = networkService;
+ this.statusService = statusService;
+ this.notificationService = notificationService;
+ }
+
+ getMonitorJob = () => {
+ return async (job) => {
+ try {
+ const monitor = job.attrs.data.monitor;
+ const monitorId = job.attrs.data.monitor._id;
+ if (!monitorId) {
+ throw new Error("No monitor id");
+ }
+
+ const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId);
+ if (maintenanceWindowActive) {
+ this.logger.info({
+ message: `Monitor ${monitorId} is in maintenance window`,
+ service: SERVICE_NAME,
+ method: "getMonitorJob",
+ });
+ return;
+ }
+
+ const networkResponse = await this.networkService.getStatus(monitor);
+ if (monitor.type === "distributed_http" || monitor.type === "distributed_test") {
+ await job.updateProgress(100);
+ return true;
+ }
+
+ if (!networkResponse) {
+ throw new Error("No network response");
+ }
+
+ const {
+ monitor: updatedMonitor,
+ statusChanged,
+ prevStatus,
+ } = await this.statusService.updateStatus(networkResponse);
+
+ this.notificationService
+ .handleNotifications({
+ ...networkResponse,
+ monitor: updatedMonitor,
+ prevStatus,
+ statusChanged,
+ })
+ .catch((error) => {
+ this.logger.error({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "createJobHandler",
+ details: `Error sending notifications for job ${job.id}: ${error.message}`,
+ stack: error.stack,
+ });
+ });
+ } catch (error) {
+ this.logger.warn({
+ message: error.message,
+ service: error.service || SERVICE_NAME,
+ method: error.method || "getMonitorJob",
+ stack: error.stack,
+ });
+ throw error;
+ }
+ };
+ };
+
+ async isInMaintenanceWindow(monitorId) {
+ const maintenanceWindows = await this.db.getMaintenanceWindowsByMonitorId(monitorId);
+ // Check for active maintenance window:
+ const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => {
+ if (window.active) {
+ const start = new Date(window.start);
+ const end = new Date(window.end);
+ const now = new Date();
+ const repeatInterval = window.repeat || 0;
+
+ // If start is < now and end > now, we're in maintenance
+ if (start <= now && end >= now) return true;
+
+ // If maintenance window was set in the past with a repeat,
+ // we need to advance start and end to see if we are in range
+
+ while (start < now && repeatInterval !== 0) {
+ start.setTime(start.getTime() + repeatInterval);
+ end.setTime(end.getTime() + repeatInterval);
+ if (start <= now && end >= now) {
+ return true;
+ }
+ }
+ return false;
+ }
+ return acc;
+ }, false);
+ return maintenanceWindowIsActive;
+ }
+}
+
+export default PulseQueueHelper;
diff --git a/server/service/networkService.js b/server/service/networkService.js
index a023ffadd..9e5e83d24 100755
--- a/server/service/networkService.js
+++ b/server/service/networkService.js
@@ -125,7 +125,7 @@ class NetworkService {
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the HTTP request.
*/
- async requestHttp(job) {
+ async requestHttp(monitor) {
try {
const {
url,
@@ -138,7 +138,7 @@ class NetworkService {
jsonPath,
matchMethod,
expectedValue,
- } = job.data;
+ } = monitor;
const config = {};
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
@@ -248,10 +248,10 @@ class NetworkService {
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the PageSpeed request.
*/
- async requestPagespeed(job) {
+ async requestPagespeed(monitor) {
try {
- const url = job.data.url;
- const updatedJob = { ...job };
+ const url = monitor.url;
+ const updatedMonitor = { ...monitor };
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
const dbSettings = await this.settingsService.getDBSettings();
@@ -266,8 +266,8 @@ class NetworkService {
});
return;
}
- updatedJob.data.url = pagespeedUrl;
- return await this.requestHttp(updatedJob);
+ updatedMonitor.url = pagespeedUrl;
+ return await this.requestHttp(updatedMonitor);
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestPagespeed";
@@ -292,9 +292,9 @@ class NetworkService {
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the hardware status request.
*/
- async requestHardware(job) {
+ async requestHardware(monitor) {
try {
- return await this.requestHttp(job);
+ return await this.requestHttp(monitor);
} catch (error) {
error.service = this.SERVICE_NAME;
error.method = "requestHardware";
@@ -318,7 +318,7 @@ class NetworkService {
* @property {number} code - The response code (200 if successful, error code otherwise).
* @property {string} message - The message indicating the result of the Docker inspection.
*/
- async requestDocker(job) {
+ async requestDocker(monitor) {
try {
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
@@ -326,19 +326,19 @@ class NetworkService {
});
const containers = await docker.listContainers({ all: true });
- const containerExists = containers.some((c) => c.Id.startsWith(job.data.url));
+ const containerExists = containers.some((c) => c.Id.startsWith(monitor.url));
if (!containerExists) {
throw new Error(this.stringService.dockerNotFound);
}
- const container = docker.getContainer(job.data.url);
+ const container = docker.getContainer(monitor.url);
const { response, responseTime, error } = await this.timeRequest(() =>
container.inspect()
);
const dockerResponse = {
- monitorId: job.data._id,
- type: job.data.type,
+ monitorId: monitor._id,
+ type: monitor.type,
responseTime,
};
@@ -360,9 +360,9 @@ class NetworkService {
}
}
- async requestPort(job) {
+ async requestPort(monitor) {
try {
- const { url, port } = job.data;
+ const { url, port } = monitor;
const { response, responseTime, error } = await this.timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
@@ -391,8 +391,8 @@ class NetworkService {
});
const portResponse = {
- monitorId: job.data._id,
- type: job.data.type,
+ monitorId: monitor._id,
+ type: monitor.type,
responseTime,
};
@@ -414,9 +414,8 @@ class NetworkService {
}
}
- async requestDistributedHttp(job) {
+ async requestDistributedHttp(monitor) {
try {
- const monitor = job.data;
const CALLBACK_URL = process.env.CALLBACK_URL;
const response = await this.axios.post(
@@ -503,23 +502,23 @@ class NetworkService {
* @returns {Promise
{
boxShadow: `none`,
}}
>
- {t("continue")}
+ {t("auth.common.navigation.continue")}
diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx
index f3b2253d2..d2be3af35 100644
--- a/client/src/Pages/Auth/Login/Login.jsx
+++ b/client/src/Pages/Auth/Login/Login.jsx
@@ -15,7 +15,7 @@ import PasswordStep from "./Components/PasswordStep";
import ThemeSwitch from "../../../Components/ThemeSwitch";
import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
import LanguageSelector from "../../../Components/LanguageSelector";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
const DEMO = import.meta.env.VITE_APP_DEMO;
@@ -83,8 +83,8 @@ const Login = () => {
if (error) {
const errorMessage = error.details[0].message;
const translatedMessage = errorMessage.startsWith("auth")
- ? t(errorMessage)
- : errorMessage;
+ ? t(errorMessage) // Localization keys are in validation.js
+ : errorMessage; // FIXME: Potential untranslated string
setErrors((prev) => ({ ...prev, email: translatedMessage }));
createToast({ body: translatedMessage });
} else {
@@ -104,31 +104,31 @@ const Login = () => {
body:
error.details && error.details.length > 0
? error.details[0].message.startsWith("auth")
- ? t(error.details[0].message)
- : error.details[0].message
- : t("Error validating data."),
+ ? t(error.details[0].message) // Localization keys are in validation.js
+ : error.details[0].message // FIXME: Potential untranslated string
+ : t("auth.common.errors.validation"),
});
} else {
const action = await dispatch(login(form));
if (action.payload.success) {
navigate("/uptime");
createToast({
- body: t("welcomeBack"),
+ body: t("auth.login.toasts.success"),
});
} else {
if (action.payload) {
if (action.payload.msg === "Incorrect password")
setErrors({
- password: "The password you provided does not match our records",
+ password: t("auth.common.fields.password.errors.incorrect"),
});
// dispatch errors
createToast({
- body: action.payload.msg,
+ body: t("auth.login.toasts.incorrectPassword"),
});
} else {
// unknown errors
createToast({
- body: "Unknown error.",
+ body: t("common.toasts.unknownError"),
});
}
}
@@ -237,21 +237,25 @@ const Login = () => {
display="inline-block"
color={theme.palette.primary.main}
>
- {t("doNotHaveAccount")}
-
- navigate("/register")}
- >
- {t("registerHere")}
+ navigate("/register")}
+ />
+ ),
+ }}
+ />
diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
index f1980e23b..aba1fefea 100644
--- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -8,7 +8,7 @@ import Background from "../../assets/Images/background-grid.svg?react";
import ConfirmIcon from "../../assets/icons/check-outlined.svg?react";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import IconBox from "../../Components/IconBox";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import "./index.css";
const NewPasswordConfirmed = () => {
@@ -96,11 +96,11 @@ const NewPasswordConfirmed = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
-
+
- {t("passwordreset")}
- {t("authNewPasswordConfirmed")}
+ {t("auth.forgotPassword.heading")}
+ {t("auth.forgotPassword.subheadings.stepFour")}
{
maxWidth: 400,
}}
>
- {t("continue")}
+ {t("auth.common.navigation.continue")}
@@ -118,15 +118,21 @@ const NewPasswordConfirmed = () => {
textAlign="center"
p={theme.spacing(12)}
>
- {t("goBackTo")}
-
- {t("authLoginTitle")}
+
+
+ ),
+ }}
+ />
diff --git a/client/src/Pages/Auth/Register/Register.jsx b/client/src/Pages/Auth/Register/Register.jsx
index 49997e8cb..390aa6e36 100644
--- a/client/src/Pages/Auth/Register/Register.jsx
+++ b/client/src/Pages/Auth/Register/Register.jsx
@@ -35,11 +35,15 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
textAlign="center"
>
- {t("signUp")}
+
+ {isSuperAdmin
+ ? t("auth.registration.heading.superAdmin")
+ : t("auth.registration.heading.user")}
+
{isSuperAdmin
- ? t("authRegisterCreateSuperAdminAccount")
- : t("authRegisterCreateAccount")}
+ ? t("auth.registration.description.superAdmin")
+ : t("auth.registration.description.user")}
@@ -62,13 +66,15 @@ const LandingPage = ({ isSuperAdmin, onSignup }) => {
}}
>
- {t("authRegisterSignUpWithEmail")}
+ {isSuperAdmin
+ ? t("auth.registration.gettingStartedButton.superAdmin")
+ : t("auth.registration.gettingStartedButton.user")}
{
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
- createToast({ body: error.details[0].message || "Error validating data." });
+ // Localization keys are in validation.js
+ createToast({ body: t(error.details[0].message || "auth.common.errors.validation") });
};
const handleStepOne = async (e) => {
@@ -246,7 +253,7 @@ const Register = ({ isSuperAdmin }) => {
const authToken = action.payload.data;
navigate("/uptime");
createToast({
- body: "Welcome! Your account was created successfully.",
+ body: t("auth.registration.toasts.success"),
});
} else {
if (action.payload) {
@@ -255,7 +262,7 @@ const Register = ({ isSuperAdmin }) => {
});
} else {
createToast({
- body: "Unknown error.",
+ body: t("common.toasts.unknownError"),
});
}
}
@@ -346,6 +353,7 @@ const Register = ({ isSuperAdmin }) => {
/>
) : step === 1 ? (
{
/>
) : step === 2 ? (
{
/>
) : step === 3 ? (
{
p={theme.spacing(12)}
>
- {t("authRegisterAlreadyHaveAccount")}
-
- {
- navigate("/login");
- }}
- sx={{ userSelect: "none", color: theme.palette.accent.main }}
- >
- {t("authRegisterLoginLink")}
+ {
+ navigate("/login");
+ }}
+ sx={{ userSelect: "none", color: theme.palette.accent.main }}
+ />
+ ),
+ }}
+ />
diff --git a/client/src/Pages/Auth/Register/StepOne/index.jsx b/client/src/Pages/Auth/Register/StepOne/index.jsx
index cb66f6704..575a9132f 100644
--- a/client/src/Pages/Auth/Register/StepOne/index.jsx
+++ b/client/src/Pages/Auth/Register/StepOne/index.jsx
@@ -7,6 +7,7 @@ import TextInput from "../../../../Components/Inputs/TextInput";
import { useTranslation } from "react-i18next";
StepOne.propTypes = {
+ isSuperAdmin: PropTypes.bool,
form: PropTypes.object,
errors: PropTypes.object,
onSubmit: PropTypes.func,
@@ -18,6 +19,7 @@ StepOne.propTypes = {
* Renders the first step of the sign up process.
*
* @param {Object} props
+ * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
@@ -26,7 +28,7 @@ StepOne.propTypes = {
* @returns {JSX.Element}
*/
-function StepOne({ form, errors, onSubmit, onChange, onBack }) {
+function StepOne({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
@@ -45,8 +47,12 @@ function StepOne({ form, errors, onSubmit, onChange, onBack }) {
textAlign="center"
>
- {t("signUp")}
- {t("authRegisterStepOnePersonalDetails")}
+
+ {isSuperAdmin
+ ? t("auth.registration.heading.superAdmin")
+ : t("auth.registration.heading.user")}
+
+ {t("auth.registration.subheadings.stepOne")}
- {t("commonBack")}
+ {t("auth.common.navigation.back")}
- {t("continue")}
+ {t("auth.common.navigation.continue")}
diff --git a/client/src/Pages/Auth/Register/StepThree/index.jsx b/client/src/Pages/Auth/Register/StepThree/index.jsx
index 1af8a60dc..f34c7af7e 100644
--- a/client/src/Pages/Auth/Register/StepThree/index.jsx
+++ b/client/src/Pages/Auth/Register/StepThree/index.jsx
@@ -8,6 +8,7 @@ import Check from "../../../../Components/Check/Check";
import { useValidatePassword } from "../../hooks/useValidatePassword";
import { useTranslation } from "react-i18next";
StepThree.propTypes = {
+ isSuperAdmin: PropTypes.bool,
onSubmit: PropTypes.func,
onBack: PropTypes.func,
};
@@ -16,11 +17,12 @@ StepThree.propTypes = {
* Renders the third step of the sign up process.
*
* @param {Object} props
+ * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
* @param {Function} props.onSubmit - Callback function to handle form submission.
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
-function StepThree({ onSubmit, onBack }) {
+function StepThree({ isSuperAdmin, onSubmit, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
@@ -39,8 +41,12 @@ function StepThree({ onSubmit, onBack }) {
textAlign="center"
>
- {t("signUp")}
- {t("createPassword")}
+
+ {isSuperAdmin
+ ? t("auth.registration.heading.superAdmin")
+ : t("auth.registration.heading.user")}
+
+ {t("auth.registration.subheadings.stepThree")}
@@ -141,7 +151,7 @@ function StepThree({ onSubmit, onBack }) {
}}
>
- {t("commonBack")}
+ {t("auth.common.navigation.back")}
- {t("continue")}
+ {t("auth.common.navigation.continue")}
diff --git a/client/src/Pages/Auth/Register/StepTwo/index.jsx b/client/src/Pages/Auth/Register/StepTwo/index.jsx
index 4fa0e8ebc..306a2b750 100644
--- a/client/src/Pages/Auth/Register/StepTwo/index.jsx
+++ b/client/src/Pages/Auth/Register/StepTwo/index.jsx
@@ -7,6 +7,7 @@ import TextInput from "../../../../Components/Inputs/TextInput";
import { useTranslation } from "react-i18next";
StepTwo.propTypes = {
+ isSuperAdmin: PropTypes.bool,
form: PropTypes.object,
errors: PropTypes.object,
onSubmit: PropTypes.func,
@@ -18,6 +19,7 @@ StepTwo.propTypes = {
* Renders the second step of the sign up process.
*
* @param {Object} props
+ * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
* @param {Object} props.form - Form state object.
* @param {Object} props.errors - Object containing form validation errors.
* @param {Function} props.onSubmit - Callback function to handle form submission.
@@ -25,7 +27,7 @@ StepTwo.propTypes = {
* @param {Function} props.onBack - Callback function to handle "Back" button click.
* @returns {JSX.Element}
*/
-function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
+function StepTwo({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
const theme = useTheme();
const inputRef = useRef(null);
const { t } = useTranslation();
@@ -43,8 +45,12 @@ function StepTwo({ form, errors, onSubmit, onChange, onBack }) {
textAlign="center"
>
- {t("signUp")}
- {t("enterEmail")}
+
+ {isSuperAdmin
+ ? t("auth.registration.heading.superAdmin")
+ : t("auth.registration.heading.user")}
+
+ {t("auth.registration.subheadings.stepTwo")}
(e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
- helperText={
- errors.email &&
- (errors.email.includes("required")
- ? t("authRegisterEmailRequired")
- : errors.email.includes("valid email")
- ? t("authRegisterEmailInvalid")
- : t(errors.email))
- }
+ helperText={t(errors.email)} // Localization keys are in validation.js
ref={inputRef}
/>
- {t("commonBack")}
+ {t("auth.common.navigation.back")}
- {t("continue")}
+ {t("auth.common.navigation.continue")}
diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx
index 21dfe2d39..c2cd64b23 100644
--- a/client/src/Pages/Auth/SetNewPassword.jsx
+++ b/client/src/Pages/Auth/SetNewPassword.jsx
@@ -16,7 +16,7 @@ import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import "./index.css";
import { useValidatePassword } from "./hooks/useValidatePassword";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
const SetNewPassword = () => {
const navigate = useNavigate();
@@ -43,20 +43,20 @@ const SetNewPassword = () => {
createToast({
body:
error.details && error.details.length > 0
- ? error.details[0].message
- : "Error validating data.",
+ ? error.details[0].message // FIXME: Potential untranslated string
+ : t("auth.common.errors.validation"),
});
} else {
const action = await dispatch(setNewPassword({ token, form }));
if (action.payload.success) {
navigate("/new-password-confirmed");
createToast({
- body: "Your password was reset successfully.",
+ body: t("auth.forgotPassword.toasts.success"),
});
} else {
const errorMessage = action.payload
- ? action.payload.msg
- : "Unable to reset password. Please try again later or contact support.";
+ ? action.payload.msg // FIXME: Potential untranslated string
+ : t("auth.forgotPassword.toasts.error");
createToast({
body: errorMessage,
});
@@ -140,11 +140,11 @@ const SetNewPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
-
+
- {t("authSetNewPasswordTitle")}
- {t("authSetNewPasswordDescription")}
+ {t("auth.forgotPassword.heading")}
+ {t("auth.forgotPassword.subheadings.stepThree")}
{
id={passwordId}
type="password"
name="password"
- label={t("commonPassword")}
+ label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••"
value={form.password}
onChange={handleChange}
error={errors.password ? true : false}
- helperText={errors.password}
+ helperText={errors.password === "auth.common.inputs.password.errors.empty"
+ ? t(errors.password)
+ : ""} // Other errors are related to required password conditions and are visualized below the input
endAdornment={}
/>
@@ -185,13 +187,13 @@ const SetNewPassword = () => {
id={confirmPasswordId}
type="password"
name="confirm"
- label={t("authSetNewPasswordConfirmPassword")}
+ label={t("auth.common.inputs.passwordConfirm.label")}
isRequired={true}
- placeholder="••••••••"
+ placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
value={form.confirm}
onChange={handleChange}
error={errors.confirm ? true : false}
- helperText={errors.confirm}
+ helperText={t(errors.confirm)} // Localization keys are in validation.js
endAdornment={}
/>
@@ -200,33 +202,33 @@ const SetNewPassword = () => {
mb={theme.spacing(12)}
>
@@ -243,7 +245,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
- {t("authSetNewPasswordResetPassword")}
+ {t("auth.forgotPassword.buttons.resetPassword")}
@@ -251,15 +253,21 @@ const SetNewPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
- {t("goBackTo")} —
- navigate("/login")}
- sx={{ userSelect: "none" }}
- >
- {t("authLoginTitle")}
+
+ navigate("/login")}
+ sx={{ userSelect: "none" }}
+ />
+ ),
+ }}
+ />
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index acb05116e..1074f3bb2 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -9,10 +9,9 @@ const nameSchema = joi
.trim()
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
.messages({
- "string.empty": "Name is required",
- "string.max": "Name must be less than 50 characters",
- "string.pattern.base":
- "Name must contain only letters, spaces, apostrophes, or hyphens",
+ "string.empty": "auth.common.inputs.firstName.errors.empty",
+ "string.max": "auth.common.inputs.firstName.errors.length",
+ "string.pattern.base": "auth.common.inputs.firstName.errors.pattern",
});
const lastnameSchema = joi
@@ -21,10 +20,9 @@ const lastnameSchema = joi
.trim()
.pattern(/^[\p{L}\p{M}''\- ]+$/u)
.messages({
- "string.empty": "Surname is required",
- "string.max": "Surname must be less than 50 characters",
- "string.pattern.base":
- "Surname must contain only letters, spaces, apostrophes, or hyphens",
+ "string.empty": "auth.common.inputs.lastName.errors.empty",
+ "string.max": "auth.common.inputs.lastName.errors.length",
+ "string.pattern.base": "auth.common.inputs.lastName.errors.pattern",
});
const newPasswordSchema = joi
@@ -56,12 +54,12 @@ const newPasswordSchema = joi
return value;
})
.messages({
- "string.empty": "Password is required",
- "string.min": "Password must be at least 8 characters long",
- uppercase: "Password must contain at least one uppercase letter",
- lowercase: "Password must contain at least one lowercase letter",
- number: "Password must contain at least one number",
- special: "Password must contain at least one special character",
+ "string.empty": "auth.common.inputs.password.errors.empty",
+ "string.min": "auth.common.inputs.password.errors.length",
+ uppercase: "auth.common.inputs.password.errors.uppercase",
+ lowercase: "auth.common.inputs.password.errors.lowercase",
+ number: "auth.common.inputs.password.errors.number",
+ special: "auth.common.inputs.password.errors.special",
});
const newOrChangedCredentials = joi.object({
@@ -73,8 +71,8 @@ const newOrChangedCredentials = joi.object({
.email({ tlds: { allow: false } })
.lowercase()
.messages({
- "string.empty": "authRegisterEmailRequired",
- "string.email": "authRegisterEmailInvalid",
+ "string.empty": "auth.common.inputs.email.errors.empty",
+ "string.email": "auth.common.inputs.email.errors.invalid",
}),
password: newPasswordSchema,
newPassword: newPasswordSchema,
@@ -89,8 +87,8 @@ const newOrChangedCredentials = joi.object({
return value;
})
.messages({
- "string.empty": "This field can't be empty",
- different: "Passwords do not match",
+ "string.empty": "auth.common.inputs.passwordConfirm.errors.empty",
+ different: "auth.common.inputs.passwordConfirm.errors.different",
}),
role: joi.array(),
teamId: joi.string().allow("").optional(),
@@ -104,13 +102,13 @@ const loginCredentials = joi.object({
.email({ tlds: { allow: false } })
.lowercase()
.messages({
- "string.empty": "authRegisterEmailRequired",
- "string.email": "authRegisterEmailInvalid",
+ "string.empty": "auth.common.inputs.email.errors.empty",
+ "string.email": "auth.common.inputs.email.errors.invalid",
}),
password: joi
.string()
.messages({
- "string.empty": "Please enter your password",
+ "string.empty": "auth.common.inputs.password.errors.empty",
}),
});
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 5f2fde219..3bb64e6c9 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Chyba připojení k síti",
- "checkConnection": "Zkontrolujte prosím své připojení k síti"
+ "checkConnection": "Zkontrolujte prosím své připojení k síti",
+ "unknownError": "Nastala neznámá chyba"
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Pokračovat",
+ "back": "Zpět"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-mail",
+ "placeholder": "jan.novak@domena.cz",
+ "errors": {
+ "empty": "Zadejte prosím e-mailovou adresu",
+ "invalid": "Překontrolujte si prosím správnost zadané e-mailové adresy"
+ }
+ },
+ "password": {
+ "label": "Heslo",
+ "rules": {
+ "length": {
+ "beginning": "Heslo musí být dlouhé alespoň",
+ "highlighted": "8 znaků"
+ },
+ "special": {
+ "beginning": "Heslo musí obsahovat alespoň",
+ "highlighted": "1 speciální znak"
+ },
+ "number": {
+ "beginning": "Heslo musí obsahovat alespoň",
+ "highlighted": "1 číslo"
+ },
+ "uppercase": {
+ "beginning": "Heslo musí obsahovat alespoň",
+ "highlighted": "1 velké písmeno"
+ },
+ "lowercase": {
+ "beginning": "Heslo musí obsahovat alespoň",
+ "highlighted": "1 malé písmeno"
+ },
+ "match": {
+ "beginning": "Obě hesla se",
+ "highlighted": "musí shodovat"
+ }
+ },
+ "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"
+ }
+ },
+ "passwordConfirm": {
+ "label": "Potvrzení hesla",
+ "placeholder": "Pro ověření správnosti zadejte heslo ještě jednou",
+ "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é"
+ }
+ },
+ "firstName": {
+ "label": "Jméno",
+ "placeholder": "Jan",
+ "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"
+ }
+ },
+ "lastName": {
+ "label": "Příjmení",
+ "placeholder": "Novák",
+ "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"
+ }
+ }
+ },
+ "errors": {
+ "validation": "Nastala chyba při ověřování dat."
+ }
+ },
+ "login": {
+ "heading": "Přihlášení",
+ "subheadings": {
+ "stepOne": "Zadejte svůj e-mail",
+ "stepTwo": "Zadejte své heslo"
+ },
+ "links": {
+ "forgotPassword": "Zapomněli jste heslo? Obnovte si ho",
+ "register": "Ještě nemáte účet? Zaregistrujte se"
+ },
+ "toasts": {
+ "success": "Vítejte zpět",
+ "incorrectPassword": "Nesprávné heslo"
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "Vytvoření superadministrátora",
+ "user": "Registrace"
+ },
+ "subheadings": {
+ "stepOne": "Zadejte své osobní údaje",
+ "stepTwo": "Zadejte svůj e-mail",
+ "stepThree": "Nastavte si heslo"
+ },
+ "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"
+ },
+ "gettingStartedButton": {
+ "superAdmin": "Vytvořit účet superadministrátora",
+ "user": "Zaregistrovat se jako běžný uživatel"
+ },
+ "termsAndPolicies": "Vytvořením účtu souhlasíte s našimi Podmínkami použití (anglicky) a Podmínkami zpracování osobních údajů (anglicky).",
+ "links": {
+ "login": "Máte již vytvořený účet? Přihlašte se"
+ },
+ "toasts": {
+ "success": "Vítejte! Váš účet byl úspěšně vytvořen."
+ }
+ },
+ "forgotPassword": {
+ "heading": "Zapomenuté heslo",
+ "subheadings": {
+ "stepOne": "Nebojte se, zašleme vám pokyny pro obnovení hesla.",
+ "stepTwo": "Odkaz pro obnovení hesla byl odeslán na ",
+ "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í."
+ },
+ "buttons": {
+ "openEmail": "Otevřít e-mailovou aplikaci",
+ "resetPassword": "Obnovit heslo"
+ },
+ "imageAlts": {
+ "passwordKey": "Ikonka přístupového klíče",
+ "email": "Ikonka e-mailu",
+ "lock": "Ikonka zámku",
+ "passwordConfirm": "Ikonka potvrzeného hesla"
+ },
+ "links": {
+ "login": "Vrátit se k Přihlášení",
+ "resend": "Nedorazil vám e-mail? Klepněte pro opětovné zaslání"
+ },
+ "toasts": {
+ "sent": "Instrukce vám dorazí na .",
+ "emailNotFound": "Nepodařilo se nalézt e-mailovou adresu.",
+ "redirect": "Přesměrování proběhne za ...",
+ "success": "Heslo bylo úspěšně obnoveno.",
+ "error": "Obnova hesla se nepodařila. Zkuste to prosím znovu nebo kontaktujte podporu."
+ }
}
},
"errorPages": {
@@ -25,24 +182,12 @@
}
},
"dontHaveAccount": "",
- "email": "",
"forgotPassword": "",
- "password": "",
- "signUp": "",
"submit": "",
"title": "",
- "continue": "",
"enterEmail": "",
- "authLoginTitle": "",
- "authLoginEnterPassword": "",
"commonPassword": "",
- "commonBack": "",
- "authForgotPasswordTitle": "",
- "authForgotPasswordResetPassword": "",
"createPassword": "",
- "createAPassword": "",
- "authRegisterAlreadyHaveAccount": "",
- "authLoginEnterEmail": "",
"authRegisterTitle": "",
"authRegisterStepOneTitle": "",
"authRegisterStepOneDescription": "",
@@ -50,36 +195,15 @@
"authRegisterStepTwoDescription": "",
"authRegisterStepThreeTitle": "",
"authRegisterStepThreeDescription": "",
- "authForgotPasswordDescription": "",
"authForgotPasswordSendInstructions": "",
"authForgotPasswordBackTo": "",
"authCheckEmailTitle": "",
- "authCheckEmailDescription": "",
"authCheckEmailResendEmail": "",
"authCheckEmailBackTo": "",
- "goBackTo": "",
- "authCheckEmailDidntReceiveEmail": "",
- "authCheckEmailClickToResend": "",
"authSetNewPasswordTitle": "",
- "authSetNewPasswordDescription": "",
"authSetNewPasswordNewPassword": "",
- "authSetNewPasswordConfirmPassword": "",
- "confirmPassword": "",
- "authSetNewPasswordResetPassword": "",
"authSetNewPasswordBackTo": "",
- "authPasswordMustBeAtLeast": "",
- "authPasswordCharactersLong": "",
- "authPasswordMustContainAtLeast": "",
- "authPasswordSpecialCharacter": "",
- "authPasswordOneNumber": "",
- "authPasswordUpperCharacter": "",
- "authPasswordLowerCharacter": "",
- "authPasswordConfirmAndPassword": "",
- "authPasswordMustMatch": "",
"authRegisterCreateAccount": "",
- "authRegisterCreateSuperAdminAccount": "",
- "authRegisterSignUpWithEmail": "",
- "authRegisterBySigningUp": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -140,9 +264,6 @@
"city": "",
"response": "",
"passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -416,10 +537,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
"authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +549,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
"validationNameRequired": "",
"validationNameTooLong": "",
"validationNameInvalidCharacters": "",
@@ -440,10 +556,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
"maskedPageSpeedKeyPlaceholder": "",
"pageSpeedApiKeyFieldTitle": "",
"pageSpeedApiKeyFieldLabel": "",
@@ -528,7 +641,6 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
"settingsThemeModeLight": "",
"settingsThemeModeDark": "",
"settingsClearAllStatsDialogCancel": "",
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index fb1a58019..753af8b3c 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "",
- "checkConnection": ""
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Weiter",
+ "back": "Zurück"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-Mail",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "Passwort",
+ "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": ""
+ }
+ },
+ "login": {
+ "heading": "Anmelden",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "Geben Sie Ihr Passwort ein"
+ },
+ "links": {
+ "forgotPassword": "Passwort vergessen? Passwort zurücksetzen",
+ "register": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Registrieren"
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "Erstelle dein Passwort"
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "Passwort zurücksetzen",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Noch kein Konto",
- "email": "E-Mail",
"forgotPassword": "Passwort vergessen",
- "password": "passwort",
- "signUp": "Registrieren",
"submit": "Absenden",
"title": "Titel",
- "continue": "Weiter",
"enterEmail": "Geben Sie Ihre E-Mail-Adresse ein",
- "authLoginTitle": "Anmelden",
- "authLoginEnterPassword": "Geben Sie Ihr Passwort ein",
"commonPassword": "Passwort",
- "commonBack": "Zurück",
- "authForgotPasswordTitle": "Passwort vergessen?",
- "authForgotPasswordResetPassword": "Passwort zurücksetzen",
- "createPassword": "Erstelle dein Passwort",
- "createAPassword": "Passwort",
- "authRegisterAlreadyHaveAccount": "Hast du schon eine Account?",
- "authLoginEnterEmail": "",
"authRegisterTitle": "",
"authRegisterStepOneTitle": "",
"authRegisterStepOneDescription": "",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "",
"authRegisterStepThreeTitle": "",
"authRegisterStepThreeDescription": "",
- "authForgotPasswordDescription": "",
"authForgotPasswordSendInstructions": "",
"authForgotPasswordBackTo": "",
"authCheckEmailTitle": "",
- "authCheckEmailDescription": "",
"authCheckEmailResendEmail": "",
"authCheckEmailBackTo": "",
- "goBackTo": "",
- "authCheckEmailDidntReceiveEmail": "",
- "authCheckEmailClickToResend": "",
"authSetNewPasswordTitle": "",
- "authSetNewPasswordDescription": "",
"authSetNewPasswordNewPassword": "",
- "authSetNewPasswordConfirmPassword": "",
- "confirmPassword": "",
- "authSetNewPasswordResetPassword": "",
"authSetNewPasswordBackTo": "",
- "authPasswordMustBeAtLeast": "",
- "authPasswordCharactersLong": "",
- "authPasswordMustContainAtLeast": "",
- "authPasswordSpecialCharacter": "",
- "authPasswordOneNumber": "",
- "authPasswordUpperCharacter": "",
- "authPasswordLowerCharacter": "",
- "authPasswordConfirmAndPassword": "",
- "authPasswordMustMatch": "",
"authRegisterCreateAccount": "",
- "authRegisterCreateSuperAdminAccount": "",
- "authRegisterSignUpWithEmail": "",
- "authRegisterBySigningUp": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -140,9 +263,6 @@
"city": "",
"response": "",
"passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
"authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +548,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
"validationNameRequired": "",
"validationNameTooLong": "",
"validationNameInvalidCharacters": "",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
"maskedPageSpeedKeyPlaceholder": "",
"pageSpeedApiKeyFieldTitle": "",
"pageSpeedApiKeyFieldLabel": "",
@@ -528,7 +640,6 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
"settingsThemeModeLight": "",
"settingsThemeModeDark": "",
"settingsClearAllStatsDialogCancel": "",
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 145aa0af3..af06f419b 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Network error",
- "checkConnection": "Please check your connection"
+ "checkConnection": "Please check your connection",
+ "unknownError": "Unknown error"
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Continue",
+ "back": "Back"
+ },
+ "inputs": {
+ "email": {
+ "label": "Email",
+ "placeholder": "jordan.ellis@domain.com",
+ "errors": {
+ "empty": "To continue, please enter your email address",
+ "invalid": "Please recheck validity of entered email address"
+ }
+ },
+ "password": {
+ "label": "Password",
+ "rules": {
+ "length": {
+ "beginning": "Must be at least",
+ "highlighted": "8 characters long"
+ },
+ "special": {
+ "beginning": "Must contain at least",
+ "highlighted": "one special character"
+ },
+ "number": {
+ "beginning": "Must contain at least",
+ "highlighted": "one number"
+ },
+ "uppercase": {
+ "beginning": "Must contain at least",
+ "highlighted": "one upper character"
+ },
+ "lowercase": {
+ "beginning": "Must contain at least",
+ "highlighted": "one lower character"
+ },
+ "match": {
+ "beginning": "Confirm password and password",
+ "highlighted": "must match"
+ }
+ },
+ "errors": {
+ "empty": "Please enter your password",
+ "length": "Password must be at least 8 characters long",
+ "uppercase": "Password must contain at least 1 uppercase letter",
+ "lowercase": "Password must contain at least 1 lowercase letter",
+ "number": "Password must contain at least 1 number",
+ "special": "Password must contain at least 1 special character",
+ "incorrect": "The password you provided does not match our records"
+ }
+ },
+ "passwordConfirm": {
+ "label": "Confirm password",
+ "placeholder": "Re-enter password to confirm",
+ "errors": {
+ "empty": "Please enter your password again for confirmation (helps with typos)",
+ "different": "Entered passwords don't match, so one of them is probably mistyped"
+ }
+ },
+ "firstName": {
+ "label": "Name",
+ "placeholder": "Jordan",
+ "errors": {
+ "empty": "Please enter your name",
+ "length": "Name must be less than 50 characters",
+ "pattern": "Name must contain only letters, spaces, apostrophes, or hyphens"
+ }
+ },
+ "lastName": {
+ "label": "Surname",
+ "placeholder": "Ellis",
+ "errors": {
+ "empty": "Please enter your surname",
+ "length": "Surname must be less than 50 characters",
+ "pattern": "Surname must contain only letters, spaces, apostrophes, or hyphens"
+ }
+ }
+ },
+ "errors": {
+ "validation": "Error validating data."
+ }
+ },
+ "login": {
+ "heading": "Log In",
+ "subheadings": {
+ "stepOne": "Enter your email",
+ "stepTwo": "Enter your password"
+ },
+ "links": {
+ "forgotPassword": "Forgot password? Reset password",
+ "register": "Do not have an account? Register here"
+ },
+ "toasts": {
+ "success": "Welcome back! You're successfully logged in.",
+ "incorrectPassword": "Incorrect password"
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "Create super admin",
+ "user": "Sign Up"
+ },
+ "subheadings": {
+ "stepOne": "Enter your personal details",
+ "stepTwo": "Enter your email",
+ "stepThree": "Create your password"
+ },
+ "description": {
+ "superAdmin": "Create your super admin account to get started",
+ "user": "Sign up as a user and ask super admin for access to your monitors"
+ },
+ "gettingStartedButton": {
+ "superAdmin": "Create super admin account",
+ "user": "Sign up regular user"
+ },
+ "termsAndPolicies": "By creating an account, you agree to our Terms of Service and Privacy Policy.",
+ "links": {
+ "login": "Already have an account? Log In"
+ },
+ "toasts": {
+ "success": "Welcome! Your account was created successfully."
+ }
+ },
+ "forgotPassword": {
+ "heading": "Forgot password?",
+ "subheadings": {
+ "stepOne": "No worries, we'll send you reset instructions.",
+ "stepTwo": "We sent a password reset link to ",
+ "stepThree": "Your new password must be different from previously used passwords.",
+ "stepFour": "Your password has been successfully reset. Click below to log in magically."
+ },
+ "buttons": {
+ "openEmail": "Open email app",
+ "resetPassword": "Reset password"
+ },
+ "imageAlts": {
+ "passwordKey": "Password key icon",
+ "email": "Email icon",
+ "lock": "Lock icon",
+ "passwordConfirm": "Password confirm icon"
+ },
+ "links": {
+ "login": "Go back to Log In",
+ "resend": "Didn't receive the email? Click to resend"
+ },
+ "toasts": {
+ "sent": "Instructions sent to .",
+ "emailNotFound": "Email not found.",
+ "redirect": "Redirecting in ...",
+ "success": "Your password was reset successfully.",
+ "error": "Unable to reset password. Please try again later or contact support."
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Don't have account",
- "email": "E-mail",
"forgotPassword": "Forgot Password",
- "password": "Password",
- "signUp": "Sign Up",
"submit": "Submit",
"title": "Title",
- "continue": "Continue",
"enterEmail": "Enter your email",
- "authLoginTitle": "Log In",
- "authLoginEnterPassword": "Enter your password",
"commonPassword": "Password",
- "commonBack": "Back",
- "authForgotPasswordTitle": "Forgot password?",
- "authForgotPasswordResetPassword": "Reset password",
- "createPassword": "Create your password",
- "createAPassword": "Password",
- "authRegisterAlreadyHaveAccount": "Already have an account?",
- "authLoginEnterEmail": "Enter your email",
"authRegisterTitle": "Create an account",
"authRegisterStepOneTitle": "Create your account",
"authRegisterStepOneDescription": "Enter your details to get started",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Tell us more about yourself",
"authRegisterStepThreeTitle": "Almost done!",
"authRegisterStepThreeDescription": "Review your information",
- "authForgotPasswordDescription": "No worries, we'll send you reset instructions.",
"authForgotPasswordSendInstructions": "Send instructions",
"authForgotPasswordBackTo": "Back to",
"authCheckEmailTitle": "Check your email",
- "authCheckEmailDescription": "We sent a password reset link to",
"authCheckEmailResendEmail": "Resend email",
"authCheckEmailBackTo": "Back to",
- "goBackTo": "Go back to",
- "authCheckEmailDidntReceiveEmail": "Didn't receive the email?",
- "authCheckEmailClickToResend": "Click to resend",
"authSetNewPasswordTitle": "Set new password",
- "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.",
"authSetNewPasswordNewPassword": "New password",
- "authSetNewPasswordConfirmPassword": "Confirm password",
- "confirmPassword": "Re-enter password to confirm",
- "authSetNewPasswordResetPassword": "Reset password",
"authSetNewPasswordBackTo": "Back to",
- "authPasswordMustBeAtLeast": "Must be at least",
- "authPasswordCharactersLong": "8 characters long",
- "authPasswordMustContainAtLeast": "Must contain at least",
- "authPasswordSpecialCharacter": "one special character",
- "authPasswordOneNumber": "one number",
- "authPasswordUpperCharacter": "one upper character",
- "authPasswordLowerCharacter": "one lower character",
- "authPasswordConfirmAndPassword": "Confirm password and password",
- "authPasswordMustMatch": "Passwords must match",
"authRegisterCreateAccount": "Create your account to get started",
- "authRegisterCreateSuperAdminAccount": "Create your super admin account to get started",
- "authRegisterSignUpWithEmail": "Create super admin account",
- "authRegisterBySigningUp": "By creating an account, you agree to our Terms of Service and Privacy Policy.",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
@@ -136,9 +259,6 @@
"city": "CITY",
"response": "RESPONSE",
"passwordreset": "Password Reset",
- "authRegisterStepOnePersonalDetails": "Enter your personal details",
- "authCheckEmailOpenEmailButton": "Open email app",
- "authNewPasswordConfirmed": "Your password has been successfully reset. Click below to log in magically.",
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
"monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding",
"webhookSendSuccess": "Webhook notification sent successfully",
@@ -408,10 +528,7 @@
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
- "authRegisterFirstName": "Name",
- "authRegisterLastName": "Surname",
"authRegisterEmail": "Email",
- "authRegisterEmailRequired": "To continue, please enter your email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
@@ -423,8 +540,6 @@
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
- "welcomeBack": "Welcome back! You're successfully logged in.",
- "authRegisterLoginLink": "Log In",
"validationNameRequired": "Please enter your name",
"validationNameTooLong": "Name should be less than 50 characters",
"validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
@@ -432,10 +547,7 @@
"settingsSystemResetDescription": "Remove all monitors from your system.",
"DeleteAccountTitle": "Remove account",
"DeleteAccountButton": "Remove account",
- "authRegisterEmailInvalid": "Please enter a valid email address",
"publicLink": "Public link",
- "doNotHaveAccount": "Do not have an account?",
- "registerHere": "Register here",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
@@ -519,7 +631,6 @@
"state": "State",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
- "authForgotPasswordInstructions": "No worries, we'll send you reset instructions.",
"settingsThemeModeLight": "Light",
"settingsThemeModeDark": "Dark",
"settingsClearAllStatsDialogCancel": "Cancel",
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index 722833d99..5c0f551ad 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "",
- "checkConnection": ""
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Continuar",
+ "back": "Atras"
+ },
+ "inputs": {
+ "email": {
+ "label": "Correo electronico",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "Contraseña",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": "8 caracteres de longitud"
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": "un numero"
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "Confirmar contraseña",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ }
+ },
+ "login": {
+ "heading": "Inicio de session",
+ "subheadings": {
+ "stepOne": "Introduce tu correo electronico",
+ "stepTwo": "Introduce tu contraseña"
+ },
+ "links": {
+ "forgotPassword": "¿Has olvidado tu contraseña? Restablecer contraseña",
+ "register": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Registrarse"
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "Introduce tu correo electronico",
+ "stepThree": "Crear contraseña"
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "¿Has olvidado tu contraseña?",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "Hemos enviado un link para reiniciar la contraseña a ",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": "Restablecer contraseña"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "Volver a Inicio de session",
+ "resend": "No has recibido el correo electronico? Presiona para reenviar"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "No tengo una cuenta",
- "email": "Correo electronico",
"forgotPassword": "He olvidado mi contraseña",
- "password": "Contraseña",
- "signUp": "Registrarse",
"submit": "Enviar",
"title": "Titulo",
- "continue": "Continuar",
"enterEmail": "Introduce tu email",
- "authLoginTitle": "Inicio de session",
- "authLoginEnterPassword": "Introduce tu contraseña",
"commonPassword": "Contraseña",
- "commonBack": "Atras",
- "authForgotPasswordTitle": "¿Has olvidado tu contraseña?",
- "authForgotPasswordResetPassword": "Restablecer contraseña",
- "createPassword": "Crear contraseña",
- "createAPassword": "Contraseña",
- "authRegisterAlreadyHaveAccount": "Ya tienes un cuenta?",
- "authLoginEnterEmail": "Introduce tu correo electronico",
"authRegisterTitle": "Crear una cuenta",
"authRegisterStepOneTitle": "Crea tu cuenta",
"authRegisterStepOneDescription": "",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Cuentano mas sobre ti",
"authRegisterStepThreeTitle": "Ya casi estamos!",
"authRegisterStepThreeDescription": "Revisa tu informacion",
- "authForgotPasswordDescription": "",
"authForgotPasswordSendInstructions": "Enviar instrucciones",
"authForgotPasswordBackTo": "Volver a",
"authCheckEmailTitle": "Comprueba tu correo",
- "authCheckEmailDescription": "Hemos enviado un link para reiniciar la contraseña a",
"authCheckEmailResendEmail": "Reenviar correo",
"authCheckEmailBackTo": "Volver a",
- "goBackTo": "Volver a",
- "authCheckEmailDidntReceiveEmail": "No has recibido el correo electronico?",
- "authCheckEmailClickToResend": "Presiona para reenviar",
"authSetNewPasswordTitle": "Configurar nueva contraseña",
- "authSetNewPasswordDescription": "",
"authSetNewPasswordNewPassword": "Nueva contraseña",
- "authSetNewPasswordConfirmPassword": "Confirmar contraseña",
- "confirmPassword": "",
- "authSetNewPasswordResetPassword": "Restablecer contraseña",
"authSetNewPasswordBackTo": "Volver a",
- "authPasswordMustBeAtLeast": "",
- "authPasswordCharactersLong": "8 caracteres de longitud",
- "authPasswordMustContainAtLeast": "",
- "authPasswordSpecialCharacter": "",
- "authPasswordOneNumber": "un numero",
- "authPasswordUpperCharacter": "",
- "authPasswordLowerCharacter": "",
- "authPasswordConfirmAndPassword": "",
- "authPasswordMustMatch": "",
"authRegisterCreateAccount": "",
- "authRegisterCreateSuperAdminAccount": "",
- "authRegisterSignUpWithEmail": "",
- "authRegisterBySigningUp": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -140,9 +263,6 @@
"city": "",
"response": "",
"passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
"authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +548,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
"validationNameRequired": "",
"validationNameTooLong": "",
"validationNameInvalidCharacters": "",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
"maskedPageSpeedKeyPlaceholder": "",
"pageSpeedApiKeyFieldTitle": "",
"pageSpeedApiKeyFieldLabel": "",
@@ -528,7 +640,6 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
"settingsThemeModeLight": "",
"settingsThemeModeDark": "",
"settingsClearAllStatsDialogCancel": "",
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index 16517778a..b4c364fe5 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Verkkovirhe",
- "checkConnection": "Tarkista yhteytesi"
+ "checkConnection": "Tarkista yhteytesi",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Jatka",
+ "back": "Takaisin"
+ },
+ "inputs": {
+ "email": {
+ "label": "Sähköposti",
+ "placeholder": "",
+ "errors": {
+ "empty": "Syötä sähköpostiosoitteesi jatkaaksesi",
+ "invalid": "Kirjoita kelvollinen sähköpostiosoite"
+ }
+ },
+ "password": {
+ "label": "Salasana",
+ "rules": {
+ "length": {
+ "beginning": "Vähintään",
+ "highlighted": "8 merkkiä pitkä"
+ },
+ "special": {
+ "beginning": "Tulee sisältää vähintään",
+ "highlighted": "yksi erikoismerkki"
+ },
+ "number": {
+ "beginning": "Tulee sisältää vähintään",
+ "highlighted": "yksi numero"
+ },
+ "uppercase": {
+ "beginning": "Tulee sisältää vähintään",
+ "highlighted": "yksi pieni kirjain"
+ },
+ "lowercase": {
+ "beginning": "Tulee sisältää vähintään",
+ "highlighted": "yksi iso kirjain"
+ },
+ "match": {
+ "beginning": "Vahvista salasana ja salasana",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "Vahvista salasana",
+ "placeholder": "Syötä salasana uudelleen",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "Nimi",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "Sukunimi",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ }
+ },
+ "login": {
+ "heading": "Kirjaudu",
+ "subheadings": {
+ "stepOne": "Syötä salasanasi",
+ "stepTwo": "Syötä salasanasi"
+ },
+ "links": {
+ "forgotPassword": "Unohditko salasanasi? Nollaa salasana",
+ "register": "Onko sinulla tili? Rekisteröidy tästä"
+ },
+ "toasts": {
+ "success": "Tervetuloa takaisin! Olet kirjautunut sisään.",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Rekisteröidy"
+ },
+ "subheadings": {
+ "stepOne": "Syötä henkilökohtaiset tietosi",
+ "stepTwo": "Syötä salasanasi",
+ "stepThree": "Luo salasanasi"
+ },
+ "description": {
+ "superAdmin": "Luo pääkäyttäjän tili aloittaaksesi",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "Luo pääkäyttäjän tili",
+ "user": ""
+ },
+ "termsAndPolicies": "Luomalla tilin hyväksyt Käyttöehdot ja Tietosuojakäytännön.",
+ "links": {
+ "login": "Onko sinulla jo tili? Kirjaudu"
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "Unohditko salasanasi?",
+ "subheadings": {
+ "stepOne": "Ei hätää, lähetämme sinulle ohjeet salasanan palauttamiseen.",
+ "stepTwo": "Lähetimme salasanan nollauslinkin osoitteeseen ",
+ "stepThree": "Uuden salasanan pitää olla eri kuin aiemmin käyttämäsi.",
+ "stepFour": "Salasanasi on onnistuneesti nollattu. Klikkaa alta kirjautuaksesi taianomaisesti."
+ },
+ "buttons": {
+ "openEmail": "Avaa sähköpostisovellus",
+ "resetPassword": "Nollaa salasana"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "Palaa edelliseen Kirjaudu",
+ "resend": "Etkö saanut sähköpostia? Klikkaa lähettääksesi uudelleen"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Ei tiliä",
- "email": "Sähköposti",
"forgotPassword": "Unohtunut salasana",
- "password": "Salasana",
- "signUp": "Rekisteröidy",
"submit": "Lähetä",
"title": "Otsikko",
- "continue": "Jatka",
"enterEmail": "Syötä sähköpostiosoitteesi",
- "authLoginTitle": "Kirjaudu",
- "authLoginEnterPassword": "Syötä salasanasi",
"commonPassword": "Salasana",
- "commonBack": "Takaisin",
- "authForgotPasswordTitle": "Unohditko salasanasi?",
- "authForgotPasswordResetPassword": "Nollaa salasana",
- "createPassword": "Luo salasanasi",
- "createAPassword": "Salasana",
- "authRegisterAlreadyHaveAccount": "Onko sinulla jo tili?",
- "authLoginEnterEmail": "Syötä salasanasi",
"authRegisterTitle": "Luo tili",
"authRegisterStepOneTitle": "Luo tilisi",
"authRegisterStepOneDescription": "Kirjoita tietosi aloittaaksesi",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Kerro meille lisää itsestäsi",
"authRegisterStepThreeTitle": "Melkein valmis!",
"authRegisterStepThreeDescription": "Tarkista tietosi",
- "authForgotPasswordDescription": "Ei hätää, lähetämme sinulle salasanan nollausohjeet.",
"authForgotPasswordSendInstructions": "Lähetä ohjeet",
"authForgotPasswordBackTo": "Palaa",
"authCheckEmailTitle": "Tarkista sähköpostisi",
- "authCheckEmailDescription": "Lähetimme salasanan nollauslinkin osoitteeseen",
"authCheckEmailResendEmail": "Lähetä sähköposti uudelleen",
"authCheckEmailBackTo": "Palaa",
- "goBackTo": "Palaa edelliseen",
- "authCheckEmailDidntReceiveEmail": "Etkö saanut sähköpostia?",
- "authCheckEmailClickToResend": "Klikkaa lähettääksesi uudelleen",
"authSetNewPasswordTitle": "Aseta uusi salasana",
- "authSetNewPasswordDescription": "Uuden salasanan pitää olla eri kuin aiemmin käyttämäsi.",
"authSetNewPasswordNewPassword": "Uusi salasana",
- "authSetNewPasswordConfirmPassword": "Vahvista salasana",
- "confirmPassword": "Syötä salasana uudelleen",
- "authSetNewPasswordResetPassword": "Nollaa salasana",
"authSetNewPasswordBackTo": "Takaisin",
- "authPasswordMustBeAtLeast": "Vähintään",
- "authPasswordCharactersLong": "8 merkkiä pitkä",
- "authPasswordMustContainAtLeast": "Tulee sisältää vähintään",
- "authPasswordSpecialCharacter": "yksi erikoismerkki",
- "authPasswordOneNumber": "yksi numero",
- "authPasswordUpperCharacter": "yksi pieni kirjain",
- "authPasswordLowerCharacter": "yksi iso kirjain",
- "authPasswordConfirmAndPassword": "Vahvista salasana ja salasana",
- "authPasswordMustMatch": "Salasanojen on oltava samat",
"authRegisterCreateAccount": "Luo tili aloittaaksesi",
- "authRegisterCreateSuperAdminAccount": "Luo pääkäyttäjän tili aloittaaksesi",
- "authRegisterSignUpWithEmail": "Luo pääkäyttäjän tili",
- "authRegisterBySigningUp": "Luomalla tilin hyväksyt Käyttöehdot ja Tietosuojakäytännön.",
"distributedStatusHeaderText": "Reaaliaikainen kattavuus oikeilla laitteilla",
"distributedStatusSubHeaderText": "Miljoonien laitteiden voimin ympäri maailman näet järjestelmän suorituskyvyn maailman alueittain, maittain tai kaupungeittain",
"settingsGeneralSettings": "Yleiset asetukset",
@@ -140,9 +263,6 @@
"city": "KAUPUNKI",
"response": "Vastaus",
"passwordreset": "Salasanan nollaus",
- "authRegisterStepOnePersonalDetails": "Syötä henkilökohtaiset tietosi",
- "authCheckEmailOpenEmailButton": "Avaa sähköpostisovellus",
- "authNewPasswordConfirmed": "Salasanasi on onnistuneesti nollattu. Klikkaa alta kirjautuaksesi taianomaisesti.",
"monitorStatusUp": "Valvontakohde {name} ({url}) on nyt toiminnassa ja vastaa",
"monitorStatusDown": "Valvontakohde {name} ({url}) ei ole toiminnassa, eikä vastaa",
"webhookSendSuccess": "Webhook-ilmoitus lähetettiin onnistuneesti",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?",
- "authRegisterFirstName": "Nimi",
- "authRegisterLastName": "Sukunimi",
"authRegisterEmail": "Sähköpostiosoite",
- "authRegisterEmailRequired": "Syötä sähköpostiosoitteesi jatkaaksesi",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +548,6 @@
"noFileSelected": "Ei valittua tiedostoa",
"fallbackPage": ""
},
- "welcomeBack": "Tervetuloa takaisin! Olet kirjautunut sisään.",
- "authRegisterLoginLink": "Kirjaudu",
"validationNameRequired": "Kirjoita nimesi",
"validationNameTooLong": "Nimen tulee olla alle 50 merkkiä",
"validationNameInvalidCharacters": "Käytäthän vain kirjaimia, välilyöntejä, heittomerkkejä tai yhdysmerkkejä",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "Poista tili",
"DeleteAccountButton": "Poista tili",
- "authRegisterEmailInvalid": "Kirjoita kelvollinen sähköpostiosoite",
"publicLink": "Julkinen linkki",
- "doNotHaveAccount": "Onko sinulla tili?",
- "registerHere": "Rekisteröidy tästä",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API-avain",
"pageSpeedApiKeyFieldLabel": "PageSpeed API-avain",
@@ -528,7 +640,6 @@
"state": "Tila",
"statusBreadCrumbsStatusPages": "Tilasivut",
"statusBreadCrumbsDetails": "Tiedot",
- "authForgotPasswordInstructions": "Ei hätää, lähetämme sinulle ohjeet salasanan palauttamiseen.",
"settingsThemeModeLight": "Vaalea",
"settingsThemeModeDark": "Tumma",
"settingsClearAllStatsDialogCancel": "Peruuta",
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index 4acde5888..092cb3c9d 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "",
- "checkConnection": "Vérifiez votre connexion"
+ "checkConnection": "Vérifiez votre connexion",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Continuer",
+ "back": "Retour"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-mail",
+ "placeholder": "",
+ "errors": {
+ "empty": "Pour continuer, veuillez saisir votre adresse e-mail",
+ "invalid": "Veuillez saisir une adresse e-mail valide"
+ }
+ },
+ "password": {
+ "label": "Mot de passe",
+ "rules": {
+ "length": {
+ "beginning": "Doit faire au moins",
+ "highlighted": "8 caractères"
+ },
+ "special": {
+ "beginning": "Doit contenir au moins",
+ "highlighted": "un caractère spécial"
+ },
+ "number": {
+ "beginning": "Doit contenir au moins",
+ "highlighted": "un nombre"
+ },
+ "uppercase": {
+ "beginning": "Doit contenir au moins",
+ "highlighted": "une lettre majuscule"
+ },
+ "lowercase": {
+ "beginning": "Doit contenir au moins",
+ "highlighted": "une lettre minuscule"
+ },
+ "match": {
+ "beginning": "Confirmer le mot de passe",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "Confirmer le mot de passe",
+ "placeholder": "Confirmez votre mot de passe",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "Prénom",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "Nom",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ }
+ },
+ "login": {
+ "heading": "Se connecter",
+ "subheadings": {
+ "stepOne": "Entrez votre e-mail",
+ "stepTwo": "Entrez votre mot de passe"
+ },
+ "links": {
+ "forgotPassword": "Mot de passe oublié ? Réinitialiser le mot de passe",
+ "register": ""
+ },
+ "toasts": {
+ "success": "Bienvenue ! Vous êtes connecté avec succès.",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "S'inscrire"
+ },
+ "subheadings": {
+ "stepOne": "Saisissez vos infos",
+ "stepTwo": "Entrez votre e-mail",
+ "stepThree": "Créez votre mot de passe"
+ },
+ "description": {
+ "superAdmin": "Créez votre compte Super admin pour commencer",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "S'inscrire avec E-mail",
+ "user": ""
+ },
+ "termsAndPolicies": "En vous inscrivant, vous acceptez nos",
+ "links": {
+ "login": "Vous avez un compte ? Se connecter"
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "Mot de passe oublié ?",
+ "subheadings": {
+ "stepOne": "Nous vous enverrons des instructions de réinitialisation.",
+ "stepTwo": "Nous avons envoyé un lien de réinitialisation à ",
+ "stepThree": "Votre nouveau mot de passe doit être différent des anciens mots de passes utilisés.",
+ "stepFour": "Votre mot de passe a été réinitialisé avec succès. Cliquez ci-dessous pour vous connecter."
+ },
+ "buttons": {
+ "openEmail": "Ouvrir l'appli mail",
+ "resetPassword": "Réinitialiser le mot de passe"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "Retourner à Se connecter",
+ "resend": "Vous n'avez pas reçu l'e-mail ? Cliquez pour renvoyer"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Vous n'avez pas de compte",
- "email": "E-mail",
"forgotPassword": "Mot de passe oublié",
- "password": "mot de passe",
- "signUp": "S'inscrire",
"submit": "Envoyer",
"title": "Titre",
- "continue": "Continuer",
"enterEmail": "Entrez votre adresse e-mail",
- "authLoginTitle": "Se connecter",
- "authLoginEnterPassword": "Entrez votre mot de passe",
"commonPassword": "Mot de passe",
- "commonBack": "Retour",
- "authForgotPasswordTitle": "Mot de passe oublié ?",
- "authForgotPasswordResetPassword": "Réinitialiser le mot de passe",
- "createPassword": "Créez votre mot de passe",
- "createAPassword": "Créer un mot de passe",
- "authRegisterAlreadyHaveAccount": "Vous avez un compte ?",
- "authLoginEnterEmail": "Entrez votre e-mail",
"authRegisterTitle": "Créer un compte",
"authRegisterStepOneTitle": "Créez votre compte",
"authRegisterStepOneDescription": "Entrez vos informations pour commencer",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Parlez-nous un peu de vous",
"authRegisterStepThreeTitle": "Presque fini !",
"authRegisterStepThreeDescription": "Vérifiez vos infos",
- "authForgotPasswordDescription": "Pas d'inquiétude, nous allons vous envoyer des instructions de réinitialisation.",
"authForgotPasswordSendInstructions": "Envoyer les instructions",
"authForgotPasswordBackTo": "Retourner à",
"authCheckEmailTitle": "Vérifiez votre e-mail",
- "authCheckEmailDescription": "Nous avons envoyé un lien de réinitialisation à",
"authCheckEmailResendEmail": "Renvoyer le mail",
"authCheckEmailBackTo": "Retourner à",
- "goBackTo": "Retourner à",
- "authCheckEmailDidntReceiveEmail": "Vous n'avez pas reçu l'e-mail ?",
- "authCheckEmailClickToResend": "Cliquez pour renvoyer",
"authSetNewPasswordTitle": "Définir un nouveau mot de passe",
- "authSetNewPasswordDescription": "Votre nouveau mot de passe doit être différent des anciens mots de passes utilisés.",
"authSetNewPasswordNewPassword": "Nouveau mot de passe",
- "authSetNewPasswordConfirmPassword": "Confirmer le mot de passe",
- "confirmPassword": "Confirmez votre mot de passe",
- "authSetNewPasswordResetPassword": "Réinitialiser le mot de passe",
"authSetNewPasswordBackTo": "Retourner à",
- "authPasswordMustBeAtLeast": "Doit faire au moins",
- "authPasswordCharactersLong": "8 caractères",
- "authPasswordMustContainAtLeast": "Doit contenir au moins",
- "authPasswordSpecialCharacter": "un caractère spécial",
- "authPasswordOneNumber": "un nombre",
- "authPasswordUpperCharacter": "une lettre majuscule",
- "authPasswordLowerCharacter": "une lettre minuscule",
- "authPasswordConfirmAndPassword": "Confirmer le mot de passe",
- "authPasswordMustMatch": "doit correspondre",
"authRegisterCreateAccount": "Créez votre compte pour commencer",
- "authRegisterCreateSuperAdminAccount": "Créez votre compte Super admin pour commencer",
- "authRegisterSignUpWithEmail": "S'inscrire avec E-mail",
- "authRegisterBySigningUp": "En vous inscrivant, vous acceptez nos",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "Paramètres généraux",
@@ -140,9 +263,6 @@
"city": "VILLE",
"response": "",
"passwordreset": "",
- "authRegisterStepOnePersonalDetails": "Saisissez vos infos",
- "authCheckEmailOpenEmailButton": "Ouvrir l'appli mail",
- "authNewPasswordConfirmed": "Votre mot de passe a été réinitialisé avec succès. Cliquez ci-dessous pour vous connecter.",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Vous supprimez vraiment ce compte ?",
- "authRegisterFirstName": "Prénom",
- "authRegisterLastName": "Nom",
"authRegisterEmail": "Email",
- "authRegisterEmailRequired": "Pour continuer, veuillez saisir votre adresse e-mail",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +548,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "Bienvenue ! Vous êtes connecté avec succès.",
- "authRegisterLoginLink": "Se connecter",
"validationNameRequired": "Veuillez saisir votre nom",
"validationNameTooLong": "",
"validationNameInvalidCharacters": "Veuillez utiliser uniquement des lettres, des espaces, des apostrophes ou des traits d'union",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "Supprimer le compte",
"DeleteAccountButton": "Supprimer le compte",
- "authRegisterEmailInvalid": "Veuillez saisir une adresse e-mail valide",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
"maskedPageSpeedKeyPlaceholder": "",
"pageSpeedApiKeyFieldTitle": "",
"pageSpeedApiKeyFieldLabel": "",
@@ -528,7 +640,6 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "Nous vous enverrons des instructions de réinitialisation.",
"settingsThemeModeLight": "Clair",
"settingsThemeModeDark": "Sombre",
"settingsClearAllStatsDialogCancel": "Annuler",
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index 25f14086e..f042e5fd6 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Erro de rede",
- "checkConnection": "Verifique sua conexão"
+ "checkConnection": "Verifique sua conexão",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Continuar",
+ "back": "Voltar"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-mail",
+ "placeholder": "",
+ "errors": {
+ "empty": "Para continuar, insira seu endereço de e-mail",
+ "invalid": "Por favor, insira um endereço de e-mail válido"
+ }
+ },
+ "password": {
+ "label": "Senha",
+ "rules": {
+ "length": {
+ "beginning": "Deve ser pelo menos",
+ "highlighted": "8 caracteres de comprimento"
+ },
+ "special": {
+ "beginning": "Deve conter pelo menos",
+ "highlighted": "um caractere especial"
+ },
+ "number": {
+ "beginning": "Deve conter pelo menos",
+ "highlighted": "um número"
+ },
+ "uppercase": {
+ "beginning": "Deve conter pelo menos",
+ "highlighted": "um caractere maiúsculo"
+ },
+ "lowercase": {
+ "beginning": "Deve conter pelo menos",
+ "highlighted": "um caractere minúsculo"
+ },
+ "match": {
+ "beginning": "Confirme a senha",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "Confirme sua senha",
+ "placeholder": "Digite a senha novamente para confirmar",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "Nome",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "Sobrenome",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ }
+ },
+ "login": {
+ "heading": "Login",
+ "subheadings": {
+ "stepOne": "Insira seu e-mail",
+ "stepTwo": "Digite sua senha"
+ },
+ "links": {
+ "forgotPassword": "Esqueceu a senha? Redefinir a senha",
+ "register": "Ainda não tem uma conta? Registre-se aqui"
+ },
+ "toasts": {
+ "success": "Bem-vindo de volta! Você fez login com sucesso.",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Inscreva-se"
+ },
+ "subheadings": {
+ "stepOne": "Insira seus dados pessoais",
+ "stepTwo": "Insira seu e-mail",
+ "stepThree": "Criar sua senha"
+ },
+ "description": {
+ "superAdmin": "Crie sua conta de super admin para começar",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "Criar conta de super admin",
+ "user": ""
+ },
+ "termsAndPolicies": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade.",
+ "links": {
+ "login": "Já possui uma conta? Login"
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "Esqueceu a senha?",
+ "subheadings": {
+ "stepOne": "Não se preocupe, enviaremos instruções de redefinição.",
+ "stepTwo": "Enviamos um link para redefinir a senha para ",
+ "stepThree": "Sua nova senha deve ser diferente das senhas usadas anteriormente.",
+ "stepFour": "Sua senha foi redefinida com sucesso. Clique abaixo para fazer login magicamente."
+ },
+ "buttons": {
+ "openEmail": "Abra o aplicativo de e-mail",
+ "resetPassword": "Redefinir senha"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "Volte para Login",
+ "resend": "Não recebeu o e-mail? Clique para reenviar"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Não tenho conta",
- "email": "E-mail",
"forgotPassword": "Esqueci a senha",
- "password": "Senha",
- "signUp": "Inscreva-se",
"submit": "Enviar",
"title": "Título",
- "continue": "Continuar",
"enterEmail": "Insira seu e-mail",
- "authLoginTitle": "Login",
- "authLoginEnterPassword": "Digite sua senha",
"commonPassword": "Senha",
- "commonBack": "Voltar",
- "authForgotPasswordTitle": "Esqueceu a senha?",
- "authForgotPasswordResetPassword": "Redefinir a senha",
- "createPassword": "Criar sua senha",
- "createAPassword": "Senha",
- "authRegisterAlreadyHaveAccount": "Já possui uma conta?",
- "authLoginEnterEmail": "Insira seu e-mail",
"authRegisterTitle": "Criar uma conta",
"authRegisterStepOneTitle": "Crie sua conta",
"authRegisterStepOneDescription": "Insira seus dados para começar",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Conte-nos mais sobre você",
"authRegisterStepThreeTitle": "Quase pronto!",
"authRegisterStepThreeDescription": "Revise suas informações",
- "authForgotPasswordDescription": "Não se preocupe, enviaremos instruções de redefinição.",
"authForgotPasswordSendInstructions": "Enviar instruções",
"authForgotPasswordBackTo": "Voltar para",
"authCheckEmailTitle": "Verifique seu e-mail",
- "authCheckEmailDescription": "Enviamos um link para redefinir a senha para",
"authCheckEmailResendEmail": "Reenviar e-mail",
"authCheckEmailBackTo": "Voltar para",
- "goBackTo": "Volte para",
- "authCheckEmailDidntReceiveEmail": "Não recebeu o e-mail?",
- "authCheckEmailClickToResend": "Clique para reenviar",
"authSetNewPasswordTitle": "Definir nova senha",
- "authSetNewPasswordDescription": "Sua nova senha deve ser diferente das senhas usadas anteriormente.",
"authSetNewPasswordNewPassword": "Nova senha",
- "authSetNewPasswordConfirmPassword": "Confirme sua senha",
- "confirmPassword": "Digite a senha novamente para confirmar",
- "authSetNewPasswordResetPassword": "Redefinir senha",
"authSetNewPasswordBackTo": "Voltar para",
- "authPasswordMustBeAtLeast": "Deve ser pelo menos",
- "authPasswordCharactersLong": "8 caracteres de comprimento",
- "authPasswordMustContainAtLeast": "Deve conter pelo menos",
- "authPasswordSpecialCharacter": "um caractere especial",
- "authPasswordOneNumber": "um número",
- "authPasswordUpperCharacter": "um caractere maiúsculo",
- "authPasswordLowerCharacter": "um caractere minúsculo",
- "authPasswordConfirmAndPassword": "Confirme a senha",
- "authPasswordMustMatch": "As senhas devem corresponder",
"authRegisterCreateAccount": "Crie sua conta para começar",
- "authRegisterCreateSuperAdminAccount": "Crie sua conta de super admin para começar",
- "authRegisterSignUpWithEmail": "Criar conta de super admin",
- "authRegisterBySigningUp": "Ao criar uma conta, você concorda com nossos Termos de Serviço e Política de Privacidade.",
"distributedStatusHeaderText": "Cobertura em tempo real e em dispositivos reais",
"distributedStatusSubHeaderText": "Impulsionado por milhões de dispositivos em todo o mundo, visualize o desempenho do sistema por região global, país ou cidade",
"settingsGeneralSettings": "Configurações gerais",
@@ -139,9 +262,6 @@
"city": "CIDADE",
"response": "RESPOSTA",
"passwordreset": "Redefinição de senha",
- "authRegisterStepOnePersonalDetails": "Insira seus dados pessoais",
- "authCheckEmailOpenEmailButton": "Abra o aplicativo de e-mail",
- "authNewPasswordConfirmed": "Sua senha foi redefinida com sucesso. Clique abaixo para fazer login magicamente.",
"monitorStatusUp": "O monitor {name} ({url}) agora está ATIVO e respondendo",
"monitorStatusDown": "O monitor {name} ({url}) está INATIVO e não está respondendo",
"webhookSendSuccess": "Notificação de webhook enviada com sucesso",
@@ -415,10 +535,7 @@
"DeleteDescriptionText": "Isso removerá a conta e todos os dados associados do servidor. Essa ação não é reversível.",
"DeleteAccountWarning": "Remover sua conta significa que você não poderá fazer login novamente e todos os seus dados serão removidos. Isso não é reversível.",
"DeleteWarningTitle": "Deseja realmente remover esta conta?",
- "authRegisterFirstName": "Nome",
- "authRegisterLastName": "Sobrenome",
"authRegisterEmail": "E-mail",
- "authRegisterEmailRequired": "Para continuar, insira seu endereço de e-mail",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Selecione o arquivo CSV para enviar",
@@ -430,8 +547,6 @@
"noFileSelected": "Nenhum arquivo selecionado",
"fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa"
},
- "welcomeBack": "Bem-vindo de volta! Você fez login com sucesso.",
- "authRegisterLoginLink": "Login",
"validationNameRequired": "Por favor digite seu nome",
"validationNameTooLong": "O nome deve ter menos de 50 caracteres",
"validationNameInvalidCharacters": "Por favor, use apenas letras, espaços, apóstrofos ou hifens",
@@ -439,10 +554,7 @@
"settingsSystemResetDescription": "Remova todos os monitores do seu sistema.",
"DeleteAccountTitle": "Remover conta",
"DeleteAccountButton": "Remover conta",
- "authRegisterEmailInvalid": "Por favor, insira um endereço de e-mail válido",
"publicLink": "Link publico",
- "doNotHaveAccount": "Ainda não tem uma conta?",
- "registerHere": "Registre-se aqui",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
"pageSpeedApiKeyFieldLabel": "PageSpeed API key",
@@ -527,7 +639,6 @@
"state": "Estado",
"statusBreadCrumbsStatusPages": "Pagina de status",
"statusBreadCrumbsDetails": "Detalhes",
- "authForgotPasswordInstructions": "Não se preocupe, enviaremos instruções de redefinição.",
"settingsThemeModeLight": "Claro",
"settingsThemeModeDark": "Escuro",
"settingsClearAllStatsDialogCancel": "Cancelar",
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index ae83dc57a..f6ccdd5fd 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Ошибка сети",
- "checkConnection": "Пожалуйста, проверьте ваше соединение"
+ "checkConnection": "Пожалуйста, проверьте ваше соединение",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Продолжить",
+ "back": "Назад"
+ },
+ "inputs": {
+ "email": {
+ "label": "Почта",
+ "placeholder": "",
+ "errors": {
+ "empty": "Чтобы продолжить, пожалуйста, введите свой адрес электронной почты",
+ "invalid": "Пожалуйста, введите действительный адрес электронной почты"
+ }
+ },
+ "password": {
+ "label": "пароль",
+ "rules": {
+ "length": {
+ "beginning": "Должно быть как минимум",
+ "highlighted": "длиной 8 символов"
+ },
+ "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": ""
+ }
+ },
+ "login": {
+ "heading": "Войти",
+ "subheadings": {
+ "stepOne": "Введите свой email",
+ "stepTwo": "Введите свой пароль"
+ },
+ "links": {
+ "forgotPassword": "Забыли пароль? Сбросить пароль",
+ "register": "У вас нет учетной записи? Зарегистрируйтесь здесь"
+ },
+ "toasts": {
+ "success": "Добро пожаловать обратно! Вы успешно вошли в систему.",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Зарегистрироваться"
+ },
+ "subheadings": {
+ "stepOne": "Введите свои личные данные",
+ "stepTwo": "Введите свой email",
+ "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": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Нет аккаунта",
- "email": "Почта",
"forgotPassword": "Забыли пароль",
- "password": "пароль",
- "signUp": "Зарегистрироваться",
"submit": "Подтвердить",
"title": "Название",
- "continue": "Продолжить",
"enterEmail": "Введите свой email",
- "authLoginTitle": "Войти",
- "authLoginEnterPassword": "Введите свой пароль",
"commonPassword": "Пароль",
- "commonBack": "Назад",
- "authForgotPasswordTitle": "Забыли пароль?",
- "authForgotPasswordResetPassword": "Сбросить пароль",
- "createPassword": "Создайте свой пароль",
- "createAPassword": "Создайте пароль",
- "authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
- "authLoginEnterEmail": "Введите свой email",
"authRegisterTitle": "Создать аккаунт",
"authRegisterStepOneTitle": "Создайте свой аккаут",
"authRegisterStepOneDescription": "Введите свои данные, чтобы начать",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Расскажите нам о себе",
"authRegisterStepThreeTitle": "Почти готово!",
"authRegisterStepThreeDescription": "Проверьте свою информацию",
- "authForgotPasswordDescription": "Не волнуйтесь, мы вышлем вам инструкции по сбросу настроек.",
"authForgotPasswordSendInstructions": "Отправить инструкции",
"authForgotPasswordBackTo": "Назад к",
"authCheckEmailTitle": "Проверьте свою почту",
- "authCheckEmailDescription": "Мы отправили ссылку для сброса пароля",
"authCheckEmailResendEmail": "Отправить письмо повторно",
"authCheckEmailBackTo": "Назад к",
- "goBackTo": "Назад к",
- "authCheckEmailDidntReceiveEmail": "Не получили письмо?",
- "authCheckEmailClickToResend": "Нажмите, чтобы отправить повторно",
"authSetNewPasswordTitle": "Установите новый пароль",
- "authSetNewPasswordDescription": "Ваш новый пароль должен отличаться от ранее использованных паролей.",
"authSetNewPasswordNewPassword": "Новый пароль",
- "authSetNewPasswordConfirmPassword": "Подтвердите пароль",
- "confirmPassword": "Подтвердите ваш пароль",
- "authSetNewPasswordResetPassword": "Сбросить пароль",
"authSetNewPasswordBackTo": "Назад к",
- "authPasswordMustBeAtLeast": "Должно быть как минимум",
- "authPasswordCharactersLong": "длиной 8 символов",
- "authPasswordMustContainAtLeast": "Должен содержать как минимум",
- "authPasswordSpecialCharacter": "один специальный символ",
- "authPasswordOneNumber": "одно число",
- "authPasswordUpperCharacter": "один верхний символ",
- "authPasswordLowerCharacter": "один нижний символ",
- "authPasswordConfirmAndPassword": "Подтвердите пароль и пароль",
- "authPasswordMustMatch": "должен совпадать",
"authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
- "authRegisterCreateSuperAdminAccount": "Создайте учетную запись суперадминистратора, чтобы начать работу",
- "authRegisterSignUpWithEmail": "Зарегистрироваться по электронной почте",
- "authRegisterBySigningUp": "Регистрируясь, вы соглашаетесь с нашими",
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
@@ -136,9 +259,6 @@
"city": "ГОРОД",
"response": "ОТВЕТ",
"passwordreset": "Сброс пароля",
- "authRegisterStepOnePersonalDetails": "Введите свои личные данные",
- "authCheckEmailOpenEmailButton": "Откройте приложение почты",
- "authNewPasswordConfirmed": "Ваш пароль успешно сброшен. Нажмите ниже, чтобы войти в систему магическим образом.",
"monitorStatusUp": "Монитор {name} ({url}) теперь включен и отвечает",
"monitorStatusDown": "Монитор {name} ({url}) ОТКЛЮЧЕН и не отвечает",
"webhookSendSuccess": "Уведомление Webhook успешно отправлено",
@@ -411,10 +531,7 @@
"DeleteDescriptionText": "Это приведет к удалению учетной записи и всех связанных с ней данных с сервера. Это необратимо.",
"DeleteAccountWarning": "Удаление вашей учетной записи означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
"DeleteWarningTitle": "Действительно удаляете эту учетную запись?",
- "authRegisterFirstName": "Имя",
- "authRegisterLastName": "Фамилия",
"authRegisterEmail": "Email",
- "authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите свой адрес электронной почты",
"bulkImport": {
"title": "Массовый импорт",
"selectFileTips": "Выберите CSV-файл для загрузки",
@@ -426,8 +543,6 @@
"noFileSelected": "Файл не выбран",
"fallbackPage": "Импортируйте файл для массовой загрузки списка серверов"
},
- "welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
- "authRegisterLoginLink": "Войти",
"validationNameRequired": "Пожалуйста, введите свое имя",
"validationNameTooLong": "Длина имени не должна превышать 50 символов",
"validationNameInvalidCharacters": "Пожалуйста, используйте только буквы, пробелы, апострофы или дефисы",
@@ -435,10 +550,7 @@
"settingsSystemResetDescription": "Удалите все мониторы из вашей системы.",
"DeleteAccountTitle": "Удалить аккаунт",
"DeleteAccountButton": "Удалить аккаунт",
- "authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты",
"publicLink": "Публичная ссылка",
- "doNotHaveAccount": "У вас нет учетной записи?",
- "registerHere": "Зарегистрируйтесь здесь",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Ключ API Google PageSpeed",
"pageSpeedApiKeyFieldLabel": "Ключ API PageSpeed",
@@ -523,7 +635,6 @@
"state": "Состояние",
"statusBreadCrumbsStatusPages": "Страницы статуса",
"statusBreadCrumbsDetails": "Подробности",
- "authForgotPasswordInstructions": "Не беспокойтесь, мы вышлем вам инструкции по сбросу настроек.",
"settingsThemeModeLight": "Светлая",
"settingsThemeModeDark": "Тёмная",
"settingsClearAllStatsDialogCancel": "Отменить",
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 59e911118..0d650e985 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "Ağ hatası",
- "checkConnection": "Lütfen bağlantınızı kontrol edin"
+ "checkConnection": "Lütfen bağlantınızı kontrol edin",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Devam Et",
+ "back": "Geri"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-posta",
+ "placeholder": "",
+ "errors": {
+ "empty": "Devam etmek için lütfen e-posta adresinizi girin",
+ "invalid": "Lütfen geçerli bir eposta hesabı girin"
+ }
+ },
+ "password": {
+ "label": "Parola",
+ "rules": {
+ "length": {
+ "beginning": "En az",
+ "highlighted": "8 karakter uzunluğunda olmalı"
+ },
+ "special": {
+ "beginning": "En az içermeli",
+ "highlighted": "bir özel karakter"
+ },
+ "number": {
+ "beginning": "En az içermeli",
+ "highlighted": "bir rakam"
+ },
+ "uppercase": {
+ "beginning": "En az içermeli",
+ "highlighted": "bir büyük harf"
+ },
+ "lowercase": {
+ "beginning": "En az içermeli",
+ "highlighted": "bir küçük harf"
+ },
+ "match": {
+ "beginning": "Onay şifresi ve şifre",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "Parolayı onayla",
+ "placeholder": "Parolanızı onaylayın",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "İsim",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "Soyisim",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ }
+ },
+ "login": {
+ "heading": "Giriş Yap",
+ "subheadings": {
+ "stepOne": "E-posta adresinizi girin",
+ "stepTwo": "Parolanızı girin"
+ },
+ "links": {
+ "forgotPassword": "Parolanızı mı unuttunuz? Parola sıfırla",
+ "register": "Bir hesabınız yok mu? Buradan kayıt olun"
+ },
+ "toasts": {
+ "success": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "Kayıt Ol"
+ },
+ "subheadings": {
+ "stepOne": "Kişisel bilgilerinizi girin",
+ "stepTwo": "E-posta adresinizi girin",
+ "stepThree": "Parolanızı oluşturun"
+ },
+ "description": {
+ "superAdmin": "Super admin hesabınızı oluşturmak için devam edin",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "E-posta ile kayıt ol",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": "Zaten hesabınız var mı? Giriş yap"
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "Parolanızı mı unuttunuz?",
+ "subheadings": {
+ "stepOne": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
+ "stepTwo": " adresine şifre sıfırlama bağlantısı gönderdik",
+ "stepThree": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
+ "stepFour": "Parolanız başarıyla sıfırlandı. Otomatik olarak giriş yapmak için aşağıya tıklayın."
+ },
+ "buttons": {
+ "openEmail": "Eposta uygulamasını aç",
+ "resetPassword": "Parola sıfırla"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "Geri dön Giriş Yap",
+ "resend": "E-posta almadınız mı? Yeniden göndermek için tıklayın"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "Hesabınız yok mu",
- "email": "E-posta",
"forgotPassword": "Parolamı unuttum",
- "password": "Parola",
- "signUp": "Kayıt Ol",
"submit": "Gönder",
"title": "Başlık",
- "continue": "Devam Et",
"enterEmail": "E-posta adresinizi girin",
- "authLoginTitle": "Giriş Yap",
- "authLoginEnterPassword": "Parolanızı girin",
"commonPassword": "Parola",
- "commonBack": "Geri",
- "authForgotPasswordTitle": "Parolanızı mı unuttunuz?",
- "authForgotPasswordResetPassword": "Parola sıfırla",
- "createPassword": "Parolanızı oluşturun",
- "createAPassword": "Bir parola oluşturun",
- "authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?",
- "authLoginEnterEmail": "E-posta adresinizi girin",
"authRegisterTitle": "Hesap oluştur",
"authRegisterStepOneTitle": "Hesabınızı oluşturun",
"authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin",
"authRegisterStepThreeTitle": "Neredeyse bitti!",
"authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin",
- "authForgotPasswordDescription": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
"authForgotPasswordSendInstructions": "Talimatları gönder",
"authForgotPasswordBackTo": "Geri dön",
"authCheckEmailTitle": "E-postanızı kontrol edin",
- "authCheckEmailDescription": "{{email}} adresine şifre sıfırlama bağlantısı gönderdik",
"authCheckEmailResendEmail": "E-postayı yeniden gönder",
"authCheckEmailBackTo": "Geri dön",
- "goBackTo": "Geri dön",
- "authCheckEmailDidntReceiveEmail": "E-posta almadınız mı?",
- "authCheckEmailClickToResend": "Yeniden göndermek için tıklayın",
"authSetNewPasswordTitle": "Yeni şifre belirle",
- "authSetNewPasswordDescription": "Yeni şifreniz daha önce kullanılan şifrelerden farklı olmalıdır.",
"authSetNewPasswordNewPassword": "Yeni şifre",
- "authSetNewPasswordConfirmPassword": "Parolayı onayla",
- "confirmPassword": "Parolanızı onaylayın",
- "authSetNewPasswordResetPassword": "Parola sıfırla",
"authSetNewPasswordBackTo": "Geri dön",
- "authPasswordMustBeAtLeast": "En az",
- "authPasswordCharactersLong": "8 karakter uzunluğunda olmalı",
- "authPasswordMustContainAtLeast": "En az içermeli",
- "authPasswordSpecialCharacter": "bir özel karakter",
- "authPasswordOneNumber": "bir rakam",
- "authPasswordUpperCharacter": "bir büyük harf",
- "authPasswordLowerCharacter": "bir küçük harf",
- "authPasswordConfirmAndPassword": "Onay şifresi ve şifre",
- "authPasswordMustMatch": "eşleşmelidir",
"authRegisterCreateAccount": "Hesap oluşturmak için devam et",
- "authRegisterCreateSuperAdminAccount": "Super admin hesabınızı oluşturmak için devam edin",
- "authRegisterSignUpWithEmail": "E-posta ile kayıt ol",
- "authRegisterBySigningUp": "Kayıt olarak, aşağıdaki şartları kabul ediyorsunuz:",
"distributedStatusHeaderText": "Gerçek zamanlı, Gerçek cihazlar kapsamı",
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
"settingsGeneralSettings": "Genel ayarlar",
@@ -140,9 +263,6 @@
"city": "ŞEHİR",
"response": "YANIT",
"passwordreset": "Parola Sıfırlama",
- "authRegisterStepOnePersonalDetails": "Kişisel bilgilerinizi girin",
- "authCheckEmailOpenEmailButton": "Eposta uygulamasını aç",
- "authNewPasswordConfirmed": "Parolanız başarıyla sıfırlandı. Otomatik olarak giriş yapmak için aşağıya tıklayın.",
"monitorStatusUp": "Monitör {name} ({url}) ayakta ve yanıt veriyor",
"monitorStatusDown": "Monitör {name} ({url}) yanıt vermiyor",
"webhookSendSuccess": "Web kanca bildirimi başarıyla gönderildi",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "Bu, hesabı ve ilişkili tüm verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
"DeleteAccountWarning": "Hesabınızı kaldırmanız, tekrar oturum açamayacağınız ve tüm verilerinizin silineceği anlamına gelir. Bu geri alınamaz.",
"DeleteWarningTitle": "Gerçekten bu hesabı silmek istiyor musunuz?",
- "authRegisterFirstName": "İsim",
- "authRegisterLastName": "Soyisim",
"authRegisterEmail": "E-posta",
- "authRegisterEmailRequired": "Devam etmek için lütfen e-posta adresinizi girin",
"bulkImport": {
"title": "Toplu içeri alma",
"selectFileTips": "Yüklenecek CSV dosyasını seçin",
@@ -431,8 +548,6 @@
"noFileSelected": "Hiçbir dosya seçilmedi",
"fallbackPage": "Toplu olarak sunucu listesini yüklemek için bir dosyayı içe aktarın"
},
- "welcomeBack": "Tekrar hoş geldiniz! Başarıyla giriş yaptınız.",
- "authRegisterLoginLink": "Giriş yap",
"validationNameRequired": "Lütfen adınızı girin",
"validationNameTooLong": "İsim 50 karakterden az olmalıdır",
"validationNameInvalidCharacters": "Lütfen sadece harfler, boşluk, tırnak ve çizgi kullanın",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "Sistemdeki tüm monitörleri silin",
"DeleteAccountTitle": "Hesabı sil",
"DeleteAccountButton": "Hesabı sil",
- "authRegisterEmailInvalid": "Lütfen geçerli bir eposta hesabı girin",
"publicLink": "Herkese açık bağlantı",
- "doNotHaveAccount": "Bir hesabınız yok mu?",
- "registerHere": "Buradan kayıt olun",
"maskedPageSpeedKeyPlaceholder": "*************************************",
"pageSpeedApiKeyFieldTitle": "Google PageSpeed API anahtarı",
"pageSpeedApiKeyFieldLabel": "PageSpeed API anahtarı",
@@ -528,7 +640,6 @@
"state": "Durum",
"statusBreadCrumbsStatusPages": "Durum sayfası",
"statusBreadCrumbsDetails": "Detaylar",
- "authForgotPasswordInstructions": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.",
"settingsThemeModeLight": "Aydınlat",
"settingsThemeModeDark": "Karart",
"settingsClearAllStatsDialogCancel": "İptal",
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index 98fed65ef..11c063580 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -7,7 +7,164 @@
},
"toasts": {
"networkError": "",
- "checkConnection": ""
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "繼續",
+ "back": "回去"
+ },
+ "inputs": {
+ "email": {
+ "label": "E-mail",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "密碼",
+ "rules": {
+ "length": {
+ "beginning": "必須至少",
+ "highlighted": "8 個字符長"
+ },
+ "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": ""
+ }
+ },
+ "login": {
+ "heading": "登入",
+ "subheadings": {
+ "stepOne": "輸入您的 E-mail",
+ "stepTwo": "輸入您的密碼"
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": "註冊"
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "輸入您的 E-mail",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "建立您的超級管理員帳號開始使用",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "用 E-mail 註冊",
+ "user": ""
+ },
+ "termsAndPolicies": "註冊即表示您同意我們的",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "忘記密碼?",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "我們已將密碼重設連結發送至 ",
+ "stepThree": "您的新密碼必須與之前使用過的密碼不同。",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": "重設密碼"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "回去 登入",
+ "resend": "沒有受到 E-mail? 點擊"
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
}
},
"errorPages": {
@@ -25,24 +182,11 @@
}
},
"dontHaveAccount": "沒有帳號",
- "email": "E-mail",
"forgotPassword": "忘記密碼",
- "password": "密碼",
- "signUp": "註冊",
"submit": "",
"title": "",
- "continue": "繼續",
"enterEmail": "輸入您的 E-mail",
- "authLoginTitle": "登入",
- "authLoginEnterPassword": "輸入您的密碼",
"commonPassword": "密碼",
- "commonBack": "回去",
- "authForgotPasswordTitle": "忘記密碼?",
- "authForgotPasswordResetPassword": "",
- "createPassword": "",
- "createAPassword": "",
- "authRegisterAlreadyHaveAccount": "已經有帳號嗎?",
- "authLoginEnterEmail": "輸入您的 E-mail",
"authRegisterTitle": "建立帳號",
"authRegisterStepOneTitle": "建立您的帳號",
"authRegisterStepOneDescription": "清輸入您的個人檔案",
@@ -50,36 +194,15 @@
"authRegisterStepTwoDescription": "請分享更多關於您的資訊",
"authRegisterStepThreeTitle": "快完成了!",
"authRegisterStepThreeDescription": "請確認您的資訊",
- "authForgotPasswordDescription": "別擔心,我們會發送重設說明給您。",
"authForgotPasswordSendInstructions": "將說明發送至",
"authForgotPasswordBackTo": "回去",
"authCheckEmailTitle": "請查看您的電子郵件",
- "authCheckEmailDescription": "我們已將密碼重設連結發送至",
"authCheckEmailResendEmail": "重新發送 E-mail",
"authCheckEmailBackTo": "回去",
- "goBackTo": "回去",
- "authCheckEmailDidntReceiveEmail": "沒有受到 E-mail?",
- "authCheckEmailClickToResend": "點擊",
"authSetNewPasswordTitle": "設定新密碼",
- "authSetNewPasswordDescription": "您的新密碼必須與之前使用過的密碼不同。",
"authSetNewPasswordNewPassword": "新密碼",
- "authSetNewPasswordConfirmPassword": "確認密碼",
- "confirmPassword": "確認您的密碼",
- "authSetNewPasswordResetPassword": "重設密碼",
"authSetNewPasswordBackTo": "回去",
- "authPasswordMustBeAtLeast": "必須至少",
- "authPasswordCharactersLong": "8 個字符長",
- "authPasswordMustContainAtLeast": "必須至少包含",
- "authPasswordSpecialCharacter": "一個特殊字符",
- "authPasswordOneNumber": "一個數字",
- "authPasswordUpperCharacter": "一個大寫字母",
- "authPasswordLowerCharacter": "一個小寫字母",
- "authPasswordConfirmAndPassword": "確認密碼",
- "authPasswordMustMatch": "必須相符",
"authRegisterCreateAccount": "建立您的帳號開始使用",
- "authRegisterCreateSuperAdminAccount": "建立您的超級管理員帳號開始使用",
- "authRegisterSignUpWithEmail": "用 E-mail 註冊",
- "authRegisterBySigningUp": "註冊即表示您同意我們的",
"distributedStatusHeaderText": "實時、真實設備覆蓋",
"distributedStatusSubHeaderText": "由全球數百萬個設備提供支持,您可以按全球區域、國家或城市查看系統效能",
"settingsGeneralSettings": "一般設定",
@@ -140,9 +263,6 @@
"city": "",
"response": "",
"passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -416,10 +536,7 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
"authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -431,8 +548,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
"validationNameRequired": "",
"validationNameTooLong": "",
"validationNameInvalidCharacters": "",
@@ -440,10 +555,7 @@
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
"maskedPageSpeedKeyPlaceholder": "",
"pageSpeedApiKeyFieldTitle": "",
"pageSpeedApiKeyFieldLabel": "",
@@ -528,7 +640,6 @@
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
"settingsThemeModeLight": "",
"settingsThemeModeDark": "",
"settingsClearAllStatsDialogCancel": "",
From 8c9084cc7a7840cad239705897959224681530c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20=C5=A0mahel?=
<48548230+ceskyDJ@users.noreply.github.com>
Date: Sun, 8 Jun 2025 16:15:37 +0200
Subject: [PATCH 078/319] i18n: Clean up auth-related keys
Removes only keys that aren't used on any place (I searched for
each of them in all Components and Pages).
---
client/src/locales/cs.json | 25 -------------------------
client/src/locales/de.json | 25 -------------------------
client/src/locales/en.json | 25 -------------------------
client/src/locales/es.json | 25 -------------------------
client/src/locales/fi.json | 25 -------------------------
client/src/locales/fr.json | 25 -------------------------
client/src/locales/pt-BR.json | 25 -------------------------
client/src/locales/ru.json | 25 -------------------------
client/src/locales/tr.json | 25 -------------------------
client/src/locales/zh-TW.json | 25 -------------------------
10 files changed, 250 deletions(-)
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 3bb64e6c9..60bc510f0 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -181,29 +181,9 @@
}
}
},
- "dontHaveAccount": "",
- "forgotPassword": "",
"submit": "",
"title": "",
- "enterEmail": "",
- "commonPassword": "",
"createPassword": "",
- "authRegisterTitle": "",
- "authRegisterStepOneTitle": "",
- "authRegisterStepOneDescription": "",
- "authRegisterStepTwoTitle": "",
- "authRegisterStepTwoDescription": "",
- "authRegisterStepThreeTitle": "",
- "authRegisterStepThreeDescription": "",
- "authForgotPasswordSendInstructions": "",
- "authForgotPasswordBackTo": "",
- "authCheckEmailTitle": "",
- "authCheckEmailResendEmail": "",
- "authCheckEmailBackTo": "",
- "authSetNewPasswordTitle": "",
- "authSetNewPasswordNewPassword": "",
- "authSetNewPasswordBackTo": "",
- "authRegisterCreateAccount": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -263,7 +243,6 @@
"country": "",
"city": "",
"response": "",
- "passwordreset": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -537,7 +516,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterEmail": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -549,9 +527,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
"settingsSystemReset": "",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index 753af8b3c..8aa520e29 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Noch kein Konto",
- "forgotPassword": "Passwort vergessen",
"submit": "Absenden",
"title": "Titel",
- "enterEmail": "Geben Sie Ihre E-Mail-Adresse ein",
- "commonPassword": "Passwort",
- "authRegisterTitle": "",
- "authRegisterStepOneTitle": "",
- "authRegisterStepOneDescription": "",
- "authRegisterStepTwoTitle": "",
- "authRegisterStepTwoDescription": "",
- "authRegisterStepThreeTitle": "",
- "authRegisterStepThreeDescription": "",
- "authForgotPasswordSendInstructions": "",
- "authForgotPasswordBackTo": "",
- "authCheckEmailTitle": "",
- "authCheckEmailResendEmail": "",
- "authCheckEmailBackTo": "",
- "authSetNewPasswordTitle": "",
- "authSetNewPasswordNewPassword": "",
- "authSetNewPasswordBackTo": "",
- "authRegisterCreateAccount": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -262,7 +242,6 @@
"country": "",
"city": "",
"response": "",
- "passwordreset": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterEmail": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -548,9 +526,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
"settingsSystemReset": "",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index af06f419b..63802c7dc 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Don't have account",
- "forgotPassword": "Forgot Password",
"submit": "Submit",
"title": "Title",
- "enterEmail": "Enter your email",
- "commonPassword": "Password",
- "authRegisterTitle": "Create an account",
- "authRegisterStepOneTitle": "Create your account",
- "authRegisterStepOneDescription": "Enter your details to get started",
- "authRegisterStepTwoTitle": "Set up your profile",
- "authRegisterStepTwoDescription": "Tell us more about yourself",
- "authRegisterStepThreeTitle": "Almost done!",
- "authRegisterStepThreeDescription": "Review your information",
- "authForgotPasswordSendInstructions": "Send instructions",
- "authForgotPasswordBackTo": "Back to",
- "authCheckEmailTitle": "Check your email",
- "authCheckEmailResendEmail": "Resend email",
- "authCheckEmailBackTo": "Back to",
- "authSetNewPasswordTitle": "Set new password",
- "authSetNewPasswordNewPassword": "New password",
- "authSetNewPasswordBackTo": "Back to",
- "authRegisterCreateAccount": "Create your account to get started",
"distributedStatusHeaderText": "Real-time, real-device coverage",
"distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city",
"settingsGeneralSettings": "General settings",
@@ -258,7 +238,6 @@
"country": "COUNTRY",
"city": "CITY",
"response": "RESPONSE",
- "passwordreset": "Password Reset",
"monitorStatusUp": "Monitor {name} ({url}) is now UP and responding",
"monitorStatusDown": "Monitor {name} ({url}) is DOWN and not responding",
"webhookSendSuccess": "Webhook notification sent successfully",
@@ -528,7 +507,6 @@
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
- "authRegisterEmail": "Email",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
@@ -540,9 +518,6 @@
"noFileSelected": "No file selected",
"fallbackPage": "Import a file to upload a list of servers in bulk"
},
- "validationNameRequired": "Please enter your name",
- "validationNameTooLong": "Name should be less than 50 characters",
- "validationNameInvalidCharacters": "Please use only letters, spaces, apostrophes, or hyphens",
"settingsSystemReset": "System reset",
"settingsSystemResetDescription": "Remove all monitors from your system.",
"DeleteAccountTitle": "Remove account",
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index 5c0f551ad..0abc46c8c 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "No tengo una cuenta",
- "forgotPassword": "He olvidado mi contraseña",
"submit": "Enviar",
"title": "Titulo",
- "enterEmail": "Introduce tu email",
- "commonPassword": "Contraseña",
- "authRegisterTitle": "Crear una cuenta",
- "authRegisterStepOneTitle": "Crea tu cuenta",
- "authRegisterStepOneDescription": "",
- "authRegisterStepTwoTitle": "Configura tu perfil",
- "authRegisterStepTwoDescription": "Cuentano mas sobre ti",
- "authRegisterStepThreeTitle": "Ya casi estamos!",
- "authRegisterStepThreeDescription": "Revisa tu informacion",
- "authForgotPasswordSendInstructions": "Enviar instrucciones",
- "authForgotPasswordBackTo": "Volver a",
- "authCheckEmailTitle": "Comprueba tu correo",
- "authCheckEmailResendEmail": "Reenviar correo",
- "authCheckEmailBackTo": "Volver a",
- "authSetNewPasswordTitle": "Configurar nueva contraseña",
- "authSetNewPasswordNewPassword": "Nueva contraseña",
- "authSetNewPasswordBackTo": "Volver a",
- "authRegisterCreateAccount": "",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "",
@@ -262,7 +242,6 @@
"country": "",
"city": "",
"response": "",
- "passwordreset": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterEmail": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -548,9 +526,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
"settingsSystemReset": "",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index b4c364fe5..bd6f9a81f 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Ei tiliä",
- "forgotPassword": "Unohtunut salasana",
"submit": "Lähetä",
"title": "Otsikko",
- "enterEmail": "Syötä sähköpostiosoitteesi",
- "commonPassword": "Salasana",
- "authRegisterTitle": "Luo tili",
- "authRegisterStepOneTitle": "Luo tilisi",
- "authRegisterStepOneDescription": "Kirjoita tietosi aloittaaksesi",
- "authRegisterStepTwoTitle": "Määritä profiilisi",
- "authRegisterStepTwoDescription": "Kerro meille lisää itsestäsi",
- "authRegisterStepThreeTitle": "Melkein valmis!",
- "authRegisterStepThreeDescription": "Tarkista tietosi",
- "authForgotPasswordSendInstructions": "Lähetä ohjeet",
- "authForgotPasswordBackTo": "Palaa",
- "authCheckEmailTitle": "Tarkista sähköpostisi",
- "authCheckEmailResendEmail": "Lähetä sähköposti uudelleen",
- "authCheckEmailBackTo": "Palaa",
- "authSetNewPasswordTitle": "Aseta uusi salasana",
- "authSetNewPasswordNewPassword": "Uusi salasana",
- "authSetNewPasswordBackTo": "Takaisin",
- "authRegisterCreateAccount": "Luo tili aloittaaksesi",
"distributedStatusHeaderText": "Reaaliaikainen kattavuus oikeilla laitteilla",
"distributedStatusSubHeaderText": "Miljoonien laitteiden voimin ympäri maailman näet järjestelmän suorituskyvyn maailman alueittain, maittain tai kaupungeittain",
"settingsGeneralSettings": "Yleiset asetukset",
@@ -262,7 +242,6 @@
"country": "MAA",
"city": "KAUPUNKI",
"response": "Vastaus",
- "passwordreset": "Salasanan nollaus",
"monitorStatusUp": "Valvontakohde {name} ({url}) on nyt toiminnassa ja vastaa",
"monitorStatusDown": "Valvontakohde {name} ({url}) ei ole toiminnassa, eikä vastaa",
"webhookSendSuccess": "Webhook-ilmoitus lähetettiin onnistuneesti",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?",
- "authRegisterEmail": "Sähköpostiosoite",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -548,9 +526,6 @@
"noFileSelected": "Ei valittua tiedostoa",
"fallbackPage": ""
},
- "validationNameRequired": "Kirjoita nimesi",
- "validationNameTooLong": "Nimen tulee olla alle 50 merkkiä",
- "validationNameInvalidCharacters": "Käytäthän vain kirjaimia, välilyöntejä, heittomerkkejä tai yhdysmerkkejä",
"settingsSystemReset": "Järjestelmän palautus",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "Poista tili",
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index 092cb3c9d..a4c31b5c6 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Vous n'avez pas de compte",
- "forgotPassword": "Mot de passe oublié",
"submit": "Envoyer",
"title": "Titre",
- "enterEmail": "Entrez votre adresse e-mail",
- "commonPassword": "Mot de passe",
- "authRegisterTitle": "Créer un compte",
- "authRegisterStepOneTitle": "Créez votre compte",
- "authRegisterStepOneDescription": "Entrez vos informations pour commencer",
- "authRegisterStepTwoTitle": "Créer votre profil",
- "authRegisterStepTwoDescription": "Parlez-nous un peu de vous",
- "authRegisterStepThreeTitle": "Presque fini !",
- "authRegisterStepThreeDescription": "Vérifiez vos infos",
- "authForgotPasswordSendInstructions": "Envoyer les instructions",
- "authForgotPasswordBackTo": "Retourner à",
- "authCheckEmailTitle": "Vérifiez votre e-mail",
- "authCheckEmailResendEmail": "Renvoyer le mail",
- "authCheckEmailBackTo": "Retourner à",
- "authSetNewPasswordTitle": "Définir un nouveau mot de passe",
- "authSetNewPasswordNewPassword": "Nouveau mot de passe",
- "authSetNewPasswordBackTo": "Retourner à",
- "authRegisterCreateAccount": "Créez votre compte pour commencer",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
"settingsGeneralSettings": "Paramètres généraux",
@@ -262,7 +242,6 @@
"country": "PAYS",
"city": "VILLE",
"response": "",
- "passwordreset": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Vous supprimez vraiment ce compte ?",
- "authRegisterEmail": "Email",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -548,9 +526,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "validationNameRequired": "Veuillez saisir votre nom",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "Veuillez utiliser uniquement des lettres, des espaces, des apostrophes ou des traits d'union",
"settingsSystemReset": "",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "Supprimer le compte",
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index f042e5fd6..e90c89a62 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Não tenho conta",
- "forgotPassword": "Esqueci a senha",
"submit": "Enviar",
"title": "Título",
- "enterEmail": "Insira seu e-mail",
- "commonPassword": "Senha",
- "authRegisterTitle": "Criar uma conta",
- "authRegisterStepOneTitle": "Crie sua conta",
- "authRegisterStepOneDescription": "Insira seus dados para começar",
- "authRegisterStepTwoTitle": "Configure seu perfil",
- "authRegisterStepTwoDescription": "Conte-nos mais sobre você",
- "authRegisterStepThreeTitle": "Quase pronto!",
- "authRegisterStepThreeDescription": "Revise suas informações",
- "authForgotPasswordSendInstructions": "Enviar instruções",
- "authForgotPasswordBackTo": "Voltar para",
- "authCheckEmailTitle": "Verifique seu e-mail",
- "authCheckEmailResendEmail": "Reenviar e-mail",
- "authCheckEmailBackTo": "Voltar para",
- "authSetNewPasswordTitle": "Definir nova senha",
- "authSetNewPasswordNewPassword": "Nova senha",
- "authSetNewPasswordBackTo": "Voltar para",
- "authRegisterCreateAccount": "Crie sua conta para começar",
"distributedStatusHeaderText": "Cobertura em tempo real e em dispositivos reais",
"distributedStatusSubHeaderText": "Impulsionado por milhões de dispositivos em todo o mundo, visualize o desempenho do sistema por região global, país ou cidade",
"settingsGeneralSettings": "Configurações gerais",
@@ -261,7 +241,6 @@
"country": "PAÍS",
"city": "CIDADE",
"response": "RESPOSTA",
- "passwordreset": "Redefinição de senha",
"monitorStatusUp": "O monitor {name} ({url}) agora está ATIVO e respondendo",
"monitorStatusDown": "O monitor {name} ({url}) está INATIVO e não está respondendo",
"webhookSendSuccess": "Notificação de webhook enviada com sucesso",
@@ -535,7 +514,6 @@
"DeleteDescriptionText": "Isso removerá a conta e todos os dados associados do servidor. Essa ação não é reversível.",
"DeleteAccountWarning": "Remover sua conta significa que você não poderá fazer login novamente e todos os seus dados serão removidos. Isso não é reversível.",
"DeleteWarningTitle": "Deseja realmente remover esta conta?",
- "authRegisterEmail": "E-mail",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Selecione o arquivo CSV para enviar",
@@ -547,9 +525,6 @@
"noFileSelected": "Nenhum arquivo selecionado",
"fallbackPage": "Importe um arquivo para enviar uma lista de servidores em massa"
},
- "validationNameRequired": "Por favor digite seu nome",
- "validationNameTooLong": "O nome deve ter menos de 50 caracteres",
- "validationNameInvalidCharacters": "Por favor, use apenas letras, espaços, apóstrofos ou hifens",
"settingsSystemReset": "Resetar sistema",
"settingsSystemResetDescription": "Remova todos os monitores do seu sistema.",
"DeleteAccountTitle": "Remover conta",
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index f6ccdd5fd..9c74921a1 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Нет аккаунта",
- "forgotPassword": "Забыли пароль",
"submit": "Подтвердить",
"title": "Название",
- "enterEmail": "Введите свой email",
- "commonPassword": "Пароль",
- "authRegisterTitle": "Создать аккаунт",
- "authRegisterStepOneTitle": "Создайте свой аккаут",
- "authRegisterStepOneDescription": "Введите свои данные, чтобы начать",
- "authRegisterStepTwoTitle": "Настройте свой профиль",
- "authRegisterStepTwoDescription": "Расскажите нам о себе",
- "authRegisterStepThreeTitle": "Почти готово!",
- "authRegisterStepThreeDescription": "Проверьте свою информацию",
- "authForgotPasswordSendInstructions": "Отправить инструкции",
- "authForgotPasswordBackTo": "Назад к",
- "authCheckEmailTitle": "Проверьте свою почту",
- "authCheckEmailResendEmail": "Отправить письмо повторно",
- "authCheckEmailBackTo": "Назад к",
- "authSetNewPasswordTitle": "Установите новый пароль",
- "authSetNewPasswordNewPassword": "Новый пароль",
- "authSetNewPasswordBackTo": "Назад к",
- "authRegisterCreateAccount": "Создайте свою учетную запись, чтобы начать",
"distributedStatusHeaderText": "Охват реального времени и реального устройства",
"distributedStatusSubHeaderText": "Работает на миллионах устройств по всему миру, просматривайте производительность системы по глобальному региону, стране или городу",
"settingsGeneralSettings": "Общие настройки",
@@ -258,7 +238,6 @@
"country": "СТРАНА",
"city": "ГОРОД",
"response": "ОТВЕТ",
- "passwordreset": "Сброс пароля",
"monitorStatusUp": "Монитор {name} ({url}) теперь включен и отвечает",
"monitorStatusDown": "Монитор {name} ({url}) ОТКЛЮЧЕН и не отвечает",
"webhookSendSuccess": "Уведомление Webhook успешно отправлено",
@@ -531,7 +510,6 @@
"DeleteDescriptionText": "Это приведет к удалению учетной записи и всех связанных с ней данных с сервера. Это необратимо.",
"DeleteAccountWarning": "Удаление вашей учетной записи означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
"DeleteWarningTitle": "Действительно удаляете эту учетную запись?",
- "authRegisterEmail": "Email",
"bulkImport": {
"title": "Массовый импорт",
"selectFileTips": "Выберите CSV-файл для загрузки",
@@ -543,9 +521,6 @@
"noFileSelected": "Файл не выбран",
"fallbackPage": "Импортируйте файл для массовой загрузки списка серверов"
},
- "validationNameRequired": "Пожалуйста, введите свое имя",
- "validationNameTooLong": "Длина имени не должна превышать 50 символов",
- "validationNameInvalidCharacters": "Пожалуйста, используйте только буквы, пробелы, апострофы или дефисы",
"settingsSystemReset": "Сброс системы",
"settingsSystemResetDescription": "Удалите все мониторы из вашей системы.",
"DeleteAccountTitle": "Удалить аккаунт",
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 0d650e985..28b9bd346 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "Hesabınız yok mu",
- "forgotPassword": "Parolamı unuttum",
"submit": "Gönder",
"title": "Başlık",
- "enterEmail": "E-posta adresinizi girin",
- "commonPassword": "Parola",
- "authRegisterTitle": "Hesap oluştur",
- "authRegisterStepOneTitle": "Hesabınızı oluşturun",
- "authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin",
- "authRegisterStepTwoTitle": "Profilinizi ayarlayın",
- "authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin",
- "authRegisterStepThreeTitle": "Neredeyse bitti!",
- "authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin",
- "authForgotPasswordSendInstructions": "Talimatları gönder",
- "authForgotPasswordBackTo": "Geri dön",
- "authCheckEmailTitle": "E-postanızı kontrol edin",
- "authCheckEmailResendEmail": "E-postayı yeniden gönder",
- "authCheckEmailBackTo": "Geri dön",
- "authSetNewPasswordTitle": "Yeni şifre belirle",
- "authSetNewPasswordNewPassword": "Yeni şifre",
- "authSetNewPasswordBackTo": "Geri dön",
- "authRegisterCreateAccount": "Hesap oluşturmak için devam et",
"distributedStatusHeaderText": "Gerçek zamanlı, Gerçek cihazlar kapsamı",
"distributedStatusSubHeaderText": "Dünya çapında milyonlarca cihaz tarafından desteklenen sistem performansını küresel bölgeye, ülkeye veya şehre göre görüntüleyin",
"settingsGeneralSettings": "Genel ayarlar",
@@ -262,7 +242,6 @@
"country": "ÜLKE",
"city": "ŞEHİR",
"response": "YANIT",
- "passwordreset": "Parola Sıfırlama",
"monitorStatusUp": "Monitör {name} ({url}) ayakta ve yanıt veriyor",
"monitorStatusDown": "Monitör {name} ({url}) yanıt vermiyor",
"webhookSendSuccess": "Web kanca bildirimi başarıyla gönderildi",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "Bu, hesabı ve ilişkili tüm verileri sunucudan kaldıracaktır. Bu geri alınamaz.",
"DeleteAccountWarning": "Hesabınızı kaldırmanız, tekrar oturum açamayacağınız ve tüm verilerinizin silineceği anlamına gelir. Bu geri alınamaz.",
"DeleteWarningTitle": "Gerçekten bu hesabı silmek istiyor musunuz?",
- "authRegisterEmail": "E-posta",
"bulkImport": {
"title": "Toplu içeri alma",
"selectFileTips": "Yüklenecek CSV dosyasını seçin",
@@ -548,9 +526,6 @@
"noFileSelected": "Hiçbir dosya seçilmedi",
"fallbackPage": "Toplu olarak sunucu listesini yüklemek için bir dosyayı içe aktarın"
},
- "validationNameRequired": "Lütfen adınızı girin",
- "validationNameTooLong": "İsim 50 karakterden az olmalıdır",
- "validationNameInvalidCharacters": "Lütfen sadece harfler, boşluk, tırnak ve çizgi kullanın",
"settingsSystemReset": "Sistem sıfırla",
"settingsSystemResetDescription": "Sistemdeki tüm monitörleri silin",
"DeleteAccountTitle": "Hesabı sil",
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index 11c063580..2f4668548 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -181,28 +181,8 @@
}
}
},
- "dontHaveAccount": "沒有帳號",
- "forgotPassword": "忘記密碼",
"submit": "",
"title": "",
- "enterEmail": "輸入您的 E-mail",
- "commonPassword": "密碼",
- "authRegisterTitle": "建立帳號",
- "authRegisterStepOneTitle": "建立您的帳號",
- "authRegisterStepOneDescription": "清輸入您的個人檔案",
- "authRegisterStepTwoTitle": "設定您的個人檔案",
- "authRegisterStepTwoDescription": "請分享更多關於您的資訊",
- "authRegisterStepThreeTitle": "快完成了!",
- "authRegisterStepThreeDescription": "請確認您的資訊",
- "authForgotPasswordSendInstructions": "將說明發送至",
- "authForgotPasswordBackTo": "回去",
- "authCheckEmailTitle": "請查看您的電子郵件",
- "authCheckEmailResendEmail": "重新發送 E-mail",
- "authCheckEmailBackTo": "回去",
- "authSetNewPasswordTitle": "設定新密碼",
- "authSetNewPasswordNewPassword": "新密碼",
- "authSetNewPasswordBackTo": "回去",
- "authRegisterCreateAccount": "建立您的帳號開始使用",
"distributedStatusHeaderText": "實時、真實設備覆蓋",
"distributedStatusSubHeaderText": "由全球數百萬個設備提供支持,您可以按全球區域、國家或城市查看系統效能",
"settingsGeneralSettings": "一般設定",
@@ -262,7 +242,6 @@
"country": "",
"city": "",
"response": "",
- "passwordreset": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -536,7 +515,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterEmail": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -548,9 +526,6 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
"settingsSystemReset": "",
"settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
From 89bc2a9293ca15a099375bb68132f285d0400fd6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Michal=20=C5=A0mahel?=
<48548230+ceskyDJ@users.noreply.github.com>
Date: Sun, 8 Jun 2025 16:33:12 +0200
Subject: [PATCH 079/319] style: Reformat code
---
.../Components/TabPanels/Account/TeamPanel.jsx | 5 ++++-
client/src/Components/ThemeSwitch/index.jsx | 2 +-
client/src/Pages/Auth/ForgotPassword.jsx | 5 ++++-
client/src/Pages/Auth/NewPasswordConfirmed.jsx | 4 +++-
.../src/Pages/Auth/Register/StepThree/index.jsx | 8 +++++---
client/src/Pages/Auth/SetNewPassword.jsx | 16 +++++++++++-----
client/src/Pages/Settings/SettingsAbout.jsx | 4 +++-
client/src/Validation/validation.js | 8 +++-----
server/validation/joi.js | 10 ++--------
9 files changed, 36 insertions(+), 26 deletions(-)
diff --git a/client/src/Components/TabPanels/Account/TeamPanel.jsx b/client/src/Components/TabPanels/Account/TeamPanel.jsx
index 52e3ea9ee..341f84ba7 100644
--- a/client/src/Components/TabPanels/Account/TeamPanel.jsx
+++ b/client/src/Components/TabPanels/Account/TeamPanel.jsx
@@ -115,7 +115,10 @@ const TeamPanel = () => {
email: newEmail,
}));
- const validation = newOrChangedCredentials.validate({ email: newEmail }, { abortEarly: false });
+ const validation = newOrChangedCredentials.validate(
+ { email: newEmail },
+ { abortEarly: false }
+ );
setErrors((prev) => {
const updatedErrors = { ...prev };
diff --git a/client/src/Components/ThemeSwitch/index.jsx b/client/src/Components/ThemeSwitch/index.jsx
index d1a7661bc..5efb17419 100644
--- a/client/src/Components/ThemeSwitch/index.jsx
+++ b/client/src/Components/ThemeSwitch/index.jsx
@@ -14,7 +14,7 @@ import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";
-import {useTranslation} from "react-i18next";
+import { useTranslation } from "react-i18next";
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
const mode = useSelector((state) => state.ui.mode);
diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx
index 3ab6913dd..2a887756e 100644
--- a/client/src/Pages/Auth/ForgotPassword.jsx
+++ b/client/src/Pages/Auth/ForgotPassword.jsx
@@ -75,7 +75,10 @@ const ForgotPassword = () => {
const { value } = event.target;
setForm({ email: value });
- const { error } = newOrChangedCredentials.validate({ email: value }, { abortEarly: false });
+ const { error } = newOrChangedCredentials.validate(
+ { email: value },
+ { abortEarly: false }
+ );
if (error) setErrors({ email: error.details[0].message });
else delete errors.email;
diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
index aba1fefea..db03f7de3 100644
--- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -100,7 +100,9 @@ const NewPasswordConfirmed = () => {
{t("auth.forgotPassword.heading")}
- {t("auth.forgotPassword.subheadings.stepFour")}
+
+ {t("auth.forgotPassword.subheadings.stepFour")}
+
{
value={form.password}
onChange={handleChange}
error={errors.password ? true : false}
- helperText={errors.password === "auth.common.inputs.password.errors.empty"
- ? t(errors.password)
- : ""} // Other errors are related to required password conditions and are visualized below the input
+ helperText={
+ errors.password === "auth.common.inputs.password.errors.empty"
+ ? t(errors.password)
+ : ""
+ } // Other errors are related to required password conditions and are visualized below the input
endAdornment={}
/>
@@ -217,12 +219,16 @@ const SetNewPassword = () => {
variant={feedbacks.number}
/>
diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx
index fd11df3ad..6acadd444 100644
--- a/client/src/Pages/Settings/SettingsAbout.jsx
+++ b/client/src/Pages/Settings/SettingsAbout.jsx
@@ -20,7 +20,9 @@ const SettingsAbout = () => {
- {t("common.appName")} {2.1}
+
+ {t("common.appName")} {2.1}
+
{t("settingsDevelopedBy")}
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 1074f3bb2..2826605c8 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -105,11 +105,9 @@ const loginCredentials = joi.object({
"string.empty": "auth.common.inputs.email.errors.empty",
"string.email": "auth.common.inputs.email.errors.invalid",
}),
- password: joi
- .string()
- .messages({
- "string.empty": "auth.common.inputs.password.errors.empty",
- }),
+ password: joi.string().messages({
+ "string.empty": "auth.common.inputs.password.errors.empty",
+ }),
});
const monitorValidation = joi.object({
diff --git a/server/validation/joi.js b/server/validation/joi.js
index 9a3c61a7c..9024c26a4 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -22,14 +22,8 @@ const passwordPattern =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]+$/;
const loginValidation = joi.object({
- email: joi
- .string()
- .email()
- .required()
- .lowercase(),
- password: joi
- .string()
- .required(),
+ email: joi.string().email().required().lowercase(),
+ password: joi.string().required(),
});
const nameValidation = joi
.string()
From f445c2b310dd0059ac0f9485d5b1f7a3f32283a4 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 09:16:38 +0800
Subject: [PATCH 080/319] add empty query object to fix default destructuring
---
server/controllers/authController.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
index 9d5fe0c20..fdd589c1c 100755
--- a/server/controllers/authController.js
+++ b/server/controllers/authController.js
@@ -404,7 +404,8 @@ class AuthController {
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.db.getMonitorsByTeamId({
- params: { teamId: user.teamId },
+ query: {},
+ params: { teamId: user.teamId.toString() },
});
if (user.role.includes("superadmin")) {
From 711e85cb6e17792a9fe9c5649e7f98b5dcebd1e5 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 09:49:53 +0800
Subject: [PATCH 081/319] implement delete cascade
---
server/controllers/authController.js | 3 --
server/controllers/monitorController.js | 47 ++-----------------------
server/db/models/Monitor.js | 40 +++++++++++++++++++++
3 files changed, 42 insertions(+), 48 deletions(-)
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
index fdd589c1c..b05a9635a 100755
--- a/server/controllers/authController.js
+++ b/server/controllers/authController.js
@@ -414,9 +414,6 @@ class AuthController {
(await Promise.all(
result.monitors.map(async (monitor) => {
await this.jobQueue.deleteJob(monitor);
- await this.db.deleteChecks(monitor._id);
- await this.db.deletePageSpeedChecksByMonitorId(monitor._id);
- await this.db.deleteNotificationsByMonitorId(monitor._id);
})
));
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index 447027c57..00b525125 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -389,51 +389,8 @@ class MonitorController {
try {
const monitor = await this.db.deleteMonitor(req, res, next);
- // Delete associated checks,alerts,and notifications
-
- try {
- const operations = [
- { name: "deleteJob", fn: () => this.jobQueue.deleteJob(monitor) },
- { name: "deleteChecks", fn: () => this.db.deleteChecks(monitor._id) },
- {
- name: "deletePageSpeedChecks",
- fn: () => this.db.deletePageSpeedChecksByMonitorId(monitor._id),
- },
-
- {
- name: "deleteHardwareChecks",
- fn: () => this.db.deleteHardwareChecksByMonitorId(monitor._id),
- },
-
- // TODO We don't actually want to delete the status page if there are other monitors in it
- // We actually just want to remove the monitor being deleted from the status page.
- // Only delete he status page if there are no other monitors in it.
- {
- name: "deleteStatusPages",
- fn: () => this.db.deleteStatusPagesByMonitorId(monitor._id),
- },
- ];
- const results = await Promise.allSettled(operations.map((op) => op.fn()));
-
- results.forEach((result, index) => {
- if (result.status === "rejected") {
- const operationName = operations[index].name;
- logger.error({
- message: `Failed to ${operationName} for monitor ${monitor._id}`,
- service: SERVICE_NAME,
- method: "deleteMonitor",
- stack: result.reason.stack,
- });
- }
- });
- } catch (error) {
- logger.error({
- message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
- service: SERVICE_NAME,
- method: "deleteMonitor",
- stack: error.stack,
- });
- }
+ await this.jobQueue.deleteJob(monitor);
+ await this.db.deleteStatusPagesByMonitorId(monitor._id);
return res.success({ msg: this.stringService.monitorDelete });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js
index 3079abfba..26a79b815 100755
--- a/server/db/models/Monitor.js
+++ b/server/db/models/Monitor.js
@@ -1,4 +1,8 @@
import mongoose from "mongoose";
+import HardwareCheck from "./HardwareCheck.js";
+import PageSpeedCheck from "./PageSpeedCheck.js";
+import Check from "./Check.js";
+import MonitorStats from "./MonitorStats.js";
const MonitorSchema = mongoose.Schema(
{
@@ -88,6 +92,42 @@ const MonitorSchema = mongoose.Schema(
}
);
+MonitorSchema.pre("findOneAndDelete", async function (next) {
+ // Delete checks and stats
+ try {
+ const doc = await this.model.findOne(this.getFilter());
+
+ if (doc.type === "pagespeed") {
+ await PageSpeedCheck.deleteMany({ monitorId: doc._id });
+ } else if (doc.type === "hardware") {
+ await HardwareCheck.deleteMany({ monitorId: doc._id });
+ } else {
+ await Check.deleteMany({ monitorId: doc._id });
+ }
+
+ await MonitorStats.deleteMany({ monitorId: doc._id.toString() });
+ next();
+ } catch (error) {
+ next(error);
+ }
+});
+
+MonitorSchema.pre("deleteMany", async function (next) {
+ const filter = this.getFilter();
+ const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
+ for (const monitor of monitors) {
+ if (monitor.type === "pagespeed") {
+ await PageSpeedCheck.deleteMany({ monitorId: monitor._id });
+ } else if (monitor.type === "hardware") {
+ await HardwareCheck.deleteMany({ monitorId: monitor._id });
+ } else {
+ await Check.deleteMany({ monitorId: monitor._id });
+ }
+ await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
+ }
+ next();
+});
+
MonitorSchema.index({ teamId: 1, type: 1 });
export default mongoose.model("Monitor", MonitorSchema);
From 76de8f11f277eba32d30051e44465fb8cb8eafae Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 09:53:17 +0800
Subject: [PATCH 082/319] formatting
---
client/src/Components/Charts/ChartBox/index.jsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/Charts/ChartBox/index.jsx
index cde6e02a0..2c13ece87 100644
--- a/client/src/Components/Charts/ChartBox/index.jsx
+++ b/client/src/Components/Charts/ChartBox/index.jsx
@@ -43,7 +43,6 @@ const ChartBox = ({
>
Date: Tue, 10 Jun 2025 11:13:19 +0800
Subject: [PATCH 083/319] implement editing notification channels
---
client/src/Hooks/useNotifications.js | 72 ++++++++++++++++++-
.../Login/Components/ForgotPasswordLabel.jsx | 4 +-
.../Hooks/useHardwareMonitorsFetch.jsx | 7 +-
.../Notifications/components/ActionMenu.jsx | 70 ++++++++++++++++++
.../src/Pages/Notifications/create/index.jsx | 33 ++++++---
client/src/Pages/Notifications/index.jsx | 15 ++--
client/src/Pages/Notifications/utils.js | 6 ++
client/src/Routes/index.jsx | 6 ++
client/src/Utils/NetworkService.js | 10 +++
client/src/locales/en.json | 5 ++
server/controllers/notificationController.js | 33 +++++++++
server/db/mongo/modules/notificationModule.js | 26 +++++++
server/routes/notificationRoute.js | 3 +
13 files changed, 265 insertions(+), 25 deletions(-)
create mode 100644 client/src/Pages/Notifications/components/ActionMenu.jsx
create mode 100644 client/src/Pages/Notifications/utils.js
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index 4ece55acd..c8dc6b3db 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -4,6 +4,7 @@ import { networkService } from "../main";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
+import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils";
const useCreateNotification = () => {
const navigate = useNavigate();
@@ -88,4 +89,73 @@ const useDeleteNotification = () => {
return [deleteNotification, isLoading, error];
};
-export { useCreateNotification, useGetNotificationsByTeamId, useDeleteNotification };
+
+const useGetNotificationById = (id, setNotification) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const getNotificationById = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getNotificationById({ id });
+
+ const notification = response?.data?.data ?? null;
+
+ const notificationData = {
+ userId: notification?.userId,
+ teamId: notification?.teamId,
+ address: notification?.address,
+ notificationName: notification?.notificationName,
+ type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
+ config: notification?.config,
+ };
+
+ setNotification(notificationData);
+ } catch (error) {
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ }, [id, setNotification]);
+
+ useEffect(() => {
+ if (id) {
+ getNotificationById();
+ }
+ }, [getNotificationById, id]);
+
+ return [isLoading, error];
+};
+
+const useEditNotification = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const { t } = useTranslation();
+
+ const editNotification = async (id, notification) => {
+ try {
+ setIsLoading(true);
+ await networkService.editNotification({ id, notification });
+ createToast({
+ body: t("notifications.edit.success"),
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.edit.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [editNotification, isLoading, error];
+};
+
+export {
+ useCreateNotification,
+ useGetNotificationsByTeamId,
+ useDeleteNotification,
+ useGetNotificationById,
+ useEditNotification,
+};
diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
index 5fc2430c7..fe8c688ac 100644
--- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
+++ b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
@@ -42,8 +42,8 @@ const ForgotPasswordLabel = ({ email, errorEmail }) => {
};
ForgotPasswordLabel.propTypes = {
- email: PropTypes.string.isRequired,
- errorEmail: PropTypes.string.isRequired,
+ email: PropTypes.string,
+ errorEmail: PropTypes.string,
};
export default ForgotPasswordLabel;
diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
index f7cb2abb1..c0b61ef62 100644
--- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
+++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
@@ -3,9 +3,7 @@ import { networkService } from "../../../../main";
const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
// Abort early if creating monitor
- if (!monitorId) {
- return { monitor: undefined, isLoading: false, networkError: undefined };
- }
+
const [isLoading, setIsLoading] = useState(true);
const [networkError, setNetworkError] = useState(false);
const [monitor, setMonitor] = useState(undefined);
@@ -13,6 +11,9 @@ const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
useEffect(() => {
const fetchData = async () => {
try {
+ if (!monitorId) {
+ return { monitor: undefined, isLoading: false, networkError: undefined };
+ }
const response = await networkService.getHardwareDetailsByMonitorId({
monitorId: monitorId,
dateRange: dateRange,
diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx
new file mode 100644
index 000000000..bd719bfee
--- /dev/null
+++ b/client/src/Pages/Notifications/components/ActionMenu.jsx
@@ -0,0 +1,70 @@
+// Components
+import Menu from "@mui/material/Menu";
+import IconButton from "@mui/material/IconButton";
+import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
+import MenuItem from "@mui/material/MenuItem";
+
+// Utils
+import { useState } from "react";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
+import PropTypes from "prop-types";
+
+const ActionMenu = ({ notification, onDelete }) => {
+ const theme = useTheme();
+ const navigate = useNavigate();
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+
+ // Handlers
+ const handleClick = (event) => {
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleRemove = () => {
+ onDelete(notification._id);
+ handleClose();
+ };
+
+ const handleConfigure = () => {
+ navigate(`/notifications/${notification._id}`);
+ handleClose();
+ };
+
+ return (
+ <>
+
+
+
+
+
+ >
+ );
+};
+
+ActionMenu.propTypes = {
+ notification: PropTypes.object,
+ onDelete: PropTypes.func,
+};
+
+export default ActionMenu;
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx
index b1469bff0..b123dcbd7 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -12,7 +12,11 @@ import TextInput from "../../../Components/Inputs/TextInput";
import { useState } from "react";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
-import { useCreateNotification } from "../../../Hooks/useNotifications";
+import {
+ useCreateNotification,
+ useGetNotificationById,
+ useEditNotification,
+} from "../../../Hooks/useNotifications";
import {
notificationEmailValidation,
notificationWebhookValidation,
@@ -20,19 +24,17 @@ import {
} from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
+import { useParams } from "react-router-dom";
+import { NOTIFICATION_TYPES } from "../utils";
// Setup
-const NOTIFICATION_TYPES = [
- { _id: 1, name: "E-mail", value: "email" },
- { _id: 2, name: "Slack", value: "webhook" },
- { _id: 3, name: "PagerDuty", value: "pager_duty" },
- { _id: 4, name: "Webhook", value: "webhook" },
-];
-
const CreateNotifications = () => {
+ const { notificationId } = useParams();
const theme = useTheme();
- const [createNotification, isLoading, error] = useCreateNotification();
+ const [createNotification, isCreating, createNotificationError] =
+ useCreateNotification();
+ const [editNotification, isEditing, editNotificationError] = useEditNotification();
const BREADCRUMBS = [
{ name: "notifications", path: "/notifications" },
{ name: "create", path: "/notifications/create" },
@@ -57,6 +59,11 @@ const CreateNotifications = () => {
const [errors, setErrors] = useState({});
const { t } = useTranslation();
+ const [notificationIsLoading, getNotificationError] = useGetNotificationById(
+ notificationId,
+ setNotification
+ );
+
// handlers
const onSubmit = (e) => {
e.preventDefault();
@@ -106,7 +113,11 @@ const CreateNotifications = () => {
return;
}
- createNotification(form);
+ if (notificationId) {
+ editNotification(notificationId, form);
+ } else {
+ createNotification(form);
+ }
};
const onChange = (e) => {
@@ -348,7 +359,7 @@ const CreateNotifications = () => {
justifyContent="flex-end"
>
{
const navigate = useNavigate();
@@ -70,13 +72,10 @@ const Notifications = () => {
content: "Actions",
render: (row) => {
return (
- onDelete(row._id)}
- >
- Delete
-
+
);
},
},
diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js
new file mode 100644
index 000000000..7135e5c63
--- /dev/null
+++ b/client/src/Pages/Notifications/utils.js
@@ -0,0 +1,6 @@
+export const NOTIFICATION_TYPES = [
+ { _id: 1, name: "E-mail", value: "email" },
+ { _id: 2, name: "Slack", value: "webhook" },
+ { _id: 3, name: "PagerDuty", value: "pager_duty" },
+ { _id: 4, name: "Webhook", value: "webhook" },
+];
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 48343cd22..3f815a665 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -157,6 +157,12 @@ const Routes = () => {
path="notifications/create"
element={}
/>
+
+ }
+ />
+
}
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 26c6bd460..56ec4dcb7 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1049,6 +1049,16 @@ class NetworkService {
const { id } = config;
return this.axiosInstance.delete(`/notifications/${id}`);
}
+
+ async getNotificationById(config) {
+ const { id } = config;
+ return this.axiosInstance.get(`/notifications/${id}`);
+ }
+
+ async editNotification(config) {
+ const { id, notification } = config;
+ return this.axiosInstance.put(`/notifications/${id}`, notification);
+ }
}
export default NetworkService;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 5510e5c54..92f490aba 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -293,6 +293,7 @@
"webhookPlaceholder": "https://your-server.com/webhook"
}
},
+
"notificationConfig": {
"title": "Notifications",
"description": "Select the notifications channels you want to use"
@@ -354,6 +355,10 @@
"delete": {
"success": "Notification deleted successfully",
"failed": "Failed to delete notification"
+ },
+ "edit": {
+ "success": "Notification updated successfully",
+ "failed": "Failed to update notification"
}
},
"testLocale": "testLocale",
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index 7cb642977..df8fe8ca3 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -217,6 +217,39 @@ class NotificationController {
next(handleError(error, SERVICE_NAME, "deleteNotification"));
}
};
+
+ getNotificationById = async (req, res, next) => {
+ try {
+ const notification = await this.db.getNotificationById(req.params.id);
+ return res.success({
+ msg: "Notification fetched successfully",
+ data: notification,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "getNotificationById"));
+ }
+ };
+
+ editNotification = async (req, res, next) => {
+ try {
+ await createNotificationBodyValidation.validateAsync(req.body, {
+ abortEarly: false,
+ });
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
+
+ try {
+ const notification = await this.db.editNotification(req.params.id, req.body);
+ return res.success({
+ msg: "Notification updated successfully",
+ data: notification,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "editNotification"));
+ }
+ };
}
export default NotificationController;
diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js
index d172ee8cf..08d57b1a9 100755
--- a/server/db/mongo/modules/notificationModule.js
+++ b/server/db/mongo/modules/notificationModule.js
@@ -84,6 +84,30 @@ const deleteNotificationById = async (id) => {
}
};
+const getNotificationById = async (id) => {
+ try {
+ const notification = await Notification.findById(id);
+ return notification;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "getNotificationById";
+ throw error;
+ }
+};
+
+const editNotification = async (id, notificationData) => {
+ try {
+ const notification = await Notification.findByIdAndUpdate(id, notificationData, {
+ new: true,
+ });
+ return notification;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "editNotification";
+ throw error;
+ }
+};
+
export {
createNotification,
getNotificationsByTeamId,
@@ -91,4 +115,6 @@ export {
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
deleteNotificationById,
+ getNotificationById,
+ editNotification,
};
diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js
index fe6b1c2dd..aa8433e53 100755
--- a/server/routes/notificationRoute.js
+++ b/server/routes/notificationRoute.js
@@ -23,6 +23,9 @@ class NotificationRoutes {
);
this.router.delete("/:id", this.notificationController.deleteNotification);
+
+ this.router.get("/:id", this.notificationController.getNotificationById);
+ this.router.put("/:id", this.notificationController.editNotification);
}
getRouter() {
From 910bf4c72e652a741c8276b5713c1a418c35e39e Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 11:16:39 +0800
Subject: [PATCH 084/319] formatting
---
client/src/Components/Charts/ChartBox/index.jsx | 1 -
1 file changed, 1 deletion(-)
diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/Charts/ChartBox/index.jsx
index cde6e02a0..2c13ece87 100644
--- a/client/src/Components/Charts/ChartBox/index.jsx
+++ b/client/src/Components/Charts/ChartBox/index.jsx
@@ -43,7 +43,6 @@ const ChartBox = ({
>
Date: Tue, 10 Jun 2025 11:22:24 +0800
Subject: [PATCH 085/319] update icon
---
client/src/Components/Sidebar/index.jsx | 8 +++-----
client/src/assets/icons/notifications.svg | 5 +++++
2 files changed, 8 insertions(+), 5 deletions(-)
create mode 100644 client/src/assets/icons/notifications.svg
diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx
index 831782b53..6be67a43d 100644
--- a/client/src/Components/Sidebar/index.jsx
+++ b/client/src/Components/Sidebar/index.jsx
@@ -38,7 +38,7 @@ import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
-import NotificationAddOutlinedIcon from "@mui/icons-material/NotificationAddOutlined";
+import Notifications from "../../assets/icons/notifications.svg?react";
import "./index.css";
@@ -59,7 +59,7 @@ const getMenu = (t) => [
{
name: t("menu.notifications"),
path: "notifications",
- icon: ,
+ icon: ,
},
{ name: t("menu.incidents"), path: "incidents", icon: },
@@ -129,9 +129,7 @@ function Sidebar() {
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const { user } = useSelector((state) => state.auth);
- const distributedUptimeEnabled = useSelector(
- (state) => state.ui.distributedUptimeEnabled
- );
+
const sidebarRef = useRef(null);
const [sidebarReady, setSidebarReady] = useState(false);
const TRANSITION_DURATION = 200;
diff --git a/client/src/assets/icons/notifications.svg b/client/src/assets/icons/notifications.svg
new file mode 100644
index 000000000..c09ac5746
--- /dev/null
+++ b/client/src/assets/icons/notifications.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
From 1ed317d0cb83d7a03c8b21b68ab1311cfbad4cbf Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 11:29:03 +0800
Subject: [PATCH 086/319] update fallback
---
client/src/Pages/Notifications/index.jsx | 2 +-
client/src/locales/en.json | 6 +++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx
index b90ff22cc..41cefda76 100644
--- a/client/src/Pages/Notifications/index.jsx
+++ b/client/src/Pages/Notifications/index.jsx
@@ -86,7 +86,7 @@ const Notifications = () => {
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 92f490aba..27b296556 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -301,7 +301,11 @@
"notifications": {
"fallback": {
"title": "notification channel",
- "checks": "Alert teams about downtime or performance issues"
+ "checks": [
+ "Alert teams about downtime or performance issues",
+ "Let engineers know when incidents happen",
+ "Keep administrators informed of system changes"
+ ]
},
"createButton": "Create notification channel",
"createTitle": "Notification channel",
From e6493df881bee71178f93c7169a3cc9385c3d828 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 12:07:31 +0800
Subject: [PATCH 087/319] add test notificaiton button and methods
---
client/src/Hooks/useNotifications.js | 26 +++++
.../src/Pages/Notifications/create/index.jsx | 62 ++++++++++++
client/src/Pages/Notifications/index.jsx | 11 ++-
client/src/Utils/NetworkService.js | 15 +--
client/src/locales/en.json | 4 +
server/controllers/notificationController.js | 94 ++++---------------
server/routes/notificationRoute.js | 2 +-
server/service/notificationService.js | 11 +++
8 files changed, 135 insertions(+), 90 deletions(-)
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index c8dc6b3db..8e7f896e9 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -152,10 +152,36 @@ const useEditNotification = () => {
return [editNotification, isLoading, error];
};
+const useTestNotification = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const { t } = useTranslation();
+
+ const testNotification = async (notification) => {
+ try {
+ setIsLoading(true);
+ await networkService.testNotification({ notification });
+ createToast({
+ body: t("notifications.test.success"),
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.test.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [testNotification, isLoading, error];
+};
+
export {
useCreateNotification,
useGetNotificationsByTeamId,
useDeleteNotification,
useGetNotificationById,
useEditNotification,
+ useTestNotification,
};
diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx
index b123dcbd7..19335c790 100644
--- a/client/src/Pages/Notifications/create/index.jsx
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -16,6 +16,7 @@ import {
useCreateNotification,
useGetNotificationById,
useEditNotification,
+ useTestNotification,
} from "../../../Hooks/useNotifications";
import {
notificationEmailValidation,
@@ -35,6 +36,8 @@ const CreateNotifications = () => {
const [createNotification, isCreating, createNotificationError] =
useCreateNotification();
const [editNotification, isEditing, editNotificationError] = useEditNotification();
+ const [testNotification, isTesting, testNotificationError] = useTestNotification();
+
const BREADCRUMBS = [
{ name: "notifications", path: "/notifications" },
{ name: "create", path: "/notifications/create" },
@@ -178,6 +181,56 @@ const CreateNotifications = () => {
setNotification(newNotification);
};
+ const onTestNotification = () => {
+ const form = {
+ ...notification,
+ type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
+ };
+
+ if (notification.type === 2) {
+ form.type = "webhook";
+ }
+
+ let error = null;
+
+ if (form.type === "email") {
+ error = notificationEmailValidation.validate(
+ { notificationName: form.notificationName, address: form.address },
+ { abortEarly: false }
+ ).error;
+ } else if (form.type === "webhook") {
+ form.config = {
+ platform: form.config.platform,
+ webhookUrl: form.config.webhookUrl,
+ };
+ error = notificationWebhookValidation.validate(
+ { notificationName: form.notificationName, config: form.config },
+ { abortEarly: false }
+ ).error;
+ } else if (form.type === "pager_duty") {
+ form.config = {
+ platform: form.config.platform,
+ routingKey: form.config.routingKey,
+ };
+ error = notificationPagerDutyValidation.validate(
+ { notificationName: form.notificationName, config: form.config },
+ { abortEarly: false }
+ ).error;
+ }
+
+ if (error) {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ createToast({ body: "Please check the form for errors." });
+ setErrors(newErrors);
+ return;
+ }
+
+ testNotification(form);
+ };
+
return (
@@ -357,7 +410,16 @@ const CreateNotifications = () => {
+
+ Test notification
+
{
id: "target",
content: "Target",
render: (row) => {
- return row.address || row.config?.webhookUrl || row.config?.routingKey;
+ const type = row.type;
+ if (type === "email") {
+ return row.address;
+ }
+ if (type === "webhook") {
+ return row.config?.webhookUrl;
+ }
+ if (type === "pager_duty") {
+ return row.config?.routingKey;
+ }
},
},
{
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 56ec4dcb7..3d124a3e6 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -703,18 +703,11 @@ class NetworkService {
* @returns {Promise} The response from the axios POST request.
*/
async testNotification(config) {
- return this.axiosInstance.post(
- "/notifications/test-webhook",
- {
- platform: config.platform,
- ...config.payload,
+ return this.axiosInstance.post("/notifications/test", config.notification, {
+ headers: {
+ "Content-Type": "application/json",
},
- {
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
+ });
}
/**
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 27b296556..4b79b88d5 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -363,6 +363,10 @@
"edit": {
"success": "Notification updated successfully",
"failed": "Failed to update notification"
+ },
+ "test": {
+ "success": "Test notification sent successfully",
+ "failed": "Failed to send test notification"
}
},
"testLocale": "testLocale",
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index df8fe8ca3..edb18adfb 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -24,7 +24,6 @@ class NotificationController {
this.statusService = statusService;
this.db = db;
this.triggerNotification = this.triggerNotification.bind(this);
- this.testWebhook = this.testWebhook.bind(this);
}
async triggerNotification(req, res, next) {
@@ -85,94 +84,35 @@ class NotificationController {
};
}
- handleTelegramTest(botToken, chatId) {
- if (!botToken || !chatId) {
- return {
- isValid: false,
- error: {
- msg: this.stringService.telegramRequiresBotTokenAndChatId,
- status: 400,
- },
- };
- }
-
- return {
- isValid: true,
- notification: {
- type: NOTIFICATION_TYPES.WEBHOOK,
- platform: PLATFORMS.TELEGRAM,
- config: { botToken, chatId },
- },
- };
- }
-
- handleWebhookTest(webhookUrl, platform) {
- if (webhookUrl === null) {
- return {
- isValid: false,
- error: {
- msg: this.stringService.webhookUrlRequired,
- status: 400,
- },
- };
- }
-
- return {
- isValid: true,
- notification: {
- type: NOTIFICATION_TYPES.WEBHOOK,
- platform: platform,
- config: { webhookUrl },
- },
- };
- }
-
- async testWebhook(req, res, next) {
+ testNotification = async (req, res, next) => {
+ console.log("TESTING");
try {
- const { webhookUrl, platform, botToken, chatId } = req.body;
+ const notification = req.body;
- if (platform === null) {
- return res.error({
- msg: this.stringService.platformRequired,
- status: 400,
+ if (notification?.type === "email") {
+ console.log("HANDLE EMAIL");
+ return res.success({
+ msg: this.stringService.emailSendSuccess,
});
}
- // Platform-specific handling
- const platformHandlers = {
- [PLATFORMS.TELEGRAM]: () => this.handleTelegramTest(botToken, chatId),
- // Default handler for webhook-based platforms (Slack, Discord, etc.)
- default: () => this.handleWebhookTest(webhookUrl, platform),
- };
-
- const handler = platformHandlers[platform] || platformHandlers.default;
- const handlerResult = handler();
-
- if (!handlerResult.isValid) {
- return res.error(handlerResult.error);
- }
-
- const networkResponse = this.createTestNetworkResponse();
-
- const result = await this.notificationService.sendWebhookNotification(
- networkResponse,
- handlerResult.notification
- );
-
- if (result && result !== false) {
+ if (notification?.type === "webhook") {
+ const success =
+ await this.notificationService.sendTestWebhookNotification(notification);
+ if (!success) {
+ return res.error({
+ msg: this.stringService.webhookSendFailed,
+ status: 400,
+ });
+ }
return res.success({
msg: this.stringService.webhookSendSuccess,
});
- } else {
- return res.error({
- msg: this.stringService.testNotificationFailed,
- status: 400,
- });
}
} catch (error) {
next(handleError(error, SERVICE_NAME, "testWebhook"));
}
- }
+ };
createNotification = async (req, res, next) => {
try {
diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js
index aa8433e53..064cd4b5d 100755
--- a/server/routes/notificationRoute.js
+++ b/server/routes/notificationRoute.js
@@ -13,7 +13,7 @@ class NotificationRoutes {
this.router.post("/trigger", this.notificationController.triggerNotification);
- this.router.post("/test-webhook", this.notificationController.testWebhook);
+ this.router.post("/test", this.notificationController.testNotification);
this.router.post("/", this.notificationController.createNotification);
diff --git a/server/service/notificationService.js b/server/service/notificationService.js
index 878881334..56c6d2c1c 100755
--- a/server/service/notificationService.js
+++ b/server/service/notificationService.js
@@ -95,6 +95,17 @@ class NotificationService {
return MESSAGE_FORMATTERS[platform](messageText, chatId);
}
+ sendTestWebhookNotification = async (notification) => {
+ const config = notification.config;
+ const response = await this.networkService.requestWebhook(
+ config.platform,
+ config.webhookUrl,
+ "This is a test notification"
+ );
+
+ return response.status;
+ };
+
/**
* Sends a webhook notification to a specified platform.
*
From c20b8c665af8632ca232c1b40a9a0f2352e87b5f Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 13:20:44 +0800
Subject: [PATCH 088/319] add tests notification for email
---
server/controllers/notificationController.js | 2 +-
server/service/notificationService.js | 18 ++++++++++++++++++
2 files changed, 19 insertions(+), 1 deletion(-)
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index edb18adfb..df16f7a75 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -90,7 +90,7 @@ class NotificationController {
const notification = req.body;
if (notification?.type === "email") {
- console.log("HANDLE EMAIL");
+ const result = await this.notificationService.sendTestEmail(notification);
return res.success({
msg: this.stringService.emailSendSuccess,
});
diff --git a/server/service/notificationService.js b/server/service/notificationService.js
index 56c6d2c1c..30c8474c1 100755
--- a/server/service/notificationService.js
+++ b/server/service/notificationService.js
@@ -196,6 +196,24 @@ class NotificationService {
return true;
}
+ sendTestEmail = async (notification) => {
+ const to = notification?.address;
+ if (!to || typeof to !== "string") {
+ throw new Error(this.stringService.errorForValidEmailAddress);
+ }
+
+ const subject = this.stringService.testEmailSubject;
+ const context = { testName: "Monitoring System" };
+
+ const messageId = await this.emailService.buildAndSendEmail(
+ "testEmailTemplate",
+ context,
+ to,
+ subject
+ );
+ console.log(messageId);
+ };
+
/**
* Sends an email notification about monitor status change
*
From 08875b07ecb07cc3ceb233304c1a526402528cfe Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 13:44:15 +0800
Subject: [PATCH 089/319] add pagerduty
---
server/controllers/notificationController.js | 31 +++++++++++---------
server/service/networkService.js | 23 +++++++++++++++
server/service/notificationService.js | 19 ++++++++++--
3 files changed, 57 insertions(+), 16 deletions(-)
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index df16f7a75..4efcf4988 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -85,30 +85,33 @@ class NotificationController {
}
testNotification = async (req, res, next) => {
- console.log("TESTING");
try {
const notification = req.body;
+ let success;
if (notification?.type === "email") {
- const result = await this.notificationService.sendTestEmail(notification);
- return res.success({
- msg: this.stringService.emailSendSuccess,
- });
+ success = await this.notificationService.sendTestEmail(notification);
}
if (notification?.type === "webhook") {
- const success =
+ success =
await this.notificationService.sendTestWebhookNotification(notification);
- if (!success) {
- return res.error({
- msg: this.stringService.webhookSendFailed,
- status: 400,
- });
- }
- return res.success({
- msg: this.stringService.webhookSendSuccess,
+ }
+
+ if (notification?.type === "pager_duty") {
+ success =
+ await this.notificationService.sendTestPagerDutyNotification(notification);
+ }
+ if (!success) {
+ return res.error({
+ msg: "Sending notification failed",
+ status: 400,
});
}
+
+ return res.success({
+ msg: "Notification sent successfully",
+ });
} catch (error) {
next(handleError(error, SERVICE_NAME, "testWebhook"));
}
diff --git a/server/service/networkService.js b/server/service/networkService.js
index 5ceddb8fd..e04d69c41 100755
--- a/server/service/networkService.js
+++ b/server/service/networkService.js
@@ -463,6 +463,29 @@ class NetworkService {
}
}
+ async requestPagerDuty({ message, routingKey, monitorUrl }) {
+ try {
+ const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, {
+ routing_key: routingKey,
+ event_action: "trigger",
+ payload: {
+ summary: message,
+ severity: "critical",
+ source: monitorUrl,
+ timestamp: new Date().toISOString(),
+ },
+ });
+
+ if (response?.data?.status !== "success") return false;
+ return true;
+ } catch (error) {
+ error.details = error.response?.data;
+ error.service = this.SERVICE_NAME;
+ error.method = "requestPagerDuty";
+ throw error;
+ }
+ }
+
/**
* Gets the status of a job based on its type and returns the appropriate response.
*
diff --git a/server/service/notificationService.js b/server/service/notificationService.js
index 30c8474c1..6e12722c2 100755
--- a/server/service/notificationService.js
+++ b/server/service/notificationService.js
@@ -211,7 +211,11 @@ class NotificationService {
to,
subject
);
- console.log(messageId);
+
+ if (messageId) {
+ return true;
+ }
+ return false;
};
/**
@@ -223,6 +227,7 @@ class NotificationService {
* @param {string} address - Email address to send the notification to
* @returns {Promise} - Indicates email was sent successfully
*/
+
async sendEmail(networkResponse, address) {
const { monitor, status, prevStatus } = networkResponse;
const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
@@ -232,6 +237,16 @@ class NotificationService {
return true;
}
+ async sendTestPagerDutyNotification(notification) {
+ const { routingKey } = notification.config;
+ const response = await this.networkService.requestPagerDuty({
+ message: "This is a test notification",
+ monitorUrl: "Test notification",
+ routingKey,
+ });
+
+ return response;
+ }
async sendPagerDutyNotification(networkResponse, notification) {
const { monitor, status, code } = networkResponse;
const { routingKey, platform } = notification.config;
@@ -251,7 +266,7 @@ class NotificationService {
routingKey,
monitorUrl: monitor.url,
});
- return response.status;
+ return response;
} catch (error) {
this.logger.error({
message: "Failed to send PagerDuty notification",
From 3224feeb9e52f383bf243d87ecb9bd1cdf8aacc5 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 13:57:00 +0800
Subject: [PATCH 090/319] fix duplicate strings, remove uptime percentage
---
.../Details/Components/StatusBoxes/index.jsx | 6 +++---
client/src/locales/en.json | 8 +++-----
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
index 447831acb..66483d448 100644
--- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
@@ -14,7 +14,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
const { determineState } = useUtils();
const { t } = useTranslation();
- const { stats, uptimePercentage } = monitor ?? {};
+ const { stats } = monitor ?? {};
const latestCheck = stats?.aggregateData?.latestCheck;
// Get data from latest check
@@ -95,7 +95,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
heading={t("disk")}
subHeading={formatBytes(diskTotalBytes)}
/>
-
@@ -103,7 +103,7 @@ const InfraStatBoxes = ({ shouldRender, monitor }) => {
%
>
}
- />
+ /> */}
to get the full container ID.",
"port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard."
- },
- "monitorStatus": {
- "checkingEvery": "Checking every {{interval}}",
- "withCaptureAgent": "with Capture agent {{version}}"
}
}
From 7587fea6d5b0eab4fd4c5fdd7bb6c3b0735a582a Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 14:03:49 +0800
Subject: [PATCH 091/319] Add row click handling to notificaitons table
---
client/src/Hooks/useNotifications.js | 3 ++-
client/src/Pages/Notifications/index.jsx | 10 ++++++++++
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
index 8e7f896e9..54e3d1bd9 100644
--- a/client/src/Hooks/useNotifications.js
+++ b/client/src/Hooks/useNotifications.js
@@ -131,7 +131,7 @@ const useEditNotification = () => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
const { t } = useTranslation();
-
+ const navigate = useNavigate();
const editNotification = async (id, notification) => {
try {
setIsLoading(true);
@@ -139,6 +139,7 @@ const useEditNotification = () => {
createToast({
body: t("notifications.edit.success"),
});
+ navigate(`/notifications`);
} catch (error) {
setError(error);
createToast({
diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx
index 2694a3fea..9ad5f1578 100644
--- a/client/src/Pages/Notifications/index.jsx
+++ b/client/src/Pages/Notifications/index.jsx
@@ -119,6 +119,16 @@ const Notifications = () => {
{t("notifications.createTitle")}
navigate(`/notifications/${row._id}`),
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
headers={headers}
data={notifications}
/>
From e44114b5e5cfd16cb817b5d3aba2be635a0ebf44 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Tue, 10 Jun 2025 14:19:57 +0800
Subject: [PATCH 092/319] fix image name
---
docker/dist/docker-compose.yaml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docker/dist/docker-compose.yaml b/docker/dist/docker-compose.yaml
index a0d13d061..580f21e97 100755
--- a/docker/dist/docker-compose.yaml
+++ b/docker/dist/docker-compose.yaml
@@ -1,6 +1,6 @@
services:
client:
- image: ghcr.io/bluewave-labs/checkmate-frontend:latest
+ image: ghcr.io/bluewave-labs/checkmate-client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1"
From 18ef81f6f15e0e37a23163a46c8e8e975e30c14b Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 10:20:14 +0800
Subject: [PATCH 093/319] refactor login page
---
.../Pages/Auth/Login/Components/EmailStep.jsx | 106 -------
.../Login/Components/ForgotPasswordLabel.jsx | 49 ----
.../Auth/Login/Components/PasswordStep.jsx | 141 ----------
client/src/Pages/Auth/Login/Login.jsx | 266 ------------------
client/src/Pages/Auth/Login/index.jsx | 162 +++++++++++
5 files changed, 162 insertions(+), 562 deletions(-)
delete mode 100644 client/src/Pages/Auth/Login/Components/EmailStep.jsx
delete mode 100644 client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
delete mode 100644 client/src/Pages/Auth/Login/Components/PasswordStep.jsx
delete mode 100644 client/src/Pages/Auth/Login/Login.jsx
create mode 100644 client/src/Pages/Auth/Login/index.jsx
diff --git a/client/src/Pages/Auth/Login/Components/EmailStep.jsx b/client/src/Pages/Auth/Login/Components/EmailStep.jsx
deleted file mode 100644
index f3f56ec26..000000000
--- a/client/src/Pages/Auth/Login/Components/EmailStep.jsx
+++ /dev/null
@@ -1,106 +0,0 @@
-import { useRef, useEffect } from "react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-/**
- * Renders the email step of the login process which includes an email field.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const EmailStep = ({ form, errors, onSubmit, onChange }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- {t("auth.login.heading")}
- {t("auth.login.subheadings.stepOne")}
-
-
-
-
-
- {t("auth.common.navigation.continue")}
-
-
-
-
- >
- );
-};
-
-EmailStep.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
-};
-
-export default EmailStep;
diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
deleted file mode 100644
index fe8c688ac..000000000
--- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Box, Typography, useTheme } from "@mui/material";
-import PropTypes from "prop-types";
-import { useNavigate } from "react-router";
-import { Trans, useTranslation } from "react-i18next";
-
-const ForgotPasswordLabel = ({ email, errorEmail }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- const handleNavigate = () => {
- if (email !== "" && !errorEmail) {
- sessionStorage.setItem("email", email);
- }
- navigate("/forgot-password");
- };
-
- return (
-
-
-
- ),
- }}
- />
-
-
- );
-};
-
-ForgotPasswordLabel.propTypes = {
- email: PropTypes.string,
- errorEmail: PropTypes.string,
-};
-
-export default ForgotPasswordLabel;
diff --git a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/client/src/Pages/Auth/Login/Components/PasswordStep.jsx
deleted file mode 100644
index 5aa56fbb0..000000000
--- a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx
+++ /dev/null
@@ -1,141 +0,0 @@
-import { useRef, useEffect } from "react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { useSelector } from "react-redux";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-/**
- * Renders the password step of the login process, including a password input field.
- *
- * @param {Object} props
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => {
- const theme = useTheme();
- const inputRef = useRef(null);
- const authState = useSelector((state) => state.auth);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- {t("auth.login.heading")}
- {t("auth.login.subheadings.stepTwo")}
-
-
- }
- />
-
-
-
- {t("auth.common.navigation.back")}{" "}
-
-
- {t("auth.common.navigation.continue")}
-
-
-
-
- >
- );
-};
-
-PasswordStep.propTypes = {
- form: PropTypes.object.isRequired,
- errors: PropTypes.object.isRequired,
- onSubmit: PropTypes.func.isRequired,
- onChange: PropTypes.func.isRequired,
- onBack: PropTypes.func.isRequired,
-};
-
-export default PasswordStep;
diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx
deleted file mode 100644
index d2be3af35..000000000
--- a/client/src/Pages/Auth/Login/Login.jsx
+++ /dev/null
@@ -1,266 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import { Box, Stack, Typography } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { loginCredentials } from "../../../Validation/validation";
-import { login } from "../../../Features/Auth/authSlice";
-import { useDispatch, useSelector } from "react-redux";
-import { createToast } from "../../../Utils/toastUtils";
-import { networkService } from "../../../main";
-import Background from "../../../assets/Images/background-grid.svg?react";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
-import "../index.css";
-import EmailStep from "./Components/EmailStep";
-import PasswordStep from "./Components/PasswordStep";
-import ThemeSwitch from "../../../Components/ThemeSwitch";
-import ForgotPasswordLabel from "./Components/ForgotPasswordLabel";
-import LanguageSelector from "../../../Components/LanguageSelector";
-import { Trans, useTranslation } from "react-i18next";
-
-const DEMO = import.meta.env.VITE_APP_DEMO;
-
-/**
- * Displays the login page.
- */
-
-const Login = () => {
- const theme = useTheme();
- const dispatch = useDispatch();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const authState = useSelector((state) => state.auth);
- const { authToken } = authState;
-
- const idMap = {
- "login-email-input": "email",
- "login-password-input": "password",
- };
-
- const [form, setForm] = useState({
- email: DEMO !== undefined ? "uptimedemo@demo.com" : "",
- password: DEMO !== undefined ? "Demouser1!" : "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- if (authToken) {
- navigate("/uptime");
- }
- }, [authToken, navigate]);
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- const lowerCasedValue =
- name === idMap["login-email-input"] ? value?.toLowerCase() || value : value;
- setForm((prev) => ({
- ...prev,
- [name]: lowerCasedValue,
- }));
-
- const { error } = loginCredentials.validate(
- { [name]: lowerCasedValue },
- { abortEarly: false }
- );
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
- const handleSubmit = async (event) => {
- event.preventDefault();
-
- if (step === 0) {
- const { error } = loginCredentials.validate(
- { email: form.email },
- { abortEarly: false }
- );
- if (error) {
- const errorMessage = error.details[0].message;
- const translatedMessage = errorMessage.startsWith("auth")
- ? t(errorMessage) // Localization keys are in validation.js
- : errorMessage; // FIXME: Potential untranslated string
- setErrors((prev) => ({ ...prev, email: translatedMessage }));
- createToast({ body: translatedMessage });
- } else {
- setStep(1);
- }
- } else if (step === 1) {
- const { error } = loginCredentials.validate(form, { abortEarly: false });
-
- if (error) {
- // validation errors
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- createToast({
- body:
- error.details && error.details.length > 0
- ? error.details[0].message.startsWith("auth")
- ? t(error.details[0].message) // Localization keys are in validation.js
- : error.details[0].message // FIXME: Potential untranslated string
- : t("auth.common.errors.validation"),
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/uptime");
- createToast({
- body: t("auth.login.toasts.success"),
- });
- } else {
- if (action.payload) {
- if (action.payload.msg === "Incorrect password")
- setErrors({
- password: t("auth.common.fields.password.errors.incorrect"),
- });
- // dispatch errors
- createToast({
- body: t("auth.login.toasts.incorrectPassword"),
- });
- } else {
- // unknown errors
- createToast({
- body: t("common.toasts.unknownError"),
- });
- }
- }
- }
- }
- };
-
- return (
-
-
-
-
-
-
-
- {t("common.appName")}
-
-
-
-
-
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.primary.lowContrast,
- backgroundColor: theme.palette.primary.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
- {step === 0 ? (
-
- ) : (
- step === 1 && (
- setStep(0)}
- />
- )
- )}
-
-
- {/* Registration link */}
-
-
- navigate("/register")}
- />
- ),
- }}
- />
-
-
-
-
- );
-};
-
-export default Login;
diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx
new file mode 100644
index 000000000..04155a9d6
--- /dev/null
+++ b/client/src/Pages/Auth/Login/index.jsx
@@ -0,0 +1,162 @@
+// Components
+import Stack from "@mui/material/Stack";
+import AuthHeader from "../components/AuthHeader";
+import Button from "@mui/material/Button";
+import TextInput from "../../../Components/Inputs/TextInput";
+import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
+import { loginCredentials } from "../../../Validation/validation";
+import TextLink from "../components/TextLink";
+
+// Utils
+import { login } from "../../../Features/Auth/authSlice";
+import { useNavigate } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { useState } from "react";
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../../Utils/toastUtils";
+
+const Login = () => {
+ // Local state
+ const [form, setForm] = useState({
+ email: "",
+ password: "",
+ });
+ const [errors, setErrors] = useState({
+ email: "",
+ password: "",
+ });
+
+ // Hooks
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const dispatch = useDispatch();
+ const navigate = useNavigate();
+
+ // Handlers
+ const onChange = (e) => {
+ const { name, value } = e.target;
+ const updatedForm = { ...form, [name]: value };
+ const { error } = loginCredentials.validate({ [name]: value });
+ setForm(updatedForm);
+ setErrors((prev) => ({
+ ...prev,
+ [name]: error?.details?.[0]?.message || "",
+ }));
+ };
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ const toSubmit = { ...form };
+ const { error } = loginCredentials.validate(toSubmit, { abortEarly: false });
+
+ if (error) {
+ const formErrors = {};
+ for (const err of error.details) {
+ formErrors[err.path[0]] = err.message;
+ }
+ setErrors(formErrors);
+ return;
+ }
+
+ const action = await dispatch(login(form));
+ if (action.payload.success) {
+ navigate("/uptime");
+ createToast({
+ body: t("auth.login.toasts.success"),
+ });
+ } else {
+ if (action.payload) {
+ if (action.payload.msg === "Incorrect password")
+ setErrors({
+ password: t("auth.login.errors.password.incorrect"),
+ });
+ // dispatch errors
+ createToast({
+ body: t("auth.login.toasts.incorrectPassword"),
+ });
+ } else {
+ // unknown errors
+ createToast({
+ body: t("common.toasts.unknownError"),
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ }
+ />
+
+ Login
+
+
+
+
+
+
+ );
+};
+
+export default Login;
From f32892adaf9694a55c10f6e41d1fd39deeaa3e7c Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 10:20:36 +0800
Subject: [PATCH 094/319] add components directory
---
.../src/Pages/Auth/components/AuthHeader.jsx | 45 +++++++++++++++++++
client/src/Pages/Auth/components/TextLink.jsx | 34 ++++++++++++++
2 files changed, 79 insertions(+)
create mode 100644 client/src/Pages/Auth/components/AuthHeader.jsx
create mode 100644 client/src/Pages/Auth/components/TextLink.jsx
diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx
new file mode 100644
index 000000000..c6ad46a47
--- /dev/null
+++ b/client/src/Pages/Auth/components/AuthHeader.jsx
@@ -0,0 +1,45 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Logo from "../../../assets/icons/checkmate-icon.svg?react";
+import LanguageSelector from "../../../Components/LanguageSelector";
+import ThemeSwitch from "../../../Components/ThemeSwitch";
+
+// Utils
+import { useTheme } from "@mui/material/styles";
+import { useTranslation } from "react-i18next";
+
+const AuthHeader = () => {
+ // Hooks
+ const theme = useTheme();
+ const { t } = useTranslation();
+
+ return (
+
+
+
+ {t("common.appName")}
+
+
+
+
+
+
+ );
+};
+
+export default AuthHeader;
diff --git a/client/src/Pages/Auth/components/TextLink.jsx b/client/src/Pages/Auth/components/TextLink.jsx
new file mode 100644
index 000000000..a10839e84
--- /dev/null
+++ b/client/src/Pages/Auth/components/TextLink.jsx
@@ -0,0 +1,34 @@
+import Typography from "@mui/material/Typography";
+import Stack from "@mui/material/Stack";
+import Link from "@mui/material/Link";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { Link as RouterLink } from "react-router-dom";
+
+const TextLink = ({ text, linkText, href }) => {
+ const theme = useTheme();
+
+ return (
+
+ {text}
+
+ {linkText}
+
+
+ );
+};
+
+TextLink.propTypes = {
+ text: PropTypes.string,
+ linkText: PropTypes.string,
+ href: PropTypes.string,
+};
+
+export default TextLink;
From cd17ecdadbb81d8bb9a0d5677c62147addb6ba32 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 10:21:33 +0800
Subject: [PATCH 095/319] fix import path
---
client/src/Routes/index.jsx | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 3f815a665..85b8da82c 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -3,7 +3,7 @@ import HomeLayout from "../Components/Layouts/HomeLayout";
import NotFound from "../Pages/NotFound";
// Auth
-import AuthLogin from "../Pages/Auth/Login/Login";
+import AuthLogin from "../Pages/Auth/Login";
import AuthRegister from "../Pages/Auth/Register/Register";
import AuthForgotPassword from "../Pages/Auth/ForgotPassword";
import AuthCheckEmail from "../Pages/Auth/CheckEmail";
@@ -48,7 +48,6 @@ import Settings from "../Pages/Settings";
import Maintenance from "../Pages/Maintenance";
import ProtectedRoute from "../Components/ProtectedRoute";
-import ProtectedDistributedUptimeRoute from "../Components/ProtectedDistributedUptimeRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport";
From 9e6b826f6e8a03c5ab4cd6d423b3a1376dd8be55 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 10:21:47 +0800
Subject: [PATCH 096/319] update translations
---
client/src/locales/en.json | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 83ecf6b55..655f427bf 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -102,12 +102,19 @@
"stepTwo": "Enter your password"
},
"links": {
- "forgotPassword": "Forgot password? Reset password",
- "register": "Do not have an account? Register here"
+ "forgotPassword": "Forgot password?",
+ "forgotPasswordLink": "Reset password",
+ "register": "Do not have an account?",
+ "registerLink": "Register here"
},
"toasts": {
"success": "Welcome back! You're successfully logged in.",
"incorrectPassword": "Incorrect password"
+ },
+ "errors": {
+ "password": {
+ "incorrect": "The password you provided does not match our records"
+ }
}
},
"registration": {
From 045b506e2ad395a9da6ed6e713aaa9d26473c876 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 10:22:17 +0800
Subject: [PATCH 097/319] remove unused component
---
.../ProtectedDistributedUptimeRoute/index.jsx | 30 -------------------
1 file changed, 30 deletions(-)
delete mode 100644 client/src/Components/ProtectedDistributedUptimeRoute/index.jsx
diff --git a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx b/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx
deleted file mode 100644
index 4a59859fe..000000000
--- a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { Navigate } from "react-router-dom";
-import { useSelector } from "react-redux";
-import PropTypes from "prop-types";
-
-/**
- * @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component.
- * @param {React.ReactNode} props.children - The children to render if the user is authenticated.
- * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page.
- */
-
-const ProtectedDistributedUptimeRoute = ({ children }) => {
- const distributedUptimeEnabled = useSelector(
- (state) => state.ui.distributedUptimeEnabled
- );
-
- return distributedUptimeEnabled === true ? (
- children
- ) : (
-
- );
-};
-
-ProtectedDistributedUptimeRoute.propTypes = {
- children: PropTypes.node.isRequired,
-};
-
-export default ProtectedDistributedUptimeRoute;
From fac960b9ac913000c9ab4e3751a6cc4dd8948899 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 12:32:03 +0800
Subject: [PATCH 098/319] set minHeight, force lowercase for email
---
client/src/Pages/Auth/Login/index.jsx | 7 +++++--
1 file changed, 5 insertions(+), 2 deletions(-)
diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx
index 04155a9d6..fbbe29a5e 100644
--- a/client/src/Pages/Auth/Login/index.jsx
+++ b/client/src/Pages/Auth/Login/index.jsx
@@ -35,7 +35,10 @@ const Login = () => {
// Handlers
const onChange = (e) => {
- const { name, value } = e.target;
+ let { name, value } = e.target;
+ if (name === "email") {
+ value = value.toLowerCase();
+ }
const updatedForm = { ...form, [name]: value };
const { error } = loginCredentials.validate({ [name]: value });
setForm(updatedForm);
@@ -87,7 +90,7 @@ const Login = () => {
return (
Date: Wed, 11 Jun 2025 12:32:16 +0800
Subject: [PATCH 099/319] update admin check HOC
---
client/src/Components/HOC/withAdminCheck.jsx | 18 +++++++++++++++---
1 file changed, 15 insertions(+), 3 deletions(-)
diff --git a/client/src/Components/HOC/withAdminCheck.jsx b/client/src/Components/HOC/withAdminCheck.jsx
index 8edf7a02b..bd6a21d07 100644
--- a/client/src/Components/HOC/withAdminCheck.jsx
+++ b/client/src/Components/HOC/withAdminCheck.jsx
@@ -1,4 +1,4 @@
-import { useEffect } from "react";
+import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { logger } from "../../Utils/Logger";
@@ -7,23 +7,35 @@ import { networkService } from "../../main";
const withAdminCheck = (WrappedComponent) => {
const WithAdminCheck = (props) => {
const navigate = useNavigate();
+ const [isChecking, setIsChecking] = useState(true);
+ const [superAdminExists, setSuperAdminExists] = useState(false);
useEffect(() => {
networkService
.doesSuperAdminExist()
.then((response) => {
- if (response.data.data === true) {
+ if (response?.data?.data === true) {
navigate("/login");
+ } else {
+ setSuperAdminExists(true);
}
})
.catch((error) => {
logger.error(error);
+ })
+ .finally(() => {
+ setIsChecking(false);
});
}, [navigate]);
+
+ if (isChecking) {
+ return null;
+ }
+
return (
);
};
From bcea3a5e66dff9d68bcc61860ad97625b4f1b55b Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 12:32:36 +0800
Subject: [PATCH 100/319] refactor register page
---
client/src/Pages/Auth/Register/Register.jsx | 413 ------------------
.../src/Pages/Auth/Register/StepOne/index.jsx | 142 ------
.../Pages/Auth/Register/StepThree/index.jsx | 185 --------
.../src/Pages/Auth/Register/StepTwo/index.jsx | 125 ------
client/src/Pages/Auth/Register/index.jsx | 347 +++++++++++++++
5 files changed, 347 insertions(+), 865 deletions(-)
delete mode 100644 client/src/Pages/Auth/Register/Register.jsx
delete mode 100644 client/src/Pages/Auth/Register/StepOne/index.jsx
delete mode 100644 client/src/Pages/Auth/Register/StepThree/index.jsx
delete mode 100644 client/src/Pages/Auth/Register/StepTwo/index.jsx
create mode 100644 client/src/Pages/Auth/Register/index.jsx
diff --git a/client/src/Pages/Auth/Register/Register.jsx b/client/src/Pages/Auth/Register/Register.jsx
deleted file mode 100644
index 390aa6e36..000000000
--- a/client/src/Pages/Auth/Register/Register.jsx
+++ /dev/null
@@ -1,413 +0,0 @@
-import { useState, useEffect } from "react";
-import { useNavigate, useParams } from "react-router-dom";
-import { useDispatch } from "react-redux";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import { StepOne } from "./StepOne";
-import { StepTwo } from "./StepTwo";
-import { StepThree } from "./StepThree";
-import { networkService } from "../../../main";
-import { newOrChangedCredentials } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import { register } from "../../../Features/Auth/authSlice";
-import Background from "../../../assets/Images/background-grid.svg?react";
-import Logo from "../../../assets/icons/checkmate-icon.svg?react";
-import Mail from "../../../assets/icons/mail.svg?react";
-import "../index.css";
-import { useTranslation, Trans } from "react-i18next";
-/**
- * Displays the initial landing page.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click.
- * @returns {JSX.Element}
- */
-const LandingPage = ({ isSuperAdmin, onSignup }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
-
- {isSuperAdmin
- ? t("auth.registration.description.superAdmin")
- : t("auth.registration.description.user")}
-
-
-
-
-
- {isSuperAdmin
- ? t("auth.registration.gettingStartedButton.superAdmin")
- : t("auth.registration.gettingStartedButton.user")}
-
-
-
-
- {
- window.open(
- "https://bluewavelabs.ca/terms-of-service-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- />
- ),
- a2: (
- {
- window.open(
- "https://bluewavelabs.ca/privacy-policy-open-source",
- "_blank",
- "noreferrer"
- );
- }}
- />
- ),
- }}
- />
-
-
-
- >
- );
-};
-
-LandingPage.propTypes = {
- isSuperAdmin: PropTypes.bool,
- onSignup: PropTypes.func,
-};
-
-const Register = ({ isSuperAdmin }) => {
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const { token } = useParams();
- const theme = useTheme();
- const { t } = useTranslation();
- // TODO If possible, change the IDs of these fields to match the backend
- const idMap = {
- "register-firstname-input": "firstName",
- "register-lastname-input": "lastName",
- "register-email-input": "email",
- "register-password-input": "password",
- "register-confirm-input": "confirm",
- };
-
- const [form, setForm] = useState({
- firstName: "",
- lastName: "",
- email: "",
- password: "",
- confirm: "",
- role: [],
- teamId: "",
- });
- const [errors, setErrors] = useState({});
- const [step, setStep] = useState(0);
-
- useEffect(() => {
- const fetchInvite = async () => {
- if (token !== undefined) {
- try {
- const res = await networkService.verifyInvitationToken(token);
- const invite = res.data.data;
- const { email } = invite;
- setForm({ ...form, email });
- } catch (error) {
- navigate("/register", { replace: true });
- }
- }
- };
- fetchInvite();
- }, []);
-
- /**
- * Validates the form data against the validation schema.
- *
- * @param {Object} data - The form data to validate.
- * @param {Object} [options] - Optional settings for validation.
- * @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`.
- */
- const validateForm = (data, options = {}) => {
- const { error } = newOrChangedCredentials.validate(data, {
- abortEarly: false,
- ...options,
- });
- return error;
- };
-
- /**
- * Handles validation errors by setting the state with error messages and displaying a toast notification.
- *
- * @param {Object} error - The validation error object returned from the validation schema.
- */
- const handleError = (error) => {
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- // Localization keys are in validation.js
- createToast({ body: t(error.details[0].message || "auth.common.errors.validation") });
- };
-
- const handleStepOne = async (e) => {
- e.preventDefault();
- let error = validateForm({
- firstName: form.firstName,
- lastName: form.lastName,
- });
-
- if (error) {
- handleError(error);
- return;
- }
-
- setStep(2);
- };
-
- const handleStepTwo = async (e) => {
- e.preventDefault();
-
- let error;
- error = validateForm({ email: form.email });
- if (error) {
- handleError(error);
- return;
- }
-
- setStep(3);
- };
-
- // Final step
- // Attempts account registration
- const handleStepThree = async (e) => {
- e.preventDefault();
- const { password, confirm } = e.target.elements;
- let registerForm = {
- ...form,
- password: password.value,
- confirm: confirm.value,
- role: isSuperAdmin ? ["superadmin"] : form.role,
- inviteToken: token ? token : "", // Add the token to the request for verification
- };
- let error = validateForm(registerForm, {
- context: { password: registerForm.password },
- });
- if (error) {
- handleError(error);
- return;
- }
-
- delete registerForm.confirm;
- const action = await dispatch(register(registerForm));
- if (action.payload.success) {
- const authToken = action.payload.data;
- navigate("/uptime");
- createToast({
- body: t("auth.registration.toasts.success"),
- });
- } else {
- if (action.payload) {
- createToast({
- body: action.payload.msg,
- });
- } else {
- createToast({
- body: t("common.toasts.unknownError"),
- });
- }
- }
- };
-
- const handleChange = (event) => {
- const { value, id } = event.target;
- const name = idMap[id];
- const lowerCasedValue =
- name === idMap["register-email-input"] ? value?.toLowerCase() || value : value;
- setForm((prev) => ({
- ...prev,
- [name]: lowerCasedValue,
- }));
-
- const { error } = newOrChangedCredentials.validate(
- { [name]: lowerCasedValue },
- { abortEarly: false, context: { password: form.password } }
- );
-
- setErrors((prev) => {
- const prevErrors = { ...prev };
- if (error) prevErrors[name] = error.details[0].message;
- else delete prevErrors[name];
- return prevErrors;
- });
- };
-
- return (
-
-
-
-
-
-
- {t("common.appName")}
-
- .MuiStack-root": {
- border: 1,
- borderRadius: theme.spacing(5),
- borderColor: theme.palette.primary.lowContrast,
- backgroundColor: theme.palette.primary.main,
- padding: {
- xs: theme.spacing(12),
- sm: theme.spacing(20),
- },
- },
- }}
- >
- {step === 0 ? (
- setStep(1)}
- />
- ) : step === 1 ? (
- setStep(0)}
- />
- ) : step === 2 ? (
- setStep(1)}
- />
- ) : step === 3 ? (
- setStep(2)}
- />
- ) : (
- ""
- )}
-
-
-
- {
- navigate("/login");
- }}
- sx={{ userSelect: "none", color: theme.palette.accent.main }}
- />
- ),
- }}
- />
-
-
-
- );
-};
-Register.propTypes = {
- isSuperAdmin: PropTypes.bool,
-};
-export default Register;
diff --git a/client/src/Pages/Auth/Register/StepOne/index.jsx b/client/src/Pages/Auth/Register/StepOne/index.jsx
deleted file mode 100644
index 575a9132f..000000000
--- a/client/src/Pages/Auth/Register/StepOne/index.jsx
+++ /dev/null
@@ -1,142 +0,0 @@
-import { useEffect, useRef } from "react";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import { useTranslation } from "react-i18next";
-
-StepOne.propTypes = {
- isSuperAdmin: PropTypes.bool,
- form: PropTypes.object,
- errors: PropTypes.object,
- onSubmit: PropTypes.func,
- onChange: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the first step of the sign up process.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-
-function StepOne({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
- {/* TODO this stack should be a component */}
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepOne")}
-
-
-
-
-
-
-
-
- {/* TODO buttons should be a component should be a component */}
-
-
- {t("auth.common.navigation.back")}
-
-
- {t("auth.common.navigation.continue")}
-
-
-
-
- >
- );
-}
-
-export { StepOne };
diff --git a/client/src/Pages/Auth/Register/StepThree/index.jsx b/client/src/Pages/Auth/Register/StepThree/index.jsx
deleted file mode 100644
index c81ae08a7..000000000
--- a/client/src/Pages/Auth/Register/StepThree/index.jsx
+++ /dev/null
@@ -1,185 +0,0 @@
-import { useEffect, useRef } from "react";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import Check from "../../../../Components/Check/Check";
-import { useValidatePassword } from "../../hooks/useValidatePassword";
-import { useTranslation } from "react-i18next";
-StepThree.propTypes = {
- isSuperAdmin: PropTypes.bool,
- onSubmit: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the third step of the sign up process.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-function StepThree({ isSuperAdmin, onSubmit, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- const { handleChange, feedbacks, form, errors } = useValidatePassword();
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepThree")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t("auth.common.navigation.back")}
-
-
- {t("auth.common.navigation.continue")}
-
-
-
-
- >
- );
-}
-
-export { StepThree };
diff --git a/client/src/Pages/Auth/Register/StepTwo/index.jsx b/client/src/Pages/Auth/Register/StepTwo/index.jsx
deleted file mode 100644
index 306a2b750..000000000
--- a/client/src/Pages/Auth/Register/StepTwo/index.jsx
+++ /dev/null
@@ -1,125 +0,0 @@
-import { useEffect, useRef } from "react";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import { Box, Button, Stack, Typography } from "@mui/material";
-import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded";
-import TextInput from "../../../../Components/Inputs/TextInput";
-import { useTranslation } from "react-i18next";
-
-StepTwo.propTypes = {
- isSuperAdmin: PropTypes.bool,
- form: PropTypes.object,
- errors: PropTypes.object,
- onSubmit: PropTypes.func,
- onChange: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the second step of the sign up process.
- *
- * @param {Object} props
- * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account
- * @param {Object} props.form - Form state object.
- * @param {Object} props.errors - Object containing form validation errors.
- * @param {Function} props.onSubmit - Callback function to handle form submission.
- * @param {Function} props.onChange - Callback function to handle form input changes.
- * @param {Function} props.onBack - Callback function to handle "Back" button click.
- * @returns {JSX.Element}
- */
-function StepTwo({ isSuperAdmin, form, errors, onSubmit, onChange, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
-
- {isSuperAdmin
- ? t("auth.registration.heading.superAdmin")
- : t("auth.registration.heading.user")}
-
- {t("auth.registration.subheadings.stepTwo")}
-
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email ? true : false}
- helperText={t(errors.email)} // Localization keys are in validation.js
- ref={inputRef}
- />
-
-
-
- {t("auth.common.navigation.back")}
-
-
- {t("auth.common.navigation.continue")}
-
-
-
-
- >
- );
-}
-
-export { StepTwo };
diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx
new file mode 100644
index 000000000..2dd0951dd
--- /dev/null
+++ b/client/src/Pages/Auth/Register/index.jsx
@@ -0,0 +1,347 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import AuthHeader from "../components/AuthHeader";
+import TextInput from "../../../Components/Inputs/TextInput";
+import Check from "../../../Components/Check/Check";
+import Button from "@mui/material/Button";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router-dom";
+import { useDispatch } from "react-redux";
+import { useParams } from "react-router-dom";
+import { networkService } from "../../../main";
+import { newOrChangedCredentials } from "../../../Validation/validation";
+import { register } from "../../../Features/Auth/authSlice";
+import { createToast } from "../../../Utils/toastUtils";
+import PropTypes from "prop-types";
+
+const getFeedbackStatus = (form, errors, field, criteria) => {
+ const fieldErrors = errors?.[field];
+ const isFieldEmpty = form?.[field]?.length === 0;
+ const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty");
+ const isCorrect = !isFieldEmpty && !hasError;
+
+ if (isCorrect) {
+ return "success";
+ } else if (hasError) {
+ return "error";
+ } else {
+ return "info";
+ }
+};
+
+const Register = ({ superAdminExists }) => {
+ // Local state
+ const [form, setForm] = useState({
+ firstName: "",
+ lastName: "",
+ email: "",
+ password: "",
+ confirm: "",
+ role: [],
+ teamId: "",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [feedback, setFeedback] = useState({});
+
+ // Hooks
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const { token } = useParams();
+ const navigate = useNavigate();
+ const dispatch = useDispatch();
+
+ // Effects
+ useEffect(() => {
+ const fetchInvite = async () => {
+ if (token !== undefined) {
+ try {
+ const res = await networkService.verifyInvitationToken(token);
+ const invite = res.data.data;
+ const { email } = invite;
+ setForm((prevForm) => {
+ if (!prevForm.email) {
+ return { ...prevForm, email };
+ }
+ return prevForm;
+ });
+ } catch (error) {
+ navigate("/register", { replace: true });
+ }
+ }
+ };
+ fetchInvite();
+ }, [form, token, navigate]);
+
+ // Handlers
+ const onChange = (e) => {
+ let { name, value } = e.target;
+ if (name === "email") value = value.toLowerCase();
+ const updatedForm = { ...form, [name]: value };
+
+ const { error } = newOrChangedCredentials.validate(
+ { [name]: value },
+ { abortEarly: false, context: { password: form.password } }
+ );
+
+ setErrors((prev) => ({
+ ...prev,
+ [name]: error?.details?.[0]?.message || "",
+ }));
+
+ setForm(updatedForm);
+ };
+
+ const onPasswordChange = (e) => {
+ const { name, value } = e.target;
+ const updatedForm = { ...form, [name]: value };
+ setForm(updatedForm);
+ const validateValue = { [name]: value };
+ const validateOptions = { abortEarly: false, context: { password: form.password } };
+ if (name === "password" && form.confirm.length > 0) {
+ validateValue.confirm = form.confirm;
+ validateOptions.context = { password: value };
+ } else if (name === "confirm") {
+ validateValue.password = form.password;
+ }
+ const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
+
+ const pwdErrors = error?.details.map((error) => ({
+ path: error.path[0],
+ type: error.type,
+ }));
+
+ const errorsByPath =
+ pwdErrors &&
+ pwdErrors.reduce((acc, { path, type }) => {
+ if (!acc[path]) {
+ acc[path] = [];
+ }
+ acc[path].push(type);
+ return acc;
+ }, {});
+
+ const oldErrors = { ...errors };
+ if (name === "password") {
+ oldErrors.password = undefined;
+ } else if (name === "confirm") {
+ oldErrors.confirm = undefined;
+ }
+ const newErrors = { ...oldErrors, ...errorsByPath };
+
+ setErrors(newErrors);
+
+ const newFeedback = {
+ length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"),
+ special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"),
+ number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"),
+ uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"),
+ lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"),
+ confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"),
+ };
+
+ setFeedback(newFeedback);
+ };
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+ const toSubmit = {
+ ...form,
+ role: superAdminExists ? form.role : ["superadmin"],
+ inviteToken: token ? token : "",
+ };
+
+ const { error } = newOrChangedCredentials.validate(toSubmit, {
+ abortEarly: false,
+ context: { password: form.password },
+ });
+
+ if (error) {
+ const formErrors = {};
+ for (const err of error.details) {
+ formErrors[err.path[0]] = err.message;
+ }
+ setErrors(formErrors);
+ return;
+ }
+
+ delete toSubmit.confirm;
+
+ const action = await dispatch(register(toSubmit));
+ if (action.payload.success) {
+ navigate("/uptime");
+ createToast({
+ body: t("auth.registration.toasts.success"),
+ });
+ } else {
+ if (action.payload) {
+ createToast({
+ body: action.payload.msg,
+ });
+ } else {
+ createToast({
+ body: t("common.toasts.unknownError"),
+ });
+ }
+ }
+ };
+
+ return (
+
+
+
+
+
+ {superAdminExists
+ ? t("auth.registration.heading.user")
+ : t("auth.registration.heading.superAdmin")}
+
+
+ {superAdminExists
+ ? t("auth.registration.description.user")
+ : t("auth.registration.description.superAdmin")}
+
+ (e.target.value = e.target.value.toLowerCase())}
+ onChange={onChange}
+ error={errors.email ? true : false}
+ helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {t("auth.common.navigation.continue")}
+
+
+
+
+ );
+};
+
+Register.propTypes = {
+ superAdminExists: PropTypes.bool,
+};
+
+export default Register;
From c0ad2dfce2d2c5d58af2592a56ea9395da37c2e0 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 12:41:58 +0800
Subject: [PATCH 101/319] fix superadmin state
---
client/src/Components/HOC/withAdminCheck.jsx | 2 +-
client/src/Routes/index.jsx | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/client/src/Components/HOC/withAdminCheck.jsx b/client/src/Components/HOC/withAdminCheck.jsx
index bd6a21d07..458330a1f 100644
--- a/client/src/Components/HOC/withAdminCheck.jsx
+++ b/client/src/Components/HOC/withAdminCheck.jsx
@@ -17,7 +17,7 @@ const withAdminCheck = (WrappedComponent) => {
if (response?.data?.data === true) {
navigate("/login");
} else {
- setSuperAdminExists(true);
+ setSuperAdminExists(false);
}
})
.catch((error) => {
diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx
index 85b8da82c..29daa2942 100644
--- a/client/src/Routes/index.jsx
+++ b/client/src/Routes/index.jsx
@@ -4,7 +4,7 @@ import NotFound from "../Pages/NotFound";
// Auth
import AuthLogin from "../Pages/Auth/Login";
-import AuthRegister from "../Pages/Auth/Register/Register";
+import AuthRegister from "../Pages/Auth/Register/";
import AuthForgotPassword from "../Pages/Auth/ForgotPassword";
import AuthCheckEmail from "../Pages/Auth/CheckEmail";
import AuthSetNewPassword from "../Pages/Auth/SetNewPassword";
From ec25ebee35bb0abd287e2ce0307ea515321784e7 Mon Sep 17 00:00:00 2001
From: Br0wnHammer
Date: Wed, 11 Jun 2025 14:11:49 +0530
Subject: [PATCH 102/319] Fix: Node Mailer Settings
---
server/service/emailService.js | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/server/service/emailService.js b/server/service/emailService.js
index 035cc6505..ad0cdac3a 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -109,7 +109,7 @@ class EmailService {
const baseEmailConfig = {
host: systemEmailHost,
- port: systemEmailPort,
+ port: Number(systemEmailPort),
secure: true,
auth: {
user: systemEmailUser || systemEmailAddress,
@@ -130,10 +130,6 @@ class EmailService {
}
: baseEmailConfig;
- if (!isSmtps) {
- delete emailConfig.auth;
- }
-
this.transporter = this.nodemailer.createTransport(emailConfig);
const buildHtml = async (template, context) => {
From 01ed3d8aaa971b8969bb9b7bfcbf039da5191aac Mon Sep 17 00:00:00 2001
From: Br0wnHammer
Date: Wed, 11 Jun 2025 14:42:51 +0530
Subject: [PATCH 103/319] Feat: New Email Settings
---
client/src/Pages/Settings/SettingsEmail.jsx | 43 +++++++++++++++++++++
client/src/locales/en.json | 4 ++
2 files changed, 47 insertions(+)
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
index 88f8f98c1..73ba11610 100644
--- a/client/src/Pages/Settings/SettingsEmail.jsx
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { createToast } from "../../Utils/toastUtils";
+import { Switch } from "@mui/material";
const SettingsEmail = ({
isAdmin,
@@ -53,6 +54,10 @@ const SettingsEmail = ({
systemEmailAddress: settingsData?.settings?.systemEmailAddress,
systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost,
+ systemEmailTLSServername: settingsData?.settings?.systemEmailTLSServername,
+ systemEmailIgnoreTLS: settingsData?.settings?.systemEmailIgnoreTLS,
+ systemEmailRequireTLS: settingsData?.settings?.systemEmailRequireTLS,
+ systemEmailRejectUnauthorized: settingsData?.settings?.systemEmailRejectUnauthorized,
};
// Basic validation
@@ -155,6 +160,44 @@ const SettingsEmail = ({
)}
+
+
+ {t("settingsEmailTLSServername")}
+
+
+
+
+ {t("settingsEmailIgnoreTLS")}
+
+ {t("settingsEmailRequireTLS")}
+
+ {t("settingsEmailRejectUnauthorized")}
+
+
{t("settingsEmailConnectionHost")}
Date: Wed, 11 Jun 2025 15:11:28 +0530
Subject: [PATCH 104/319] TLS Options Set Up
---
client/src/Pages/Settings/index.jsx | 8 ++++++--
client/src/Validation/validation.js | 4 ++++
server/service/emailService.js | 9 ++++++++-
3 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx
index b0e3c476b..b51fb0110 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/Settings/index.jsx
@@ -56,17 +56,21 @@ const Settings = () => {
// Handlers
const handleChange = async (e) => {
- const { name, value } = e.target;
+ const { name, value, checked } = e.target;
// Special case for showURL until handled properly in the backend
if (name === "showURL") {
dispatch(setShowURL(value));
return;
}
+ let newValue;
+ if (name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || name === "systemEmailRejectUnauthorized") {
+ newValue = checked;
+ }
// Build next state early
const newSettingsData = {
...settingsData,
- settings: { ...settingsData.settings, [name]: value },
+ settings: { ...settingsData.settings, [name]: newValue ?? value },
};
// Validate
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 043e88f50..cb2d4e797 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -294,6 +294,10 @@ const settingsValidation = joi.object({
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
systemEmailConnectionHost: joi.string().allow(""),
+ systemEmailTLSServername: joi.string().allow(""),
+ systemEmailIgnoreTLS: joi.boolean().optional(),
+ systemEmailRequireTLS: joi.boolean().optional(),
+ systemEmailRejectUnauthorized: joi.boolean().optional(),
});
const dayjsValidator = (value, helpers) => {
diff --git a/server/service/emailService.js b/server/service/emailService.js
index ad0cdac3a..3b627fb95 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -105,6 +105,10 @@ class EmailService {
systemEmailAddress,
systemEmailPassword,
systemEmailConnectionHost,
+ systemEmailTLSServername,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
} = config;
const baseEmailConfig = {
@@ -126,7 +130,10 @@ class EmailService {
name: systemEmailConnectionHost || "localhost",
secure: false,
pool: true,
- tls: { rejectUnauthorized: false },
+ tls: { rejectUnauthorized: systemEmailRejectUnauthorized },
+ ignoreTLS: systemEmailIgnoreTLS,
+ requireTLS: systemEmailRequireTLS,
+ servername: systemEmailTLSServername,
}
: baseEmailConfig;
From 1794429cbeb801c76c728b515a06487e4509d61b Mon Sep 17 00:00:00 2001
From: Br0wnHammer
Date: Wed, 11 Jun 2025 15:29:25 +0530
Subject: [PATCH 105/319] Formatting Run
---
client/src/Pages/Settings/SettingsEmail.jsx | 7 +++----
client/src/Pages/Settings/index.jsx | 6 +++++-
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
index 73ba11610..7d6538734 100644
--- a/client/src/Pages/Settings/SettingsEmail.jsx
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -57,7 +57,8 @@ const SettingsEmail = ({
systemEmailTLSServername: settingsData?.settings?.systemEmailTLSServername,
systemEmailIgnoreTLS: settingsData?.settings?.systemEmailIgnoreTLS,
systemEmailRequireTLS: settingsData?.settings?.systemEmailRequireTLS,
- systemEmailRejectUnauthorized: settingsData?.settings?.systemEmailRejectUnauthorized,
+ systemEmailRejectUnauthorized:
+ settingsData?.settings?.systemEmailRejectUnauthorized,
};
// Basic validation
@@ -161,9 +162,7 @@ const SettingsEmail = ({
)}
-
- {t("settingsEmailTLSServername")}
-
+ {t("settingsEmailTLSServername")}
{
return;
}
let newValue;
- if (name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || name === "systemEmailRejectUnauthorized") {
+ if (
+ name === "systemEmailIgnoreTLS" ||
+ name === "systemEmailRequireTLS" ||
+ name === "systemEmailRejectUnauthorized"
+ ) {
newValue = checked;
}
// Build next state early
From 0e4b371925f4630096cdebec5e1470b40bf9dde2 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Wed, 11 Jun 2025 20:12:02 +0800
Subject: [PATCH 106/319] add login header
---
client/src/Pages/Auth/Login/index.jsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx
index 04155a9d6..ec0afd054 100644
--- a/client/src/Pages/Auth/Login/index.jsx
+++ b/client/src/Pages/Auth/Login/index.jsx
@@ -6,6 +6,7 @@ import TextInput from "../../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import { loginCredentials } from "../../../Validation/validation";
import TextLink from "../components/TextLink";
+import Typography from "@mui/material/Typography";
// Utils
import { login } from "../../../Features/Auth/authSlice";
@@ -96,13 +97,14 @@ const Login = () => {
alignItems="center"
gap={theme.spacing(10)}
>
+ {t("auth.login.heading")}
+
Date: Thu, 12 Jun 2025 00:39:47 +0530
Subject: [PATCH 107/319] Implement remaining email config
---
client/src/Pages/Settings/SettingsEmail.jsx | 23 ++++++++++++++++++
client/src/Pages/Settings/index.jsx | 4 ++-
client/src/Utils/NetworkService.js | 6 +++++
client/src/Validation/validation.js | 2 ++
client/src/locales/en.json | 2 ++
server/controllers/settingsController.js | 12 +++++++++
server/service/emailService.js | 27 ++++++++-------------
server/validation/joi.js | 6 +++++
8 files changed, 64 insertions(+), 18 deletions(-)
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
index 7d6538734..113d5bf44 100644
--- a/client/src/Pages/Settings/SettingsEmail.jsx
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -50,6 +50,8 @@ const SettingsEmail = ({
const emailConfig = {
systemEmailHost: settingsData?.settings?.systemEmailHost,
systemEmailPort: settingsData?.settings?.systemEmailPort,
+ systemEmailSecure: settingsData?.settings?.systemEmailSecure,
+ systemEmailPool: settingsData?.settings?.systemEmailPool,
systemEmailUser: settingsData?.settings?.systemEmailUser,
systemEmailAddress: settingsData?.settings?.systemEmailAddress,
systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
@@ -110,6 +112,27 @@ const SettingsEmail = ({
onChange={handleChange}
/>
+
+ {t("settingsEmailSecure")}
+
+ {t("settingsEmailPool")}
+
+
{t("settingsEmailUser")}
{
if (
name === "systemEmailIgnoreTLS" ||
name === "systemEmailRequireTLS" ||
- name === "systemEmailRejectUnauthorized"
+ name === "systemEmailRejectUnauthorized" ||
+ name === "systemEmailSecure" ||
+ name === "systemEmailPool"
) {
newValue = checked;
}
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index 3d124a3e6..dcb21df92 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -1014,6 +1014,12 @@ class NetworkService {
systemEmailPort: emailConfig.systemEmailPort,
systemEmailAddress: emailConfig.systemEmailAddress,
systemEmailPassword: emailConfig.systemEmailPassword,
+ systemEmailSecure: emailConfig.systemEmailSecure,
+ systemEmailPool: emailConfig.systemEmailPool,
+ systemEmailIgnoreTLS: emailConfig.systemEmailIgnoreTLS,
+ systemEmailRequireTLS: emailConfig.systemEmailRequireTLS,
+ systemEmailRejectUnauthorized: emailConfig.systemEmailRejectUnauthorized,
+ systemEmailTLSServername: emailConfig.systemEmailTLSServername,
// Only include these if they are present
...(emailConfig.systemEmailConnectionHost && {
systemEmailConnectionHost: emailConfig.systemEmailConnectionHost,
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index cb2d4e797..089722a8d 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -290,6 +290,8 @@ const settingsValidation = joi.object({
timezone: joi.string().allow("").optional(),
systemEmailHost: joi.string().allow(""),
systemEmailPort: joi.number().allow(null, ""),
+ systemEmailSecure: joi.boolean().optional(),
+ systemEmailPool: joi.boolean().optional(),
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 028f9483c..610c56d0e 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -683,6 +683,8 @@
"settingsEmailIgnoreTLS": "Ignore TLS",
"settingsEmailRequireTLS": "Require TLS",
"settingsEmailRejectUnauthorized": "Reject Unauthorized",
+ "settingsEmailSecure": "Secure",
+ "settingsEmailPool": "Pool",
"state": "State",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
diff --git a/server/controllers/settingsController.js b/server/controllers/settingsController.js
index 58e8ce9f4..d1010c093 100755
--- a/server/controllers/settingsController.js
+++ b/server/controllers/settingsController.js
@@ -74,6 +74,12 @@ class SettingsController {
systemEmailPassword,
systemEmailUser,
systemEmailConnectionHost,
+ systemEmailSecure,
+ systemEmailPool,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
+ systemEmailTLSServername,
} = req.body;
const subject = this.stringService.testEmailSubject;
@@ -91,6 +97,12 @@ class SettingsController {
systemEmailAddress,
systemEmailPassword,
systemEmailConnectionHost,
+ systemEmailSecure,
+ systemEmailPool,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
+ systemEmailTLSServername,
}
);
diff --git a/server/service/emailService.js b/server/service/emailService.js
index 3b627fb95..325871fac 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -101,6 +101,8 @@ class EmailService {
const {
systemEmailHost,
systemEmailPort,
+ systemEmailSecure,
+ systemEmailPool,
systemEmailUser,
systemEmailAddress,
systemEmailPassword,
@@ -111,32 +113,23 @@ class EmailService {
systemEmailRejectUnauthorized,
} = config;
- const baseEmailConfig = {
+ const emailConfig = {
host: systemEmailHost,
port: Number(systemEmailPort),
- secure: true,
+ secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: systemEmailPassword,
},
connectionTimeout: 5000,
+ pool: systemEmailPool,
+ tls: { rejectUnauthorized: systemEmailRejectUnauthorized },
+ ignoreTLS: systemEmailIgnoreTLS,
+ requireTLS: systemEmailRequireTLS,
+ servername: systemEmailTLSServername,
+ name: systemEmailConnectionHost || "localhost",
};
- const isSmtps = Number(systemEmailPort) === 465;
-
- const emailConfig = !isSmtps
- ? {
- ...baseEmailConfig,
- name: systemEmailConnectionHost || "localhost",
- secure: false,
- pool: true,
- tls: { rejectUnauthorized: systemEmailRejectUnauthorized },
- ignoreTLS: systemEmailIgnoreTLS,
- requireTLS: systemEmailRequireTLS,
- servername: systemEmailTLSServername,
- }
- : baseEmailConfig;
-
this.transporter = this.nodemailer.createTransport(emailConfig);
const buildHtml = async (template, context) => {
diff --git a/server/validation/joi.js b/server/validation/joi.js
index 271cd2d21..b7858f92d 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -617,10 +617,16 @@ const sendTestEmailBodyValidation = joi.object({
to: joi.string().required(),
systemEmailHost: joi.string(),
systemEmailPort: joi.number(),
+ systemEmailSecure: joi.boolean(),
+ systemEmailPool: joi.boolean(),
systemEmailAddress: joi.string(),
systemEmailPassword: joi.string(),
systemEmailUser: joi.string(),
systemEmailConnectionHost: joi.string(),
+ systemEmailIgnoreTLS: joi.boolean(),
+ systemEmailRequireTLS: joi.boolean(),
+ systemEmailRejectUnauthorized: joi.boolean(),
+ systemEmailTLSServername: joi.string(),
});
export {
From 9242612f30818bbbd19d99bf4b1f77771efe39d4 Mon Sep 17 00:00:00 2001
From: anuragparashar26
Date: Thu, 12 Jun 2025 05:01:09 +0530
Subject: [PATCH 108/319] fix: update TextInput border color to match dropdown
(#2406)
---
client/src/Utils/Theme/globalTheme.js | 110 ++++++++++++++------------
1 file changed, 58 insertions(+), 52 deletions(-)
diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js
index 59c26866a..95f277a87 100644
--- a/client/src/Utils/Theme/globalTheme.js
+++ b/client/src/Utils/Theme/globalTheme.js
@@ -302,59 +302,65 @@ const baseTheme = (palette) => ({
},
MuiTextField: {
- styleOverrides: {
- root: ({ theme }) => ({
- "& fieldset": {
- borderColor: theme.palette.primary.contrastBorder,
- borderRadius: theme.shape.borderRadius,
- },
+ styleOverrides: {
+ root: ({ theme }) => ({
+ "& fieldset": {
+ borderColor: theme.palette.primary.lowContrast,
+ borderRadius: theme.shape.borderRadius,
+ },
- "& .MuiInputBase-input": {
- padding: ".75em",
- minHeight: "var(--env-var-height-2)",
- fontSize: "var(--env-var-font-size-medium)",
- fontWeight: 400,
- color: palette.primary.contrastTextSecondary,
- "&.Mui-disabled": {
- opacity: 0.3,
- WebkitTextFillColor: "unset",
- },
- "& .Mui-focused": {
- /* color: "#ff0000", */
- /* borderColor: theme.palette.primary.contrastText, */
- },
- },
- "& .MuiInputBase-input:-webkit-autofill": {
- transition: "background-color 5000s ease-in-out 0s",
- WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.primary.main} inset`,
- WebkitTextFillColor: theme.palette.primary.contrastText,
- },
- "& .MuiInputBase-input.MuiOutlinedInput-input": {
- padding: "0 var(--env-var-spacing-1-minus) !important",
- },
- "& .MuiOutlinedInput-root": {
- color: theme.palette.primary.contrastTextSecondary,
- borderRadius: 4,
- },
- "& .MuiOutlinedInput-notchedOutline": {
- borderRadius: 4,
- },
+ "& .MuiInputBase-input": {
+ padding: ".75em",
+ minHeight: "var(--env-var-height-2)",
+ fontSize: "var(--env-var-font-size-medium)",
+ fontWeight: 400,
+ color: palette.primary.contrastTextSecondary,
+ "&.Mui-disabled": {
+ opacity: 0.3,
+ WebkitTextFillColor: "unset",
+ },
+ "& .Mui-focused": {
+ /* color: "#ff0000", */
+ /* borderColor: theme.palette.primary.contrastText, */
+ },
+ },
- "& .MuiFormHelperText-root": {
- color: palette.error.main,
- opacity: 0.8,
- fontSize: "var(--env-var-font-size-medium)",
- marginLeft: 0,
- },
- "& .MuiFormHelperText-root.Mui-error": {
- opacity: 0.8,
- fontSize: "var(--env-var-font-size-medium)",
- color: palette.error.main,
- whiteSpace: "nowrap",
- },
- }),
- },
+ "& .MuiInputBase-input:-webkit-autofill": {
+ transition: "background-color 5000s ease-in-out 0s",
+ WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.primary.main} inset`,
+ WebkitTextFillColor: theme.palette.primary.contrastText,
+ },
+
+ "& .MuiInputBase-input.MuiOutlinedInput-input": {
+ padding: "0 var(--env-var-spacing-1-minus) !important",
+ },
+
+ "& .MuiOutlinedInput-root": {
+ color: theme.palette.primary.contrastTextSecondary,
+ borderRadius: 4,
+ },
+
+ "& .MuiOutlinedInput-notchedOutline": {
+ borderRadius: 4,
+ },
+
+ "& .MuiFormHelperText-root": {
+ color: palette.error.main,
+ opacity: 0.8,
+ fontSize: "var(--env-var-font-size-medium)",
+ marginLeft: 0,
+ },
+
+ "& .MuiFormHelperText-root.Mui-error": {
+ opacity: 0.8,
+ fontSize: "var(--env-var-font-size-medium)",
+ color: palette.error.main,
+ whiteSpace: "nowrap",
+ },
+ }),
+ },
},
+
MuiOutlinedInput: {
styleOverrides: {
root: {
@@ -365,12 +371,12 @@ const baseTheme = (palette) => ({
borderColor: palette.primary.contrastBorderDisabled,
},
"&:hover .MuiOutlinedInput-notchedOutline": {
- borderColor: palette.primary.contrastText, // Adjust hover border color
+ borderColor: palette.primary.lowContrast, // Adjust hover border color
},
"&.Mui-focused .MuiOutlinedInput-notchedOutline": {
borderColor: palette.accent.main, // Adjust focus border color
},
- color: palette.primary.contrastTextTertiary,
+ color: palette.primary.contrastText,
},
},
},
From 6dea8b434b99fbd09740105e95bf156d4e71adaa Mon Sep 17 00:00:00 2001
From: anuragparashar26
Date: Thu, 12 Jun 2025 05:20:16 +0530
Subject: [PATCH 109/319] fix: update TextInput border color to match dropdown
(#2406)
---
client/src/Utils/Theme/globalTheme.js | 100 +++++++++++++-------------
1 file changed, 50 insertions(+), 50 deletions(-)
diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js
index 95f277a87..37526ed73 100644
--- a/client/src/Utils/Theme/globalTheme.js
+++ b/client/src/Utils/Theme/globalTheme.js
@@ -302,63 +302,63 @@ const baseTheme = (palette) => ({
},
MuiTextField: {
- styleOverrides: {
- root: ({ theme }) => ({
- "& fieldset": {
- borderColor: theme.palette.primary.lowContrast,
- borderRadius: theme.shape.borderRadius,
- },
+ styleOverrides: {
+ root: ({ theme }) => ({
+ "& fieldset": {
+ borderColor: theme.palette.primary.lowContrast,
+ borderRadius: theme.shape.borderRadius,
+ },
- "& .MuiInputBase-input": {
- padding: ".75em",
- minHeight: "var(--env-var-height-2)",
- fontSize: "var(--env-var-font-size-medium)",
- fontWeight: 400,
- color: palette.primary.contrastTextSecondary,
- "&.Mui-disabled": {
- opacity: 0.3,
- WebkitTextFillColor: "unset",
- },
- "& .Mui-focused": {
- /* color: "#ff0000", */
- /* borderColor: theme.palette.primary.contrastText, */
- },
- },
+ "& .MuiInputBase-input": {
+ padding: ".75em",
+ minHeight: "var(--env-var-height-2)",
+ fontSize: "var(--env-var-font-size-medium)",
+ fontWeight: 400,
+ color: palette.primary.contrastTextSecondary,
+ "&.Mui-disabled": {
+ opacity: 0.3,
+ WebkitTextFillColor: "unset",
+ },
+ "& .Mui-focused": {
+ /* color: "#ff0000", */
+ /* borderColor: theme.palette.primary.contrastText, */
+ },
+ },
- "& .MuiInputBase-input:-webkit-autofill": {
- transition: "background-color 5000s ease-in-out 0s",
- WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.primary.main} inset`,
- WebkitTextFillColor: theme.palette.primary.contrastText,
- },
+ "& .MuiInputBase-input:-webkit-autofill": {
+ transition: "background-color 5000s ease-in-out 0s",
+ WebkitBoxShadow: `0 0 0px 1000px ${theme.palette.primary.main} inset`,
+ WebkitTextFillColor: theme.palette.primary.contrastText,
+ },
- "& .MuiInputBase-input.MuiOutlinedInput-input": {
- padding: "0 var(--env-var-spacing-1-minus) !important",
- },
+ "& .MuiInputBase-input.MuiOutlinedInput-input": {
+ padding: "0 var(--env-var-spacing-1-minus) !important",
+ },
- "& .MuiOutlinedInput-root": {
- color: theme.palette.primary.contrastTextSecondary,
- borderRadius: 4,
- },
+ "& .MuiOutlinedInput-root": {
+ color: theme.palette.primary.contrastTextSecondary,
+ borderRadius: 4,
+ },
- "& .MuiOutlinedInput-notchedOutline": {
- borderRadius: 4,
- },
+ "& .MuiOutlinedInput-notchedOutline": {
+ borderRadius: 4,
+ },
- "& .MuiFormHelperText-root": {
- color: palette.error.main,
- opacity: 0.8,
- fontSize: "var(--env-var-font-size-medium)",
- marginLeft: 0,
- },
+ "& .MuiFormHelperText-root": {
+ color: palette.error.main,
+ opacity: 0.8,
+ fontSize: "var(--env-var-font-size-medium)",
+ marginLeft: 0,
+ },
- "& .MuiFormHelperText-root.Mui-error": {
- opacity: 0.8,
- fontSize: "var(--env-var-font-size-medium)",
- color: palette.error.main,
- whiteSpace: "nowrap",
- },
- }),
- },
+ "& .MuiFormHelperText-root.Mui-error": {
+ opacity: 0.8,
+ fontSize: "var(--env-var-font-size-medium)",
+ color: palette.error.main,
+ whiteSpace: "nowrap",
+ },
+ }),
+ },
},
MuiOutlinedInput: {
From b58fe4ae74bd18fed1916d687e2b1f3ecfe51b46 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 12 Jun 2025 09:04:19 +0800
Subject: [PATCH 110/319] add loading state
---
client/src/Pages/Auth/Register/index.jsx | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx
index 2dd0951dd..97b27e5cf 100644
--- a/client/src/Pages/Auth/Register/index.jsx
+++ b/client/src/Pages/Auth/Register/index.jsx
@@ -11,7 +11,7 @@ import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
-import { useDispatch } from "react-redux";
+import { useDispatch, useSelector } from "react-redux";
import { useParams } from "react-router-dom";
import { networkService } from "../../../main";
import { newOrChangedCredentials } from "../../../Validation/validation";
@@ -35,6 +35,9 @@ const getFeedbackStatus = (form, errors, field, criteria) => {
};
const Register = ({ superAdminExists }) => {
+ // Redux
+ const { isLoading } = useSelector((state) => state.auth);
+
// Local state
const [form, setForm] = useState({
firstName: "",
@@ -203,6 +206,8 @@ const Register = ({ superAdminExists }) => {
alignItems="center"
gap={theme.spacing(10)}
>
+ {t("auth.registration.heading.user")}
+
{
/>
Date: Thu, 12 Jun 2025 09:37:51 +0800
Subject: [PATCH 111/319] add a background component
---
client/src/App.jsx | 16 ++----
.../Components/Layouts/AppLayout/index.jsx | 30 ++++++++++++
client/src/Components/Sidebar/index.jsx | 1 -
client/src/Components/StarPrompt/index.jsx | 1 -
client/src/Utils/Theme/constants.js | 7 +++
client/src/assets/Images/background.svg | 49 +++++++++++++++++++
6 files changed, 91 insertions(+), 13 deletions(-)
create mode 100644 client/src/Components/Layouts/AppLayout/index.jsx
create mode 100644 client/src/assets/Images/background.svg
diff --git a/client/src/App.jsx b/client/src/App.jsx
index 7fb63ee61..b599890f1 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import WalletProvider from "./Components/WalletProvider";
+import AppLayout from "./Components/Layouts/AppLayout";
function App() {
const mode = useSelector((state) => state.ui.mode);
@@ -27,17 +28,10 @@ function App() {
- {
- return {
- body: {
- backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`,
- color: palette.primary.contrastText,
- },
- };
- }}
- />
-
+
+
+
+
diff --git a/client/src/Components/Layouts/AppLayout/index.jsx b/client/src/Components/Layouts/AppLayout/index.jsx
new file mode 100644
index 000000000..564012d72
--- /dev/null
+++ b/client/src/Components/Layouts/AppLayout/index.jsx
@@ -0,0 +1,30 @@
+import Box from "@mui/material/Box";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import BackgroundSVG from "../../../assets/Images/background.svg";
+
+const AppLayout = ({ children }) => {
+ const theme = useTheme();
+
+ return (
+
+ {children}
+
+ );
+};
+
+AppLayout.propTypes = {
+ children: PropTypes.node,
+};
+
+export default AppLayout;
diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx
index 6be67a43d..f487148f5 100644
--- a/client/src/Components/Sidebar/index.jsx
+++ b/client/src/Components/Sidebar/index.jsx
@@ -231,7 +231,6 @@ function Sidebar() {
borderRight: `1px solid ${theme.palette.primary.lowContrast}`,
borderColor: theme.palette.primary.lowContrast,
borderRadius: 0,
- backgroundColor: theme.palette.primary.main,
"& :is(p, span, .MuiListSubheader-root)": {
/*
Text color for unselected menu items and menu headings
diff --git a/client/src/Components/StarPrompt/index.jsx b/client/src/Components/StarPrompt/index.jsx
index 8bf434085..64fce1d3f 100644
--- a/client/src/Components/StarPrompt/index.jsx
+++ b/client/src/Components/StarPrompt/index.jsx
@@ -33,7 +33,6 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0,
gap: theme.spacing(1.5),
- backgroundColor: theme.palette.primary.main,
}}
>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
From 948e5e932b824037799a10b35c4349af814bb3f7 Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 12 Jun 2025 12:44:35 +0800
Subject: [PATCH 112/319] refactor hooks to common util
---
.../checkHooks.js} | 15 +-
client/src/Hooks/monitorHooks.js | 379 ++++++++++++++++++
.../src/Hooks/useFetchMonitorsWithChecks.js | 72 ----
.../src/Hooks/useFetchMonitorsWithSummary.js | 37 --
client/src/Hooks/useFetchUptimeMonitorById.js | 35 --
.../src/Hooks/useFetchUptimeMonitorDetails.js | 35 --
client/src/Hooks/useMonitorControls.js | 28 --
client/src/Hooks/useMonitorUtils.js | 22 +-
.../Hooks/useHardwareMonitorsFetch.jsx | 38 --
.../Monitors/Hooks/useMonitorFetch.jsx | 46 ---
.../Details/Hooks/useMonitorFetch.jsx | 39 --
.../Monitors/Hooks/useMonitorsFetch.jsx | 44 --
.../Uptime/Details/Hooks/useMonitorFetch.jsx | 33 --
.../Monitors/Hooks/useMonitorsFetch.jsx | 77 ----
.../Pages/Uptime/Monitors/Hooks/useUtils.jsx | 110 -----
15 files changed, 405 insertions(+), 605 deletions(-)
rename client/src/{Pages/Uptime/Details/Hooks/useChecksFetch.jsx => Hooks/checkHooks.js} (81%)
create mode 100644 client/src/Hooks/monitorHooks.js
delete mode 100644 client/src/Hooks/useFetchMonitorsWithChecks.js
delete mode 100644 client/src/Hooks/useFetchMonitorsWithSummary.js
delete mode 100644 client/src/Hooks/useFetchUptimeMonitorById.js
delete mode 100644 client/src/Hooks/useFetchUptimeMonitorDetails.js
delete mode 100644 client/src/Hooks/useMonitorControls.js
delete mode 100644 client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
delete mode 100644 client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx
delete mode 100644 client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx
delete mode 100644 client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx
delete mode 100644 client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx
delete mode 100644 client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx
delete mode 100644 client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx
diff --git a/client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx b/client/src/Hooks/checkHooks.js
similarity index 81%
rename from client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
rename to client/src/Hooks/checkHooks.js
index 586cc8359..cc942b294 100644
--- a/client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
+++ b/client/src/Hooks/checkHooks.js
@@ -1,14 +1,9 @@
import { useState } from "react";
import { useEffect } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-export const useChecksFetch = ({
- monitorId,
- monitorType,
- dateRange,
- page,
- rowsPerPage,
-}) => {
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+
+const useFetchChecks = ({ monitorId, monitorType, dateRange, page, rowsPerPage }) => {
const [checks, setChecks] = useState(undefined);
const [checksCount, setChecksCount] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
@@ -47,4 +42,4 @@ export const useChecksFetch = ({
return [checks, checksCount, isLoading, networkError];
};
-export default useChecksFetch;
+export { useFetchChecks };
diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js
new file mode 100644
index 000000000..a39fd3c5e
--- /dev/null
+++ b/client/src/Hooks/monitorHooks.js
@@ -0,0 +1,379 @@
+import { useEffect, useState } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTheme } from "@emotion/react";
+import { useMonitorUtils } from "./useMonitorUtils";
+import { useNavigate } from "react-router-dom";
+
+const useFetchMonitorsWithSummary = ({ teamId, types, monitorUpdateTrigger }) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [monitors, setMonitors] = useState(undefined);
+ const [monitorsSummary, setMonitorsSummary] = useState(undefined);
+ const [networkError, setNetworkError] = useState(false);
+
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.getMonitorsWithSummaryByTeamId({
+ teamId,
+ types,
+ });
+ const { monitors, summary } = res?.data?.data ?? {};
+ setMonitors(monitors);
+ setMonitorsSummary(summary);
+ } catch (error) {
+ console.error(error);
+ setNetworkError(true);
+ createToast({
+ body: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [teamId, types, monitorUpdateTrigger]);
+ return [monitors, monitorsSummary, isLoading, networkError];
+};
+
+const useFetchMonitorsWithChecks = ({
+ teamId,
+ types,
+ limit,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ monitorUpdateTrigger,
+}) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [count, setCount] = useState(undefined);
+ const [monitors, setMonitors] = useState(undefined);
+ const [networkError, setNetworkError] = useState(false);
+
+ const theme = useTheme();
+ const { getMonitorWithPercentage } = useMonitorUtils();
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.getMonitorsWithChecksByTeamId({
+ teamId,
+ limit,
+ types,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ });
+ const { count, monitors } = res?.data?.data ?? {};
+ const mappedMonitors = monitors.map((monitor) =>
+ getMonitorWithPercentage(monitor, theme)
+ );
+ setMonitors(mappedMonitors);
+ setCount(count?.monitorsCount ?? 0);
+ } catch (error) {
+ console.error(error);
+ setNetworkError(true);
+ createToast({
+ body: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [
+ field,
+ filter,
+ getMonitorWithPercentage,
+ limit,
+ order,
+ page,
+ rowsPerPage,
+ teamId,
+ theme,
+ types,
+ monitorUpdateTrigger,
+ ]);
+ return [monitors, count, isLoading, networkError];
+};
+
+const useFetchMonitorsByTeamId = ({
+ teamId,
+ types,
+ limit,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ updateTrigger,
+}) => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [monitors, setMonitors] = useState(undefined);
+ const [summary, setSummary] = useState(undefined);
+ const [networkError, setNetworkError] = useState(false);
+
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.getMonitorsByTeamId({
+ teamId: teamId,
+ limit,
+ types,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ });
+ if (res?.data?.data?.filteredMonitors) {
+ setMonitors(res.data.data.filteredMonitors);
+ setSummary(res.data.data.summary);
+ }
+ } catch (error) {
+ setNetworkError(true);
+ createToast({
+ body: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [teamId, types, limit, page, rowsPerPage, filter, field, order, updateTrigger]);
+ return [monitors, summary, isLoading, networkError];
+};
+
+const useFetchStatsByMonitorId = ({
+ monitorId,
+ sortOrder,
+ limit,
+ dateRange,
+ numToDisplay,
+ normalize,
+}) => {
+ const [monitor, setMonitor] = useState(undefined);
+ const [audits, setAudits] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(true);
+ const [networkError, setNetworkError] = useState(false);
+ useEffect(() => {
+ const fetchMonitor = async () => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.getStatsByMonitorId({
+ monitorId: monitorId,
+ sortOrder,
+ limit,
+ dateRange,
+ numToDisplay,
+ normalize,
+ });
+ setMonitor(res?.data?.data ?? undefined);
+ setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined);
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error.message });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitor();
+ }, [monitorId, dateRange, numToDisplay, normalize, sortOrder, limit]);
+ return [monitor, audits, isLoading, networkError];
+};
+
+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();
+ }, [monitorId, setMonitor, updateTrigger]);
+ return [isLoading];
+};
+
+const useFetchHardwareMonitorById = ({ monitorId, dateRange }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [networkError, setNetworkError] = useState(false);
+ const [monitor, setMonitor] = useState(undefined);
+
+ useEffect(() => {
+ const fetchMonitor = async () => {
+ try {
+ if (!monitorId) {
+ return { monitor: undefined, isLoading: false, networkError: undefined };
+ }
+ const response = await networkService.getHardwareDetailsByMonitorId({
+ monitorId: monitorId,
+ dateRange: dateRange,
+ });
+ setMonitor(response.data.data);
+ } catch (error) {
+ setNetworkError(true);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitor();
+ }, [monitorId, dateRange]);
+ return [monitor, isLoading, networkError];
+};
+
+const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => {
+ const [networkError, setNetworkError] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [monitor, setMonitor] = useState(undefined);
+ const [monitorStats, setMonitorStats] = useState(undefined);
+ useEffect(() => {
+ const fetchMonitors = async () => {
+ try {
+ const res = await networkService.getUptimeDetailsById({
+ monitorId: monitorId,
+ dateRange: dateRange,
+ normalize: true,
+ });
+ const { monitorData, monitorStats } = res?.data?.data ?? {};
+ setMonitor(monitorData);
+ setMonitorStats(monitorStats);
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error.message });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchMonitors();
+ }, [dateRange, monitorId, trigger]);
+ return [monitor, monitorStats, isLoading, networkError];
+};
+
+const useCreateMonitor = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const navigate = useNavigate();
+ const createMonitor = async ({ monitor, redirect }) => {
+ try {
+ setIsLoading(true);
+ await networkService.createMonitor({ monitor });
+ createToast({ body: "Monitor created successfully!" });
+ if (redirect) {
+ navigate(redirect);
+ }
+ } catch (error) {
+ createToast({ body: "Failed to create monitor." });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return [createMonitor, isLoading];
+};
+
+const useDeleteMonitor = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const navigate = useNavigate();
+ const deleteMonitor = async ({ monitor, redirect }) => {
+ try {
+ setIsLoading(true);
+ await networkService.deleteMonitorById({ monitorId: monitor._id });
+ createToast({ body: "Monitor deleted successfully!" });
+ if (redirect) {
+ navigate(redirect);
+ }
+ } catch (error) {
+ createToast({ body: "Failed to delete monitor." });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return [deleteMonitor, isLoading];
+};
+
+const useUpdateMonitor = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const navigate = useNavigate();
+ const updateMonitor = async ({ monitor, redirect }) => {
+ try {
+ setIsLoading(true);
+ const updatedFields = {
+ name: monitor.name,
+ description: monitor.description,
+ interval: monitor.interval,
+ notifications: monitor.notifications,
+ matchMethod: monitor.matchMethod,
+ expectedValue: monitor.expectedValue,
+ ignoreTlsErrors: monitor.ignoreTlsErrors,
+ jsonPath: monitor.jsonPath,
+ ...(monitor.type === "port" && { port: monitor.port }),
+ ...(monitor.type === "hardware" && {
+ thresholds: monitor.thresholds,
+ secret: monitor.secret,
+ }),
+ };
+ await networkService.updateMonitor({
+ monitorId: monitor._id,
+ updatedFields,
+ });
+
+ createToast({ body: "Monitor updated successfully!" });
+ if (redirect) {
+ navigate(redirect);
+ }
+ } catch (error) {
+ createToast({ body: "Failed to update monitor." });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return [updateMonitor, isLoading];
+};
+
+const usePauseMonitor = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const pauseMonitor = async ({ monitorId, triggerUpdate }) => {
+ try {
+ setIsLoading(true);
+ const res = await networkService.pauseMonitorById({ monitorId });
+ createToast({
+ body: res.data.data.isActive
+ ? "Monitor resumed successfully"
+ : "Monitor paused successfully",
+ });
+ triggerUpdate();
+ } catch (error) {
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [pauseMonitor, isLoading, error];
+};
+
+export {
+ useFetchMonitorsWithSummary,
+ useFetchMonitorsWithChecks,
+ useFetchMonitorsByTeamId,
+ useFetchStatsByMonitorId,
+ useFetchMonitorById,
+ useFetchUptimeMonitorById,
+ useFetchHardwareMonitorById,
+ useCreateMonitor,
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+};
diff --git a/client/src/Hooks/useFetchMonitorsWithChecks.js b/client/src/Hooks/useFetchMonitorsWithChecks.js
deleted file mode 100644
index a6f0437b9..000000000
--- a/client/src/Hooks/useFetchMonitorsWithChecks.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "./useMonitorUtils";
-
-export const useFetchMonitorsWithChecks = ({
- teamId,
- types,
- limit,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- monitorUpdateTrigger,
-}) => {
- const [isLoading, setIsLoading] = useState(false);
- const [count, setCount] = useState(undefined);
- const [monitors, setMonitors] = useState(undefined);
- const [networkError, setNetworkError] = useState(false);
-
- const theme = useTheme();
- const { getMonitorWithPercentage } = useMonitorUtils();
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.getMonitorsWithChecksByTeamId({
- teamId,
- limit,
- types,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- });
- const { count, monitors } = res?.data?.data ?? {};
- const mappedMonitors = monitors.map((monitor) =>
- getMonitorWithPercentage(monitor, theme)
- );
- setMonitors(mappedMonitors);
- setCount(count?.monitorsCount ?? 0);
- } catch (error) {
- console.error(error);
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitors();
- }, [
- field,
- filter,
- getMonitorWithPercentage,
- limit,
- order,
- page,
- rowsPerPage,
- teamId,
- theme,
- types,
- monitorUpdateTrigger,
- ]);
- return [monitors, count, isLoading, networkError];
-};
-
-export default useFetchMonitorsWithChecks;
diff --git a/client/src/Hooks/useFetchMonitorsWithSummary.js b/client/src/Hooks/useFetchMonitorsWithSummary.js
deleted file mode 100644
index cd21271d4..000000000
--- a/client/src/Hooks/useFetchMonitorsWithSummary.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-
-export const useFetchMonitorsWithSummary = ({ teamId, types, monitorUpdateTrigger }) => {
- const [isLoading, setIsLoading] = useState(false);
- const [monitors, setMonitors] = useState(undefined);
- const [monitorsSummary, setMonitorsSummary] = useState(undefined);
- const [networkError, setNetworkError] = useState(false);
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.getMonitorsWithSummaryByTeamId({
- teamId,
- types,
- });
- const { monitors, summary } = res?.data?.data ?? {};
- setMonitors(monitors);
- setMonitorsSummary(summary);
- } catch (error) {
- console.error(error);
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitors();
- }, [teamId, types, monitorUpdateTrigger]);
- return [monitors, monitorsSummary, isLoading, networkError];
-};
-
-export default useFetchMonitorsWithSummary;
diff --git a/client/src/Hooks/useFetchUptimeMonitorById.js b/client/src/Hooks/useFetchUptimeMonitorById.js
deleted file mode 100644
index cb344569c..000000000
--- a/client/src/Hooks/useFetchUptimeMonitorById.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useState, useEffect } from "react";
-import { useDispatch } from "react-redux";
-import { getUptimeMonitorById } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { useNavigate } from "react-router";
-
-const useFetchUptimeMonitorById = (monitorId, updateTrigger) => {
- const [isLoading, setIsLoading] = useState(true);
- const [error, setError] = useState(null);
- const [monitor, setMonitor] = useState(null);
- const navigate = useNavigate();
- const dispatch = useDispatch();
- useEffect(() => {
- const fetchMonitor = async () => {
- try {
- setIsLoading(true);
- const action = await dispatch(getUptimeMonitorById({ monitorId }));
-
- if (getUptimeMonitorById.fulfilled.match(action)) {
- const monitor = action.payload.data;
- setMonitor(monitor);
- } else if (getUptimeMonitorById.rejected.match(action)) {
- throw new Error(action.error.message);
- }
- } catch (error) {
- navigate("/not-found", { replace: true });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitor();
- }, [monitorId, dispatch, navigate, updateTrigger]);
- return [monitor, isLoading, error];
-};
-
-export { useFetchUptimeMonitorById };
diff --git a/client/src/Hooks/useFetchUptimeMonitorDetails.js b/client/src/Hooks/useFetchUptimeMonitorDetails.js
deleted file mode 100644
index 3c0ac7ee6..000000000
--- a/client/src/Hooks/useFetchUptimeMonitorDetails.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../main";
-import { useNavigate } from "react-router-dom";
-import { createToast } from "../Utils/toastUtils";
-
-export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => {
- const [networkError, setNetworkError] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [monitor, setMonitor] = useState(undefined);
- const [monitorStats, setMonitorStats] = useState(undefined);
- const navigate = useNavigate();
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- const res = await networkService.getUptimeDetailsById({
- monitorId: monitorId,
- dateRange: dateRange,
- normalize: true,
- });
- const { monitorData, monitorStats } = res?.data?.data ?? {};
- setMonitor(monitorData);
- setMonitorStats(monitorStats);
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitors();
- }, [dateRange, monitorId, navigate, trigger]);
- return [monitor, monitorStats, isLoading, networkError];
-};
-
-export default useFetchUptimeMonitorDetails;
diff --git a/client/src/Hooks/useMonitorControls.js b/client/src/Hooks/useMonitorControls.js
deleted file mode 100644
index b47603b58..000000000
--- a/client/src/Hooks/useMonitorControls.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import { useState } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-
-const usePauseMonitor = ({ monitorId, triggerUpdate }) => {
- const [isLoading, setIsLoading] = useState(false);
- const [error, setError] = useState(undefined);
- const pauseMonitor = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.pauseMonitorById({ monitorId });
- createToast({
- body: res.data.data.isActive
- ? "Monitor resumed successfully"
- : "Monitor paused successfully",
- });
- triggerUpdate();
- } catch (error) {
- setError(error);
- } finally {
- setIsLoading(false);
- }
- };
-
- return [pauseMonitor, isLoading, error];
-};
-
-export { usePauseMonitor };
diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/useMonitorUtils.js
index 424112a81..417c6eadd 100644
--- a/client/src/Hooks/useMonitorUtils.js
+++ b/client/src/Hooks/useMonitorUtils.js
@@ -46,7 +46,27 @@ const useMonitorUtils = () => {
pending: theme.palette.warning.lowContrast,
};
- return { getMonitorWithPercentage, determineState, statusColor };
+ const statusToTheme = {
+ up: "success",
+ down: "error",
+ paused: "warning",
+ pending: "secondary",
+ "cannot resolve": "tertiary",
+ };
+
+ const pagespeedStatusMsg = {
+ up: "Live (collecting data)",
+ down: "Inactive",
+ paused: "Paused",
+ };
+
+ return {
+ getMonitorWithPercentage,
+ determineState,
+ statusColor,
+ statusToTheme,
+ pagespeedStatusMsg,
+ };
};
export { useMonitorUtils };
diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
deleted file mode 100644
index c0b61ef62..000000000
--- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareMonitorsFetch.jsx
+++ /dev/null
@@ -1,38 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-
-const useHardwareMonitorsFetch = ({ monitorId, dateRange }) => {
- // Abort early if creating monitor
-
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- const [monitor, setMonitor] = useState(undefined);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- if (!monitorId) {
- return { monitor: undefined, isLoading: false, networkError: undefined };
- }
- const response = await networkService.getHardwareDetailsByMonitorId({
- monitorId: monitorId,
- dateRange: dateRange,
- });
- setMonitor(response.data.data);
- } catch (error) {
- setNetworkError(true);
- } finally {
- setIsLoading(false);
- }
- };
- fetchData();
- }, [monitorId, dateRange]);
-
- return {
- isLoading,
- networkError,
- monitor,
- };
-};
-
-export { useHardwareMonitorsFetch };
diff --git a/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx b/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx
deleted file mode 100644
index 883205c77..000000000
--- a/client/src/Pages/Infrastructure/Monitors/Hooks/useMonitorFetch.jsx
+++ /dev/null
@@ -1,46 +0,0 @@
-import { useState, useEffect } from "react";
-import { useSelector } from "react-redux";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-
-const useMonitorFetch = ({ page, field, filter, rowsPerPage, updateTrigger }) => {
- // Redux state
- const { user } = useSelector((state) => state.auth);
-
- // Local state
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- const [monitors, setMonitors] = useState(undefined);
- const [summary, setSummary] = useState(undefined);
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- const response = await networkService.getMonitorsByTeamId({
- teamId: user.teamId,
- limit: 1,
- field: field,
- filter: filter,
- types: ["hardware"],
- page: page,
- rowsPerPage: rowsPerPage,
- });
- setMonitors(response?.data?.data?.filteredMonitors ?? []);
- setSummary(response?.data?.data?.summary ?? {});
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchMonitors();
- }, [page, field, filter, rowsPerPage, user.teamId, updateTrigger]);
-
- return { monitors, summary, isLoading, networkError };
-};
-
-export { useMonitorFetch };
diff --git a/client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx b/client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx
deleted file mode 100644
index 18ffd8782..000000000
--- a/client/src/Pages/PageSpeed/Details/Hooks/useMonitorFetch.jsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-import { useNavigate } from "react-router-dom";
-const useMonitorFetch = ({ monitorId }) => {
- const navigate = useNavigate();
-
- const [monitor, setMonitor] = useState(undefined);
- const [audits, setAudits] = useState(undefined);
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- useEffect(() => {
- const fetchMonitor = async () => {
- try {
- const res = await networkService.getStatsByMonitorId({
- monitorId: monitorId,
- sortOrder: "desc",
- limit: 50,
- dateRange: "day",
- numToDisplay: null,
- normalize: null,
- });
- setMonitor(res?.data?.data ?? undefined);
- setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined);
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchMonitor();
- }, [monitorId, navigate]);
-
- return { monitor, audits, isLoading };
-};
-
-export { useMonitorFetch };
diff --git a/client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx b/client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx
deleted file mode 100644
index d2f0f4842..000000000
--- a/client/src/Pages/PageSpeed/Monitors/Hooks/useMonitorsFetch.jsx
+++ /dev/null
@@ -1,44 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-
-const useMonitorsFetch = ({ teamId }) => {
- const [isLoading, setIsLoading] = useState(true);
- const [monitors, setMonitors] = useState([]);
- const [summary, setSummary] = useState({});
- const [networkError, setNetworkError] = useState(false);
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.getMonitorsByTeamId({
- teamId: teamId,
- limit: 10,
- types: ["pagespeed"],
- page: null,
- rowsPerPage: null,
- filter: null,
- field: null,
- order: null,
- });
- if (res?.data?.data?.filteredMonitors) {
- setMonitors(res.data.data.filteredMonitors);
- setSummary(res.data.data.summary);
- }
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchMonitors();
- }, [teamId]);
- return { isLoading, monitors, summary, networkError };
-};
-
-export default useMonitorsFetch;
diff --git a/client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx
deleted file mode 100644
index 9491e7cc3..000000000
--- a/client/src/Pages/Uptime/Details/Hooks/useMonitorFetch.jsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { useNavigate } from "react-router-dom";
-import { createToast } from "../../../../Utils/toastUtils";
-
-export const useMonitorFetch = ({ monitorId, dateRange }) => {
- const [networkError, setNetworkError] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [monitor, setMonitor] = useState(undefined);
- const navigate = useNavigate();
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- const res = await networkService.getUptimeDetailsById({
- monitorId: monitorId,
- dateRange: dateRange,
- normalize: true,
- });
- setMonitor(res?.data?.data ?? {});
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitors();
- }, [dateRange, monitorId, navigate]);
- return [monitor, isLoading, networkError];
-};
-
-export default useMonitorFetch;
diff --git a/client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx b/client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx
deleted file mode 100644
index 1f1d15212..000000000
--- a/client/src/Pages/Uptime/Monitors/Hooks/useMonitorsFetch.jsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils";
-
-export const useMonitorFetch = ({
- teamId,
- limit,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- triggerUpdate,
-}) => {
- const [monitorsAreLoading, setMonitorsAreLoading] = useState(false);
- const [monitors, setMonitors] = useState(undefined);
- const [filteredMonitors, setFilteredMonitors] = useState(undefined);
- const [monitorsSummary, setMonitorsSummary] = useState(undefined);
- const [networkError, setNetworkError] = useState(false);
-
- const theme = useTheme();
- const { getMonitorWithPercentage } = useMonitorUtils();
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- setMonitorsAreLoading(true);
- const res = await networkService.getMonitorsByTeamId({
- teamId,
- limit,
- types: ["http", "ping", "docker", "port"],
- page,
- rowsPerPage,
- filter,
- field,
- order,
- });
- const { monitors, filteredMonitors, summary } = res.data.data;
- const mappedMonitors = filteredMonitors.map((monitor) =>
- getMonitorWithPercentage(monitor, theme)
- );
- setMonitors(monitors);
- setFilteredMonitors(mappedMonitors);
- setMonitorsSummary(summary);
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setMonitorsAreLoading(false);
- }
- };
- fetchMonitors();
- }, [
- teamId,
- limit,
- field,
- filter,
- order,
- page,
- rowsPerPage,
- theme,
- triggerUpdate,
- getMonitorWithPercentage,
- ]);
- return {
- monitors,
- filteredMonitors,
- monitorsSummary,
- monitorsAreLoading,
- networkError,
- };
-};
-
-export default useMonitorFetch;
diff --git a/client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx b/client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx
deleted file mode 100644
index ff3288cdf..000000000
--- a/client/src/Pages/Uptime/Monitors/Hooks/useUtils.jsx
+++ /dev/null
@@ -1,110 +0,0 @@
-import { useTheme } from "@mui/material";
-
-const useUtils = () => {
- const determineState = (monitor) => {
- if (typeof monitor === "undefined") return "pending";
- if (monitor.isActive === false) return "paused";
- if (monitor?.status === undefined) return "pending";
- return monitor?.status == true ? "up" : "down";
- };
-
- /* TODO Refactor: from here on shouldn't live in a custom hook, but on theme, or constants */
- const theme = useTheme();
-
- const statusColor = {
- up: theme.palette.success.lowContrast,
- down: theme.palette.error.lowContrast,
- paused: theme.palette.warning.lowContrast,
- pending: theme.palette.warning.lowContrast,
- };
-
- const statusMsg = {
- up: "Your site is up.",
- down: "Your site is down.",
- paused: "Pending...",
- };
-
- const pagespeedStatusMsg = {
- up: "Live (collecting data)",
- down: "Inactive",
- paused: "Paused",
- };
-
- /*
- TODO
- This is used on
- 1) Details > Gradient card */
- /* These are rediections. We should do something that maps up to success, down to error, and get the theme by that
- See Client\src\Components\Label\index.jsx
- */
-
- const statusToTheme = {
- up: "success",
- down: "error",
- paused: "warning",
- pending: "secondary",
- "cannot resolve": "tertiary",
- };
-
- const getStatusStyles = (status) => {
- const themeColor = statusToTheme[status];
-
- return {
- backgroundColor: theme.palette[themeColor].lowContrast,
- background: `linear-gradient(340deg, ${theme.palette[themeColor].main} -60%, ${theme.palette[themeColor].lowContrast} 35%)`,
- borderColor: theme.palette[themeColor].lowContrast,
- "& h2": {
- color: theme.palette[themeColor].contrastText,
- textTransform: "uppercase",
- },
- "& p": {
- color: theme.palette[themeColor].contrastText,
- },
- };
- };
-
- const statusStyles = {
- up: {
- backgroundColor: theme.palette.success.lowContrast,
- background: `linear-gradient(340deg, ${theme.palette.tertiary.main} -60%, ${theme.palette.success.lowContrast} 35%)`, // CAIO_REVIEW
- borderColor: theme.palette.success.contrastText,
- // "& h2": { color: theme.palette.success.contrastText }, // CAIO_REVIEW
- },
- down: {
- backgroundColor: theme.palette.error.lowContrast,
- background: `linear-gradient(340deg, ${theme.palette.tertiary.main} -60%, ${theme.palette.error.lowContrast} 35%)`, // CAIO_REVIEW
- borderColor: theme.palette.error.contrastText,
- "& h2": { color: theme.palette.error.contrastText }, // CAIO_REVIEW
- "& .MuiTypography-root": { color: theme.palette.error.contrastText }, // CAIO_REVIEW
- },
- paused: {
- backgroundColor: theme.palette.warning.lowContrast,
- background: `linear-gradient(340deg, ${theme.palette.tertiary.main} -60%, ${theme.palette.warning.lowContrast} 35%)`, // CAIO_REVIEW
- borderColor: theme.palette.warning.contrastText,
- "& h2": { color: theme.palette.warning.contrastText }, // CAIO_REVIEW
- "& .MuiTypography-root": { color: theme.palette.warning.contrastText }, // CAIO_REVIEW
- },
- pending: {
- backgroundColor: theme.palette.warning.lowContrast,
- background: `linear-gradient(340deg, ${theme.palette.tertiary.main} -60%, ${theme.palette.warning.lowContrast} 35%)`, // CAIO_REVIEW
- borderColor: theme.palette.warning.contrastText,
- "& h2": { color: theme.palette.warning.contrastText }, // CAIO_REVIEW
- },
- };
-
- /* These are rediections. We should do something that maps up to success, down to error, and get the theme by that
-
- */
-
- return {
- determineState,
- statusColor,
- statusMsg,
- pagespeedStatusMsg,
- statusStyles,
- statusToTheme,
- getStatusStyles,
- };
-};
-
-export default useUtils;
From 50986d44938243351e98214371cad3b5cff18cbc Mon Sep 17 00:00:00 2001
From: Alex Holliday
Date: Thu, 12 Jun 2025 12:44:46 +0800
Subject: [PATCH 113/319] use hooks
---
.../src/Components/Inputs/Checkbox/index.jsx | 2 +-
.../MonitorDetailsControlHeader/index.jsx | 13 +-
.../MonitorDetailsControlHeader/status.jsx | 5 +-
.../Components/MonitorStatusHeader/index.jsx | 4 +-
client/src/Components/StatBox/index.jsx | 4 +-
.../src/Pages/Infrastructure/Create/index.jsx | 37 ++----
.../Details/Components/StatusBoxes/index.jsx | 4 +-
.../Pages/Infrastructure/Details/index.jsx | 4 +-
.../Components/MonitorsTable/index.jsx | 4 +-
.../Pages/Infrastructure/Monitors/index.jsx | 12 +-
.../src/Pages/PageSpeed/Configure/index.jsx | 91 ++++----------
client/src/Pages/PageSpeed/Create/index.jsx | 61 +++------
client/src/Pages/PageSpeed/Details/index.jsx | 10 +-
.../Monitors/Components/Card/index.jsx | 8 +-
client/src/Pages/PageSpeed/Monitors/index.jsx | 17 ++-
.../Status/Components/MonitorsList/index.jsx | 4 +-
client/src/Pages/Uptime/Configure/index.jsx | 116 +++++++++---------
client/src/Pages/Uptime/Create/index.jsx | 47 ++++---
.../Components/UptimeStatusBoxes/index.jsx | 4 +-
client/src/Pages/Uptime/Details/index.jsx | 13 +-
.../Components/UptimeDataTable/index.jsx | 4 +-
client/src/Pages/Uptime/Monitors/index.jsx | 8 +-
client/src/locales/en.json | 1 +
23 files changed, 204 insertions(+), 269 deletions(-)
diff --git a/client/src/Components/Inputs/Checkbox/index.jsx b/client/src/Components/Inputs/Checkbox/index.jsx
index 58b83c7c7..1497c44fb 100644
--- a/client/src/Components/Inputs/Checkbox/index.jsx
+++ b/client/src/Components/Inputs/Checkbox/index.jsx
@@ -105,7 +105,7 @@ const Checkbox = ({
};
Checkbox.propTypes = {
- id: PropTypes.string.isRequired,
+ id: PropTypes.string,
name: PropTypes.string,
label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired,
size: PropTypes.oneOf(["small", "medium", "large"]),
diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx
index aa9ef91b9..8c9f845e9 100644
--- a/client/src/Components/MonitorDetailsControlHeader/index.jsx
+++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx
@@ -11,10 +11,9 @@ import EmailIcon from "@mui/icons-material/Email";
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
-import { usePauseMonitor } from "../../Hooks/useMonitorControls";
+import { usePauseMonitor } from "../../Hooks/monitorHooks";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { useTranslation } from "react-i18next";
-
/**
* MonitorDetailsControlHeader component displays the control header for monitor details.
* It includes status display, pause/resume button, and a configure button for admins.
@@ -38,10 +37,7 @@ const MonitorDetailsControlHeader = ({
const navigate = useNavigate();
const theme = useTheme();
const { t } = useTranslation();
- const [pauseMonitor, isPausing, error] = usePauseMonitor({
- monitorId: monitor?._id,
- triggerUpdate,
- });
+ const [pauseMonitor, isPausing, error] = usePauseMonitor();
const [isSending, emailError, sendTestEmail] = useSendTestEmail();
@@ -88,7 +84,10 @@ const MonitorDetailsControlHeader = ({
monitor?.isActive ? :
}
onClick={() => {
- pauseMonitor();
+ pauseMonitor({
+ monitorId: monitor?._id,
+ triggerUpdate,
+ });
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
diff --git a/client/src/Components/MonitorDetailsControlHeader/status.jsx b/client/src/Components/MonitorDetailsControlHeader/status.jsx
index 3d6e62c60..fdfb955f4 100644
--- a/client/src/Components/MonitorDetailsControlHeader/status.jsx
+++ b/client/src/Components/MonitorDetailsControlHeader/status.jsx
@@ -7,8 +7,7 @@ import Dot from "../../Components/Dot";
import { formatDurationRounded } from "../../Utils/timeUtils";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
-import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
-
+import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
/**
* Status component displays the status information of a monitor.
* It includes the monitor's name, URL, and check interval.
@@ -23,7 +22,7 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
*/
const Status = ({ monitor }) => {
const theme = useTheme();
- const { statusColor, determineState } = useUtils();
+ const { statusColor, determineState } = useMonitorUtils();
return (
diff --git a/client/src/Components/MonitorStatusHeader/index.jsx b/client/src/Components/MonitorStatusHeader/index.jsx
index 7ea95410b..a81131152 100644
--- a/client/src/Components/MonitorStatusHeader/index.jsx
+++ b/client/src/Components/MonitorStatusHeader/index.jsx
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
import PulseDot from "../Animated/PulseDot";
import Dot from "../Dot";
import { useTheme } from "@emotion/react";
-import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
import { formatDurationRounded } from "../../Utils/timeUtils";
import ConfigButton from "./ConfigButton";
import SkeletonLayout from "./skeleton";
@@ -12,7 +12,7 @@ import { useTranslation } from "react-i18next";
const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
const theme = useTheme();
const { t } = useTranslation();
- const { statusColor, determineState } = useUtils();
+ const { statusColor, determineState } = useMonitorUtils();
if (isLoading) {
return ;
}
diff --git a/client/src/Components/StatBox/index.jsx b/client/src/Components/StatBox/index.jsx
index 673736ea9..93fa46c2b 100644
--- a/client/src/Components/StatBox/index.jsx
+++ b/client/src/Components/StatBox/index.jsx
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
import Image from "../Image";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
-import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
/**
* StatBox Component
@@ -41,7 +41,7 @@ const StatBox = ({
sx,
}) => {
const theme = useTheme();
- const { statusToTheme } = useUtils();
+ const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx
index 9425b7429..fa63dd4dc 100644
--- a/client/src/Pages/Infrastructure/Create/index.jsx
+++ b/client/src/Pages/Infrastructure/Create/index.jsx
@@ -1,20 +1,17 @@
// React, Redux, Router
import { useTheme } from "@emotion/react";
-import { useNavigate, useParams } from "react-router-dom";
+import { useParams } from "react-router-dom";
import { useState, useEffect } from "react";
-import { useSelector, useDispatch } from "react-redux";
+import { useSelector } from "react-redux";
// Utility and Network
import { infrastructureMonitorValidation } from "../../../Validation/validation";
-import {
- createInfrastructureMonitor,
- updateInfrastructureMonitor,
-} from "../../../Features/InfrastructureMonitors/infrastructureMonitorsSlice";
-import { useHardwareMonitorsFetch } from "../Details/Hooks/useHardwareMonitorsFetch";
+import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { useTranslation } from "react-i18next";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import NotificationsConfig from "../../../Components/NotificationConfig";
+import { useUpdateMonitor, useCreateMonitor } from "../../../Hooks/monitorHooks";
// MUI
import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
@@ -55,9 +52,6 @@ const getAlertError = (errors) => {
const CreateInfrastructureMonitor = () => {
const theme = useTheme();
const { user } = useSelector((state) => state.auth);
- const monitorState = useSelector((state) => state.infrastructureMonitor);
- const dispatch = useDispatch();
- const navigate = useNavigate();
const { monitorId } = useParams();
const { t } = useTranslation();
@@ -65,9 +59,11 @@ const CreateInfrastructureMonitor = () => {
const isCreate = typeof monitorId === "undefined";
// Fetch monitor details if editing
- const { monitor, isLoading, networkError } = useHardwareMonitorsFetch({ monitorId });
+ const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({ monitorId });
const [notifications, notificationsAreLoading, notificationsError] =
useGetNotificationsByTeamId();
+ const [updateMonitor, isUpdating] = useUpdateMonitor();
+ const [createMonitor, isCreating] = useCreateMonitor();
// State
const [errors, setErrors] = useState({});
@@ -187,6 +183,7 @@ const CreateInfrastructureMonitor = () => {
};
form = {
+ ...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
teamId: user.teamId,
@@ -197,19 +194,9 @@ const CreateInfrastructureMonitor = () => {
};
// Handle create or update
- const action = isCreate
- ? await dispatch(createInfrastructureMonitor({ monitor: form }))
- : await dispatch(updateInfrastructureMonitor({ monitorId, monitor: form }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({
- body: isCreate
- ? t("infrastructureMonitorCreated")
- : t("infrastructureMonitorUpdated"),
- });
- navigate("/infrastructure");
- } else {
- createToast({ body: "Failed to save monitor." });
- }
+ isCreate
+ ? await createMonitor({ monitor: form, redirect: "/infrastructure" })
+ : await updateMonitor({ monitor: form, redirect: "/infrastructure" });
};
const onChange = (event) => {
@@ -448,7 +435,7 @@ const CreateInfrastructureMonitor = () => {
type="submit"
variant="contained"
color="accent"
- loading={monitorState?.isLoading}
+ loading={isLoading || isUpdating || isCreating || notificationsAreLoading}
>
{t(isCreate ? "infrastructureCreateMonitor" : "infrastructureEditMonitor")}
diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
index 66483d448..6c11e7c82 100644
--- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
@@ -4,14 +4,14 @@ import StatusBoxes from "../../../../../Components/StatusBoxes";
import StatBox from "../../../../../Components/StatBox";
//Utils
-import useUtils from "../../../../../Pages/Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import { useHardwareUtils } from "../../Hooks/useHardwareUtils";
import { useTranslation } from "react-i18next";
const InfraStatBoxes = ({ shouldRender, monitor }) => {
// Utils
const { formatBytes } = useHardwareUtils();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const { t } = useTranslation();
const { stats } = monitor ?? {};
diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx
index fefb7d2aa..842e0f6d9 100644
--- a/client/src/Pages/Infrastructure/Details/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/index.jsx
@@ -11,7 +11,7 @@ import GenericFallback from "../../../Components/GenericFallback";
// Utils
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useHardwareMonitorsFetch } from "./Hooks/useHardwareMonitorsFetch";
+import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useParams } from "react-router-dom";
@@ -33,7 +33,7 @@ const InfrastructureDetails = () => {
const { t } = useTranslation();
const isAdmin = useIsAdmin();
- const { isLoading, networkError, monitor } = useHardwareMonitorsFetch({
+ const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({
monitorId,
dateRange,
});
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
index 0ec435c60..761ecaa0e 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx
@@ -10,7 +10,7 @@ import CustomGauge from "../../../../../Components/Charts/CustomGauge";
// Utils
import { useTheme } from "@emotion/react";
-import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -19,7 +19,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
// Utils
const theme = useTheme();
const { t } = useTranslation();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const navigate = useNavigate();
// Handlers
diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx
index bfba843c0..6d49f64a7 100644
--- a/client/src/Pages/Infrastructure/Monitors/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/index.jsx
@@ -10,15 +10,20 @@ import Fallback from "../../../Components/Fallback";
import Filter from "./Components/Filters";
// Utils
import { useTheme } from "@emotion/react";
-import { useMonitorFetch } from "./Hooks/useMonitorFetch";
import { useState } from "react";
+import { useSelector } from "react-redux";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
+import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
// Constants
+const TYPES = ["hardware"];
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
const InfrastructureMonitors = () => {
// Redux state
+ const { user } = useSelector((state) => state.auth);
+
+ // Local state
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(5);
const [updateTrigger, setUpdateTrigger] = useState(false);
@@ -50,7 +55,10 @@ const InfrastructureMonitors = () => {
const field = toFilterStatus !== undefined ? "status" : undefined;
- const { monitors, summary, isLoading, networkError } = useMonitorFetch({
+ const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({
+ teamId: user.teamId,
+ limit: 1,
+ types: TYPES,
page,
field: field,
filter: toFilterStatus,
diff --git a/client/src/Pages/PageSpeed/Configure/index.jsx b/client/src/Pages/PageSpeed/Configure/index.jsx
index 487106247..71e1e111e 100644
--- a/client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/client/src/Pages/PageSpeed/Configure/index.jsx
@@ -12,45 +12,43 @@ import NotificationsConfig from "../../../Components/NotificationConfig";
import Dialog from "../../../Components/Dialog";
// Utils
-import { useEffect, useState } from "react";
+import { useState } from "react";
import { useTheme } from "@emotion/react";
-import { useDispatch, useSelector } from "react-redux";
-import { useNavigate, useParams } from "react-router";
-import {
- deletePageSpeed,
- getPagespeedMonitorById,
- getPageSpeedByTeamId,
- updatePageSpeed,
- pausePageSpeed,
-} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
+import { useParams } from "react-router";
import { monitorValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
import { useTranslation } from "react-i18next";
-import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
-
+import {
+ useFetchMonitorById,
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+} from "../../../Hooks/monitorHooks";
const PageSpeedConfigure = () => {
// Redux state
- const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
// Local state
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
- const [buttonLoading, setButtonLoading] = useState(false);
const [isOpen, setIsOpen] = useState(false);
+ const [updateTrigger, setUpdateTrigger] = useState(false);
// Utils
const theme = useTheme();
const { t } = useTranslation();
- const navigate = useNavigate();
- const dispatch = useDispatch();
const MS_PER_MINUTE = 60000;
const { monitorId } = useParams();
- const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
+ 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" },
@@ -61,24 +59,10 @@ const PageSpeedConfigure = () => {
{ _id: 10080, name: "1 week" },
];
- useEffect(() => {
- const fetchMonitor = async () => {
- try {
- const action = await dispatch(getPagespeedMonitorById({ monitorId }));
-
- if (getPagespeedMonitorById.fulfilled.match(action)) {
- const monitor = action.payload.data;
- setMonitor(monitor);
- } else if (getPagespeedMonitorById.rejected.match(action)) {
- throw new Error(action.error.message);
- }
- } catch (error) {
- logger.error("Error fetching monitor of id: " + monitorId);
- navigate("/not-found", { replace: true });
- }
- };
- fetchMonitor();
- }, [dispatch, monitorId, navigate]);
+ // Handlers
+ const triggerUpdate = () => {
+ setUpdateTrigger(!updateTrigger);
+ };
const onChange = (event) => {
let { value, name } = event.target;
@@ -106,42 +90,17 @@ const PageSpeedConfigure = () => {
};
const handlePause = async () => {
- try {
- const action = await dispatch(pausePageSpeed({ monitorId }));
- if (pausePageSpeed.fulfilled.match(action)) {
- const monitor = action.payload.data;
- setMonitor(monitor);
- const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
- createToast({ body: `Monitor ${state} successfully.` });
- } else if (pausePageSpeed.rejected.match(action)) {
- throw new Error(action.error.message);
- }
- } catch (error) {
- createToast({ body: "Failed to pause monitor" });
- }
+ await pauseMonitor({ monitorId, triggerUpdate });
};
const onSubmit = async (event) => {
event.preventDefault();
- const action = await dispatch(updatePageSpeed({ monitor: monitor }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor updated successfully!" });
- dispatch(getPageSpeedByTeamId());
- } else {
- createToast({ body: "Failed to update monitor." });
- }
+ await updateMonitor({ monitor, redirect: "/pagespeed" });
};
const handleRemove = async (event) => {
event.preventDefault();
- setButtonLoading(true);
- const action = await dispatch(deletePageSpeed({ monitor }));
- if (action.meta.requestStatus === "fulfilled") {
- navigate("/pagespeed");
- } else {
- createToast({ body: "Failed to delete monitor." });
- }
- setButtonLoading(false);
+ await deleteMonitor({ monitor, redirect: "/pagespeed" });
};
return (
@@ -363,7 +322,7 @@ const PageSpeedConfigure = () => {
mt="auto"
>
{
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
- isLoading={buttonLoading}
+ isLoading={isLoading || isDeleting || isUpdating || isPausing}
/>
);
diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx
index 2b3e6e4e4..a138fe0a2 100644
--- a/client/src/Pages/PageSpeed/Create/index.jsx
+++ b/client/src/Pages/PageSpeed/Create/index.jsx
@@ -1,31 +1,28 @@
-// React, Redux, Router
-import { useNavigate } from "react-router-dom";
-import { useState } from "react";
-import { useSelector, useDispatch } from "react-redux";
-// Utility and Network
-import { monitorValidation } from "../../../Validation/validation";
-import {
- createPageSpeed,
- checkEndpointResolution,
-} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
-import { parseDomainName } from "../../../Utils/monitorUtils";
-import { useTranslation } from "react-i18next";
-import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
-
-// MUI
-import { useTheme } from "@emotion/react";
-import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
-
//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 { createToast } from "../../../Utils/toastUtils";
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 = [
@@ -56,13 +53,11 @@ const CreatePageSpeed = () => {
const [https, setHttps] = useState(true);
const [errors, setErrors] = useState({});
const { user } = useSelector((state) => state.auth);
- const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId();
// Setup
- const dispatch = useDispatch();
- const navigate = useNavigate();
const theme = useTheme();
+ const [createMonitor, isCreating] = useCreateMonitor();
// Handlers
const onSubmit = async (event) => {
@@ -88,18 +83,6 @@ const CreatePageSpeed = () => {
return;
}
- const checkEndpointAction = await dispatch(
- checkEndpointResolution({ monitorURL: form.url })
- );
-
- if (checkEndpointAction.meta.requestStatus === "rejected") {
- createToast({
- body: "The endpoint you entered doesn't resolve. Check the URL again.",
- });
- setErrors({ url: "The entered URL is not reachable." });
- return;
- }
-
form = {
...form,
description: form.name,
@@ -108,13 +91,7 @@ const CreatePageSpeed = () => {
notifications: monitor.notifications,
};
- const action = await dispatch(createPageSpeed({ monitor: form }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor created successfully!" });
- navigate("/pagespeed");
- } else {
- createToast({ body: "Failed to create monitor." });
- }
+ await createMonitor({ monitor: form, redirect: "/pagespeed" });
};
const handleChange = (event) => {
@@ -325,7 +302,7 @@ const CreatePageSpeed = () => {
variant="contained"
color="accent"
disabled={!Object.values(errors).every((value) => value === undefined)}
- loading={isLoading}
+ loading={isCreating}
>
{t("createMonitor")}
diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx
index 5724f12a1..e8afcf8c8 100644
--- a/client/src/Pages/PageSpeed/Details/index.jsx
+++ b/client/src/Pages/PageSpeed/Details/index.jsx
@@ -11,8 +11,7 @@ import GenericFallback from "../../../Components/GenericFallback";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useParams } from "react-router-dom";
-import { useSelector } from "react-redux";
-import { useMonitorFetch } from "./Hooks/useMonitorFetch";
+import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks";
import { useState } from "react";
import { useTranslation } from "react-i18next";
// Constants
@@ -28,8 +27,13 @@ const PageSpeedDetails = () => {
const isAdmin = useIsAdmin();
const { monitorId } = useParams();
- const { monitor, audits, isLoading, networkError } = useMonitorFetch({
+ const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({
monitorId,
+ sortOrder: "desc",
+ limit: 50,
+ dateRange: "day",
+ numToDisplay: null,
+ normalize: null,
});
const [metrics, setMetrics] = useState({
diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
index 8c83f102a..2220a4b16 100644
--- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
+++ b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx
@@ -8,7 +8,7 @@ import { useTheme } from "@emotion/react";
import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts";
import { useSelector } from "react-redux";
import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils";
-import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import IconBox from "../../../../../Components/IconBox";
@@ -118,7 +118,7 @@ const processData = (data) => {
const PagespeedAreaChart = ({ data, status }) => {
const theme = useTheme();
const [isHovered, setIsHovered] = useState(false);
- const { statusToTheme } = useUtils();
+ const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
@@ -206,7 +206,7 @@ PagespeedAreaChart.propTypes = {
* @returns {JSX.Element} - The rendered card.
*/
const Card = ({ monitor }) => {
- const { determineState, pagespeedStatusMsg } = useUtils();
+ const { determineState, pagespeedStatusMsg } = useMonitorUtils();
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
@@ -275,7 +275,7 @@ const Card = ({ monitor }) => {
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx
index 808541cee..319b0da21 100644
--- a/client/src/Pages/PageSpeed/Monitors/index.jsx
+++ b/client/src/Pages/PageSpeed/Monitors/index.jsx
@@ -5,17 +5,17 @@ import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
import MonitorCountHeader from "../../../Components/MonitorCountHeader";
import MonitorGrid from "./Components/MonitorGrid";
import Fallback from "../../../Components/Fallback";
+import GenericFallback from "../../../Components/GenericFallback";
// Utils
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import useMonitorsFetch from "./Hooks/useMonitorsFetch";
-import GenericFallback from "../../../Components/GenericFallback";
import { useTranslation } from "react-i18next";
+import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
// Constants
const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }];
-
+const TYPES = ["pagespeed"];
const PageSpeed = () => {
const theme = useTheme();
const { t } = useTranslation();
@@ -23,8 +23,15 @@ const PageSpeed = () => {
const { user } = useSelector((state) => state.auth);
const { pagespeedApiKey } = useSelector((state) => state.settings);
- const { isLoading, monitors, summary, networkError } = useMonitorsFetch({
+ const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({
teamId: user.teamId,
+ limit: 10,
+ types: TYPES,
+ page: null,
+ rowsPerPage: null,
+ filter: null,
+ field: null,
+ order: null,
});
if (networkError === true) {
@@ -68,7 +75,7 @@ const PageSpeed = () => {
/>
{
const theme = useTheme();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const { showURL } = useSelector((state) => state.ui);
diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx
index 8718a2209..5a380f8e5 100644
--- a/client/src/Pages/Uptime/Configure/index.jsx
+++ b/client/src/Pages/Uptime/Configure/index.jsx
@@ -1,37 +1,37 @@
-import { useNavigate, useParams } from "react-router";
-import { useTheme } from "@emotion/react";
-import { useDispatch, useSelector } from "react-redux";
-import { useState, useEffect } from "react";
-import {
- Box,
- Stack,
- Tooltip,
- Typography,
- Button,
- FormControlLabel,
- Switch,
-} from "@mui/material";
-import { monitorValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import { useTranslation } from "react-i18next";
+// Components
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Tooltip from "@mui/material/Tooltip";
+import Typography from "@mui/material/Typography";
+import Button from "@mui/material/Button";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Switch from "@mui/material/Switch";
import ConfigBox from "../../../Components/ConfigBox";
-import {
- updateUptimeMonitor,
- deleteUptimeMonitor,
-} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
import Select from "../../../Components/Inputs/Select";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import PulseDot from "../../../Components/Animated/PulseDot";
-import "./index.css";
import Dialog from "../../../Components/Dialog";
-import { usePauseMonitor } from "../../../Hooks/useMonitorControls";
+import PulseDot from "../../../Components/Animated/PulseDot";
+import Checkbox from "../../../Components/Inputs/Checkbox";
+
+// Utils
+import { useParams } from "react-router";
+import { useTheme } from "@emotion/react";
+import { useState } from "react";
+import { monitorValidation } from "../../../Validation/validation";
+import { createToast } from "../../../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
-import { useFetchUptimeMonitorById } from "../../../Hooks/useFetchUptimeMonitorById";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import {
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+ useFetchMonitorById,
+} from "../../../Hooks/monitorHooks";
import NotificationsConfig from "../../../Components/NotificationConfig";
/**
@@ -76,18 +76,19 @@ const Configure = () => {
};
// Network
- const [monitor, isLoading, error] = useFetchUptimeMonitorById(monitorId, updateTrigger);
const [notifications, notificationsAreLoading, notificationsError] =
useGetNotificationsByTeamId();
- const [pauseMonitor, isPausing, pauseError] = usePauseMonitor({
- monitorId: monitor?._id,
- triggerUpdate,
+ const [pauseMonitor, isPausing, pauseError] = usePauseMonitor({});
+ const [deleteMonitor, isDeleting] = useDeleteMonitor();
+ const [updateMonitor, isUpdating] = useUpdateMonitor();
+ const [isLoading] = useFetchMonitorById({
+ monitorId,
+ setMonitor: setForm,
+ updateTrigger,
});
const MS_PER_MINUTE = 60000;
- const navigate = useNavigate();
const theme = useTheme();
- const dispatch = useDispatch();
const matchMethodOptions = [
{ _id: "equal", name: "Equal" },
@@ -111,7 +112,7 @@ const Configure = () => {
// Handlers
const handlePause = async () => {
- const res = await pauseMonitor();
+ const res = await pauseMonitor({ monitorId: form?._id, triggerUpdate });
if (typeof res !== "undefined") {
triggerUpdate();
}
@@ -119,12 +120,7 @@ const Configure = () => {
const handleRemove = async (event) => {
event.preventDefault();
- const action = await dispatch(deleteUptimeMonitor({ monitor }));
- if (action.meta.requestStatus === "fulfilled") {
- navigate("/uptime");
- } else {
- createToast({ body: "Failed to delete monitor." });
- }
+ await deleteMonitor({ monitor: form, redirect: "/uptime" });
};
const onChange = (event) => {
@@ -134,6 +130,17 @@ const Configure = () => {
value = checked;
}
+ if (name === "useAdvancedMatching") {
+ setForm((prevForm) => {
+ return {
+ ...prevForm,
+ matchMethod: "equal",
+ };
+ });
+ setUseAdvancedMatching(!useAdvancedMatching);
+ return;
+ }
+
if (name === "interval") {
value = value * MS_PER_MINUTE;
}
@@ -146,7 +153,6 @@ const Configure = () => {
setErrors((prev) => {
const updatedErrors = { ...prev };
-
if (validation.error) updatedErrors[name] = validation.error.details[0].message;
else delete updatedErrors[name];
return updatedErrors;
@@ -183,7 +189,7 @@ const Configure = () => {
if (validation.error) {
const newErrors = {};
- error.details.forEach((err) => {
+ validation.error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
setErrors(newErrors);
@@ -192,27 +198,12 @@ const Configure = () => {
}
toSubmit.notifications = form.notifications;
- const action = await dispatch(updateUptimeMonitor({ monitor: toSubmit }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor updated successfully!" });
- } else {
- createToast({ body: "Failed to update monitor." });
- }
+ console.log(JSON.stringify(toSubmit, null, 2));
+ // await updateMonitor({ monitor: toSubmit, redirect: "/uptime" });
};
- // Effects
- useEffect(() => {
- if (monitor?.matchMethod) {
- setUseAdvancedMatching(true);
- }
-
- setForm({
- ...monitor,
- });
- }, [monitor, notifications]);
-
// Parse the URL
- const parsedUrl = parseUrl(monitor?.url);
+ const parsedUrl = parseUrl(form?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
const { determineState, statusColor } = useMonitorUtils();
@@ -434,7 +425,13 @@ const Configure = () => {
onChange={onChange}
items={frequencies}
/>
- {form.type === "http" && (
+
+ {form.type === "http" && useAdvancedMatching && (
<>
- {t("settingsSystemResetDescription")}
+ {t("settingsPage.systemResetSettings.description")}
- {t("settingsRemoveAllMonitors")}
setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
- {t("settingsRemoveAllMonitorsButton")}
+ {t("settingsPage.systemResetSettings.buttonRemoveAllMonitors")}