diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 26956309b..16273e9d5 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -20,7 +20,8 @@ const { t } = useTranslation();
```
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
-- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions.
+- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
+- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml
new file mode 100644
index 000000000..d3d4c0d6b
--- /dev/null
+++ b/.github/workflows/check-format.yml
@@ -0,0 +1,62 @@
+name: Format Check (Client & Server)
+on:
+ pull_request:
+ workflow_dispatch:
+
+jobs:
+ format-client:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Install client dependencies
+ working-directory: client
+ run: npm ci
+
+ - name: Check client formatting
+ working-directory: client
+ run: npm run format-check
+
+ format-server:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+
+ - name: Install server dependencies
+ working-directory: server
+ run: npm ci
+
+ - name: Check server formatting
+ working-directory: server
+ run: npm run format-check
+ close-pr-if-needed:
+ if: always()
+ runs-on: ubuntu-latest
+ needs: [format-client, format-server]
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+
+ - name: Get PR number
+ id: pr
+ run: echo "PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH")" >> $GITHUB_ENV
+
+ - name: Close PR using GitHub CLI
+ if: |
+ needs.format-client.result == 'failure' ||
+ needs.format-server.result == 'failure'
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed.
+ Please run \`npm run format\` and push again."
diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml
new file mode 100644
index 000000000..cf06f7840
--- /dev/null
+++ b/.github/workflows/deploy-images-on-release.yml
@@ -0,0 +1,159 @@
+name: Deploy images on release
+
+on:
+ push:
+ tags:
+ - "v*"
+ workflow_dispatch:
+jobs:
+ docker-build-and-push-client:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - 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
+ with:
+ fetch-depth: 0
+
+ - 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
+ build-args: |
+ VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }}
+
+ docker-build-and-push-server-mono:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - 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 \
+ --build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \
+ .
+
+ - 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..fa39fed85
--- /dev/null
+++ b/.github/workflows/deploy-images.yml
@@ -0,0 +1,154 @@
+name: Deploy images
+
+on:
+ push:
+ branches: ["master"]
+ workflow_dispatch:
+jobs:
+ docker-build-and-push-client:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
+ - 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 \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
+ .
+
+ - 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
+ with:
+ fetch-depth: 0
+
+ - 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: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
+
+ - 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
+ build-args: |
+ VITE_APP_VERSION=${{ env.VERSION }}
+
+ docker-build-and-push-server-mono:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Log in to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
+
+ - 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 \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
+ .
+
+ - 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/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml
index 6956f5caa..7764c488b 100644
--- a/.github/workflows/production-deploy.yml
+++ b/.github/workflows/production-deploy.yml
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-demo \
-f ./docker/prod/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml
index 34fa243c6..ea5477815 100644
--- a/.github/workflows/staging-deploy.yml
+++ b/.github/workflows/staging-deploy.yml
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
@@ -18,12 +20,16 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Get version
+ id: vars
+ run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV
- name: Build Client Docker image
run: |
docker build \
-t ghcr.io/bluewave-labs/checkmate:frontend-staging \
-f ./docker/staging/client.Dockerfile \
--label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \
+ --build-arg VITE_APP_VERSION=${{ env.VERSION }} \
.
- name: Push Client Docker image
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.
diff --git a/README.md b/README.md
index d276af81f..c9c9073ad 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@
An open source uptime and infrastructure monitoring application
-
+
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
@@ -22,7 +22,23 @@ Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/c
Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks.
-We **love** what we are building here, and we continuously learn a few things about Reactjs, Nodejs, MongoDB, and Docker while building Checkmate.
+**If you would like to sponsor a feature, [see this link](https://checkmate.so/sponsored-features).**
+
+## 📚 Table of contents
+
+- [📦 Demo](#-demo)
+- [🔗 User's guide](#-users-guide)
+- [🛠️ Installation](#️-installation)
+- [🏁 Translations](#-translations)
+- [🚀 Performance](#-performance)
+- [💚 Questions & Ideas](#-questions--ideas)
+- [🧩 Features](#-features)
+- [🏗️ Screenshots](#-screenshots)
+- [🏗️ Tech stack](#-tech-stack)
+- [🔗 A few links](#a-few-links)
+- [🤝 Contributing](#-contributing)
+- [💰 Our sponsors](#-our-sponsors)
+
## 📦 Demo
@@ -80,14 +96,19 @@ If you have any questions, suggestions or comments, please use our [Discord chan
## 🏗️ Screenshots
-
+
-
+
-
+
+
+
+
+
+
## 🏗️ Tech stack
@@ -109,7 +130,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan
We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers.
-We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 5800+ stars and attracted 70+ contributors from around the globe.
+We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe.
Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us!
@@ -127,11 +148,13 @@ Here's how you can contribute:
+[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
+
## 💰 Our sponsors
Thanks to [Gitbook](https://gitbook.io/) for giving us a free tier for their documentation platform, and [Poeditor](https://poeditor.com/) providing us a free account to use their i18n services. If you would like to sponsor Checkmate, please send an email to hello@bluewavelabs.ca
-[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
+If you would like to sponsor a feature, [see this page](https://checkmate.so/sponsored-features).
Also check other developer and contributor-friendly projects of BlueWave:
diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs
index 7d6029d4b..86d523a6f 100644
--- a/client/.eslintrc.cjs
+++ b/client/.eslintrc.cjs
@@ -16,4 +16,8 @@ module.exports = {
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"react/no-unescaped-entities": "off",
},
+ globals: {
+ __APP_VERSION__: "readonly",
+ process: "readonly",
+ },
};
diff --git a/client/package-lock.json b/client/package-lock.json
index e12e76f97..f58f56f44 100644
--- a/client/package-lock.json
+++ b/client/package-lock.json
@@ -10,13 +10,11 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
- "@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^18.0.0",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.29.0",
"@mui/x-date-pickers": "7.29.0",
"@reduxjs/toolkit": "2.7.0",
"axios": "^1.7.4",
@@ -24,14 +22,9 @@
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
- "immutability-helper": "^3.1.1",
"joi": "17.13.3",
- "jwt-decode": "^4.0.0",
- "maplibre-gl": "5.3.1",
"mui-color-input": "^6.0.0",
"react": "18.3.1",
- "react-dnd": "^16.0.1",
- "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
@@ -52,7 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.4.19"
+ "vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
@@ -499,9 +492,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
- "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
+ "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
@@ -511,13 +504,13 @@
"aix"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
- "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
+ "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
@@ -527,13 +520,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
- "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
+ "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
@@ -543,13 +536,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
- "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
+ "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
@@ -559,13 +552,13 @@
"android"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
- "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
+ "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
@@ -575,13 +568,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
- "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
+ "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
@@ -591,13 +584,13 @@
"darwin"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
- "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
@@ -607,13 +600,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
- "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
+ "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
@@ -623,13 +616,13 @@
"freebsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
- "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
+ "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
@@ -639,13 +632,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
- "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
+ "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
@@ -655,13 +648,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
- "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
+ "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
@@ -671,13 +664,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
- "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
+ "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
@@ -687,13 +680,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
- "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
+ "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
@@ -703,13 +696,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
- "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
+ "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
@@ -719,13 +712,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
- "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
+ "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
@@ -735,13 +728,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
- "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
+ "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
@@ -751,13 +744,13 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
- "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
+ "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
@@ -767,13 +760,29 @@
"linux"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
- "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
@@ -783,13 +792,29 @@
"netbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
+ "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
- "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
+ "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
@@ -799,13 +824,13 @@
"openbsd"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
- "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
+ "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
@@ -815,13 +840,13 @@
"sunos"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
- "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
+ "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
@@ -831,13 +856,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
- "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
+ "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
@@ -847,13 +872,13 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
- "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
+ "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
@@ -863,7 +888,7 @@
"win32"
],
"engines": {
- "node": ">=12"
+ "node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -983,15 +1008,6 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
- "node_modules/@fontsource/roboto": {
- "version": "5.2.5",
- "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz",
- "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==",
- "license": "OFL-1.1",
- "funding": {
- "url": "https://github.com/sponsors/ayuhito"
- }
- },
"node_modules/@hapi/hoek": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz",
@@ -1110,83 +1126,6 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
- "node_modules/@mapbox/geojson-rewind": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
- "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
- "license": "ISC",
- "dependencies": {
- "get-stream": "^6.0.1",
- "minimist": "^1.2.6"
- },
- "bin": {
- "geojson-rewind": "geojson-rewind"
- }
- },
- "node_modules/@mapbox/jsonlint-lines-primitives": {
- "version": "2.0.2",
- "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz",
- "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/@mapbox/point-geometry": {
- "version": "0.1.0",
- "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz",
- "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==",
- "license": "ISC"
- },
- "node_modules/@mapbox/tiny-sdf": {
- "version": "2.0.6",
- "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz",
- "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==",
- "license": "BSD-2-Clause"
- },
- "node_modules/@mapbox/unitbezier": {
- "version": "0.0.1",
- "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz",
- "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==",
- "license": "BSD-2-Clause"
- },
- "node_modules/@mapbox/vector-tile": {
- "version": "1.3.1",
- "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz",
- "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@mapbox/point-geometry": "~0.1.0"
- }
- },
- "node_modules/@mapbox/whoots-js": {
- "version": "3.1.0",
- "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz",
- "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==",
- "license": "ISC",
- "engines": {
- "node": ">=6.0.0"
- }
- },
- "node_modules/@maplibre/maplibre-gl-style-spec": {
- "version": "23.3.0",
- "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz",
- "integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==",
- "license": "ISC",
- "dependencies": {
- "@mapbox/jsonlint-lines-primitives": "~2.0.2",
- "@mapbox/unitbezier": "^0.0.1",
- "json-stringify-pretty-compact": "^4.0.0",
- "minimist": "^1.2.8",
- "quickselect": "^3.0.0",
- "rw": "^1.3.3",
- "tinyqueue": "^3.0.0"
- },
- "bin": {
- "gl-style-format": "dist/gl-style-format.mjs",
- "gl-style-migrate": "dist/gl-style-migrate.mjs",
- "gl-style-validate": "dist/gl-style-validate.mjs"
- }
- },
"node_modules/@mui/base": {
"version": "5.0.0-beta.42",
"resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz",
@@ -1861,44 +1800,6 @@
"robust-predicates": "^3.0.2"
}
},
- "node_modules/@mui/x-data-grid": {
- "version": "7.29.0",
- "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.0.tgz",
- "integrity": "sha512-v3HiwcT/oqgv6xkynOd3oxH26IS0OvspgmfHtfPF9WeRsnUHcTTZ7E1EoAKjT9/Ms2f3oKKJAR6rZBZ5ymik1g==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.25.7",
- "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0",
- "@mui/x-internals": "7.29.0",
- "clsx": "^2.1.1",
- "prop-types": "^15.8.1",
- "reselect": "^5.1.1",
- "use-sync-external-store": "^1.0.0"
- },
- "engines": {
- "node": ">=14.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/mui-org"
- },
- "peerDependencies": {
- "@emotion/react": "^11.9.0",
- "@emotion/styled": "^11.8.1",
- "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0",
- "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0",
- "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
- "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
- },
- "peerDependenciesMeta": {
- "@emotion/react": {
- "optional": true
- },
- "@emotion/styled": {
- "optional": true
- }
- }
- },
"node_modules/@mui/x-date-pickers": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.0.tgz",
@@ -2033,24 +1934,6 @@
"url": "https://opencollective.com/popperjs"
}
},
- "node_modules/@react-dnd/asap": {
- "version": "5.0.2",
- "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz",
- "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==",
- "license": "MIT"
- },
- "node_modules/@react-dnd/invariant": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz",
- "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==",
- "license": "MIT"
- },
- "node_modules/@react-dnd/shallowequal": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz",
- "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==",
- "license": "MIT"
- },
"node_modules/@react-spring/animated": {
"version": "9.7.5",
"resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz",
@@ -2832,50 +2715,12 @@
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
"license": "MIT"
},
- "node_modules/@types/geojson": {
- "version": "7946.0.16",
- "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz",
- "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
- "license": "MIT"
- },
- "node_modules/@types/geojson-vt": {
- "version": "3.2.5",
- "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz",
- "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==",
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*"
- }
- },
- "node_modules/@types/mapbox__point-geometry": {
- "version": "0.1.4",
- "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz",
- "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==",
- "license": "MIT"
- },
- "node_modules/@types/mapbox__vector-tile": {
- "version": "1.3.4",
- "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz",
- "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==",
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*",
- "@types/mapbox__point-geometry": "*",
- "@types/pbf": "*"
- }
- },
"node_modules/@types/parse-json": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz",
"integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==",
"license": "MIT"
},
- "node_modules/@types/pbf": {
- "version": "3.0.5",
- "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz",
- "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==",
- "license": "MIT"
- },
"node_modules/@types/prop-types": {
"version": "15.7.14",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz",
@@ -2911,15 +2756,6 @@
"@types/react": "*"
}
},
- "node_modules/@types/supercluster": {
- "version": "7.1.3",
- "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz",
- "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==",
- "license": "MIT",
- "dependencies": {
- "@types/geojson": "*"
- }
- },
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
@@ -3236,9 +3072,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.11",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
- "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
+ "version": "1.1.12",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+ "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3368,23 +3204,6 @@
],
"license": "CC-BY-4.0"
},
- "node_modules/chalk": {
- "version": "4.1.2",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
- "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/chalk?sponsor=1"
- }
- },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -3455,6 +3274,15 @@
"node": ">=10"
}
},
+ "node_modules/cosmiconfig/node_modules/yaml": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
+ "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3771,26 +3599,6 @@
"node": ">=0.4.0"
}
},
- "node_modules/dnd-core": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz",
- "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==",
- "license": "MIT",
- "dependencies": {
- "@react-dnd/asap": "^5.0.1",
- "@react-dnd/invariant": "^4.0.1",
- "redux": "^4.2.0"
- }
- },
- "node_modules/dnd-core/node_modules/redux": {
- "version": "4.2.1",
- "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
- "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
- "license": "MIT",
- "dependencies": {
- "@babel/runtime": "^7.9.2"
- }
- },
"node_modules/doctrine": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
@@ -3838,12 +3646,6 @@
"node": ">= 0.4"
}
},
- "node_modules/earcut": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz",
- "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==",
- "license": "ISC"
- },
"node_modules/electron-to-chromium": {
"version": "1.5.155",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz",
@@ -4042,41 +3844,43 @@
}
},
"node_modules/esbuild": {
- "version": "0.21.5",
- "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
- "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "version": "0.25.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
+ "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
- "node": ">=12"
+ "node": ">=18"
},
"optionalDependencies": {
- "@esbuild/aix-ppc64": "0.21.5",
- "@esbuild/android-arm": "0.21.5",
- "@esbuild/android-arm64": "0.21.5",
- "@esbuild/android-x64": "0.21.5",
- "@esbuild/darwin-arm64": "0.21.5",
- "@esbuild/darwin-x64": "0.21.5",
- "@esbuild/freebsd-arm64": "0.21.5",
- "@esbuild/freebsd-x64": "0.21.5",
- "@esbuild/linux-arm": "0.21.5",
- "@esbuild/linux-arm64": "0.21.5",
- "@esbuild/linux-ia32": "0.21.5",
- "@esbuild/linux-loong64": "0.21.5",
- "@esbuild/linux-mips64el": "0.21.5",
- "@esbuild/linux-ppc64": "0.21.5",
- "@esbuild/linux-riscv64": "0.21.5",
- "@esbuild/linux-s390x": "0.21.5",
- "@esbuild/linux-x64": "0.21.5",
- "@esbuild/netbsd-x64": "0.21.5",
- "@esbuild/openbsd-x64": "0.21.5",
- "@esbuild/sunos-x64": "0.21.5",
- "@esbuild/win32-arm64": "0.21.5",
- "@esbuild/win32-ia32": "0.21.5",
- "@esbuild/win32-x64": "0.21.5"
+ "@esbuild/aix-ppc64": "0.25.5",
+ "@esbuild/android-arm": "0.25.5",
+ "@esbuild/android-arm64": "0.25.5",
+ "@esbuild/android-x64": "0.25.5",
+ "@esbuild/darwin-arm64": "0.25.5",
+ "@esbuild/darwin-x64": "0.25.5",
+ "@esbuild/freebsd-arm64": "0.25.5",
+ "@esbuild/freebsd-x64": "0.25.5",
+ "@esbuild/linux-arm": "0.25.5",
+ "@esbuild/linux-arm64": "0.25.5",
+ "@esbuild/linux-ia32": "0.25.5",
+ "@esbuild/linux-loong64": "0.25.5",
+ "@esbuild/linux-mips64el": "0.25.5",
+ "@esbuild/linux-ppc64": "0.25.5",
+ "@esbuild/linux-riscv64": "0.25.5",
+ "@esbuild/linux-s390x": "0.25.5",
+ "@esbuild/linux-x64": "0.25.5",
+ "@esbuild/netbsd-arm64": "0.25.5",
+ "@esbuild/netbsd-x64": "0.25.5",
+ "@esbuild/openbsd-arm64": "0.25.5",
+ "@esbuild/openbsd-x64": "0.25.5",
+ "@esbuild/sunos-x64": "0.25.5",
+ "@esbuild/win32-arm64": "0.25.5",
+ "@esbuild/win32-ia32": "0.25.5",
+ "@esbuild/win32-x64": "0.25.5"
}
},
"node_modules/escalade": {
@@ -4274,6 +4078,23 @@
"url": "https://opencollective.com/eslint"
}
},
+ "node_modules/eslint/node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
"node_modules/eslint/node_modules/globals": {
"version": "13.24.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
@@ -4370,6 +4191,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@@ -4405,6 +4227,20 @@
"reusify": "^1.0.4"
}
},
+ "node_modules/fdir": {
+ "version": "6.4.6",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
+ "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -4590,12 +4426,6 @@
"node": ">=6.9.0"
}
},
- "node_modules/geojson-vt": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz",
- "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==",
- "license": "ISC"
- },
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
@@ -4633,18 +4463,6 @@
"node": ">= 0.4"
}
},
- "node_modules/get-stream": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
- "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
- "license": "MIT",
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/get-symbol-description": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz",
@@ -4663,12 +4481,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/gl-matrix": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz",
- "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==",
- "license": "MIT"
- },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -4704,44 +4516,6 @@
"node": ">=10.13.0"
}
},
- "node_modules/global-prefix": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz",
- "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==",
- "license": "MIT",
- "dependencies": {
- "ini": "^4.1.3",
- "kind-of": "^6.0.3",
- "which": "^4.0.0"
- },
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/global-prefix/node_modules/isexe": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz",
- "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==",
- "license": "ISC",
- "engines": {
- "node": ">=16"
- }
- },
- "node_modules/global-prefix/node_modules/which": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz",
- "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==",
- "license": "ISC",
- "dependencies": {
- "isexe": "^3.1.1"
- },
- "bin": {
- "node-which": "bin/which.js"
- },
- "engines": {
- "node": "^16.13.0 || >=18.0.0"
- }
- },
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
@@ -4946,26 +4720,6 @@
}
}
},
- "node_modules/ieee754": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
- "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/feross"
- },
- {
- "type": "patreon",
- "url": "https://www.patreon.com/feross"
- },
- {
- "type": "consulting",
- "url": "https://feross.org/support"
- }
- ],
- "license": "BSD-3-Clause"
- },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -4986,12 +4740,6 @@
"url": "https://opencollective.com/immer"
}
},
- "node_modules/immutability-helper": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz",
- "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==",
- "license": "MIT"
- },
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -5037,15 +4785,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/ini": {
- "version": "4.1.3",
- "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz",
- "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==",
- "license": "ISC",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/internal-slot": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz",
@@ -5555,12 +5294,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/json-stringify-pretty-compact": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz",
- "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==",
- "license": "MIT"
- },
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
@@ -5589,21 +5322,6 @@
"node": ">=4.0"
}
},
- "node_modules/jwt-decode": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
- "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
- "license": "MIT",
- "engines": {
- "node": ">=18"
- }
- },
- "node_modules/kdbush": {
- "version": "4.0.2",
- "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz",
- "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==",
- "license": "ISC"
- },
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -5614,15 +5332,6 @@
"json-buffer": "3.0.1"
}
},
- "node_modules/kind-of": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
- "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -5702,47 +5411,6 @@
"yallist": "^3.0.2"
}
},
- "node_modules/maplibre-gl": {
- "version": "5.3.1",
- "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.1.tgz",
- "integrity": "sha512-Ihx+oUUSsZkjMou1Cw5J6silE+5OtFFQSPslWF9+7v4yFC/XDHrpsORYO9lWE4KZI0djCEUpZQJpkpnMArAbeA==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "@mapbox/geojson-rewind": "^0.5.2",
- "@mapbox/jsonlint-lines-primitives": "^2.0.2",
- "@mapbox/point-geometry": "^0.1.0",
- "@mapbox/tiny-sdf": "^2.0.6",
- "@mapbox/unitbezier": "^0.0.1",
- "@mapbox/vector-tile": "^1.3.1",
- "@mapbox/whoots-js": "^3.1.0",
- "@maplibre/maplibre-gl-style-spec": "^23.1.0",
- "@types/geojson": "^7946.0.16",
- "@types/geojson-vt": "3.2.5",
- "@types/mapbox__point-geometry": "^0.1.4",
- "@types/mapbox__vector-tile": "^1.3.4",
- "@types/pbf": "^3.0.5",
- "@types/supercluster": "^7.1.3",
- "earcut": "^3.0.1",
- "geojson-vt": "^4.0.2",
- "gl-matrix": "^3.4.3",
- "global-prefix": "^4.0.0",
- "kdbush": "^4.0.2",
- "murmurhash-js": "^1.0.0",
- "pbf": "^3.3.0",
- "potpack": "^2.0.0",
- "quickselect": "^3.0.0",
- "supercluster": "^8.0.1",
- "tinyqueue": "^3.0.0",
- "vt-pbf": "^3.1.3"
- },
- "engines": {
- "node": ">=16.14.0",
- "npm": ">=8.1.0"
- },
- "funding": {
- "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
- }
- },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5786,15 +5454,6 @@
"node": "*"
}
},
- "node_modules/minimist": {
- "version": "1.2.8",
- "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
- "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/ljharb"
- }
- },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -5823,12 +5482,6 @@
}
}
},
- "node_modules/murmurhash-js": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz",
- "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==",
- "license": "MIT"
- },
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -6130,19 +5783,6 @@
"node": ">=8"
}
},
- "node_modules/pbf": {
- "version": "3.3.0",
- "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz",
- "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==",
- "license": "BSD-3-Clause",
- "dependencies": {
- "ieee754": "^1.1.12",
- "resolve-protobuf-schema": "^2.1.0"
- },
- "bin": {
- "pbf": "bin/pbf"
- }
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -6199,12 +5839,6 @@
"node": "^10 || ^12 || >=14"
}
},
- "node_modules/potpack": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz",
- "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==",
- "license": "ISC"
- },
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
@@ -6248,12 +5882,6 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
- "node_modules/protocol-buffers-schema": {
- "version": "3.6.0",
- "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz",
- "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==",
- "license": "MIT"
- },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -6291,12 +5919,6 @@
],
"license": "MIT"
},
- "node_modules/quickselect": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz",
- "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
- "license": "ISC"
- },
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
@@ -6315,45 +5937,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/react-dnd": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz",
- "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==",
- "license": "MIT",
- "dependencies": {
- "@react-dnd/invariant": "^4.0.1",
- "@react-dnd/shallowequal": "^4.0.1",
- "dnd-core": "^16.0.1",
- "fast-deep-equal": "^3.1.3",
- "hoist-non-react-statics": "^3.3.2"
- },
- "peerDependencies": {
- "@types/hoist-non-react-statics": ">= 3.3.1",
- "@types/node": ">= 12",
- "@types/react": ">= 16",
- "react": ">= 16.14"
- },
- "peerDependenciesMeta": {
- "@types/hoist-non-react-statics": {
- "optional": true
- },
- "@types/node": {
- "optional": true
- },
- "@types/react": {
- "optional": true
- }
- }
- },
- "node_modules/react-dnd-html5-backend": {
- "version": "16.0.1",
- "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz",
- "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==",
- "license": "MIT",
- "dependencies": {
- "dnd-core": "^16.0.1"
- }
- },
"node_modules/react-dom": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
@@ -6658,15 +6241,6 @@
"node": ">=4"
}
},
- "node_modules/resolve-protobuf-schema": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz",
- "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
- "license": "MIT",
- "dependencies": {
- "protocol-buffers-schema": "^3.3.1"
- }
- },
"node_modules/reusify": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
@@ -6764,12 +6338,6 @@
"queue-microtask": "^1.2.2"
}
},
- "node_modules/rw": {
- "version": "1.3.3",
- "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz",
- "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
- "license": "BSD-3-Clause"
- },
"node_modules/safe-array-concat": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz",
@@ -7149,15 +6717,6 @@
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==",
"license": "MIT"
},
- "node_modules/supercluster": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz",
- "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==",
- "license": "ISC",
- "dependencies": {
- "kdbush": "^4.0.2"
- }
- },
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
@@ -7211,11 +6770,21 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
- "node_modules/tinyqueue": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz",
- "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==",
- "license": "ISC"
+ "node_modules/tinyglobby": {
+ "version": "0.2.14",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz",
+ "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
},
"node_modules/tslib": {
"version": "2.8.1",
@@ -7427,20 +6996,23 @@
}
},
"node_modules/vite": {
- "version": "5.4.19",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
- "integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
+ "version": "6.3.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
+ "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"dependencies": {
- "esbuild": "^0.21.3",
- "postcss": "^8.4.43",
- "rollup": "^4.20.0"
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
- "node": "^18.0.0 || >=20.0.0"
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -7449,19 +7021,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
- "@types/node": "^18.0.0 || >=20.0.0",
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
- "terser": "^5.4.0"
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
+ "jiti": {
+ "optional": true
+ },
"less": {
"optional": true
},
@@ -7482,6 +7060,12 @@
},
"terser": {
"optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
}
}
},
@@ -7508,17 +7092,6 @@
"node": ">=0.10.0"
}
},
- "node_modules/vt-pbf": {
- "version": "3.1.3",
- "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz",
- "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==",
- "license": "MIT",
- "dependencies": {
- "@mapbox/point-geometry": "0.1.0",
- "@mapbox/vector-tile": "^1.3.1",
- "pbf": "^3.2.1"
- }
- },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -7648,12 +7221,17 @@
"license": "ISC"
},
"node_modules/yaml": {
- "version": "1.10.2",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
- "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
+ "version": "2.8.0",
+ "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
+ "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
+ "optional": true,
+ "peer": true,
+ "bin": {
+ "yaml": "bin.mjs"
+ },
"engines": {
- "node": ">= 6"
+ "node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
diff --git a/client/package.json b/client/package.json
index 238ced4ac..611ba5093 100644
--- a/client/package.json
+++ b/client/package.json
@@ -15,13 +15,11 @@
"dependencies": {
"@emotion/react": "^11.13.3",
"@emotion/styled": "^11.13.0",
- "@fontsource/roboto": "^5.0.13",
"@hello-pangea/dnd": "^18.0.0",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
- "@mui/x-data-grid": "7.29.0",
"@mui/x-date-pickers": "7.29.0",
"@reduxjs/toolkit": "2.7.0",
"axios": "^1.7.4",
@@ -29,14 +27,9 @@
"flag-icons": "7.3.2",
"html2canvas": "^1.4.1",
"i18next": "^24.2.2",
- "immutability-helper": "^3.1.1",
"joi": "17.13.3",
- "jwt-decode": "^4.0.0",
- "maplibre-gl": "5.3.1",
"mui-color-input": "^6.0.0",
"react": "18.3.1",
- "react-dnd": "^16.0.1",
- "react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-i18next": "^15.4.0",
"react-icons": "5.5.0",
@@ -65,7 +58,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
- "vite": "^5.4.19"
+ "vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
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/ActionsMenu/index.jsx b/client/src/Components/ActionsMenu/index.jsx
index 4d5ab7c01..1b590a471 100644
--- a/client/src/Components/ActionsMenu/index.jsx
+++ b/client/src/Components/ActionsMenu/index.jsx
@@ -1,17 +1,18 @@
+// Components
+import IconButton from "@mui/material/IconButton";
+import Menu from "@mui/material/Menu";
+import MenuItem from "@mui/material/MenuItem";
+import Settings from "../../assets/icons/settings-bold.svg?react";
+import Dialog from "../../Components/Dialog";
+
+// Utils
import { useState } from "react";
-import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../Utils/toastUtils";
-import { logger } from "../../Utils/Logger";
-import { IconButton, Menu, MenuItem } from "@mui/material";
-import {
- deleteUptimeMonitor,
- pauseUptimeMonitor,
-} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import Settings from "../../assets/icons/settings-bold.svg?react";
+
import PropTypes from "prop-types";
-import Dialog from "../../Components/Dialog";
+import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks";
const ActionsMenu = ({
monitor,
@@ -23,38 +24,27 @@ const ActionsMenu = ({
const [anchorEl, setAnchorEl] = useState(null);
const [actions, setActions] = useState({});
const [isOpen, setIsOpen] = useState(false);
- const dispatch = useDispatch();
const theme = useTheme();
- const { isLoading } = useSelector((state) => state.uptimeMonitors);
+ const [pauseMonitor, isPausing, error] = usePauseMonitor();
+ const [deleteMonitor, isDeleting] = useDeleteMonitor();
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { _id: actions.id };
- const action = await dispatch(deleteUptimeMonitor({ monitor }));
- if (action.meta.requestStatus === "fulfilled") {
- setIsOpen(false); // close modal
- updateRowCallback();
- createToast({ body: "Monitor deleted successfully." });
- } else {
- createToast({ body: "Failed to delete monitor." });
- }
+ await deleteMonitor({ monitor });
+ updateRowCallback();
};
const handlePause = async () => {
try {
setIsLoading(true);
- const action = await dispatch(pauseUptimeMonitor({ monitorId: monitor._id }));
- if (pauseUptimeMonitor.fulfilled.match(action)) {
- const state = action?.payload?.data.isActive === false ? "resumed" : "paused";
- createToast({ body: `Monitor ${state} successfully.` });
- pauseCallback();
- } else {
- throw new Error(action?.error?.message ?? "Failed to pause monitor.");
- }
+ await pauseMonitor({ monitorId: monitor._id });
+ pauseCallback();
} catch (error) {
- logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
+ } finally {
+ setIsLoading(false);
}
};
@@ -210,7 +200,7 @@ const ActionsMenu = ({
e.stopPropagation();
handleRemove(e);
}}
- isLoading={isLoading}
+ isLoading={isDeleting}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
diff --git a/client/src/Components/Breadcrumbs/index.jsx b/client/src/Components/Breadcrumbs/index.jsx
index 0ebd8dab1..bf04afa2e 100644
--- a/client/src/Components/Breadcrumbs/index.jsx
+++ b/client/src/Components/Breadcrumbs/index.jsx
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material";
import { useTheme } from "@emotion/react";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import "./index.css";
diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/Charts/ChartBox/index.jsx
index 6b74ace68..2c13ece87 100644
--- a/client/src/Components/Charts/ChartBox/index.jsx
+++ b/client/src/Components/Charts/ChartBox/index.jsx
@@ -43,7 +43,6 @@ const ChartBox = ({
>
{
const location = useLocation();
const navigate = useNavigate();
- // Debugging: Log the current theme mode
- console.log("Current theme mode:", mode);
-
const logoSrc =
mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png";
diff --git a/client/src/Components/Dialog/index.jsx b/client/src/Components/Dialog/index.jsx
index e07597df4..678eb8721 100644
--- a/client/src/Components/Dialog/index.jsx
+++ b/client/src/Components/Dialog/index.jsx
@@ -57,7 +57,7 @@ Dialog.propTypes = {
onCancel: PropTypes.func.isRequired,
confirmationButtonLabel: PropTypes.string.isRequired,
onConfirm: PropTypes.func.isRequired,
- isLoading: PropTypes.bool.isRequired,
+ isLoading: PropTypes.bool,
};
export default Dialog;
diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/Fallback/index.jsx
index b00ddbecc..b77432d58 100644
--- a/client/src/Components/Fallback/index.jsx
+++ b/client/src/Components/Fallback/index.jsx
@@ -11,7 +11,7 @@ import { useSelector } from "react-redux";
import Alert from "../Alert";
import { useTranslation } from "react-i18next";
import "./index.css";
-import { useFetchSettings } from "../../Hooks/useFetchSettings";
+import { useFetchSettings } from "../../Hooks/settingsHooks";
import { useState } from "react";
/**
* Fallback component to display a fallback UI with a title, a list of checks, and a navigation button.
@@ -39,7 +39,11 @@ const Fallback = ({
const { t } = useTranslation();
const [settingsData, setSettingsData] = useState(undefined);
- const [isLoading, error] = useFetchSettings({ setSettingsData });
+ const [isLoading, error] = useFetchSettings({
+ setSettingsData,
+ setIsApiKeySet: () => {},
+ setIsEmailPasswordSet: () => {},
+ });
// Custom warning message with clickable link
const renderWarningMessage = () => {
return (
diff --git a/client/src/Components/GenericFallback/NetworkError.jsx b/client/src/Components/GenericFallback/NetworkError.jsx
index b26037a57..ddffa1c31 100644
--- a/client/src/Components/GenericFallback/NetworkError.jsx
+++ b/client/src/Components/GenericFallback/NetworkError.jsx
@@ -1,8 +1,11 @@
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
const NetworkError = () => {
const theme = useTheme();
+ const { t } = useTranslation();
+
return (
<>
{
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- Network error
+ {t("common.toasts.networkError")}
- Please check your connection
+ {t("common.toasts.checkConnection")}
>
);
};
diff --git a/client/src/Components/HOC/withAdminCheck.jsx b/client/src/Components/HOC/withAdminCheck.jsx
index 8edf7a02b..458330a1f 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(false);
}
})
.catch((error) => {
logger.error(error);
+ })
+ .finally(() => {
+ setIsChecking(false);
});
}, [navigate]);
+
+ if (isChecking) {
+ return null;
+ }
+
return (
);
};
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/Inputs/Radio/index.jsx b/client/src/Components/Inputs/Radio/index.jsx
index 8c1c06b38..646b38e93 100644
--- a/client/src/Components/Inputs/Radio/index.jsx
+++ b/client/src/Components/Inputs/Radio/index.jsx
@@ -16,26 +16,27 @@ import "./index.css";
* size="small"
* />
*
- * @param {Object} props - The component props.
- * @param {string} props.id - The id of the radio button.
- * @param {string} props.title - The title of the radio button.
- * @param {string} [props.desc] - The description of the radio button.
- * @param {string} [props.size="small"] - The size of the radio button.
+ * @param {Object} props - The component
+ * @param {string} id - The id of the radio button.
+ * @param {string} title - The title of the radio button.
+ * @param {string} [desc] - The description of the radio button.
+ * @param {string} [size="small"] - The size of the radio button.
* @returns {JSX.Element} - The rendered Radio component.
*/
-const Radio = (props) => {
+const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => {
const theme = useTheme();
return (
}
sx={{
color: "transparent",
@@ -49,16 +50,16 @@ const Radio = (props) => {
}}
/>
}
- onChange={props.onChange}
+ onChange={onChange}
label={
<>
- {props.title}
+ {title}
- {props.desc}
+ {desc}
>
}
@@ -81,9 +82,14 @@ const Radio = (props) => {
};
Radio.propTypes = {
- title: PropTypes.string.isRequired,
+ title: PropTypes.string,
desc: PropTypes.string,
size: PropTypes.string,
+ name: PropTypes.string,
+ checked: PropTypes.bool,
+ value: PropTypes.string,
+ id: PropTypes.string,
+ onChange: PropTypes.func,
};
export default Radio;
diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx
index 86ce57779..4624b2cba 100644
--- a/client/src/Components/Inputs/Search/index.jsx
+++ b/client/src/Components/Inputs/Search/index.jsx
@@ -1,7 +1,17 @@
import PropTypes from "prop-types";
-import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material";
+import {
+ Box,
+ ListItem,
+ Autocomplete,
+ TextField,
+ Stack,
+ Typography,
+ Checkbox,
+} from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
+import React, { useEffect } from "react";
+import { useTranslation } from "react-i18next";
/**
* Search component using Material UI's Autocomplete.
@@ -60,24 +70,72 @@ const Search = ({
onBlur,
}) => {
const theme = useTheme();
+ const { t } = useTranslation();
+ const [selectAll, setSelectAll] = React.useState(false);
+
+ const [open, setOpen] = React.useState(false);
+ const enhancedOptions = React.useMemo(() => {
+ return multiple && isAdorned
+ ? [
+ { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
+ ...options,
+ ]
+ : options;
+ }, [multiple, isAdorned, options, filteredBy]);
+ const isOptionSelected = (option) => {
+ if (!multiple && !isAdorned) return false;
+ if (Array.isArray(value)) {
+ return value.some((item) => item._id === option._id);
+ }
+ return false;
+ };
+ const handleSelectAll = (isSelectAll) => {
+ const newValue = isSelectAll ? [...options] : [];
+ handleChange(newValue);
+ setSelectAll(isSelectAll);
+ };
+ useEffect(() => {
+ const allSelected =
+ Array.isArray(value) && Array.isArray(options) && value.length === options.length;
+ if (selectAll !== allSelected) setSelectAll(allSelected);
+ }, [value, options]);
return (
setOpen(true)}
+ onClose={(event, reason) => {
+ if (reason === "blur" || reason === "escape") {
+ setOpen(false);
+ }
+ }}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
onChange={(_, newValue) => {
- handleChange(newValue);
+ if (multiple && isAdorned) {
+ const hasSelectAllSelected =
+ Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
+ if (hasSelectAllSelected) {
+ handleSelectAll(!selectAll);
+ } else {
+ handleChange(newValue);
+ setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
+ }
+ } else {
+ handleChange(newValue);
+ setOpen(false);
+ }
}}
fullWidth
freeSolo
disabled={disabled}
disableClearable
- options={options}
+ options={enhancedOptions}
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
renderInput={(params) => (
@@ -102,27 +160,7 @@ const Search = ({
...(endAdornment && { endAdornment: endAdornment }),
},
}}
- sx={{
- "& fieldset": {
- borderColor: theme.palette.primary.lowContrast,
- borderRadius: theme.shape.borderRadius,
- },
- "& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
- {
- borderColor: theme.palette.primary.lowContrast,
- },
- "& .MuiOutlinedInput-root": {
- paddingY: 0,
- },
- "& .MuiAutocomplete-tag": {
- // CAIO_REVIEW
- color: theme.palette.primary.contrastText,
- backgroundColor: theme.palette.primary.lowContrast,
- },
- "& .MuiChip-deleteIcon": {
- color: theme.palette.primary.contrastText, // CAIO_REVIEW
- },
- }}
+ sx={{}}
/>
{error && (
)}
filterOptions={(options, { inputValue }) => {
+ if (inputValue.trim() === "" && multiple && isAdorned) {
+ return enhancedOptions;
+ }
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
@@ -156,6 +197,7 @@ const Search = ({
const { key, ...optionProps } = props;
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
const port = option["port"];
+ const selected = isOptionSelected(option);
return (
+ {multiple && isAdorned && !option.noOptions && (
+
+ )}
{option[filteredBy] +
(hasSecondaryLabel
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx
index a31fedf66..679a5b3b5 100644
--- a/client/src/Components/Inputs/Select/index.jsx
+++ b/client/src/Components/Inputs/Select/index.jsx
@@ -12,9 +12,9 @@ import "./index.css";
* @param {string} props.placeholder - The label of the select element.
* @param {string} props.placeholder - The placeholder text when no option is selected.
* @param {boolean} props.isHidden - Whether the placeholder should be hidden.
- * @param {string} props.value - The currently selected value.
+ * @param {(string | number | boolean)} props.value - The currently selected value.
* @param {object[]} props.items - The array of items to populate in the select dropdown.
- * @param {(string | number)} props.items._id - The unique identifier of each item.
+ * @param {(string | number | boolean)} props.items._id - The unique identifier of each item.
* @param {string} props.items.name - The display name of each item.
* @param {function} props.onChange - The function to handle onChange event.
* @param {object} props.sx - The custom styles object for MUI Select component.
@@ -161,11 +161,13 @@ Select.propTypes = {
label: PropTypes.string,
placeholder: PropTypes.string,
isHidden: PropTypes.bool,
- value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
+ value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
+ .isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
+
name: PropTypes.string.isRequired,
})
).isRequired,
diff --git a/client/src/Components/Layouts/AppLayout/index.jsx b/client/src/Components/Layouts/AppLayout/index.jsx
new file mode 100644
index 000000000..d13f6d879
--- /dev/null
+++ b/client/src/Components/Layouts/AppLayout/index.jsx
@@ -0,0 +1,31 @@
+import Box from "@mui/material/Box";
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import BackgroundSVG from "../../../assets/Images/background.svg";
+import { useSelector } from "react-redux";
+
+const AppLayout = ({ children }) => {
+ const theme = useTheme();
+ const ui = useSelector((state) => state.ui);
+ return (
+
+ {children}
+
+ );
+};
+
+AppLayout.propTypes = {
+ children: PropTypes.node,
+};
+
+export default AppLayout;
diff --git a/client/src/Components/Link/index.jsx b/client/src/Components/Link/index.jsx
index 7a623a3b0..987c56f37 100644
--- a/client/src/Components/Link/index.jsx
+++ b/client/src/Components/Link/index.jsx
@@ -1,4 +1,6 @@
import { Link as MuiLink, useTheme } from "@mui/material";
+import { Link as RouterLink } from "react-router-dom";
+
import PropTypes from "prop-types";
/**
@@ -10,7 +12,7 @@ import PropTypes from "prop-types";
* @returns {JSX.Element}
*/
-const Link = ({ level, label, url }) => {
+const Link = ({ level, label, url, external = true }) => {
const theme = useTheme();
const levelConfig = {
@@ -49,11 +51,12 @@ const Link = ({ level, label, url }) => {
const { sx, color } = levelConfig[level];
return (
{label}
@@ -64,6 +67,7 @@ Link.propTypes = {
url: PropTypes.string.isRequired,
level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]),
label: PropTypes.string.isRequired,
+ external: PropTypes.bool,
};
export default Link;
diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx
new file mode 100644
index 000000000..ac5d89a47
--- /dev/null
+++ b/client/src/Components/MonitorActions/index.jsx
@@ -0,0 +1,116 @@
+import * as React from "react";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
+import ClickAwayListener from "@mui/material/ClickAwayListener";
+import Grow from "@mui/material/Grow";
+import Paper from "@mui/material/Paper";
+import Popper from "@mui/material/Popper";
+import MenuItem from "@mui/material/MenuItem";
+import MenuList from "@mui/material/MenuList";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { createToast } from "../../Utils/toastUtils";
+import { useExportMonitors } from "../../Hooks/monitorHooks";
+
+const MonitorActions = ({ isLoading }) => {
+ const [open, setOpen] = React.useState(false);
+ const anchorRef = React.useRef(null);
+ const [selectedIndex, setSelectedIndex] = React.useState(0);
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [exportMonitors, isExporting] = useExportMonitors();
+
+ const options = [t("monitorActions.import"), t("monitorActions.export")];
+
+ const handleClick = async () => {
+ if (selectedIndex === 0) {
+ // Import
+ navigate("/uptime/bulk-import");
+ } else {
+ // Export
+ const [success, error] = await exportMonitors();
+ if (!success) {
+ createToast({ body: error || t("export.failed") });
+ }
+ }
+ };
+
+ const handleMenuItemClick = (event, index) => {
+ setSelectedIndex(index);
+ setOpen(false);
+ };
+
+ const handleToggle = () => {
+ setOpen((prevOpen) => !prevOpen);
+ };
+
+ const handleClose = (event) => {
+ if (anchorRef.current && anchorRef.current.contains(event.target)) {
+ return;
+ }
+ setOpen(false);
+ };
+
+ return (
+
+
+ {options[selectedIndex]}
+
+
+
+
+
+ {({ TransitionProps, placement }) => (
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default MonitorActions;
diff --git a/client/src/Components/MonitorCountHeader/index.jsx b/client/src/Components/MonitorCountHeader/index.jsx
index 0b3478df5..7610a9d5c 100644
--- a/client/src/Components/MonitorCountHeader/index.jsx
+++ b/client/src/Components/MonitorCountHeader/index.jsx
@@ -4,14 +4,14 @@ import { useTheme } from "@emotion/react";
import SkeletonLayout from "./skeleton";
const MonitorCountHeader = ({
- shouldRender = true,
+ isLoading = false,
monitorCount,
heading = "monitors",
sx,
children,
}) => {
const theme = useTheme();
- if (!shouldRender) return ;
+ if (isLoading) return ;
if (monitorCount === 1) {
heading = "monitor";
@@ -42,7 +42,7 @@ const MonitorCountHeader = ({
};
MonitorCountHeader.propTypes = {
- shouldRender: PropTypes.bool,
+ isLoading: PropTypes.bool,
monitorCount: PropTypes.number,
heading: PropTypes.string,
children: PropTypes.node,
diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx
index b630cb259..b3b50681c 100644
--- a/client/src/Components/MonitorCreateHeader/index.jsx
+++ b/client/src/Components/MonitorCreateHeader/index.jsx
@@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
+import MonitorActions from "../MonitorActions";
const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => {
const navigate = useNavigate();
@@ -28,18 +29,7 @@ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath
>
{label || t("createNew")}
- {bulkPath && (
- {
- navigate(`${bulkPath}`);
- }}
- >
- {t("bulkImport.title")}
-
- )}
+ {/* {bulkPath && } */}
);
};
diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx
index aa9ef91b9..5ad665069 100644
--- a/client/src/Components/MonitorDetailsControlHeader/index.jsx
+++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx
@@ -11,10 +11,10 @@ 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";
-
+import { useTestAllNotifications } from "../../Hooks/useNotifications";
/**
* MonitorDetailsControlHeader component displays the control header for monitor details.
* It includes status display, pause/resume button, and a configure button for admins.
@@ -38,12 +38,12 @@ 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();
+ // const [isSending, emailError, sendTestEmail] = useSendTestEmail();
+
+ const [testAllNotifications, isSending, errorAllNotifications] =
+ useTestAllNotifications();
if (isLoading) {
return ;
@@ -66,10 +66,13 @@ const MonitorDetailsControlHeader = ({
loading={isSending}
startIcon={ }
onClick={() => {
- sendTestEmail();
+ testAllNotifications({ monitorId: monitor?._id });
+ }}
+ sx={{
+ whiteSpace: "nowrap",
}}
>
- {t("sendTestEmail")}
+ {t("sendTestNotifications")}
:
}
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 9e01dd502..65671a609 100644
--- a/client/src/Components/MonitorDetailsControlHeader/status.jsx
+++ b/client/src/Components/MonitorDetailsControlHeader/status.jsx
@@ -7,8 +7,8 @@ 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";
+import { formatMonitorUrl } from "../../Utils/utils";
/**
* Status component displays the status information of a monitor.
* It includes the monitor's name, URL, and check interval.
@@ -23,23 +23,18 @@ import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
*/
const Status = ({ monitor }) => {
const theme = useTheme();
- const { statusColor, determineState } = useUtils();
+ const { statusColor, determineState } = useMonitorUtils();
return (
- {monitor?.name}
+ {monitor?.name}
-
- {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
-
+ {formatMonitorUrl(monitor?.url)}
Checking every {formatDurationRounded(monitor?.interval)}.
diff --git a/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx b/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx
deleted file mode 100644
index c966abf33..000000000
--- a/client/src/Components/MonitorStatusHeader/ConfigButton/index.jsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import { Button, Box } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { useNavigate } from "react-router-dom";
-import SettingsIcon from "../../../assets/icons/settings-bold.svg?react";
-import PropTypes from "prop-types";
-const ConfigButton = ({ shouldRender = true, monitorId, path }) => {
- const theme = useTheme();
- const navigate = useNavigate();
-
- if (!shouldRender) return null;
-
- return (
-
- navigate(`/${path}/configure/${monitorId}`)}
- sx={{
- px: theme.spacing(5),
- "& svg": {
- mr: theme.spacing(3),
- "& path": {
- /* Should always be contrastText for the button color */
- stroke: theme.palette.secondary.contrastText,
- },
- },
- }}
- >
- Configure
-
-
- );
-};
-
-ConfigButton.propTypes = {
- shouldRender: PropTypes.bool,
- monitorId: PropTypes.string.isRequired,
- path: PropTypes.string.isRequired,
-};
-
-export default ConfigButton;
diff --git a/client/src/Components/MonitorStatusHeader/index.jsx b/client/src/Components/MonitorStatusHeader/index.jsx
deleted file mode 100644
index edadfb8df..000000000
--- a/client/src/Components/MonitorStatusHeader/index.jsx
+++ /dev/null
@@ -1,59 +0,0 @@
-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 { formatDurationRounded } from "../../Utils/timeUtils";
-import ConfigButton from "./ConfigButton";
-import SkeletonLayout from "./skeleton";
-import PropTypes from "prop-types";
-
-const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => {
- const theme = useTheme();
- const { statusColor, determineState } = useUtils();
- if (isLoading) {
- return ;
- }
-
- return (
-
-
- {monitor?.name}
-
-
-
- {monitor?.url?.replace(/^https?:\/\//, "") || "..."}
-
-
-
- Checking every {formatDurationRounded(monitor?.interval)}.
-
-
-
-
-
- );
-};
-
-MonitorStatusHeader.propTypes = {
- path: PropTypes.string.isRequired,
- isLoading: PropTypes.bool,
- isAdmin: PropTypes.bool,
- monitor: PropTypes.object,
-};
-
-export default MonitorStatusHeader;
diff --git a/client/src/Components/MonitorStatusHeader/skeleton.jsx b/client/src/Components/MonitorStatusHeader/skeleton.jsx
deleted file mode 100644
index 64dc0547f..000000000
--- a/client/src/Components/MonitorStatusHeader/skeleton.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-
-const SkeletonLayout = () => {
- return (
-
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/NotificationConfig/index.jsx
new file mode 100644
index 000000000..d3707eae4
--- /dev/null
+++ b/client/src/Components/NotificationConfig/index.jsx
@@ -0,0 +1,106 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Divider from "@mui/material/Divider";
+import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
+import Search from "../Inputs/Search";
+
+// Utils
+import { useState, useEffect } from "react";
+import { useTheme } from "@mui/material/styles";
+import PropTypes from "prop-types";
+
+const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => {
+ // Local state
+ const [notificationsSearch, setNotificationsSearch] = useState("");
+ const [selectedNotifications, setSelectedNotifications] = useState([]);
+
+ const handleSearch = (value) => {
+ setSelectedNotifications(value);
+ setMonitor((prev) => {
+ return {
+ ...prev,
+ notifications: value.map((notification) => notification._id),
+ };
+ });
+ };
+
+ // Handlers
+ const handleDelete = (id) => {
+ const updatedNotifications = selectedNotifications.filter(
+ (notification) => notification._id !== id
+ );
+
+ setSelectedNotifications(updatedNotifications);
+ setMonitor((prev) => {
+ return {
+ ...prev,
+ notifications: updatedNotifications.map((notification) => notification._id),
+ };
+ });
+ };
+
+ // Setup
+ const theme = useTheme();
+
+ useEffect(() => {
+ if (setNotifications) {
+ const toSet = setNotifications.map((notification) => {
+ return notifications.find((n) => n._id === notification);
+ });
+ setSelectedNotifications(toSet);
+ }
+ }, [setNotifications, notifications]);
+
+ return (
+
+ {
+ handleSearch(value);
+ }}
+ />
+
+ {selectedNotifications.map((notification, index) => (
+
+
+ {notification.notificationName}
+
+ {
+ handleDelete(notification._id);
+ }}
+ sx={{ cursor: "pointer" }}
+ />
+ {index < selectedNotifications.length - 1 && }
+
+ ))}
+
+
+ );
+};
+
+NotificationConfig.propTypes = {
+ notifications: PropTypes.array,
+ setMonitor: PropTypes.func,
+ setNotifications: PropTypes.array,
+};
+
+export default NotificationConfig;
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;
diff --git a/client/src/Components/RoleProtectedRoute/index.jsx b/client/src/Components/RoleProtectedRoute/index.jsx
new file mode 100644
index 000000000..c7b7dfc12
--- /dev/null
+++ b/client/src/Components/RoleProtectedRoute/index.jsx
@@ -0,0 +1,35 @@
+import { Navigate } from "react-router-dom";
+import { useSelector } from "react-redux";
+import PropTypes from "prop-types";
+
+/**
+ * ProtectedRoute is a wrapper component that ensures only authenticated users
+ * can access the wrapped content. It checks authentication status (e.g., from Redux or Context).
+ * If the user is authenticated, it renders the children; otherwise, it redirects to the login page.
+ *
+ * @param {Object} props - The props passed to the ProtectedRoute 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 RoleProtectedRoute = ({ roles, children }) => {
+ const authState = useSelector((state) => state.auth);
+ const userRoles = authState?.user?.role || [];
+ const canAccess = userRoles.some((role) => roles.includes(role));
+
+ return canAccess ? (
+ children
+ ) : (
+
+ );
+};
+
+RoleProtectedRoute.propTypes = {
+ children: PropTypes.element.isRequired,
+ roles: PropTypes.array,
+};
+
+export default RoleProtectedRoute;
diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx
index f78ac62f0..219ceff1e 100644
--- a/client/src/Components/Sidebar/index.jsx
+++ b/client/src/Components/Sidebar/index.jsx
@@ -8,7 +8,6 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
- ListSubheader,
Menu,
MenuItem,
Stack,
@@ -23,7 +22,6 @@ import UserSvg from "../../assets/icons/user.svg?react";
import TeamSvg from "../../assets/icons/user-two.svg?react";
import LogoutSvg from "../../assets/icons/logout.svg?react";
import Support from "../../assets/icons/support.svg?react";
-import Account from "../../assets/icons/user-edit.svg?react";
import Maintenance from "../../assets/icons/maintenance.svg?react";
import Monitors from "../../assets/icons/monitors.svg?react";
import Incidents from "../../assets/icons/incidents.svg?react";
@@ -37,10 +35,11 @@ import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
-import Folder from "../../assets/icons/folder.svg?react";
import StatusPages from "../../assets/icons/status-pages.svg?react";
import Discussions from "../../assets/icons/discussions.svg?react";
-import DistributedUptimeIcon from "../../assets/icons/distributed-uptime.svg?react";
+import Notifications from "../../assets/icons/notifications.svg?react";
+import Logs from "../../assets/icons/logs.svg?react";
+
import "./index.css";
// Utils
@@ -50,22 +49,25 @@ import { useDispatch, useSelector } from "react-redux";
import { useTranslation } from "react-i18next";
import { clearAuthState } from "../../Features/Auth/authSlice";
import { toggleSidebar } from "../../Features/UI/uiSlice";
-import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import { TurnedIn } from "@mui/icons-material";
+import { rules } from "eslint-plugin-react-refresh";
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: },
+
{ name: t("menu.infrastructure"), path: "infrastructure", icon: },
{
- name: t("menu.distributedUptime"),
- path: "distributed-uptime",
- icon: ,
+ name: t("menu.notifications"),
+ path: "notifications",
+ icon: ,
},
{ name: t("menu.incidents"), path: "incidents", icon: },
{ name: t("menu.statusPages"), path: "status", icon: },
{ name: t("menu.maintenance"), path: "maintenance", icon: },
- // { name: t("menu.integrations"), path: "integrations", icon: },
+ { name: t("menu.logs"), path: "logs", icon: },
+
{
name: t("menu.settings"),
icon: ,
@@ -95,14 +97,13 @@ const URL_MAP = {
support: "https://discord.com/invite/NAb6H3UTjK",
discussions: "https://github.com/bluewave-labs/checkmate/discussions",
docs: "https://bluewavelabs.gitbook.io/checkmate",
- changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
+ changelog: "https://github.com/bluewave-labs/checkmate/releases",
};
const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
infrastructure: "Dashboard",
- ["distributed-uptime"]: "Dashboard",
account: "Account",
settings: "Settings",
};
@@ -122,7 +123,6 @@ function Sidebar() {
const { t } = useTranslation();
const authState = useSelector((state) => state.auth);
- const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
const accountMenuItems = getAccountMenuItems(t);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
@@ -130,12 +130,17 @@ 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;
+ let menu = getMenu(t);
+ menu = menu.filter((item) => {
+ if (item.path === "logs") {
+ return user.role?.includes("admin") || user.role?.includes("superadmin");
+ }
+ return true;
+ });
useEffect(() => {
if (!collapsed) {
@@ -200,7 +205,6 @@ function Sidebar() {
const logout = async () => {
// Clear auth state
dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
navigate("/login");
};
@@ -234,7 +238,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
@@ -346,7 +349,7 @@ function Sidebar() {
mt={theme.spacing(2)}
sx={{ opacity: 0.8, fontWeight: 500 }}
>
- Checkmate
+ {t("common.appName")}
@@ -375,12 +378,6 @@ function Sidebar() {
}}
>
{menu.map((item) => {
- if (
- item.path === "distributed-uptime" &&
- distributedUptimeEnabled === false
- ) {
- return null;
- }
return item.path ? (
/* If item has a path */
{
const theme = useTheme();
- const { statusToTheme } = useUtils();
+ const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
@@ -136,8 +136,8 @@ const StatBox = ({
};
StatBox.propTypes = {
- heading: PropTypes.string.isRequired,
- subHeading: PropTypes.node.isRequired,
+ heading: PropTypes.string,
+ subHeading: PropTypes.node,
gradient: PropTypes.bool,
status: PropTypes.string,
sx: PropTypes.object,
diff --git a/client/src/Components/StatusBoxes/index.jsx b/client/src/Components/StatusBoxes/index.jsx
index 40ea2d182..d97bb6211 100644
--- a/client/src/Components/StatusBoxes/index.jsx
+++ b/client/src/Components/StatusBoxes/index.jsx
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
-const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
+const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
diff --git a/client/src/Components/TabPanels/Account/PasswordPanel.jsx b/client/src/Components/TabPanels/Account/PasswordPanel.jsx
index 88604c72b..84d16e8b0 100644
--- a/client/src/Components/TabPanels/Account/PasswordPanel.jsx
+++ b/client/src/Components/TabPanels/Account/PasswordPanel.jsx
@@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments";
import TextInput from "../../Inputs/TextInput";
-import { credentials } from "../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../Validation/validation";
import Alert from "../../Alert";
import { update } from "../../../Features/Auth/authSlice";
import { useDispatch, useSelector } from "react-redux";
@@ -61,7 +61,7 @@ const PasswordPanel = () => {
[name]: true,
};
- const validation = credentials.validate(
+ const validation = newOrChangedCredentials.validate(
{ ...updatedData },
{ abortEarly: false, context: { password: updatedData.newPassword } }
);
@@ -79,7 +79,7 @@ const PasswordPanel = () => {
const handleSubmit = async (event) => {
event.preventDefault();
- const { error } = credentials.validate(localData, {
+ const { error } = newOrChangedCredentials.validate(localData, {
abortEarly: false,
context: { password: localData.newPassword },
});
diff --git a/client/src/Components/TabPanels/Account/ProfilePanel.jsx b/client/src/Components/TabPanels/Account/ProfilePanel.jsx
index 53a8a8d77..9b209bf3e 100644
--- a/client/src/Components/TabPanels/Account/ProfilePanel.jsx
+++ b/client/src/Components/TabPanels/Account/ProfilePanel.jsx
@@ -5,10 +5,9 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material";
import Avatar from "../../Avatar";
import TextInput from "../../Inputs/TextInput";
import ImageUpload from "../../Inputs/ImageUpload";
-import { credentials } from "../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice";
-import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import { GenericDialog } from "../../Dialog/genericDialog";
@@ -58,7 +57,7 @@ const ProfilePanel = () => {
[name]: value,
}));
- validateField({ [name]: value }, credentials, name);
+ validateField({ [name]: value }, newOrChangedCredentials, name);
};
// Validates input against provided schema and updates error state
@@ -162,7 +161,6 @@ const ProfilePanel = () => {
const action = await dispatch(deleteUser());
if (action.payload.success) {
dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
} else {
if (action.payload) {
// dispatch errors
@@ -240,7 +238,7 @@ const ProfilePanel = () => {
gap={SPACING_GAP}
>
- {t("email")}
+ {t("auth.common.inputs.email.label")}
{
email: newEmail,
}));
- const validation = credentials.validate({ email: newEmail }, { abortEarly: false });
+ const validation = newOrChangedCredentials.validate(
+ { email: newEmail },
+ { abortEarly: false }
+ );
setErrors((prev) => {
const updatedErrors = { ...prev };
@@ -142,7 +145,7 @@ const TeamPanel = () => {
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
setToInvite((prev) => ({ ...prev, role: ["user"] }));
- const { error } = credentials.validate(
+ const { error } = newOrChangedCredentials.validate(
{ email: toInvite.email },
{
abortEarly: false,
@@ -165,7 +168,7 @@ const TeamPanel = () => {
});
} catch (error) {
createToast({
- body: error.message || "Unknown error.",
+ body: error?.response?.data?.msg || error.message || "Unknown error.",
});
} finally {
setIsSendingInvite(false);
@@ -265,7 +268,7 @@ const TeamPanel = () => {
value={toInvite.email}
onChange={handleChange}
error={errors.email ? true : false}
- helperText={errors.email}
+ helperText={t(errors.email)}
/>
{}) => {
+ return ({ id, content, onClick = () => {}, render = () => {} }) => {
+ return {
+ id,
+ content,
+ onClick,
+ getCellSx,
+ render,
+ };
+ };
+};
diff --git a/client/src/Components/TextLink/index.jsx b/client/src/Components/TextLink/index.jsx
new file mode 100644
index 000000000..83ae251a5
--- /dev/null
+++ b/client/src/Components/TextLink/index.jsx
@@ -0,0 +1,36 @@
+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, target = "_self" }) => {
+ const theme = useTheme();
+
+ return (
+
+ {text}
+
+ {linkText}
+
+
+ );
+};
+
+TextLink.propTypes = {
+ text: PropTypes.string,
+ linkText: PropTypes.string,
+ href: PropTypes.string,
+ target: PropTypes.string,
+};
+
+export default TextLink;
diff --git a/client/src/Components/ThemeSwitch/index.jsx b/client/src/Components/ThemeSwitch/index.jsx
index 6c879b289..5efb17419 100644
--- a/client/src/Components/ThemeSwitch/index.jsx
+++ b/client/src/Components/ThemeSwitch/index.jsx
@@ -14,10 +14,12 @@ import SunAndMoonIcon from "./SunAndMoonIcon";
import { useDispatch, useSelector } from "react-redux";
import { setMode } from "../../Features/UI/uiSlice";
import "./index.css";
+import { useTranslation } from "react-i18next";
const ThemeSwitch = ({ width = 48, height = 48, color }) => {
const mode = useSelector((state) => state.ui.mode);
const dispatch = useDispatch();
+ const { t } = useTranslation();
const toggleTheme = () => {
dispatch(setMode(mode === "light" ? "dark" : "light"));
@@ -26,7 +28,7 @@ const ThemeSwitch = ({ width = 48, height = 48, color }) => {
return (
{
- try {
- const { monitor } = data;
- const res = await networkService.createMonitor({ monitor: monitor });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const checkInfrastructureEndpointResolution = createAsyncThunk(
- "infrastructureMonitors/CheckEndpoint",
- async (data, thunkApi) => {
- try {
- const { monitorURL } = data;
- const res = await networkService.checkEndpointResolution({
- monitorURL: monitorURL,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getInfrastructureMonitorById = createAsyncThunk(
- "infrastructureMonitors/getMonitorById",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.getMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getInfrastructureMonitorsByTeamId = createAsyncThunk(
- "infrastructureMonitors/getMonitorsByTeamId",
- async (_, thunkApi) => {
- const user = thunkApi.getState().auth.user;
- try {
- const res = await networkService.getMonitorsAndSummaryByTeamId({
- teamId: user.teamId,
- types: ["hardware"],
- limit: 1,
- rowsPerPage: 0,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const updateInfrastructureMonitor = createAsyncThunk(
- "infrastructureMonitors/updateMonitor",
- async ({ monitorId, monitor }, thunkApi) => {
- try {
- const updatedFields = {
- name: monitor.name,
- description: monitor.description,
- interval: monitor.interval,
- notifications: monitor.notifications,
- thresholds: monitor.thresholds,
- secret: monitor.secret,
- };
- const res = await networkService.updateMonitor({
- monitorId,
- monitor,
- updatedFields,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteInfrastructureMonitor = createAsyncThunk(
- "infrastructureMonitors/deleteMonitor",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const pauseInfrastructureMonitor = createAsyncThunk(
- "infrastructureMonitors/pauseMonitor",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.pauseMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk(
- "infrastructureMonitors/deleteChecksByTeamId",
- async (data, thunkApi) => {
- try {
- const { teamId } = data;
- const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteAllInfrastructureMonitors = createAsyncThunk(
- "infrastructureMonitors/deleteAllMonitors",
- async (data, thunkApi) => {
- try {
- const res = await networkService.deleteAllMonitors();
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-const infrastructureMonitorsSlice = createSlice({
- name: "infrastructureMonitors",
- initialState,
- reducers: {
- clearInfrastructureMonitorState: (state) => {
- state.isLoading = false;
- state.monitorsSummary = [];
- state.success = null;
- state.msg = null;
- },
- },
- extraReducers: (builder) => {
- builder
- // *****************************************************
- // Monitors by teamId
- // *****************************************************
-
- .addCase(getInfrastructureMonitorsByTeamId.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.msg;
- state.monitorsSummary = action.payload.data;
- })
- .addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Getting infrastructure monitors failed";
- })
-
- // *****************************************************
- // Create Monitor
- // *****************************************************
- .addCase(createInfrastructureMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(createInfrastructureMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(createInfrastructureMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to create infrastructure monitor";
- })
- // *****************************************************
- // Resolve Endpoint
- // *****************************************************
- .addCase(checkInfrastructureEndpointResolution.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to check endpoint resolution";
- })
- // *****************************************************
- // Get Monitor By Id
- // *****************************************************
- .addCase(getInfrastructureMonitorById.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getInfrastructureMonitorById.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(getInfrastructureMonitorById.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to get infrastructure monitor";
- })
- // *****************************************************
- // update Monitor
- // *****************************************************
- .addCase(updateInfrastructureMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(updateInfrastructureMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(updateInfrastructureMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to update infrastructure monitor";
- })
-
- // *****************************************************
- // Delete Monitor
- // *****************************************************
- .addCase(deleteInfrastructureMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteInfrastructureMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to delete infrastructure monitor";
- })
- // *****************************************************
- // Delete Monitor checks by Team ID
- // *****************************************************
- .addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to delete monitor checks";
- })
- // *****************************************************
- // Pause Monitor
- // *****************************************************
- .addCase(pauseInfrastructureMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(pauseInfrastructureMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to pause infrastructure monitor";
- })
- // *****************************************************
- // Delete all Monitors
- // *****************************************************
- .addCase(deleteAllInfrastructureMonitors.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
- });
- },
-});
-
-export const { setInfrastructureMonitors, clearInfrastructureMonitorState } =
- infrastructureMonitorsSlice.actions;
-
-export default infrastructureMonitorsSlice.reducer;
diff --git a/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
deleted file mode 100644
index 3ed2da13f..000000000
--- a/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js
+++ /dev/null
@@ -1,309 +0,0 @@
-import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
-import { networkService } from "../../main";
-const initialState = {
- isLoading: false,
- monitorsSummary: [],
- success: null,
- msg: null,
-};
-
-export const createPageSpeed = createAsyncThunk(
- "pageSpeedMonitors/createPageSpeed",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const res = await networkService.createMonitor({ monitor: monitor });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const checkEndpointResolution = createAsyncThunk(
- "monitors/checkEndpoint",
- async (data, thunkApi) => {
- try {
- const { monitorURL } = data;
- const res = await networkService.checkEndpointResolution({
- monitorURL: monitorURL,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getPagespeedMonitorById = createAsyncThunk(
- "monitors/getMonitorById",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.getMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getPageSpeedByTeamId = createAsyncThunk(
- "pageSpeedMonitors/getPageSpeedByTeamId",
- async (_, thunkApi) => {
- const user = thunkApi.getState().auth.user;
- try {
- const res = await networkService.getMonitorsAndSummaryByTeamId({
- teamId: user.teamId,
- types: ["pagespeed"],
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const updatePageSpeed = createAsyncThunk(
- "pageSpeedMonitors/updatePageSpeed",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const updatedFields = {
- name: monitor.name,
- description: monitor.description,
- interval: monitor.interval,
- notifications: monitor.notifications,
- };
- const res = await networkService.updateMonitor({
- monitorId: monitor._id,
- updatedFields: updatedFields,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deletePageSpeed = createAsyncThunk(
- "pageSpeedMonitors/deletePageSpeed",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-export const pausePageSpeed = createAsyncThunk(
- "pageSpeedMonitors/pausePageSpeed",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.pauseMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-const pageSpeedMonitorSlice = createSlice({
- name: "pageSpeedMonitor",
- initialState,
- reducers: {
- clearMonitorState: (state) => {
- state.isLoading = false;
- state.monitorsSummary = [];
- state.success = null;
- state.msg = null;
- },
- },
- extraReducers: (builder) => {
- builder
- // *****************************************************
- // Monitors by teamId
- // *****************************************************
-
- .addCase(getPageSpeedByTeamId.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getPageSpeedByTeamId.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.msg;
- state.monitorsSummary = action.payload.data;
- })
- .addCase(getPageSpeedByTeamId.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Getting page speed monitors failed";
- })
-
- // *****************************************************
- .addCase(getPagespeedMonitorById.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getPagespeedMonitorById.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(getPagespeedMonitorById.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to get pagespeed monitor";
- })
-
- // *****************************************************
- // Create Monitor
- // *****************************************************
- .addCase(createPageSpeed.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(createPageSpeed.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(createPageSpeed.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to create page speed monitor";
- })
- // *****************************************************
- // Resolve Endpoint
- // *****************************************************
- .addCase(checkEndpointResolution.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(checkEndpointResolution.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(checkEndpointResolution.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to check endpoint resolution";
- })
- // *****************************************************
- // Update Monitor
- // *****************************************************
- .addCase(updatePageSpeed.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(updatePageSpeed.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(updatePageSpeed.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to update page speed monitor";
- })
-
- // *****************************************************
- // Delete Monitor
- // *****************************************************
- .addCase(deletePageSpeed.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deletePageSpeed.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deletePageSpeed.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to delete page speed monitor";
- })
- // *****************************************************
- // Pause Monitor
- // *****************************************************
- .addCase(pausePageSpeed.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(pausePageSpeed.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(pausePageSpeed.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to pause page speed monitor";
- });
- },
-});
-
-export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions;
-
-export default pageSpeedMonitorSlice.reducer;
diff --git a/client/src/Features/Settings/settingsSlice.js b/client/src/Features/Settings/settingsSlice.js
deleted file mode 100644
index d0725627a..000000000
--- a/client/src/Features/Settings/settingsSlice.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { networkService } from "../../main";
-import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
-
-const initialState = {
- isLoading: false,
- apiBaseUrl: "",
- logLevel: "debug",
- pagespeedApiKey: "",
-};
-
-export const getAppSettings = createAsyncThunk(
- "settings/getSettings",
- async (data, thunkApi) => {
- try {
- const res = await networkService.getAppSettings();
- return res.data;
- } catch (error) {
- if (error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const updateAppSettings = createAsyncThunk(
- "settings/updateSettings",
- async ({ settings }, thunkApi) => {
- try {
- const parsedSettings = {
- language: settings.language,
- pagespeedApiKey: settings.pagespeedApiKey,
- };
- const res = await networkService.updateAppSettings({ settings: parsedSettings });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-const handleGetSettingsFulfilled = (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- state.apiBaseUrl = action.payload.data.apiBaseUrl;
- state.logLevel = action.payload.data.logLevel;
- state.language = action.payload.data.language;
- state.pagespeedApiKey = action.payload.data.pagespeedApiKey;
-};
-const handleGetSettingsRejected = (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to get settings.";
-};
-const handleUpdateSettingsFulfilled = (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- state.apiBaseUrl = action.payload.data.apiBaseUrl;
- state.logLevel = action.payload.data.logLevel;
-};
-const handleUpdateSettingsRejected = (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to update settings.";
-};
-
-const settingsSlice = createSlice({
- name: "settings",
- initialState,
- extraReducers: (builder) => {
- builder
- .addCase(getAppSettings.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled)
- .addCase(getAppSettings.rejected, handleGetSettingsRejected);
-
- builder
- .addCase(updateAppSettings.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled)
- .addCase(updateAppSettings.rejected, handleUpdateSettingsRejected);
- },
-});
-
-export default settingsSlice.reducer;
diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js
index 3addfbb7a..45ba267b0 100644
--- a/client/src/Features/UI/uiSlice.js
+++ b/client/src/Features/UI/uiSlice.js
@@ -16,6 +16,9 @@ const initialState = {
maintenance: {
rowsPerPage: 5,
},
+ infrastructure: {
+ rowsPerPage: 5,
+ },
sidebar: {
collapsed: false,
},
diff --git a/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
deleted file mode 100644
index 74ad803d3..000000000
--- a/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js
+++ /dev/null
@@ -1,380 +0,0 @@
-import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
-import { networkService } from "../../main";
-const initialState = {
- isLoading: false,
- monitorsSummary: [],
- success: null,
- msg: null,
-};
-
-export const createUptimeMonitor = createAsyncThunk(
- "monitors/createMonitor",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const res = await networkService.createMonitor({ monitor: monitor });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const checkEndpointResolution = createAsyncThunk(
- "monitors/checkEndpoint",
- async (data, thunkApi) => {
- try {
- const { monitorURL } = data;
- const res = await networkService.checkEndpointResolution({
- monitorURL: monitorURL,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const getUptimeMonitorById = createAsyncThunk(
- "monitors/getMonitorById",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.getMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const updateUptimeMonitor = createAsyncThunk(
- "monitors/updateMonitor",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- 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 }),
- };
- const res = await networkService.updateMonitor({
- monitorId: monitor._id,
- updatedFields,
- });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteUptimeMonitor = createAsyncThunk(
- "monitors/deleteMonitor",
- async (data, thunkApi) => {
- try {
- const { monitor } = data;
- const res = await networkService.deleteMonitorById({ monitorId: monitor._id });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const pauseUptimeMonitor = createAsyncThunk(
- "monitors/pauseMonitor",
- async (data, thunkApi) => {
- try {
- const { monitorId } = data;
- const res = await networkService.pauseMonitorById({ monitorId: monitorId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteMonitorChecksByTeamId = createAsyncThunk(
- "monitors/deleteChecksByTeamId",
- async (data, thunkApi) => {
- try {
- const { teamId } = data;
- const res = await networkService.deleteChecksByTeamId({ teamId: teamId });
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-export const addDemoMonitors = createAsyncThunk(
- "monitors/addDemoMonitors",
- async (data, thunkApi) => {
- try {
- const res = await networkService.addDemoMonitors();
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-export const deleteAllMonitors = createAsyncThunk(
- "monitors/deleteAllMonitors",
- async (data, thunkApi) => {
- try {
- const res = await networkService.deleteAllMonitors();
- return res.data;
- } catch (error) {
- if (error.response && error.response.data) {
- return thunkApi.rejectWithValue(error.response.data);
- }
- const payload = {
- status: false,
- msg: error.message ? error.message : "Unknown error",
- };
- return thunkApi.rejectWithValue(payload);
- }
- }
-);
-
-const uptimeMonitorsSlice = createSlice({
- name: "uptimeMonitors",
- initialState,
- reducers: {
- clearUptimeMonitorState: (state) => {
- state.isLoading = false;
- state.monitorsSummary = [];
- state.success = null;
- state.msg = null;
- },
- },
- extraReducers: (builder) => {
- builder
- // *****************************************************
- // Create Monitor
- // *****************************************************
- .addCase(createUptimeMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(createUptimeMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(createUptimeMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to create uptime monitor";
- })
- // *****************************************************
- // Resolve Endpoint
- // *****************************************************
- .addCase(checkEndpointResolution.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(checkEndpointResolution.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(checkEndpointResolution.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to check endpoint resolution";
- })
- // *****************************************************
- // Get Monitor By Id
- // *****************************************************
- .addCase(getUptimeMonitorById.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(getUptimeMonitorById.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(getUptimeMonitorById.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to get uptime monitor";
- })
- // *****************************************************
- // update Monitor
- // *****************************************************
- .addCase(updateUptimeMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(updateUptimeMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(updateUptimeMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to update uptime monitor";
- })
-
- // *****************************************************
- // Delete Monitor
- // *****************************************************
- .addCase(deleteUptimeMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteUptimeMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteUptimeMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to delete uptime monitor";
- })
- // *****************************************************
- // Delete Monitor checks by Team ID
- // *****************************************************
- .addCase(deleteMonitorChecksByTeamId.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to delete monitor checks";
- })
- // *****************************************************
- // Pause Monitor
- // *****************************************************
- .addCase(pauseUptimeMonitor.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(pauseUptimeMonitor.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(pauseUptimeMonitor.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to pause uptime monitor";
- })
- // *****************************************************
- // Add Demo Monitors
- // *****************************************************
- .addCase(addDemoMonitors.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(addDemoMonitors.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(addDemoMonitors.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload
- ? action.payload.msg
- : "Failed to add demo uptime monitors";
- })
- // *****************************************************
- // Delete all Monitors
- // *****************************************************
- .addCase(deleteAllMonitors.pending, (state) => {
- state.isLoading = true;
- })
- .addCase(deleteAllMonitors.fulfilled, (state, action) => {
- state.isLoading = false;
- state.success = action.payload.success;
- state.msg = action.payload.msg;
- })
- .addCase(deleteAllMonitors.rejected, (state, action) => {
- state.isLoading = false;
- state.success = false;
- state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors";
- });
- },
-});
-
-export const { setUptimeMonitors, clearUptimeMonitorState } = uptimeMonitorsSlice.actions;
-
-export default uptimeMonitorsSlice.reducer;
diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js
new file mode 100644
index 000000000..683b89b20
--- /dev/null
+++ b/client/src/Hooks/checkHooks.js
@@ -0,0 +1,120 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+
+const useFetchChecksTeam = ({
+ status,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ enabled = true,
+}) => {
+ const [checks, setChecks] = useState(undefined);
+ const [checksCount, setChecksCount] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [networkError, setNetworkError] = useState(false);
+
+ useEffect(() => {
+ const fetchChecks = async () => {
+ if (!enabled) {
+ return;
+ }
+
+ const config = {
+ status,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ };
+
+ try {
+ setIsLoading(true);
+ const res = await networkService.getChecksByTeam(config);
+ setChecks(res.data.data.checks);
+ setChecksCount(res.data.data.checksCount);
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error.message });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchChecks();
+ }, [status, sortOrder, limit, dateRange, filter, page, rowsPerPage, enabled]);
+
+ return [checks, checksCount, isLoading, networkError];
+};
+
+const useFetchChecksByMonitor = ({
+ monitorId,
+ type,
+ status,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ enabled = true,
+}) => {
+ const [checks, setChecks] = useState(undefined);
+ const [checksCount, setChecksCount] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [networkError, setNetworkError] = useState(false);
+
+ useEffect(() => {
+ const fetchChecks = async () => {
+ if (!enabled || !type) {
+ return;
+ }
+
+ const config = {
+ monitorId,
+ type,
+ status,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ };
+
+ try {
+ setIsLoading(true);
+ const res = await networkService.getChecksByMonitor(config);
+ setChecks(res.data.data.checks);
+ setChecksCount(res.data.data.checksCount);
+ } catch (error) {
+ setNetworkError(true);
+ createToast({ body: error.message });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchChecks();
+ }, [
+ monitorId,
+ type,
+ status,
+ sortOrder,
+ limit,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ enabled,
+ ]);
+
+ return [checks, checksCount, isLoading, networkError];
+};
+
+export { useFetchChecksByMonitor, useFetchChecksTeam };
diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js
new file mode 100644
index 000000000..000e4d0f0
--- /dev/null
+++ b/client/src/Hooks/logHooks.js
@@ -0,0 +1,35 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+
+const useFetchLogs = () => {
+ const { t } = useTranslation();
+ const [logs, setLogs] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ const fetchLogs = async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getLogs();
+ setLogs(response.data.data);
+ createToast({
+ body: t("logsPage.toast.fetchLogsSuccess"),
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ message: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchLogs();
+ }, [t]);
+ return [logs, isLoading, error];
+};
+
+export { useFetchLogs };
diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js
new file mode 100644
index 000000000..013e7fc25
--- /dev/null
+++ b/client/src/Hooks/monitorHooks.js
@@ -0,0 +1,506 @@
+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";
+import { useTranslation } from "react-i18next";
+
+const useFetchMonitorsWithSummary = ({ 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({
+ 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();
+ }, [types, monitorUpdateTrigger]);
+ return [monitors, monitorsSummary, isLoading, networkError];
+};
+
+const useFetchMonitorsWithChecks = ({
+ 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({
+ 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,
+ theme,
+ types,
+ monitorUpdateTrigger,
+ ]);
+ return [monitors, count, isLoading, networkError];
+};
+
+const useFetchMonitorsByTeamId = ({
+ types,
+ limit,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ checkOrder,
+ normalize,
+ status,
+ 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({
+ limit,
+ types,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ checkOrder,
+ status,
+ normalize,
+ });
+ 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();
+ }, [
+ types,
+ limit,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ updateTrigger,
+ checkOrder,
+ normalize,
+ status,
+ ]);
+ return [monitors, summary, isLoading, networkError];
+};
+
+const useFetchStatsByMonitorId = ({
+ monitorId,
+ sortOrder,
+ limit,
+ dateRange,
+ numToDisplay,
+ normalize,
+ updateTrigger,
+}) => {
+ 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, updateTrigger]);
+ 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, updateTrigger }) => {
+ 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, updateTrigger]);
+ 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];
+};
+
+const useAddDemoMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+ const addDemoMonitors = async () => {
+ try {
+ setIsLoading(true);
+ await networkService.addDemoMonitors();
+ createToast({ body: t("settingsDemoMonitorsAdded") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToAddDemoMonitors") });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return [addDemoMonitors, isLoading];
+};
+
+const useDeleteAllMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+ const deleteAllMonitors = async () => {
+ try {
+ setIsLoading(true);
+ await networkService.deleteAllMonitors();
+ createToast({ body: t("settingsMonitorsDeleted") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToDeleteMonitors") });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ return [deleteAllMonitors, isLoading];
+};
+
+const useDeleteMonitorStats = () => {
+ const { t } = useTranslation();
+ const [isLoading, setIsLoading] = useState(false);
+ const deleteMonitorStats = async () => {
+ setIsLoading(true);
+ try {
+ await networkService.deleteChecksByTeamId();
+ createToast({ body: t("settingsStatsCleared") });
+ } catch (error) {
+ createToast({ body: t("settingsFailedToClearStats") });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [deleteMonitorStats, isLoading];
+};
+
+const useCreateBulkMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+
+ const createBulkMonitors = async (file, user) => {
+ setIsLoading(true);
+
+ const formData = new FormData();
+ formData.append("csvFile", file);
+
+ try {
+ const response = await networkService.createBulkMonitors(formData);
+ return [true, response.data, null]; // [success, data, error]
+ } catch (err) {
+ const errorMessage = err?.response?.data?.msg || err.message;
+ return [false, null, errorMessage];
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [createBulkMonitors, isLoading];
+};
+
+const useExportMonitors = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const { t } = useTranslation();
+
+ const exportMonitors = async () => {
+ setIsLoading(true);
+ try {
+ const response = await networkService.exportMonitors();
+
+ // Create a download link
+ const url = window.URL.createObjectURL(response.data);
+ const link = document.createElement("a");
+ link.href = url;
+ link.setAttribute("download", "monitors.csv");
+ document.body.appendChild(link);
+ link.click();
+ link.remove();
+ window.URL.revokeObjectURL(url);
+
+ createToast({ body: t("export.success") });
+ return [true, null];
+ } catch (err) {
+ const errorMessage = err?.response?.data?.msg || err.message;
+ createToast({ body: errorMessage || t("export.failed") });
+ return [false, errorMessage];
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [exportMonitors, isLoading];
+};
+
+export {
+ useFetchMonitorsWithSummary,
+ useFetchMonitorsWithChecks,
+ useFetchMonitorsByTeamId,
+ useFetchStatsByMonitorId,
+ useFetchMonitorById,
+ useFetchUptimeMonitorById,
+ useFetchHardwareMonitorById,
+ useCreateMonitor,
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+ useAddDemoMonitors,
+ useDeleteAllMonitors,
+ useDeleteMonitorStats,
+ useCreateBulkMonitors,
+ useExportMonitors,
+};
diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js
new file mode 100644
index 000000000..f7a25b3d3
--- /dev/null
+++ b/client/src/Hooks/queueHooks.js
@@ -0,0 +1,57 @@
+import { useState, useEffect } from "react";
+import { networkService } from "../main";
+import { createToast } from "../Utils/toastUtils";
+
+const useFetchQueueData = (trigger) => {
+ const [jobs, setJobs] = useState(undefined);
+ const [metrics, setMetrics] = useState(undefined);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ useEffect(() => {
+ const fetchJobs = async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getQueueData();
+ if (response.status === 200) {
+ setJobs(response.data.data.jobs);
+ setMetrics(response.data.data.metrics);
+ }
+ } catch (error) {
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchJobs();
+ }, [trigger]);
+
+ return [jobs, metrics, isLoading, error];
+};
+
+const useFlushQueue = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+
+ const flushQueue = async (trigger, setTrigger) => {
+ try {
+ setIsLoading(true);
+ await networkService.flushQueue();
+ createToast({
+ body: "Queue flushed",
+ });
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: error.message,
+ });
+ } finally {
+ setIsLoading(false);
+ setTrigger(!trigger);
+ }
+ };
+ return [flushQueue, isLoading, error];
+};
+
+export { useFetchQueueData, useFlushQueue };
diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/settingsHooks.js
similarity index 59%
rename from client/src/Hooks/useFetchSettings.js
rename to client/src/Hooks/settingsHooks.js
index f7651e789..98a21c1fa 100644
--- a/client/src/Hooks/useFetchSettings.js
+++ b/client/src/Hooks/settingsHooks.js
@@ -3,7 +3,7 @@ import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
-const useFetchSettings = ({ setSettingsData }) => {
+const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
@@ -12,6 +12,8 @@ const useFetchSettings = ({ setSettingsData }) => {
try {
const response = await networkService.getAppSettings();
setSettingsData(response?.data?.data);
+ setIsApiKeySet(response?.data?.data?.pagespeedKeySet);
+ setIsEmailPasswordSet(response?.data?.data?.emailPasswordSet);
} catch (error) {
createToast({ body: "Failed to fetch settings" });
setError(error);
@@ -20,12 +22,18 @@ const useFetchSettings = ({ setSettingsData }) => {
}
};
fetchSettings();
- }, []);
+ }, [setSettingsData]);
return [isLoading, error];
};
-const useSaveSettings = () => {
+const useSaveSettings = ({
+ setSettingsData,
+ setIsApiKeySet,
+ setApiKeyHasBeenReset,
+ setIsEmailPasswordSet,
+ setEmailPasswordHasBeenReset,
+}) => {
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
const { t } = useTranslation();
@@ -33,12 +41,21 @@ const useSaveSettings = () => {
const saveSettings = async (settings) => {
setIsLoading(true);
try {
- await networkService.updateAppSettings({ settings });
+ const settingsResponse = await networkService.updateAppSettings({ settings });
if (settings.checkTTL) {
await networkService.updateChecksTTL({
ttl: settings.checkTTL,
});
}
+ setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet);
+ setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet);
+ if (settingsResponse.data.data.pagespeedKeySet === true) {
+ setApiKeyHasBeenReset(false);
+ }
+ if (settingsResponse.data.data.emailPasswordSet === true) {
+ setEmailPasswordHasBeenReset(false);
+ }
+ setSettingsData(settingsResponse.data.data);
createToast({ body: t("settingsSuccessSaved") });
} catch (error) {
createToast({ body: t("settingsFailedToSave") });
diff --git a/client/src/Hooks/useBulkMonitors.js b/client/src/Hooks/useBulkMonitors.js
deleted file mode 100644
index a834f0140..000000000
--- a/client/src/Hooks/useBulkMonitors.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { useState } from "react";
-import { networkService } from "../main";
-
-export const useBulkMonitors = () => {
- const [isLoading, setIsLoading] = useState(false);
-
- const createBulkMonitors = async (file, user) => {
- setIsLoading(true);
-
- const formData = new FormData();
- formData.append("csvFile", file);
- formData.append("userId", user._id);
- formData.append("teamId", user.teamId);
-
- try {
- const response = await networkService.createBulkMonitors(formData);
- return [true, response.data, null]; // [success, data, error]
- } catch (err) {
- const errorMessage = err?.response?.data?.msg || err.message;
- return [false, null, errorMessage];
- } finally {
- setIsLoading(false);
- }
- };
-
- return [createBulkMonitors, isLoading];
-};
diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js
deleted file mode 100644
index 64138929f..000000000
--- a/client/src/Hooks/useDeleteMonitorStats.js
+++ /dev/null
@@ -1,24 +0,0 @@
-import { useState } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-import { useTranslation } from "react-i18next";
-
-const UseDeleteMonitorStats = () => {
- const { t } = useTranslation();
- const [isLoading, setIsLoading] = useState(false);
- const deleteMonitorStats = async ({ teamId }) => {
- setIsLoading(true);
- try {
- const res = await networkService.deleteChecksByTeamId({ teamId });
- createToast({ body: t("settingsStatsCleared") });
- } catch (error) {
- createToast({ body: t("settingsFailedToClearStats") });
- } finally {
- setIsLoading(false);
- }
- };
-
- return [deleteMonitorStats, isLoading];
-};
-
-export { UseDeleteMonitorStats };
diff --git a/client/src/Hooks/useFetchDepinStatusPage.js b/client/src/Hooks/useFetchDepinStatusPage.js
deleted file mode 100644
index cb436d6d6..000000000
--- a/client/src/Hooks/useFetchDepinStatusPage.js
+++ /dev/null
@@ -1,58 +0,0 @@
-import { useState, useEffect } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-import { useSelector } from "react-redux";
-import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "./useMonitorUtils";
-
-const useFetchDepinStatusPage = ({ url, timeFrame, isCreate = false }) => {
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- const [statusPage, setStatusPage] = useState(undefined);
- const [monitorId, setMonitorId] = useState(undefined);
- const [isPublished, setIsPublished] = useState(false);
- const { authToken } = useSelector((state) => state.auth);
- const theme = useTheme();
- const { getMonitorWithPercentage } = useMonitorUtils();
-
- useEffect(() => {
- if (isCreate) return;
- const fetchStatusPageByUrl = async () => {
- try {
- const response = await networkService.getDistributedStatusPageByUrl({
- authToken,
- url,
- type: "distributed",
- timeFrame,
- });
- if (!response?.data?.data) return;
- const statusPage = response.data.data;
-
- const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) =>
- getMonitorWithPercentage(monitor, theme)
- );
-
- const statusPageWithSubmonitorPercentages = {
- ...statusPage,
- subMonitors: monitorsWithPercentage,
- };
- setStatusPage(statusPageWithSubmonitorPercentages);
-
- setMonitorId(statusPage?.monitors[0]);
- setIsPublished(statusPage?.isPublished);
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
- fetchStatusPageByUrl();
- }, [authToken, url, getMonitorWithPercentage, theme, timeFrame, isCreate]);
-
- return [isLoading, networkError, statusPage, monitorId, isPublished];
-};
-
-export { useFetchDepinStatusPage };
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/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/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js
new file mode 100644
index 000000000..cc6c3e875
--- /dev/null
+++ b/client/src/Hooks/useNotifications.js
@@ -0,0 +1,206 @@
+import { useState, useEffect, useCallback } from "react";
+import { createToast } from "../Utils/toastUtils";
+import { networkService } from "../main";
+import { useNavigate } from "react-router-dom";
+import { useTranslation } from "react-i18next";
+import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils";
+
+const useCreateNotification = () => {
+ const navigate = useNavigate();
+ const { t } = useTranslation();
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ const createNotification = async (notification) => {
+ try {
+ setIsLoading(true);
+ await networkService.createNotification({ notification });
+ createToast({
+ body: t("notifications.create.success"),
+ });
+ navigate("/notifications");
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.create.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [createNotification, isLoading, error];
+};
+
+const useGetNotificationsByTeamId = (updateTrigger) => {
+ const [notifications, setNotifications] = useState([]);
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const { t } = useTranslation();
+
+ const getNotifications = useCallback(async () => {
+ try {
+ setIsLoading(true);
+ const response = await networkService.getNotificationsByTeamId();
+ setNotifications(response?.data?.data ?? []);
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.fetch.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ }, [t]);
+
+ useEffect(() => {
+ getNotifications();
+ }, [getNotifications, updateTrigger]);
+
+ return [notifications, isLoading, error];
+};
+
+const useDeleteNotification = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(null);
+ const { t } = useTranslation();
+
+ const deleteNotification = async (id, triggerUpdate) => {
+ try {
+ setIsLoading(true);
+ await networkService.deleteNotificationById({ id });
+ createToast({
+ body: t("notifications.delete.success"),
+ });
+ triggerUpdate();
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.delete.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [deleteNotification, isLoading, error];
+};
+
+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 = {
+ address: notification?.address,
+ notificationName: notification?.notificationName,
+ type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id,
+ };
+
+ 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 navigate = useNavigate();
+ const editNotification = async (id, notification) => {
+ try {
+ setIsLoading(true);
+ await networkService.editNotification({ id, notification });
+ createToast({
+ body: t("notifications.edit.success"),
+ });
+ navigate(`/notifications`);
+ } catch (error) {
+ setError(error);
+ createToast({
+ body: t("notifications.edit.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ 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: error?.response?.data?.msg || t("notifications.test.failed"),
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [testNotification, isLoading, error];
+};
+
+const useTestAllNotifications = () => {
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState(undefined);
+ const { t } = useTranslation();
+ const testAllNotifications = async ({ monitorId }) => {
+ try {
+ setIsLoading(true);
+ await networkService.testAllNotifications({ monitorId });
+ createToast({
+ body: t("notifications.test.success"),
+ });
+ } catch (error) {
+ createToast({
+ body: error.response?.data?.msg || error.message,
+ });
+ setError(error);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return [testAllNotifications, isLoading, error];
+};
+
+export {
+ useCreateNotification,
+ useGetNotificationsByTeamId,
+ useDeleteNotification,
+ useGetNotificationById,
+ useEditNotification,
+ useTestNotification,
+ useTestAllNotifications,
+};
diff --git a/client/src/Hooks/useSubscribeToDepinDetails.js b/client/src/Hooks/useSubscribeToDepinDetails.js
deleted file mode 100644
index 0e8c2744f..000000000
--- a/client/src/Hooks/useSubscribeToDepinDetails.js
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useState, useEffect } from "react";
-import { networkService } from "../main";
-import { createToast } from "../Utils/toastUtils";
-
-const useSubscribeToDepinDetails = ({ monitorId, isPublic, isPublished, dateRange }) => {
- const [isLoading, setIsLoading] = useState(true);
- const [connectionStatus, setConnectionStatus] = useState(undefined);
- const [retryCount, setRetryCount] = useState(0);
- const [networkError, setNetworkError] = useState(false);
- const [monitor, setMonitor] = useState(undefined);
- const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0);
-
- useEffect(() => {
- if (typeof monitorId === "undefined") {
- return;
- }
- // If this page is public and not published, don't subscribe to details
- if (isPublic && isPublished === false) {
- return;
- }
-
- // Get initial data
-
- const fetchInitialData = async () => {
- try {
- const res = await networkService.getDistributedUptimeDetails({
- monitorId,
- dateRange: dateRange,
- normalize: true,
- isPublic,
- });
- const responseData = res?.data?.data;
-
- if (typeof responseData === "undefined") {
- throw new Error("No data");
- }
- setConnectionStatus("up");
- setLastUpdateTrigger(Date.now());
- setMonitor(responseData);
- } catch (error) {
- setNetworkError(true);
- setConnectionStatus("down");
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchInitialData();
-
- try {
- const cleanup = networkService.subscribeToDistributedUptimeDetails({
- monitorId,
- dateRange: dateRange,
- normalize: true,
- onUpdate: (data) => {
- if (isLoading === true) {
- setIsLoading(false);
- }
- if (networkError === true) {
- setNetworkError(false);
- }
- setLastUpdateTrigger(Date.now());
- setMonitor(data.monitor);
- },
- onOpen: () => {
- setConnectionStatus("up");
- setRetryCount(0); // Reset retry count on successful connection
- },
- onError: () => {
- setIsLoading(false);
- setNetworkError(true);
- setConnectionStatus("down");
- },
- });
- return cleanup;
- } catch (error) {
- setNetworkError(true);
- }
- }, [
- dateRange,
- monitorId,
- retryCount,
- setConnectionStatus,
- networkError,
- isLoading,
- isPublic,
- isPublished,
- ]);
-
- return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger];
-};
-
-export { useSubscribeToDepinDetails };
diff --git a/client/src/Hooks/useSubscribeToDepinMonitors.js b/client/src/Hooks/useSubscribeToDepinMonitors.js
deleted file mode 100644
index 918ead6bc..000000000
--- a/client/src/Hooks/useSubscribeToDepinMonitors.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../main";
-import { useSelector } from "react-redux";
-import { useTheme } from "@emotion/react";
-import { useMonitorUtils } from "./useMonitorUtils";
-import { createToast } from "../Utils/toastUtils";
-
-const useSubscribeToDepinMonitors = (page, rowsPerPage) => {
- // Redux
- const { user } = useSelector((state) => state.auth);
-
- // Local state
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- const [count, setCount] = useState(undefined);
- const [monitors, setMonitors] = useState(undefined);
-
- const theme = useTheme();
- const { getMonitorWithPercentage } = useMonitorUtils();
-
- useEffect(() => {
- const fetchInitialData = async () => {
- try {
- const initialDataRes = await networkService.getDistributedUptimeMonitors({
- teamId: user.teamId,
- limit: 25,
- types: ["distributed_http"],
- page,
- rowsPerPage,
- });
-
- const { count, monitors } = initialDataRes?.data?.data ?? {};
- const responseData = initialDataRes?.data?.data;
- if (typeof responseData === "undefined") throw new Error("No data");
-
- const mappedMonitors = monitors?.map((monitor) =>
- getMonitorWithPercentage(monitor, theme)
- );
-
- setMonitors(mappedMonitors);
- setCount(count?.monitorsCount ?? 0);
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchInitialData();
-
- try {
- const cleanup = networkService.subscribeToDistributedUptimeMonitors({
- teamId: user.teamId,
- limit: 25,
- types: [
- typeof import.meta.env.VITE_DEPIN_TESTING === "undefined"
- ? "distributed_http"
- : "distributed_test",
- ],
- page,
- rowsPerPage,
- filter: null,
- field: null,
- order: null,
- onUpdate: (data) => {
- if (isLoading === true) {
- setIsLoading(false);
- }
- const { count, monitors } = data;
- const mappedMonitors = monitors.map((monitor) =>
- getMonitorWithPercentage(monitor, theme)
- );
- setMonitors(mappedMonitors);
- setCount(count?.monitorsCount ?? 0);
- },
- onError: () => {
- setIsLoading(false);
- setNetworkError(true);
- },
- });
-
- return cleanup;
- } catch (error) {
- createToast({
- body: error.message,
- });
- setNetworkError(true);
- }
- }, [user, getMonitorWithPercentage, theme, isLoading, page, rowsPerPage]);
- return [monitors, count, isLoading, networkError];
-};
-export { useSubscribeToDepinMonitors };
diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/Account/index.jsx
index 9ba57eb06..d8672f51c 100644
--- a/client/src/Pages/Account/index.jsx
+++ b/client/src/Pages/Account/index.jsx
@@ -1,6 +1,6 @@
import { useState } from "react";
import PropTypes from "prop-types";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { Box, Tab, useTheme } from "@mui/material";
import CustomTabList from "../../Components/Tab";
diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/Auth/CheckEmail.jsx
index 695be62b0..e1f5e43ed 100644
--- a/client/src/Pages/Auth/CheckEmail.jsx
+++ b/client/src/Pages/Auth/CheckEmail.jsx
@@ -2,10 +2,10 @@ import { Box, Button, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTheme } from "@emotion/react";
import { useDispatch } from "react-redux";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { createToast } from "../../Utils/toastUtils";
import { forgotPassword } from "../../Features/Auth/authSlice";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import Background from "../../assets/Images/background-grid.svg?react";
import EmailIcon from "../../assets/icons/email.svg?react";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
@@ -31,16 +31,16 @@ const CheckEmail = () => {
const toastFail = [
{
- body: "Email not found.",
+ body: t("auth.forgotPassword.toasts.emailNotFound"),
},
{
- body: "Redirecting in 3...",
+ body: t("auth.forgotPassword.toasts.redirect").replace(" ", "3"),
},
{
- body: "Redirecting in 2...",
+ body: t("auth.forgotPassword.toasts.redirect").replace(" ", "2"),
},
{
- body: "Redirecting in 1...",
+ body: t("auth.forgotPassword.toasts.redirect").replace(" ", "1"),
},
];
@@ -62,19 +62,19 @@ const CheckEmail = () => {
const action = await dispatch(forgotPassword(form));
if (action.payload.success) {
createToast({
- body: `Instructions sent to ${form.email}.`,
+ body: t("auth.forgotPassword.toasts.sent").replace(" ", form.email),
});
setDisabled(false);
} else {
if (action.payload) {
// dispatch errors
createToast({
- body: action.payload.msg,
+ body: action.payload.msg, // FIXME: Potential untranslated string
});
} else {
// unknown errors
createToast({
- body: "Unknown error.",
+ body: t("common.toasts.unknownError"),
});
}
}
@@ -118,7 +118,7 @@ const CheckEmail = () => {
gap={theme.spacing(4)}
>
- Checkmate
+ {t("common.appName")}
{
svgHeight={24}
mb={theme.spacing(4)}
>
-
+
- {t("authCheckEmailTitle")}
+ {t("auth.forgotPassword.heading")}
- {t("authCheckEmailDescription")}{" "}
-
- {email || "username@email.com"}
-
+
+ {email || "username@email.com"}
+
+ ),
+ }}
+ />
{
maxWidth: 400,
}}
>
- {t("authCheckEmailOpenEmailButton")}
+ {t("auth.forgotPassword.buttons.openEmail")}
- {t("authCheckEmailDidntReceiveEmail")}{" "}
-
+ ),
}}
- >
- {t("authCheckEmailClickToResend")}
-
+ />
@@ -207,15 +217,21 @@ const CheckEmail = () => {
textAlign="center"
p={theme.spacing(12)}
>
- {t("goBackTo")}
-
- {t("authLoginTitle")}
+
+
+ ),
+ }}
+ />
diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx
index 6a31092c9..2a887756e 100644
--- a/client/src/Pages/Auth/ForgotPassword.jsx
+++ b/client/src/Pages/Auth/ForgotPassword.jsx
@@ -4,14 +4,14 @@ import { createToast } from "../../Utils/toastUtils";
import { useDispatch, useSelector } from "react-redux";
import { forgotPassword } from "../../Features/Auth/authSlice";
import { useEffect, useState } from "react";
-import { credentials } from "../../Validation/validation";
+import { newOrChangedCredentials } from "../../Validation/validation";
import { useNavigate } from "react-router-dom";
import TextInput from "../../Components/Inputs/TextInput";
import Logo from "../../assets/icons/checkmate-icon.svg?react";
import Key from "../../assets/icons/key.svg?react";
import Background from "../../assets/Images/background-grid.svg?react";
import IconBox from "../../Components/IconBox";
-import { useTranslation } from "react-i18next";
+import { Trans, useTranslation } from "react-i18next";
import "./index.css";
const ForgotPassword = () => {
@@ -35,14 +35,14 @@ const ForgotPassword = () => {
const handleSubmit = async (event) => {
event.preventDefault();
- const { error } = credentials.validate(form, { abortEarly: false });
+ const { error } = newOrChangedCredentials.validate(form, { abortEarly: false });
if (error) {
// validation errors
const err =
error.details && error.details.length > 0
- ? error.details[0].message
- : "Error validating data.";
+ ? error.details[0].message // FIXME: Possibly untranslated string
+ : t("auth.common.errors.validation");
setErrors({ email: err });
createToast({
body: err,
@@ -53,18 +53,18 @@ const ForgotPassword = () => {
sessionStorage.setItem("email", form.email);
navigate("/check-email");
createToast({
- body: `Instructions sent to ${form.email}.`,
+ body: t("auth.forgotPassword.toasts.sent").replace(" ", form.email),
});
} else {
if (action.payload) {
// dispatch errors
createToast({
- body: action.payload.msg,
+ body: action.payload.msg, // FIXME: Potentially untranslated string
});
} else {
// unknown errors
createToast({
- body: "Unknown error.",
+ body: t("common.toasts.unknownError"),
});
}
}
@@ -75,7 +75,10 @@ const ForgotPassword = () => {
const { value } = event.target;
setForm({ email: value });
- const { error } = credentials.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;
@@ -120,7 +123,7 @@ const ForgotPassword = () => {
gap={theme.spacing(4)}
>
- Checkmate
+ {t("common.appName")}
{
svgHeight={24}
mb={theme.spacing(4)}
>
-
+
- {t("authForgotPasswordTitle")}
- {t("authForgotPasswordInstructions")}
+ {t("auth.forgotPassword.heading")}
+ {t("auth.forgotPassword.subheadings.stepOne")}
{
{
mt: theme.spacing(15),
}}
>
- {t("continue")}
+ {t("auth.common.navigation.continue")}
@@ -207,15 +210,21 @@ const ForgotPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
- {t("goBackTo")}
-
- {t("authLoginTitle")}
+
+
+ ),
+ }}
+ />
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 45babead1..000000000
--- a/client/src/Pages/Auth/Login/Components/EmailStep.jsx
+++ /dev/null
@@ -1,107 +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("authLoginTitle")}
- {t("authLoginEnterEmail")}
-
-
- (e.target.value = e.target.value.toLowerCase())}
- onChange={onChange}
- error={errors.email ? true : false}
- helperText={errors.email ? t(errors.email) : ""}
- ref={inputRef}
- />
-
-
- {t("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 a9cb17d7c..000000000
--- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import { Box, Typography, useTheme } from "@mui/material";
-import PropTypes from "prop-types";
-import { useNavigate } from "react-router";
-import { 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 (
-
-
- {t("authForgotPasswordTitle")}
-
-
- {t("authForgotPasswordResetPassword")}
-
-
- );
-};
-
-ForgotPasswordLabel.propTypes = {
- email: PropTypes.string.isRequired,
- errorEmail: PropTypes.string.isRequired,
-};
-
-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 1fbdf93f0..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("authLoginTitle")}
- {t("authLoginEnterPassword")}
-
-
- }
- />
-
-
-
- {t("commonBack")}{" "}
-
-
- {t("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 5df2ed011..000000000
--- a/client/src/Pages/Auth/Login/Login.jsx
+++ /dev/null
@@ -1,262 +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 { credentials } 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 { 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 } = credentials.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 } = credentials.validate(
- { email: form.email },
- { abortEarly: false }
- );
- if (error) {
- const errorMessage = error.details[0].message;
- const translatedMessage = errorMessage.startsWith("auth")
- ? t(errorMessage)
- : errorMessage;
- setErrors((prev) => ({ ...prev, email: translatedMessage }));
- createToast({ body: translatedMessage });
- } else {
- setStep(1);
- }
- } else if (step === 1) {
- const { error } = credentials.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)
- : error.details[0].message
- : t("Error validating data."),
- });
- } else {
- const action = await dispatch(login(form));
- if (action.payload.success) {
- navigate("/uptime");
- createToast({
- body: t("welcomeBack"),
- });
- } else {
- if (action.payload) {
- if (action.payload.msg === "Incorrect password")
- setErrors({
- password: "The password you provided does not match our records",
- });
- // dispatch errors
- createToast({
- body: action.payload.msg,
- });
- } else {
- // unknown errors
- createToast({
- body: "Unknown error.",
- });
- }
- }
- }
- }
- };
-
- return (
-
-
-
-
-
-
-
- Checkmate
-
-
-
-
-
-
- .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 */}
-
-
- {t("doNotHaveAccount")}
-
- navigate("/register")}
- >
- {t("registerHere")}
-
-
-
-
- );
-};
-
-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..098fbbf08
--- /dev/null
+++ b/client/src/Pages/Auth/Login/index.jsx
@@ -0,0 +1,167 @@
+// 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";
+import Typography from "@mui/material/Typography";
+
+// 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) => {
+ let { name, value } = e.target;
+ if (name === "email") {
+ value = value.toLowerCase();
+ }
+ 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 (
+
+
+
+ {t("auth.login.heading")}
+
+
+
+ }
+ />
+
+ Login
+
+
+
+
+
+
+ );
+};
+
+export default Login;
diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
index 57a4aaf25..7ace77abc 100644
--- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx
+++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx
@@ -1,14 +1,13 @@
import { Box, Button, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { useDispatch } from "react-redux";
import { clearAuthState } from "../../Features/Auth/authSlice";
-import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
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 = () => {
@@ -19,7 +18,6 @@ const NewPasswordConfirmed = () => {
const handleNavigate = () => {
dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
navigate("/login");
};
@@ -54,7 +52,7 @@ const NewPasswordConfirmed = () => {
gap={theme.spacing(4)}
>
- Checkmate
+ {t("common.appName")}
{
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
deleted file mode 100644
index ffa1dd0ad..000000000
--- a/client/src/Pages/Auth/Register/Register.jsx
+++ /dev/null
@@ -1,399 +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 { credentials } 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 (
- <>
-
-
- {t("signUp")}
-
- {isSuperAdmin
- ? t("authRegisterCreateSuperAdminAccount")
- : t("authRegisterCreateAccount")}
-
-
-
-
-
- {t("authRegisterSignUpWithEmail")}
-
-
-
-
- {
- 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 } = credentials.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);
- createToast({ body: error.details[0].message || "Error validating data." });
- };
-
- 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: "Welcome! Your account was created successfully.",
- });
- } else {
- if (action.payload) {
- createToast({
- body: action.payload.msg,
- });
- } else {
- createToast({
- body: "Unknown error.",
- });
- }
- }
- };
-
- 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 } = credentials.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("commonAppName")}
-
- .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)}
- />
- ) : (
- ""
- )}
-
-
-
- {t("authRegisterAlreadyHaveAccount")}
-
- {
- navigate("/login");
- }}
- sx={{ userSelect: "none", color: theme.palette.accent.main }}
- >
- {t("authRegisterLoginLink")}
-
-
-
- );
-};
-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 cb66f6704..000000000
--- a/client/src/Pages/Auth/Register/StepOne/index.jsx
+++ /dev/null
@@ -1,136 +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 = {
- 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 {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({ 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 */}
-
-
- {t("signUp")}
- {t("authRegisterStepOnePersonalDetails")}
-
-
-
-
-
-
-
-
- {/* TODO buttons should be a component should be a component */}
-
-
- {t("commonBack")}
-
-
- {t("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 1af8a60dc..000000000
--- a/client/src/Pages/Auth/Register/StepThree/index.jsx
+++ /dev/null
@@ -1,173 +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 = {
- onSubmit: PropTypes.func,
- onBack: PropTypes.func,
-};
-
-/**
- * Renders the third step of the sign up process.
- *
- * @param {Object} props
- * @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 }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- const { handleChange, feedbacks, form, errors } = useValidatePassword();
- return (
- <>
-
-
- {t("signUp")}
- {t("createPassword")}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {t("commonBack")}
-
-
- {t("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 4fa0e8ebc..000000000
--- a/client/src/Pages/Auth/Register/StepTwo/index.jsx
+++ /dev/null
@@ -1,126 +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 = {
- 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 {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({ form, errors, onSubmit, onChange, onBack }) {
- const theme = useTheme();
- const inputRef = useRef(null);
- const { t } = useTranslation();
-
- useEffect(() => {
- if (inputRef.current) {
- inputRef.current.focus();
- }
- }, []);
-
- return (
- <>
-
-
- {t("signUp")}
- {t("enterEmail")}
-
-
-
- (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))
- }
- ref={inputRef}
- />
-
-
-
- {t("commonBack")}
-
-
- {t("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..97b27e5cf
--- /dev/null
+++ b/client/src/Pages/Auth/Register/index.jsx
@@ -0,0 +1,353 @@
+// 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, useSelector } 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 }) => {
+ // Redux
+ const { isLoading } = useSelector((state) => state.auth);
+
+ // 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 (
+
+
+
+ {t("auth.registration.heading.user")}
+
+
+
+ {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;
diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx
index 4467a4adf..b4899f47c 100644
--- a/client/src/Pages/Auth/SetNewPassword.jsx
+++ b/client/src/Pages/Auth/SetNewPassword.jsx
@@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react";
import { Box, Stack, Typography, Button } from "@mui/material";
import { setNewPassword } from "../../Features/Auth/authSlice";
import { createToast } from "../../Utils/toastUtils";
-import { credentials } from "../../Validation/validation";
+import { newOrChangedCredentials } from "../../Validation/validation";
import Check from "../../Components/Check/Check";
import TextInput from "../../Components/Inputs/TextInput";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
@@ -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();
@@ -34,7 +34,7 @@ const SetNewPassword = () => {
const handleSubmit = async (e) => {
e.preventDefault();
- const { error } = credentials.validate(form, {
+ const { error } = newOrChangedCredentials.validate(form, {
abortEarly: false,
context: { password: form.password },
});
@@ -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,
});
@@ -98,7 +98,7 @@ const SetNewPassword = () => {
gap={theme.spacing(4)}
>
- {t("commonAppName")}
+ {t("common.appName")}
{
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 +189,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 +204,37 @@ const SetNewPassword = () => {
mb={theme.spacing(12)}
>
@@ -243,7 +251,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
- {t("authSetNewPasswordResetPassword")}
+ {t("auth.forgotPassword.buttons.resetPassword")}
@@ -251,15 +259,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/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/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx
index b49d0944b..102d8d73e 100644
--- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx
+++ b/client/src/Pages/Auth/hooks/useValidatePassword.jsx
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
-import { credentials } from "../../../Validation/validation";
+import { newOrChangedCredentials } from "../../../Validation/validation";
const getFeedbackStatus = (form, errors, field, criteria) => {
const fieldErrors = errors[field];
@@ -36,7 +36,7 @@ function useValidatePassword() {
validateValue.password = form.password;
}
- const { error } = credentials.validate(validateValue, validateOptions);
+ const { error } = newOrChangedCredentials.validate(validateValue, validateOptions);
const errors = error?.details.map((error) => ({
path: error.path[0],
type: error.type,
diff --git a/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx b/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx
deleted file mode 100644
index b2634e9a7..000000000
--- a/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useState } from "react";
-import { networkService } from "../../../../main";
-import { useSelector } from "react-redux";
-import { createToast } from "../../../../Utils/toastUtils";
-
-const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => {
- const [isLoading, setIsLoading] = useState(false);
- const [networkError, setNetworkError] = useState(false);
- const createDistributedUptimeMonitor = async ({ form }) => {
- setIsLoading(true);
- try {
- if (isCreate) {
- await networkService.createMonitor({ monitor: form });
- } else {
- await networkService.updateMonitor({ monitor: form, monitorId });
- }
-
- return true;
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error?.response?.data?.msg ?? error.message });
- return false;
- } finally {
- setIsLoading(false);
- }
- };
-
- return [createDistributedUptimeMonitor, isLoading, networkError];
-};
-
-export { useCreateDistributedUptimeMonitor };
diff --git a/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx b/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx
deleted file mode 100644
index efffbe437..000000000
--- a/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useEffect, useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-
-export const useMonitorFetch = ({ monitorId, isCreate }) => {
- const [networkError, setNetworkError] = useState(false);
- const [isLoading, setIsLoading] = useState(true);
- const [monitor, setMonitor] = useState(undefined);
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- if (isCreate) return;
- const res = await networkService.getUptimeDetailsById({
- monitorId: monitorId,
- normalize: true,
- });
- setMonitor(res?.data?.data ?? {});
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
- fetchMonitors();
- }, [monitorId, isCreate]);
- return [monitor, isLoading, networkError];
-};
-
-export default useMonitorFetch;
diff --git a/client/src/Pages/DistributedUptime/Create/index.jsx b/client/src/Pages/DistributedUptime/Create/index.jsx
deleted file mode 100644
index aff2a7d22..000000000
--- a/client/src/Pages/DistributedUptime/Create/index.jsx
+++ /dev/null
@@ -1,349 +0,0 @@
-// Components
-import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
-
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import ConfigBox from "../../../Components/ConfigBox";
-import TextInput from "../../../Components/Inputs/TextInput";
-import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import Radio from "../../../Components/Inputs/Radio";
-import Checkbox from "../../../Components/Inputs/Checkbox";
-import Select from "../../../Components/Inputs/Select";
-import { createToast } from "../../../Utils/toastUtils";
-
-// Utility
-import { useTheme } from "@emotion/react";
-import { useState, useEffect } from "react";
-import { useNavigate } from "react-router-dom";
-import { useSelector } from "react-redux";
-import { monitorValidation } from "../../../Validation/validation";
-import { useParams } from "react-router-dom";
-import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor";
-import { useMonitorFetch } from "./Hooks/useMonitorFetch";
-import { useTranslation } from "react-i18next";
-
-// Constants
-const MS_PER_MINUTE = 60000;
-const SELECT_VALUES = [
- { _id: 1, name: "1 minute" },
- { _id: 2, name: "2 minutes" },
- { _id: 3, name: "3 minutes" },
- { _id: 4, name: "4 minutes" },
- { _id: 5, name: "5 minutes" },
-];
-
-const parseUrl = (url) => {
- try {
- return new URL(url);
- } catch (error) {
- return null;
- }
-};
-
-const CreateDistributedUptime = () => {
- const { monitorId } = useParams();
- const isCreate = typeof monitorId === "undefined";
- const { t } = useTranslation();
-
- let BREADCRUMBS = [
- { name: `distributed uptime`, path: "/distributed-uptime" },
- ...(isCreate ? [] : [{ name: "details", path: `/distributed-uptime/${monitorId}` }]),
- { name: isCreate ? "create" : "configure", path: `` },
- ];
-
- // Redux state
- const { user } = useSelector((state) => state.auth);
-
- // Local state
- const [https, setHttps] = useState(true);
- const [notifications, setNotifications] = useState([]);
- const [form, setForm] = useState({
- type: "distributed_http",
- name: "",
- url: "",
- interval: 1,
- });
- const [errors, setErrors] = useState({});
-
- //utils
- const theme = useTheme();
- const navigate = useNavigate();
- const [createDistributedUptimeMonitor, isLoading, networkError] =
- useCreateDistributedUptimeMonitor({ isCreate, monitorId });
-
- const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({
- monitorId,
- isCreate,
- });
-
- // Effect to set monitor to fetched monitor
- useEffect(() => {
- if (typeof monitor !== "undefined") {
- const parsedUrl = parseUrl(monitor?.url);
- const protocol = parsedUrl?.protocol?.replace(":", "") || "";
- setHttps(protocol === "https");
-
- const newForm = {
- name: monitor.name,
- interval: monitor.interval / MS_PER_MINUTE,
- url: parsedUrl.host,
- type: monitor.type,
- };
-
- setForm(newForm);
- }
- }, [monitor]);
-
- // Handlers
- const handleCreateMonitor = async () => {
- const monitorToSubmit = { ...form };
-
- // Prepend protocol to url
- monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url;
-
- const { error } = monitorValidation.validate(monitorToSubmit, {
- abortEarly: false,
- });
- if (error) {
- const newErrors = {};
- error.details.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors(newErrors);
- createToast({ body: "Please check the form for errors." });
- return;
- }
-
- // Append needed fields
- monitorToSubmit.description = form.name;
- monitorToSubmit.interval = form.interval * MS_PER_MINUTE;
- monitorToSubmit.teamId = user.teamId;
- monitorToSubmit.userId = user._id;
- monitorToSubmit.notifications = notifications;
-
- const success = await createDistributedUptimeMonitor({ form: monitorToSubmit });
- if (success) {
- createToast({ body: "Monitor created successfully!" });
- navigate("/distributed-uptime");
- } else {
- createToast({ body: "Failed to create monitor." });
- }
- };
-
- const handleChange = (event) => {
- let { name, value } = event.target;
-
- setForm({
- ...form,
- [name]: value,
- });
- const { error } = monitorValidation.validate(
- { [name]: value },
- { abortEarly: false }
- );
- setErrors((prev) => ({
- ...prev,
- ...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
- }));
- };
-
- const handleNotifications = (event, type) => {
- const { value } = event.target;
- let currentNotifications = [...notifications];
- const notificationAlreadyExists = notifications.some((notification) => {
- if (notification.type === type && notification.address === value) {
- return true;
- }
- return false;
- });
- if (notificationAlreadyExists) {
- currentNotifications = currentNotifications.filter((notification) => {
- if (notification.type === type && notification.address === value) {
- return false;
- }
- return true;
- });
- } else {
- currentNotifications.push({ type, address: value });
- }
- setNotifications(currentNotifications);
- };
-
- return (
-
-
- console.log("submit")}
- >
-
-
- {isCreate ? "Create your" : "Edit your"}{" "}
-
-
- {t("monitor")}
-
-
-
-
- {t("settingsGeneralSettings")}
- {t("distributedUptimeCreateSelectURL")}
-
-
- }
- label="URL to monitor"
- https={https}
- placeholder={"www.google.com"}
- disabled={!isCreate}
- value={form.url}
- name="url"
- onChange={handleChange}
- error={errors["url"] ? true : false}
- helperText={errors["url"]}
- />
-
-
-
-
-
- {t("distributedUptimeCreateChecks")}
-
- {t("distributedUptimeCreateChecksDescription")}
-
-
-
-
-
- {form.type === "http" || form.type === "distributed_http" ? (
-
- setHttps(true)}
- >
- {t("https")}
-
- setHttps(false)}
- >
- {t("http")}
-
-
- ) : (
- ""
- )}
-
-
- {errors["type"] ? (
-
-
- {errors["type"]}
-
-
- ) : (
- ""
- )}
-
-
-
-
-
- {t("distributedUptimeCreateIncidentNotification")}
-
-
- {t("distributedUptimeCreateIncidentDescription")}
-
-
-
- notification.type === "email"
- )}
- value={user?.email}
- onChange={(event) => handleNotifications(event, "email")}
- />
-
-
-
-
-
- {t("distributedUptimeCreateAdvancedSettings")}
-
-
-
-
-
-
-
- handleCreateMonitor()}
- disabled={!Object.values(errors).every((value) => value === undefined)}
- loading={isLoading}
- >
- {isCreate ? "Create monitor" : "Configure monitor"}
-
-
-
-
- );
-};
-
-export default CreateDistributedUptime;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx
deleted file mode 100644
index 05f631d95..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/Chatbot/index.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-// Components
-import { Stack, Typography } from "@mui/material";
-import { ColContainer } from "../../../../../Components/StandardContainer";
-import SmartToyIcon from "@mui/icons-material/SmartToy";
-import Dot from "../../../../../Components/Dot";
-// Utils
-import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
-
-const MESSAGES = [
- "I've checked the network status, and we're seeing excellent performance across all regions.",
- "The network is stable and functioning optimally. All connections are active and stable.",
- "I've reviewed the network status, and everything looks great. No issues detected.",
- "The network is up and running smoothly. All connections are active and stable.",
- "I've checked the network status, and everything is looking good. No issues detected.",
-];
-
-const ChatBot = ({ sx }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
-
-
- Status Bot
-
-
- {t("now")}
-
-
- {MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}
-
- );
-};
-
-export default ChatBot;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/ControlsHeader/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/ControlsHeader/index.jsx
deleted file mode 100644
index 206a67d1b..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/ControlsHeader/index.jsx
+++ /dev/null
@@ -1,92 +0,0 @@
-// Components
-import { Box, Stack, Button } from "@mui/material";
-import Image from "../../../../../Components/Image";
-import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react";
-
-//Utils
-import { useTheme } from "@mui/material/styles";
-import { useNavigate } from "react-router-dom";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, monitorId }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const { t } = useTranslation();
-
- return (
-
-
- setIsDeleteOpen(!isDeleteOpen)}
- loading={isDeleting}
- >
- {t("delete")}
-
-
-
- {
- navigate(`/distributed-uptime/configure/${monitorId}`);
- }}
- sx={{
- px: theme.spacing(5),
- "& svg": {
- mr: theme.spacing(3),
- "& path": {
- stroke: theme.palette.secondary.contrastText,
- },
- },
- }}
- >
- {t("configure")}
-
-
-
- );
-};
-
-Controls.propTypes = {
- isDeleting: PropTypes.bool,
- monitorId: PropTypes.string,
- isDeleteOpen: PropTypes.bool.isRequired,
- setIsDeleteOpen: PropTypes.func.isRequired,
-};
-
-const ControlsHeader = ({ isDeleting, isDeleteOpen, setIsDeleteOpen, monitorId }) => {
- const theme = useTheme();
-
- return (
-
-
-
- );
-};
-
-ControlsHeader.propTypes = {
- monitorId: PropTypes.string,
- isDeleting: PropTypes.bool,
- isDeleteOpen: PropTypes.bool.isRequired,
- setIsDeleteOpen: PropTypes.func.isRequired,
-};
-
-export default ControlsHeader;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
deleted file mode 100644
index 92780717b..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DeviceTicker/index.jsx
+++ /dev/null
@@ -1,98 +0,0 @@
-// Components
-import { Stack, Typography } from "@mui/material";
-import PulseDot from "../../../../../Components/Animated/PulseDot";
-import "flag-icons/css/flag-icons.min.css";
-import { ColContainer } from "../../../../../Components/StandardContainer";
-
-// Utils
-import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
-
-const DeviceTicker = ({ data, width = "100%", connectionStatus }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- const statusColor = {
- up: theme.palette.success.main,
- down: theme.palette.error.main,
- undefined: theme.palette.warning.main,
- };
-
- return (
-
-
-
-
-
- {connectionStatus === "up" ? "Connected" : "Connecting..."}
-
-
-
-
-
-
-
- {t("country")}
-
-
- {t("city")}
-
-
- {t("response")}
-
-
- {"UPT BURNED"}
-
-
-
-
- {data.map((dataPoint) => {
- const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
- const flag = countryCode ? `fi fi-${countryCode}` : null;
- const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
- return (
-
-
-
- {flag ? : null}{" "}
- {countryCode?.toUpperCase() ?? "N/A"}
-
-
-
- {city}
-
-
-
- {Math.floor(dataPoint.responseTime)} {t("ms")}
-
-
-
-
- +{dataPoint.uptBurnt}
-
-
-
- );
- })}
-
-
-
-
- );
-};
-
-export default DeviceTicker;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json
deleted file mode 100644
index 86ca8204d..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/DistributedUptimeMapStyle.json
+++ /dev/null
@@ -1,169 +0,0 @@
-{
- "id": "43f36e14-e3f5-43c1-84c0-50a9c80dc5c7",
- "name": "MapLibre",
- "zoom": 0.861983335785597,
- "pitch": 0,
- "center": [17.6543171043124, 32.9541203267468],
- "glyphs": "https://demotiles.maplibre.org/font/{fontstack}/{range}.pbf",
- "layers": [
- {
- "id": "background",
- "type": "background",
- "paint": {
- "background-color": "#121217"
- },
- "filter": ["all"],
- "layout": {
- "visibility": "visible"
- },
- "maxzoom": 24
- },
- {
- "id": "coastline",
- "type": "line",
- "paint": {
- "line-blur": 0.5,
- "line-color": "#000000",
- "line-width": {
- "stops": [
- [0, 2],
- [6, 6],
- [14, 9],
- [22, 18]
- ]
- }
- },
- "filter": ["all"],
- "layout": {
- "line-cap": "round",
- "line-join": "round",
- "visibility": "visible"
- },
- "source": "maplibre",
- "maxzoom": 24,
- "minzoom": 0,
- "source-layer": "countries"
- },
- {
- "id": "countries-fill",
- "type": "fill",
- "paint": {
- "fill-color": "#292929"
- },
- "filter": ["all"],
- "layout": {
- "visibility": "visible"
- },
- "source": "maplibre",
- "maxzoom": 24,
- "source-layer": "countries"
- },
- {
- "id": "countries-boundary",
- "type": "line",
- "paint": {
- "line-color": "#484848",
- "line-width": {
- "stops": [
- [1, 1],
- [6, 2],
- [14, 6],
- [22, 12]
- ]
- },
- "line-opacity": {
- "stops": [
- [3, 0.5],
- [6, 1]
- ]
- }
- },
- "layout": {
- "line-cap": "round",
- "line-join": "round",
- "visibility": "visible"
- },
- "source": "maplibre",
- "maxzoom": 24,
- "source-layer": "countries"
- },
- {
- "id": "countries-label",
- "type": "symbol",
- "paint": {
- "text-color": "rgba(8, 37, 77, 1)",
- "text-halo-blur": {
- "stops": [
- [2, 0.2],
- [6, 0]
- ]
- },
- "text-halo-color": "rgba(255, 255, 255, 1)",
- "text-halo-width": {
- "stops": [
- [2, 1],
- [6, 1.6]
- ]
- }
- },
- "filter": ["all"],
- "layout": {
- "text-font": ["Open Sans Semibold"],
- "text-size": {
- "stops": [
- [2, 10],
- [4, 12],
- [6, 16]
- ]
- },
- "text-field": {
- "stops": [
- [2, "{ABBREV}"],
- [4, "{NAME}"]
- ]
- },
- "visibility": "visible",
- "text-max-width": 10,
- "text-transform": {
- "stops": [
- [0, "uppercase"],
- [2, "none"]
- ]
- }
- },
- "source": "maplibre",
- "maxzoom": 24,
- "minzoom": 2,
- "source-layer": "centroids"
- },
- {
- "id": "data-dots",
- "type": "circle",
- "source": "data-dots",
- "paint": {
- "circle-radius": 3,
- "circle-color": ["get", "color"],
- "circle-opacity": 0.5
- }
- }
- ],
- "bearing": 0,
- "sources": {
- "maplibre": {
- "url": "https://demotiles.maplibre.org/tiles/tiles.json",
- "type": "vector"
- },
- "data-dots": {
- "type": "geojson",
- "data": {
- "type": "FeatureCollection",
- "features": []
- }
- }
- },
- "version": 8,
- "metadata": {
- "maptiler:copyright": "This style was generated on MapTiler Cloud. Usage is governed by the license terms in https://github.com/maplibre/demotiles/blob/gh-pages/LICENSE",
- "openmaptiles:version": "3.x"
- }
-}
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/buildStyle.js b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/buildStyle.js
deleted file mode 100644
index e4aaa49ce..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/buildStyle.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import baseStyle from "./DistributedUptimeMapStyle.json";
-
-const buildStyle = (theme, mode) => {
- const style = JSON.parse(JSON.stringify(baseStyle));
-
- if (mode === "dark") {
- return baseStyle;
- }
-
- if (style.layers) {
- const newLayers = style.layers.map((layer) => {
- if (layer.id === "background") {
- layer.paint["background-color"] = theme.palette.map.main;
- }
-
- if (layer.id === "countries-fill") {
- layer.paint["fill-color"] = theme.palette.map.lowContrast;
- }
-
- if (layer.id === "coastline") {
- layer.paint["line-color"] = theme.palette.map.highContrast;
- }
- if (layer.id === "countries-boundary") {
- layer.paint["line-color"] = theme.palette.map.highContrast;
- }
- return layer;
- });
- style.layers = newLayers;
- }
- return style;
-};
-
-export default buildStyle;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
deleted file mode 100644
index 03993dcc3..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeMap/index.jsx
+++ /dev/null
@@ -1,118 +0,0 @@
-import "maplibre-gl/dist/maplibre-gl.css";
-import PropTypes from "prop-types";
-import { useRef, useState, useEffect } from "react";
-import { useTheme } from "@mui/material/styles";
-import maplibregl from "maplibre-gl";
-import { useSelector } from "react-redux";
-import buildStyle from "./buildStyle";
-
-const DistributedUptimeMap = ({
- width = "100%",
- checks,
- height,
- minHeight = "350px",
-}) => {
- const mapContainer = useRef(null);
- const map = useRef(null);
- const theme = useTheme();
- const [mapLoaded, setMapLoaded] = useState(false);
- const mode = useSelector((state) => state.ui.mode);
- const initialTheme = useRef(theme);
- const initialMode = useRef(mode);
-
- const colorLookup = (avgResponseTime) => {
- if (avgResponseTime <= 150) {
- return "#00FF00"; // Green
- } else if (avgResponseTime <= 250) {
- return "#FFFF00"; // Yellow
- } else {
- return "#FF0000"; // Red
- }
- };
-
- useEffect(() => {
- if (mapContainer.current && !map.current) {
- const initialStyle = buildStyle(initialTheme.current, initialMode.current);
-
- map.current = new maplibregl.Map({
- container: mapContainer.current,
- style: initialStyle,
- center: [0, 20],
- zoom: 0.8,
- attributionControl: false,
- canvasContextAttributes: {
- antialias: true,
- preserveDrawingBuffer: true,
- },
- });
- }
- map.current.on("load", () => {
- setMapLoaded(true);
- });
-
- return () => {
- if (map.current) {
- map.current.remove();
- map.current = null;
- }
- };
- }, []);
-
- useEffect(() => {
- const style = buildStyle(theme, mode);
- if (map.current && mapLoaded) {
- map.current.setStyle(style);
- }
- }, [theme, mode, mapLoaded]);
-
- useEffect(() => {
- if (map.current && checks?.length > 0) {
- // Convert dots to GeoJSON
- const geojson = {
- type: "FeatureCollection",
- features: checks.map((check) => {
- return {
- type: "Feature",
- geometry: {
- type: "Point",
- coordinates: [check._id.lng, check._id.lat],
- },
- properties: {
- color: theme.palette.accent.main,
- // color: colorLookup(check.avgResponseTime) || "blue", // Default to blue if no color specified
- },
- };
- }),
- };
-
- // Update the source with new dots
- const source = map.current.getSource("data-dots");
- if (source) {
- source.setData(geojson);
- }
- }
- }, [checks, theme, mapLoaded]);
- return (
-
- );
-};
-
-DistributedUptimeMap.propTypes = {
- checks: PropTypes.array,
- width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
- minHeight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-};
-
-export default DistributedUptimeMap;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Area/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Area/index.jsx
deleted file mode 100644
index 1102f72b8..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Area/index.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import { useState } from "react";
-import { useTheme } from "@emotion/react";
-import {
- AreaChart,
- Area,
- XAxis,
- Tooltip,
- CartesianGrid,
- ResponsiveContainer,
-} from "recharts";
-import CustomTick from "../Helpers/Tick";
-import CustomToolTip from "../Helpers/ToolTip";
-import PropTypes from "prop-types";
-
-const DistributedUptimeResponseAreaChart = ({ checks }) => {
- const theme = useTheme();
- const [isHovered, setIsHovered] = useState(false);
- return (
-
- setIsHovered(true)}
- onMouseLeave={() => setIsHovered(false)}
- >
-
-
-
-
-
-
-
- }
- minTickGap={0}
- axisLine={false}
- tickLine={false}
- height={20}
- />
- }
- wrapperStyle={{ pointerEvents: "none" }}
- />
-
-
-
- );
-};
-
-DistributedUptimeResponseAreaChart.propTypes = {
- checks: PropTypes.array,
-};
-
-export default DistributedUptimeResponseAreaChart;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx
deleted file mode 100644
index a6d3d455b..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Bar/index.jsx
+++ /dev/null
@@ -1,95 +0,0 @@
-import {
- BarChart,
- Bar,
- CartesianGrid,
- ResponsiveContainer,
- Tooltip,
- XAxis,
-} from "recharts";
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-import CustomToolTip from "../Helpers/ToolTip";
-import CustomTick from "../Helpers/Tick";
-
-const DistributedUptimeResponseBarChart = ({ checks }) => {
- const theme = useTheme();
- return (
-
-
-
-
-
-
-
-
-
- }
- wrapperStyle={{ pointerEvents: "none" }}
- />
- }
- minTickGap={0}
- axisLine={false}
- tickLine={false}
- height={20}
- />
-
-
-
- );
-};
-
-DistributedUptimeResponseBarChart.propTypes = {
- checks: PropTypes.array,
-};
-
-export default DistributedUptimeResponseBarChart;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/Tick.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/Tick.jsx
deleted file mode 100644
index 15efd5dfa..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/Tick.jsx
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useTheme } from "@emotion/react";
-import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../../../Utils/timeUtils";
-import PropTypes from "prop-types";
-import { Text } from "recharts";
-const CustomTick = ({ x, y, payload, index }) => {
- const theme = useTheme();
-
- const uiTimezone = useSelector((state) => state.ui.timezone);
- return (
-
- {formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
-
- );
-};
-
-CustomTick.propTypes = {
- x: PropTypes.number,
- y: PropTypes.number,
- payload: PropTypes.object,
- index: PropTypes.number,
-};
-
-export default CustomTick;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx
deleted file mode 100644
index 737b26106..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/Helpers/ToolTip.jsx
+++ /dev/null
@@ -1,89 +0,0 @@
-import { Box, Stack, Typography } from "@mui/material";
-import { useSelector } from "react-redux";
-import { formatDateWithTz } from "../../../../../../Utils/timeUtils";
-import PropTypes from "prop-types";
-import { useTheme } from "@emotion/react";
-import AccessTimeIcon from "@mui/icons-material/AccessTime";
-import { useTranslation } from "react-i18next";
-
-const CustomToolTip = ({ active, payload, label }) => {
- const uiTimezone = useSelector((state) => state.ui.timezone);
- const theme = useTheme();
- const { t } = useTranslation();
- if (active && payload && payload.length) {
- const responseTime = payload[0]?.payload?.originalAvgResponseTime
- ? payload[0]?.payload?.originalAvgResponseTime
- : (payload[0]?.payload?.avgResponseTime ?? 0);
- return (
-
-
- {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)}
-
-
-
-
-
-
- {t("responseTime")}
-
-
- {Math.floor(responseTime)}
-
- {t("ms")}
-
-
-
-
- );
- }
- return null;
-};
-
-CustomToolTip.propTypes = {
- active: PropTypes.bool,
- payload: PropTypes.arrayOf(
- PropTypes.shape({
- value: PropTypes.number,
- payload: PropTypes.shape({
- _id: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
- avgResponseTime: PropTypes.number,
- originalAvgResponseTime: PropTypes.number,
- }),
- })
- ),
- label: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
-};
-export default CustomToolTip;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
deleted file mode 100644
index 90a7671db..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/DistributedUptimeResponseChart/index.jsx
+++ /dev/null
@@ -1,53 +0,0 @@
-//Components
-import { Stack, Switch, Typography } from "@mui/material";
-import { useState } from "react";
-import DistributedUptimeResponseAreaChart from "./Area";
-import DistributedUptimeResponseBarChart from "./Bar";
-import ChartBox from "../../../../../Components/Charts/ChartBox";
-import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react";
-
-// Utils
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-const DistributedUptimeResponseChart = ({ checks }) => {
- const [chartType, setChartType] = useState("bar");
- const { t } = useTranslation();
- let Chart = null;
- if (chartType === "area") {
- Chart = DistributedUptimeResponseAreaChart;
- }
- if (chartType === "bar") {
- Chart = DistributedUptimeResponseBarChart;
- }
- return (
-
-
- {t("bar")}
- setChartType(e.target.checked ? "area" : "bar")}
- />
- {t("area")}
-
- }
- header="Response Times"
- sx={{ padding: 0 }}
- >
-
-
-
- );
-};
-
-DistributedUptimeResponseChart.propTypes = {
- checks: PropTypes.array,
- type: PropTypes.string,
-};
-
-export default DistributedUptimeResponseChart;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx
deleted file mode 100644
index 62a1f465a..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/Footer/index.jsx
+++ /dev/null
@@ -1,34 +0,0 @@
-import { Stack, Typography, Box } from "@mui/material";
-import SolanaLogo from "../../../../../assets/icons/solana_logo.svg?react";
-import { useTheme } from "@mui/material/styles";
-import { useTranslation } from "react-i18next";
-
-const Footer = () => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
- {t("distributedUptimeDetailsFooterHeading")}
-
- {t("distributedUptimeDetailsFooterBuilt")}
-
- {t("distributedUptimeDetailsFooterSolana")}
-
-
- );
-};
-
-export default Footer;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx
deleted file mode 100644
index a095b5feb..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/LastUpdate/index.jsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import { useState, useEffect } from "react";
-const LastUpdate = ({ suffix, lastUpdateTime, trigger }) => {
- const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
-
- useEffect(() => {
- setElapsedMs(lastUpdateTime);
- const timer = setInterval(() => {
- setElapsedMs((prev) => prev + 1000);
- }, 1000);
- return () => clearInterval(timer);
- }, [lastUpdateTime, trigger]);
-
- return `${Math.floor(elapsedMs / 1000)} ${suffix}`;
-};
-export default LastUpdate;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/MonitorHeader/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/MonitorHeader/index.jsx
deleted file mode 100644
index 056bd721a..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/MonitorHeader/index.jsx
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Stack, Typography } from "@mui/material";
-import { useTheme } from "@mui/material/styles";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-const MonitorHeader = ({ monitor }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
-
- {monitor.name}
- {t("distributedUptimeDetailsMonitorHeader")}
-
-
- );
-};
-
-MonitorHeader.propTypes = {
- monitor: PropTypes.object,
-};
-
-export default MonitorHeader;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx
deleted file mode 100644
index 03949a9f7..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/NextExpectedCheck/index.jsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import { LinearProgress } from "@mui/material";
-import { useState, useEffect } from "react";
-
-const NextExpectedCheck = ({ lastUpdateTime, interval, trigger }) => {
- const [elapsedMs, setElapsedMs] = useState(lastUpdateTime);
-
- useEffect(() => {
- setElapsedMs(lastUpdateTime);
- const timer = setInterval(() => {
- setElapsedMs((prev) => {
- const newElapsedMs = prev + 100;
- return newElapsedMs;
- });
- }, 100);
- return () => clearInterval(timer);
- }, [interval, trigger]);
-
- return (
-
- );
-};
-
-export default NextExpectedCheck;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx
deleted file mode 100644
index 233fc3c06..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/Skeleton/index.jsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-
-export const SkeletonLayout = () => {
- return (
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx
deleted file mode 100644
index 97513bb4f..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/StatBoxes/index.jsx
+++ /dev/null
@@ -1,60 +0,0 @@
-// Components
-import { Stack } from "@mui/material";
-import InfoBox from "../../../../../Components/InfoBox";
-import LastUpdate from "../LastUpdate";
-
-// Utils
-import { useTheme } from "@mui/material/styles";
-import PropTypes from "prop-types";
-
-const StatBoxes = ({ monitor, lastUpdateTrigger }) => {
- const theme = useTheme();
-
- return (
-
-
-
-
- }
- />
-
- }
- />
-
- );
-};
-
-StatBoxes.propTypes = {
- monitor: PropTypes.object,
- lastUpdateTrigger: PropTypes.number,
-};
-
-export default StatBoxes;
diff --git a/client/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx b/client/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx
deleted file mode 100644
index 22a56bd7e..000000000
--- a/client/src/Pages/DistributedUptime/Details/Components/StatusHeader/index.jsx
+++ /dev/null
@@ -1,94 +0,0 @@
-// Components
-import { ColContainer } from "../../../../../Components/StandardContainer";
-import { Stack, Typography } from "@mui/material";
-import PulseDot from "../../../../../Components/Animated/PulseDot";
-import LastUpdate from "../LastUpdate";
-import ChatBot from "../Chatbot";
-import ShareComponent from "../../../../../Components/ShareComponent";
-
-// Utils
-import { useTheme } from "@emotion/react";
-import PropTypes from "prop-types";
-import { useTranslation } from "react-i18next";
-
-const StatusHeader = ({ monitor, connectionStatus, elementToCapture }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- const COLOR_MAP = {
- up: theme.palette.successSecondary.main,
- down: theme.palette.error.lowContrast,
- };
-
- const MSG_MAP = {
- up: "All Systems Operational",
- down: "Last Check Failed",
- };
-
- const PULSE_COLOR = {
- up: theme.palette.success.main,
- down: theme.palette.error.main,
- };
-
- let bgColor = COLOR_MAP[connectionStatus];
- return (
-
-
-
-
-
-
-
- {MSG_MAP[connectionStatus]}
-
-
- {t("distributedUptimeDetailsStatusHeaderUptime")}{" "}
- {(monitor.uptimePercentage * 100).toFixed(2)}%
-
-
-
- {t("distributedUptimeDetailsStatusHeaderLastUpdate")}{" "}
-
-
-
-
-
-
-
-
- );
-};
-
-StatusHeader.propTypes = {
- monitor: PropTypes.object,
- connectionStatus: PropTypes.string,
-};
-
-export default StatusHeader;
diff --git a/client/src/Pages/DistributedUptime/Details/Hooks/useDeleteMonitor.jsx b/client/src/Pages/DistributedUptime/Details/Hooks/useDeleteMonitor.jsx
deleted file mode 100644
index a26de2a5e..000000000
--- a/client/src/Pages/DistributedUptime/Details/Hooks/useDeleteMonitor.jsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { useState } from "react";
-import { networkService } from "../../../../main";
-import { createToast } from "../../../../Utils/toastUtils";
-
-const useDeleteMonitor = ({ monitorId }) => {
- const [isLoading, setIsLoading] = useState(false);
- const deleteMonitor = async () => {
- try {
- setIsLoading(true);
- await networkService.deleteMonitorById({ monitorId });
- return true;
- } catch (error) {
- createToast({
- body: error.message,
- });
- return false;
- } finally {
- setIsLoading(false);
- }
- };
-
- return [deleteMonitor, isLoading];
-};
-
-export { useDeleteMonitor };
diff --git a/client/src/Pages/DistributedUptime/Details/index.jsx b/client/src/Pages/DistributedUptime/Details/index.jsx
deleted file mode 100644
index 27b39b600..000000000
--- a/client/src/Pages/DistributedUptime/Details/index.jsx
+++ /dev/null
@@ -1,148 +0,0 @@
-//Components
-import DistributedUptimeMap from "./Components/DistributedUptimeMap";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { Stack, Typography } from "@mui/material";
-import DeviceTicker from "./Components/DeviceTicker";
-import ResponseTimeChart from "./Components/DistributedUptimeResponseChart";
-import NextExpectedCheck from "./Components/NextExpectedCheck";
-import Footer from "./Components/Footer";
-import StatBoxes from "./Components/StatBoxes";
-import MonitorHeader from "./Components/MonitorHeader";
-import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import GenericFallback from "../../../Components/GenericFallback";
-import MonitorCreateHeader from "../../../Components/MonitorCreateHeader";
-import SkeletonLayout from "./Components/Skeleton";
-import ControlsHeader from "./Components/ControlsHeader";
-import Dialog from "../../../Components/Dialog";
-//Utils
-import { useTheme } from "@mui/material/styles";
-import { useState } from "react";
-import { useParams } from "react-router-dom";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useSubscribeToDepinDetails } from "../../../Hooks/useSubscribeToDepinDetails";
-import { useDeleteMonitor } from "./Hooks/useDeleteMonitor";
-import { useNavigate } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-
-const DistributedUptimeDetails = () => {
- const { monitorId } = useParams();
- // Local State
- const [dateRange, setDateRange] = useState("recent");
- const [isDeleteOpen, setIsDeleteOpen] = useState(false);
-
- // Utils
- const theme = useTheme();
- const isAdmin = useIsAdmin();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
- useSubscribeToDepinDetails({ monitorId, dateRange });
-
- const [deleteMonitor, isDeleting] = useDeleteMonitor({ monitorId });
- // Constants
- const BREADCRUMBS = [
- { name: "Distributed Uptime", path: "/distributed-uptime" },
- { name: "Details", path: `/distributed-uptime/${monitorId}` },
- ];
-
- if (isLoading) {
- return ;
- }
-
- if (networkError) {
- return (
-
-
- {t("networkError")}
-
- {t("checkConnection")}
-
- );
- }
- if (
- typeof monitor === "undefined" ||
- typeof monitor?.totalChecks === "undefined" ||
- monitor?.totalChecks === 0
- ) {
- return (
-
-
-
- {t("distributedUptimeDetailsNoMonitorHistory")}
-
-
- );
- }
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- {
- deleteMonitor();
- setIsDeleteOpen(false);
- navigate("/distributed-uptime");
- }}
- onCancel={() => {
- setIsDeleteOpen(false);
- }}
- open={isDeleteOpen}
- confirmationButtonLabel="Yes, delete monitor"
- description="Once deleted, your monitor cannot be retrieved."
- isLoading={isDeleting || isLoading}
- />
-
- );
-};
-
-export default DistributedUptimeDetails;
diff --git a/client/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx b/client/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx
deleted file mode 100644
index fa8593cf1..000000000
--- a/client/src/Pages/DistributedUptime/Monitors/Components/MonitorTable/index.jsx
+++ /dev/null
@@ -1,93 +0,0 @@
-// Components
-import DataTable from "../../../../../Components/Table";
-import BarChart from "../../../../../Components/Charts/BarChart";
-import { Box } from "@mui/material";
-import { StatusLabel } from "../../../../../Components/Label";
-import Host from "../../../../../Components/Host";
-
-// Utils
-import { useTheme } from "@mui/material/styles";
-import { useNavigate } from "react-router-dom";
-import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
-import PropTypes from "prop-types";
-
-// Constants
-const TYPE_MAP = {
- distributed_http: "Distributed HTTP",
-};
-
-const MonitorTable = ({ isLoading, monitors }) => {
- const theme = useTheme();
- const navigate = useNavigate();
- const { determineState } = useMonitorUtils();
- const headers = [
- {
- id: "name",
- content: Host ,
- render: (row) => (
-
- ),
- },
- {
- id: "status",
- content: Status ,
- render: (row) => {
- const status = determineState(row?.monitor);
- return (
-
- );
- },
- },
- {
- id: "responseTime",
- content: "Response Time",
- render: (row) => ,
- },
- {
- id: "type",
- content: "Type",
- render: (row) => {TYPE_MAP[row.monitor.type]} ,
- },
- {
- id: "actions",
- content: "Actions",
- render: (row) => {"TODO"} ,
- },
- ];
-
- return (
- {
- navigate(`/distributed-uptime/${row._id}`);
- },
- }}
- />
- );
-};
-
-MonitorTable.propTypes = {
- isLoading: PropTypes.bool,
- monitors: PropTypes.array,
-};
-
-export default MonitorTable;
diff --git a/client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx
deleted file mode 100644
index 233fc3c06..000000000
--- a/client/src/Pages/DistributedUptime/Monitors/Components/Skeleton/index.jsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-
-export const SkeletonLayout = () => {
- return (
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Pages/DistributedUptime/Monitors/index.jsx b/client/src/Pages/DistributedUptime/Monitors/index.jsx
deleted file mode 100644
index d2cea7432..000000000
--- a/client/src/Pages/DistributedUptime/Monitors/index.jsx
+++ /dev/null
@@ -1,102 +0,0 @@
-// Components
-import { Stack, Typography } from "@mui/material";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import CreateMonitorHeader from "../../../Components/MonitorCreateHeader";
-import MonitorTable from "./Components/MonitorTable";
-import Pagination from "../../../Components/Table/TablePagination";
-import Fallback from "../../../Components/Fallback";
-import GenericFallback from "../../../Components/GenericFallback";
-
-// Utils
-import { useState } from "react";
-import { useTheme } from "@mui/material/styles";
-import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import { useSubscribeToDepinMonitors } from "../../../Hooks/useSubscribeToDepinMonitors";
-import SkeletonLayout from "./Components/Skeleton";
-import { useTranslation } from "react-i18next";
-// Constants
-const BREADCRUMBS = [{ name: `Distributed Uptime`, path: "/distributed-uptime" }];
-
-const DistributedUptimeMonitors = () => {
- // Local state
- const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(10);
-
- // Utils
- const theme = useTheme();
- const isAdmin = useIsAdmin();
- const { t } = useTranslation();
- const [monitors, count, isLoading, networkError] = useSubscribeToDepinMonitors(
- page,
- rowsPerPage
- );
- // Handlers
- const handleChangePage = (event, newPage) => {
- setPage(newPage);
- };
-
- const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(event.target.value);
- setPage(0);
- };
-
- if (isLoading) {
- return ;
- }
-
- if (networkError) {
- return (
-
-
- {t("networkError")}
-
- {t("checkConnection")}
-
- );
- }
-
- if (count === 0) {
- return (
-
- );
- }
- return (
-
-
-
-
-
-
- );
-};
-
-export default DistributedUptimeMonitors;
diff --git a/client/src/Pages/DistributedUptimeStatus/Create/Components/VisuallyHiddenInput/index.jsx b/client/src/Pages/DistributedUptimeStatus/Create/Components/VisuallyHiddenInput/index.jsx
deleted file mode 100644
index fd002d2a4..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Create/Components/VisuallyHiddenInput/index.jsx
+++ /dev/null
@@ -1,22 +0,0 @@
-const VisuallyHiddenInput = ({ onChange }) => {
- return (
-
- );
-};
-
-export default VisuallyHiddenInput;
diff --git a/client/src/Pages/DistributedUptimeStatus/Create/index.jsx b/client/src/Pages/DistributedUptimeStatus/Create/index.jsx
deleted file mode 100644
index 308f9489b..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Create/index.jsx
+++ /dev/null
@@ -1,322 +0,0 @@
-// Components
-import { Stack, Typography, Button, Box } from "@mui/material";
-import ConfigBox from "../../../Components/ConfigBox";
-import Checkbox from "../../../Components/Inputs/Checkbox";
-import TextInput from "../../../Components/Inputs/TextInput";
-import VisuallyHiddenInput from "./Components/VisuallyHiddenInput";
-import Image from "../../../Components/Image";
-import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import Search from "../../../Components/Inputs/Search";
-import MonitorList from "../../StatusPage/Create/Components/MonitorList";
-// Utils
-import { useTheme } from "@emotion/react";
-import { useState, useEffect } from "react";
-import { useParams } from "react-router-dom";
-import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage";
-import { statusPageValidation } from "../../../Validation/validation";
-import { buildErrors } from "../../../Validation/error";
-import { createToast } from "../../../Utils/toastUtils";
-import { useNavigate } from "react-router-dom";
-import { useMonitorsFetch } from "../../StatusPage/Create/Hooks/useMonitorsFetch";
-import { useTranslation } from "react-i18next";
-import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage";
-const CreateStatus = () => {
- const theme = useTheme();
- const { monitorId, url } = useParams();
- const navigate = useNavigate();
- const { t } = useTranslation();
- const isCreate = typeof url === "undefined";
-
- const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate);
-
- const [statusPageIsLoading, statusPageNetworkError, statusPage, _, isPublished] =
- useFetchDepinStatusPage({
- url,
- timeFrame: 30,
- isCreate,
- });
-
- const [monitors, monitorsIsLoading, monitorsNetworkError] = useMonitorsFetch();
-
- const BREADCRUMBS = [
- { name: "distributed uptime", path: "/distributed-uptime" },
- {
- name: "details",
- path: `/distributed-uptime/${isCreate ? monitorId : statusPage?.monitors[0]}`,
- },
- { name: isCreate ? "create status page" : "edit status page", path: `` },
- ];
- // Local state
- const [form, setForm] = useState({
- type: "distributed",
- isPublished: false,
- url: url ?? Math.floor(Math.random() * 1000000).toFixed(0),
- logo: undefined,
- companyName: "",
- monitors: [monitorId],
- });
- const [errors, setErrors] = useState({});
- const [search, setSearch] = useState("");
- const [selectedMonitors, setSelectedMonitors] = useState([]);
-
- const handleFormChange = (e) => {
- const { name, value, checked, type } = e.target;
-
- // Check for errors
- const { error } = statusPageValidation.validate(
- { [name]: value },
- { abortEarly: false }
- );
-
- setErrors((prev) => buildErrors(prev, name, error));
-
- if (type === "checkbox") {
- setForm({ ...form, [name]: checked });
- return;
- }
- setForm({ ...form, [name]: value });
- };
-
- const handleMonitorsChange = (selectedMonitors) => {
- handleFormChange({
- target: {
- name: "subMonitors",
- value: selectedMonitors.map((monitor) => monitor._id),
- },
- });
- setSelectedMonitors(selectedMonitors);
- };
-
- const handleImageUpload = (e) => {
- const img = e.target?.files?.[0];
- setForm((prev) => ({
- ...prev,
- logo: img,
- }));
- };
- const handleSubmit = async () => {
- let logoToSubmit = undefined;
-
- // Handle image
- if (typeof form.logo !== "undefined" && typeof form.logo.src === "undefined") {
- logoToSubmit = {
- src: URL.createObjectURL(form.logo),
- name: form.logo.name,
- type: form.logo.type,
- size: form.logo.size,
- };
- } else if (typeof form.logo !== "undefined") {
- logoToSubmit = form.logo;
- }
- const formToSubmit = { ...form };
- if (typeof logoToSubmit !== "undefined") {
- formToSubmit.logo = logoToSubmit;
- }
- // Validate
- const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false });
- if (typeof error === "undefined") {
- const success = await createStatusPage({ form: formToSubmit });
- if (success) {
- const verb = isCreate ? "created" : "updated";
- createToast({ body: `Status page ${verb} successfully` });
- navigate(`/status/distributed/${form.url}`);
- }
- return;
- }
- const newErrors = {};
- error?.details?.forEach((err) => {
- newErrors[err.path[0]] = err.message;
- });
- setErrors((prev) => ({ ...prev, ...newErrors }));
- };
-
- // If we are configuring, populate fields
- useEffect(() => {
- if (isCreate) return;
- if (typeof statusPage === "undefined") {
- return;
- }
-
- let newLogo = undefined;
- if (statusPage.logo) {
- newLogo = {
- src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`,
- name: "logo",
- type: statusPage.logo.contentType,
- size: null,
- };
- }
-
- setForm((prev) => {
- return {
- ...prev,
- companyName: statusPage?.companyName,
- isPublished: statusPage?.isPublished,
- timezone: statusPage?.timezone,
- monitors: statusPage?.monitors,
- subMonitors: statusPage?.subMonitors.map((monitor) => monitor._id),
- color: statusPage?.color,
- logo: newLogo,
- };
- });
- setSelectedMonitors(statusPage?.subMonitors);
- }, [isCreate, statusPage]);
-
- const imgSrc = form?.logo?.src
- ? form.logo.src
- : form.logo
- ? URL.createObjectURL(form.logo)
- : undefined;
-
- return (
-
-
-
-
- {isCreate
- ? t("distributedUptimeStatusCreateYour")
- : t("distributedUptimeStatusEditYour")}{" "}
-
-
- {t("distributedUptimeStatusCreateStatusPage")}
-
-
-
-
-
- {t("distributedUptimeStatusCreateStatusPageAccess")}
-
-
- {t("distributedUptimeStatusCreateStatusPageReady")}
-
-
-
-
-
-
-
-
-
- {t("distributedUptimeStatusBasicInfoHeader")}
-
-
- {t("distributedUptimeStatusBasicInfoDescription")}
-
-
-
-
-
-
-
-
-
- {t("distributedUptimeStatusLogoHeader")}
-
- {t("distributedUptimeStatusLogoDescription")}{" "}
-
-
-
-
-
-
- {t("distributedUptimeStatusLogoUploadButton")}
-
-
-
-
-
-
-
-
- {t("distributedUptimeStatusStandardMonitorsHeader")}
-
-
- {t("distributedUptimeStatusStandardMonitorsDescription")}
-
-
-
-
-
-
-
-
-
- {t("settingsSave")}
-
-
-
- );
-};
-
-export default CreateStatus;
diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx
deleted file mode 100644
index d9ed16e63..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-// Components
-import { Stack, Box } from "@mui/material";
-import Host from "../../../../../Components/Host";
-import DePINStatusPageBarChart from "../../../../../Components/Charts/DePINStatusPageBarChart";
-import { StatusLabel } from "../../../../../Components/Label";
-
-//Utils
-import { useTheme } from "@mui/material/styles";
-import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
-import PropTypes from "prop-types";
-const MonitorsList = ({
- isLoading = false,
- shouldRender = true,
- monitors = [],
- timeFrame,
-}) => {
- const theme = useTheme();
- const { determineState } = useUtils();
- return (
- <>
- {monitors?.map((monitor) => {
- const status = determineState(monitor);
- return (
-
-
-
-
-
-
-
-
-
-
-
- );
- })}
- >
- );
-};
-
-MonitorsList.propTypes = {
- monitors: PropTypes.array.isRequired,
-};
-
-export default MonitorsList;
diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx
deleted file mode 100644
index 233fc3c06..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { Stack, Skeleton } from "@mui/material";
-
-export const SkeletonLayout = () => {
- return (
-
-
-
- );
-};
-
-export default SkeletonLayout;
diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx
deleted file mode 100644
index 7ee4ff292..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Stack, Button, ButtonGroup } from "@mui/material";
-import { useTheme } from "@emotion/react";
-import { useTranslation } from "react-i18next";
-
-const TimeFrameHeader = ({ timeFrame, setTimeFrame, sx, ...props }) => {
- const theme = useTheme();
- const { t } = useTranslation();
- return (
-
-
- setTimeFrame(30)}
- >
- {t("distributedUptimeStatus30Days")}
-
- setTimeFrame(60)}
- >
- {t("distributedUptimeStatus60Days")}
-
- setTimeFrame(90)}
- >
- {t("distributedUptimeStatus90Days")}
-
-
-
- );
-};
-
-export default TimeFrameHeader;
diff --git a/client/src/Pages/DistributedUptimeStatus/Status/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/index.jsx
deleted file mode 100644
index 117a297e5..000000000
--- a/client/src/Pages/DistributedUptimeStatus/Status/index.jsx
+++ /dev/null
@@ -1,319 +0,0 @@
-//Components
-import DistributedUptimeMap from "../../DistributedUptime/Details/Components/DistributedUptimeMap";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import { Stack, Typography } from "@mui/material";
-import DeviceTicker from "../../DistributedUptime/Details/Components/DeviceTicker";
-import DistributedUptimeResponseChart from "../../DistributedUptime/Details/Components/DistributedUptimeResponseChart";
-import NextExpectedCheck from "../../DistributedUptime/Details/Components/NextExpectedCheck";
-import Footer from "../../DistributedUptime/Details/Components/Footer";
-import StatBoxes from "../../DistributedUptime/Details/Components/StatBoxes";
-import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader";
-import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import GenericFallback from "../../../Components/GenericFallback";
-import Dialog from "../../../Components/Dialog";
-import SkeletonLayout from "./Components/Skeleton";
-import UptLogo from "../../../assets/icons/upt_logo.png";
-import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined";
-import InfoBox from "../../../Components/InfoBox";
-import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader";
-import MonitorsList from "./Components/MonitorsList";
-import { RowContainer } from "../../../Components/StandardContainer";
-
-//Utils
-import { useTheme } from "@mui/material/styles";
-import { useState, useEffect, useRef } from "react";
-import { useParams } from "react-router-dom";
-import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage";
-import { useSubscribeToDepinDetails } from "../../../Hooks/useSubscribeToDepinDetails";
-import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete";
-import TimeFrameHeader from "./Components/TimeframeHeader";
-import SubHeader from "../../../Components/Subheader";
-import { safelyParseFloat } from "../../../Utils/utils";
-import { useNavigate, useLocation } from "react-router-dom";
-import { useTranslation } from "react-i18next";
-import { useSelector, useDispatch } from "react-redux";
-import { setMode } from "../../../Features/UI/uiSlice";
-
-// Responsive
-import { useMediaQuery } from "@mui/material";
-
-const DistributedUptimeStatus = () => {
- const { url } = useParams();
- const location = useLocation();
- const { t } = useTranslation();
- const isPublic = location.pathname.startsWith("/status/distributed/public");
- const elementToCapture = useRef(null);
-
- // Redux state
- const mode = useSelector((state) => state.ui.mode);
- const originalModeRef = useRef(null);
-
- // Local State
- const [dateRange, setDateRange] = useState("recent");
- const [isDeleteOpen, setIsDeleteOpen] = useState(false);
- const [timeFrame, setTimeFrame] = useState(30);
- // Utils
- const theme = useTheme();
- const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm"));
- const navigate = useNavigate();
- const dispatch = useDispatch();
-
- const [
- statusPageIsLoading,
- statusPageNetworkError,
- statusPage,
- monitorId,
- isPublished,
- ] = useFetchDepinStatusPage({
- url,
- timeFrame,
- });
-
- const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] =
- useSubscribeToDepinDetails({ monitorId, dateRange, isPublic, isPublished });
-
- const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => {
- navigate("/distributed-uptime");
- }, url);
- // Constants
- const BREADCRUMBS = [
- { name: "Distributed Uptime", path: "/distributed-uptime" },
- { name: "details", path: `/distributed-uptime/${monitorId}` },
- { name: "status", path: `` },
- ];
-
- let sx = {};
- if (isPublic) {
- sx = {
- paddingTop: "10vh",
- paddingRight: "10vw",
- paddingBottom: "10vh",
- paddingLeft: "10vw",
- };
- }
-
- // Default to dark mode
- useEffect(() => {
- const cleanup = () => {
- if (originalModeRef.current === null) {
- originalModeRef.current = mode;
- }
-
- if (isPublic) {
- dispatch(setMode(originalModeRef.current));
- }
- };
-
- if (isPublic) {
- dispatch(setMode("dark"));
- }
-
- window.addEventListener("beforeunload", cleanup);
- return () => {
- window.removeEventListener("beforeunload", cleanup);
- };
- }, [dispatch, isPublic]);
-
- // Done loading, a status page doesn't exist
- if (!statusPageIsLoading && typeof statusPage === "undefined") {
- return (
-
-
-
- {t("distributedUptimeStatusPageNotSetUp")}
-
- {t("distributedUptimeStatusContactAdmin")}
-
-
- );
- }
-
- // Done loading, a status page exists but is not public
- if (!statusPageIsLoading && isPublic && statusPage.isPublished === false) {
- return (
-
-
- {t("distributedUptimeStatusPageNotPublic")}
-
-
- );
- }
-
- if (isLoading || statusPageIsLoading) {
- return ;
- }
-
- if (networkError || statusPageNetworkError) {
- return (
-
-
- {t("networkError")}
-
- {t("checkConnection")}
-
- );
- }
-
- if (
- typeof statusPage === "undefined" ||
- typeof monitor === "undefined" ||
- monitor.totalChecks === 0
- ) {
- return (
-
-
-
- {t("distributedUptimeDetailsNoMonitorHistory")}
-
-
- );
- }
-
- return (
-
- {!isPublic && }
-
-
-
-
-
-
-
-
- {t("distributedRightCategoryTitle")}
-
- {statusPage.companyName}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 0}
- direction={{ s: "column", md: "row" }}
- headerText={t("distributedStatusServerMonitors")}
- subHeaderText={t("distributedStatusServerMonitorsDescription")}
- gap={isSmallScreen ? theme.spacing(10) : 0}
- alignItems={{ s: "flex-start", md: "flex-end" }}
- >
-
-
-
-
-
- {
- deleteStatusPage();
- setIsDeleteOpen(false);
- }}
- onCancel={() => {
- setIsDeleteOpen(false);
- }}
- open={isDeleteOpen}
- confirmationButtonLabel={t("distributedUptimeStatusPageDeleteConfirm")}
- description={t("distributedUptimeStatusPageDeleteDescription")}
- isLoading={isDeleting || isLoading}
- />
-
- );
-};
-
-export default DistributedUptimeStatus;
diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
index cc2d212f0..3b2e07238 100644
--- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
+++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx
@@ -11,9 +11,10 @@ import NetworkError from "../../../../Components/GenericFallback/NetworkError";
import { formatDateWithTz } from "../../../../Utils/timeUtils";
import { useSelector } from "react-redux";
import { useState } from "react";
-import useChecksFetch from "../../Hooks/useChecksFetch";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
+import { useFetchChecksTeam } from "../../../../Hooks/checkHooks";
+import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks";
const IncidentTable = ({
shouldRender,
@@ -30,14 +31,37 @@ const IncidentTable = ({
const [rowsPerPage, setRowsPerPage] = useState(10);
const selectedMonitorDetails = monitors?.[selectedMonitor];
const selectedMonitorType = selectedMonitorDetails?.type;
- const { isLoading, networkError, checks, checksCount } = useChecksFetch({
- selectedMonitor,
- selectedMonitorType,
- filter,
- dateRange,
- page,
- rowsPerPage,
- });
+
+ const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] =
+ useFetchChecksByMonitor({
+ monitorId: selectedMonitor === "0" ? undefined : selectedMonitor,
+ type: selectedMonitorType,
+ status: false,
+ sortOrder: "desc",
+ limit: null,
+ dateRange,
+ filter: filter,
+ page: page,
+ rowsPerPage: rowsPerPage,
+ enabled: selectedMonitor !== "0",
+ });
+
+ const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] =
+ useFetchChecksTeam({
+ status: false,
+ sortOrder: "desc",
+ limit: null,
+ dateRange,
+ filter: filter,
+ page: page,
+ rowsPerPage: rowsPerPage,
+ enabled: selectedMonitor === "0",
+ });
+
+ const checks = selectedMonitor === "0" ? checksTeam : checksMonitor;
+ const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor;
+ const isLoading = isLoadingTeam || isLoadingMonitor;
+ const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor;
const { t } = useTranslation();
diff --git a/client/src/Pages/Incidents/Hooks/useChecksFetch.jsx b/client/src/Pages/Incidents/Hooks/useChecksFetch.jsx
deleted file mode 100644
index a2f2c8211..000000000
--- a/client/src/Pages/Incidents/Hooks/useChecksFetch.jsx
+++ /dev/null
@@ -1,66 +0,0 @@
-import { useState, useEffect } from "react";
-import { networkService } from "../../../main";
-import { createToast } from "../../../Utils/toastUtils";
-import { useSelector } from "react-redux";
-const useChecksFetch = ({
- selectedMonitor,
- selectedMonitorType,
- filter,
- dateRange,
- page,
- rowsPerPage,
-}) => {
- //Redux
- const { user } = useSelector((state) => state.auth);
-
- //Local
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
- const [checks, setChecks] = useState(undefined);
- const [checksCount, setChecksCount] = useState(undefined);
-
- useEffect(() => {
- const fetchChecks = async () => {
- try {
- setIsLoading(true);
- let res;
-
- if (selectedMonitor === "0") {
- res = await networkService.getChecksByTeam({
- status: false,
- teamId: user.teamId,
- sortOrder: "desc",
- limit: null,
- dateRange,
- filter: filter,
- page: page,
- rowsPerPage: rowsPerPage,
- });
- } else {
- res = await networkService.getChecksByMonitor({
- status: false,
- monitorId: selectedMonitor,
- type: selectedMonitorType,
- sortOrder: "desc",
- limit: null,
- dateRange,
- filter: filter,
- page,
- rowsPerPage,
- });
- }
- setChecks(res.data.data.checks);
- setChecksCount(res.data.data.checksCount);
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
- fetchChecks();
- }, [user, dateRange, page, rowsPerPage, filter, selectedMonitor, selectedMonitorType]);
- return { isLoading, networkError, checks, checksCount };
-};
-
-export default useChecksFetch;
diff --git a/client/src/Pages/Incidents/Hooks/useMonitorsFetch.jsx b/client/src/Pages/Incidents/Hooks/useMonitorsFetch.jsx
deleted file mode 100644
index 46d187aaf..000000000
--- a/client/src/Pages/Incidents/Hooks/useMonitorsFetch.jsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { useState, useEffect } from "react";
-import { networkService } from "../../../main";
-import { createToast } from "../../../Utils/toastUtils";
-const useMonitorsFetch = ({ teamId }) => {
- //Local state
- const [isLoading, setIsLoading] = useState(true);
- const [networkError, setNetworkError] = useState(false);
-
- const [monitors, setMonitors] = useState(undefined);
-
- useEffect(() => {
- const fetchMonitors = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.getMonitorsByTeamId({
- teamId,
- limit: null,
- types: null,
- status: null,
- checkOrder: null,
- normalize: null,
- page: null,
- rowsPerPage: null,
- filter: null,
- field: null,
- order: null,
- });
- if (res?.data?.data?.filteredMonitors?.length > 0) {
- const monitorLookup = res.data.data.filteredMonitors.reduce((acc, monitor) => {
- acc[monitor._id] = {
- _id: monitor._id,
- name: monitor.name,
- type: monitor.type,
- };
- return acc;
- }, {});
- setMonitors(monitorLookup);
- }
- } catch (error) {
- setNetworkError(true);
- createToast({
- body: error.message,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- fetchMonitors();
- }, [teamId]);
- return { isLoading, monitors, networkError };
-};
-
-export { useMonitorsFetch };
diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx
index 16a62a621..09d4f0e25 100644
--- a/client/src/Pages/Incidents/index.jsx
+++ b/client/src/Pages/Incidents/index.jsx
@@ -1,22 +1,21 @@
// Components
import { Stack } from "@mui/material";
import Breadcrumbs from "../../Components/Breadcrumbs";
+import GenericFallback from "../../Components/GenericFallback";
+import IncidentTable from "./Components/IncidentTable";
+import OptionsHeader from "./Components/OptionsHeader";
//Utils
import { useTheme } from "@emotion/react";
-import { useMonitorsFetch } from "./Hooks/useMonitorsFetch";
-import { useSelector } from "react-redux";
-import OptionsHeader from "./Components/OptionsHeader";
-import { useState } from "react";
-import IncidentTable from "./Components/IncidentTable";
-import GenericFallback from "../../Components/GenericFallback";
+import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks";
+import { useState, useEffect } from "react";
import NetworkError from "../../Components/GenericFallback/NetworkError";
import { useTranslation } from "react-i18next";
-//Constants
+import { useParams } from "react-router-dom";
+//Constants
const Incidents = () => {
// Redux state
- const { user } = useSelector((state) => state.auth);
const { t } = useTranslation();
const BREADCRUMBS = [
@@ -27,12 +26,30 @@ const Incidents = () => {
const [selectedMonitor, setSelectedMonitor] = useState("0");
const [filter, setFilter] = useState(undefined);
const [dateRange, setDateRange] = useState(undefined);
+ const [monitorLookup, setMonitorLookup] = useState(undefined);
+
//Utils
const theme = useTheme();
+ const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({});
+ const { monitorId } = useParams();
- const { monitors, isLoading, networkError } = useMonitorsFetch({
- teamId: user.teamId,
- });
+ useEffect(() => {
+ if (monitorId) {
+ setSelectedMonitor(monitorId);
+ }
+ }, [monitorId]);
+
+ useEffect(() => {
+ const monitorLookup = monitors?.reduce((acc, monitor) => {
+ acc[monitor._id] = {
+ _id: monitor._id,
+ name: monitor.name,
+ type: monitor.type,
+ };
+ return acc;
+ }, {});
+ setMonitorLookup(monitorLookup);
+ }, [monitors]);
if (networkError) {
return (
@@ -47,7 +64,7 @@ const Incidents = () => {
{
/>
{
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();
@@ -64,7 +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({});
@@ -93,8 +92,7 @@ const CreateInfrastructureMonitor = () => {
setInfrastructureMonitor({
url: monitor.url.replace(/^https?:\/\//, ""),
name: monitor.name || "",
- notifications: monitor.notifications?.filter((n) => typeof n === "object") || [],
- notify_email: (monitor.notifications?.length ?? 0) > 0,
+ notifications: monitor.notifications,
interval: monitor.interval / MS_PER_MINUTE,
cpu: monitor.thresholds?.usage_cpu !== undefined,
usage_cpu: monitor.thresholds?.usage_cpu ? monitor.thresholds.usage_cpu * 100 : "",
@@ -119,17 +117,9 @@ const CreateInfrastructureMonitor = () => {
}, [isCreate, monitor]);
// Handlers
- const handleCreateInfrastructureMonitor = async (event) => {
+ const onSubmit = async (event) => {
event.preventDefault();
- const formattedNotifications = infrastructureMonitor.notifications.map((n) =>
- typeof n === "string" ? { type: "email", address: n } : n
- );
-
- if (infrastructureMonitor.notify_email) {
- formattedNotifications.push({ type: "email", address: user.email });
- }
-
// Build the form
let form = {
url: `http${https ? "s" : ""}://` + infrastructureMonitor.url,
@@ -155,7 +145,6 @@ const CreateInfrastructureMonitor = () => {
? { usage_temperature: infrastructureMonitor.usage_temperature }
: {}),
secret: infrastructureMonitor.secret,
- notifications: formattedNotifications,
};
const { error } = infrastructureMonitorValidation.validate(form, {
@@ -167,6 +156,7 @@ const CreateInfrastructureMonitor = () => {
error.details.forEach((err) => {
newErrors[err.path[0]] = err.message;
});
+ console.log(newErrors);
setErrors(newErrors);
createToast({ body: "Please check the form for errors." });
return;
@@ -193,32 +183,21 @@ const CreateInfrastructureMonitor = () => {
};
form = {
+ ...(isCreate ? {} : { _id: monitorId }),
...rest,
description: form.name,
- teamId: user.teamId,
- userId: user._id,
type: "hardware",
notifications: infrastructureMonitor.notifications,
thresholds,
};
// 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 handleChange = (event) => {
+ const onChange = (event) => {
const { value, name } = event.target;
setInfrastructureMonitor({
...infrastructureMonitor,
@@ -244,27 +223,6 @@ const CreateInfrastructureMonitor = () => {
});
};
- const handleNotifications = (event, type) => {
- const { value, checked } = event.target;
- let notifications = [...infrastructureMonitor.notifications];
-
- if (checked) {
- if (!notifications.some((n) => n.type === type && n.address === value)) {
- notifications.push({ type, address: value });
- }
- } else {
- notifications = notifications.filter(
- (n) => !(n.type === type && n.address === value)
- );
- }
-
- setInfrastructureMonitor((prev) => ({
- ...prev,
- notifications,
- ...(type === "email" ? { notify_email: checked } : {}),
- }));
- };
-
return (
{
/>
{
@@ -336,7 +293,7 @@ const CreateInfrastructureMonitor = () => {
label={t("infrastructureServerUrlLabel")}
https={https}
value={infrastructureMonitor.url}
- onChange={handleChange}
+ onChange={onChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
disabled={!isCreate}
@@ -370,7 +327,7 @@ const CreateInfrastructureMonitor = () => {
placeholder="Google"
isOptional={true}
value={infrastructureMonitor.name}
- onChange={handleChange}
+ onChange={onChange}
error={errors["name"]}
/>
{
name="secret"
label={t("infrastructureAuthorizationSecretLabel")}
value={infrastructureMonitor.secret}
- onChange={handleChange}
+ onChange={onChange}
error={errors["secret"] ? true : false}
helperText={errors["secret"]}
/>
@@ -387,25 +344,14 @@ const CreateInfrastructureMonitor = () => {
-
- {t("distributedUptimeCreateIncidentNotification")}
-
-
- {t("distributedUptimeCreateIncidentDescription")}
-
+ {t("notificationConfig.title")}
+ {t("notificationConfig.description")}
-
- handleNotifications(event, "email")}
- />
-
+
@@ -438,7 +384,7 @@ const CreateInfrastructureMonitor = () => {
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
- onFieldChange={handleChange}
+ onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
@@ -474,7 +420,7 @@ const CreateInfrastructureMonitor = () => {
name="interval"
label="Check frequency"
value={infrastructureMonitor.interval || 15}
- onChange={handleChange}
+ onChange={onChange}
items={SELECT_VALUES}
/>
@@ -484,10 +430,10 @@ const CreateInfrastructureMonitor = () => {
justifyContent="flex-end"
>
{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 447831acb..6c11e7c82 100644
--- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx
@@ -4,17 +4,17 @@ 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, 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 }) => {
%
>
}
- />
+ /> */}
{
- // 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);
-
- useEffect(() => {
- const fetchData = async () => {
- try {
- const response = await networkService.getHardwareDetailsByMonitorId({
- monitorId: monitorId,
- dateRange: dateRange,
- });
- response.data.data;
- 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/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx
index f7f96f931..b57e0b91e 100644
--- a/client/src/Pages/Infrastructure/Details/index.jsx
+++ b/client/src/Pages/Infrastructure/Details/index.jsx
@@ -1,7 +1,7 @@
// Components
import { Stack, Typography } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
-import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
+import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import StatusBoxes from "./Components/StatusBoxes";
import GaugeBoxes from "./Components/GaugeBoxes";
@@ -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";
@@ -26,6 +26,7 @@ const InfrastructureDetails = () => {
// Local state
const [dateRange, setDateRange] = useState("recent");
+ const [trigger, setTrigger] = useState(false);
// Utils
const theme = useTheme();
@@ -33,11 +34,16 @@ const InfrastructureDetails = () => {
const { t } = useTranslation();
const isAdmin = useIsAdmin();
- const { isLoading, networkError, monitor } = useHardwareMonitorsFetch({
+ const [monitor, isLoading, networkError] = useFetchHardwareMonitorById({
monitorId,
dateRange,
+ updateTrigger: trigger,
});
+ const triggerUpdate = () => {
+ setTrigger(!trigger);
+ };
+
if (networkError === true) {
return (
@@ -46,9 +52,9 @@ const InfrastructureDetails = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -57,11 +63,12 @@ const InfrastructureDetails = () => {
return (
-
{t("distributedUptimeDetailsNoMonitorHistory")}
@@ -73,11 +80,12 @@ const InfrastructureDetails = () => {
return (
-
{
+const MonitorsTable = ({
+ isLoading,
+ monitors,
+ isAdmin,
+ handleActionMenuDelete,
+ isSearching,
+}) => {
// Utils
const theme = useTheme();
const { t } = useTranslation();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const navigate = useNavigate();
// Handlers
@@ -86,6 +94,7 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
monitor={row}
isAdmin={isAdmin}
updateCallback={handleActionMenuDelete}
+ isLoading={isLoading}
/>
),
},
@@ -119,31 +128,35 @@ const MonitorsTable = ({ shouldRender, monitors, isAdmin, handleActionMenuDelete
});
return (
-
+
+ openDetails(row.id),
- emptyView: "No monitors found",
- }}
- />
+ onRowClick: (row) => openDetails(row.id),
+ emptyView: "No monitors found",
+ }}
+ />
+
);
};
MonitorsTable.propTypes = {
- shouldRender: PropTypes.bool,
+ isLoading: PropTypes.bool,
monitors: PropTypes.array,
isAdmin: PropTypes.bool,
handleActionMenuDelete: PropTypes.func,
+ isSearching: PropTypes.bool,
};
export default MonitorsTable;
diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
index bce3020a6..15193da3e 100644
--- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx
@@ -1,7 +1,6 @@
/* TODO I basically copied and pasted this component from the actionsMenu. Check how we can make it reusable */
import { useRef, useState } from "react";
-import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../../../Utils/toastUtils";
@@ -30,7 +29,6 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
const [isOpen, setIsOpen] = useState(false);
const [isDialogOpen, setIsDialogOpen] = useState(false);
const theme = useTheme();
- const { isLoading } = useSelector((state) => state.uptimeMonitors);
const openMenu = (e) => {
e.stopPropagation();
@@ -117,7 +115,6 @@ const InfrastructureMenu = ({ monitor, isAdmin, updateCallback }) => {
onCancel={cancelRemove}
confirmationButtonLabel="Delete"
onConfirm={handleRemove}
- isLoading={isLoading}
modelTitle="modal-delete-monitor"
modelDescription="delete-monitor-confirmation"
/>
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/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx
index 9797736c4..e0f5c642b 100644
--- a/client/src/Pages/Infrastructure/Monitors/index.jsx
+++ b/client/src/Pages/Infrastructure/Monitors/index.jsx
@@ -8,22 +8,31 @@ import Pagination from "../../..//Components/Table/TablePagination";
import GenericFallback from "../../../Components/GenericFallback";
import Fallback from "../../../Components/Fallback";
import Filter from "./Components/Filters";
+import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent";
// Utils
import { useTheme } from "@emotion/react";
-import { useMonitorFetch } from "./Hooks/useMonitorFetch";
import { useState } from "react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
import { useTranslation } from "react-i18next";
+import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks";
+import { useDispatch, useSelector } from "react-redux";
+import { setRowsPerPage } from "../../../Features/UI/uiSlice";
// Constants
+const TYPES = ["hardware"];
const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }];
const InfrastructureMonitors = () => {
// Redux state
+ const rowsPerPage = useSelector((state) => state.ui?.infrastructure?.rowsPerPage ?? 5);
+ const dispatch = useDispatch();
+
+ // Local state
const [page, setPage] = useState(0);
- const [rowsPerPage, setRowsPerPage] = useState(5);
const [updateTrigger, setUpdateTrigger] = useState(false);
const [selectedStatus, setSelectedStatus] = useState(undefined);
const [toFilterStatus, setToFilterStatus] = useState(undefined);
+ const [search, setSearch] = useState(undefined);
+ const [isSearching, setIsSearching] = useState(false);
// Utils
const theme = useTheme();
@@ -40,7 +49,13 @@ const InfrastructureMonitors = () => {
};
const handleChangeRowsPerPage = (event) => {
- setRowsPerPage(event.target.value);
+ dispatch(
+ setRowsPerPage({
+ value: parseInt(event.target.value, 10),
+ table: "infrastructure",
+ })
+ );
+ setPage(0);
};
const handleReset = () => {
@@ -50,10 +65,12 @@ const InfrastructureMonitors = () => {
const field = toFilterStatus !== undefined ? "status" : undefined;
- const { monitors, summary, isLoading, networkError } = useMonitorFetch({
+ const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({
+ limit: 1,
+ types: TYPES,
page,
field: field,
- filter: toFilterStatus,
+ filter: toFilterStatus ?? search,
rowsPerPage,
updateTrigger,
});
@@ -66,9 +83,9 @@ const InfrastructureMonitors = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -99,7 +116,7 @@ const InfrastructureMonitors = () => {
/>
{
setToFilterStatus={setToFilterStatus}
handleReset={handleReset}
/>
+
+
{
+ const levelColors = {
+ info: theme.palette.success.main,
+ warn: theme.palette.warning.main,
+ error: theme.palette.error.main,
+ debug: theme.palette.accent.main,
+ };
+
+ const color = levelColors[log.level] || theme.palette.primary.contrastText;
+
+ return (
+
+ [{log.timestamp}] {" "}
+ {log.level.toUpperCase()}
+ {": "}
+ {`(${log.service})`}
+ {`(${log.method})`}
+ {": "}
+ {log.message}
+
+
+ );
+};
+
+const Logs = () => {
+ // Local state
+ const [logLevel, setLogLevel] = useState("all");
+
+ // Hooks
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [logs, isLoading, error] = useFetchLogs();
+ // Setup
+ const LOG_LEVELS = [
+ { _id: "all", name: t("logsPage.logLevelSelect.values.all") },
+ { _id: "info", name: t("logsPage.logLevelSelect.values.info") },
+ { _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
+ { _id: "error", name: t("logsPage.logLevelSelect.values.error") },
+ { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
+ ];
+ return (
+
+
+ {t("logsPage.description")}
+
+
+ {t("logsPage.logLevelSelect.title")}
+ {
+ setLogLevel(e.target.value);
+ }}
+ />
+
+
+
+
+ {logs
+ ?.filter((log) => {
+ if (logLevel === "all") return true;
+ return log.level === logLevel;
+ })
+ .reverse()
+ .map((log, idx) => formatLog(theme, log, idx))}
+
+
+
+ );
+};
+
+export default Logs;
diff --git a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
new file mode 100644
index 000000000..94fc2f5c3
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx
@@ -0,0 +1,82 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../../Components/Table";
+
+import { useNavigate } from "react-router-dom";
+import { useTheme } from "@emotion/react";
+import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import PropTypes from "prop-types";
+import { useTranslation } from "react-i18next";
+
+const FailedJobTable = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const jobsWithFailures = metrics?.jobsWithFailures;
+ const navigate = useNavigate();
+
+ const headers = [
+ {
+ id: "monitorId",
+ content: t("queuePage.failedJobTable.monitorIdHeader"),
+ render: (row) => {
+ return row.monitorId;
+ },
+ },
+ {
+ id: "monitorUrl",
+ content: t("queuePage.failedJobTable.monitorUrlHeader"),
+ render: (row) => {
+ return row.monitorUrl;
+ },
+ },
+ {
+ id: "failCount",
+ content: t("queuePage.failedJobTable.failCountHeader"),
+ render: (row) => {
+ return row.failCount;
+ },
+ },
+ {
+ id: "failedAt",
+ content: t("queuePage.failedJobTable.failedAtHeader"),
+ render: (row) => {
+ return row.failedAt;
+ },
+ },
+ {
+ id: "failReason",
+ content: t("queuePage.failedJobTable.failReasonHeader"),
+ render: (row) => {
+ return row.failReason;
+ },
+ },
+ ];
+ return (
+
+ {t("queuePage.failedJobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+FailedJobTable.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default FailedJobTable;
diff --git a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
new file mode 100644
index 000000000..e27dd103f
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/JobTable/index.jsx
@@ -0,0 +1,125 @@
+import Stack from "@mui/material/Stack";
+import DataTable from "../../../../../Components/Table";
+import Typography from "@mui/material/Typography";
+// Utils
+import PropTypes from "prop-types";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
+import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
+import { useTranslation } from "react-i18next";
+import { createHeaderFactory } from "../../../../../Components/Table/TableUtils";
+
+const JobTable = ({ jobs = [] }) => {
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const navigate = useNavigate();
+ const buildSx = (row) => {
+ if (row.lockedAt) {
+ return {
+ color: `${theme.palette.success.main} !important`,
+ };
+ }
+ if (!row.active) {
+ return {
+ color: `${theme.palette.warning.main} !important`,
+ };
+ }
+
+ if (row.failCount > 0 && row.lastFailedAt >= row.lastFinishedAt) {
+ return {
+ color: `${theme.palette.error.main} !important`,
+ };
+ }
+
+ return {};
+ };
+
+ const createHeader = createHeaderFactory(buildSx);
+ const headersData = [
+ {
+ id: "id",
+ content: t("queuePage.jobTable.idHeader"),
+ render: (row) => row.monitorId,
+ },
+ {
+ id: "url",
+ content: t("queuePage.jobTable.urlHeader"),
+ render: (row) => row.monitorUrl,
+ },
+ {
+ id: "type",
+ content: t("queuePage.jobTable.typeHeader"),
+ render: (row) => row.monitorType,
+ },
+ {
+ id: "active",
+ content: t("queuePage.jobTable.activeHeader"),
+ render: (row) => row.active.toString(),
+ },
+ {
+ id: "runCount",
+ content: t("queuePage.jobTable.runCountHeader"),
+ render: (row) => row.runCount,
+ },
+ {
+ id: "failCount",
+ content: t("queuePage.jobTable.failCountHeader"),
+ render: (row) => row.failCount,
+ },
+ {
+ id: "lastRun",
+ content: t("queuePage.jobTable.lastRunHeader"),
+ render: (row) => row.lastRunAt || "-",
+ },
+ {
+ id: "lockedAt",
+ content: t("queuePage.jobTable.lockedAtHeader"),
+ render: (row) => row.lockedAt || "-",
+ },
+
+ {
+ id: "lastFinish",
+ content: t("queuePage.jobTable.lastFinishedAtHeader"),
+ render: (row) => row.lastFinishedAt || "-",
+ },
+ {
+ id: "lastRunTook",
+ content: t("queuePage.jobTable.lastRunTookHeader"),
+ render: (row) => {
+ const value = row.lastRunTook ? row.lastRunTook + " ms" : "-";
+ return value;
+ },
+ },
+ ];
+
+ const headers = headersData.map((header) => createHeader(header));
+
+ return (
+
+ {t("queuePage.jobTable.title")}
+ {
+ const path = TypeToPathMap[row.monitorType];
+ navigate(`/${path}/${row.monitorId}`);
+ },
+ rowSX: {
+ cursor: "pointer",
+ "&:hover td": {
+ backgroundColor: theme.palette.tertiary.main,
+ transition: "background-color .3s ease",
+ },
+ },
+ }}
+ />
+
+ );
+};
+
+JobTable.propTypes = {
+ jobs: PropTypes.array,
+};
+
+export default JobTable;
diff --git a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
new file mode 100644
index 000000000..af1cf8010
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/Metrics/index.jsx
@@ -0,0 +1,43 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import StatBox from "../../../../../Components/StatBox";
+import StatusBoxes from "../../../../../Components/StatusBoxes";
+
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+
+const camelToTitle = (str) => {
+ return str
+ .replace(/([A-Z])/g, " $1")
+ .toLowerCase()
+ .replace(/^./, (m) => m.toUpperCase());
+};
+
+const Metrics = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ const data = Object.keys(metrics)
+ .filter((key) => key !== "jobsWithFailures")
+ .map((key) => {
+ return { key, title: camelToTitle(key), value: metrics[key] };
+ });
+
+ return (
+
+ {t("queuePage.metricsTable.title")}
+
+ {data.map((metric) => {
+ return (
+
+ );
+ })}
+
+
+ );
+};
+export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
new file mode 100644
index 000000000..204b90035
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx
@@ -0,0 +1,60 @@
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import DataTable from "../../../../../Components/Table";
+
+// Utils
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import PropTypes from "prop-types";
+
+const camelToTitle = (str) => {
+ return str
+ .replace(/([A-Z])/g, " $1")
+ .toLowerCase()
+ .replace(/^./, (m) => m.toUpperCase());
+};
+
+const Metrics = ({ metrics = {} }) => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const keys = Object.keys(metrics);
+
+ const headers = [
+ {
+ id: "metric",
+ content: t("queuePage.metricsTable.metricHeader"),
+ render: (row) => {
+ return {row.key} ;
+ },
+ },
+ {
+ id: "value",
+ content: t("queuePage.metricsTable.valueHeader"),
+ render: (row) => {
+ return {row.value} ;
+ },
+ },
+ ];
+
+ const data = keys
+ .filter((key) => key !== "jobsWithFailures")
+ .map((key) => {
+ return { key: camelToTitle(key), value: metrics[key] };
+ });
+
+ return (
+
+ {t("queuePage.metricsTable.title")}
+
+
+ );
+};
+
+Metrics.propTypes = {
+ metrics: PropTypes.object,
+};
+
+export default Metrics;
diff --git a/client/src/Pages/Logs/Queue/index.jsx b/client/src/Pages/Logs/Queue/index.jsx
new file mode 100644
index 000000000..729f87fe3
--- /dev/null
+++ b/client/src/Pages/Logs/Queue/index.jsx
@@ -0,0 +1,66 @@
+// Components
+import Stack from "@mui/material/Stack";
+import JobTable from "./components/JobTable";
+import Metrics from "./components/Metrics";
+import FailedJobTable from "./components/FailedJobTable";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Button from "@mui/material/Button";
+
+// Utils
+import { useState } from "react";
+import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
+import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+
+const QueueDetails = () => {
+ // Local state
+ const [trigger, setTrigger] = useState(false);
+
+ // Hooks
+ const { t } = useTranslation();
+ const theme = useTheme();
+ const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
+ const [flushQueue, isFlushing, flushError] = useFlushQueue();
+
+ if (isLoading) return Loading...
;
+ if (error || flushError) return Error: {error.message}
;
+
+ return (
+
+
+
+
+
+
+ {
+ setTrigger(!trigger);
+ }}
+ loading={isLoading}
+ >
+ {t("queuePage.refreshButton")}
+
+ flushQueue(trigger, setTrigger)}
+ loading={isFlushing}
+ >
+ {t("queuePage.flushButton")}
+
+
+
+ );
+};
+
+export default QueueDetails;
diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/Logs/index.jsx
new file mode 100644
index 000000000..482491ad0
--- /dev/null
+++ b/client/src/Pages/Logs/index.jsx
@@ -0,0 +1,41 @@
+import Stack from "@mui/material/Stack";
+import Breadcrumbs from "../../Components/Breadcrumbs";
+import Tabs from "@mui/material/Tabs";
+import Tab from "@mui/material/Tab";
+import Queue from "./Queue";
+import LogsComponent from "./Logs";
+
+import { useTheme } from "@emotion/react";
+import { useTranslation } from "react-i18next";
+import { useState } from "react";
+
+const Logs = () => {
+ const { t } = useTranslation();
+ const theme = useTheme();
+
+ // Local state
+ const [value, setValue] = useState(0);
+
+ // Handlers
+ const handleChange = (event, newValue) => {
+ setValue(newValue);
+ };
+
+ const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
+ return (
+
+
+
+
+
+
+ {value === 0 && }
+ {value === 1 && }
+
+ );
+};
+
+export default Logs;
diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx
index f3754c6b0..e71c7112c 100644
--- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx
+++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx
@@ -135,7 +135,6 @@ const CreateMaintenance = () => {
setIsLoading(true);
try {
const response = await networkService.getMonitorsByTeamId({
- teamId: user.teamId,
limit: null,
types: ["http", "ping", "pagespeed", "port"],
});
diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
index 0cd8ada17..bb5842e9f 100644
--- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
+++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx
@@ -11,6 +11,8 @@ import { formatDurationRounded } from "../../../Utils/timeUtils";
import { StatusLabel } from "../../../Components/Label";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useTranslation } from "react-i18next";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
/**
* Component for pagination actions (first, previous, next, last).
@@ -34,9 +36,10 @@ const MaintenanceTable = ({
maintenanceWindowCount,
updateCallback,
}) => {
- const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
+ const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
const dispatch = useDispatch();
-
+ const theme = useTheme();
+ const navigate = useNavigate();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
@@ -180,6 +183,18 @@ const MaintenanceTable = ({
return (
<>
{
+ navigate(`/maintenance/create/${row._id}`);
+ },
+ }}
headers={headers}
data={maintenanceWindows}
/>
diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/Maintenance/index.jsx
index eccc95b50..66b80f87d 100644
--- a/client/src/Pages/Maintenance/index.jsx
+++ b/client/src/Pages/Maintenance/index.jsx
@@ -17,7 +17,7 @@ const Maintenance = () => {
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
- const { rowsPerPage } = useSelector((state) => state.ui.maintenance);
+ const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
const isAdmin = useIsAdmin();
const [maintenanceWindows, setMaintenanceWindows] = useState([]);
const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0);
@@ -58,9 +58,9 @@ const Maintenance = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
diff --git a/client/src/Pages/NotFound/index.jsx b/client/src/Pages/NotFound/index.jsx
index f9dfe8ec1..fdae92cb5 100644
--- a/client/src/Pages/NotFound/index.jsx
+++ b/client/src/Pages/NotFound/index.jsx
@@ -1,8 +1,7 @@
-import React from "react";
import PropTypes from "prop-types";
import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg";
import { Button, Stack, Typography } from "@mui/material";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
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 (
+ <>
+
+
+
+
+
+ Configure
+
+ Remove
+
+
+ >
+ );
+};
+
+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
new file mode 100644
index 000000000..0fb6cce08
--- /dev/null
+++ b/client/src/Pages/Notifications/create/index.jsx
@@ -0,0 +1,228 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
+import Button from "@mui/material/Button";
+import ConfigBox from "../../../Components/ConfigBox";
+import Box from "@mui/material/Box";
+import Select from "../../../Components/Inputs/Select";
+import TextInput from "../../../Components/Inputs/TextInput";
+
+// Utils
+import { useState } from "react";
+import { useTheme } from "@emotion/react";
+import {
+ useCreateNotification,
+ useGetNotificationById,
+ useEditNotification,
+ useTestNotification,
+} from "../../../Hooks/useNotifications";
+import { notificationValidation } from "../../../Validation/validation";
+import { createToast } from "../../../Utils/toastUtils";
+import { useTranslation } from "react-i18next";
+import { useParams } from "react-router-dom";
+import {
+ NOTIFICATION_TYPES,
+ TITLE_MAP,
+ DESCRIPTION_MAP,
+ LABEL_MAP,
+ PLACEHOLDER_MAP,
+} from "../utils";
+
+// Setup
+
+const CreateNotifications = () => {
+ const { notificationId } = useParams();
+ const theme = useTheme();
+ 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" },
+ ];
+
+ // Redux state
+
+ // local state
+ const [notification, setNotification] = useState({
+ notificationName: "",
+ address: "",
+ type: NOTIFICATION_TYPES[0]._id,
+ });
+ const [errors, setErrors] = useState({});
+ const { t } = useTranslation();
+
+ const [notificationIsLoading, getNotificationError] = useGetNotificationById(
+ notificationId,
+ setNotification
+ );
+
+ // handlers
+ const onSubmit = (e) => {
+ e.preventDefault();
+ const form = {
+ ...notification,
+ type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
+ };
+
+ let error = null;
+
+ error = notificationValidation.validate(form, { abortEarly: false }).error;
+
+ if (error) {
+ const newErrors = {};
+ error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ console.log(JSON.stringify(newErrors));
+ console.log(JSON.stringify(form, null, 2));
+ createToast({ body: "Please check the form for errors." });
+ setErrors(newErrors);
+ return;
+ }
+
+ if (notificationId) {
+ editNotification(notificationId, form);
+ } else {
+ createNotification(form);
+ }
+ };
+
+ const onChange = (e) => {
+ const { name, value } = e.target;
+
+ const newNotification = { ...notification, [name]: value };
+
+ const { error } = notificationValidation.extract(name).validate(value);
+ setErrors((prev) => ({
+ ...prev,
+ [name]: error?.message,
+ }));
+
+ setNotification(newNotification);
+ };
+
+ const onTestNotification = () => {
+ const form = {
+ ...notification,
+ type: NOTIFICATION_TYPES.find((type) => type._id === notification.type).value,
+ };
+
+ let error = null;
+
+ error = notificationValidation.validate(form, { 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);
+ };
+
+ const type = NOTIFICATION_TYPES.find((type) => type._id === notification.type).value;
+ return (
+
+
+ {t("createNotifications.title")}
+
+
+
+
+ {t("createNotifications.nameSettings.title")}
+
+
+ {t("createNotifications.nameSettings.description")}
+
+
+
+
+
+
+
+
+
+ {t("createNotifications.typeSettings.title")}
+
+
+ {t("createNotifications.typeSettings.description")}
+
+
+
+
+
+
+
+
+ {t(TITLE_MAP[type])}
+ {t(DESCRIPTION_MAP[type])}
+
+
+
+
+
+
+
+
+ Test notification
+
+
+ Submit
+
+
+
+
+ );
+};
+
+export default CreateNotifications;
diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx
new file mode 100644
index 000000000..f744d31e8
--- /dev/null
+++ b/client/src/Pages/Notifications/index.jsx
@@ -0,0 +1,122 @@
+// Components
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Breadcrumbs from "../../Components/Breadcrumbs";
+import Button from "@mui/material/Button";
+import DataTable from "../../Components/Table";
+import Fallback from "../../Components/Fallback";
+import ActionMenu from "./components/ActionMenu";
+
+// Utils
+import { useIsAdmin } from "../../Hooks/useIsAdmin";
+import { useState } from "react";
+import { useTheme } from "@emotion/react";
+import { useNavigate } from "react-router-dom";
+import {
+ useGetNotificationsByTeamId,
+ useDeleteNotification,
+} from "../../Hooks/useNotifications";
+import { useTranslation } from "react-i18next";
+
+const Notifications = () => {
+ const navigate = useNavigate();
+ const theme = useTheme();
+ const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }];
+ const [updateTrigger, setUpdateTrigger] = useState(false);
+ const isAdmin = useIsAdmin();
+ const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger);
+ const [deleteNotification, isDeleting, deleteError] = useDeleteNotification();
+ const { t } = useTranslation();
+ // Handlers
+ const triggerUpdate = () => {
+ setUpdateTrigger(!updateTrigger);
+ };
+
+ const onDelete = (id) => {
+ deleteNotification(id, triggerUpdate);
+ };
+
+ const headers = [
+ {
+ id: "name",
+ content: "Name",
+ render: (row) => {
+ return row.notificationName;
+ },
+ },
+ {
+ id: "target",
+ content: "Target",
+ render: (row) => {
+ return row.address;
+ },
+ },
+ {
+ id: "platform",
+ content: "Platform",
+ render: (row) => {
+ return row?.config?.platform || row.type;
+ },
+ },
+
+ {
+ id: "actions",
+ content: "Actions",
+ render: (row) => {
+ return (
+
+ );
+ },
+ },
+ ];
+
+ if (notifications?.length === 0) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ navigate("/notifications/create")}
+ >
+ {t("notifications.createButton")}
+
+
+ {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}
+ />
+
+ );
+};
+
+export default Notifications;
diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js
new file mode 100644
index 000000000..7ef6e5a60
--- /dev/null
+++ b/client/src/Pages/Notifications/utils.js
@@ -0,0 +1,39 @@
+export const NOTIFICATION_TYPES = [
+ { _id: 1, name: "E-mail", value: "email" },
+ { _id: 2, name: "Slack", value: "slack" },
+ { _id: 3, name: "PagerDuty", value: "pager_duty" },
+ { _id: 4, name: "Webhook", value: "webhook" },
+ { _id: 5, name: "Discord", value: "discord" },
+];
+
+export const TITLE_MAP = {
+ email: "createNotifications.emailSettings.title",
+ slack: "createNotifications.slackSettings.title",
+ pager_duty: "createNotifications.pagerDutySettings.title",
+ webhook: "createNotifications.webhookSettings.title",
+ discord: "createNotifications.discordSettings.title",
+};
+
+export const DESCRIPTION_MAP = {
+ email: "createNotifications.emailSettings.description",
+ slack: "createNotifications.slackSettings.description",
+ pager_duty: "createNotifications.pagerDutySettings.description",
+ webhook: "createNotifications.webhookSettings.description",
+ discord: "createNotifications.discordSettings.description",
+};
+
+export const LABEL_MAP = {
+ email: "createNotifications.emailSettings.emailLabel",
+ slack: "createNotifications.slackSettings.webhookLabel",
+ pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel",
+ webhook: "createNotifications.webhookSettings.webhookLabel",
+ discord: "createNotifications.discordSettings.webhookLabel",
+};
+
+export const PLACEHOLDER_MAP = {
+ email: "createNotifications.emailSettings.emailPlaceholder",
+ slack: "createNotifications.slackSettings.webhookPlaceholder",
+ pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder",
+ webhook: "createNotifications.webhookSettings.webhookPlaceholder",
+ discord: "createNotifications.discordSettings.webhookPlaceholder",
+};
diff --git a/client/src/Pages/PageSpeed/Configure/index.jsx b/client/src/Pages/PageSpeed/Configure/index.jsx
index d81387e1b..71e1e111e 100644
--- a/client/src/Pages/PageSpeed/Configure/index.jsx
+++ b/client/src/Pages/PageSpeed/Configure/index.jsx
@@ -1,52 +1,53 @@
-import { useEffect, useState } from "react";
-import { useTheme } from "@emotion/react";
+// Components
import { Box, Stack, Tooltip, Typography, Button } from "@mui/material";
-import { useDispatch, useSelector } from "react-redux";
-import { useNavigate, useParams } from "react-router";
-import {
- deletePageSpeed,
- getPagespeedMonitorById,
- getPageSpeedByTeamId,
- updatePageSpeed,
- pausePageSpeed,
-} from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
-import { monitorValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import { logger } from "../../../Utils/Logger";
-import { useTranslation } from "react-i18next";
import ConfigBox from "../../../Components/ConfigBox";
-import TextInput from "../../../Components/Inputs/TextInput";
import Select from "../../../Components/Inputs/Select";
-import Checkbox from "../../../Components/Inputs/Checkbox";
+import TextInput from "../../../Components/Inputs/TextInput";
import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import PulseDot from "../../../Components/Animated/PulseDot";
import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded";
import SkeletonLayout from "./skeleton";
-import useUtils from "../../Uptime/Monitors/Hooks/useUtils";
-import "./index.css";
+import NotificationsConfig from "../../../Components/NotificationConfig";
import Dialog from "../../../Components/Dialog";
+// Utils
+import { useState } from "react";
+import { useTheme } from "@emotion/react";
+import { useParams } from "react-router";
+import { monitorValidation } from "../../../Validation/validation";
+import { useTranslation } from "react-i18next";
+import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
+import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import {
+ useFetchMonitorById,
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+} from "../../../Hooks/monitorHooks";
const PageSpeedConfigure = () => {
- const theme = useTheme();
- const { t } = useTranslation();
- const navigate = useNavigate();
- const dispatch = useDispatch();
- const MS_PER_MINUTE = 60000;
- const { user } = useSelector((state) => state.auth);
- const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
- const { monitorId } = useParams();
+ // Redux state
+
+ // Local state
const [monitor, setMonitor] = useState({});
const [errors, setErrors] = useState({});
- const { statusColor, pagespeedStatusMsg, determineState } = useUtils();
- const [buttonLoading, setButtonLoading] = useState(false);
- const idMap = {
- "monitor-url": "url",
- "monitor-name": "name",
- "monitor-checks-http": "type",
- "monitor-checks-ping": "type",
- "notify-email-default": "notification-email",
- };
+ const [isOpen, setIsOpen] = useState(false);
+ const [updateTrigger, setUpdateTrigger] = useState(false);
+
+ // Utils
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const MS_PER_MINUTE = 60000;
+ const { monitorId } = useParams();
+ const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
+
+ const [notifications, notificationsAreLoading, notificationsError] =
+ useGetNotificationsByTeamId();
+
+ const [isLoading] = useFetchMonitorById({ monitorId, setMonitor, updateTrigger });
+ const [deleteMonitor, isDeleting] = useDeleteMonitor();
+ const [updateMonitor, isUpdating] = useUpdateMonitor();
+ const [pauseMonitor, isPausing] = usePauseMonitor();
const frequencies = [
{ _id: 3, name: "3 minutes" },
@@ -58,117 +59,48 @@ const PageSpeedConfigure = () => {
{ _id: 10080, name: "1 week" },
];
- useEffect(() => {
- const fetchMonitor = async () => {
- try {
- const action = await dispatch(getPagespeedMonitorById({ monitorId }));
+ // Handlers
+ const triggerUpdate = () => {
+ setUpdateTrigger(!updateTrigger);
+ };
- 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]);
+ const onChange = (event) => {
+ let { value, name } = event.target;
- const handleChange = (event, name) => {
- let { value, id } = event.target;
- if (!name) name = idMap[id];
-
- if (name.includes("notification-")) {
- name = name.replace("notification-", "");
- let hasNotif = monitor.notifications.some(
- (notification) => notification.type === name
- );
- setMonitor((prev) => {
- const notifs = [...prev.notifications];
- if (hasNotif) {
- return {
- ...prev,
- notifications: notifs.filter((notif) => notif.type !== name),
- };
- } else {
- return {
- ...prev,
- notifications: [
- ...notifs,
- name === "email"
- ? { type: name, address: value }
- : // TODO - phone number
- { type: name, phone: value },
- ],
- };
- }
- });
- } else {
- if (name === "interval") {
- value = value * MS_PER_MINUTE;
- }
- setMonitor((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- const validation = monitorValidation.validate(
- { [name]: value },
- { abortEarly: false }
- );
-
- setErrors((prev) => {
- const updatedErrors = { ...prev };
-
- if (validation.error) updatedErrors[name] = validation.error.details[0].message;
- else delete updatedErrors[name];
- return updatedErrors;
- });
+ if (name === "interval") {
+ value = value * MS_PER_MINUTE;
}
+ setMonitor((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+
+ const validation = monitorValidation.validate(
+ { [name]: value },
+ { abortEarly: false }
+ );
+
+ setErrors((prev) => {
+ const updatedErrors = { ...prev };
+
+ if (validation.error) updatedErrors[name] = validation.error.details[0].message;
+ else delete updatedErrors[name];
+ return updatedErrors;
+ });
};
const handlePause = async () => {
- 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) {
- logger.error("Error pausing monitor: " + monitorId);
- createToast({ body: "Failed to pause monitor" });
- }
+ await pauseMonitor({ monitorId, triggerUpdate });
};
- const handleSave = async (event) => {
+ 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 [isOpen, setIsOpen] = useState(false);
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 (
@@ -191,7 +123,7 @@ const PageSpeedConfigure = () => {
component="form"
noValidate
spellCheck="false"
- onSubmit={handleSave}
+ onSubmit={onSubmit}
flex={1}
gap={theme.spacing(10)}
>
@@ -202,7 +134,7 @@ const PageSpeedConfigure = () => {
{monitor.name}
@@ -234,7 +166,7 @@ const PageSpeedConfigure = () => {
{monitor.url?.replace(/^https?:\/\//, "") || "..."}
@@ -329,24 +261,24 @@ const PageSpeedConfigure = () => {
}}
>
@@ -354,62 +286,16 @@ const PageSpeedConfigure = () => {
-
- {t("distributedUptimeCreateIncidentNotification")}
-
+ {t("notificationConfig.title")}
- {t("distributedUptimeCreateIncidentDescription")}
+ {t("notificationConfig.description")}
-
- {t("whenNewIncident")}
- logger.warn("disabled")}
- isDisabled={true}
- />
- notification.type === "email"
- ) || false
- }
- value={user?.email}
- onChange={(event) => handleChange(event)}
- />
- logger.warn("disabled")}
- isDisabled={true}
- />
- {monitor?.notifications?.some(
- (notification) => notification.type === "emails"
- ) ? (
-
- logger.warn("disabled")}
- />
- {t("seperateEmails")}
-
- ) : (
- ""
- )}
-
+
@@ -422,11 +308,11 @@ const PageSpeedConfigure = () => {
handleChange(event, "interval")}
+ onChange={onChange}
/>
@@ -436,11 +322,10 @@ const PageSpeedConfigure = () => {
mt="auto"
>
{t("settingsSave")}
@@ -457,7 +342,7 @@ const PageSpeedConfigure = () => {
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 1d40d4667..5cc4a1a54 100644
--- a/client/src/Pages/PageSpeed/Create/index.jsx
+++ b/client/src/Pages/PageSpeed/Create/index.jsx
@@ -1,30 +1,27 @@
-// 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";
-
-// 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 Checkbox from "../../../Components/Inputs/Checkbox";
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;
@@ -56,15 +53,14 @@ 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 handleCreateMonitor = async (event) => {
+ const onSubmit = async (event) => {
event.preventDefault();
let form = {
url: `http${https ? "s" : ""}://` + monitor.url,
@@ -87,33 +83,13 @@ 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,
- teamId: user.teamId,
- userId: user._id,
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) => {
@@ -134,32 +110,6 @@ const CreatePageSpeed = () => {
}));
};
- const handleNotifications = (event, type) => {
- const { value } = event.target;
- let notifications = [...monitor.notifications];
- const notificationExists = notifications.some((notification) => {
- if (notification.type === type && notification.address === value) {
- return true;
- }
- return false;
- });
- if (notificationExists) {
- notifications = notifications.filter((notification) => {
- if (notification.type === type && notification.address === value) {
- return false;
- }
- return true;
- });
- } else {
- notifications.push({ type, address: value });
- }
-
- setMonitor((prev) => ({
- ...prev,
- notifications,
- }));
- };
-
const handleBlur = (event) => {
const { name } = event.target;
if (name === "url") {
@@ -189,7 +139,7 @@ const CreatePageSpeed = () => {
{
component="h2"
variant="h2"
>
- {t("distributedUptimeCreateIncidentNotification")}
-
-
- {t("distributedUptimeCreateIncidentDescription")}
+ {t("notificationConfig.title")}
+ {t("notificationConfig.description")}
-
- {t("whenNewIncident")}
- notification.type === "email"
- )}
- value={user?.email}
- onChange={(event) => handleNotifications(event, "email")}
- />
-
+
@@ -356,11 +296,11 @@ const CreatePageSpeed = () => {
justifyContent="flex-end"
>
value === undefined)}
- loading={isLoading}
+ loading={isCreating}
>
{t("createMonitor")}
diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
index d3c597c3d..cb79198e9 100644
--- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
+++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx
@@ -5,13 +5,8 @@ import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import { useTranslation } from "react-i18next";
const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
- const { time: uptimeDuration, units: uptimeUnits } = getHumanReadableDuration(
- monitor?.uptimeDuration
- );
-
- const { time: lastCheckTime, units: lastCheckUnits } = getHumanReadableDuration(
- monitor?.lastChecked
- );
+ const uptimeDuration = getHumanReadableDuration(monitor?.uptimeDuration);
+ const time = getHumanReadableDuration(monitor?.lastChecked);
const { t } = useTranslation();
@@ -22,7 +17,6 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
subHeading={
<>
{uptimeDuration}
- {uptimeUnits}
{t("ago")}
>
}
@@ -31,8 +25,7 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => {
heading="last check"
subHeading={
<>
- {lastCheckTime}
- {lastCheckUnits}
+ {time}
{t("ago")}
>
}
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/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx
index d429858d2..cfc799b36 100644
--- a/client/src/Pages/PageSpeed/Details/index.jsx
+++ b/client/src/Pages/PageSpeed/Details/index.jsx
@@ -2,8 +2,8 @@
import { Stack, Typography } from "@mui/material";
import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
-import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes";
+import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
import PageSpeedAreaChart from "./Components/PageSpeedAreaChart";
import PerformanceReport from "./Components/PerformanceReport";
import GenericFallback from "../../../Components/GenericFallback";
@@ -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,22 +27,33 @@ const PageSpeedDetails = () => {
const isAdmin = useIsAdmin();
const { monitorId } = useParams();
- const { monitor, audits, isLoading, networkError } = useMonitorFetch({
- monitorId,
- });
-
+ // Local state
const [metrics, setMetrics] = useState({
accessibility: true,
bestPractices: true,
performance: true,
seo: true,
});
+ const [trigger, setTrigger] = useState(false);
+ // Network
+ const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({
+ monitorId,
+ sortOrder: "desc",
+ limit: 50,
+ dateRange: "day",
+ numToDisplay: null,
+ normalize: null,
+ updateTrigger: trigger,
+ });
// Handlers
const handleMetrics = (id) => {
setMetrics((prev) => ({ ...prev, [id]: !prev[id] }));
};
+ const triggerUpdate = () => {
+ setTrigger(!trigger);
+ };
if (networkError === true) {
return (
@@ -52,9 +62,9 @@ const PageSpeedDetails = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -64,10 +74,12 @@ const PageSpeedDetails = () => {
return (
-
{t("distributedUptimeDetailsNoMonitorHistory")}
@@ -79,11 +91,12 @@ const PageSpeedDetails = () => {
return (
-
{
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();
@@ -245,7 +245,7 @@ const Card = ({ monitor }) => {
@@ -275,7 +275,7 @@ const Card = ({ monitor }) => {
sx={{ gridColumnStart: 1, gridColumnEnd: 4 }}
>
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/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx
index bd40aedff..fabc0859f 100644
--- a/client/src/Pages/PageSpeed/Monitors/index.jsx
+++ b/client/src/Pages/PageSpeed/Monitors/index.jsx
@@ -5,26 +5,29 @@ 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();
const isAdmin = useIsAdmin();
- const { user } = useSelector((state) => state.auth);
- const { pagespeedApiKey } = useSelector((state) => state.settings);
- const { isLoading, monitors, summary, networkError } = useMonitorsFetch({
- teamId: user.teamId,
+ const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({
+ limit: 10,
+ types: TYPES,
+ page: null,
+ rowsPerPage: null,
+ filter: null,
+ field: null,
+ order: null,
});
if (networkError === true) {
@@ -35,9 +38,9 @@ const PageSpeed = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -53,7 +56,7 @@ const PageSpeed = () => {
]}
link="/pagespeed/create"
isAdmin={isAdmin}
- showPageSpeedWarning={isAdmin && !pagespeedApiKey}
+ // showPageSpeedWarning={isAdmin && !pagespeedApiKey}
/>
);
}
@@ -67,8 +70,8 @@ const PageSpeed = () => {
path="/pagespeed/create"
/>
{
// If successful, show toast and navigate to login page
createToast({
- body: t("backendReconnected", "Connection to server restored"),
+ body: t("errorPages.serverUnreachable.toasts.reconnected"),
});
navigate("/login");
} catch (error) {
// If still unreachable, stay on this page and show toast
createToast({
- body: t("backendStillUnreachable", "Server is still unreachable"),
+ body: t("errorPages.serverUnreachable.toasts.stillUnreachable"),
});
} finally {
setIsCheckingConnection(false);
@@ -71,7 +71,7 @@ const ServerUnreachable = () => {
gap={theme.spacing(4)}
>
- Checkmate
+ {t("common.appName")}
{
>
@@ -131,10 +131,7 @@ const ServerUnreachable = () => {
align="center"
color={theme.palette.primary.contrastTextSecondary}
>
- {t(
- "backendUnreachableMessage",
- "The Checkmate server is not responding. Please check your deployment configuration or try again later."
- )}
+ {t("errorPages.serverUnreachable.description")}
@@ -156,8 +153,8 @@ const ServerUnreachable = () => {
}}
>
{isCheckingConnection
- ? t("retryingConnection", "Retrying Connection...")
- : t("retryConnection", "Retry Connection")}
+ ? t("errorPages.serverUnreachable.retryButton.processing")
+ : t("errorPages.serverUnreachable.retryButton.default")}
diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx
index 121b732f8..a80351393 100644
--- a/client/src/Pages/Settings/SettingsAbout.jsx
+++ b/client/src/Pages/Settings/SettingsAbout.jsx
@@ -16,13 +16,15 @@ const SettingsAbout = () => {
component="h1"
variant="h2"
>
- {t("settingsAbout")}
+ {t("settingsPage.aboutSettings.title")}
- Checkmate {2.1}
+
+ {t("common.appName")} {__APP_VERSION__}
+
- {t("settingsDevelopedBy")}
+ {t("settingsPage.aboutSettings.labelDevelopedBy")}
- {t("settingsDemoMonitors")}
+ {t("settingsPage.demoMonitorsSettings.title")}
+
+
+ {t("settingsPage.demoMonitorsSettings.description")}
- {t("settingsDemoMonitorsDescription")}
- {t("settingsAddDemoMonitors")}
- {t("settingsAddDemoMonitorsButton")}
+ {t("settingsPage.demoMonitorsSettings.buttonAddMonitors")}
@@ -57,14 +58,13 @@ const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) =
component="h1"
variant="h2"
>
- {t("settingsSystemReset")}
+ {t("settingsPage.systemResetSettings.title")}
- {t("settingsSystemResetDescription")}
+ {t("settingsPage.systemResetSettings.description")}
- {t("settingsRemoveAllMonitors")}
setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
- {t("settingsRemoveAllMonitorsButton")}
+ {t("settingsPage.systemResetSettings.buttonRemoveAllMonitors")}
setIsOpen(false)}
- confirmationButtonLabel={t("settingsRemoveAllMonitorsDialogConfirm")}
+ confirmationButtonLabel={t("settingsPage.systemResetSettings.dialogConfirm")}
onConfirm={() => {
const syntheticEvent = {
target: {
diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx
index 88f8f98c1..6c527eaec 100644
--- a/client/src/Pages/Settings/SettingsEmail.jsx
+++ b/client/src/Pages/Settings/SettingsEmail.jsx
@@ -4,6 +4,8 @@ import ConfigBox from "../../Components/ConfigBox";
import TextInput from "../../Components/Inputs/TextInput";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
+import { Switch } from "@mui/material";
+import TextLink from "../../Components/TextLink";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
@@ -19,15 +21,31 @@ const SettingsEmail = ({
handleChange,
settingsData,
setSettingsData,
- isPasswordSet,
+ isEmailPasswordSet,
+ emailPasswordHasBeenReset,
+ setEmailPasswordHasBeenReset,
}) => {
// Setup
const { t } = useTranslation();
const theme = useTheme();
+ // Destructure settings with default values
+ const {
+ systemEmailHost = "",
+ systemEmailPort = "",
+ systemEmailSecure = false,
+ systemEmailPool = false,
+ systemEmailUser = "",
+ systemEmailAddress = "",
+ systemEmailPassword = "",
+ systemEmailTLSServername = "",
+ systemEmailConnectionHost = "localhost",
+ systemEmailIgnoreTLS = false,
+ systemEmailRequireTLS = false,
+ systemEmailRejectUnauthorized = true,
+ } = settingsData?.settings || {};
// Local state
const [password, setPassword] = useState("");
- const [hasBeenReset, setHasBeenReset] = useState(false);
// Network
const [isSending, , sendTestEmail] = useSendTestEmail(); // Using empty placeholder for unused error variable
@@ -47,18 +65,32 @@ const SettingsEmail = ({
const handleSendTestEmail = () => {
// Collect current form values
const emailConfig = {
- systemEmailHost: settingsData?.settings?.systemEmailHost,
- systemEmailPort: settingsData?.settings?.systemEmailPort,
- systemEmailUser: settingsData?.settings?.systemEmailUser,
- systemEmailAddress: settingsData?.settings?.systemEmailAddress,
- systemEmailPassword: password || settingsData?.settings?.systemEmailPassword,
- systemEmailConnectionHost: settingsData?.settings?.systemEmailConnectionHost,
+ systemEmailHost,
+ systemEmailPort,
+ systemEmailSecure,
+ systemEmailPool,
+ systemEmailUser,
+ systemEmailAddress,
+ systemEmailPassword: password || systemEmailPassword,
+ systemEmailTLSServername,
+ systemEmailConnectionHost,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
};
// Basic validation
- if (!emailConfig.systemEmailHost || !emailConfig.systemEmailPort) {
+ if (
+ !emailConfig.systemEmailHost ||
+ !emailConfig.systemEmailPort ||
+ !emailConfig.systemEmailAddress ||
+ !emailConfig.systemEmailPassword
+ ) {
createToast({
- body: t("settingsEmailRequiredFields", "Email host and port are required"),
+ body: t(
+ "settingsEmailRequiredFields",
+ "Email address, host, port and password are required"
+ ),
variant: "error",
});
return;
@@ -79,52 +111,54 @@ const SettingsEmail = ({
component="h1"
variant="h2"
>
- {t("settingsEmail")}
+ {t("settingsPage.emailSettings.title")}
+
+
+ {t("settingsPage.emailSettings.description")}
- {t("settingsEmailDescription")}
- {t("settingsEmailHost")}
+ {t("settingsPage.emailSettings.labelHost")}
- {t("settingsEmailPort")}
+ {t("settingsPage.emailSettings.labelPort")}
- {t("settingsEmailUser")}
+ {t("settingsPage.emailSettings.labelUser")}
- {t("settingsEmailAddress")}
+ {t("settingsPage.emailSettings.labelAddress")}
- {(isPasswordSet === false || hasBeenReset === true) && (
+ {(isEmailPasswordSet === false || emailPasswordHasBeenReset === true) && (
- {t("settingsEmailPassword")}
+ {t("settingsPage.emailSettings.labelPassword")}
)}
- {isPasswordSet === true && hasBeenReset === false && (
+
+ {isEmailPasswordSet === true && emailPasswordHasBeenReset === false && (
- {t("settingsEmailFieldResetLabel")}
+ {t("settingsPage.emailSettings.labelPasswordSet")}
{
setPassword("");
@@ -145,7 +180,7 @@ const SettingsEmail = ({
...settingsData,
settings: { ...settingsData.settings, systemEmailPassword: "" },
});
- setHasBeenReset(true);
+ setEmailPasswordHasBeenReset(true);
}}
variant="contained"
color="error"
@@ -156,23 +191,129 @@ const SettingsEmail = ({
)}
- {t("settingsEmailConnectionHost")}
+ {t("settingsPage.emailSettings.labelTLSServername")}
- {t("settingsPage.emailSettings.labelConnectionHost")}
+
+
+
+ {[
+ [
+ "settingsPage.emailSettings.labelSecure",
+ "systemEmailSecure",
+ systemEmailSecure,
+ ],
+ [
+ "settingsPage.emailSettings.labelPool",
+ "systemEmailPool",
+ systemEmailPool,
+ ],
+ [
+ "settingsPage.emailSettings.labelIgnoreTLS",
+ "systemEmailIgnoreTLS",
+ systemEmailIgnoreTLS,
+ ],
+ [
+ "settingsPage.emailSettings.labelRequireTLS",
+ "systemEmailRequireTLS",
+ systemEmailRequireTLS,
+ ],
+ [
+ "settingsPage.emailSettings.labelRejectUnauthorized",
+ "systemEmailRejectUnauthorized",
+ systemEmailRejectUnauthorized,
+ ],
+ ].map(([labelKey, name, value]) => (
+
+ {t(labelKey)}
+
+
+ ))}
+
+
+
- {t("settingsTestEmail", "Send test e-mail")}
-
+
+ {JSON.stringify(
+ {
+ host: systemEmailHost,
+ port: systemEmailPort,
+ secure: systemEmailSecure,
+ auth: {
+ user: systemEmailUser || systemEmailAddress,
+ pass: "",
+ },
+ name: systemEmailConnectionHost || "localhost",
+ pool: systemEmailPool,
+ tls: {
+ rejectUnauthorized: systemEmailRejectUnauthorized,
+ ignoreTLS: systemEmailIgnoreTLS,
+ requireTLS: systemEmailRequireTLS,
+ servername: systemEmailTLSServername,
+ },
+ },
+ null,
+ 2
+ )}
+
+
+
+
+
+ {systemEmailHost &&
+ systemEmailPort &&
+ systemEmailAddress &&
+ systemEmailPassword && (
+
+ {t("settingsPage.emailSettings.buttonSendTestEmail")}
+
+ )}
diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx
index c7007b5bd..157c75dac 100644
--- a/client/src/Pages/Settings/SettingsPagespeed.jsx
+++ b/client/src/Pages/Settings/SettingsPagespeed.jsx
@@ -8,7 +8,7 @@ import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornmen
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
-import { useState, useEffect } from "react";
+import { useState } from "react";
import { useTranslation } from "react-i18next";
const SettingsPagespeed = ({
@@ -17,13 +17,14 @@ const SettingsPagespeed = ({
settingsData,
setSettingsData,
isApiKeySet,
+ apiKeyHasBeenReset,
+ setApiKeyHasBeenReset,
}) => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [apiKey, setApiKey] = useState("");
- const [hasBeenReset, setHasBeenReset] = useState(false);
// Handler
const handleChange = (e) => {
@@ -45,15 +46,17 @@ const SettingsPagespeed = ({
component="h1"
variant="h2"
>
- {t("pageSpeedApiKeyFieldTitle")}
+ {t("settingsPage.pageSpeedSettings.title")}
+
+
+ {t("settingsPage.pageSpeedSettings.description")}
- {t("pageSpeedApiKeyFieldDescription")}
- {(isApiKeySet === false || hasBeenReset === true) && (
+ {(isApiKeySet === false || apiKeyHasBeenReset === true) && (
)}
- {isApiKeySet === true && hasBeenReset === false && (
+ {isApiKeySet === true && apiKeyHasBeenReset === false && (
- {t("pageSpeedApiKeyFieldResetLabel")}
+ {t("settingsPage.pageSpeedSettings.labelApiKeySet")}
{
setApiKey("");
@@ -72,7 +75,7 @@ const SettingsPagespeed = ({
...settingsData,
settings: { ...settingsData.settings, pagespeedApiKey: "" },
});
- setHasBeenReset(true);
+ setApiKeyHasBeenReset(true);
}}
variant="contained"
color="error"
@@ -93,6 +96,9 @@ SettingsPagespeed.propTypes = {
settingsData: PropTypes.object,
setSettingsData: PropTypes.func,
isApiKeySet: PropTypes.bool,
+ setIsApiKeySet: PropTypes.func,
+ apiKeyHasBeenReset: PropTypes.bool,
+ setApiKeyHasBeenReset: PropTypes.func,
};
export default SettingsPagespeed;
diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx
index 333dfd229..740ac3a37 100644
--- a/client/src/Pages/Settings/SettingsStats.jsx
+++ b/client/src/Pages/Settings/SettingsStats.jsx
@@ -29,17 +29,17 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors
variant="h2"
sx={HEADING_SX}
>
- {t("settingsHistoryAndMonitoring")}
+ {t("settingsPage.statsSettings.title")}
- {t("settingsHistoryAndMonitoringDescription")}
+ {t("settingsPage.statsSettings.description")}
- {t("settingsClearAllStats")}
+
+ {t("settingsPage.statsSettings.clearAllStatsDescription")}
+
setIsOpen(true)}
sx={{ mt: theme.spacing(4) }}
>
- {t("settingsClearAllStatsButton")}
+ {t("settingsPage.statsSettings.clearAllStatsButton")}
setIsOpen(false)}
- confirmationButtonLabel={t("settingsClearAllStatsDialogConfirm")}
+ confirmationButtonLabel={t(
+ "settingsPage.statsSettings.clearAllStatsDialogConfirm"
+ )}
onConfirm={() => {
const syntheticEvent = {
target: {
diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx
index 86fc72992..346e4e546 100644
--- a/client/src/Pages/Settings/SettingsTimeZone.jsx
+++ b/client/src/Pages/Settings/SettingsTimeZone.jsx
@@ -19,16 +19,17 @@ const SettingsTimeZone = ({ HEADING_SX, handleChange, timezone }) => {
component="h1"
variant="h2"
>
- {t("settingsGeneralSettings")}
+ {t("settingsPage.timezoneSettings.title")}
- {t("settingsDisplayTimezone")} -{" "}
- {t("settingsDisplayTimezoneDescription")}
+
+ {t("settingsPage.timezoneSettings.description")}
+
{
component="h1"
variant="h2"
>
- {t("settingsAppearance")}
+ {t("settingsPage.uiSettings.title")}
+
+
+ {t("settingsPage.uiSettings.description")}
- {t("settingsAppearanceDescription")}
{
>
({ _id: lang, name: lang.toUpperCase() }))}
diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx
index 4b4cf7683..298dfa821 100644
--- a/client/src/Pages/Settings/SettingsURL.jsx
+++ b/client/src/Pages/Settings/SettingsURL.jsx
@@ -9,24 +9,26 @@ import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
import { useTranslation } from "react-i18next";
-const SettingsURL = ({ HEADING_SX, handleChange, showURL }) => {
+const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {
const { t } = useTranslation();
const theme = useTheme();
return (
- {t("settingsURLTitle")}
- {t("settingsURLDescription")}
+ {t("settingsPage.urlSettings.title")}
+
+ {t("settingsPage.urlSettings.description")}
+
diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx
index 17cffce0d..dae6bd680 100644
--- a/client/src/Pages/Settings/index.jsx
+++ b/client/src/Pages/Settings/index.jsx
@@ -11,64 +11,116 @@ import SettingsEmail from "./SettingsEmail";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "../../Validation/validation";
-import { createToast } from "../../Utils/toastUtils";
import { useState } from "react";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useSelector, useDispatch } from "react-redux";
import { setTimezone, setMode, setLanguage, setShowURL } from "../../Features/UI/uiSlice";
import SettingsStats from "./SettingsStats";
-import {
- deleteMonitorChecksByTeamId,
- addDemoMonitors,
- deleteAllMonitors,
-} from "../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { useFetchSettings, useSaveSettings } from "../../Hooks/useFetchSettings";
-import { UseDeleteMonitorStats } from "../../Hooks/useDeleteMonitorStats";
-import { useIsAdmin } from "../../Hooks/useIsAdmin";
+import { useFetchSettings, useSaveSettings } from "../../Hooks/settingsHooks";
+import { useIsAdmin } from "../../Hooks/useIsAdmin";
+import {
+ useAddDemoMonitors,
+ useDeleteAllMonitors,
+ useDeleteMonitorStats,
+} from "../../Hooks/monitorHooks";
// Constants
const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }];
const Settings = () => {
// Redux state
const { mode, language, timezone, showURL } = useSelector((state) => state.ui);
- const { user } = useSelector((state) => state.auth);
// Local state
const [settingsData, setSettingsData] = useState({});
const [errors, setErrors] = useState({});
+ const [isApiKeySet, setIsApiKeySet] = useState(settingsData?.pagespeedKeySet ?? false);
+ const [apiKeyHasBeenReset, setApiKeyHasBeenReset] = useState(false);
+ const [isEmailPasswordSet, setIsEmailPasswordSet] = useState(
+ settingsData?.emailPasswordSet ?? false
+ );
+ const [emailPasswordHasBeenReset, setEmailPasswordHasBeenReset] = useState(false);
// Network
const [isSettingsLoading, settingsError] = useFetchSettings({
setSettingsData,
+ setIsApiKeySet,
+ setIsEmailPasswordSet,
});
+ const [addDemoMonitors, isAddingDemoMonitors] = useAddDemoMonitors();
- const [isSaving, saveError, saveSettings] = useSaveSettings();
- const [deleteMonitorStats, isDeletingMonitorStats] = UseDeleteMonitorStats();
+ const [isSaving, saveError, saveSettings] = useSaveSettings({
+ setSettingsData,
+ setIsApiKeySet,
+ setApiKeyHasBeenReset,
+ setIsEmailPasswordSet,
+ setEmailPasswordHasBeenReset,
+ });
+ const [deleteAllMonitors, isDeletingMonitors] = useDeleteAllMonitors();
+ const [deleteMonitorStats, isDeletingMonitorStats] = useDeleteMonitorStats();
// Setup
const isAdmin = useIsAdmin();
const theme = useTheme();
const HEADING_SX = { mt: theme.spacing(2), mb: theme.spacing(2) };
- const { t, i18n } = useTranslation();
+ const { t } = useTranslation();
const dispatch = useDispatch();
-
// 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" ||
+ name === "systemEmailSecure" ||
+ name === "systemEmailPool"
+ ) {
+ newValue = checked;
+ }
// Build next state early
const newSettingsData = {
...settingsData,
- settings: { ...settingsData.settings, [name]: value },
+ settings: { ...settingsData.settings, [name]: newValue ?? value },
};
+ if (name === "timezone") {
+ dispatch(setTimezone({ timezone: value }));
+ return;
+ }
+
+ if (name === "mode") {
+ dispatch(setMode(value));
+ return;
+ }
+
+ if (name === "language") {
+ dispatch(setLanguage(value));
+ return;
+ }
+
+ if (name === "deleteStats") {
+ await deleteMonitorStats();
+ return;
+ }
+
+ if (name === "demo") {
+ await addDemoMonitors();
+ return;
+ }
+
+ if (name === "deleteMonitors") {
+ await deleteAllMonitors();
+ return;
+ }
+
// Validate
const { error } = settingsValidation.validate(newSettingsData.settings, {
abortEarly: false,
@@ -83,56 +135,10 @@ const Settings = () => {
setErrors(newErrors);
}
- if (name === "timezone") {
- dispatch(setTimezone({ timezone: value }));
- }
-
- if (name === "mode") {
- dispatch(setMode(value));
- }
-
- if (name === "language") {
- dispatch(setLanguage(value));
- }
-
- if (name === "deleteStats") {
- await deleteMonitorStats({ teamId: user.teamId });
- return;
- }
-
- if (name === "demo") {
- try {
- const action = await dispatch(addDemoMonitors());
- if (addDemoMonitors.fulfilled.match(action)) {
- createToast({ body: t("settingsDemoMonitorsAdded") });
- } else {
- createToast({ body: t("settingsFailedToAddDemoMonitors") });
- }
- } catch (error) {
- createToast({ body: t("settingsFailedToAddDemoMonitors") });
- }
- return;
- }
-
- if (name === "deleteMonitors") {
- try {
- const action = await dispatch(deleteAllMonitors());
- if (deleteAllMonitors.fulfilled.match(action)) {
- createToast({ body: t("settingsMonitorsDeleted") });
- } else {
- createToast({ body: t("settingsFailedToDeleteMonitors") });
- }
- } catch (error) {
- createToast({ body: t("settingsFailedToDeleteMonitors") });
- }
- return;
- }
-
setSettingsData(newSettingsData);
};
const handleSave = () => {
- console.log(settingsData.settings);
const { error } = settingsValidation.validate(settingsData.settings, {
abortEarly: false,
});
@@ -151,7 +157,7 @@ const Settings = () => {
return (
- Settings
+ {t("settingsPage.title")}
{
HEADING_SX={HEADING_SX}
settingsData={settingsData}
setSettingsData={setSettingsData}
- isApiKeySet={settingsData?.pagespeedKeySet ?? false}
+ isApiKeySet={isApiKeySet}
+ apiKeyHasBeenReset={apiKeyHasBeenReset}
+ setApiKeyHasBeenReset={setApiKeyHasBeenReset}
/>
{
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
handleChange={handleChange}
- isLoading={isSettingsLoading || isSaving || isDeletingMonitorStats}
+ isLoading={
+ isSettingsLoading || isSaving || isDeletingMonitorStats || isAddingDemoMonitors
+ }
/>
{
handleChange={handleChange}
settingsData={settingsData}
setSettingsData={setSettingsData}
- isPasswordSet={settingsData?.emailPasswordSet ?? false}
+ isEmailPasswordSet={isEmailPasswordSet}
+ emailPasswordHasBeenReset={emailPasswordHasBeenReset}
+ setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
/>
+
0}
variant="contained"
color="accent"
- sx={{ px: theme.spacing(12), mt: theme.spacing(20) }}
+ sx={{ px: theme.spacing(12), py: theme.spacing(8) }}
onClick={handleSave}
>
- {t("settingsSave")}
+ {t("settingsPage.saveButtonLabel")}
diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
index 7423b05bd..327a2fb0a 100644
--- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
+++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx
@@ -90,6 +90,13 @@ const Content = ({
isChecked={form.showUptimePercentage}
onChange={handleFormChange}
/>
+
diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx
index 8407955a6..a2ac5f55d 100644
--- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx
+++ b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx
@@ -1,17 +1,14 @@
import { useState } from "react";
import { networkService } from "../../../../main";
-import { useSelector } from "react-redux";
import { createToast } from "../../../../Utils/toastUtils";
const useCreateStatusPage = (isCreate, url) => {
- const { user } = useSelector((state) => state.auth);
-
const [isLoading, setIsLoading] = useState(false);
const [networkError, setNetworkError] = useState(false);
const createStatusPage = async ({ form }) => {
setIsLoading(true);
try {
- await networkService.createStatusPage({ user, form, isCreate, url });
+ await networkService.createStatusPage({ form, isCreate, url });
return true;
} catch (error) {
setNetworkError(true);
diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx
index e3c4ae2fc..f380bed9a 100644
--- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx
+++ b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx
@@ -13,9 +13,8 @@ const useMonitorsFetch = () => {
const fetchMonitors = async () => {
try {
const response = await networkService.getMonitorsByTeamId({
- teamId: user.teamId,
limit: null, // donot return any checks for the monitors
- types: ["http", "ping"], // status page is available for uptime and ping monitors
+ types: ["http", "ping", "port"], // status page is available for uptime, ping, and port monitors
});
setMonitors(response.data.data.monitors);
} catch (error) {
diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx
index d1bb8d196..63c248786 100644
--- a/client/src/Pages/StatusPage/Create/index.jsx
+++ b/client/src/Pages/StatusPage/Create/index.jsx
@@ -20,7 +20,7 @@ const TAB_LIST = ["General settings", "Contents"];
const ERROR_TAB_MAPPING = [
["companyName", "url", "timezone", "color", "isPublished", "logo"],
- ["monitors", "showUptimePercentage", "showCharts"],
+ ["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"],
];
const CreateStatusPage = () => {
@@ -39,6 +39,7 @@ const CreateStatusPage = () => {
monitors: [],
showCharts: true,
showUptimePercentage: true,
+ showAdminLoginLink: false,
});
const [errors, setErrors] = useState({});
const [selectedMonitors, setSelectedMonitors] = useState([]);
@@ -195,6 +196,7 @@ const CreateStatusPage = () => {
logo: newLogo,
showCharts: statusPage?.showCharts ?? true,
showUptimePercentage: statusPage?.showUptimePercentage ?? true,
+ showAdminLoginLink: statusPage?.showAdminLoginLink ?? false,
};
});
setSelectedMonitors(statusPageMonitors);
@@ -208,9 +210,9 @@ const CreateStatusPage = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
diff --git a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
index 7546e8725..dcbf96399 100644
--- a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
+++ b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx
@@ -22,10 +22,6 @@ const Controls = ({ isDeleteOpen, setIsDeleteOpen, isDeleting, url, type }) => {
return null;
}
- if (currentPath.startsWith("/status/distributed/public")) {
- return null;
- }
-
return (
{
onClick={() => {
if (type === "uptime") {
navigate(`/status/uptime/configure/${url}`);
- } else {
- navigate(`/status/distributed/configure/${url}`);
}
}}
sx={{
diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
index 089ab1384..1097cf1da 100644
--- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
+++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx
@@ -6,14 +6,14 @@ import { StatusLabel } from "../../../../../Components/Label";
//Utils
import { useTheme } from "@mui/material/styles";
-import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import PropTypes from "prop-types";
import { useSelector } from "react-redux";
const MonitorsList = ({ isLoading = false, shouldRender = true, monitors = [] }) => {
const theme = useTheme();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const { showURL } = useSelector((state) => state.ui);
diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/StatusPage/Status/index.jsx
index 341c6d731..45822f6c6 100644
--- a/client/src/Pages/StatusPage/Status/index.jsx
+++ b/client/src/Pages/StatusPage/Status/index.jsx
@@ -8,6 +8,7 @@ import StatusBar from "./Components/StatusBar";
import MonitorsList from "./Components/MonitorsList";
import Dialog from "../../../Components/Dialog";
import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx";
+import TextLink from "../../../Components/TextLink";
// Utils
import { useStatusPageFetch } from "./Hooks/useStatusPageFetch";
@@ -30,6 +31,7 @@ const PublicStatus = () => {
const { t } = useTranslation();
const location = useLocation();
const navigate = useNavigate();
+ const isAdmin = useIsAdmin();
const [statusPage, monitors, isLoading, networkError, fetchStatusPage] =
useStatusPageFetch(false, url);
@@ -46,7 +48,7 @@ const PublicStatus = () => {
let link = undefined;
const isPublic = location.pathname.startsWith("/status/uptime/public");
// Public status page
- if (isPublic) {
+ if (isPublic && statusPage && statusPage.showAdminLoginLink === true) {
sx = {
paddingTop: theme.spacing(20),
paddingLeft: "20vw",
@@ -60,6 +62,26 @@ const PublicStatus = () => {
return ;
}
+ if (monitors.length === 0) {
+ return (
+
+
+ {"Theres nothing here yet"}
+
+ {isAdmin && (
+
+ )}
+
+ );
+ }
+
// Error fetching data
if (networkError === true) {
return (
@@ -69,9 +91,9 @@ const PublicStatus = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
index a7b69ded0..04d7a6cfb 100644
--- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
+++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx
@@ -23,10 +23,7 @@ const StatusPagesTable = ({ data }) => {
onClick: (e, row) => {
if (row.isPublished) {
e.stopPropagation();
- const url =
- row.type === "distributed"
- ? `/status/distributed/public/${row.url}`
- : `/status/uptime/public/${row.url}`;
+ const url = `/status/uptime/public/${row.url}`;
window.open(url, "_blank", "noopener,noreferrer");
}
},
@@ -79,11 +76,7 @@ const StatusPagesTable = ({ data }) => {
];
const handleRowClick = (statusPage) => {
- if (statusPage.type === "distributed") {
- navigate(`/status/distributed/${statusPage.url}`);
- } else if (statusPage.type === "uptime") {
- navigate(`/status/uptime/${statusPage.url}`);
- }
+ navigate(`/status/uptime/${statusPage.url}`);
};
return (
diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
index db7a33396..b236368c8 100644
--- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
+++ b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx
@@ -13,9 +13,7 @@ const useStatusPagesFetch = () => {
useEffect(() => {
const fetchStatusPages = async () => {
try {
- const res = await networkService.getStatusPagesByTeamId({
- teamId: user.teamId,
- });
+ const res = await networkService.getStatusPagesByTeamId();
setStatusPages(res?.data?.data);
} catch (error) {
setNetworkError(true);
diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx
index 095023d76..650307bdb 100644
--- a/client/src/Pages/StatusPage/StatusPages/index.jsx
+++ b/client/src/Pages/StatusPage/StatusPages/index.jsx
@@ -32,9 +32,9 @@ const StatusPages = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
diff --git a/client/src/Pages/Uptime/BulkImport/Upload.jsx b/client/src/Pages/Uptime/BulkImport/Upload.jsx
index 261dcffe6..24bc0ec38 100644
--- a/client/src/Pages/Uptime/BulkImport/Upload.jsx
+++ b/client/src/Pages/Uptime/BulkImport/Upload.jsx
@@ -23,7 +23,7 @@ const UploadFile = ({ onFileSelect }) => {
// Basic file validation
if (!selectedFile) return;
- if (!selectedFile.name.endsWith(".csv")) {
+ if (!selectedFile.name.toLowerCase().endsWith(".csv")) {
setError(t("bulkImport.invalidFileType"));
return;
}
diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/Uptime/BulkImport/index.jsx
index 99a6d51d8..9490b4eab 100644
--- a/client/src/Pages/Uptime/BulkImport/index.jsx
+++ b/client/src/Pages/Uptime/BulkImport/index.jsx
@@ -10,9 +10,9 @@ import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox";
import UploadFile from "./Upload";
import { useSelector } from "react-redux";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { Trans, useTranslation } from "react-i18next";
-import { useBulkMonitors } from "../../../Hooks/useBulkMonitors";
+import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks";
const BulkImport = () => {
const theme = useTheme();
@@ -26,7 +26,7 @@ const BulkImport = () => {
{ name: t("bulkImport.title"), path: `/uptime/bulk-import` },
];
- const [createBulkMonitors, hookLoading] = useBulkMonitors();
+ const [createBulkMonitors, hookLoading] = useCreateBulkMonitors();
const handleSubmit = async () => {
if (!selectedFile) {
diff --git a/client/src/Pages/Uptime/Configure/index.jsx b/client/src/Pages/Uptime/Configure/index.jsx
index 395486228..2dacc26ab 100644
--- a/client/src/Pages/Uptime/Configure/index.jsx
+++ b/client/src/Pages/Uptime/Configure/index.jsx
@@ -1,43 +1,38 @@
-import { useNavigate, useParams } from "react-router";
-import { useTheme } from "@emotion/react";
-import { useDispatch, useSelector } from "react-redux";
-import { useEffect, useState } from "react";
-import {
- Box,
- Stack,
- Tooltip,
- Typography,
- Button,
- FormControlLabel,
- Switch,
-} from "@mui/material";
-import { monitorValidation } from "../../../Validation/validation";
-import { createToast } from "../../../Utils/toastUtils";
-import { logger } from "../../../Utils/Logger";
-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,
- pauseUptimeMonitor,
- getUptimeMonitorById,
- deleteUptimeMonitor,
-} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
+import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import PauseIcon from "../../../assets/icons/pause-icon.svg?react";
-import ResumeIcon from "../../../assets/icons/resume-icon.svg?react";
import Select from "../../../Components/Inputs/Select";
-import Checkbox from "../../../Components/Inputs/Checkbox";
-import Breadcrumbs from "../../../Components/Breadcrumbs";
-import PulseDot from "../../../Components/Animated/PulseDot";
-import SkeletonLayout from "./skeleton";
-import "./index.css";
import Dialog from "../../../Components/Dialog";
-import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
-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 { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import {
+ useDeleteMonitor,
+ useUpdateMonitor,
+ usePauseMonitor,
+ useFetchMonitorById,
+} from "../../../Hooks/monitorHooks";
+import NotificationsConfig from "../../../Components/NotificationConfig";
/**
* Parses a URL string and returns a URL object.
@@ -58,139 +53,49 @@ const parseUrl = (url) => {
* @component
*/
const Configure = () => {
- const MS_PER_MINUTE = 60000;
- const navigate = useNavigate();
- const theme = useTheme();
- const dispatch = useDispatch();
- const { user } = useSelector((state) => state.auth);
- const { isLoading } = useSelector((state) => state.uptimeMonitors);
- const [monitor, setMonitor] = useState({});
- const [errors, setErrors] = useState({});
const { monitorId } = useParams();
- const idMap = {
- "monitor-url": "url",
- "monitor-name": "name",
- "monitor-checks-http": "type",
- "monitor-checks-ping": "type",
- "notify-email-default": "notification-email",
+
+ // Local state
+ const [form, setForm] = useState({
+ ignoreTlsErrors: false,
+ interval: 60000,
+ matchMethod: "equal",
+ expectedValue: "",
+ jsonPath: "",
+ notifications: [],
+ port: "",
+ type: "http",
+ });
+ const [useAdvancedMatching, setUseAdvancedMatching] = useState(false);
+ const [updateTrigger, setUpdateTrigger] = useState(false);
+ const [isOpen, setIsOpen] = useState(false);
+ const [errors, setErrors] = useState({});
+
+ const triggerUpdate = () => {
+ setUpdateTrigger(!updateTrigger);
};
+ // Network
+ const [notifications, notificationsAreLoading, notificationsError] =
+ useGetNotificationsByTeamId();
+ 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 theme = useTheme();
+
const matchMethodOptions = [
{ _id: "equal", name: "Equal" },
{ _id: "include", name: "Include" },
{ _id: "regex", name: "Regex" },
];
- const expectedValuePlaceholders = {
- regex: "^(success|ok)$",
- equal: "success",
- include: "ok",
- };
-
- const [trigger, setTrigger] = useState(false);
- const triggerUpdate = () => {
- setTrigger(!trigger);
- };
- const [pauseMonitor, isPausing, error] = usePauseMonitor({
- monitorId: monitor?._id,
- triggerUpdate,
- });
-
- useEffect(() => {
- const fetchMonitor = async () => {
- try {
- 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) {
- logger.error("Error fetching monitor of id: " + monitorId);
- navigate("/not-found", { replace: true });
- }
- };
- fetchMonitor();
- }, [monitorId, navigate, trigger]);
-
- const handleChange = (event, name) => {
- let { checked, value, id } = event.target;
- if (!name) name = idMap[id];
-
- if (name.includes("notification-")) {
- name = name.replace("notification-", "");
- let hasNotif = monitor.notifications.some(
- (notification) => notification.type === name
- );
- setMonitor((prev) => {
- const notifs = [...prev.notifications];
- if (hasNotif) {
- return {
- ...prev,
- notifications: notifs.filter((notif) => notif.type !== name),
- };
- } else {
- return {
- ...prev,
- notifications: [
- ...notifs,
- name === "email"
- ? { type: name, address: value }
- : // TODO - phone number
- { type: name, phone: value },
- ],
- };
- }
- });
- } else {
- if (name === "interval") {
- value = value * MS_PER_MINUTE;
- }
- if (name === "ignoreTlsErrors") {
- value = checked;
- }
- setMonitor((prev) => ({
- ...prev,
- [name]: value,
- }));
-
- const validation = monitorValidation.validate(
- { [name]: value },
- { abortEarly: false }
- );
-
- setErrors((prev) => {
- const updatedErrors = { ...prev };
-
- if (validation.error) updatedErrors[name] = validation.error.details[0].message;
- else delete updatedErrors[name];
- return updatedErrors;
- });
- }
- };
-
- const handleSubmit = async (event) => {
- event.preventDefault();
- const action = await dispatch(updateUptimeMonitor({ monitor: monitor }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor updated successfully!" });
- } else {
- createToast({ body: "Failed to update monitor." });
- }
- };
-
- const [isOpen, setIsOpen] = useState(false);
- 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." });
- }
- };
-
const frequencies = [
{ _id: 1, name: "1 minute" },
{ _id: 2, name: "2 minutes" },
@@ -199,395 +104,411 @@ const Configure = () => {
{ _id: 5, name: "5 minutes" },
];
+ const expectedValuePlaceholders = {
+ regex: "^(success|ok)$",
+ equal: "success",
+ include: "ok",
+ };
+
+ // Handlers
+ const handlePause = async () => {
+ const res = await pauseMonitor({ monitorId: form?._id, triggerUpdate });
+ if (typeof res !== "undefined") {
+ triggerUpdate();
+ }
+ };
+
+ const handleRemove = async (event) => {
+ event.preventDefault();
+ await deleteMonitor({ monitor: form, redirect: "/uptime" });
+ };
+
+ const onChange = (event) => {
+ let { name, value, checked } = event.target;
+
+ if (name === "ignoreTlsErrors") {
+ value = checked;
+ }
+
+ if (name === "useAdvancedMatching") {
+ setForm((prevForm) => {
+ return {
+ ...prevForm,
+ matchMethod: "equal",
+ };
+ });
+ setUseAdvancedMatching(!useAdvancedMatching);
+ return;
+ }
+
+ if (name === "interval") {
+ value = value * MS_PER_MINUTE;
+ }
+ setForm({ ...form, [name]: value });
+
+ const validation = monitorValidation.validate(
+ { [name]: value },
+ { abortEarly: false }
+ );
+
+ setErrors((prev) => {
+ const updatedErrors = { ...prev };
+ if (validation.error) updatedErrors[name] = validation.error.details[0].message;
+ else delete updatedErrors[name];
+ return updatedErrors;
+ });
+ };
+
+ const onSubmit = async (e) => {
+ e.preventDefault();
+
+ const toSubmit = {
+ _id: form._id,
+ url: form.url,
+ name: form.name,
+ type: form.type,
+ matchMethod: form.matchMethod,
+ expectedValue: form.expectedValue,
+ jsonPath: form.jsonPath,
+ interval: form.interval,
+ teamId: form.teamId,
+ userId: form.userId,
+ port: form.port,
+ ignoreTlsErrors: form.ignoreTlsErrors,
+ };
+
+ if (!useAdvancedMatching) {
+ toSubmit.matchMethod = "";
+ toSubmit.expectedValue = "";
+ toSubmit.jsonPath = "";
+ }
+
+ const validation = monitorValidation.validate(toSubmit, {
+ abortEarly: false,
+ });
+
+ if (validation.error) {
+ const newErrors = {};
+ validation.error.details.forEach((err) => {
+ newErrors[err.path[0]] = err.message;
+ });
+ setErrors(newErrors);
+ createToast({ body: "Please check the form for errors." });
+ return;
+ }
+
+ toSubmit.notifications = form.notifications;
+ await updateMonitor({ monitor: toSubmit, redirect: "/uptime" });
+ };
+
// Parse the URL
- const parsedUrl = parseUrl(monitor?.url);
+ const parsedUrl = parseUrl(form?.url);
const protocol = parsedUrl?.protocol?.replace(":", "") || "";
- // Notification modal state
- const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
-
- const handleOpenNotificationModal = () => {
- setIsNotificationModalOpen(true);
- };
-
- const handleClosenNotificationModal = () => {
- setIsNotificationModalOpen(false);
- };
-
const { determineState, statusColor } = useMonitorUtils();
const { t } = useTranslation();
return (
-
- {Object.keys(monitor).length === 0 ? (
-
- ) : (
- <>
-
-
+
+
+
+
+
+
+ {form.name}
+
-
-
- {monitor.name}
-
-
-
-
-
-
-
-
- {monitor.url?.replace(/^https?:\/\//, "") || "..."}
-
-
- {t("editing")}
-
-
-
-
- :
- }
- onClick={() => {
- pauseMonitor();
- }}
- >
- {monitor?.isActive ? t("pause") : t("resume")}
-
- setIsOpen(true)}
- >
- {t("remove")}
-
-
-
-
-
-
- {t("settingsGeneralSettings")}
-
-
- {t("distributedUptimeCreateSelectURL")}
-
-
-
-
- )
- }
- id="monitor-url"
- label={t("urlMonitor")}
- placeholder="google.com"
- value={parsedUrl?.host || monitor?.url || ""}
- disabled={true}
- />
- handleChange(event, "port")}
- error={errors["port"] ? true : false}
- helperText={errors["port"]}
- hidden={monitor.type !== "port"}
- />
-
-
-
-
-
-
- {t("distributedUptimeCreateIncidentNotification")}
-
-
- {t("distributedUptimeCreateIncidentDescription")}
-
-
-
- {t("whenNewIncident")}
- {/* {Leaving components commented for future funtionality implimentation} */}
- {/* logger.warn("disabled")}
- isDisabled={true}
- /> */}
- notification.type === "email"
- ) || false
- }
- value={user?.email}
- onChange={(event) => handleChange(event)}
- />
-
-
- {t("notifications.integrationButton")}
-
+
+
- {/* logger.warn("disabled")}
- isDisabled={true}
- /> */}
- {/* {monitor?.notifications?.some(
- (notification) => notification.type === "emails"
- ) ? (
-
- logger.warn("disabled")}
- />
-
- You can separate multiple emails with a comma
-
-
- ) : (
- ""
- )} */}
-
-
-
-
-
- {t("ignoreTLSError")}
-
- {t("ignoreTLSErrorDescription")}
-
-
- handleChange(event, "ignoreTlsErrors")}
- sx={{ mr: theme.spacing(2) }}
- />
- }
- label={t("tlsErrorIgnored")}
- />
-
-
-
-
-
- {t("distributedUptimeCreateAdvancedSettings")}
-
-
-
- handleChange(event, "interval")}
- items={frequencies}
- />
- {monitor.type === "http" && (
- <>
- handleChange(event, "matchMethod")}
- items={matchMethodOptions}
- />
-
- handleChange(event, "expectedValue")}
- error={errors["expectedValue"] ? true : false}
- helperText={errors["expectedValue"]}
- />
-
- {t("uptimeCreate")}
-
-
-
- handleChange(event, "jsonPath")}
- error={errors["jsonPath"] ? true : false}
- helperText={errors["jsonPath"]}
- />
-
- {t("uptimeCreateJsonPath")}
-
- jmespath.org
-
- {t("uptimeCreateJsonPathQuery")}
-
-
- >
- )}
-
-
-
-
+
- {t("settingsSave")}
-
+ {form.url?.replace(/^https?:\/\//, "") || "..."}
+
+
+ {t("editing")}
+
+
+
+ :
+ }
+ onClick={handlePause}
+ >
+ {form?.isActive ? t("pause") : t("resume")}
+
+ setIsOpen(true)}
+ >
+ {t("remove")}
+
+
+
+
+
+
+ {t("settingsGeneralSettings")}
+
+ {t("distributedUptimeCreateSelectURL")}
+
+
+
+ }
+ id="monitor-url"
+ label={t("urlMonitor")}
+ placeholder="google.com"
+ value={parsedUrl?.host || form?.url || ""}
+ disabled={true}
+ />
+
+
- >
- )}
-
+
+
+
+ {t("notificationConfig.title")}
+ {t("notificationConfig.description")}
+
+
+
+
+
+
+ {t("ignoreTLSError")}
+
+ {t("ignoreTLSErrorDescription")}
+
+
+
+ }
+ label={t("tlsErrorIgnored")}
+ />
+
+
+
+
+
+ {t("distributedUptimeCreateAdvancedSettings")}
+
+
+
+
+
+ {form.type === "http" && useAdvancedMatching && (
+ <>
+
+
+
+
+ {t("uptimeCreate")}
+
+
+
+
+
+ {t("uptimeCreateJsonPath")}
+
+ jmespath.org
+
+ {t("uptimeCreateJsonPathQuery")}
+
+
+ >
+ )}
+
+
+
+
+ {t("settingsSave")}
+
+
+
{
onConfirm={handleRemove}
isLoading={isLoading}
/>
-
-
);
};
diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx
index 65a025b83..3db73481a 100644
--- a/client/src/Pages/Uptime/Create/index.jsx
+++ b/client/src/Pages/Uptime/Create/index.jsx
@@ -1,32 +1,52 @@
-// React, Redux, Router
-import { useTheme } from "@emotion/react";
-import { useNavigate, useParams } from "react-router-dom";
-import { useEffect } from "react";
-import { useState } from "react";
-import { useSelector, useDispatch } from "react-redux";
-import { useTranslation } from "react-i18next";
-
-// Utility and Network
-import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { monitorValidation } from "../../../Validation/validation";
-import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
-import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
-// MUI
-import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
-import Switch from "@mui/material/Switch";
-import FormControlLabel from "@mui/material/FormControlLabel";
-
//Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import TextInput from "../../../Components/Inputs/TextInput";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments";
-import { createToast } from "../../../Utils/toastUtils";
import Radio from "../../../Components/Inputs/Radio";
-import Checkbox from "../../../Components/Inputs/Checkbox";
import Select from "../../../Components/Inputs/Select";
import ConfigBox from "../../../Components/ConfigBox";
-import NotificationIntegrationModal from "../../../Components/NotificationIntegrationModal/Components/NotificationIntegrationModal";
+import NotificationsConfig from "../../../Components/NotificationConfig";
+import Button from "@mui/material/Button";
+import ButtonGroup from "@mui/material/ButtonGroup";
+import Box from "@mui/material/Box";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
+import Switch from "@mui/material/Switch";
+import FormControlLabel from "@mui/material/FormControlLabel";
+import Checkbox from "../../../Components/Inputs/Checkbox";
+
+// Utils
+import { useTheme } from "@emotion/react";
+import { useState } from "react";
+import { useTranslation } from "react-i18next";
+import { monitorValidation } from "../../../Validation/validation";
+import { createToast } from "../../../Utils/toastUtils";
+import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
+import { useCreateMonitor } from "../../../Hooks/monitorHooks";
+
const CreateMonitor = () => {
+ // Local state
+ const [errors, setErrors] = useState({});
+ const [https, setHttps] = useState(true);
+ const [useAdvancedMatching, setUseAdvancedMatching] = useState(false);
+ const [monitor, setMonitor] = useState({
+ url: "",
+ name: "",
+ type: "http",
+ matchMethod: "equal",
+ expectedValue: "",
+ jsonPath: "",
+ notifications: [],
+ interval: 1,
+ ignoreTlsErrors: false,
+ });
+
+ // Setup
+ const theme = useTheme();
+ const { t } = useTranslation();
+ const [notifications, notificationsAreLoading, error] = useGetNotificationsByTeamId();
+ const [createMonitor, isCreating] = useCreateMonitor();
+
const MS_PER_MINUTE = 60000;
const SELECT_VALUES = [
{ _id: 1, name: "1 minute" },
@@ -71,37 +91,19 @@ const CreateMonitor = () => {
},
};
- const { user } = useSelector((state) => state.auth);
- const { isLoading } = useSelector((state) => state.uptimeMonitors);
- const dispatch = useDispatch();
- const navigate = useNavigate();
- const theme = useTheme();
- const { monitorId } = useParams();
- const crumbs = [
+ const BREADCRUMBS = [
{ name: "uptime", path: "/uptime" },
{ name: "create", path: `/uptime/create` },
];
- // State
- const [isNotificationModalOpen, setIsNotificationModalOpen] = useState(false);
+ // Handlers
- const handleOpenNotificationModal = () => {
- setIsNotificationModalOpen(true);
- };
- const [errors, setErrors] = useState({});
- const [https, setHttps] = useState(true);
- const [monitor, setMonitor] = useState({
- url: "",
- name: "",
- type: "http",
- ignoreTlsErrors: false,
- notifications: [],
- interval: 1,
- });
-
- const handleCreateMonitor = async (event) => {
+ const onSubmit = async (event) => {
event.preventDefault();
+ const { notifications, ...rest } = monitor;
+
let form = {
+ ...rest,
url:
//prepending protocol for url
monitor.type === "http"
@@ -110,14 +112,14 @@ const CreateMonitor = () => {
port: monitor.type === "port" ? monitor.port : undefined,
name: monitor.name || monitor.url.substring(0, 50),
type: monitor.type,
- ignoreTlsErrors: monitor.ignoreTlsErrors,
interval: monitor.interval * MS_PER_MINUTE,
};
- if (monitor.type === "http") {
- form.expectedValue = monitor.expectedValue;
- form.jsonPath = monitor.jsonPath;
- form.matchMethod = monitor.matchMethod;
+ // If not using advanced matching, remove advanced settings
+ if (!useAdvancedMatching) {
+ form.matchMethod = undefined;
+ form.expectedValue = undefined;
+ form.jsonPath = undefined;
}
const { error } = monitorValidation.validate(form, {
@@ -137,128 +139,73 @@ const CreateMonitor = () => {
form = {
...form,
description: monitor.name || monitor.url,
- teamId: user.teamId,
- userId: user._id,
notifications: monitor.notifications,
};
- const action = await dispatch(createUptimeMonitor({ monitor: form }));
- if (action.meta.requestStatus === "fulfilled") {
- createToast({ body: "Monitor created successfully!" });
- navigate("/uptime");
- } else {
- createToast({ body: "Failed to create monitor." });
- }
+
+ await createMonitor({ monitor: form, redirect: "/uptime" });
};
- const handleChange = (event, formName) => {
- const { type, checked, value } = event.target;
+ const onChange = (event) => {
+ const { name, value, checked } = event.target;
- const newVal = type === "checkbox" ? checked : value;
-
- const newMonitor = {
- ...monitor,
- [formName]: newVal,
- };
- if (formName === "type") {
- newMonitor.url = "";
+ let newValue = value;
+ if (name === "ignoreTlsErrors") {
+ newValue = checked;
}
- setMonitor(newMonitor);
+
+ if (name === "useAdvancedMatching") {
+ setUseAdvancedMatching(checked);
+ return;
+ }
+
+ const updatedMonitor = {
+ ...monitor,
+ [name]: newValue,
+ };
+
+ setMonitor(updatedMonitor);
const { error } = monitorValidation.validate(
- { type: monitor.type, [formName]: newVal },
+ { type: monitor.type, [name]: newValue },
{ abortEarly: false }
);
+
setErrors((prev) => ({
...prev,
- url: undefined,
- ...(error ? { [formName]: error.details[0].message } : { [formName]: undefined }),
+ ...(error ? { [name]: error.details[0].message } : { [name]: undefined }),
}));
};
- const handleNotifications = (event, type) => {
- const { value } = event.target;
- let notifications = [...monitor.notifications];
- const notificationExists = notifications.some((notification) => {
- if (notification.type === type && notification.address === value) {
- return true;
- }
- return false;
- });
- if (notificationExists) {
- notifications = notifications.filter((notification) => {
- if (notification.type === type && notification.address === value) {
- return false;
- }
- return true;
- });
- } else {
- notifications.push({ type, address: value });
- }
-
- setMonitor((prev) => ({
- ...prev,
- notifications,
- }));
- };
-
- useEffect(() => {
- const fetchMonitor = async () => {
- if (monitorId) {
- const action = await dispatch(getUptimeMonitorById({ monitorId }));
-
- if (action.payload.success) {
- const data = action.payload.data;
- const { name, ...rest } = data; //data.name is read-only
- if (rest.type === "http") {
- const url = new URL(rest.url);
- rest.url = url.host;
- }
- rest.name = `${name} (Clone)`;
- rest.interval /= MS_PER_MINUTE;
- setMonitor({
- ...rest,
- });
- } else {
- navigate("/not-found", { replace: true });
- createToast({
- body: "There was an error cloning the monitor.",
- });
- }
- }
- };
- fetchMonitor();
- }, [monitorId, dispatch, navigate]);
-
- const { t } = useTranslation();
-
return (
-
-
-
+
+
+
-
- {t("createYour")}{" "}
-
-
- {t("monitor")}
-
+ {t("createYour")}{" "}
+
+ {t("monitor")}
+
+
+
{
handleChange(event, "type")}
+ onChange={onChange}
/>
{monitor.type === "http" ? (
@@ -304,31 +251,31 @@ const CreateMonitor = () => {
)}
handleChange(event, "type")}
+ onChange={onChange}
/>
handleChange(event, "type")}
+ onChange={onChange}
/>
handleChange(event, "type")}
+ onChange={onChange}
/>
{errors["type"] ? (
@@ -353,12 +300,14 @@ const CreateMonitor = () => {
>
{t("settingsGeneralSettings")}
- {t("uptimeCreateSelectURL")}
+
+ {t(`uptimeGeneralInstructions.${monitor.type}`)}
+
: null
}
@@ -366,29 +315,29 @@ const CreateMonitor = () => {
https={https}
placeholder={monitorTypeMaps[monitor.type].placeholder || ""}
value={monitor.url}
- onChange={(event) => handleChange(event, "url")}
+ onChange={onChange}
error={errors["url"] ? true : false}
helperText={errors["url"]}
/>
handleChange(event, "port")}
+ onChange={onChange}
error={errors["port"] ? true : false}
helperText={errors["port"]}
hidden={monitor.type !== "port"}
/>
handleChange(event, "name")}
+ onChange={onChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
@@ -396,37 +345,13 @@ const CreateMonitor = () => {
-
- {t("distributedUptimeCreateIncidentNotification")}
-
-
- {t("distributedUptimeCreateIncidentDescription")}
-
+ {t("notificationConfig.title")}
+ {t("notificationConfig.description")}
-
- notification.type === "email"
- )}
- value={user?.email}
- onChange={(event) => handleNotifications(event, "email")}
- />
-
-
-
- {t("notifications.integrationButton")}
-
-
-
+
@@ -443,9 +368,9 @@ const CreateMonitor = () => {
sx={{ marginLeft: 0 }}
control={
handleChange(event, "ignoreTlsErrors")}
+ onChange={onChange}
sx={{ mr: theme.spacing(2) }}
/>
}
@@ -464,32 +389,38 @@ const CreateMonitor = () => {
handleChange(event, "interval")}
+ onChange={onChange}
items={SELECT_VALUES}
/>
- {monitor.type === "http" && (
+
+ {monitor.type === "http" && useAdvancedMatching && (
<>
handleChange(event, "matchMethod")}
+ onChange={onChange}
items={matchMethodOptions}
/>
handleChange(event, "expectedValue")}
+ onChange={onChange}
error={errors["expectedValue"] ? true : false}
helperText={errors["expectedValue"]}
/>
@@ -503,13 +434,13 @@ const CreateMonitor = () => {
handleChange(event, "jsonPath")}
+ onChange={onChange}
error={errors["jsonPath"] ? true : false}
helperText={errors["jsonPath"]}
/>
@@ -539,24 +470,17 @@ const CreateMonitor = () => {
justifyContent="flex-end"
>
value === undefined)}
- loading={isLoading}
+ loading={isCreating}
>
{t("createMonitor")}
-
- setIsNotificationModalOpen(false)}
- monitor={monitor}
- setMonitor={setMonitor}
- />
-
+
);
};
diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
index ddc7e223b..abeb251fe 100644
--- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
+++ b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { getHumanReadableDuration } from "../../../../../Utils/timeUtils";
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
-import useUtils from "../../../Monitors/Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
const UptimeStatusBoxes = ({
isLoading = false,
@@ -14,7 +14,7 @@ const UptimeStatusBoxes = ({
certificateExpiry,
}) => {
const theme = useTheme();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
// Determine time since last failure
const timeOfLastFailure = monitorStats?.timeOfLastFailure;
@@ -24,30 +24,22 @@ const UptimeStatusBoxes = ({
const timeOfLastCheck = monitorStats?.lastCheckTimestamp;
const timeSinceLastCheck = Date.now() - timeOfLastCheck;
- const { time: streakTime, units: streakUnits } =
- getHumanReadableDuration(timeSinceLastFailure);
+ const streakTime = getHumanReadableDuration(timeSinceLastFailure);
- const { time: lastCheckTime, units: lastCheckUnits } =
- getHumanReadableDuration(timeSinceLastCheck);
+ const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck);
return (
- {streakTime}
- {streakUnits}
- >
- }
+ subHeading={streakTime}
/>
{lastCheckTime}
- {lastCheckUnits}
{"ago"}
>
}
@@ -80,7 +72,7 @@ const UptimeStatusBoxes = ({
};
UptimeStatusBoxes.propTypes = {
- shouldRender: PropTypes.bool,
+ isLoading: PropTypes.bool,
monitor: PropTypes.object,
monitorStats: PropTypes.object,
certificateExpiry: PropTypes.string,
diff --git a/client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
deleted file mode 100644
index 586cc8359..000000000
--- a/client/src/Pages/Uptime/Details/Hooks/useChecksFetch.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-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,
-}) => {
- const [checks, setChecks] = useState(undefined);
- const [checksCount, setChecksCount] = useState(undefined);
- const [isLoading, setIsLoading] = useState(false);
- const [networkError, setNetworkError] = useState(false);
-
- useEffect(() => {
- if (!monitorType) {
- return;
- }
-
- const fetchChecks = async () => {
- try {
- setIsLoading(true);
- const res = await networkService.getChecksByMonitor({
- monitorId: monitorId,
- type: monitorType,
- sortOrder: "desc",
- limit: null,
- dateRange: dateRange,
- filter: null,
- page: page,
- rowsPerPage: rowsPerPage,
- });
- setChecks(res.data.data.checks);
- setChecksCount(res.data.data.checksCount);
- } catch (error) {
- setNetworkError(true);
- createToast({ body: error.message });
- } finally {
- setIsLoading(false);
- }
- };
- fetchChecks();
- }, [monitorId, monitorType, dateRange, page, rowsPerPage]);
-
- return [checks, checksCount, isLoading, networkError];
-};
-
-export default useChecksFetch;
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/Details/index.jsx b/client/src/Pages/Uptime/Details/index.jsx
index 5ad8bd61b..c72a5756f 100644
--- a/client/src/Pages/Uptime/Details/index.jsx
+++ b/client/src/Pages/Uptime/Details/index.jsx
@@ -1,15 +1,14 @@
// Components
import Breadcrumbs from "../../../Components/Breadcrumbs";
import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader";
-import MonitorStatusHeader from "../../../Components/MonitorStatusHeader";
import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader";
import ChartBoxes from "./Components/ChartBoxes";
import ResponseTimeChart from "./Components/Charts/ResponseTimeChart";
import ResponseTable from "./Components/ResponseTable";
import UptimeStatusBoxes from "./Components/UptimeStatusBoxes";
import GenericFallback from "../../../Components/GenericFallback";
-// MUI Components
-import { Stack, Typography } from "@mui/material";
+import Stack from "@mui/material/Stack";
+import Typography from "@mui/material/Typography";
// Utils
import { useState } from "react";
@@ -17,9 +16,9 @@ import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useTheme } from "@emotion/react";
import { useIsAdmin } from "../../../Hooks/useIsAdmin";
-import useFetchUptimeMonitorDetails from "../../../Hooks/useFetchUptimeMonitorDetails";
+import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks";
import useCertificateFetch from "./Hooks/useCertificateFetch";
-import useChecksFetch from "./Hooks/useChecksFetch";
+import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks";
import { useTranslation } from "react-i18next";
// Constants
@@ -50,7 +49,7 @@ const UptimeDetails = () => {
const { t } = useTranslation();
const [monitorData, monitorStats, monitorIsLoading, monitorNetworkError] =
- useFetchUptimeMonitorDetails({
+ useFetchUptimeMonitorById({
monitorId,
dateRange,
trigger,
@@ -66,13 +65,18 @@ const UptimeDetails = () => {
});
const monitorType = monitor?.type;
- const [checks, checksCount, checksAreLoading, checksNetworkError] = useChecksFetch({
- monitorId,
- monitorType,
- dateRange,
- page,
- rowsPerPage,
- });
+
+ const [checks, checksCount, checksAreLoading, checksNetworkError] =
+ useFetchChecksByMonitor({
+ monitorId,
+ type: monitorType,
+ sortOrder: "desc",
+ limit: null,
+ dateRange,
+ filter: null,
+ page,
+ rowsPerPage,
+ });
// Handlers
const triggerUpdate = () => {
@@ -95,9 +99,9 @@ const UptimeDetails = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -107,6 +111,7 @@ const UptimeDetails = () => {
return (
+
{
monitor={monitor}
triggerUpdate={triggerUpdate}
/>
+
+
+
{t("distributedUptimeDetailsNoMonitorHistory")}
diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx
index ba4a139e3..63a40f85a 100644
--- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx
+++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx
@@ -13,7 +13,7 @@ import TableSkeleton from "../../../../../Components/Table/skeleton";
// Utils
import { useTheme } from "@emotion/react";
-import useUtils from "../../Hooks/useUtils";
+import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils";
import { useNavigate } from "react-router-dom";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -58,7 +58,7 @@ const UptimeDataTable = ({
}) => {
// Utils
const navigate = useNavigate();
- const { determineState } = useUtils();
+ const { determineState } = useMonitorUtils();
const theme = useTheme();
const { t } = useTranslation();
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;
diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx
index ce9123816..f3342e101 100644
--- a/client/src/Pages/Uptime/Monitors/index.jsx
+++ b/client/src/Pages/Uptime/Monitors/index.jsx
@@ -28,9 +28,12 @@ import { useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import PropTypes from "prop-types";
-import useFetchMonitorsWithSummary from "../../../Hooks/useFetchMonitorsWithSummary";
-import useFetchMonitorsWithChecks from "../../../Hooks/useFetchMonitorsWithChecks";
+import {
+ useFetchMonitorsWithSummary,
+ useFetchMonitorsWithChecks,
+} from "../../../Hooks/monitorHooks";
import { useTranslation } from "react-i18next";
+
const TYPES = ["http", "ping", "docker", "port"];
const CreateMonitorButton = ({ shouldRender }) => {
// Utils
@@ -61,8 +64,7 @@ CreateMonitorButton.propTypes = {
const UptimeMonitors = () => {
// Redux state
- const { user } = useSelector((state) => state.auth);
- const rowsPerPage = useSelector((state) => state.ui.monitors.rowsPerPage);
+ const rowsPerPage = useSelector((state) => state.ui?.monitors?.rowsPerPage ?? 10);
// Local state
const [search, setSearch] = useState(undefined);
@@ -78,7 +80,6 @@ const UptimeMonitors = () => {
// Utils
const theme = useTheme();
- const navigate = useNavigate();
const isAdmin = useIsAdmin();
const dispatch = useDispatch();
const { t } = useTranslation();
@@ -104,11 +105,8 @@ const UptimeMonitors = () => {
setMonitorUpdateTrigger((prev) => !prev);
}, []);
- const teamId = user.teamId;
-
const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] =
useFetchMonitorsWithSummary({
- teamId,
types: TYPES,
monitorUpdateTrigger,
});
@@ -138,7 +136,6 @@ const UptimeMonitors = () => {
monitorsWithChecksIsLoading,
monitorsWithChecksNetworkError,
] = useFetchMonitorsWithChecks({
- teamId,
types: effectiveTypes,
limit: 25,
page: page,
@@ -158,9 +155,9 @@ const UptimeMonitors = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
- {t("networkError")}
+ {t("common.toasts.networkError")}
- {t("checkConnection")}
+ {t("common.toasts.checkConnection")}
);
}
@@ -204,9 +201,9 @@ const UptimeMonitors = () => {
0 && !monitorsWithSummaryIsLoading}
+ isLoading={monitorsWithSummaryIsLoading}
monitorCount={monitorsSummary?.totalMonitors}
- >
+ />
{
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -99,39 +92,6 @@ const Routes = () => {
path="/uptime/configure/:monitorId/"
element={ }
/>
- {/*
- {" "}
-
- }
- /> */}
-
- {/*
-
-
- }
- />
-
-
-
- }
- /> */}
- {/*
-
-
- }
- /> */}
{
element={ }
/>
- {/*
-
-
- }
- /> */}
-
}
/>
- {/*
-
-
- }
- /> */}
-
}
/>
- {/*
-
-
- }
- /> */}
+ }
+ />
+ }
+ />
}
+ path="notifications/:notificationId"
+ element={ }
/>
+
}
@@ -245,6 +188,15 @@ const Routes = () => {
path="account/team"
element={ }
/>
+
+
+
+
+ }
+ />
{
path="/status/uptime/public/:url"
element={ }
/>
- {/* }
- /> */}
{
- const state = store.getState();
- logLevel = state.settings.logLevel || "debug";
+ logLevel = "debug";
this.updateLogLevel(logLevel);
});
}
diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js
index cf2202c33..714268b0b 100644
--- a/client/src/Utils/NetworkService.js
+++ b/client/src/Utils/NetworkService.js
@@ -3,7 +3,6 @@ import i18next from "i18next";
const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
import { clearAuthState } from "../Features/Auth/authSlice";
-import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
class NetworkService {
constructor(store, dispatch, navigate) {
this.store = store;
@@ -56,7 +55,6 @@ class NetworkService {
if (error.response && error.response.status === 401) {
dispatch(clearAuthState());
- dispatch(clearUptimeMonitorState());
navigate("/login");
} else if (error.request && !error.response) {
return Promise.reject(error);
@@ -137,36 +135,6 @@ class NetworkService {
});
}
- /**
- *
- * ************************************
- * Gets monitors and summary of stats by TeamID
- * ************************************
- *
- * @async
- * @param {Object} config - The configuration object.
- * @param {string} config.teamId - Team ID
- * @param {Array} config.types - Array of monitor types
- * @returns {Promise} The response from the axios POST request.
- */
- async getMonitorsSummaryByTeamId(config) {
- const params = new URLSearchParams();
-
- if (config.types) {
- config.types.forEach((type) => {
- params.append("type", type);
- });
- }
- return this.axiosInstance.get(
- `/monitors/team/summary/${config.teamId}?${params.toString()}`,
- {
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
- }
-
/**
* ************************************
* Get all uptime monitors for a Team
@@ -186,7 +154,7 @@ class NetworkService {
*/
async getMonitorsByTeamId(config) {
- const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config;
+ const { limit, types, page, rowsPerPage, filter, field, order } = config;
const params = new URLSearchParams();
if (limit) params.append("limit", limit);
@@ -201,7 +169,7 @@ class NetworkService {
if (field) params.append("field", field);
if (order) params.append("order", order);
- return this.axiosInstance.get(`/monitors/team/${teamId}?${params.toString()}`, {
+ return this.axiosInstance.get(`/monitors/team?${params.toString()}`, {
headers: {
"Content-Type": "application/json",
},
@@ -304,7 +272,7 @@ class NetworkService {
* @returns {Promise} The response from the axios DELETE request.
*/
async deleteChecksByTeamId(config) {
- return this.axiosInstance.delete(`/checks/team/${config.teamId}`, {
+ return this.axiosInstance.delete(`/checks/team`, {
headers: {
"Content-Type": "application/json",
},
@@ -587,7 +555,7 @@ class NetworkService {
*
*/
- async getChecksByMonitor(config) {
+ getChecksByMonitor = async (config) => {
const params = new URLSearchParams();
if (config.type) params.append("type", config.type);
if (config.sortOrder) params.append("sortOrder", config.sortOrder);
@@ -599,7 +567,7 @@ class NetworkService {
if (config.status !== undefined) params.append("status", config.status);
return this.axiosInstance.get(`/checks/${config.monitorId}?${params.toString()}`);
- }
+ };
/**
* ************************************
@@ -618,7 +586,7 @@ class NetworkService {
* @returns {Promise} The response from the axios GET request.
*
*/
- async getChecksByTeam(config) {
+ getChecksByTeam = async (config) => {
const params = new URLSearchParams();
if (config.sortOrder) params.append("sortOrder", config.sortOrder);
if (config.limit) params.append("limit", config.limit);
@@ -627,8 +595,8 @@ class NetworkService {
if (config.page) params.append("page", config.page);
if (config.rowsPerPage) params.append("rowsPerPage", config.rowsPerPage);
if (config.status !== undefined) params.append("status", config.status);
- return this.axiosInstance.get(`/checks/team/${config.teamId}?${params.toString()}`);
- }
+ return this.axiosInstance.get(`/checks/team?${params.toString()}`);
+ };
/**
* ************************************
@@ -703,18 +671,21 @@ 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",
- },
- }
- );
+ });
+ }
+
+ async testAllNotifications(config) {
+ const { monitorId } = config;
+ return this.axiosInstance.post("/notifications/test/all", {
+ monitorId,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
}
/**
@@ -847,7 +818,7 @@ class NetworkService {
*/
async fetchGithubLatestRelease() {
return this.axiosInstance.get(
- "https://api.github.com/repos/bluewave-labs/bluewave-uptime/releases/latest",
+ "https://api.github.com/repos/bluewave-labs/checkmate/releases/latest",
{
headers: {
Authorization: null, // No authorization header for this request
@@ -856,141 +827,6 @@ class NetworkService {
);
}
- getDistributedUptimeMonitors(config) {
- const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config;
-
- const params = new URLSearchParams();
-
- if (limit) params.append("limit", limit);
- if (types) {
- types.forEach((type) => {
- params.append("type", type);
- });
- }
- if (page) params.append("page", page);
- if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
- if (filter) params.append("filter", filter);
- if (field) params.append("field", field);
- if (order) params.append("order", order);
-
- if (this.eventSource) {
- this.eventSource.close();
- }
-
- const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}/initial?${params.toString()}`;
- return this.axiosInstance.get(url);
- }
-
- subscribeToDistributedUptimeMonitors(config) {
- const {
- teamId,
- onUpdate,
- onError,
- onOpen,
- limit,
- types,
- page,
- rowsPerPage,
- filter,
- field,
- order,
- } = config;
-
- const params = new URLSearchParams();
-
- if (limit) params.append("limit", limit);
- if (types) {
- types.forEach((type) => {
- params.append("type", type);
- });
- }
- if (page) params.append("page", page);
- if (rowsPerPage) params.append("rowsPerPage", rowsPerPage);
- if (filter) params.append("filter", filter);
- if (field) params.append("field", field);
- if (order) params.append("order", order);
-
- if (this.eventSource) {
- this.eventSource.close();
- }
-
- const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/${teamId}?${params.toString()}`;
- this.eventSource = new EventSource(url);
-
- this.eventSource.onopen = () => {
- onOpen?.();
- };
-
- this.eventSource.addEventListener("open", (e) => {});
-
- this.eventSource.onmessage = (event) => {
- const data = JSON.parse(event.data);
- onUpdate(data);
- };
-
- this.eventSource.onerror = (error) => {
- console.error("Monitor stream error:", error);
- onError?.();
- this.eventSource.close();
- };
-
- // Returns a cleanup function
- return () => {
- if (this.eventSource) {
- this.eventSource.close();
- this.eventSource = null;
- }
- return () => {
- console.log("Nothing to cleanup");
- };
- };
- }
-
- getDistributedUptimeDetails(config) {
- const params = new URLSearchParams();
- const { monitorId, dateRange, normalize, isPublic } = config;
- if (dateRange) params.append("dateRange", dateRange);
- if (normalize) params.append("normalize", normalize);
- let url;
- if (isPublic) {
- url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/public/${monitorId}/initial?${params.toString()}`;
- } else {
- url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}/initial?${params.toString()}`;
- }
- return this.axiosInstance.get(url);
- }
-
- subscribeToDistributedUptimeDetails(config) {
- const params = new URLSearchParams();
- const { monitorId, onUpdate, onOpen, onError, dateRange, normalize } = config;
- if (dateRange) params.append("dateRange", dateRange);
- if (normalize) params.append("normalize", normalize);
-
- const url = `${this.axiosInstance.defaults.baseURL}/distributed-uptime/monitors/details/${monitorId}?${params.toString()}`;
- this.eventSource = new EventSource(url);
-
- this.eventSource.onopen = (e) => {
- onOpen?.();
- };
-
- this.eventSource.onmessage = (event) => {
- const data = JSON.parse(event.data);
- onUpdate(data);
- };
-
- this.eventSource.onerror = (error) => {
- console.error("Monitor stream error:", error);
- onError?.();
- this.eventSource.close();
- };
- return () => {
- if (this.eventSource) {
- this.eventSource.close();
- this.eventSource = null;
- }
- };
- }
-
async getStatusPage() {
return this.axiosInstance.get(`/status-page`, {
headers: {
@@ -1010,24 +846,9 @@ class NetworkService {
},
});
}
- async getDistributedStatusPageByUrl(config) {
- const { url, type, timeFrame } = config;
- const params = new URLSearchParams();
- params.append("type", type);
- params.append("timeFrame", timeFrame);
- return this.axiosInstance.get(
- `/status-page/distributed/${url}?${params.toString()}`,
- {
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
- }
async getStatusPagesByTeamId(config) {
- const { teamId } = config;
- return this.axiosInstance.get(`/status-page/team/${teamId}`, {
+ return this.axiosInstance.get(`/status-page/team`, {
headers: {
"Content-Type": "application/json",
},
@@ -1035,11 +856,9 @@ class NetworkService {
}
async createStatusPage(config) {
- const { user, form, isCreate } = config;
+ const { form, isCreate } = config;
const fd = new FormData();
- fd.append("teamId", user.teamId);
- fd.append("userId", user._id);
fd.append("type", form.type);
form.isPublished !== undefined && fd.append("isPublished", form.isPublished);
form.companyName && fd.append("companyName", form.companyName);
@@ -1052,6 +871,9 @@ class NetworkService {
if (form.showUptimePercentage !== undefined) {
fd.append("showUptimePercentage", String(form.showUptimePercentage));
}
+ if (form.showAdminLoginLink !== undefined) {
+ fd.append("showAdminLoginLink", String(form.showAdminLoginLink));
+ }
form.monitors &&
form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
@@ -1107,7 +929,7 @@ class NetworkService {
// Fetch monitors with summary by TeamID
// ************************************
async getMonitorsWithSummaryByTeamId(config) {
- const { teamId, types } = config;
+ const { types } = config;
const params = new URLSearchParams();
if (types) {
@@ -1116,21 +938,18 @@ class NetworkService {
});
}
- return this.axiosInstance.get(
- `/monitors/summary/team/${teamId}?${params.toString()}`,
- {
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
+ return this.axiosInstance.get(`/monitors/summary/team?${params.toString()}`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
}
// ************************************
// Fetch monitors with checks by TeamID
// ************************************
async getMonitorsWithChecksByTeamId(config) {
- const { teamId, limit, types, page, rowsPerPage, filter, field, order } = config;
+ const { limit, types, page, rowsPerPage, filter, field, order } = config;
const params = new URLSearchParams();
if (limit) params.append("limit", limit);
@@ -1145,14 +964,11 @@ class NetworkService {
if (field) params.append("field", field);
if (order) params.append("order", order);
- return this.axiosInstance.get(
- `/monitors/team/${teamId}/with-checks?${params.toString()}`,
- {
- headers: {
- "Content-Type": "application/json",
- },
- }
- );
+ return this.axiosInstance.get(`/monitors/team/with-checks?${params.toString()}`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
}
// ************************************
@@ -1170,10 +986,12 @@ class NetworkService {
systemEmailPort: emailConfig.systemEmailPort,
systemEmailAddress: emailConfig.systemEmailAddress,
systemEmailPassword: emailConfig.systemEmailPassword,
- // Only include these if they are present
- ...(emailConfig.systemEmailConnectionHost && {
- systemEmailConnectionHost: emailConfig.systemEmailConnectionHost,
- }),
+ systemEmailSecure: emailConfig.systemEmailSecure,
+ systemEmailPool: emailConfig.systemEmailPool,
+ systemEmailIgnoreTLS: emailConfig.systemEmailIgnoreTLS,
+ systemEmailRequireTLS: emailConfig.systemEmailRequireTLS,
+ systemEmailRejectUnauthorized: emailConfig.systemEmailRejectUnauthorized,
+ systemEmailTLSServername: emailConfig.systemEmailTLSServername,
...(emailConfig.systemEmailUser && {
systemEmailUser: emailConfig.systemEmailUser,
}),
@@ -1183,6 +1001,57 @@ class NetworkService {
// Fallback to original behavior for backward compatibility
return this.axiosInstance.post(`/settings/test-email`, { to });
}
+
+ async createNotification(config) {
+ const { notification } = config;
+ return this.axiosInstance.post(`/notifications`, notification);
+ }
+
+ async getNotificationsByTeamId(config) {
+ return this.axiosInstance.get(`/notifications/team`);
+ }
+
+ async deleteNotificationById(config) {
+ 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);
+ }
+
+ async getQueueData() {
+ return this.axiosInstance.get(`/queue/all-metrics`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ }
+
+ async flushQueue() {
+ return this.axiosInstance.post(`/queue/flush`);
+ }
+
+ async exportMonitors() {
+ const response = await this.axiosInstance.get("/monitors/export", {
+ responseType: "blob",
+ });
+ return response;
+ }
+
+ async getLogs() {
+ return this.axiosInstance.get(`/logs`, {
+ headers: {
+ "Content-Type": "application/json",
+ },
+ });
+ }
}
export default NetworkService;
diff --git a/client/src/Utils/NetworkServiceProvider.jsx b/client/src/Utils/NetworkServiceProvider.jsx
index 684152e69..73c3a2f94 100644
--- a/client/src/Utils/NetworkServiceProvider.jsx
+++ b/client/src/Utils/NetworkServiceProvider.jsx
@@ -1,5 +1,5 @@
import { useDispatch } from "react-redux";
-import { useNavigate } from "react-router";
+import { useNavigate } from "react-router-dom";
import { setNetworkService } from "./NetworkService";
import NetworkService, { networkService } from "./NetworkService";
import { store } from "../store";
diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js
index 920a55f69..1b905bb79 100644
--- a/client/src/Utils/Theme/constants.js
+++ b/client/src/Utils/Theme/constants.js
@@ -85,6 +85,8 @@ const semanticColors = {
const newColors = {
offWhite: "#FEFEFE",
offBlack: "#131315",
+ gray0: "#FDFDFD",
+ gray10: "#F4F4FF",
gray100: "#F3F3F3",
gray200: "#EFEFEF",
gray500: "#A2A3A3",
@@ -175,6 +177,12 @@ const newSemanticColors = {
dark: newColors.blueGray600,
},
},
+ primaryBackground: {
+ main: {
+ light: newColors.gray0,
+ dark: "#000000",
+ },
+ },
secondary: {
main: {
light: newColors.gray200,
diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js
index 631f056dd..eb827a907 100644
--- a/client/src/Utils/Theme/globalTheme.js
+++ b/client/src/Utils/Theme/globalTheme.js
@@ -36,6 +36,11 @@ const baseTheme = (palette) => ({
color: palette.primary.contrastTextTertiary,
fontWeight: 400,
},
+ label: {
+ fontSize: "var(--env-var-font-size-medium)",
+ color: palette.primary.contrastTextSecondary,
+ fontWeight: 500,
+ },
},
/* TODO change to 4 */
spacing: 2,
@@ -305,7 +310,7 @@ const baseTheme = (palette) => ({
styleOverrides: {
root: ({ theme }) => ({
"& fieldset": {
- borderColor: theme.palette.primary.contrastBorder,
+ borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
@@ -324,18 +329,22 @@ const baseTheme = (palette) => ({
/* 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,
},
@@ -346,6 +355,7 @@ const baseTheme = (palette) => ({
fontSize: "var(--env-var-font-size-medium)",
marginLeft: 0,
},
+
"& .MuiFormHelperText-root.Mui-error": {
opacity: 0.8,
fontSize: "var(--env-var-font-size-medium)",
@@ -355,6 +365,7 @@ const baseTheme = (palette) => ({
}),
},
},
+
MuiOutlinedInput: {
styleOverrides: {
root: {
@@ -365,15 +376,43 @@ 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,
},
},
},
+
+ MuiAutocomplete: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ "& .MuiOutlinedInput-root": {
+ paddingTop: 0,
+ paddingBottom: 0,
+ },
+ "& fieldset": {
+ borderColor: theme.palette.primary.lowContrast,
+ borderRadius: theme.shape.borderRadius,
+ },
+ "& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset":
+ {
+ borderColor: theme.palette.primary.lowContrast,
+ },
+
+ "& .MuiAutocomplete-tag": {
+ color: theme.palette.primary.contrastText,
+ backgroundColor: theme.palette.primary.lowContrast,
+ },
+ "& .MuiChip-deleteIcon": {
+ color: theme.palette.primary.contrastText, // CAIO_REVIEW
+ },
+ }),
+ },
+ },
+
MuiTab: {
styleOverrides: {
root: ({ theme }) => ({
@@ -442,6 +481,8 @@ const baseTheme = (palette) => ({
MuiTabs: {
styleOverrides: {
root: ({ theme }) => ({
+ height: "34px",
+ minHeight: "34px",
display: "inline-flex",
borderRadius: 0,
"& .MuiTabs-indicator": {
@@ -632,6 +673,35 @@ const baseTheme = (palette) => ({
}),
},
},
+ MuiTypography: {
+ variants: [
+ {
+ props: { variant: "monitorName" },
+ style: {
+ fontSize: typographyLevels.xl,
+ color: palette.primary.contrastText,
+ fontWeight: 500,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ maxWidth: "calc((100vw - var(--env-var-width-2)) / 2)",
+ },
+ },
+ {
+ props: { variant: "monitorUrl" },
+ style: {
+ fontSize: typographyLevels.l,
+ color: palette.primary.contrastTextSecondary,
+ fontWeight: "bolder",
+ fontFamily: "monospace",
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ maxWidth: "calc((100vw - var(--env-var-width-2)) / 2)",
+ },
+ },
+ ],
+ },
},
shape: {
borderRadius: 2,
diff --git a/client/src/Utils/i18n.js b/client/src/Utils/i18n.js
index 4468917a2..030091d48 100644
--- a/client/src/Utils/i18n.js
+++ b/client/src/Utils/i18n.js
@@ -24,6 +24,7 @@ i18n.use(initReactI18next).init({
interpolation: {
escapeValue: false,
},
+ returnEmptyString: false,
});
export default i18n;
diff --git a/client/src/Utils/monitorUtils.js b/client/src/Utils/monitorUtils.js
index cc92a86ae..e3645b0d0 100644
--- a/client/src/Utils/monitorUtils.js
+++ b/client/src/Utils/monitorUtils.js
@@ -36,3 +36,12 @@ export const parseDomainName = (url) => {
return url;
};
+
+export const TypeToPathMap = {
+ http: "uptime",
+ port: "uptime",
+ docker: "uptime",
+ ping: "uptime",
+ hardware: "infrastructure",
+ pagespeed: "pagespeed",
+};
diff --git a/client/src/Utils/timeUtils.js b/client/src/Utils/timeUtils.js
index 3a84dfcf4..568b77b14 100644
--- a/client/src/Utils/timeUtils.js
+++ b/client/src/Utils/timeUtils.js
@@ -79,22 +79,35 @@ export const formatDurationSplit = (ms) => {
export const getHumanReadableDuration = (ms) => {
const durationObj = dayjs.duration(ms);
- if (durationObj.asDays() >= 1) {
- const days = Math.floor(durationObj.asDays());
- return { time: days, units: days === 1 ? "day" : "days" };
- } else if (durationObj.asHours() >= 1) {
- const hoursRounded = Math.round(durationObj.asHours() * 10) / 10;
- const hours = Number.isInteger(hoursRounded)
- ? Math.floor(hoursRounded)
- : hoursRounded;
- return { time: hours, units: hours <= 1 ? "hour" : "hours" };
- } else if (durationObj.asMinutes() >= 1) {
- const minutes = Math.floor(durationObj.asMinutes());
- return { time: minutes, units: minutes === 1 ? "minute" : "minutes" };
- } else {
- const seconds = Math.floor(durationObj.asSeconds());
- return { time: seconds, units: seconds === 1 ? "second" : "seconds" };
+
+ const parts = {
+ days: Math.floor(durationObj.asDays()),
+ hours: durationObj.hours(),
+ minutes: durationObj.minutes(),
+ seconds: durationObj.seconds(),
+ };
+
+ const result = [];
+
+ if (parts.days > 0) {
+ result.push(`${parts.days}d`);
}
+ if (parts.hours > 0) {
+ result.push(`${parts.hours}h`);
+ }
+ if (result.length < 2 && parts.minutes > 0) {
+ result.push(`${parts.minutes}m`);
+ }
+ if (result.length < 2 && parts.seconds > 0) {
+ result.push(`${parts.seconds}s`);
+ }
+
+ if (result.length === 0) {
+ // fallback for durations < 1s
+ return "0s";
+ }
+
+ return result.join(" ");
};
export const formatDate = (date, customOptions) => {
diff --git a/client/src/Utils/utils.js b/client/src/Utils/utils.js
index 8bd9c9019..81ec44b06 100644
--- a/client/src/Utils/utils.js
+++ b/client/src/Utils/utils.js
@@ -5,3 +5,11 @@ export const safelyParseFloat = (value) => {
}
return parsedValue;
};
+
+export const formatMonitorUrl = (url, maxLength = 55) => {
+ if (!url) return "";
+ const strippedUrl = url.replace(/^https?:\/\//, "");
+ return strippedUrl.length > maxLength
+ ? `${strippedUrl.slice(0, maxLength)}…`
+ : strippedUrl;
+};
diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js
index 211e62995..d86bbd556 100644
--- a/client/src/Validation/validation.js
+++ b/client/src/Validation/validation.js
@@ -7,27 +7,25 @@ const nameSchema = joi
.string()
.max(50)
.trim()
- .pattern(/^[\p{L}\p{M}''\- ]+$/u)
+ .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
.string()
.max(50)
.trim()
- .pattern(/^[\p{L}\p{M}''\- ]+$/u)
+ .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 passwordSchema = joi
+const newPasswordSchema = joi
.string()
.trim()
.min(8)
@@ -56,34 +54,28 @@ const passwordSchema = 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 credentials = joi.object({
+const newOrChangedCredentials = joi.object({
firstName: nameSchema,
lastName: lastnameSchema,
email: joi
.string()
.trim()
.email({ tlds: { allow: false } })
- .custom((value, helpers) => {
- const lowercasedValue = value.toLowerCase();
- if (value !== lowercasedValue) {
- return helpers.message("Email must be in lowercase");
- }
- return lowercasedValue;
- })
+ .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: passwordSchema,
- newPassword: passwordSchema,
+ password: newPasswordSchema,
+ newPassword: newPasswordSchema,
confirm: joi
.string()
.trim()
@@ -95,15 +87,33 @@ const credentials = 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(),
inviteToken: joi.string().allow(""),
});
+const loginCredentials = joi.object({
+ email: joi
+ .string()
+ .trim()
+ .email({ tlds: { allow: false } })
+ .lowercase()
+ .messages({
+ "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",
+ }),
+});
+
const monitorValidation = joi.object({
+ _id: joi.string(),
+ userId: joi.string(),
+ teamId: joi.string(),
url: joi.when("type", {
is: "docker",
then: joi
@@ -191,9 +201,9 @@ const monitorValidation = joi.object({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
- expectedValue: joi.string().allow(""),
- jsonPath: joi.string().allow(""),
- matchMethod: joi.string(),
+ expectedValue: joi.string().allow(null, ""),
+ jsonPath: joi.string().allow(null, ""),
+ matchMethod: joi.string().allow(null, ""),
});
const imageValidation = joi.object({
@@ -239,7 +249,7 @@ const logoImageValidation = joi
.optional(); // Make entire object optional
const statusPageValidation = joi.object({
- type: joi.string().valid("uptime", "distributed").required(),
+ type: joi.string().valid("uptime").required(),
isPublished: joi.bool(),
companyName: joi
.string()
@@ -267,6 +277,7 @@ const statusPageValidation = joi.object({
logo: logoImageValidation,
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),
+ showAdminLoginLink: joi.boolean(),
});
const settingsValidation = joi.object({
@@ -280,10 +291,16 @@ 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(""),
- systemEmailConnectionHost: joi.string().allow(""),
+ systemEmailConnectionHost: joi.string().allow("").optional(),
+ systemEmailTLSServername: joi.string().allow(""),
+ systemEmailIgnoreTLS: joi.boolean(),
+ systemEmailRequireTLS: joi.boolean(),
+ systemEmailRejectUnauthorized: joi.boolean(),
});
const dayjsValidator = (value, helpers) => {
@@ -385,19 +402,28 @@ const infrastructureMonitorValidation = joi.object({
"number.base": "Frequency must be a number.",
"any.required": "Frequency is required.",
}),
- notifications: joi.array().items(
- joi.object({
- type: joi.string().valid("email").required(),
- address: joi
- .string()
- .email({ tlds: { allow: false } })
- .required(),
- })
- ),
+ notifications: joi.array().items(joi.string()),
+});
+
+const notificationValidation = joi.object({
+ notificationName: joi.string().required().messages({
+ "string.empty": "Notification name is required",
+ "any.required": "Notification name is required",
+ }),
+ address: joi.string().required().messages({
+ "string.empty": "This field cannot be empty",
+ "string.base": "This field must be a string",
+ "any.required": "This field is required",
+ }),
+ type: joi.string().required().messages({
+ "string.empty": "This field is required",
+ "any.required": "This field is required",
+ }),
});
export {
- credentials,
+ newOrChangedCredentials,
+ loginCredentials,
imageValidation,
monitorValidation,
settingsValidation,
@@ -406,4 +432,5 @@ export {
infrastructureMonitorValidation,
statusPageValidation,
logoImageValidation,
+ notificationValidation,
};
diff --git a/client/src/assets/Images/background.svg b/client/src/assets/Images/background.svg
new file mode 100644
index 000000000..728597e95
--- /dev/null
+++ b/client/src/assets/Images/background.svg
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/client/src/assets/icons/logs.svg b/client/src/assets/icons/logs.svg
new file mode 100644
index 000000000..556c3ba33
--- /dev/null
+++ b/client/src/assets/icons/logs.svg
@@ -0,0 +1,5 @@
+
+
+
\ No newline at end of file
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
diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json
index 5dcb98d51..a7b64dd80 100644
--- a/client/src/locales/ar.json
+++ b/client/src/locales/ar.json
@@ -1,101 +1,13 @@
{
- "dontHaveAccount": "",
- "email": "",
- "forgotPassword": "",
- "password": "",
- "signUp": "",
"submit": "",
"title": "",
- "continue": "",
- "enterEmail": "",
- "authLoginTitle": "",
- "authLoginEnterPassword": "",
- "commonPassword": "",
- "commonBack": "",
- "authForgotPasswordTitle": "",
- "authForgotPasswordResetPassword": "",
- "createPassword": "",
- "createAPassword": "",
- "authRegisterAlreadyHaveAccount": "",
- "commonAppName": "",
- "authLoginEnterEmail": "",
- "authRegisterTitle": "",
- "authRegisterStepOneTitle": "",
- "authRegisterStepOneDescription": "",
- "authRegisterStepTwoTitle": "",
- "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": "",
- "settingsDisplayTimezone": "",
- "settingsDisplayTimezoneDescription": "",
- "settingsAppearance": "",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "",
"settingsDisabled": "",
- "settingsHistoryAndMonitoring": "",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "",
- "settingsSave": "",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -108,7 +20,6 @@
"now": "",
"delete": "",
"configure": "",
- "networkError": "",
"responseTime": "",
"ms": "",
"bar": "",
@@ -116,11 +27,6 @@
"country": "",
"city": "",
"response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,32 +333,10 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
@@ -445,11 +347,6 @@
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
"admin": "",
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
@@ -501,23 +397,14 @@
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
"commonSaving": "",
"navControls": "",
"incidentsPageTitle": "",
@@ -532,26 +419,417 @@
"passwordRequirements": "",
"saving": ""
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json
index 330812453..a5249ebbd 100644
--- a/client/src/locales/cs.json
+++ b/client/src/locales/cs.json
@@ -1,101 +1,13 @@
{
- "dontHaveAccount": "",
- "email": "E-mail",
- "forgotPassword": "Zapomenuté heslo",
- "password": "",
- "signUp": "Registace",
"submit": "Odeslat",
"title": "",
- "continue": "Pokračovat",
- "enterEmail": "Zadejte svůj e-mail",
- "authLoginTitle": "Přihlášení",
- "authLoginEnterPassword": "Zadejte své heslo",
- "commonPassword": "Heslo",
- "commonBack": "Zpět",
- "authForgotPasswordTitle": "Zapomněli jste heslo?",
- "authForgotPasswordResetPassword": "Resetovat heslo",
- "createPassword": "",
- "createAPassword": "",
- "authRegisterAlreadyHaveAccount": "Máte již účet?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Zadejte svůj e-mail",
- "authRegisterTitle": "Vytvoření účtu",
- "authRegisterStepOneTitle": "",
- "authRegisterStepOneDescription": "Začněte tím, že zadáte své údaje",
- "authRegisterStepTwoTitle": "",
- "authRegisterStepTwoDescription": "Řekněte nám o sobě víc",
- "authRegisterStepThreeTitle": "Téměř hotovo!",
- "authRegisterStepThreeDescription": "Zkontrolujte své údaje",
- "authForgotPasswordDescription": "Není se čeho obávat, zašleme vám instrukce k obnově.",
- "authForgotPasswordSendInstructions": "",
- "authForgotPasswordBackTo": "",
- "authCheckEmailTitle": "",
- "authCheckEmailDescription": "Odkaz pro obnovu hesla zašleme na",
- "authCheckEmailResendEmail": "Znovu odeslat e-mail",
- "authCheckEmailBackTo": "",
- "goBackTo": "",
- "authCheckEmailDidntReceiveEmail": "Neobdrželi jste e-mail?",
- "authCheckEmailClickToResend": "Klepněte pro opětovné odeslání",
- "authSetNewPasswordTitle": "Nastavení nového hesla",
- "authSetNewPasswordDescription": "Nové heslo se musí lišit od těch, které jste použili dříve.",
- "authSetNewPasswordNewPassword": "Nové heslo",
- "authSetNewPasswordConfirmPassword": "",
- "confirmPassword": "",
- "authSetNewPasswordResetPassword": "Resetovat heslo",
- "authSetNewPasswordBackTo": "",
- "authPasswordMustBeAtLeast": "Musí být alespoň",
- "authPasswordCharactersLong": "",
- "authPasswordMustContainAtLeast": "",
- "authPasswordSpecialCharacter": "",
- "authPasswordOneNumber": "",
- "authPasswordUpperCharacter": "",
- "authPasswordLowerCharacter": "",
- "authPasswordConfirmAndPassword": "",
- "authPasswordMustMatch": "",
- "authRegisterCreateAccount": "",
- "authRegisterCreateSuperAdminAccount": "Začněte tím, že si vytvoříte účet superadministrátora",
- "authRegisterSignUpWithEmail": "Registrovat přes e-mail",
- "authRegisterBySigningUp": "Registrací souhlasíte s našimi",
"distributedStatusHeaderText": "",
"distributedStatusSubHeaderText": "",
- "settingsGeneralSettings": "Obecná nastavení",
- "settingsDisplayTimezone": "",
- "settingsDisplayTimezoneDescription": "",
- "settingsAppearance": "Vzhled",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "Jazyk",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "Zapnuto",
"settingsDisabled": "Vypnuto",
- "settingsHistoryAndMonitoring": "Historie a monitorování",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "Vyvinuto společností Bluewave Labs.",
- "settingsSave": "Uložit",
"settingsSuccessSaved": "Nastavení bylo úspěšně uloženo",
"settingsFailedToSave": "Nepodařilo se uložit nastavení",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -108,7 +20,6 @@
"now": "",
"delete": "",
"configure": "",
- "networkError": "",
"responseTime": "",
"ms": "",
"bar": "",
@@ -116,11 +27,6 @@
"country": "",
"city": "",
"response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "Zadejte své osobní údaje",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,32 +333,10 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
@@ -445,11 +347,6 @@
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
"admin": "",
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
@@ -501,23 +397,14 @@
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
"commonSaving": "",
"navControls": "",
"incidentsPageTitle": "",
@@ -532,26 +419,417 @@
"passwordRequirements": "",
"saving": ""
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "Checkmate",
+ "monitoringAgentName": "Capture",
+ "buttons": {
+ "toggleTheme": "Přepnout mezi světlým a tmavým motivem"
+ },
+ "toasts": {
+ "networkError": "Chyba 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."
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "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 ",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "Vítejte zpět",
+ "incorrectPassword": "Nesprávné heslo"
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "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": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "Podařilo se úspěšně obnovit spojení se serverem",
+ "stillUnreachable": "Server je stále nedosažitelný. Zkuste to prosím později."
+ },
+ "alertBox": "Chyba připojení k serveru",
+ "description": "Nepodařilo se připojit k serveru. Zkontrolujte si prosím internetové připojení a pokud problém přetrvává, ověřte konfiguraci aplikace Checkmate.",
+ "retryButton": {
+ "default": "Zkusit se znovu připojit",
+ "processing": "Probíhá připojování…"
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/de.json b/client/src/locales/de.json
index 29a79c93c..948c384d8 100644
--- a/client/src/locales/de.json
+++ b/client/src/locales/de.json
@@ -1,126 +1,32 @@
{
- "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?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "",
- "authRegisterTitle": "",
- "authRegisterStepOneTitle": "",
- "authRegisterStepOneDescription": "",
- "authRegisterStepTwoTitle": "",
- "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": "",
- "settingsDisplayTimezone": "",
- "settingsDisplayTimezoneDescription": "",
- "settingsAppearance": "",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "",
- "settingsDisabled": "",
- "settingsHistoryAndMonitoring": "",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "",
- "settingsSave": "",
- "settingsSuccessSaved": "",
- "settingsFailedToSave": "",
- "settingsStatsCleared": "",
- "settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
+ "settingsDisabled": "Inaktiv",
+ "settingsSuccessSaved": "Einstellungen erfolgreich gespeichert",
+ "settingsFailedToSave": "Fehler beim Speichern der Einstellungen",
+ "settingsStatsCleared": "Statistiken gelöscht",
+ "settingsFailedToClearStats": "Fehler beim löschen der Statistiken",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
"starPromptTitle": "",
"starPromptDescription": "",
- "https": "",
- "http": "",
+ "https": "HTTPS",
+ "http": "HTTP",
"monitor": "",
- "aboutus": "",
- "now": "",
- "delete": "",
- "configure": "",
- "networkError": "",
- "responseTime": "",
- "ms": "",
+ "aboutus": "Über uns",
+ "now": "Jetzt",
+ "delete": "Löschen",
+ "configure": "Einstellungen",
+ "responseTime": "Antwortzeit",
+ "ms": "ms",
"bar": "",
"area": "",
- "country": "",
- "city": "",
- "response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
+ "country": "Land",
+ "city": "Stadt",
+ "response": "Antwort",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -134,12 +40,8 @@
"distributedUptimeCreateChecksDescription": "",
"distributedUptimeCreateIncidentNotification": "",
"distributedUptimeCreateIncidentDescription": "",
- "distributedUptimeCreateAdvancedSettings": "",
+ "distributedUptimeCreateAdvancedSettings": "Erweiterte Einstellungen",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
@@ -147,33 +49,33 @@
"testNotification": "",
"addOrEditNotifications": "",
"slack": {
- "label": "",
+ "label": "Slack",
"description": "",
- "webhookLabel": "",
+ "webhookLabel": "Webhook URL",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"discord": {
- "label": "",
+ "label": "Discord",
"description": "",
- "webhookLabel": "",
+ "webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"telegram": {
- "label": "",
+ "label": "Telegram",
"description": "",
"tokenLabel": "",
- "tokenPlaceholder": "",
+ "tokenPlaceholder": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"chatIdLabel": "",
- "chatIdPlaceholder": "",
+ "chatIdPlaceholder": "-1001234567890",
"fieldsRequired": ""
},
"webhook": {
- "label": "",
+ "label": "Webhooks",
"description": "",
- "urlLabel": "",
- "urlPlaceholder": "",
+ "urlLabel": "Webhook URL",
+ "urlPlaceholder": "https://your-server.com/webhook",
"urlRequired": ""
},
"testNotificationDevelop": "",
@@ -181,7 +83,33 @@
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "",
@@ -196,14 +124,14 @@
"distributedUptimeStatusLogoUploadButton": "",
"distributedUptimeStatusStandardMonitorsHeader": "",
"distributedUptimeStatusStandardMonitorsDescription": "",
- "distributedUptimeStatusCreateYour": "",
- "distributedUptimeStatusEditYour": "",
+ "distributedUptimeStatusCreateYour": "Erstelle Dein",
+ "distributedUptimeStatusEditYour": "Ändere Dein",
"distributedUptimeStatusPublishedLabel": "",
- "distributedUptimeStatusCompanyNameLabel": "",
+ "distributedUptimeStatusCompanyNameLabel": "Firmenname",
"distributedUptimeStatusPageAddressLabel": "",
- "distributedUptimeStatus30Days": "",
- "distributedUptimeStatus60Days": "",
- "distributedUptimeStatus90Days": "",
+ "distributedUptimeStatus30Days": "30 Tage",
+ "distributedUptimeStatus60Days": "60 Tage",
+ "distributedUptimeStatus90Days": "90 Tage",
"distributedUptimeStatusPageNotSetUp": "",
"distributedUptimeStatusContactAdmin": "",
"distributedUptimeStatusPageNotPublic": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,32 +333,10 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
@@ -445,11 +347,6 @@
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
"admin": "",
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
@@ -501,23 +397,14 @@
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
"commonSaving": "",
"navControls": "",
"incidentsPageTitle": "",
@@ -532,26 +419,417 @@
"passwordRequirements": "",
"saving": ""
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 08e3511a7..fe4b1fa55 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -1,101 +1,13 @@
{
- "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?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Enter your email",
- "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",
- "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",
- "settingsDisplayTimezone": "Display timezone",
- "settingsDisplayTimezoneDescription": "Select the timezone used to display dates and times throughout the application.",
- "settingsAppearance": "Appearance",
- "settingsAppearanceDescription": "Switch between light and dark mode, or change user interface language",
- "settingsThemeMode": "Theme Mode",
- "settingsLanguage": "Language",
- "settingsDistributedUptime": "Distributed uptime",
- "settingsDistributedUptimeDescription": "Enable/disable distributed uptime monitoring.",
- "settingsEnabled": "Enabled",
"settingsDisabled": "Disabled",
- "settingsHistoryAndMonitoring": "History of monitoring",
- "settingsHistoryAndMonitoringDescription": "Define how long you want to retain historical data. You can also clear all existing data.",
- "settingsTTLLabel": "The days you want to keep monitoring history.",
- "settingsTTLOptionalLabel": "0 for infinite",
- "settingsClearAllStats": "Clear all stats. This is irreversible.",
- "settingsClearAllStatsButton": "Clear all stats",
- "settingsClearAllStatsDialogTitle": "Do you want to clear all stats?",
- "settingsClearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
- "settingsClearAllStatsDialogConfirm": "Yes, clear all stats",
- "settingsDemoMonitors": "Demo monitors",
- "settingsDemoMonitorsDescription": "Add sample monitors for demonstration purposes.",
- "settingsAddDemoMonitors": "Adding demo monitors",
- "settingsAddDemoMonitorsButton": "Add demo monitors",
- "settingsRemoveAllMonitors": "Removing all monitors",
- "settingsRemoveAllMonitorsButton": "Remove all monitors",
- "settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
- "settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
- "settingsWallet": "Wallet",
- "settingsWalletDescription": "Connect your wallet here. This is required for the Distributed Uptime monitor to connect to multiple nodes globally.",
- "settingsAbout": "About",
- "settingsDevelopedBy": "Developed by Bluewave Labs.",
- "settingsSave": "Save",
"settingsSuccessSaved": "Settings saved successfully",
"settingsFailedToSave": "Failed to save settings",
"settingsStatsCleared": "Stats cleared successfully",
"settingsFailedToClearStats": "Failed to clear stats",
- "settingsDemoMonitorsAdded": "Successfully added demo monitors",
"settingsFailedToAddDemoMonitors": "Failed to add demo monitors",
"settingsMonitorsDeleted": "Successfully deleted all monitors",
"settingsFailedToDeleteMonitors": "Failed to delete all monitors",
@@ -108,7 +20,6 @@
"now": "Now",
"delete": "Delete",
"configure": "Configure",
- "networkError": "Network error",
"responseTime": "Response time",
"ms": "ms",
"bar": "Bar",
@@ -116,11 +27,6 @@
"country": "COUNTRY",
"city": "CITY",
"response": "RESPONSE",
- "checkConnection": "Please check your connection",
- "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",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.",
"distributedUptimeCreateAdvancedSettings": "Advanced settings",
"distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.",
- "distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
- "distributedUptimeDetailsFooterBuilt": "Built on",
- "distributedUptimeDetailsFooterSolana": "Solana",
- "distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Uptime:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated",
"notifications": {
@@ -181,7 +83,37 @@
"testSuccess": "Test notification sent successfully!",
"testFailed": "Failed to send test notification",
"unsupportedType": "Unsupported notification type",
- "networkError": "Network error occurred"
+ "networkError": "Network error occurred",
+ "fallback": {
+ "title": "notification channel",
+ "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",
+ "create": {
+ "success": "Notification created successfully",
+ "failed": "Failed to create notification"
+ },
+ "fetch": {
+ "success": "Notifications fetched successfully",
+ "failed": "Failed to fetch notifications"
+ },
+ "delete": {
+ "success": "Notification deleted successfully",
+ "failed": "Failed to delete notification"
+ },
+ "edit": {
+ "success": "Notification updated successfully",
+ "failed": "Failed to update notification"
+ },
+ "test": {
+ "success": "Test notification sent successfully",
+ "failed": "Failed to send test notification"
+ }
},
"testLocale": "testLocale",
"add": "Add",
@@ -394,10 +326,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?",
- "authRegisterFirstName": "Name",
- "authRegisterLastName": "Surname",
- "authRegisterEmail": "Email",
- "authRegisterEmailRequired": "To continue, please enter your email address",
"bulkImport": {
"title": "Bulk Import",
"selectFileTips": "Select CSV file to upload",
@@ -409,32 +337,10 @@
"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",
- "settingsSystemReset": "System reset",
- "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",
- "backendUnreachable": "Server Connection Error",
- "backendUnreachableMessage": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
- "backendUnreachableError": "Cannot connect to the server. Please try again later.",
- "retryConnection": "Retry connection",
- "retryingConnection": "Connecting...",
- "backendReconnected": "Successfully reconnected to the server.",
- "backendStillUnreachable": "Server is still unreachable. Please try again later.",
- "backendConnectionError": "Error connecting to the server. Please check your network connection.",
"maskedPageSpeedKeyPlaceholder": "*************************************",
- "pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
- "pageSpeedApiKeyFieldLabel": "PageSpeed API key",
- "pageSpeedApiKeyFieldDescription": "Enter your Google PageSpeed API key to enable pagespeed monitoring. Click Reset to update the key.",
- "pageSpeedApiKeyFieldResetLabel": "API key is set. Click Reset to change it.",
"reset": "Reset",
"ignoreTLSError": "Ignore TLS/SSL error",
"tlsErrorIgnored": "TLS/SSL errors ignored",
@@ -445,11 +351,6 @@
"append": "The afternoon is your playground—let's make it epic!",
"overview": "Here's an overview of your {{type}} monitors."
},
- "monitorStatus": {
- "up": "up",
- "down": "down",
- "paused": "paused"
- },
"roles": {
"superAdmin": "Super admin",
"admin": "Admin",
@@ -488,7 +389,6 @@
"uptime": "Uptime",
"pagespeed": "Pagespeed",
"infrastructure": "Infrastructure",
- "distributedUptime": "Distributed Uptime",
"incidents": "Incidents",
"statusPages": "Status pages",
"maintenance": "Maintenance",
@@ -501,23 +401,14 @@
"profile": "Profile",
"password": "Password",
"team": "Team",
- "logOut": "Log out"
+ "logOut": "Log out",
+ "notifications": "Notifications",
+ "logs": "Logs"
},
- "settingsEmail": "Email",
- "settingsEmailDescription": "Configure the email settings for your system. This is used to send notifications and alerts.",
- "settingsEmailHost": "Email host - Hostname or IP address of the SMTP server",
- "settingsEmailPort": "Email port - Port to connect to",
- "settingsEmailAddress": "Email address - Used for authentication",
- "settingsEmailPassword": "Email password - Password for authentication",
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
- "settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.",
"state": "State",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
- "authForgotPasswordInstructions": "No worries, we'll send you reset instructions.",
- "settingsThemeModeLight": "Light",
- "settingsThemeModeDark": "Dark",
- "settingsClearAllStatsDialogCancel": "Cancel",
"commonSaving": "Saving...",
"navControls": "Controls",
"incidentsPageTitle": "Incidents",
@@ -532,26 +423,417 @@
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
"saving": "Saving..."
},
- "uptimeCreateSelectURL": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
- "settingsEmailConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting",
- "sendTestEmail": "Send test email",
"emailSent": "Email sent successfully",
"failedToSendEmail": "Failed to send email",
- "settingsTestEmail": "Send test e-mail",
"settingsTestEmailSuccess": "Test email sent successfully",
"settingsTestEmailFailed": "Failed to send test email",
"settingsTestEmailFailedWithReason": "Failed to send test email: {{reason}}",
"settingsTestEmailUnknownError": "Unknown error",
- "settingsEmailRequiredFields": "Email host and port are required",
"statusMsg": {
"paused": "Monitoring is paused.",
"up": "Your site is up.",
"down": "Your site is down.",
"pending": "Pending..."
},
- "settingsURLTitle": "Monitor IP/URL on Status Page",
- "settingsURLDescription": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
- "settingsURLSelectTitle": "Display IP/URL on status page",
- "settingsURLEnabled": "Enabled",
- "settingURLDisabled": "Disabled"
+ "uptimeGeneralInstructions": {
+ "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.",
+ "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.",
+ "docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect 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."
+ },
+ "common": {
+ "appName": "Checkmate",
+ "monitoringAgentName": "Capture",
+ "buttons": {
+ "toggleTheme": "Toggles light & dark"
+ },
+ "toasts": {
+ "networkError": "Network error",
+ "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."
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": "The password you provided does not match our records"
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "Log In",
+ "subheadings": {
+ "stepOne": "Enter your email",
+ "stepTwo": "Enter your password"
+ },
+ "links": {
+ "forgotPassword": "Forgot password?",
+ "register": "Do not have an account?",
+ "forgotPasswordLink": "Reset password",
+ "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": {
+ "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": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "Successfully reconnected to the server.",
+ "stillUnreachable": "Server is still unreachable. Please try again later."
+ },
+ "alertBox": "Server Connection Error",
+ "description": "We're unable to connect to the server. Please check your internet connection or verify your deployment configuration if the problem persists.",
+ "retryButton": {
+ "default": "Retry connection",
+ "processing": "Connecting..."
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "Create notification channel",
+ "nameSettings": {
+ "title": "Name",
+ "description": "A descriptive name for your integration.",
+ "nameLabel": "Name",
+ "namePlaceholder": "e.g. Slack notifications"
+ },
+ "typeSettings": {
+ "title": "Type",
+ "description": "Select the type of notification channel you want to create.",
+ "typeLabel": "Type"
+ },
+ "emailSettings": {
+ "title": "Email",
+ "description": "Destination email addresses.",
+ "emailLabel": "Email address",
+ "emailPlaceholder": "e.g. john@example.com"
+ },
+ "slackSettings": {
+ "title": "Slack",
+ "description": "Configure your Slack webhook here",
+ "webhookLabel": "Slack webhook URL",
+ "webhookPlaceholder": "https://hooks.slack.com/services/..."
+ },
+ "pagerdutySettings": {
+ "title": "PagerDuty",
+ "description": "Configure your PagerDuty integration here",
+ "integrationKeyLabel": "Integration key",
+ "integrationKeyPlaceholder": "1234567890"
+ },
+ "discordSettings": {
+ "title": "Discord",
+ "description": "Configure your Discord webhook here",
+ "webhookLabel": "Discord Webhook URL",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ },
+ "webhookSettings": {
+ "title": "Webhook",
+ "description": "Configure your webhook here",
+ "webhookLabel": "Webhook URL",
+ "webhookPlaceholder": "https://your-server.com/webhook"
+ }
+ },
+ "notificationConfig": {
+ "title": "Notifications",
+ "description": "Select the notifications channels you want to use"
+ },
+ "monitorStatus": {
+ "checkingEvery": "Checking every {{interval}}",
+ "withCaptureAgent": "with Capture agent {{version}}",
+ "up": "up",
+ "down": "down",
+ "paused": "paused"
+ },
+ "advancedMatching": "Advanced matching",
+ "sendTestNotifications": "Send test notifications",
+ "selectAll": "Select all",
+ "showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page",
+ "logsPage": {
+ "title": "Logs",
+ "description": "This page shows the latest 1000 lines of logs from the Checkmate server",
+ "tabs": {
+ "queue": "Job queue",
+ "logs": "Server logs"
+ },
+ "toast": {
+ "fetchLogsSuccess": "Logs fetched successfully"
+ },
+ "logLevelSelect": {
+ "title": "Log level",
+ "values": {
+ "all": "All",
+ "info": "Info",
+ "warn": "Warn",
+ "error": "Error",
+ "debug": "Debug"
+ }
+ }
+ },
+ "queuePage": {
+ "title": "Queue",
+ "refreshButton": "Refresh",
+ "flushButton": "Flush queue",
+ "jobTable": {
+ "title": "Jobs currently in queue",
+ "idHeader": "Monitor ID",
+ "urlHeader": "URL",
+ "typeHeader": "Type",
+ "activeHeader": "Active",
+ "lockedAtHeader": "Locked at",
+ "runCountHeader": "Run count",
+ "failCountHeader": "Fail count",
+ "lastRunHeader": "Last run at",
+ "lastFinishedAtHeader": "Last finished at",
+ "lastRunTookHeader": "Last run took"
+ },
+ "metricsTable": {
+ "title": "Queue metrics",
+ "metricHeader": "Metric",
+ "valueHeader": "Value"
+ },
+ "failedJobTable": {
+ "title": "Failed jobs",
+ "monitorIdHeader": "Monitor ID",
+ "monitorUrlHeader": "Monitor URL",
+ "failCountHeader": "Fail count",
+ "failedAtHeader": "Last failed at",
+ "failReasonHeader": "Fail reason"
+ }
+ },
+ "export": {
+ "title": "Export Monitors",
+ "success": "Monitors exported successfully!",
+ "failed": "Failed to export monitors"
+ },
+ "monitorActions": {
+ "title": "Export/Import",
+ "import": "Import Monitors",
+ "export": "Export Monitors"
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "Developed by Bluewave Labs",
+ "labelVersion": "Version",
+ "title": "About"
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "Add demo monitors",
+ "description": "Add sample monitors for demonstration purposes.",
+ "title": "Demo monitors"
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "Send test e-mail",
+ "description": "Configure the email settings for your system. This is used to send notifications and alerts.",
+ "descriptionTransport": "This builds an SMTP transport for NodeMailer",
+ "labelAddress": "Email address - Used for authentication",
+ "labelConnectionHost": "Email connection host - Hostname to use in the HELO/EHLO greeting",
+ "labelHost": "Email host - Hostname or IP address to connect to",
+ "labelIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it",
+ "labelPassword": "Email password - Password for authentication",
+ "labelPasswordSet": "Password is set. Click Reset to change it.",
+ "labelPool": "Enable connection pooling: Reuse existing connections to improve performance",
+ "labelPort": "Email port - Port to connect to",
+ "labelRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates",
+ "labelRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported",
+ "labelSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS",
+ "labelTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP",
+ "labelUser": "Email user - Username for authentication, overrides email address if specified",
+ "linkTransport": "See specifications here",
+ "placeholderUser": "Leave empty if not required",
+ "title": "Email"
+ },
+ "pageSpeedSettings": {
+ "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
+ "labelApiKeySet": "API key is set. Click Reset to change it.",
+ "labelApiKey": "PageSpeed API key",
+ "title": "Google PageSpeed API key"
+ },
+ "saveButtonLabel": "Save",
+ "statsSettings": {
+ "clearAllStatsButton": "Clear all stats",
+ "clearAllStatsDescription": "Clear all stats. This is irreversible.",
+ "clearAllStatsDialogConfirm": "Yes, clear all stats",
+ "clearAllStatsDialogDescription": "Once removed, the monitoring history and stats cannot be retrieved.",
+ "clearAllStatsDialogTitle": "Do you want to clear all stats?",
+ "description": "Define how long you want to retain historical data. You can also clear all existing data.",
+ "labelTTL": "The days you want to keep monitoring history.",
+ "labelTTLOptional": "0 for infinite",
+ "title": "Monitor history"
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "Remove all monitors",
+ "description": "Remove all monitors from your system.",
+ "dialogConfirm": "Yes, remove all monitors",
+ "dialogDescription": "Once removed, the monitors cannot be retrieved.",
+ "dialogTitle": "Do you want to remove all monitors?",
+ "title": "System reset"
+ },
+ "timezoneSettings": {
+ "description": "Select the timezone used to display dates and times throughout the application.",
+ "label": "Display timezone",
+ "title": "Display timezone"
+ },
+ "title": "Settings",
+ "uiSettings": {
+ "description": "Switch between light and dark mode, or change user interface language.",
+ "labelLanguage": "Language",
+ "labelTheme": "Theme mode",
+ "title": "Appearance"
+ },
+ "urlSettings": {
+ "description": "Display the IP address or URL of monitor on the public Status page. If it's disabled, only the monitor name will be shown to protect sensitive information.",
+ "label": "Display IP/URL on status page",
+ "selectDisabled": "Disabled",
+ "selectEnabled": "Enabled",
+ "title": "Monitor IP/URL on Status Page"
+ }
+ }
}
diff --git a/client/src/locales/es.json b/client/src/locales/es.json
index 616ece47a..4cbd91336 100644
--- a/client/src/locales/es.json
+++ b/client/src/locales/es.json
@@ -1,101 +1,13 @@
{
- "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?",
- "commonAppName": "",
- "authLoginEnterEmail": "Introduce tu correo electronico",
- "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",
- "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": "",
- "settingsDisplayTimezone": "",
- "settingsDisplayTimezoneDescription": "",
- "settingsAppearance": "",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "",
"settingsDisabled": "",
- "settingsHistoryAndMonitoring": "",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "",
- "settingsSave": "",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -108,7 +20,6 @@
"now": "",
"delete": "",
"configure": "",
- "networkError": "",
"responseTime": "",
"ms": "",
"bar": "",
@@ -116,11 +27,6 @@
"country": "",
"city": "",
"response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,32 +333,10 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
@@ -445,11 +347,6 @@
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
"admin": "",
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
@@ -501,23 +397,14 @@
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
"commonSaving": "",
"navControls": "",
"incidentsPageTitle": "",
@@ -532,26 +419,417 @@
"passwordRequirements": "",
"saving": ""
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json
index c1543cc13..1783f8a07 100644
--- a/client/src/locales/fi.json
+++ b/client/src/locales/fi.json
@@ -1,101 +1,13 @@
{
- "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?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Syötä salasanasi",
- "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",
- "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",
- "settingsDisplayTimezone": "Näytä aikavyöhyke",
- "settingsDisplayTimezoneDescription": "Valitse sovelluksessa käytettävä aikavyöhyke päivämäärien ja aikojen näyttämiseen.",
- "settingsAppearance": "Ulkonäkö",
- "settingsAppearanceDescription": "Vaihda teema vaaleaksi tai tummaksi tai vaihda käyttöliittymän kieli",
- "settingsThemeMode": "Teematila",
- "settingsLanguage": "Kieli",
- "settingsDistributedUptime": "Hajautettu käyttöaika",
- "settingsDistributedUptimeDescription": "Ota käyttöön tai poista käytöstä hajautettu käyttöaikavalvonta.",
- "settingsEnabled": "Käytössä",
"settingsDisabled": "Poistettu käytöstä",
- "settingsHistoryAndMonitoring": "Valvonnan historia",
- "settingsHistoryAndMonitoringDescription": "Määritä, kuinka pitkään haluat säilyttää historiallisia tietoja. Voit myös tyhjentää kaikki olemassa olevat tiedot.",
- "settingsTTLLabel": "Kuinka monta päivää haluat säilyttää valvontahistorian.",
- "settingsTTLOptionalLabel": "0 on ääretön",
- "settingsClearAllStats": "Tyhjennä kaikki tilastot. Tämä on peruuttamaton.",
- "settingsClearAllStatsButton": "Tyhjennä kaikki tilastot",
- "settingsClearAllStatsDialogTitle": "Haluatko tyhjentää kaikki tilastot?",
- "settingsClearAllStatsDialogDescription": "Kun valvontahistoria ja tilastot on poistettu, niitä ei voi palauttaa.",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "Lompakko",
- "settingsWalletDescription": "",
- "settingsAbout": "Tietoja",
- "settingsDevelopedBy": "Kehittänyt Bluewave Labs.",
- "settingsSave": "Tallenna",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -108,7 +20,6 @@
"now": "Nyt",
"delete": "Poista",
"configure": "Määritä",
- "networkError": "Verkkovirhe",
"responseTime": "Vasteaika",
"ms": "ms",
"bar": "Palkki",
@@ -116,11 +27,6 @@
"country": "MAA",
"city": "KAUPUNKI",
"response": "Vastaus",
- "checkConnection": "Tarkista yhteytesi",
- "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",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "Jos ilmenee häiriö, ilmoita käyttäjille.",
"distributedUptimeCreateAdvancedSettings": "Lisäasetukset",
"distributedUptimeDetailsNoMonitorHistory": "Tälle valvontakohteelle ei ole vielä tarkistushistoriaa.",
- "distributedUptimeDetailsFooterHeading": "Tehty ❤️:lla UpRockin ja Bluewave Labsin toimesta",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "Solana",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Viimeksi päivitetty",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "Testi ilmoituksen lähetys onnistui!",
"testFailed": "Testi ilmoituksen lähetys epäonnistui",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "Lisää",
@@ -237,7 +165,7 @@
"infrastructureCustomizeAlerts": "",
"infrastructureAlertNotificationDescription": "",
"infrastructureCreateMonitor": "",
- "infrastructureProtocol": "",
+ "infrastructureProtocol": "Protokolla",
"infrastructureServerUrlLabel": "Palvelimen URL-osoite",
"infrastructureDisplayNameLabel": "",
"infrastructureAuthorizationSecretLabel": "",
@@ -353,11 +281,11 @@
"statusPageCreateTabsContent": "",
"statusPageCreateTabsContentDescription": "",
"statusPageCreateTabsContentFeaturesDescription": "",
- "showCharts": "",
+ "showCharts": "Näytä kaaviot",
"showUptimePercentage": "",
"removeLogo": "Poista logo",
"statusPageStatus": "",
- "statusPageStatusContactAdmin": "",
+ "statusPageStatusContactAdmin": "Ota yhteyttä järjestelmänvalvojaan",
"statusPageStatusNotPublic": "",
"statusPageStatusNoPage": "",
"statusPageStatusServiceStatus": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "Haluatko todella poistaa tämän tilin?",
- "authRegisterFirstName": "Nimi",
- "authRegisterLastName": "Sukunimi",
- "authRegisterEmail": "Sähköpostiosoite",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,55 +333,28 @@
"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ä",
- "settingsSystemReset": "Järjestelmän palautus",
- "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ä",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "*************************************",
- "pageSpeedApiKeyFieldTitle": "Google PageSpeed API-avain",
- "pageSpeedApiKeyFieldLabel": "PageSpeed API-avain",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "Palauta",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
"ignoreTLSErrorDescription": "",
"createNew": "Luo uusi",
"greeting": {
- "prepend": "",
+ "prepend": "Hei",
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "Ylös",
- "down": "Alas",
- "paused": "Tauko"
- },
"roles": {
"superAdmin": "Pääylläpitäjä",
"admin": "Ylläpitäjä",
- "teamMember": "",
+ "teamMember": "Tiimin jäsen",
"demoUser": "Demokäyttäjä"
},
"teamPanel": {
- "teamMembers": "",
+ "teamMembers": "Tiimin jäsenet",
"filter": {
"all": "Kaikki",
"member": "Jäsen"
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "Infrastruktuuri",
- "distributedUptime": "",
"incidents": "",
"statusPages": "Tilasivut",
"maintenance": "Huolto",
@@ -501,23 +397,14 @@
"profile": "Profiili",
"password": "Salasana",
"team": "Tiimi",
- "logOut": "Kirjaudu ulos"
+ "logOut": "Kirjaudu ulos",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "Sähköpostiasetukset",
- "settingsEmailDescription": "Määritä sähköpostiasetukset",
- "settingsEmailHost": "",
- "settingsEmailPort": "Sähköpostin portti - Portti johon yhdistetään",
- "settingsEmailAddress": "Sähköpostiosoite - Käytetään tunnistautumiseen",
- "settingsEmailPassword": "Sähköpostin salasana - Salasana tunnistautumiseen",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "Salasana on asetettu. Paina Palauta vaihtaaksesi sen.",
"state": "Tila",
"statusBreadCrumbsStatusPages": "Tilasivut",
"statusBreadCrumbsDetails": "Tiedot",
- "authForgotPasswordInstructions": "Ei hätää, lähetämme sinulle ohjeet salasanan palauttamiseen.",
- "settingsThemeModeLight": "Vaalea",
- "settingsThemeModeDark": "Tumma",
- "settingsClearAllStatsDialogCancel": "Peruuta",
"commonSaving": "Tallennetaan...",
"navControls": "Ohjaimet",
"incidentsPageTitle": "Häiriöt",
@@ -532,26 +419,417 @@
"passwordRequirements": "Uuden salasanan on oltava vähintään 8 merkkiä pitkä ja sen on sisältävä vähintään yksi iso kirjain, yksi pieni kirjain, yksi numero sekä yksi erikoismerkki.",
"saving": "Tallennetaan..."
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "Lähetä testi sähköposti",
"emailSent": "Sähköpostin lähetys onnistui",
"failedToSendEmail": "Sähköpostin lähetys epäonnistui",
- "settingsTestEmail": "Lähetä testi sähköposti",
"settingsTestEmailSuccess": "Testisähköpostin lähetys onnistui",
"settingsTestEmailFailed": "Testisähköpostin lähetys epäonnistui",
"settingsTestEmailFailedWithReason": "Testisähköpostin lähetys epäonnistui: {{reason}}",
"settingsTestEmailUnknownError": "Tuntematon virhe",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "Valvonta on keskeytetty.",
"up": "Sivusi on ylhäällä.",
"down": "Sivusi on alhaalla.",
"pending": "Odottaa..."
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json
index 68ac45f33..5aabeb6e3 100644
--- a/client/src/locales/fr.json
+++ b/client/src/locales/fr.json
@@ -1,101 +1,13 @@
{
- "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 ?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Entrez votre e-mail",
- "authRegisterTitle": "Créer un compte",
- "authRegisterStepOneTitle": "Créez votre compte",
- "authRegisterStepOneDescription": "Entrez vos informations pour commencer",
- "authRegisterStepTwoTitle": "",
- "authRegisterStepTwoDescription": "",
- "authRegisterStepThreeTitle": "Presque fini !",
- "authRegisterStepThreeDescription": "",
- "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": "",
- "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",
- "settingsDisplayTimezone": "Fuseau horaire d'affichage",
- "settingsDisplayTimezoneDescription": "Le fuseau horaire du tableau de bord que vous affichez publiquement.",
- "settingsAppearance": "Apparence",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "Langue",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "Activé",
"settingsDisabled": "Désactivé",
- "settingsHistoryAndMonitoring": "",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "",
- "settingsSave": "",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -104,59 +16,49 @@
"https": "",
"http": "",
"monitor": "",
- "aboutus": "",
+ "aboutus": "A propos de nous",
"now": "",
- "delete": "",
+ "delete": "Supprimer",
"configure": "",
- "networkError": "",
"responseTime": "",
"ms": "",
"bar": "",
- "area": "",
- "country": "",
- "city": "",
+ "area": "Zone",
+ "country": "PAYS",
+ "city": "VILLE",
"response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
"webhookSendError": "",
"webhookUnsupportedPlatform": "",
- "distributedRightCategoryTitle": "",
+ "distributedRightCategoryTitle": "Moniteur",
"distributedStatusServerMonitors": "",
"distributedStatusServerMonitorsDescription": "",
"distributedUptimeCreateSelectURL": "",
"distributedUptimeCreateChecks": "",
"distributedUptimeCreateChecksDescription": "",
- "distributedUptimeCreateIncidentNotification": "",
+ "distributedUptimeCreateIncidentNotification": "Notifications d'incidents",
"distributedUptimeCreateIncidentDescription": "",
- "distributedUptimeCreateAdvancedSettings": "",
+ "distributedUptimeCreateAdvancedSettings": "Paramètres avancés",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
"enableNotifications": "",
- "testNotification": "",
- "addOrEditNotifications": "",
+ "testNotification": "Notification de test",
+ "addOrEditNotifications": "Ajouter ou éditer des notifications",
"slack": {
"label": "",
"description": "",
- "webhookLabel": "",
+ "webhookLabel": "URL du webhook",
"webhookPlaceholder": "",
"webhookRequired": ""
},
"discord": {
"label": "",
"description": "",
- "webhookLabel": "",
+ "webhookLabel": "URL du webhook Discord",
"webhookPlaceholder": "",
"webhookRequired": ""
},
@@ -165,71 +67,97 @@
"description": "",
"tokenLabel": "",
"tokenPlaceholder": "",
- "chatIdLabel": "",
- "chatIdPlaceholder": "",
+ "chatIdLabel": "Votre Chat ID",
+ "chatIdPlaceholder": "-1001234567890",
"fieldsRequired": ""
},
"webhook": {
"label": "",
"description": "",
- "urlLabel": "",
- "urlPlaceholder": "",
- "urlRequired": ""
+ "urlLabel": "URL du webhook",
+ "urlPlaceholder": "https://votre-serveur.fr/webhook",
+ "urlRequired": "L'URL du webhook est nécessaire"
},
- "testNotificationDevelop": "",
- "integrationButton": "",
- "testSuccess": "",
- "testFailed": "",
+ "testNotificationDevelop": "Notification de test 2",
+ "integrationButton": "Intégration de notification",
+ "testSuccess": "La notification de test a été envoyée avec succès !",
+ "testFailed": "Échec de l'envoi de la notification de test",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
- "add": "",
+ "add": "Ajouter",
"monitors": "",
- "distributedUptimeStatusCreateStatusPage": "",
- "distributedUptimeStatusCreateStatusPageAccess": "",
+ "distributedUptimeStatusCreateStatusPage": "page de statut",
+ "distributedUptimeStatusCreateStatusPageAccess": "Accès",
"distributedUptimeStatusCreateStatusPageReady": "",
"distributedUptimeStatusBasicInfoHeader": "",
"distributedUptimeStatusBasicInfoDescription": "",
- "distributedUptimeStatusLogoHeader": "",
+ "distributedUptimeStatusLogoHeader": "Logo",
"distributedUptimeStatusLogoDescription": "",
- "distributedUptimeStatusLogoUploadButton": "",
+ "distributedUptimeStatusLogoUploadButton": "Téléverser un logo",
"distributedUptimeStatusStandardMonitorsHeader": "",
"distributedUptimeStatusStandardMonitorsDescription": "",
"distributedUptimeStatusCreateYour": "",
"distributedUptimeStatusEditYour": "",
"distributedUptimeStatusPublishedLabel": "",
- "distributedUptimeStatusCompanyNameLabel": "",
- "distributedUptimeStatusPageAddressLabel": "",
- "distributedUptimeStatus30Days": "",
- "distributedUptimeStatus60Days": "",
- "distributedUptimeStatus90Days": "",
+ "distributedUptimeStatusCompanyNameLabel": "Nom de l'entreprise",
+ "distributedUptimeStatusPageAddressLabel": "Adresse de votre page de statut",
+ "distributedUptimeStatus30Days": "30 jours",
+ "distributedUptimeStatus60Days": "60 jours",
+ "distributedUptimeStatus90Days": "90 jours",
"distributedUptimeStatusPageNotSetUp": "",
- "distributedUptimeStatusContactAdmin": "",
- "distributedUptimeStatusPageNotPublic": "",
- "distributedUptimeStatusPageDeleteDialog": "",
- "distributedUptimeStatusPageDeleteConfirm": "",
- "distributedUptimeStatusPageDeleteDescription": "",
+ "distributedUptimeStatusContactAdmin": "Veuillez contacter votre administrateur",
+ "distributedUptimeStatusPageNotPublic": "Cette page de statut n'est pas publique.",
+ "distributedUptimeStatusPageDeleteDialog": "Voulez-vous supprimer cette page de statut ?",
+ "distributedUptimeStatusPageDeleteConfirm": "Oui, supprimer la page de statut",
+ "distributedUptimeStatusPageDeleteDescription": "Supprimer votre page de statut est irréversible.",
"distributedUptimeStatusDevices": "",
"distributedUptimeStatusUpt": "",
"distributedUptimeStatusUptBurned": "",
"distributedUptimeStatusUptLogo": "",
"incidentsTableNoIncidents": "",
- "incidentsTablePaginationLabel": "",
+ "incidentsTablePaginationLabel": "incidents",
"incidentsTableMonitorName": "",
"incidentsTableStatus": "",
- "incidentsTableDateTime": "",
+ "incidentsTableDateTime": "Date et heure",
"incidentsTableStatusCode": "",
- "incidentsTableMessage": "",
+ "incidentsTableMessage": "Message",
"incidentsOptionsHeader": "",
"incidentsOptionsHeaderFilterBy": "",
"incidentsOptionsHeaderFilterAll": "",
"incidentsOptionsHeaderFilterDown": "",
"incidentsOptionsHeaderFilterCannotResolve": "",
"incidentsOptionsHeaderShow": "",
- "incidentsOptionsHeaderLastHour": "",
- "incidentsOptionsHeaderLastDay": "",
- "incidentsOptionsHeaderLastWeek": "",
+ "incidentsOptionsHeaderLastHour": "Dernière heure",
+ "incidentsOptionsHeaderLastDay": "Dernier jour",
+ "incidentsOptionsHeaderLastWeek": "Dernière semaine",
"incidentsOptionsPlaceholderAllServers": "",
"infrastructureCreateYour": "",
"infrastructureCreateGeneralSettingsDescription": "",
@@ -237,18 +165,18 @@
"infrastructureCustomizeAlerts": "",
"infrastructureAlertNotificationDescription": "",
"infrastructureCreateMonitor": "",
- "infrastructureProtocol": "",
- "infrastructureServerUrlLabel": "",
+ "infrastructureProtocol": "Protocole",
+ "infrastructureServerUrlLabel": "URL du serveur",
"infrastructureDisplayNameLabel": "",
"infrastructureAuthorizationSecretLabel": "",
"gb": "",
"mb": "",
"mem": "",
"memoryUsage": "",
- "cpu": "",
+ "cpu": "CPU",
"cpuUsage": "",
"cpuTemperature": "",
- "diskUsage": "",
+ "diskUsage": "Utilisation du disque",
"used": "",
"total": "",
"cores": "",
@@ -256,36 +184,36 @@
"status": "",
"cpuPhysical": "",
"cpuLogical": "",
- "cpuFrequency": "",
- "avgCpuTemperature": "",
+ "cpuFrequency": "Fréquence CPU",
+ "avgCpuTemperature": "Température moyenne du CPU",
"memory": "",
- "disk": "",
+ "disk": "Disque",
"uptime": "",
"os": "",
"host": "",
- "actions": "",
- "integrations": "",
- "integrationsPrism": "",
- "integrationsSlack": "",
- "integrationsSlackInfo": "",
- "integrationsDiscord": "",
- "integrationsDiscordInfo": "",
- "integrationsZapier": "",
- "integrationsZapierInfo": "",
- "commonSave": "",
- "createYour": "",
+ "actions": "Actions",
+ "integrations": "Intégrations",
+ "integrationsPrism": "Connecter Prism à votre service préféré.",
+ "integrationsSlack": "Slack",
+ "integrationsSlackInfo": "Connecter à Slack et voir les incidents dans un canal",
+ "integrationsDiscord": "Discord",
+ "integrationsDiscordInfo": "Connecter à Discord et voir les incidents dans un canal",
+ "integrationsZapier": "Zapier",
+ "integrationsZapierInfo": "Envoyer tous les incidents à Zapier",
+ "commonSave": "Sauvegarder",
+ "createYour": "Créez votre",
"createMonitor": "",
"pause": "",
"resume": "",
"editing": "",
- "url": "",
- "access": "",
- "timezone": "",
+ "url": "URL",
+ "access": "Accès",
+ "timezone": "Fuseau horaire",
"features": "",
- "administrator": "",
+ "administrator": "Administrateur ?",
"loginHere": "",
- "displayName": "",
- "urlMonitor": "",
+ "displayName": "Nom d'affichage",
+ "urlMonitor": "URL à suivre",
"portToMonitor": "",
"websiteMonitoring": "",
"websiteMonitoringDescription": "",
@@ -296,7 +224,7 @@
"portMonitoring": "",
"portMonitoringDescription": "",
"createMaintenanceWindow": "",
- "createMaintenance": "",
+ "createMaintenance": "Créer une maintenance",
"editMaintenance": "",
"maintenanceWindowName": "",
"friendlyNameInput": "",
@@ -304,20 +232,20 @@
"maintenanceRepeat": "",
"maintenance": "",
"duration": "",
- "addMonitors": "",
+ "addMonitors": "Ajouter des moniteurs",
"window": "",
- "cancel": "",
+ "cancel": "Annuler",
"message": "",
"low": "",
"high": "",
"statusCode": "",
- "date&Time": "",
+ "date&Time": "Date et heure",
"type": "",
"statusPageName": "",
"publicURL": "",
"repeat": "",
"edit": "",
- "createA": "",
+ "createA": "Créer un",
"remove": "",
"maintenanceWindowDescription": "",
"startTime": "",
@@ -327,22 +255,22 @@
"notFoundButton": "",
"pageSpeedConfigureSettingsDescription": "",
"monitorDisplayName": "",
- "whenNewIncident": "",
+ "whenNewIncident": "Lors d'un nouvel incident,",
"notifySMS": "",
"notifyEmails": "",
"seperateEmails": "",
- "checkFrequency": "",
+ "checkFrequency": "Vérifier la fréquence",
"matchMethod": "",
"expectedValue": "",
"deleteDialogTitle": "",
"deleteDialogDescription": "",
"pageSpeedMonitor": "",
"shown": "",
- "ago": "",
+ "ago": "depuis",
"companyName": "",
"pageSpeedDetailsPerformanceReport": "",
"pageSpeedDetailsPerformanceReportCalculator": "",
- "checkingEvery": "",
+ "checkingEvery": "Vérification tous les",
"statusPageCreateSettings": "",
"basicInformation": "",
"statusPageCreateBasicInfoDescription": "",
@@ -362,7 +290,7 @@
"statusPageStatusNoPage": "",
"statusPageStatusServiceStatus": "",
"deleteStatusPage": "",
- "deleteStatusPageConfirm": "",
+ "deleteStatusPageConfirm": "Oui, supprimer la page de statut",
"deleteStatusPageDescription": "",
"uptimeCreate": "",
"uptimeCreateJsonPath": "",
@@ -379,25 +307,21 @@
"pageSpeedLearnMoreLink": "",
"pageSpeedAddApiKey": "",
"update": "",
- "invalidFileFormat": "",
- "invalidFileSize": "",
- "ClickUpload": "",
+ "invalidFileFormat": "Format de fichier non supporté !",
+ "invalidFileSize": "Le fichier est trop volumineux !",
+ "ClickUpload": "Cliquez pour téléverser",
"DragandDrop": "",
"MaxSize": "",
"SupportedFormats": "",
"FirstName": "",
"LastName": "",
"EmailDescriptionText": "",
- "YourPhoto": "",
+ "YourPhoto": "Photo de profil",
"PhotoDescriptionText": "",
"save": "",
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
- "DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
+ "DeleteWarningTitle": "Vous supprimez vraiment ce compte ?",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -405,153 +329,507 @@
"selectFile": "",
"parsingFailed": "",
"uploadSuccess": "",
- "validationFailed": "",
+ "validationFailed": "Echec de la validation",
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
- "DeleteAccountTitle": "",
- "DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
+ "DeleteAccountTitle": "Supprimer le compte",
+ "DeleteAccountButton": "Supprimer le compte",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
"ignoreTLSErrorDescription": "",
- "createNew": "",
+ "createNew": "Créer un nouveau",
"greeting": {
"prepend": "",
- "append": "",
+ "append": "L'après-midi est votre libre, rendons-le épique !",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
- "admin": "",
+ "admin": "Admin",
"teamMember": "",
"demoUser": ""
},
"teamPanel": {
- "teamMembers": "",
+ "teamMembers": "Membres de l'équipe",
"filter": {
- "all": "",
+ "all": "Tous",
"member": ""
},
"inviteTeamMember": "",
"inviteNewTeamMember": "",
"inviteDescription": "",
- "email": "",
+ "email": "Email",
"selectRole": "",
- "inviteLink": "",
- "cancel": "",
+ "inviteLink": "Lien d'invitation",
+ "cancel": "Annuler",
"noMembers": "",
"getToken": "",
"emailToken": "",
"table": {
"name": "",
- "email": "",
+ "email": "Email",
"role": "",
- "created": ""
+ "created": "Créée"
}
},
"monitorState": {
"paused": "",
"resumed": "",
- "active": ""
+ "active": "Actif"
},
"menu": {
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
- "integrations": "",
+ "integrations": "Intégrations",
"settings": "",
"support": "",
- "discussions": "",
+ "discussions": "Discussions",
"docs": "",
- "changelog": "",
+ "changelog": "Changelog",
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "Déconnexion",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
- "commonSaving": "",
+ "commonSaving": "Sauvegarde...",
"navControls": "",
- "incidentsPageTitle": "",
+ "incidentsPageTitle": "Incidents",
"passwordPanel": {
- "passwordChangedSuccess": "",
- "passwordInputIncorrect": "",
- "currentPassword": "",
- "enterCurrentPassword": "",
- "newPassword": "",
- "enterNewPassword": "",
- "confirmNewPassword": "",
- "passwordRequirements": "",
- "saving": ""
+ "passwordChangedSuccess": "Votre mot de passe a été modifié avec succès.",
+ "passwordInputIncorrect": "La saisie de votre mot de passe est incorrecte.",
+ "currentPassword": "Mot de passe actuel",
+ "enterCurrentPassword": "Entrez votre mot de passe actuel",
+ "newPassword": "Nouveau mot de passe",
+ "enterNewPassword": "Entrez votre nouveau mot de passe",
+ "confirmNewPassword": "Confirmer le nouveau mot de passe",
+ "passwordRequirements": "Le nouveau mot de passe doit contenir au moins 8 caractères et au moins une lettre majuscule, une lettre minuscule, un chiffre et un caractère spécial.",
+ "saving": "Sauvegarde..."
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
- "settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
+ "settingsTestEmailUnknownError": "Erreur inconnue",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json
index 5e7810cde..37604b53b 100644
--- a/client/src/locales/pt-BR.json
+++ b/client/src/locales/pt-BR.json
@@ -1,101 +1,13 @@
{
- "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?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Insira seu e-mail",
- "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",
- "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",
- "settingsDisplayTimezone": "Fuso horário",
- "settingsDisplayTimezoneDescription": "Selecione o fuso horário usado para exibir datas e horas em todo o aplicativo.",
- "settingsAppearance": "Aparência",
- "settingsAppearanceDescription": "Alterne entre o modo claro e escuro ou altere o idioma da interface do usuário",
- "settingsThemeMode": "Tema",
- "settingsLanguage": "Linguagem",
- "settingsDistributedUptime": "Disponibilidade distribuída",
- "settingsDistributedUptimeDescription": "Habilitar/desabilitar o monitoramento de tempo de disponibilidade distribuído.",
- "settingsEnabled": "Habilitado",
"settingsDisabled": "Desabilitado",
- "settingsHistoryAndMonitoring": "Histórico de monitoramento",
- "settingsHistoryAndMonitoringDescription": "Defina por quanto tempo você deseja manter os dados de histórico. Você também pode limpar todos os dados existentes.",
- "settingsTTLLabel": "Os dias em que você deseja manter os dados de histórico.",
- "settingsTTLOptionalLabel": "0 para infinito",
- "settingsClearAllStats": "Limpe todas as estatísticas. Isso é irreversível.",
- "settingsClearAllStatsButton": "Limpar todas as estatísticas",
- "settingsClearAllStatsDialogTitle": "Você quer limpar todas as estatísticas?",
- "settingsClearAllStatsDialogDescription": "Uma vez limpo, o histórico de monitoramento e as estatísticas não podem ser recuperados.",
- "settingsClearAllStatsDialogConfirm": "Sim, limpar todas as estatísticas",
- "settingsDemoMonitors": "Monitores de demonstração",
- "settingsDemoMonitorsDescription": "Adicione monitores de genéricos para fins de demonstração.",
- "settingsAddDemoMonitors": "Adicionando monitores de demonstração",
- "settingsAddDemoMonitorsButton": "Adicionar monitores de demonstração",
- "settingsRemoveAllMonitors": "Removendo todos os monitores",
- "settingsRemoveAllMonitorsButton": "Remover todos os monitores",
- "settingsRemoveAllMonitorsDialogTitle": "Você deseja remover todos os monitores?",
- "settingsRemoveAllMonitorsDialogConfirm": "Sim, remova todos os monitores",
- "settingsWallet": "Carteira",
- "settingsWalletDescription": "Conecte sua carteira aqui. Isso é necessário para que o Distributed Uptime Monitor se conecte a vários nós globalmente.",
- "settingsAbout": "Sobre",
- "settingsDevelopedBy": "Desenvolvido pela Bluewave Labs.",
- "settingsSave": "Salvar",
"settingsSuccessSaved": "Configurações salvas com sucesso",
"settingsFailedToSave": "Falha ao salvar as configurações",
"settingsStatsCleared": "Estatísticas limpas com sucesso",
"settingsFailedToClearStats": "Falha ao limpar estatísticas",
- "settingsDemoMonitorsAdded": "Monitores de demonstração adicionados com sucesso",
"settingsFailedToAddDemoMonitors": "Falha ao adicionar monitores de demonstração",
"settingsMonitorsDeleted": "Todos os monitores foram excluídos com sucesso",
"settingsFailedToDeleteMonitors": "Falha ao excluir todos os monitores",
@@ -108,7 +20,6 @@
"now": "Agora",
"delete": "Deletar",
"configure": "Configurar",
- "networkError": "Erro de rede",
"responseTime": "Tempo de resposta",
"ms": "ms",
"bar": "Bar",
@@ -116,11 +27,6 @@
"country": "PAÍS",
"city": "CIDADE",
"response": "RESPOSTA",
- "checkConnection": "Verifique sua conexão",
- "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",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "Quando ocorrer um incidente, notifique os usuários.",
"distributedUptimeCreateAdvancedSettings": "Configurações avançadas",
"distributedUptimeDetailsNoMonitorHistory": "Ainda não há histórico de verificações para este monitor.",
- "distributedUptimeDetailsFooterHeading": "Feito com ❤️ por UpRock e Bluewave Labs",
- "distributedUptimeDetailsFooterBuilt": "Construído com",
- "distributedUptimeDetailsFooterSolana": "Solana",
- "distributedUptimeDetailsMonitorHeader": "Monitoramento de Uptime distribuído com tecnologia DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Uptime:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Última atualização",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "Notificação de teste enviada com sucesso!",
"testFailed": "Falha ao enviar notificação de teste",
"unsupportedType": "Tipo de notificação não suportado",
- "networkError": "Ocorreu um erro de rede"
+ "networkError": "Ocorreu um erro de rede",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "testLocale",
"add": "Adicionar",
@@ -394,10 +322,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?",
- "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",
@@ -409,32 +333,10 @@
"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",
- "settingsSystemReset": "Resetar sistema",
- "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",
- "backendUnreachable": "Erro de conexão com o servidor",
- "backendUnreachableMessage": "Não conseguimos conectar ao servidor. Verifique sua conexão com a internet ou sua configuração de implantação se o problema persistir.",
- "backendUnreachableError": "Não foi possível conectar ao servidor. Tente novamente mais tarde.",
- "retryConnection": "Reconectar",
- "retryingConnection": "Conectando...",
- "backendReconnected": "Reconectado ao servidor com sucesso.",
- "backendStillUnreachable": "O servidor ainda está inacessível. Tente novamente mais tarde.",
- "backendConnectionError": "Erro ao conectar ao servidor. Verifique sua conexão de internet.",
"maskedPageSpeedKeyPlaceholder": "*************************************",
- "pageSpeedApiKeyFieldTitle": "Google PageSpeed API key",
- "pageSpeedApiKeyFieldLabel": "PageSpeed API key",
- "pageSpeedApiKeyFieldDescription": "Insira sua chave da API do Google PageSpeed para ativar o monitoramento de velocidade de página. Clique em Resetar para atualizar a chave.",
- "pageSpeedApiKeyFieldResetLabel": "A chave de API está definida. Clique em Resetar para alterá-la.",
"reset": "Resetar",
"ignoreTLSError": "Ignorar erro TLS/SSL",
"tlsErrorIgnored": "Erros TLS/SSL ignorados",
@@ -445,11 +347,6 @@
"append": "A tarde é seu playground — vamos torná-la épica!",
"overview": "Aqui está uma visão geral dos seus monitores {{type}}."
},
- "monitorStatus": {
- "up": "Online",
- "down": "Offline",
- "paused": "pausado"
- },
"roles": {
"superAdmin": "Super Admin",
"admin": "Admin",
@@ -488,7 +385,6 @@
"uptime": "Uptime",
"pagespeed": "Pagespeed",
"infrastructure": "Infraestrutura",
- "distributedUptime": "Uptime distribuído",
"incidents": "Incidentes",
"statusPages": "Pagina de status",
"maintenance": "Manutenção",
@@ -501,23 +397,14 @@
"profile": "Perfil",
"password": "Senha",
"team": "Equipe",
- "logOut": "Sair"
+ "logOut": "Sair",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "E-mail",
- "settingsEmailDescription": "Configurações de e-mail do seu sistema. Elas são usadas para enviar notificações e alertas.",
- "settingsEmailHost": "Host de e-mail - Nome do host ou endereço IP do servidor SMTP",
- "settingsEmailPort": "Porta de e-mail - Porta para conectar",
- "settingsEmailAddress": "Endereço de e-mail - Usado para autenticação",
- "settingsEmailPassword": "Senha de e-mail - Senha para autenticação",
"settingsEmailUser": "Usuário de e-mail - Nome de usuário para autenticação, substitui o endereço de e-mail, se especificado",
- "settingsEmailFieldResetLabel": "A senha foi definida. Clique em Redefinir para alterá-la.",
"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",
"commonSaving": "Salvando...",
"navControls": "Controles",
"incidentsPageTitle": "Incidentes",
@@ -532,26 +419,417 @@
"passwordRequirements": "A nova senha deve conter pelo menos 8 caracteres e deve ter pelo menos uma letra maiúscula, uma letra minúscula, um número e um caractere especial.",
"saving": "Salvando..."
},
- "uptimeCreateSelectURL": "Insira a URL ou IP a ser monitorado (por exemplo, https://example.com/ ou 192.168.1.100) e adicione um nome de exibição que irá aparecer no painel.",
- "settingsEmailConnectionHost": "Host de conexão de e-mail - Nome do host a ser usado na saudação HELO/EHLO",
- "sendTestEmail": "Enviar e-mail de teste",
"emailSent": "E-mail enviado com sucesso",
"failedToSendEmail": "Falha ao enviar e-mail",
- "settingsTestEmail": "Enviar e-mail de teste",
"settingsTestEmailSuccess": "E-mail de teste enviado com sucesso",
"settingsTestEmailFailed": "Falha ao enviar e-mail de teste",
"settingsTestEmailFailedWithReason": "Falha ao enviar e-mail de teste: {{reason}}",
"settingsTestEmailUnknownError": "Erro desconhecido",
- "settingsEmailRequiredFields": "O host e a porta do e-mail são obrigatórios",
"statusMsg": {
"paused": "O monitoramento está pausado.",
"up": "Seu site está no ar.",
"down": "Seu site está fora do ar.",
"pending": "Pendente..."
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json
index 60627c23f..a4ec3290f 100644
--- a/client/src/locales/ru.json
+++ b/client/src/locales/ru.json
@@ -1,101 +1,13 @@
{
- "dontHaveAccount": "Нет аккаунта",
- "email": "Почта",
- "forgotPassword": "Забыли пароль",
- "password": "пароль",
- "signUp": "Зарегистрироваться",
"submit": "Подтвердить",
"title": "Название",
- "continue": "Продолжить",
- "enterEmail": "Введите свой email",
- "authLoginTitle": "Войти",
- "authLoginEnterPassword": "Введите свой пароль",
- "commonPassword": "Пароль",
- "commonBack": "Назад",
- "authForgotPasswordTitle": "Забыли пароль?",
- "authForgotPasswordResetPassword": "Сбросить пароль",
- "createPassword": "Создайте свой пароль",
- "createAPassword": "Создайте пароль",
- "authRegisterAlreadyHaveAccount": "Уже есть аккаунт?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "Введите свой email",
- "authRegisterTitle": "Создать аккаунт",
- "authRegisterStepOneTitle": "Создайте свой аккаут",
- "authRegisterStepOneDescription": "Введите свои данные, чтобы начать",
- "authRegisterStepTwoTitle": "Настройте свой профиль",
- "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": "Общие настройки",
- "settingsDisplayTimezone": "Отображать часовой пояс",
- "settingsDisplayTimezoneDescription": "Часовой пояс панели мониторинга, которую вы публично отображаете.",
- "settingsAppearance": "Внешний вид",
- "settingsAppearanceDescription": "Переключение между светлым и темным режимом или изменение языка пользовательского интерфейса",
- "settingsThemeMode": "Тема",
- "settingsLanguage": "Язык",
- "settingsDistributedUptime": "Distributed uptime",
- "settingsDistributedUptimeDescription": "Включить/выключить distributed uptime monitoring.",
- "settingsEnabled": "Включено",
"settingsDisabled": "Выключено",
- "settingsHistoryAndMonitoring": "История и мониторинг",
- "settingsHistoryAndMonitoringDescription": "Определите здесь, как долго вы хотите хранить данные. Вы также можете удалить все прошлые данные.",
- "settingsTTLLabel": "Дни, за которыми вы хотите следить.",
- "settingsTTLOptionalLabel": "0 для бесконечности",
- "settingsClearAllStats": "Очистить всю статистику. Это необратимо.",
- "settingsClearAllStatsButton": "Очистить всю статистику",
- "settingsClearAllStatsDialogTitle": "Хотите очистить всю статистику?",
- "settingsClearAllStatsDialogDescription": "После удаления ваши мониторы не могут быть восстановлены.",
- "settingsClearAllStatsDialogConfirm": "Да, очистить всю статистику",
- "settingsDemoMonitors": "Демо мониторы",
- "settingsDemoMonitorsDescription": "Здесь вы можете добавлять и удалять демонстрационные мониторы.",
- "settingsAddDemoMonitors": "Добавьте демонстрационные мониторы",
- "settingsAddDemoMonitorsButton": "Добавьте демонстрационные мониторы",
- "settingsRemoveAllMonitors": "Удалить все демонстрационные мониторы",
- "settingsRemoveAllMonitorsButton": "Удалить все демонстрационные мониторы",
- "settingsRemoveAllMonitorsDialogTitle": "Хотите удалить все мониторы?",
- "settingsRemoveAllMonitorsDialogConfirm": "Да, очистить все мониторы",
- "settingsWallet": "Кошелёк",
- "settingsWalletDescription": "Подключите свой кошелек здесь. Это необходимо для того, чтобы монитор Distributed Uptime мог подключиться к нескольким узлам по всему миру.",
- "settingsAbout": "О",
- "settingsDevelopedBy": "Developed by Bluewave Labs.",
- "settingsSave": "Сохранить",
"settingsSuccessSaved": "Настройки успешно сохранены",
"settingsFailedToSave": "Не удалось сохранить настройки",
"settingsStatsCleared": "Статистика успешно очищена",
"settingsFailedToClearStats": "Не удалось очистить статистику",
- "settingsDemoMonitorsAdded": "Успешно добавлены демонстрационные мониторы",
"settingsFailedToAddDemoMonitors": "Не удалось добавить демонстрационные мониторы",
"settingsMonitorsDeleted": "Успешно удалены все мониторы",
"settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы",
@@ -108,7 +20,6 @@
"now": "Сейчас",
"delete": "Удалить",
"configure": "Настроить",
- "networkError": "Ошибка сети",
"responseTime": "Время ответа:",
"ms": "мс",
"bar": "Bar",
@@ -116,11 +27,6 @@
"country": "СТРАНА",
"city": "ГОРОД",
"response": "ОТВЕТ",
- "checkConnection": "Пожалуйста, проверьте ваше соединение",
- "passwordreset": "Сброс пароля",
- "authRegisterStepOnePersonalDetails": "Введите свои личные данные",
- "authCheckEmailOpenEmailButton": "Откройте приложение почты",
- "authNewPasswordConfirmed": "Ваш пароль успешно сброшен. Нажмите ниже, чтобы войти в систему магическим образом.",
"monitorStatusUp": "Монитор {name} ({url}) теперь включен и отвечает",
"monitorStatusDown": "Монитор {name} ({url}) ОТКЛЮЧЕН и не отвечает",
"webhookSendSuccess": "Уведомление Webhook успешно отправлено",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "В случае возникновения инцидента сообщите об этом пользователям.",
"distributedUptimeCreateAdvancedSettings": "Расширенные настройки",
"distributedUptimeDetailsNoMonitorHistory": "Для этого монитора пока нет истории проверок.",
- "distributedUptimeDetailsFooterHeading": "Made with ❤️ by UpRock & Bluewave Labs",
- "distributedUptimeDetailsFooterBuilt": "Built on",
- "distributedUptimeDetailsFooterSolana": "Solana",
- "distributedUptimeDetailsMonitorHeader": "Distributed Uptime Monitoring powered by DePIN",
"distributedUptimeDetailsStatusHeaderUptime": "Аптайм:",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Последнее обновление",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "Уведомление о тестировании отправлено успешно!",
"testFailed": "Не удалось отправить тестовое уведомление",
"unsupportedType": "Неподдерживаемый тип уведомления",
- "networkError": "Произошла сетевая ошибка"
+ "networkError": "Произошла сетевая ошибка",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "testLocale",
"add": "Добавить",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "Это приведет к удалению учетной записи и всех связанных с ней данных с сервера. Это необратимо.",
"DeleteAccountWarning": "Удаление вашей учетной записи означает, что вы не сможете снова войти в систему, и все ваши данные будут удалены. Это необратимо.",
"DeleteWarningTitle": "Действительно удаляете эту учетную запись?",
- "authRegisterFirstName": "Имя",
- "authRegisterLastName": "Фамилия",
- "authRegisterEmail": "Email",
- "authRegisterEmailRequired": "Чтобы продолжить, пожалуйста, введите свой адрес электронной почты",
"bulkImport": {
"title": "Массовый импорт",
"selectFileTips": "Выберите CSV-файл для загрузки",
@@ -409,32 +333,10 @@
"noFileSelected": "Файл не выбран",
"fallbackPage": "Импортируйте файл для массовой загрузки списка серверов"
},
- "welcomeBack": "Добро пожаловать обратно! Вы успешно вошли в систему.",
- "authRegisterLoginLink": "Войти",
- "validationNameRequired": "Пожалуйста, введите свое имя",
- "validationNameTooLong": "Длина имени не должна превышать 50 символов",
- "validationNameInvalidCharacters": "Пожалуйста, используйте только буквы, пробелы, апострофы или дефисы",
- "settingsSystemReset": "Сброс системы",
- "settingsSystemResetDescription": "Удалите все мониторы из вашей системы.",
"DeleteAccountTitle": "Удалить аккаунт",
"DeleteAccountButton": "Удалить аккаунт",
- "authRegisterEmailInvalid": "Пожалуйста, введите действительный адрес электронной почты",
"publicLink": "Публичная ссылка",
- "doNotHaveAccount": "У вас нет учетной записи?",
- "registerHere": "Зарегистрируйтесь здесь",
- "backendUnreachable": "Ошибка подключения к серверу",
- "backendUnreachableMessage": "Нам не удается подключиться к серверу. Пожалуйста, проверьте подключение к Интернету или конфигурацию вашего развертывания, если проблема не устранена.",
- "backendUnreachableError": "Не удается подключиться к серверу. Пожалуйста, повторите попытку позже.",
- "retryConnection": "Повторите попытку подключения",
- "retryingConnection": "Подключение...",
- "backendReconnected": "Успешно подключен к серверу.",
- "backendStillUnreachable": "Сервер по-прежнему недоступен. Пожалуйста, повторите попытку позже.",
- "backendConnectionError": "Ошибка подключения к серверу. Пожалуйста, проверьте свое сетевое подключение.",
"maskedPageSpeedKeyPlaceholder": "*************************************",
- "pageSpeedApiKeyFieldTitle": "Ключ API Google PageSpeed",
- "pageSpeedApiKeyFieldLabel": "Ключ API PageSpeed",
- "pageSpeedApiKeyFieldDescription": "Введите свой API-ключ Google PageSpeed, чтобы включить мониторинг скорости страницы. Нажмите \"Сброс\", чтобы обновить ключ.",
- "pageSpeedApiKeyFieldResetLabel": "Установлен ключ API. Нажмите \"Сброс\", чтобы изменить его.",
"reset": "Сброс",
"ignoreTLSError": "Игнорировать ошибку TLS/SSL",
"tlsErrorIgnored": "Ошибки TLS/SSL игнорируются",
@@ -445,11 +347,6 @@
"append": "Вторая половина дня — это ваша игровая площадка - давайте сделаем ее эпичной!",
"overview": "Вот обзор ваших мониторов {{type}}."
},
- "monitorStatus": {
- "up": "доступен",
- "down": "недоступен",
- "paused": "остановлен"
- },
"roles": {
"superAdmin": "Главный админ",
"admin": "Админ",
@@ -488,7 +385,6 @@
"uptime": "Аптайм",
"pagespeed": "Pagespeed",
"infrastructure": "Инфраструктура",
- "distributedUptime": "Распределенный Аптайм",
"incidents": "Инциденты",
"statusPages": "Страницы статуса",
"maintenance": "Обслуживание",
@@ -501,23 +397,14 @@
"profile": "Профиль",
"password": "Пароль",
"team": "Команда",
- "logOut": "Выйти"
+ "logOut": "Выйти",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "Настройки Email",
- "settingsEmailDescription": "Настройка параметров email",
- "settingsEmailHost": "Email host",
- "settingsEmailPort": "Email port",
- "settingsEmailAddress": "Email address",
- "settingsEmailPassword": "Email пароль",
"settingsEmailUser": "Email пользователь",
- "settingsEmailFieldResetLabel": "Пароль установлен. Нажмите \"Сброс\", чтобы изменить его.",
"state": "Состояние",
"statusBreadCrumbsStatusPages": "Страницы статуса",
"statusBreadCrumbsDetails": "Подробности",
- "authForgotPasswordInstructions": "Не беспокойтесь, мы вышлем вам инструкции по сбросу настроек.",
- "settingsThemeModeLight": "Светлая",
- "settingsThemeModeDark": "Тёмная",
- "settingsClearAllStatsDialogCancel": "Отменить",
"commonSaving": "Сохранение...",
"navControls": "Управления",
"incidentsPageTitle": "Инциденты",
@@ -532,26 +419,417 @@
"passwordRequirements": "Новый пароль должен содержать не менее 8 символов и содержать как минимум одну заглавную букву, одну строчную букву, одну цифру и один специальный символ.",
"saving": "Сохранение..."
},
- "uptimeCreateSelectURL": "Введите URL-адрес или IP-адрес для отслеживания (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет отображаться на панели мониторинга.",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json
index 91626478e..e9b8f0850 100644
--- a/client/src/locales/tr.json
+++ b/client/src/locales/tr.json
@@ -1,101 +1,13 @@
{
- "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ı?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "E-posta adresinizi girin",
- "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",
- "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",
- "settingsDisplayTimezone": "Görüntüleme saat dilimi",
- "settingsDisplayTimezoneDescription": "Herkese açık olarak görüntülediğiniz kontrol panelinin saat dilimi.",
- "settingsAppearance": "Görünüm",
- "settingsAppearanceDescription": "Açık ve koyu mod arasında geçiş yapın veya kullanıcı arayüzü dilini değiştirin",
- "settingsThemeMode": "Tema",
- "settingsLanguage": "Dil",
- "settingsDistributedUptime": "Dağıtılmış çalışma süresi",
- "settingsDistributedUptimeDescription": "Dağıtılmış çalışma süresi izlemeyi etkinleştirin/devre dışı bırakın.",
- "settingsEnabled": "Etkin",
"settingsDisabled": "Devre dışı",
- "settingsHistoryAndMonitoring": "Geçmiş ve izleme",
- "settingsHistoryAndMonitoringDescription": "Verileri ne kadar süreyle saklamak istediğinizi burada tanımlayın. Ayrıca tüm geçmiş verileri kaldırabilirsiniz.",
- "settingsTTLLabel": "İzleme geçmişini saklamak istediğiniz gün sayısı.",
- "settingsTTLOptionalLabel": "Sınırsız için 0",
- "settingsClearAllStats": "Tüm istatistikleri temizle. Bu geri alınamaz.",
- "settingsClearAllStatsButton": "Tüm istatistikleri temizle",
- "settingsClearAllStatsDialogTitle": "Tüm istatistikleri temizlemek istiyor musunuz?",
- "settingsClearAllStatsDialogDescription": "Silindikten sonra, monitörleriniz geri alınamaz.",
- "settingsClearAllStatsDialogConfirm": "Evet, tüm istatistikleri temizle",
- "settingsDemoMonitors": "Demo monitörler",
- "settingsDemoMonitorsDescription": "Burada demo monitörler ekleyebilir ve kaldırabilirsiniz.",
- "settingsAddDemoMonitors": "Demo monitörler ekle",
- "settingsAddDemoMonitorsButton": "Demo monitörler ekle",
- "settingsRemoveAllMonitors": "Tüm monitörleri kaldır",
- "settingsRemoveAllMonitorsButton": "Tüm monitörleri kaldır",
- "settingsRemoveAllMonitorsDialogTitle": "Tüm monitörleri kaldırmak istiyor musunuz?",
- "settingsRemoveAllMonitorsDialogConfirm": "Evet, tüm monitörleri temizle",
- "settingsWallet": "Cüzdan",
- "settingsWalletDescription": "Cüzdanınızı buradan bağlayın. Bu, Dağıtılmış Çalışma Süresi monitörünün küresel olarak birden çok düğüme bağlanması için gereklidir.",
- "settingsAbout": "Hakkında",
- "settingsDevelopedBy": "Bluewave Labs tarafından geliştirilmiştir.",
- "settingsSave": "Kaydet",
"settingsSuccessSaved": "Ayarlar başarıyla kaydedildi",
"settingsFailedToSave": "Ayarlar kaydedilemedi",
"settingsStatsCleared": "İstatistikler başarıyla temizlendi",
"settingsFailedToClearStats": "İstatistikler temizlenemedi",
- "settingsDemoMonitorsAdded": "Demo monitörler başarıyla eklendi",
"settingsFailedToAddDemoMonitors": "Demo monitörler eklenemedi",
"settingsMonitorsDeleted": "Tüm monitörler başarıyla silindi",
"settingsFailedToDeleteMonitors": "Monitörler silinemedi",
@@ -108,7 +20,6 @@
"now": "Şimdi",
"delete": "Sil",
"configure": "Yapılandır",
- "networkError": "Ağ hatası",
"responseTime": "Yanıt süresi:",
"ms": "ms",
"bar": "Çubuk",
@@ -116,11 +27,6 @@
"country": "ÜLKE",
"city": "ŞEHİR",
"response": "YANIT",
- "checkConnection": "Lütfen bağlantınızı kontrol edin",
- "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",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "Bir olay olduğunda kullanıcıları bilgilendir.",
"distributedUptimeCreateAdvancedSettings": "Gelişmiş ayarlar",
"distributedUptimeDetailsNoMonitorHistory": "Bu monitör için henüz bir denetim günlüğü oluşturulmadı.",
- "distributedUptimeDetailsFooterHeading": "UpRock & Bluewave Labs tarafından geliştirildi",
- "distributedUptimeDetailsFooterBuilt": "Altyapı",
- "distributedUptimeDetailsFooterSolana": "Solana",
- "distributedUptimeDetailsMonitorHeader": "Depin Destekli Dağıtık Kesintisizlik İzleme",
"distributedUptimeDetailsStatusHeaderUptime": "Erişilebilirlik",
"distributedUptimeDetailsStatusHeaderLastUpdate": "Son güncelleme",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "Test bildirimi başarıyla gönderildi",
"testFailed": "Test bildirimi gönderilemedi",
"unsupportedType": "Desteklenmeyen bildirim türü",
- "networkError": "Ağ hatası oluştu"
+ "networkError": "Ağ hatası oluştu",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "Bildirim kanalı oluştur",
+ "createTitle": "Bildirim kanalı",
+ "create": {
+ "success": "",
+ "failed": "Bildirim kanalı oluşturulamadı"
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "Test bildirimi başarıyla gönderildi",
+ "failed": ""
+ }
},
"testLocale": "TEST13 UPLOAD",
"add": "Ekle",
@@ -394,10 +322,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?",
- "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",
@@ -409,65 +333,38 @@
"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",
- "settingsSystemReset": "Sistem sıfırla",
- "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",
- "backendUnreachable": "Sunucu bağlantı hatası",
- "backendUnreachableMessage": "Sunucuya bağlanamıyoruz. Lütfen internet bağlantınızı kontrol edin veya sorun devam ederse dağıtım yapılandırmanızı doğrulayın.",
- "backendUnreachableError": "Sunucuya bağlanılamıyor. Lütfen daha sonra tekrar deneyin.",
- "retryConnection": "Bağlantıyı yeniden deneyin",
- "retryingConnection": "Bağlanıyor...",
- "backendReconnected": "Sunucuya başarıyla yeniden bağlanıldı.",
- "backendStillUnreachable": "Sunucuya hala ulaşılamıyor. Lütfen daha sonra tekrar deneyin.",
- "backendConnectionError": "Sunucuya bağlanırken hata oluştu. Lütfen ağ bağlantınızı kontrol edin.",
"maskedPageSpeedKeyPlaceholder": "*************************************",
- "pageSpeedApiKeyFieldTitle": "Google PageSpeed API anahtarı",
- "pageSpeedApiKeyFieldLabel": "PageSpeed API anahtarı",
- "pageSpeedApiKeyFieldDescription": "Sayfa hızı izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.",
- "pageSpeedApiKeyFieldResetLabel": "API anahtarı ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.",
"reset": "Sıfırla",
- "ignoreTLSError": "",
- "tlsErrorIgnored": "",
+ "ignoreTLSError": "TLS/SSL hatalarını gözardı et",
+ "tlsErrorIgnored": "TLS/SSL hataları gözardı ediliyor",
"ignoreTLSErrorDescription": "",
- "createNew": "",
+ "createNew": "Yeni oluştur",
"greeting": {
- "prepend": "",
+ "prepend": "Merhaba",
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
- "superAdmin": "",
- "admin": "",
- "teamMember": "",
- "demoUser": ""
+ "superAdmin": "Süper yönetici",
+ "admin": "Yönetici",
+ "teamMember": "Ekip elemanı",
+ "demoUser": "Demo kullanıcısı"
},
"teamPanel": {
- "teamMembers": "",
+ "teamMembers": "Ekip elemanları",
"filter": {
- "all": "",
- "member": ""
+ "all": "Tümü",
+ "member": "Eleman"
},
- "inviteTeamMember": "",
- "inviteNewTeamMember": "",
+ "inviteTeamMember": "Ekip elemanı davet et",
+ "inviteNewTeamMember": "Yeni ekip elemanı davet et",
"inviteDescription": "",
- "email": "",
- "selectRole": "",
- "inviteLink": "",
+ "email": "Eposta",
+ "selectRole": "Rolü seçin",
+ "inviteLink": "Davet linki",
"cancel": "",
"noMembers": "",
"getToken": "",
@@ -488,70 +385,451 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
- "maintenance": "",
- "integrations": "",
- "settings": "",
- "support": "",
- "discussions": "",
- "docs": "",
- "changelog": "",
- "profile": "",
- "password": "",
- "team": "",
- "logOut": ""
+ "maintenance": "Bakım",
+ "integrations": "Entegrasyonlar",
+ "settings": "Ayarlar",
+ "support": "Destek",
+ "discussions": "Soru-cevap",
+ "docs": "Dokümanlar",
+ "changelog": "Değişiklik günlüğü",
+ "profile": "Profil",
+ "password": "Parola",
+ "team": "Ekip",
+ "logOut": "Çıkış yap",
+ "notifications": "Bildirimler",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
- "settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
- "state": "",
- "statusBreadCrumbsStatusPages": "",
- "statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
- "commonSaving": "",
- "navControls": "",
- "incidentsPageTitle": "",
+ "settingsEmailUser": "E-posta kullanıcısı – Kimlik doğrulama için kullanıcı adı; belirtilirse e-posta adresinin yerine geçer.",
+ "state": "Durum",
+ "statusBreadCrumbsStatusPages": "Durum sayfası",
+ "statusBreadCrumbsDetails": "Detaylar",
+ "commonSaving": "Kaydediliyor.",
+ "navControls": "Kontroller",
+ "incidentsPageTitle": "Olaylar",
"passwordPanel": {
- "passwordChangedSuccess": "",
- "passwordInputIncorrect": "",
- "currentPassword": "",
- "enterCurrentPassword": "",
- "newPassword": "",
- "enterNewPassword": "",
- "confirmNewPassword": "",
- "passwordRequirements": "",
- "saving": ""
+ "passwordChangedSuccess": "Şifreniz başarı ile değiştirildi.",
+ "passwordInputIncorrect": "Şifreniz hatalı",
+ "currentPassword": "Mevcut parola",
+ "enterCurrentPassword": "Mevcut parolanızı girin",
+ "newPassword": "Yeni parola",
+ "enterNewPassword": "Yeni parolanızı girin",
+ "confirmNewPassword": "Yeni şifrenizi onaylayın",
+ "passwordRequirements": "Yeni şifreniz en az 8 karakterden oluşmalı ve en az bir büyük harf, bir küçük harf, bir rakam ve bir özel karakter içermelidir.",
+ "saving": "Kaydediliyor..."
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
- "emailSent": "",
- "failedToSendEmail": "",
- "settingsTestEmail": "",
- "settingsTestEmailSuccess": "",
- "settingsTestEmailFailed": "",
- "settingsTestEmailFailedWithReason": "",
- "settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
+ "emailSent": "E-posta başarı ile gönderildi.",
+ "failedToSendEmail": "E-posta gönderimi başarısız.",
+ "settingsTestEmailSuccess": "Test E-postası başarı ile gönderildi.",
+ "settingsTestEmailFailed": "Test E-postası gönderimi başarısız.",
+ "settingsTestEmailFailedWithReason": "Test E-postası gönderimi başarısız: {{reason}}",
+ "settingsTestEmailUnknownError": "Bilinmeyen hata.",
"statusMsg": {
- "paused": "",
- "up": "",
- "down": "",
- "pending": ""
+ "paused": "İzleme duraklatıldı.",
+ "up": "Siteniz yayında.",
+ "down": "Siteniz kapalı.",
+ "pending": "Sırada.."
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "Checkmate",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "Lütfen bağlantınızı kontrol edin",
+ "unknownError": "Bilinmeyen hata"
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "Devam et",
+ "back": "Geri"
+ },
+ "inputs": {
+ "email": {
+ "label": "Eposta",
+ "placeholder": "",
+ "errors": {
+ "empty": "Devam etmek için eposta adresinizi girin",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "Parola",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": "8 karakter uzunluğunda"
+ },
+ "special": {
+ "beginning": "En az içermelidir: ",
+ "highlighted": "bir özel karakter"
+ },
+ "number": {
+ "beginning": "En az içermelidir: ",
+ "highlighted": "bir sayı"
+ },
+ "uppercase": {
+ "beginning": "En az içermelidir: ",
+ "highlighted": "Bir büyük harf"
+ },
+ "lowercase": {
+ "beginning": "En az içermelidir: ",
+ "highlighted": "Bir küçük harf"
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": "eşleşmelidir"
+ }
+ },
+ "errors": {
+ "empty": "Lütfen parolanızı girin",
+ "length": "Parolanız en az 8 karakter uzunluğunda olmalıdır",
+ "uppercase": "Parola en az 1 büyük harf içermelidir",
+ "lowercase": "Parola en az 1 küçük harf içermelidir",
+ "number": "Parola en az 1 sayı içermelidir",
+ "special": "Parola en az 1 özel karakter içermelidir",
+ "incorrect": "Parolanız sistemdeki ile uyuşmuyor"
+ }
+ },
+ "passwordConfirm": {
+ "label": "Parolayı onayla",
+ "placeholder": "Onaylamak için parolayı yeniden gir",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "Soyadı",
+ "placeholder": "Ellis",
+ "errors": {
+ "empty": "Lütfen soyadınızı girin",
+ "length": "Soyadı en çok 50 karakter olmalıdır",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "Eposta adresinizi girin",
+ "stepTwo": "Parolanızı girin"
+ },
+ "links": {
+ "forgotPassword": "Parolanızı mı unuttunuz?",
+ "register": "Hesabınız yok mu?",
+ "forgotPasswordLink": "Parolayı sıfırla",
+ "registerLink": "Buradan kayıt olun"
+ },
+ "toasts": {
+ "success": "Tekrar hoşgeldiniz! Başarıyla sisteme giriş yaptınız.",
+ "incorrectPassword": "Hatalı parola"
+ },
+ "errors": {
+ "password": {
+ "incorrect": "Giriş yaptığınız parola sistemdekiyle uyuşmuyor"
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "Süper yönetici oluştur",
+ "user": "Kayıt ol"
+ },
+ "subheadings": {
+ "stepOne": "Kişisel bilgilerinizi girin",
+ "stepTwo": "Eposta adresinizi girin",
+ "stepThree": "Parolanızı oluturun"
+ },
+ "description": {
+ "superAdmin": "Başlamak için süper yönetici hesabı oluşturun",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "Süper yönetici hesabı oluşturun",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": "Bir hesabınız mı var? Giriş yapın "
+ },
+ "toasts": {
+ "success": "Hoşgeldiniz! Hesabınız başarıyla oluşturuldu."
+ }
+ },
+ "forgotPassword": {
+ "heading": "Parolanızı mı kaybettiniz?",
+ "subheadings": {
+ "stepOne": "Endişelenmeyin, sıfırlamak için gerekli bilgileri göndereceğiz.",
+ "stepTwo": "Parola sıfırlama için gerekli bilgileri hesabına ilettik.",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "Eposta uygulamasını aç",
+ "resetPassword": "Parolayı sıfırla"
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "Sunucu bağlantı hatası",
+ "description": "",
+ "retryButton": {
+ "default": "Bağlantıyı tekrarla",
+ "processing": "Bağlanıyor..."
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "Bildirim kanalları oluştur",
+ "nameSettings": {
+ "title": "İsim",
+ "description": "Entegrasyon için açıklama",
+ "nameLabel": "İsim",
+ "namePlaceholder": "örn. Slack bildirimleri"
+ },
+ "typeSettings": {
+ "title": "Tür",
+ "description": "",
+ "typeLabel": "Tür"
+ },
+ "emailSettings": {
+ "title": "Eposta",
+ "description": "Hedef eposta adresi",
+ "emailLabel": "Eposta adresi",
+ "emailPlaceholder": "örn. john@example.com"
+ },
+ "slackSettings": {
+ "title": "Slack",
+ "description": "Slack webhook'u burada tanımlayın",
+ "webhookLabel": "Slack webhook adresi",
+ "webhookPlaceholder": "https://hooks.slack.com/services/...\n"
+ },
+ "pagerdutySettings": {
+ "title": "PagerDuty",
+ "description": "PagerDuty entegrasyonunu burada yapın",
+ "integrationKeyLabel": "Entegrasyon anahtarı",
+ "integrationKeyPlaceholder": "1234567890"
+ },
+ "discordSettings": {
+ "title": "Discord",
+ "description": "Discord webhook'u buradan ayarlayın",
+ "webhookLabel": "Discord Webhook adresi",
+ "webhookPlaceholder": "https://sunucu-adı.com/webhook\n"
+ },
+ "webhookSettings": {
+ "title": "Webhook",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": "https://sunucu-adı.com/webhook\n"
+ }
+ },
+ "notificationConfig": {
+ "title": "Bildirimler",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "ayakta",
+ "down": "çalışmıyor",
+ "paused": "beklemede"
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "Bildirimleri dene",
+ "selectAll": "Tümünü seç",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json
index 8a34a6835..21f53a7bf 100644
--- a/client/src/locales/zh-TW.json
+++ b/client/src/locales/zh-TW.json
@@ -1,101 +1,13 @@
{
- "dontHaveAccount": "沒有帳號",
- "email": "E-mail",
- "forgotPassword": "忘記密碼",
- "password": "密碼",
- "signUp": "註冊",
- "submit": "",
- "title": "",
- "continue": "繼續",
- "enterEmail": "輸入您的 E-mail",
- "authLoginTitle": "登入",
- "authLoginEnterPassword": "輸入您的密碼",
- "commonPassword": "密碼",
- "commonBack": "回去",
- "authForgotPasswordTitle": "忘記密碼?",
- "authForgotPasswordResetPassword": "",
- "createPassword": "",
- "createAPassword": "",
- "authRegisterAlreadyHaveAccount": "已經有帳號嗎?",
- "commonAppName": "Checkmate",
- "authLoginEnterEmail": "輸入您的 E-mail",
- "authRegisterTitle": "建立帳號",
- "authRegisterStepOneTitle": "建立您的帳號",
- "authRegisterStepOneDescription": "清輸入您的個人檔案",
- "authRegisterStepTwoTitle": "設定您的個人檔案",
- "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": "註冊即表示您同意我們的",
+ "submit": "提交",
+ "title": "名稱",
"distributedStatusHeaderText": "實時、真實設備覆蓋",
"distributedStatusSubHeaderText": "由全球數百萬個設備提供支持,您可以按全球區域、國家或城市查看系統效能",
- "settingsGeneralSettings": "一般設定",
- "settingsDisplayTimezone": "顯示時區",
- "settingsDisplayTimezoneDescription": "您公開顯示的儀表板的時區。",
- "settingsAppearance": "外觀",
- "settingsAppearanceDescription": "",
- "settingsThemeMode": "",
- "settingsLanguage": "",
- "settingsDistributedUptime": "",
- "settingsDistributedUptimeDescription": "",
- "settingsEnabled": "",
"settingsDisabled": "",
- "settingsHistoryAndMonitoring": "",
- "settingsHistoryAndMonitoringDescription": "",
- "settingsTTLLabel": "",
- "settingsTTLOptionalLabel": "",
- "settingsClearAllStats": "",
- "settingsClearAllStatsButton": "",
- "settingsClearAllStatsDialogTitle": "",
- "settingsClearAllStatsDialogDescription": "",
- "settingsClearAllStatsDialogConfirm": "",
- "settingsDemoMonitors": "",
- "settingsDemoMonitorsDescription": "",
- "settingsAddDemoMonitors": "",
- "settingsAddDemoMonitorsButton": "",
- "settingsRemoveAllMonitors": "",
- "settingsRemoveAllMonitorsButton": "",
- "settingsRemoveAllMonitorsDialogTitle": "",
- "settingsRemoveAllMonitorsDialogConfirm": "",
- "settingsWallet": "",
- "settingsWalletDescription": "",
- "settingsAbout": "",
- "settingsDevelopedBy": "",
- "settingsSave": "",
"settingsSuccessSaved": "",
"settingsFailedToSave": "",
"settingsStatsCleared": "",
"settingsFailedToClearStats": "",
- "settingsDemoMonitorsAdded": "",
"settingsFailedToAddDemoMonitors": "",
"settingsMonitorsDeleted": "",
"settingsFailedToDeleteMonitors": "",
@@ -108,7 +20,6 @@
"now": "",
"delete": "",
"configure": "",
- "networkError": "",
"responseTime": "",
"ms": "",
"bar": "",
@@ -116,11 +27,6 @@
"country": "",
"city": "",
"response": "",
- "checkConnection": "",
- "passwordreset": "",
- "authRegisterStepOnePersonalDetails": "",
- "authCheckEmailOpenEmailButton": "",
- "authNewPasswordConfirmed": "",
"monitorStatusUp": "",
"monitorStatusDown": "",
"webhookSendSuccess": "",
@@ -136,10 +42,6 @@
"distributedUptimeCreateIncidentDescription": "",
"distributedUptimeCreateAdvancedSettings": "",
"distributedUptimeDetailsNoMonitorHistory": "",
- "distributedUptimeDetailsFooterHeading": "",
- "distributedUptimeDetailsFooterBuilt": "",
- "distributedUptimeDetailsFooterSolana": "",
- "distributedUptimeDetailsMonitorHeader": "",
"distributedUptimeDetailsStatusHeaderUptime": "",
"distributedUptimeDetailsStatusHeaderLastUpdate": "",
"notifications": {
@@ -181,7 +83,33 @@
"testSuccess": "",
"testFailed": "",
"unsupportedType": "",
- "networkError": ""
+ "networkError": "",
+ "fallback": {
+ "title": "",
+ "checks": [""]
+ },
+ "createButton": "",
+ "createTitle": "",
+ "create": {
+ "success": "",
+ "failed": ""
+ },
+ "fetch": {
+ "success": "",
+ "failed": ""
+ },
+ "delete": {
+ "success": "",
+ "failed": ""
+ },
+ "edit": {
+ "success": "",
+ "failed": ""
+ },
+ "test": {
+ "success": "",
+ "failed": ""
+ }
},
"testLocale": "",
"add": "",
@@ -394,10 +322,6 @@
"DeleteDescriptionText": "",
"DeleteAccountWarning": "",
"DeleteWarningTitle": "",
- "authRegisterFirstName": "",
- "authRegisterLastName": "",
- "authRegisterEmail": "",
- "authRegisterEmailRequired": "",
"bulkImport": {
"title": "",
"selectFileTips": "",
@@ -409,32 +333,10 @@
"noFileSelected": "",
"fallbackPage": ""
},
- "welcomeBack": "",
- "authRegisterLoginLink": "",
- "validationNameRequired": "",
- "validationNameTooLong": "",
- "validationNameInvalidCharacters": "",
- "settingsSystemReset": "",
- "settingsSystemResetDescription": "",
"DeleteAccountTitle": "",
"DeleteAccountButton": "",
- "authRegisterEmailInvalid": "",
"publicLink": "",
- "doNotHaveAccount": "",
- "registerHere": "",
- "backendUnreachable": "",
- "backendUnreachableMessage": "",
- "backendUnreachableError": "",
- "retryConnection": "",
- "retryingConnection": "",
- "backendReconnected": "",
- "backendStillUnreachable": "",
- "backendConnectionError": "",
"maskedPageSpeedKeyPlaceholder": "",
- "pageSpeedApiKeyFieldTitle": "",
- "pageSpeedApiKeyFieldLabel": "",
- "pageSpeedApiKeyFieldDescription": "",
- "pageSpeedApiKeyFieldResetLabel": "",
"reset": "",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
@@ -445,11 +347,6 @@
"append": "",
"overview": ""
},
- "monitorStatus": {
- "up": "",
- "down": "",
- "paused": ""
- },
"roles": {
"superAdmin": "",
"admin": "",
@@ -488,7 +385,6 @@
"uptime": "",
"pagespeed": "",
"infrastructure": "",
- "distributedUptime": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
@@ -501,23 +397,14 @@
"profile": "",
"password": "",
"team": "",
- "logOut": ""
+ "logOut": "",
+ "notifications": "",
+ "logs": ""
},
- "settingsEmail": "",
- "settingsEmailDescription": "",
- "settingsEmailHost": "",
- "settingsEmailPort": "",
- "settingsEmailAddress": "",
- "settingsEmailPassword": "",
"settingsEmailUser": "",
- "settingsEmailFieldResetLabel": "",
"state": "",
"statusBreadCrumbsStatusPages": "",
"statusBreadCrumbsDetails": "",
- "authForgotPasswordInstructions": "",
- "settingsThemeModeLight": "",
- "settingsThemeModeDark": "",
- "settingsClearAllStatsDialogCancel": "",
"commonSaving": "",
"navControls": "",
"incidentsPageTitle": "",
@@ -532,26 +419,417 @@
"passwordRequirements": "",
"saving": ""
},
- "uptimeCreateSelectURL": "",
- "settingsEmailConnectionHost": "",
- "sendTestEmail": "",
"emailSent": "",
"failedToSendEmail": "",
- "settingsTestEmail": "",
"settingsTestEmailSuccess": "",
"settingsTestEmailFailed": "",
"settingsTestEmailFailedWithReason": "",
"settingsTestEmailUnknownError": "",
- "settingsEmailRequiredFields": "",
"statusMsg": {
"paused": "",
"up": "",
"down": "",
"pending": ""
},
- "settingsURLTitle": "",
- "settingsURLDescription": "",
- "settingsURLSelectTitle": "",
- "settingsURLEnabled": "",
- "settingURLDisabled": ""
+ "uptimeGeneralInstructions": {
+ "http": "",
+ "ping": "",
+ "docker": "",
+ "port": ""
+ },
+ "common": {
+ "appName": "",
+ "monitoringAgentName": "",
+ "buttons": {
+ "toggleTheme": ""
+ },
+ "toasts": {
+ "networkError": "",
+ "checkConnection": "",
+ "unknownError": ""
+ }
+ },
+ "auth": {
+ "common": {
+ "navigation": {
+ "continue": "",
+ "back": ""
+ },
+ "inputs": {
+ "email": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "invalid": ""
+ }
+ },
+ "password": {
+ "label": "",
+ "rules": {
+ "length": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "special": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "number": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "uppercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "lowercase": {
+ "beginning": "",
+ "highlighted": ""
+ },
+ "match": {
+ "beginning": "",
+ "highlighted": ""
+ }
+ },
+ "errors": {
+ "empty": "",
+ "length": "",
+ "uppercase": "",
+ "lowercase": "",
+ "number": "",
+ "special": "",
+ "incorrect": ""
+ }
+ },
+ "passwordConfirm": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "different": ""
+ }
+ },
+ "firstName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ },
+ "lastName": {
+ "label": "",
+ "placeholder": "",
+ "errors": {
+ "empty": "",
+ "length": "",
+ "pattern": ""
+ }
+ }
+ },
+ "errors": {
+ "validation": ""
+ },
+ "fields": {
+ "password": {
+ "errors": {
+ "incorrect": ""
+ }
+ }
+ }
+ },
+ "login": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": ""
+ },
+ "links": {
+ "forgotPassword": "",
+ "register": "",
+ "forgotPasswordLink": "",
+ "registerLink": ""
+ },
+ "toasts": {
+ "success": "",
+ "incorrectPassword": ""
+ },
+ "errors": {
+ "password": {
+ "incorrect": ""
+ }
+ }
+ },
+ "registration": {
+ "heading": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": ""
+ },
+ "description": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "gettingStartedButton": {
+ "superAdmin": "",
+ "user": ""
+ },
+ "termsAndPolicies": "",
+ "links": {
+ "login": ""
+ },
+ "toasts": {
+ "success": ""
+ }
+ },
+ "forgotPassword": {
+ "heading": "",
+ "subheadings": {
+ "stepOne": "",
+ "stepTwo": "",
+ "stepThree": "",
+ "stepFour": ""
+ },
+ "buttons": {
+ "openEmail": "",
+ "resetPassword": ""
+ },
+ "imageAlts": {
+ "passwordKey": "",
+ "email": "",
+ "lock": "",
+ "passwordConfirm": ""
+ },
+ "links": {
+ "login": "",
+ "resend": ""
+ },
+ "toasts": {
+ "sent": "",
+ "emailNotFound": "",
+ "redirect": "",
+ "success": "",
+ "error": ""
+ }
+ }
+ },
+ "errorPages": {
+ "serverUnreachable": {
+ "toasts": {
+ "reconnected": "",
+ "stillUnreachable": ""
+ },
+ "alertBox": "",
+ "description": "",
+ "retryButton": {
+ "default": "",
+ "processing": ""
+ }
+ }
+ },
+ "createNotifications": {
+ "title": "",
+ "nameSettings": {
+ "title": "",
+ "description": "",
+ "nameLabel": "",
+ "namePlaceholder": ""
+ },
+ "typeSettings": {
+ "title": "",
+ "description": "",
+ "typeLabel": ""
+ },
+ "emailSettings": {
+ "title": "",
+ "description": "",
+ "emailLabel": "",
+ "emailPlaceholder": ""
+ },
+ "slackSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "pagerdutySettings": {
+ "title": "",
+ "description": "",
+ "integrationKeyLabel": "",
+ "integrationKeyPlaceholder": ""
+ },
+ "discordSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ },
+ "webhookSettings": {
+ "title": "",
+ "description": "",
+ "webhookLabel": "",
+ "webhookPlaceholder": ""
+ }
+ },
+ "notificationConfig": {
+ "title": "",
+ "description": ""
+ },
+ "monitorStatus": {
+ "checkingEvery": "",
+ "withCaptureAgent": "",
+ "up": "",
+ "down": "",
+ "paused": ""
+ },
+ "advancedMatching": "",
+ "sendTestNotifications": "",
+ "selectAll": "",
+ "showAdminLoginLink": "",
+ "logsPage": {
+ "title": "",
+ "description": "",
+ "tabs": {
+ "queue": "",
+ "logs": ""
+ },
+ "toast": {
+ "fetchLogsSuccess": ""
+ },
+ "logLevelSelect": {
+ "title": "",
+ "values": {
+ "all": "",
+ "info": "",
+ "warn": "",
+ "error": "",
+ "debug": ""
+ }
+ }
+ },
+ "queuePage": {
+ "title": "",
+ "refreshButton": "",
+ "flushButton": "",
+ "jobTable": {
+ "title": "",
+ "idHeader": "",
+ "urlHeader": "",
+ "typeHeader": "",
+ "activeHeader": "",
+ "lockedAtHeader": "",
+ "runCountHeader": "",
+ "failCountHeader": "",
+ "lastRunHeader": "",
+ "lastFinishedAtHeader": "",
+ "lastRunTookHeader": ""
+ },
+ "metricsTable": {
+ "title": "",
+ "metricHeader": "",
+ "valueHeader": ""
+ },
+ "failedJobTable": {
+ "title": "",
+ "monitorIdHeader": "",
+ "monitorUrlHeader": "",
+ "failCountHeader": "",
+ "failedAtHeader": "",
+ "failReasonHeader": ""
+ }
+ },
+ "export": {
+ "title": "",
+ "success": "",
+ "failed": ""
+ },
+ "monitorActions": {
+ "title": "",
+ "import": "",
+ "export": ""
+ },
+ "settingsPage": {
+ "aboutSettings": {
+ "labelDevelopedBy": "",
+ "labelVersion": "",
+ "title": ""
+ },
+ "demoMonitorsSettings": {
+ "buttonAddMonitors": "",
+ "description": "",
+ "title": ""
+ },
+ "emailSettings": {
+ "buttonSendTestEmail": "",
+ "description": "",
+ "descriptionTransport": "",
+ "labelAddress": "",
+ "labelConnectionHost": "",
+ "labelHost": "",
+ "labelIgnoreTLS": "",
+ "labelPassword": "",
+ "labelPasswordSet": "",
+ "labelPool": "",
+ "labelPort": "",
+ "labelRejectUnauthorized": "",
+ "labelRequireTLS": "",
+ "labelSecure": "",
+ "labelTLSServername": "",
+ "labelUser": "",
+ "linkTransport": "",
+ "placeholderUser": "",
+ "title": ""
+ },
+ "pageSpeedSettings": {
+ "description": "",
+ "labelApiKeySet": "",
+ "labelApiKey": "",
+ "title": ""
+ },
+ "saveButtonLabel": "",
+ "statsSettings": {
+ "clearAllStatsButton": "",
+ "clearAllStatsDescription": "",
+ "clearAllStatsDialogConfirm": "",
+ "clearAllStatsDialogDescription": "",
+ "clearAllStatsDialogTitle": "",
+ "description": "",
+ "labelTTL": "",
+ "labelTTLOptional": "",
+ "title": ""
+ },
+ "systemResetSettings": {
+ "buttonRemoveAllMonitors": "",
+ "description": "",
+ "dialogConfirm": "",
+ "dialogDescription": "",
+ "dialogTitle": "",
+ "title": ""
+ },
+ "timezoneSettings": {
+ "description": "",
+ "label": "",
+ "title": ""
+ },
+ "title": "",
+ "uiSettings": {
+ "description": "",
+ "labelLanguage": "",
+ "labelTheme": "",
+ "title": ""
+ },
+ "urlSettings": {
+ "description": "",
+ "label": "",
+ "selectDisabled": "",
+ "selectEnabled": "",
+ "title": ""
+ }
+ }
}
diff --git a/client/src/store.js b/client/src/store.js
index 92ddfad00..69aa3d50b 100644
--- a/client/src/store.js
+++ b/client/src/store.js
@@ -1,11 +1,7 @@
import { configureStore, combineReducers } from "@reduxjs/toolkit";
-import uptimeMonitorsReducer from "./Features/UptimeMonitors/uptimeMonitorsSlice";
-import infrastructureMonitorsReducer from "./Features/InfrastructureMonitors/infrastructureMonitorsSlice";
-import pageSpeedMonitorReducer from "./Features/PageSpeedMonitor/pageSpeedMonitorSlice";
import authReducer from "./Features/Auth/authSlice";
import uiReducer from "./Features/UI/uiSlice";
-import settingsReducer from "./Features/Settings/settingsSlice";
import storage from "redux-persist/lib/storage";
import { persistReducer, persistStore, createTransform } from "redux-persist";
@@ -23,17 +19,13 @@ const authTransform = createTransform(
const persistConfig = {
key: "root",
storage,
- whitelist: ["auth", "monitors", "pageSpeed", "ui", "settings"],
+ whitelist: ["auth", "ui"],
transforms: [authTransform],
};
const rootReducer = combineReducers({
- uptimeMonitors: uptimeMonitorsReducer,
- infrastructureMonitors: infrastructureMonitorsReducer,
auth: authReducer,
- pageSpeedMonitors: pageSpeedMonitorReducer,
ui: uiReducer,
- settings: settingsReducer,
});
const persistedReducer = persistReducer(persistConfig, rootReducer);
diff --git a/client/vite.config.js b/client/vite.config.js
index dc84ce055..838cc5142 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,12 +1,20 @@
-import { defineConfig } from "vite";
+import { defineConfig, loadEnv } from "vite";
import react from "@vitejs/plugin-react";
import svgr from "vite-plugin-svgr";
+import { execSync } from "child_process";
-// https://vitejs.dev/config/
-export default defineConfig({
- base: "/",
- plugins: [svgr(), react()],
- optimizeDeps: {
- include: ["@mui/material/Tooltip", "@emotion/styled"],
- },
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), "");
+ let version = 2.2;
+
+ return {
+ base: "/",
+ plugins: [svgr(), react()],
+ optimizeDeps: {
+ include: ["@mui/material/Tooltip", "@emotion/styled"],
+ },
+ define: {
+ __APP_VERSION__: JSON.stringify(version),
+ },
+ };
});
diff --git a/docker/.gitignore b/docker/.gitignore
index 81ba730c9..9a6df883a 100755
--- a/docker/.gitignore
+++ b/docker/.gitignore
@@ -10,4 +10,5 @@ prod/redis/data/*
dist/docker-compose-test.yaml
/dist-mono/mongo/data/*
/dist-mono/redis/data/*
+/dist-arm/mongo/data/*
*.env
diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml
index f66640f71..921d3b8c1 100644
--- a/docker/dist-arm/docker-compose.yaml
+++ b/docker/dist-arm/docker-compose.yaml
@@ -1,6 +1,6 @@
services:
server:
- image: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch
+ image: ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest
restart: always
ports:
- "52345:52345"
@@ -12,16 +12,7 @@ services:
- CLIENT_HOST=http://localhost
- JWT_SECRET=my_secret
depends_on:
- - redis
- mongodb
- redis:
- image: redis:7
- container_name: checkmate-redis
- ports:
- - "6379:6379"
- volumes:
- - ./redis/data:/data
- restart: unless-stopped
mongodb:
image: mongo:4.4.18
container_name: checkmate-mongodb
diff --git a/docker/dist-mono/docker-compose.yaml b/docker/dist-mono/docker-compose.yaml
index 9b3364975..0dd97aafb 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"
@@ -12,21 +12,9 @@ services:
- CLIENT_HOST=http://localhost
- JWT_SECRET=my_secret
depends_on:
- - redis
- mongodb
- redis:
- image: ghcr.io/bluewave-labs/checkmate:redis-dist
- restart: always
- volumes:
- - ./redis/data:/data
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 30s
- timeout: 10s
- 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..ff2f1b7ea 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-client:latest
restart: always
environment:
UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1"
@@ -11,12 +11,11 @@ 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"
depends_on:
- - redis
- mongodb
environment:
- DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db?replicaSet=rs0
@@ -25,19 +24,8 @@ services:
- JWT_SECRET=my_secret
# volumes:
# - /var/run/docker.sock:/var/run/docker.sock:ro
- redis:
- image: ghcr.io/bluewave-labs/checkmate:redis-dist
- restart: always
- volumes:
- - ./redis/data:/data
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 30s
- timeout: 10s
- 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
diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml
index 9cc4a82cb..980a0e2f5 100755
--- a/docker/prod/docker-compose.yaml
+++ b/docker/prod/docker-compose.yaml
@@ -21,6 +21,8 @@ services:
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
+ entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
+
server:
image: ghcr.io/bluewave-labs/checkmate:backend-demo
restart: always
@@ -29,21 +31,9 @@ services:
env_file:
- server.env
depends_on:
- - redis
- mongodb
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- redis:
- image: ghcr.io/bluewave-labs/checkmate:redis-demo
- restart: always
- volumes:
- - ./redis/data:/data
- healthcheck:
- test: ["CMD", "redis-cli", "ping"]
- interval: 30s
- timeout: 10s
- retries: 5
- start_period: 5s
mongodb:
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
restart: always
diff --git a/server/.github/pull_request_template.md b/server/.github/pull_request_template.md
index ed81cc04b..4f4da5d80 100755
--- a/server/.github/pull_request_template.md
+++ b/server/.github/pull_request_template.md
@@ -1,23 +1,25 @@
-**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
+**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
## Describe your changes
-Briefly describe the changes you made and their purpose.
+Briefly describe the changes you made and their purpose.
## Write your issue number after "Fixes "
-Fixes #123
+Fixes #123
## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.
- [ ] (Do not skip this or your PR will be closed) I deployed the application locally.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code.
- [ ] I have included the issue # in the PR.
-- [ ] I have added i18n support to visible strings (instead of `Add
`, use):
+- [ ] I have added i18n support to visible strings (instead of `Add
`, use):
+
```Javascript
const { t } = useTranslation();
{t('add')}
```
+
- [ ] The issue I am working on is assigned to me.
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] My PR is granular and targeted to one specific feature.
diff --git a/server/README.md b/server/README.md
index 03f1b1629..db609cb02 100755
--- a/server/README.md
+++ b/server/README.md
@@ -2,7 +2,7 @@
The backend service for Checkmate, an open source uptime and infrastructure monitoring application
-This directory contains the **backend** of Checkmate, which handles data processing, storage, and API services for the Checkmate monitoring tool. The backend is responsible for managing uptime checks, handling real-time alerts, and storing historical monitoring data.
+This directory contains the **backend** of Checkmate, which handles data processing, storage, and API services for the Checkmate monitoring tool. The backend is responsible for managing uptime checks, handling real-time alerts, and storing historical monitoring data.
Checkmate's backend is designed to be lightweight and scalable, ensuring reliable performance even with a high number of active monitors.
diff --git a/server/controllers/authController.js b/server/controllers/authController.js
index 9d5fe0c20..1d8c2af0e 100755
--- a/server/controllers/authController.js
+++ b/server/controllers/authController.js
@@ -7,7 +7,6 @@ import {
recoveryTokenValidation,
newPasswordValidation,
} from "../validation/joi.js";
-import logger from "../utils/logger.js";
import jwt from "jsonwebtoken";
import { getTokenFromHeaders } from "../utils/utils.js";
import crypto from "crypto";
@@ -15,12 +14,13 @@ import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "authController";
class AuthController {
- constructor(db, settingsService, emailService, jobQueue, stringService) {
+ constructor({ db, settingsService, emailService, jobQueue, stringService, logger }) {
this.db = db;
this.settingsService = settingsService;
this.emailService = emailService;
this.jobQueue = jobQueue;
this.stringService = stringService;
+ this.logger = logger;
}
/**
@@ -82,7 +82,7 @@ class AuthController {
}
const newUser = await this.db.insertUser({ ...req.body }, req.file);
- logger.info({
+ this.logger.info({
message: this.stringService.authCreateUser,
service: SERVICE_NAME,
details: newUser._id,
@@ -96,21 +96,28 @@ class AuthController {
const token = this.issueToken(userForToken, appSettings);
- this.emailService
- .buildAndSendEmail(
- "welcomeEmailTemplate",
- { name: newUser.firstName },
- newUser.email,
- "Welcome to Uptime Monitor"
- )
- .catch((error) => {
- logger.error({
- message: error.message,
- service: SERVICE_NAME,
- method: "registerUser",
- stack: error.stack,
- });
+ try {
+ const html = await this.emailService.buildEmail("welcomeEmailTemplate", {
+ name: newUser.firstName,
});
+ this.emailService
+ .sendEmail(newUser.email, "Welcome to Uptime Monitor", html)
+ .catch((error) => {
+ this.logger.warn({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "registerUser",
+ stack: error.stack,
+ });
+ });
+ } catch (error) {
+ this.logger.warn({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "registerUser",
+ stack: error.stack,
+ });
+ }
res.success({
msg: this.stringService.authCreateUser,
@@ -300,15 +307,16 @@ class AuthController {
const name = user.firstName;
const { clientHost } = this.settingsService.getSettings();
const url = `${clientHost}/set-new-password/${recoveryToken.token}`;
- const msgId = await this.emailService.buildAndSendEmail(
- "passwordResetTemplate",
- {
- name,
- email,
- url,
- },
+
+ const html = await this.emailService.buildEmail("passwordResetTemplate", {
+ name,
email,
- "Checkmate Password Reset"
+ url,
+ });
+ const msgId = await this.emailService.sendEmail(
+ email,
+ "Checkmate Password Reset",
+ html
);
return res.success({
@@ -404,7 +412,7 @@ class AuthController {
// 1. Find all the monitors associated with the team ID if superadmin
const result = await this.db.getMonitorsByTeamId({
- params: { teamId: user.teamId },
+ teamId: user.teamId.toString(),
});
if (user.role.includes("superadmin")) {
@@ -413,18 +421,8 @@ 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);
})
));
-
- // 3. Delete team
- await this.db.deleteTeam(user.teamId);
- // 4. Delete all other team members
- await this.db.deleteAllOtherUsers();
- // 5. Delete each monitor
- await this.db.deleteMonitorsByUserId(user._id);
}
// 6. Delete the user by id
await this.db.deleteUser(user._id);
diff --git a/server/controllers/checkController.js b/server/controllers/checkController.js
index 2addd7359..387839beb 100755
--- a/server/controllers/checkController.js
+++ b/server/controllers/checkController.js
@@ -54,7 +54,18 @@ class CheckController {
}
try {
- const result = await this.db.getChecksByMonitor(req);
+ const { monitorId } = req.params;
+ let { type, sortOrder, dateRange, filter, page, rowsPerPage, status } = req.query;
+ const result = await this.db.getChecksByMonitor({
+ monitorId,
+ type,
+ sortOrder,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ status,
+ });
return res.success({
msg: this.stringService.checkGet,
@@ -74,8 +85,17 @@ class CheckController {
return;
}
try {
- const checkData = await this.db.getChecksByTeam(req);
+ let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query;
+ const { teamId } = req.user;
+ const checkData = await this.db.getChecksByTeam({
+ sortOrder,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ teamId,
+ });
return res.success({
msg: this.stringService.checkGet,
data: checkData,
@@ -114,7 +134,8 @@ class CheckController {
}
try {
- const deletedCount = await this.db.deleteChecksByTeamId(req.params.teamId);
+ const { teamId } = req.user;
+ const deletedCount = await this.db.deleteChecksByTeamId(teamId);
return res.success({
msg: this.stringService.checkDelete,
@@ -137,9 +158,7 @@ class CheckController {
try {
// Get user's teamId
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { teamId } = jwt.verify(token, jwtSecret);
+ const { teamId } = req.user;
const ttl = parseInt(req.body.ttl, 10) * SECONDS_PER_DAY;
await this.db.updateChecksTTL(teamId, ttl);
diff --git a/server/controllers/diagnosticController.js b/server/controllers/diagnosticController.js
index c46ac5ccb..8fa5fa42b 100755
--- a/server/controllers/diagnosticController.js
+++ b/server/controllers/diagnosticController.js
@@ -4,25 +4,11 @@ const SERVICE_NAME = "diagnosticController";
class DiagnosticController {
constructor(db) {
this.db = db;
- this.getDistributedUptimeDbExecutionStats =
- this.getDistributedUptimeDbExecutionStats.bind(this);
this.getMonitorsByTeamIdExecutionStats =
this.getMonitorsByTeamIdExecutionStats.bind(this);
this.getDbStats = this.getDbStats.bind(this);
}
- async getDistributedUptimeDbExecutionStats(req, res, next) {
- try {
- const data = await this.db.getDistributedUptimeDbExecutionStats(req);
- return res.success({
- msg: "OK",
- data,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "getDbExecutionStats"));
- }
- }
-
async getMonitorsByTeamIdExecutionStats(req, res, next) {
try {
const data = await this.db.getMonitorsByTeamIdExecutionStats(req);
diff --git a/server/controllers/distributedUptimeController.js b/server/controllers/distributedUptimeController.js
deleted file mode 100755
index c036e97bc..000000000
--- a/server/controllers/distributedUptimeController.js
+++ /dev/null
@@ -1,339 +0,0 @@
-import { handleError } from "./controllerUtils.js";
-import Monitor from "../db/models/Monitor.js";
-import DistributedUptimeCheck from "../db/models/DistributedUptimeCheck.js";
-const SERVICE_NAME = "DistributedUptimeQueueController";
-
-class DistributedUptimeController {
- constructor({ db, http, statusService, logger }) {
- this.db = db;
- this.http = http;
- this.statusService = statusService;
- this.logger = logger;
- this.resultsCallback = this.resultsCallback.bind(this);
- this.getDistributedUptimeMonitors = this.getDistributedUptimeMonitors.bind(this);
- this.subscribeToDistributedUptimeMonitors =
- this.subscribeToDistributedUptimeMonitors.bind(this);
-
- this.subscribeToDistributedUptimeMonitorDetails =
- this.subscribeToDistributedUptimeMonitorDetails.bind(this);
- this.getDistributedUptimeMonitorDetails =
- this.getDistributedUptimeMonitorDetails.bind(this);
- }
-
- async resultsCallback(req, res, next) {
- try {
- const { id, result } = req.body;
- // Calculate response time
- const {
- first_byte_took,
- body_read_took,
- dns_took,
- conn_took,
- connect_took,
- tls_took,
- status_code,
- error,
- upt_burnt,
- } = result;
-
- // Calculate response time
- const responseTime = first_byte_took / 1_000_000;
- if (!isFinite(responseTime) || responseTime <= 0 || responseTime > 30000) {
- this.logger.info({
- message: `Unreasonable response time detected: ${responseTime}ms from first_byte_took: ${first_byte_took}ns`,
- service: SERVICE_NAME,
- method: "resultsCallback",
- });
- return;
- }
-
- // Calculate if server is up or down
- const isErrorStatus = status_code >= 400;
- const hasError = error !== "";
-
- const status = isErrorStatus || hasError ? false : true;
-
- // Build response
- const distributedUptimeResponse = {
- monitorId: id,
- type: "distributed_http",
- payload: result,
- status,
- code: status_code,
- responseTime,
- first_byte_took,
- body_read_took,
- dns_took,
- conn_took,
- connect_took,
- tls_took,
- upt_burnt,
- };
- if (error) {
- const code = status_code || this.NETWORK_ERROR;
- distributedUptimeResponse.code = code;
- distributedUptimeResponse.message =
- this.http.STATUS_CODES[code] || "Network Error";
- } else {
- distributedUptimeResponse.message = this.http.STATUS_CODES[status_code];
- }
-
- await this.statusService.updateStatus(distributedUptimeResponse);
-
- res.status(200).json({ message: "OK" });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "resultsCallback"));
- }
- }
-
- async getDistributedUptimeMonitors(req, res, next) {
- try {
- const monitors = await this.db.getMonitorsWithChecksByTeamId(req);
- return res.success({
- msg: "OK",
- data: monitors,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitors"));
- }
- }
-
- async subscribeToDistributedUptimeMonitors(req, res, next) {
- try {
- res.setHeader("Content-Type", "text/event-stream");
- res.setHeader("Cache-Control", "no-cache");
- res.setHeader("Connection", "keep-alive");
- res.setHeader("Access-Control-Allow-Origin", "*");
- // Disable compression
- req.headers["accept-encoding"] = "identity";
- res.removeHeader("Content-Encoding");
- const BATCH_DELAY = 1000;
- let batchTimeout = null;
- let opInProgress = false;
- let monitorStream = null;
- let checksStream = null;
-
- // Do things here
- const notifyChange = async () => {
- if (opInProgress) {
- // Get data
- const { count, monitors } = await this.db.getMonitorsWithChecksByTeamId(req);
- res.write(`data: ${JSON.stringify({ count, monitors })}\n\n`);
- opInProgress = false;
- }
- batchTimeout = null;
- };
-
- const handleChange = () => {
- opInProgress = true;
- if (batchTimeout) clearTimeout(batchTimeout);
- batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
- };
-
- const createMonitorStream = () => {
- if (monitorStream) {
- try {
- monitorStream.close();
- } catch (error) {
- this.logger.error({
- message: "Error closing monitor stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitors",
- stack: error.stack,
- });
- }
- }
- monitorStream = Monitor.watch(
- [{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
- { fullDocument: "updateLookup" }
- );
-
- monitorStream.on("change", handleChange);
- monitorStream.on("error", (error) => {
- this.logger.error({
- message: "Error in monitor stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitors",
- stack: error.stack,
- });
- createMonitorStream();
- });
- monitorStream.on("close", () => {
- monitorStream = null;
- });
- };
-
- const createChecksStream = () => {
- if (checksStream) {
- try {
- checksStream.close();
- } catch (error) {
- this.logger.error({
- message: "Error closing checks stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitors",
- details: error,
- });
- }
- }
- checksStream = DistributedUptimeCheck.watch(
- [{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
- { fullDocument: "updateLookup" }
- );
- checksStream.on("change", handleChange);
- checksStream.on("error", (error) => {
- this.logger.error({
- message: "Error in checks stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitors",
- stack: error.stack,
- });
- createChecksStream();
- });
- checksStream.on("close", () => {
- checksStream = null;
- });
- };
-
- createMonitorStream();
- createChecksStream();
-
- req.on("close", () => {
- if (batchTimeout) {
- clearTimeout(batchTimeout);
- }
- monitorStream.close();
- checksStream.close();
- clearInterval(keepAlive);
- });
-
- // Keep connection alive
- const keepAlive = setInterval(() => {
- res.write(": keepalive\n\n");
- }, 10000);
-
- // Clean up on close
- req.on("close", () => {
- clearInterval(keepAlive);
- });
- } catch (error) {
- this.logger.error({
- message: "Error in subscribeToDistributedUptimeMonitors",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitors",
- stack: error.stack,
- });
- next(handleError(error, SERVICE_NAME, "subscribeToDistributedUptimeMonitors"));
- }
- }
-
- async getDistributedUptimeMonitorDetails(req, res, next) {
- try {
- const monitor = await this.db.getDistributedUptimeDetailsById(req);
- return res.success({
- msg: "OK",
- data: monitor,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
- }
- }
-
- async subscribeToDistributedUptimeMonitorDetails(req, res, next) {
- try {
- res.setHeader("Content-Type", "text/event-stream");
- res.setHeader("Cache-Control", "no-cache");
- res.setHeader("Connection", "keep-alive");
- res.setHeader("Access-Control-Allow-Origin", "*");
-
- // disable compression
- req.headers["accept-encoding"] = "identity";
- res.removeHeader("Content-Encoding");
-
- const BATCH_DELAY = 1000;
- let batchTimeout = null;
- let opInProgress = false;
- let checksStream = null;
- // Do things here
- const notifyChange = async () => {
- try {
- if (opInProgress) {
- // Get data
- const monitor = await this.db.getDistributedUptimeDetailsById(req);
- res.write(`data: ${JSON.stringify({ monitor })}\n\n`);
- opInProgress = false;
- }
- batchTimeout = null;
- } catch (error) {
- opInProgress = false;
- batchTimeout = null;
- this.logger.error({
- message: "Error in notifyChange",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitorDetails",
- stack: error.stack,
- });
- next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
- }
- };
-
- const handleChange = () => {
- opInProgress = true;
- if (batchTimeout) clearTimeout(batchTimeout);
- batchTimeout = setTimeout(notifyChange, BATCH_DELAY);
- };
-
- const createCheckStream = () => {
- if (checksStream) {
- try {
- checksStream.close();
- } catch (error) {
- this.logger.error({
- message: "Error closing checks stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitorDetails",
- stack: error.stack,
- });
- }
- }
- checksStream = DistributedUptimeCheck.watch(
- [{ $match: { operationType: { $in: ["insert", "update", "delete"] } } }],
- { fullDocument: "updateLookup" }
- );
-
- checksStream.on("change", handleChange);
- checksStream.on("error", (error) => {
- this.logger.error({
- message: "Error in checks stream",
- service: SERVICE_NAME,
- method: "subscribeToDistributedUptimeMonitorDetails",
- stack: error.stack,
- });
- createCheckStream();
- });
- checksStream.on("close", () => {
- checksStream = null;
- });
- };
-
- createCheckStream();
-
- // Handle client disconnect
- req.on("close", () => {
- if (batchTimeout) {
- clearTimeout(batchTimeout);
- }
- checksStream.close();
- clearInterval(keepAlive);
- });
-
- // Keep connection alive
- const keepAlive = setInterval(() => {
- res.write(": keepalive\n\n");
- }, 10000);
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "getDistributedUptimeMonitorDetails"));
- }
- }
-}
-export default DistributedUptimeController;
diff --git a/server/controllers/inviteController.js b/server/controllers/inviteController.js
index bce40dfa1..3799c58b9 100755
--- a/server/controllers/inviteController.js
+++ b/server/controllers/inviteController.js
@@ -71,24 +71,30 @@ class InviteController {
const inviteToken = await this.db.requestInviteToken({ ...req.body });
const { clientHost } = this.settingsService.getSettings();
- this.emailService
- .buildAndSendEmail(
- "employeeActivationTemplate",
- {
- name: firstname,
- link: `${clientHost}/register/${inviteToken.token}`,
- },
- req.body.email,
- "Welcome to Uptime Monitor"
- )
- .catch((error) => {
- logger.error({
- message: error.message,
- service: SERVICE_NAME,
- method: "issueInvitation",
- stack: error.stack,
- });
+
+ try {
+ const html = await this.emailService.buildEmail("employeeActivationTemplate", {
+ name: firstname,
+ link: `${clientHost}/register/${inviteToken.token}`,
});
+ const result = await this.emailService.sendEmail(
+ req.body.email,
+ "Welcome to Uptime Monitor",
+ html
+ );
+ if (!result) {
+ return res.error({
+ msg: "Failed to send invite e-mail... Please verify your settings.",
+ });
+ }
+ } catch (error) {
+ logger.warn({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "sendInviteEmail",
+ stack: error.stack,
+ });
+ }
return res.success({
msg: this.stringService.inviteIssued,
diff --git a/server/controllers/logController.js b/server/controllers/logController.js
new file mode 100644
index 000000000..5381fd3fa
--- /dev/null
+++ b/server/controllers/logController.js
@@ -0,0 +1,23 @@
+import { handleError } from "./controllerUtils.js";
+
+const SERVICE_NAME = "JobQueueController";
+
+class LogController {
+ constructor(logger) {
+ this.logger = logger;
+ }
+
+ getLogs = async (req, res, next) => {
+ try {
+ const logs = await this.logger.getLogs();
+ res.success({
+ msg: "Logs fetched successfully",
+ data: logs,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "getLogs"));
+ return;
+ }
+ };
+}
+export default LogController;
diff --git a/server/controllers/maintenanceWindowController.js b/server/controllers/maintenanceWindowController.js
index a11f957ed..20077cc65 100755
--- a/server/controllers/maintenanceWindowController.js
+++ b/server/controllers/maintenanceWindowController.js
@@ -7,8 +7,6 @@ import {
getMaintenanceWindowsByTeamIdQueryValidation,
deleteMaintenanceWindowByIdParamValidation,
} from "../validation/joi.js";
-import jwt from "jsonwebtoken";
-import { getTokenFromHeaders } from "../utils/utils.js";
import { handleValidationError, handleError } from "./controllerUtils.js";
const SERVICE_NAME = "maintenanceWindowController";
@@ -28,9 +26,7 @@ class MaintenanceWindowController {
return;
}
try {
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { teamId } = jwt.verify(token, jwtSecret);
+ const { teamId } = req.user;
const monitorIds = req.body.monitors;
const dbTransactions = monitorIds.map((monitorId) => {
return this.db.createMaintenanceWindow({
@@ -81,9 +77,7 @@ class MaintenanceWindowController {
}
try {
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { teamId } = jwt.verify(token, jwtSecret);
+ const { teamId } = req.user;
const maintenanceWindows = await this.db.getMaintenanceWindowsByTeamId(
teamId,
req.query
diff --git a/server/controllers/monitorController.js b/server/controllers/monitorController.js
index e8eaeb106..d32ef463e 100755
--- a/server/controllers/monitorController.js
+++ b/server/controllers/monitorController.js
@@ -1,4 +1,3 @@
-import Monitor from "../db/models/Monitor.js";
import {
getMonitorByIdParamValidation,
getMonitorByIdQueryValidation,
@@ -16,13 +15,10 @@ import {
getHardwareDetailsByIdQueryValidation,
} from "../validation/joi.js";
import sslChecker from "ssl-checker";
-import jwt from "jsonwebtoken";
-import { getTokenFromHeaders } from "../utils/utils.js";
import logger from "../utils/logger.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
import axios from "axios";
import seedDb from "../db/mongo/utils/seedDb.js";
-import { seedDistributedTest } from "../db/mongo/utils/seedDb.js";
const SERVICE_NAME = "monitorController";
import pkg from "papaparse";
@@ -79,7 +75,14 @@ class MonitorController {
getUptimeDetailsById = async (req, res, next) => {
try {
- const data = await this.db.getUptimeDetailsById(req);
+ const { monitorId } = req.params;
+ const { dateRange, normalize } = req.query;
+
+ const data = await this.db.getUptimeDetailsById({
+ monitorId,
+ dateRange,
+ normalize,
+ });
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data,
@@ -108,7 +111,17 @@ class MonitorController {
}
try {
- const monitorStats = await this.db.getMonitorStatsById(req);
+ let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
+ const { monitorId } = req.params;
+
+ const monitorStats = await this.db.getMonitorStatsById({
+ monitorId,
+ limit,
+ sortOrder,
+ dateRange,
+ numToDisplay,
+ normalize,
+ });
return res.success({
msg: this.stringService.monitorStatsById,
data: monitorStats,
@@ -136,7 +149,9 @@ class MonitorController {
return;
}
try {
- const monitor = await this.db.getHardwareDetailsById(req);
+ const { monitorId } = req.params;
+ const { dateRange } = req.query;
+ const monitor = await this.db.getHardwareDetailsById({ monitorId, dateRange });
return res.success({
msg: this.stringService.monitorGetByIdSuccess,
data: monitor,
@@ -220,19 +235,13 @@ class MonitorController {
}
try {
- const notifications = req.body.notifications;
- const monitor = await this.db.createMonitor(req, res);
+ const { _id, teamId } = req.user;
+ const monitor = await this.db.createMonitor({
+ body: req.body,
+ teamId,
+ userId: _id,
+ });
- if (notifications && notifications.length > 0) {
- monitor.notifications = await Promise.all(
- notifications.map(async (notification) => {
- notification.monitorId = monitor._id;
- return await this.db.createNotification(notification);
- })
- );
- }
-
- await monitor.save();
// Add monitor to job queue
this.jobQueue.addJob(monitor._id, monitor);
return res.success({
@@ -273,10 +282,10 @@ class MonitorController {
throw new Error("File is empty");
}
- const { userId, teamId } = req.body;
+ const { _id, teamId } = req.user;
- if (!userId || !teamId) {
- throw new Error("Missing userId or teamId in form data");
+ if (!_id || !teamId) {
+ throw new Error("Missing userId or teamId");
}
// Get file buffer from memory and convert to string
@@ -311,7 +320,7 @@ class MonitorController {
}
const enrichedData = data.map((monitor) => ({
- userId,
+ userId: _id,
teamId,
...monitor,
description: monitor.description || monitor.name || monitor.url,
@@ -326,18 +335,6 @@ class MonitorController {
await Promise.all(
monitors.map(async (monitor, index) => {
- const notifications = enrichedData[index].notifications;
-
- if (notifications?.length) {
- monitor.notifications = await Promise.all(
- notifications.map(async (notification) => {
- notification.monitorId = monitor._id;
- return await this.db.createNotification(notification);
- })
- );
- await monitor.save();
- }
-
this.jobQueue.addJob(monitor._id, monitor);
})
);
@@ -412,59 +409,10 @@ 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: "deleteNotifications",
- fn: () => this.db.deleteNotificationsByMonitorId(monitor._id),
- },
- {
- name: "deleteHardwareChecks",
- fn: () => this.db.deleteHardwareChecksByMonitorId(monitor._id),
- },
- {
- name: "deleteDistributedUptimeChecks",
- fn: () => this.db.deleteDistributedChecksByMonitorId(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,
- });
- }
+ const monitorId = req.params.monitorId;
+ const monitor = await this.db.deleteMonitor({ monitorId });
+ await this.jobQueue.deleteJob(monitor);
+ await this.db.deleteStatusPagesByMonitorId(monitor._id);
return res.success({ msg: this.stringService.monitorDelete });
} catch (error) {
next(handleError(error, SERVICE_NAME, "deleteMonitor"));
@@ -484,9 +432,7 @@ class MonitorController {
*/
deleteAllMonitors = async (req, res, next) => {
try {
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { teamId } = jwt.verify(token, jwtSecret);
+ const { teamId } = req.user;
const { monitors, deletedCount } = await this.db.deleteAllMonitors(teamId);
await Promise.all(
monitors.map(async (monitor) => {
@@ -535,25 +481,11 @@ class MonitorController {
try {
const { monitorId } = req.params;
- const monitorBeforeEdit = await this.db.getMonitorById(monitorId);
- // Get notifications from the request body
- const notifications = req.body.notifications ?? [];
const editedMonitor = await this.db.editMonitor(monitorId, req.body);
- await this.db.deleteNotificationsByMonitorId(editedMonitor._id);
+ await this.jobQueue.updateJob(editedMonitor);
- await Promise.all(
- notifications.map(async (notification) => {
- notification.monitorId = editedMonitor._id;
- await this.db.createNotification(notification);
- })
- );
-
- // Delete the old job(editedMonitor has the same ID as the old monitor)
- await this.jobQueue.deleteJob(monitorBeforeEdit);
- // Add the new job back to the queue
- await this.jobQueue.addJob(editedMonitor._id, editedMonitor);
return res.success({
msg: this.stringService.monitorEdit,
data: editedMonitor,
@@ -582,21 +514,11 @@ class MonitorController {
}
try {
- const monitor = await Monitor.findOneAndUpdate(
- { _id: req.params.monitorId },
- [
- {
- $set: {
- isActive: { $not: "$isActive" },
- status: "$$REMOVE",
- },
- },
- ],
- { new: true }
- );
+ const monitorId = req.params.monitorId;
+ const monitor = await this.db.pauseMonitor({ monitorId });
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
@@ -622,9 +544,7 @@ class MonitorController {
*/
addDemoMonitors = async (req, res, next) => {
try {
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { _id, teamId } = jwt.verify(token, jwtSecret);
+ const { _id, teamId } = req.user;
const demoMonitors = await this.db.addDemoMonitors(_id, teamId);
await Promise.all(
demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))
@@ -660,12 +580,8 @@ class MonitorController {
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
- const messageId = await this.emailService.buildAndSendEmail(
- "testEmailTemplate",
- context,
- to,
- subject
- );
+ const html = await this.emailService.buildEmail("testEmailTemplate", context);
+ const messageId = await this.emailService.sendEmail(to, subject, html);
if (!messageId) {
return res.error({
@@ -691,7 +607,19 @@ class MonitorController {
}
try {
- const monitors = await this.db.getMonitorsByTeamId(req);
+ let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
+ const teamId = req.user.teamId;
+
+ const monitors = await this.db.getMonitorsByTeamId({
+ limit,
+ type,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ teamId,
+ });
return res.success({
msg: this.stringService.monitorGetByTeamId,
data: monitors,
@@ -710,7 +638,15 @@ class MonitorController {
}
try {
- const result = await this.db.getMonitorsAndSummaryByTeamId(req);
+ const { explain } = req;
+ const { type } = req.query;
+ const { teamId } = req.user;
+
+ const result = await this.db.getMonitorsAndSummaryByTeamId({
+ type,
+ explain,
+ teamId,
+ });
return res.success({
msg: "OK", // TODO
data: result,
@@ -729,9 +665,23 @@ class MonitorController {
}
try {
- const result = await this.db.getMonitorsWithChecksByTeamId(req);
+ const { explain } = req;
+ let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
+ const { teamId } = req.user;
+
+ const result = await this.db.getMonitorsWithChecksByTeamId({
+ limit,
+ type,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ teamId,
+ explain,
+ });
return res.success({
- msg: "OK", // TODO
+ msg: "OK",
data: result,
});
} catch (error) {
@@ -741,20 +691,49 @@ class MonitorController {
seedDb = async (req, res, next) => {
try {
- const { type } = req.body;
- const token = getTokenFromHeaders(req.headers);
- const { jwtSecret } = this.settingsService.getSettings();
- const { _id, teamId } = jwt.verify(token, jwtSecret);
- if (type === "distributed_test") {
- await seedDistributedTest(_id, teamId);
- } else {
- await seedDb(_id, teamId);
- }
+ const { _id, teamId } = req.user;
+ await seedDb(_id, teamId);
res.success({ msg: "Database seeded" });
} catch (error) {
next(handleError(error, SERVICE_NAME, "seedDb"));
}
};
+
+ exportMonitorsToCSV = async (req, res, next) => {
+ try {
+ const { teamId } = req.user;
+
+ const monitors = await this.db.getMonitorsByTeamId({ teamId });
+ if (!monitors || monitors.length === 0) {
+ return res.success({
+ msg: this.stringService.noMonitorsFound,
+ data: null,
+ });
+ }
+ const csvData = monitors?.filteredMonitors?.map((monitor) => ({
+ name: monitor.name,
+ description: monitor.description,
+ type: monitor.type,
+ url: monitor.url,
+ interval: monitor.interval,
+ port: monitor.port,
+ ignoreTlsErrors: monitor.ignoreTlsErrors,
+ isActive: monitor.isActive,
+ }));
+
+ const csv = pkg.unparse(csvData);
+
+ return res.file({
+ data: csv,
+ headers: {
+ "Content-Type": "text/csv",
+ "Content-Disposition": "attachment; filename=monitors.csv",
+ },
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "exportMonitorsToCSV"));
+ }
+ };
}
export default MonitorController;
diff --git a/server/controllers/notificationController.js b/server/controllers/notificationController.js
index 3682821eb..e7e2a1f2a 100755
--- a/server/controllers/notificationController.js
+++ b/server/controllers/notificationController.js
@@ -1,174 +1,147 @@
-import { triggerNotificationBodyValidation } from "../validation/joi.js";
+import {
+ triggerNotificationBodyValidation,
+ createNotificationBodyValidation,
+} from "../validation/joi.js";
import { handleError, handleValidationError } from "./controllerUtils.js";
const SERVICE_NAME = "NotificationController";
const NOTIFICATION_TYPES = {
- WEBHOOK: 'webhook',
- TELEGRAM: 'telegram'
+ WEBHOOK: "webhook",
+ TELEGRAM: "telegram",
};
const PLATFORMS = {
- SLACK: 'slack',
- DISCORD: 'discord',
- TELEGRAM: 'telegram'
+ SLACK: "slack",
+ DISCORD: "discord",
+ TELEGRAM: "telegram",
};
class NotificationController {
- constructor(notificationService, stringService, statusService) {
- this.notificationService = notificationService;
- this.stringService = stringService;
- this.statusService = statusService;
- this.triggerNotification = this.triggerNotification.bind(this);
- this.testWebhook = this.testWebhook.bind(this);
- }
+ constructor({ notificationService, stringService, statusService, db }) {
+ this.notificationService = notificationService;
+ this.stringService = stringService;
+ this.statusService = statusService;
+ this.db = db;
+ }
- async triggerNotification(req, res, next) {
- try {
- await triggerNotificationBodyValidation.validateAsync(req.body, {
- abortEarly: false,
- stripUnknown: true,
- });
- } catch (error) {
- next(handleValidationError(error, SERVICE_NAME));
- return;
- }
+ testNotification = async (req, res, next) => {
+ try {
+ const notification = req.body;
- try {
- const { monitorId, type, platform, config, status = false } = req.body;
+ const success = await this.notificationService.sendTestNotification(notification);
- // Create a simplified networkResponse similar to what would come from monitoring
- const networkResponse = {
- monitorId,
- status
- };
-
- // Use the statusService to get monitor details and handle status change logic
- // This returns { monitor, statusChanged, prevStatus } exactly like your job queue uses
- const statusResult = await this.statusService.updateStatus(networkResponse);
-
- if (type === NOTIFICATION_TYPES.WEBHOOK) {
- const notification = {
- type,
- platform,
- config,
- };
+ if (!success) {
+ return res.error({
+ msg: "Sending notification failed",
+ status: 400,
+ });
+ }
- await this.notificationService.sendWebhookNotification(
- statusResult, // Contains monitor, statusChanged, and prevStatus
- notification
- );
- }
+ return res.success({
+ msg: "Notification sent successfully",
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "testWebhook"));
+ }
+ };
- return res.success({
- msg: this.stringService.webhookSendSuccess,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "triggerNotification"));
- }
- }
+ createNotification = async (req, res, next) => {
+ try {
+ await createNotificationBodyValidation.validateAsync(req.body, {
+ abortEarly: false,
+ });
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
- createTestNetworkResponse() {
- return {
- monitor: {
- _id: "test-monitor-id",
- name: "Test Monitor",
- url: "https://example.com"
- },
- status: true,
- statusChanged: true,
- prevStatus: false,
- };
- }
+ try {
+ const body = req.body;
+ const { _id, teamId } = req.user;
+ body.userId = _id;
+ body.teamId = teamId;
+ const notification = await this.db.createNotification(body);
+ return res.success({
+ msg: "Notification created successfully",
+ data: notification,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "createNotification"));
+ }
+ };
- handleTelegramTest(botToken, chatId) {
- if (!botToken || !chatId) {
- return {
- isValid: false,
- error: {
- msg: this.stringService.telegramRequiresBotTokenAndChatId,
- status: 400
- }
- };
- }
+ getNotificationsByTeamId = async (req, res, next) => {
+ try {
+ const notifications = await this.db.getNotificationsByTeamId(req.user.teamId);
+ return res.success({
+ msg: "Notifications fetched successfully",
+ data: notifications,
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "getNotificationsByTeamId"));
+ }
+ };
- return {
- isValid: true,
- notification: {
- type: NOTIFICATION_TYPES.WEBHOOK,
- platform: PLATFORMS.TELEGRAM,
- config: { botToken, chatId }
- }
- };
- }
+ deleteNotification = async (req, res, next) => {
+ try {
+ await this.db.deleteNotificationById(req.params.id);
+ return res.success({
+ msg: "Notification deleted successfully",
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "deleteNotification"));
+ }
+ };
- handleWebhookTest(webhookUrl, platform) {
- if (webhookUrl === null) {
- return {
- isValid: false,
- error: {
- msg: this.stringService.webhookUrlRequired,
- status: 400
- }
- };
- }
+ 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"));
+ }
+ };
- return {
- isValid: true,
- notification: {
- type: NOTIFICATION_TYPES.WEBHOOK,
- platform: platform,
- config: { webhookUrl }
- }
- };
- }
+ editNotification = async (req, res, next) => {
+ try {
+ await createNotificationBodyValidation.validateAsync(req.body, {
+ abortEarly: false,
+ });
+ } catch (error) {
+ next(handleValidationError(error, SERVICE_NAME));
+ return;
+ }
- async testWebhook(req, res, next) {
- try {
- const { webhookUrl, platform, botToken, chatId } = req.body;
+ 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"));
+ }
+ };
- if (platform === null) {
- return res.error({
- msg: this.stringService.platformRequired,
- status: 400
- });
- }
-
- // 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) {
- 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"));
- }
- }
+ testAllNotifications = async (req, res, next) => {
+ try {
+ const { monitorId } = req.body;
+ const monitor = await this.db.getMonitorById(monitorId);
+ const notifications = monitor.notifications;
+ if (notifications.length === 0) throw new Error("No notifications");
+ const result = await this.notificationService.testAllNotifications(notifications);
+ if (!result) throw new Error("Failed to send all notifications");
+ return res.success({
+ msg: "All notifications sent successfully",
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "testAllNotifications"));
+ }
+ };
}
-export default NotificationController;
\ No newline at end of file
+export default NotificationController;
diff --git a/server/controllers/queueController.js b/server/controllers/queueController.js
index be3d764e7..968106a5a 100755
--- a/server/controllers/queueController.js
+++ b/server/controllers/queueController.js
@@ -25,7 +25,7 @@ class JobQueueController {
try {
const jobs = await this.jobQueue.getJobs();
return res.success({
- msg: this.stringService.queueGetMetrics,
+ msg: this.stringService.queueGetJobs,
data: jobs,
});
} catch (error) {
@@ -34,6 +34,20 @@ class JobQueueController {
}
};
+ getAllMetrics = async (req, res, next) => {
+ try {
+ const jobs = await this.jobQueue.getJobs();
+ const metrics = await this.jobQueue.getMetrics();
+ return res.success({
+ msg: this.stringService.queueGetAllMetrics,
+ data: { jobs, metrics },
+ });
+ } catch (error) {
+ next(handleError(error, SERVICE_NAME, "getAllMetrics"));
+ return;
+ }
+ };
+
addJob = async (req, res, next) => {
try {
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
@@ -46,18 +60,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/controllers/settingsController.js b/server/controllers/settingsController.js
index 58e8ce9f4..ef1d689d6 100755
--- a/server/controllers/settingsController.js
+++ b/server/controllers/settingsController.js
@@ -11,8 +11,7 @@ class SettingsController {
this.emailService = emailService;
}
- getAppSettings = async (req, res, next) => {
- const dbSettings = await this.settingsService.getDBSettings();
+ buildAppSettings = (dbSettings) => {
const sanitizedSettings = { ...dbSettings };
delete sanitizedSettings.version;
@@ -30,6 +29,13 @@ class SettingsController {
delete sanitizedSettings.systemEmailPassword;
}
returnSettings.settings = sanitizedSettings;
+ return returnSettings;
+ };
+
+ getAppSettings = async (req, res, next) => {
+ const dbSettings = await this.settingsService.getDBSettings();
+
+ const returnSettings = this.buildAppSettings(dbSettings);
return res.success({
msg: this.stringService.getAppSettings,
data: returnSettings,
@@ -45,12 +51,11 @@ class SettingsController {
}
try {
- await this.db.updateAppSettings(req.body);
- const updatedSettings = { ...(await this.settingsService.reloadSettings()) };
- delete updatedSettings.jwtSecret;
+ const updatedSettings = await this.db.updateAppSettings(req.body);
+ const returnSettings = this.buildAppSettings(updatedSettings);
return res.success({
msg: this.stringService.updateAppSettings,
- data: updatedSettings,
+ data: returnSettings,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "updateAppSettings"));
@@ -74,25 +79,32 @@ class SettingsController {
systemEmailPassword,
systemEmailUser,
systemEmailConnectionHost,
+ systemEmailSecure,
+ systemEmailPool,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
+ systemEmailTLSServername,
} = req.body;
const subject = this.stringService.testEmailSubject;
const context = { testName: "Monitoring System" };
- const messageId = await this.emailService.buildAndSendEmail(
- "testEmailTemplate",
- context,
- to,
- subject,
- {
- systemEmailHost,
- systemEmailPort,
- systemEmailUser,
- systemEmailAddress,
- systemEmailPassword,
- systemEmailConnectionHost,
- }
- );
+ const html = await this.emailService.buildEmail("testEmailTemplate", context);
+ const messageId = await this.emailService.sendEmail(to, subject, html, {
+ systemEmailHost,
+ systemEmailPort,
+ systemEmailUser,
+ systemEmailAddress,
+ systemEmailPassword,
+ systemEmailConnectionHost,
+ systemEmailSecure,
+ systemEmailPool,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
+ systemEmailTLSServername,
+ });
if (!messageId) {
return res.error({
@@ -105,7 +117,7 @@ class SettingsController {
data: { messageId },
});
} catch (error) {
- next(handleValidationError(error, SERVICE_NAME));
+ next(handleError(error, SERVICE_NAME));
return;
}
};
diff --git a/server/controllers/statusPageController.js b/server/controllers/statusPageController.js
index d0bdcf4b5..577cf4997 100755
--- a/server/controllers/statusPageController.js
+++ b/server/controllers/statusPageController.js
@@ -24,7 +24,13 @@ class StatusPageController {
}
try {
- const statusPage = await this.db.createStatusPage(req.body, req.file);
+ const { _id, teamId } = req.user;
+ const statusPage = await this.db.createStatusPage({
+ statusPageData: req.body,
+ image: req.file,
+ userId: _id,
+ teamId,
+ });
return res.success({
msg: this.stringService.statusPageCreate,
data: statusPage,
@@ -71,29 +77,6 @@ class StatusPageController {
}
};
- getDistributedStatusPageByUrl = async (req, res, next) => {
- try {
- await getStatusPageParamValidation.validateAsync(req.params);
- await getStatusPageQueryValidation.validateAsync(req.query);
- } catch (error) {
- next(handleValidationError(error, SERVICE_NAME));
- return;
- }
-
- try {
- const statusPage = await this.db.getDistributedStatusPageByUrl({
- url: req.params.url,
- daysToShow: req.params.timeFrame,
- });
- return res.success({
- msg: this.stringService.statusPageByUrl,
- data: statusPage,
- });
- } catch (error) {
- next(handleError(error, SERVICE_NAME, "getDistributedStatusPageByUrl"));
- }
- };
-
getStatusPageByUrl = async (req, res, next) => {
try {
await getStatusPageParamValidation.validateAsync(req.params);
@@ -116,7 +99,7 @@ class StatusPageController {
getStatusPagesByTeamId = async (req, res, next) => {
try {
- const teamId = req.params.teamId;
+ const teamId = req.user.teamId;
const statusPages = await this.db.getStatusPagesByTeamId(teamId);
return res.success({
diff --git a/server/db/models/AppSettings.js b/server/db/models/AppSettings.js
index c47de06fa..cd63118e9 100755
--- a/server/db/models/AppSettings.js
+++ b/server/db/models/AppSettings.js
@@ -32,6 +32,29 @@ const AppSettingsSchema = mongoose.Schema(
type: String,
default: "localhost",
},
+ systemEmailTLSServername: {
+ type: String,
+ },
+ systemEmailSecure: {
+ type: Boolean,
+ default: false,
+ },
+ systemEmailPool: {
+ type: Boolean,
+ default: false,
+ },
+ systemEmailIgnoreTLS: {
+ type: Boolean,
+ default: false,
+ },
+ systemEmailRequireTLS: {
+ type: Boolean,
+ default: false,
+ },
+ systemEmailRejectUnauthorized: {
+ type: Boolean,
+ default: true,
+ },
singleton: {
type: Boolean,
required: true,
diff --git a/server/db/models/DistributedUptimeCheck.js b/server/db/models/DistributedUptimeCheck.js
deleted file mode 100755
index cb88e2c31..000000000
--- a/server/db/models/DistributedUptimeCheck.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import mongoose from "mongoose";
-
-import { BaseCheckSchema } from "./Check.js";
-
-// {
-// "id": "12123",
-// "result": {
-// "task_arrived": "2025-01-13T19:21:37.463466602Z",
-// "dns_start": "2025-01-14T00:21:33.1801319+05:00",
-// "dns_end": "2025-01-14T00:21:33.4582552+05:00",
-// "conn_start": "2025-01-14T00:21:33.1801319+05:00",
-// "conn_end": "2025-01-14T00:21:33.7076318+05:00",
-// "connect_start": "2025-01-14T00:21:33.4582552+05:00",
-// "connect_end": "2025-01-14T00:21:33.541899+05:00",
-// "tls_hand_shake_start": "2025-01-14T00:21:33.541899+05:00",
-// "tls_hand_shake_end": "2025-01-14T00:21:33.7076318+05:00",
-// "body_read_start": "2025-01-14T00:21:34.1894707+05:00",
-// "body_read_end": "2025-01-14T00:21:34.1894707+05:00",
-// "wrote_request": "2025-01-14T00:21:33.7076318+05:00",
-// "got_first_response_byte": "2025-01-14T00:21:34.1327652+05:00",
-// "first_byte_took": 425133400,
-// "body_read_took": 56030000,
-// "dns_took": 278123300,
-// "conn_took": 527499900,
-// "connect_took": 83643800,
-// "tls_took": 165732800,
-// "sni_name": "uprock.com",
-// "status_code": 200,
-// "body_size": 19320,
-// "request_header_size": 95,
-// "response_header_size": 246,
-// "response_headers": "X-Vercel-Id: bom1::iad1::sm87v-1736796096856-aec270c01f23\nDate: Mon, 13 Jan 2025 19:21:37 GMT\nServer: Vercel\nStrict-Transport-Security: max-age=63072000\nVary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url\nX-Matched-Path: /\nX-Powered-By: Next.js\nX-Vercel-Cache: MISS\nAge: 0\nCache-Control: private, no-cache, no-store, max-age=0, must-revalidate\nContent-Type: text/html; charset=utf-8",
-// "error": "",
-// "device_id": "d5f578e143a2cd603dd6bf5f846a86a538bde4a8fbe2ad1fca284ad9f033daf8",
-// "ip_address": "223.123.19.0",
-// "proof": "",
-// "created_at": "2025-01-13T19:21:37.463466912Z",
-// "continent": "AS",
-// "country_code": "PK",
-// "city": "Muzaffargarh",
-// "upt_burnt" : "0.01",
-// "location": {
-// "lat": 71.0968,
-// "lng": 30.0208
-// },
-// "payload": {
-// "callback": "https://webhook.site/2a15b0af-545a-4ac2-b913-153b97592d7a",
-// "x": "y"
-// }
-// }
-// }
-
-const LocationSchema = new mongoose.Schema(
- {
- lat: { type: Number, required: true },
- lng: { type: Number, required: true },
- },
- { _id: false }
-);
-
-const DistributedUptimeCheckSchema = mongoose.Schema(
- {
- ...BaseCheckSchema.obj,
- first_byte_took: {
- type: Number,
- required: false,
- },
- body_read_took: {
- type: Number,
- required: false,
- },
- dns_took: {
- type: Number,
- required: false,
- },
- conn_took: {
- type: Number,
- required: false,
- },
- connect_took: {
- type: Number,
- required: false,
- },
- tls_took: {
- type: Number,
- required: false,
- },
- location: {
- type: LocationSchema,
- required: false,
- },
- continent: {
- type: String,
- required: false,
- },
- countryCode: {
- type: String,
- required: false,
- },
- city: {
- type: String,
- required: false,
- },
- uptBurnt: {
- type: mongoose.Schema.Types.Decimal128,
- required: false,
- },
- count: {
- type: Number,
- required: false,
- },
- },
- { timestamps: true }
-);
-
-DistributedUptimeCheckSchema.pre("save", function (next) {
- if (this.isModified("uptBurnt") && typeof this.uptBurnt === "string") {
- this.uptBurnt = mongoose.Types.Decimal128.fromString(this.uptBurnt);
- }
- next();
-});
-
-DistributedUptimeCheckSchema.index({ createdAt: 1 });
-DistributedUptimeCheckSchema.index({ monitorId: 1, updatedAt: 1 });
-DistributedUptimeCheckSchema.index({ monitorId: 1, updatedAt: -1 });
-DistributedUptimeCheckSchema.index(
- {
- monitorId: 1,
- createdAt: -1,
- city: 1,
- "location.lat": 1,
- "location.lng": 1,
- responseTime: 1,
- },
- { background: true }
-);
-export default mongoose.model("DistributedUptimeCheck", DistributedUptimeCheckSchema);
diff --git a/server/db/models/HardwareCheck.js b/server/db/models/HardwareCheck.js
index 76b9dfc2e..bda9f693c 100755
--- a/server/db/models/HardwareCheck.js
+++ b/server/db/models/HardwareCheck.js
@@ -35,6 +35,11 @@ const errorSchema = mongoose.Schema({
err: { type: String, default: "" },
});
+const captureSchema = mongoose.Schema({
+ version: { type: String, default: "" },
+ mode: { type: String, default: "" },
+});
+
const HardwareCheckSchema = mongoose.Schema(
{
...BaseCheckSchema.obj,
@@ -59,6 +64,11 @@ const HardwareCheckSchema = mongoose.Schema(
type: [errorSchema],
default: () => [],
},
+
+ capture: {
+ type: captureSchema,
+ default: () => ({}),
+ },
},
{ timestamps: true }
);
diff --git a/server/db/models/Monitor.js b/server/db/models/Monitor.js
index 508c1c655..38aeff0c3 100755
--- a/server/db/models/Monitor.js
+++ b/server/db/models/Monitor.js
@@ -1,4 +1,9 @@
import mongoose from "mongoose";
+import HardwareCheck from "./HardwareCheck.js";
+import PageSpeedCheck from "./PageSpeedCheck.js";
+import Check from "./Check.js";
+import MonitorStats from "./MonitorStats.js";
+import StatusPage from "./StatusPage.js";
const MonitorSchema = mongoose.Schema(
{
@@ -28,16 +33,7 @@ const MonitorSchema = mongoose.Schema(
type: {
type: String,
required: true,
- enum: [
- "http",
- "ping",
- "pagespeed",
- "hardware",
- "docker",
- "port",
- "distributed_http",
- "distributed_test",
- ],
+ enum: ["http", "ping", "pagespeed", "hardware", "docker", "port"],
},
ignoreTlsErrors: {
type: Boolean,
@@ -51,7 +47,7 @@ const MonitorSchema = mongoose.Schema(
},
matchMethod: {
type: String,
- enum: ["equal", "include", "regex"],
+ enum: ["equal", "include", "regex", ""],
},
url: {
type: String,
@@ -73,15 +69,6 @@ const MonitorSchema = mongoose.Schema(
type: Number,
default: undefined,
},
- thresholds: {
- type: {
- usage_cpu: { type: Number },
- usage_memory: { type: Number },
- usage_disk: { type: Number },
- usage_temperature: { type: Number },
- },
- _id: false,
- },
notifications: [
{
type: mongoose.Schema.Types.ObjectId,
@@ -91,12 +78,115 @@ const MonitorSchema = mongoose.Schema(
secret: {
type: String,
},
+ thresholds: {
+ type: {
+ usage_cpu: { type: Number },
+ usage_memory: { type: Number },
+ usage_disk: { type: Number },
+ usage_temperature: { type: Number },
+ },
+ _id: false,
+ },
+ alertThreshold: {
+ type: Number,
+ default: 5,
+ },
+ cpuAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ memoryAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ diskAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
+ tempAlertThreshold: {
+ type: Number,
+ default: function () {
+ return this.alertThreshold;
+ },
+ },
},
{
timestamps: true,
}
);
+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 });
+ }
+
+ // Deal with status pages
+ await StatusPage.updateMany({ monitors: doc._id }, { $pull: { monitors: 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.pre("save", function (next) {
+ if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) {
+ this.cpuAlertThreshold = this.alertThreshold;
+ }
+ if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) {
+ this.memoryAlertThreshold = this.alertThreshold;
+ }
+ if (!this.diskAlertThreshold || this.isModified("alertThreshold")) {
+ this.diskAlertThreshold = this.alertThreshold;
+ }
+ if (!this.tempAlertThreshold || this.isModified("alertThreshold")) {
+ this.tempAlertThreshold = this.alertThreshold;
+ }
+ next();
+});
+
+MonitorSchema.pre("findOneAndUpdate", function (next) {
+ const update = this.getUpdate();
+ if (update.alertThreshold) {
+ update.cpuAlertThreshold = update.alertThreshold;
+ update.memoryAlertThreshold = update.alertThreshold;
+ update.diskAlertThreshold = update.alertThreshold;
+ update.tempAlertThreshold = update.alertThreshold;
+ }
+ next();
+});
+
MonitorSchema.index({ teamId: 1, type: 1 });
export default mongoose.model("Monitor", MonitorSchema);
diff --git a/server/db/models/NetworkCheck.js b/server/db/models/NetworkCheck.js
new file mode 100644
index 000000000..eca69eef9
--- /dev/null
+++ b/server/db/models/NetworkCheck.js
@@ -0,0 +1,46 @@
+import mongoose from "mongoose";
+import { BaseCheckSchema } from "./Check.js";
+
+const networkInterfaceSchema = mongoose.Schema({
+ name: { type: String, required: true },
+ bytes_sent: { type: Number, default: 0 },
+ bytes_recv: { type: Number, default: 0 },
+ packets_sent: { type: Number, default: 0 },
+ packets_recv: { type: Number, default: 0 },
+ err_in: { type: Number, default: 0 },
+ err_out: { type: Number, default: 0 },
+ drop_in: { type: Number, default: 0 },
+ drop_out: { type: Number, default: 0 },
+ fifo_in: { type: Number, default: 0 },
+ fifo_out: { type: Number, default: 0 },
+});
+
+const captureSchema = mongoose.Schema({
+ version: { type: String, default: "" },
+ mode: { type: String, default: "" },
+});
+
+const NetworkCheckSchema = mongoose.Schema(
+ {
+ ...BaseCheckSchema.obj,
+ data: {
+ type: [networkInterfaceSchema],
+ default: () => [],
+ },
+ capture: {
+ type: captureSchema,
+ default: () => ({}),
+ },
+ errors: {
+ type: mongoose.Schema.Types.Mixed,
+ default: null,
+ },
+ },
+ { timestamps: true }
+);
+
+NetworkCheckSchema.index({ createdAt: 1 });
+NetworkCheckSchema.index({ monitorId: 1, createdAt: 1 });
+NetworkCheckSchema.index({ monitorId: 1, createdAt: -1 });
+
+export default mongoose.model("NetworkCheck", NetworkCheckSchema);
diff --git a/server/db/models/Notification.js b/server/db/models/Notification.js
index 43eb73c93..a6a3c7c09 100755
--- a/server/db/models/Notification.js
+++ b/server/db/models/Notification.js
@@ -1,31 +1,26 @@
import mongoose from "mongoose";
-const configSchema = mongoose.Schema(
- {
- webhookUrl: { type: String },
- botToken: { type: String },
- chatId: { type: String },
- },
- { _id: false }
-);
-
const NotificationSchema = mongoose.Schema(
{
- monitorId: {
+ userId: {
type: mongoose.Schema.Types.ObjectId,
- ref: "Monitor",
+ ref: "User",
immutable: true,
+ required: true,
+ },
+ teamId: {
+ type: mongoose.Schema.Types.ObjectId,
+ ref: "Team",
+ immutable: true,
+ required: true,
},
type: {
type: String,
- enum: ["email", "sms", "webhook"],
+ enum: ["email", "slack", "discord", "webhook", "pager_duty"],
},
- platform: {
+ notificationName: {
type: String,
- },
- config: {
- type: configSchema,
- default: () => ({}),
+ required: true,
},
address: {
type: String,
@@ -33,34 +28,6 @@ const NotificationSchema = mongoose.Schema(
phone: {
type: String,
},
- alertThreshold: {
- type: Number,
- default: 5,
- },
- cpuAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- memoryAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- diskAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
- tempAlertThreshold: {
- type: Number,
- default: function () {
- return this.alertThreshold;
- },
- },
},
{
timestamps: true,
diff --git a/server/db/models/StatusPage.js b/server/db/models/StatusPage.js
index ab7be6955..92cbab1cd 100755
--- a/server/db/models/StatusPage.js
+++ b/server/db/models/StatusPage.js
@@ -18,7 +18,7 @@ const StatusPageSchema = mongoose.Schema(
type: String,
required: true,
default: "uptime",
- enum: ["uptime", "distributed"],
+ enum: ["uptime"],
},
companyName: {
type: String,
@@ -70,6 +70,10 @@ const StatusPageSchema = mongoose.Schema(
type: Boolean,
default: true,
},
+ showAdminLoginLink: {
+ type: Boolean,
+ default: false,
+ },
},
{ timestamps: true }
);
diff --git a/server/db/models/User.js b/server/db/models/User.js
index 74ee1166e..f180cfb86 100755
--- a/server/db/models/User.js
+++ b/server/db/models/User.js
@@ -1,6 +1,9 @@
import mongoose from "mongoose";
import bcrypt from "bcryptjs";
import logger from "../../utils/logger.js";
+import Monitor from "./Monitor.js";
+import Team from "./Team.js";
+import Notification from "./Notification.js";
const UserSchema = mongoose.Schema(
{
@@ -74,6 +77,25 @@ UserSchema.pre("findOneAndUpdate", function (next) {
next();
});
+UserSchema.pre("findOneAndDelete", async function (next) {
+ try {
+ const userToDelete = await this.model.findOne(this.getFilter());
+ if (!userToDelete) return next();
+ if (userToDelete.role.includes("superadmin")) {
+ await Team.deleteOne({ _id: userToDelete.teamId });
+ await Monitor.deleteMany({ userId: userToDelete._id });
+ await this.model.deleteMany({
+ teamId: userToDelete.teamId,
+ _id: { $ne: userToDelete._id },
+ });
+ await Notification.deleteMany({ teamId: userToDelete.teamId });
+ }
+ next();
+ } catch (error) {
+ next(error);
+ }
+});
+
UserSchema.methods.comparePassword = async function (submittedPassword) {
const res = await bcrypt.compare(submittedPassword, this.password);
return res;
diff --git a/server/db/mongo/MongoDB.js b/server/db/mongo/MongoDB.js
index b657caec4..5e5e7aec7 100755
--- a/server/db/mongo/MongoDB.js
+++ b/server/db/mongo/MongoDB.js
@@ -43,11 +43,6 @@ import * as hardwareCheckModule from "./modules/hardwareCheckModule.js";
import * as checkModule from "./modules/checkModule.js";
-//****************************************
-// Distributed Checks
-//****************************************
-import * as distributedCheckModule from "./modules/distributedCheckModule.js";
-
//****************************************
// Maintenance Window
//****************************************
@@ -85,7 +80,6 @@ class MongoDB {
Object.assign(this, pageSpeedCheckModule);
Object.assign(this, hardwareCheckModule);
Object.assign(this, checkModule);
- Object.assign(this, distributedCheckModule);
Object.assign(this, maintenanceWindowModule);
Object.assign(this, notificationModule);
Object.assign(this, settingsModule);
diff --git a/server/db/mongo/modules/checkModule.js b/server/db/mongo/modules/checkModule.js
index 2339edbed..a53384d1a 100755
--- a/server/db/mongo/modules/checkModule.js
+++ b/server/db/mongo/modules/checkModule.js
@@ -2,7 +2,6 @@ import Check from "../../models/Check.js";
import Monitor from "../../models/Monitor.js";
import HardwareCheck from "../../models/HardwareCheck.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
-import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import User from "../../models/User.js";
import logger from "../../../utils/logger.js";
import { ObjectId } from "mongodb";
@@ -58,16 +57,23 @@ const createChecks = async (checks) => {
* @returns {Promise>}
* @throws {Error}
*/
-const getChecksByMonitor = async (req) => {
+const getChecksByMonitor = async ({
+ monitorId,
+ type,
+ sortOrder,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ status,
+}) => {
try {
- const { monitorId } = req.params;
- let { type, sortOrder, dateRange, filter, page, rowsPerPage, status } = req.query;
status = typeof status !== "undefined" ? false : undefined;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
// Match
const matchStage = {
- monitorId: ObjectId.createFromHexString(monitorId),
+ monitorId: new ObjectId(monitorId),
...(typeof status !== "undefined" && { status }),
...(dateRangeLookup[dateRange] && {
createdAt: {
@@ -111,8 +117,6 @@ const getChecksByMonitor = async (req) => {
port: Check,
pagespeed: PageSpeedCheck,
hardware: HardwareCheck,
- distributed_http: DistributedUptimeCheck,
- distributed_test: DistributedUptimeCheck,
};
const Model = checkModels[type];
@@ -145,14 +149,19 @@ const getChecksByMonitor = async (req) => {
}
};
-const getChecksByTeam = async (req) => {
+const getChecksByTeam = async ({
+ sortOrder,
+ dateRange,
+ filter,
+ page,
+ rowsPerPage,
+ teamId,
+}) => {
try {
- let { sortOrder, dateRange, filter, page, rowsPerPage } = req.query;
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
- const { teamId } = req.params;
const matchStage = {
- teamId: ObjectId.createFromHexString(teamId),
+ teamId: new ObjectId(teamId),
status: false,
...(dateRangeLookup[dateRange] && {
createdAt: {
@@ -202,12 +211,7 @@ const getChecksByTeam = async (req) => {
pipeline: [{ $match: matchStage }],
},
},
- {
- $unionWith: {
- coll: "distributeduptimechecks",
- pipeline: [{ $match: matchStage }],
- },
- },
+
{ $sort: { createdAt: sortOrder } },
{
$facet: {
diff --git a/server/db/mongo/modules/diagnosticModule.js b/server/db/mongo/modules/diagnosticModule.js
index 2fd8acc71..e59830518 100755
--- a/server/db/mongo/modules/diagnosticModule.js
+++ b/server/db/mongo/modules/diagnosticModule.js
@@ -1,4 +1,3 @@
-import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import Monitor from "../../models/Monitor.js";
import { ObjectId } from "mongodb";
@@ -7,57 +6,7 @@ import {
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
- buildDePINDetailsByDateRange,
- buildDePINLatestChecks,
} from "./monitorModuleQueries.js";
-import { getDateRange } from "./monitorModule.js";
-
-const getDistributedUptimeDbExecutionStats = async (req) => {
- try {
- const { monitorId } = req?.params ?? {};
- if (typeof monitorId === "undefined") {
- throw new Error();
- }
- const monitor = await Monitor.findById(monitorId);
- if (monitor === null || monitor === undefined) {
- throw new Error(this.stringService.dbFindMonitorById(monitorId));
- }
-
- const { dateRange } = req.query;
- const dates = getDateRange(dateRange);
- const formatLookup = {
- recent: "%Y-%m-%dT%H:%M:00Z",
- day: {
- $concat: [
- { $dateToString: { format: "%Y-%m-%dT%H:", date: "$createdAt" } },
- {
- $cond: [{ $lt: [{ $minute: "$createdAt" }, 30] }, "00:00Z", "30:00Z"],
- },
- ],
- },
- week: "%Y-%m-%dT%H:00:00Z",
- month: "%Y-%m-%dT00:00:00Z",
- };
-
- const dateString = formatLookup[dateRange];
-
- const dePINDetailsByDateRangeStats = await DistributedUptimeCheck.aggregate(
- buildDePINDetailsByDateRange(monitor, dates, dateString)
- ).explain("executionStats");
- const latestChecksStats = await DistributedUptimeCheck.aggregate(
- buildDePINLatestChecks(monitor)
- ).explain("executionStats");
-
- return {
- dePINDetailsByDateRangeStats,
- latestChecksStats,
- };
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "getAllMonitorsWithUptimeStats";
- throw error;
- }
-};
const getMonitorsByTeamIdExecutionStats = async (req) => {
try {
@@ -70,7 +19,7 @@ const getMonitorsByTeamIdExecutionStats = async (req) => {
order = "asc";
}
// Build match stage
- const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
+ const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
@@ -104,4 +53,4 @@ const getMonitorsByTeamIdExecutionStats = async (req) => {
}
};
-export { getDistributedUptimeDbExecutionStats, getMonitorsByTeamIdExecutionStats };
+export { getMonitorsByTeamIdExecutionStats };
diff --git a/server/db/mongo/modules/distributedCheckModule.js b/server/db/mongo/modules/distributedCheckModule.js
deleted file mode 100755
index 7200cba33..000000000
--- a/server/db/mongo/modules/distributedCheckModule.js
+++ /dev/null
@@ -1,156 +0,0 @@
-import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
-import { ObjectId } from "mongodb";
-
-const SERVICE_NAME = "distributedCheckModule";
-
-const createDistributedCheck = async (checkData) => {
- try {
- if (typeof checkData.monitorId === "string") {
- checkData.monitorId = ObjectId.createFromHexString(checkData.monitorId);
- }
- const check = await DistributedUptimeCheck.findOneAndUpdate(
- {
- monitorId: checkData.monitorId,
- city: checkData.city,
- },
- [
- {
- $set: {
- ...checkData,
-
- responseTime: {
- $cond: {
- if: { $ifNull: ["$count", false] },
- then: {
- $cond: {
- // Check if the new value is an outlier (3x the current average)
- if: {
- $and: [
- { $gt: ["$responseTime", 0] },
- {
- $gt: [
- checkData.responseTime,
- { $multiply: ["$responseTime", 3] },
- ],
- },
- ],
- },
- then: "$responseTime", // Keep the current value if it's an outlier
- else: {
- // Normal case - calculate new average
- $round: [
- {
- $divide: [
- {
- $add: [
- { $multiply: ["$responseTime", "$count"] },
- checkData.responseTime,
- ],
- },
- { $add: ["$count", 1] },
- ],
- },
- 2,
- ],
- },
- },
- },
- else: checkData.responseTime,
- },
- },
- count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
- },
- },
- ],
- {
- upsert: true,
- new: true,
- runValidators: true,
- }
- );
- return check;
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "createCheck";
- throw error;
- }
-};
-
-const createDistributedChecks = async (checksData) => {
- try {
- if (!Array.isArray(checksData) || checksData.length === 0) {
- return;
- }
-
- const bulkOps = checksData.map((checkData) => {
- if (typeof checkData.monitorId === "string") {
- checkData.monitorId = ObjectId.createFromHexString(checkData.monitorId);
- }
-
- return {
- updateOne: {
- filter: {
- monitorId: checkData.monitorId,
- city: checkData.city,
- },
- update: [
- {
- $set: {
- ...checkData,
- responseTime: {
- $cond: {
- if: { $ifNull: ["$count", false] },
- then: {
- $round: [
- {
- $divide: [
- {
- $add: [
- { $multiply: ["$responseTime", "$count"] },
- checkData.responseTime,
- ],
- },
- { $add: ["$count", 1] },
- ],
- },
- 2,
- ],
- },
- else: checkData.responseTime,
- },
- },
- count: { $add: [{ $ifNull: ["$count", 0] }, 1] },
- },
- },
- ],
- upsert: true,
- },
- };
- });
-
- await DistributedUptimeCheck.bulkWrite(bulkOps, {
- ordered: false,
- });
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "createDistributedChecks";
- throw error;
- }
-};
-
-const deleteDistributedChecksByMonitorId = async (monitorId) => {
- try {
- const result = await DistributedUptimeCheck.deleteMany({ monitorId });
- return result.deletedCount;
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "deleteDistributedChecksByMonitorId";
- throw error;
- }
-};
-
-export {
- createDistributedCheck,
- createDistributedChecks,
- deleteDistributedChecksByMonitorId,
-};
diff --git a/server/db/mongo/modules/monitorModule.js b/server/db/mongo/modules/monitorModule.js
index fe8e3c2fc..bc31915c2 100755
--- a/server/db/mongo/modules/monitorModule.js
+++ b/server/db/mongo/modules/monitorModule.js
@@ -3,8 +3,6 @@ import MonitorStats from "../../models/MonitorStats.js";
import Check from "../../models/Check.js";
import PageSpeedCheck from "../../models/PageSpeedCheck.js";
import HardwareCheck from "../../models/HardwareCheck.js";
-import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
-import Notification from "../../models/Notification.js";
import { NormalizeData, NormalizeDataUptimeDetails } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
@@ -16,14 +14,11 @@ import { ObjectId } from "mongodb";
import {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
- buildMonitorStatsPipeline,
buildMonitorSummaryByTeamIdPipeline,
buildMonitorsByTeamIdPipeline,
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
- buildDePINDetailsByDateRange,
- buildDePINLatestChecks,
} from "./monitorModuleQueries.js";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
@@ -336,11 +331,9 @@ const calculateGroupStats = (group) => {
* @returns {Promise}
* @throws {Error}
*/
-const getUptimeDetailsById = async (req) => {
+const getUptimeDetailsById = async ({ monitorId, dateRange, normalize }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
- const { monitorId } = req.params;
- const { dateRange, normalize } = req.query;
const dates = getDateRange(dateRange);
const formatLookup = {
recent: "%Y-%m-%dT%H:%M:00Z",
@@ -385,72 +378,6 @@ const getUptimeDetailsById = async (req) => {
}
};
-const getDistributedUptimeDetailsById = async (req) => {
- try {
- const { monitorId } = req?.params ?? {};
- if (typeof monitorId === "undefined") {
- throw new Error();
- }
- const monitor = await Monitor.findById(monitorId);
- if (monitor === null || monitor === undefined) {
- throw new Error(this.stringService.dbFindMonitorById(monitorId));
- }
-
- const { dateRange, normalize } = req.query;
- const dates = getDateRange(dateRange);
- const formatLookup = {
- recent: "%Y-%m-%dT%H:%M:00Z",
- day: {
- $concat: [
- { $dateToString: { format: "%Y-%m-%dT%H:", date: "$updatedAt" } },
- {
- $cond: [{ $lt: [{ $minute: "$updatedAt" }, 30] }, "00:00Z", "30:00Z"],
- },
- ],
- },
- week: "%Y-%m-%dT%H:00:00Z",
- month: "%Y-%m-%dT00:00:00Z",
- };
-
- const dateString = formatLookup[dateRange];
-
- const monitorStatsResult = await MonitorStats.aggregate(
- buildMonitorStatsPipeline(monitor)
- );
- const monitorStats = monitorStatsResult[0];
- const dePINDetailsByDateRange = await DistributedUptimeCheck.aggregate(
- buildDePINDetailsByDateRange(monitor, dates, dateString)
- );
- const latestChecks = await DistributedUptimeCheck.aggregate(
- buildDePINLatestChecks(monitor)
- );
-
- const checkData = dePINDetailsByDateRange[0];
- const normalizedGroupChecks = NormalizeDataUptimeDetails(
- checkData.groupedChecks,
- 10,
- 100
- );
- const data = {
- ...monitor.toObject(),
- latestChecks,
- totalChecks: monitorStats?.totalChecks,
- avgResponseTime: monitorStats?.avgResponseTime,
- uptimePercentage: monitorStats?.uptimePercentage,
- timeSinceLastCheck: monitorStats?.timeSinceLastCheck,
- uptBurnt: monitorStats?.uptBurnt,
- groupedChecks: normalizedGroupChecks,
- groupedMapChecks: checkData.groupedMapChecks,
- };
-
- return data;
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "getDistributedUptimeDetailsById";
- throw error;
- }
-};
-
/**
* Get stats by monitor ID
* @async
@@ -459,11 +386,16 @@ const getDistributedUptimeDetailsById = async (req) => {
* @returns {Promise}
* @throws {Error}
*/
-const getMonitorStatsById = async (req) => {
+const getMonitorStatsById = async ({
+ monitorId,
+ limit,
+ sortOrder,
+ dateRange,
+ numToDisplay,
+ normalize,
+}) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
- const { monitorId } = req.params;
-
// Get monitor, if we can't find it, abort with error
const monitor = await Monitor.findById(monitorId);
if (monitor === null || monitor === undefined) {
@@ -471,7 +403,6 @@ const getMonitorStatsById = async (req) => {
}
// Get query params
- let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query;
const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested
@@ -521,10 +452,8 @@ const getMonitorStatsById = async (req) => {
}
};
-const getHardwareDetailsById = async (req) => {
+const getHardwareDetailsById = async ({ monitorId, dateRange }) => {
try {
- const { monitorId } = req.params;
- const { dateRange } = req.query;
const monitor = await Monitor.findById(monitorId);
const dates = getDateRange(dateRange);
const formatLookup = {
@@ -567,14 +496,6 @@ const getMonitorById = async (monitorId) => {
error.status = 404;
throw error;
}
- // Get notifications
- const notifications = await Notification.find({
- monitorId: monitorId,
- });
-
- // Update monitor with notifications and save
- monitor.notifications = notifications;
- await monitor.save();
return monitor;
} catch (error) {
@@ -584,8 +505,16 @@ const getMonitorById = async (monitorId) => {
}
};
-const getMonitorsByTeamId = async (req) => {
- let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
+const getMonitorsByTeamId = async ({
+ limit,
+ type,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ teamId,
+}) => {
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
@@ -594,7 +523,7 @@ const getMonitorsByTeamId = async (req) => {
order = "asc";
}
// Build match stage
- const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
+ const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
@@ -632,16 +561,14 @@ const getMonitorsByTeamId = async (req) => {
return { summary, monitors, filteredMonitors: normalizedFilteredMonitors };
};
-const getMonitorsAndSummaryByTeamId = async (req) => {
+const getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => {
try {
- const { type } = req.query;
- const teamId = ObjectId.createFromHexString(req.params.teamId);
- const matchStage = { teamId };
+ const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
- if (req.explain === true) {
+ if (explain === true) {
return Monitor.aggregate(
buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })
).explain("executionStats");
@@ -659,9 +586,18 @@ const getMonitorsAndSummaryByTeamId = async (req) => {
}
};
-const getMonitorsWithChecksByTeamId = async (req) => {
+const getMonitorsWithChecksByTeamId = async ({
+ limit,
+ type,
+ page,
+ rowsPerPage,
+ filter,
+ field,
+ order,
+ teamId,
+ explain,
+}) => {
try {
- let { limit, type, page, rowsPerPage, filter, field, order } = req.query;
limit = parseInt(limit);
page = parseInt(page);
rowsPerPage = parseInt(rowsPerPage);
@@ -669,14 +605,13 @@ const getMonitorsWithChecksByTeamId = async (req) => {
field = "name";
order = "asc";
}
- const teamId = ObjectId.createFromHexString(req.params.teamId);
// Build match stage
- const matchStage = { teamId };
+ const matchStage = { teamId: new ObjectId(teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
- if (req.explain === true) {
+ if (explain === true) {
return Monitor.aggregate(
buildMonitorsWithChecksByTeamIdPipeline({
matchStage,
@@ -728,13 +663,11 @@ const getMonitorsWithChecksByTeamId = async (req) => {
* @returns {Promise}
* @throws {Error}
*/
-const createMonitor = async (req, res) => {
+const createMonitor = async ({ body, teamId, userId }) => {
try {
- const monitor = new Monitor({ ...req.body });
- // Remove notifications fom monitor as they aren't needed here
- monitor.notifications = undefined;
- await monitor.save();
- return monitor;
+ const monitor = new Monitor({ ...body, teamId, userId });
+ const saved = await monitor.save();
+ return saved;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "createMonitor";
@@ -771,15 +704,15 @@ const createBulkMonitors = async (req) => {
* @returns {Promise}
* @throws {Error}
*/
-const deleteMonitor = async (req, res) => {
+const deleteMonitor = async ({ monitorId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
-
- const monitorId = req.params.monitorId;
try {
const monitor = await Monitor.findByIdAndDelete(monitorId);
+
if (!monitor) {
throw new Error(stringService.getDbFindMonitorById(monitorId));
}
+
return monitor;
} catch (error) {
error.service = SERVICE_NAME;
@@ -831,8 +764,6 @@ const deleteMonitorsByUserId = async (userId) => {
* @throws {Error}
*/
const editMonitor = async (candidateId, candidateMonitor) => {
- candidateMonitor.notifications = undefined;
-
try {
const editedMonitor = await Monitor.findByIdAndUpdate(candidateId, candidateMonitor, {
new: true,
@@ -867,6 +798,29 @@ const addDemoMonitors = async (userId, teamId) => {
}
};
+const pauseMonitor = async ({ monitorId }) => {
+ try {
+ const monitor = await Monitor.findOneAndUpdate(
+ { _id: monitorId },
+ [
+ {
+ $set: {
+ isActive: { $not: "$isActive" },
+ status: "$$REMOVE",
+ },
+ },
+ ],
+ { new: true }
+ );
+
+ return monitor;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "pauseMonitor";
+ throw error;
+ }
+};
+
export {
getAllMonitors,
getAllMonitorsWithUptimeStats,
@@ -876,7 +830,6 @@ export {
getMonitorsAndSummaryByTeamId,
getMonitorsWithChecksByTeamId,
getUptimeDetailsById,
- getDistributedUptimeDetailsById,
createMonitor,
createBulkMonitors,
deleteMonitor,
@@ -885,6 +838,7 @@ export {
editMonitor,
addDemoMonitors,
getHardwareDetailsById,
+ pauseMonitor,
};
// Helper functions
diff --git a/server/db/mongo/modules/monitorModuleQueries.js b/server/db/mongo/modules/monitorModuleQueries.js
index 4af3f8985..ab45eebb6 100755
--- a/server/db/mongo/modules/monitorModuleQueries.js
+++ b/server/db/mongo/modules/monitorModuleQueries.js
@@ -4,7 +4,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
return [
{
$match: {
- monitorId: ObjectId.createFromHexString(monitorId),
+ monitorId: new ObjectId(monitorId),
createdAt: { $gte: dates.start, $lte: dates.end },
},
},
@@ -148,6 +148,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
type: 1,
url: 1,
isActive: 1,
+ notifications: 1,
},
},
],
@@ -406,6 +407,16 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
],
},
},
+ { $unwind: "$checks" },
+ { $sort: { "checks._id": 1 } },
+ {
+ $group: {
+ _id: "$_id",
+ checks: { $push: "$checks" },
+ aggregateData: { $first: "$aggregateData" },
+ upChecks: { $first: "$upChecks" },
+ },
+ },
{
$project: {
aggregateData: {
@@ -414,12 +425,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
upChecks: {
$arrayElemAt: ["$upChecks", 0],
},
- checks: {
- $sortArray: {
- input: "$checks",
- sortBy: { _id: 1 },
- },
- },
+ checks: 1,
},
},
];
@@ -613,8 +619,6 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
- } else if (type === "distributed_http" || type === "distributed_test") {
- checksCollection = "distributeduptimechecks";
}
monitorsPipeline.push({
$lookup: {
@@ -702,8 +706,6 @@ const buildFilteredMonitorsByTeamIdPipeline = ({
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
- } else if (type === "distributed_http" || type === "distributed_test") {
- checksCollection = "distributeduptimechecks";
}
pipeline.push({
$lookup: {
@@ -737,7 +739,7 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
order = "asc";
}
// Build the match stage
- const matchStage = { teamId: ObjectId.createFromHexString(req.params.teamId) };
+ const matchStage = { teamId: new ObjectId(req.params.teamId) };
if (type !== undefined) {
matchStage.type = Array.isArray(type) ? { $in: type } : type;
}
@@ -861,26 +863,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
},
]
: []),
- ...(limit
- ? [
- {
- $lookup: {
- from: "distributeduptimechecks",
- let: { monitorId: "$_id" },
- pipeline: [
- {
- $match: {
- $expr: { $eq: ["$monitorId", "$$monitorId"] },
- },
- },
- { $sort: { createdAt: -1 } },
- ...(limit ? [{ $limit: limit }] : []),
- ],
- as: "distributeduptimechecks",
- },
- },
- ]
- : []),
{
$addFields: {
@@ -899,14 +881,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
- {
- case: { $eq: ["$type", "distributed_http"] },
- then: "$distributeduptimechecks",
- },
- {
- case: { $eq: ["$type", "distributed_test"] },
- then: "$distributeduptimechecks",
- },
],
default: [],
},
@@ -933,102 +907,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
];
};
-const buildDePINDetailsByDateRange = (monitor, dates, dateString) => {
- return [
- {
- $match: {
- monitorId: monitor._id,
- updatedAt: { $gte: dates.start, $lte: dates.end },
- },
- },
- {
- $project: {
- _id: 0,
- city: 1,
- updatedAt: 1,
- "location.lat": 1,
- "location.lng": 1,
- responseTime: 1,
- },
- },
- {
- $facet: {
- groupedMapChecks: [
- {
- $group: {
- _id: {
- city: "$city",
- lat: "$location.lat",
- lng: "$location.lng",
- },
-
- avgResponseTime: {
- $avg: "$responseTime",
- },
- },
- },
- ],
- groupedChecks: [
- {
- $group: {
- _id: {
- date: {
- $dateToString: {
- format: dateString,
- date: "$updatedAt",
- },
- },
- },
- avgResponseTime: {
- $avg: "$responseTime",
- },
- },
- },
- {
- $sort: {
- "_id.date": 1,
- },
- },
- ],
- },
- },
- {
- $project: {
- groupedMapChecks: "$groupedMapChecks",
- groupedChecks: "$groupedChecks",
- },
- },
- ];
-};
-
-const buildDePINLatestChecks = (monitor) => {
- return [
- {
- $match: {
- monitorId: monitor._id,
- },
- },
- {
- $sort: { updatedAt: -1 },
- },
- {
- $limit: 5,
- },
- {
- $project: {
- responseTime: 1,
- city: 1,
- countryCode: 1,
- uptBurnt: {
- $toString: {
- $ifNull: ["$uptBurnt", 0],
- },
- },
- },
- },
- ];
-};
-
export {
buildUptimeDetailsPipeline,
buildHardwareDetailsPipeline,
@@ -1039,6 +917,4 @@ export {
buildMonitorsAndSummaryByTeamIdPipeline,
buildMonitorsWithChecksByTeamIdPipeline,
buildFilteredMonitorsByTeamIdPipeline,
- buildDePINDetailsByDateRange,
- buildDePINLatestChecks,
};
diff --git a/server/db/mongo/modules/networkCheckModule.js b/server/db/mongo/modules/networkCheckModule.js
new file mode 100644
index 000000000..53c55c7e3
--- /dev/null
+++ b/server/db/mongo/modules/networkCheckModule.js
@@ -0,0 +1,46 @@
+import NetworkCheck from "../../models/NetworkCheck.js";
+
+const SERVICE_NAME = "networkCheckModule";
+
+/**
+ * Creates and saves a new network check document to the database.
+ * @async
+ * @param {object} networkCheckData - The data for the new network check. This should conform to the NetworkCheckSchema.
+ * @param {string} networkCheckData.monitorId - The ID of the monitor associated with this check.
+ * @returns {Promise} A promise that resolves to the newly created network check document.
+ * @throws {Error} Throws an error if the database operation fails.
+ */
+const createNetworkCheck = async (networkCheckData) => {
+ try {
+ const networkCheck = await new NetworkCheck(networkCheckData);
+ await networkCheck.save();
+ return networkCheck;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "createNetworkCheck";
+ throw error;
+ }
+};
+
+/**
+ * Retrieves a list of network checks for a specific monitor, sorted by most recent.
+ * @async
+ * @param {string} monitorId - The ID of the monitor to retrieve checks for.
+ * @param {number} [limit=100] - The maximum number of checks to return. Defaults to 100.
+ * @returns {Promise>} A promise that resolves to an array of network check documents.
+ * @throws {Error} Throws an error if the database operation fails.
+ */
+const getNetworkChecksByMonitorId = async (monitorId, limit = 100) => {
+ try {
+ const networkChecks = await NetworkCheck.find({ monitorId })
+ .sort({ createdAt: -1 })
+ .limit(limit);
+ return networkChecks;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "getNetworkChecksByMonitorId";
+ throw error;
+ }
+};
+
+export { createNetworkCheck, getNetworkChecksByMonitorId };
diff --git a/server/db/mongo/modules/notificationModule.js b/server/db/mongo/modules/notificationModule.js
index aab2d03bd..08d57b1a9 100755
--- a/server/db/mongo/modules/notificationModule.js
+++ b/server/db/mongo/modules/notificationModule.js
@@ -1,4 +1,5 @@
import Notification from "../../models/Notification.js";
+import Monitor from "../../models/Monitor.js";
const SERVICE_NAME = "notificationModule";
/**
* Creates a new notification.
@@ -21,6 +22,28 @@ const createNotification = async (notificationData) => {
}
};
+const getNotificationsByTeamId = async (teamId) => {
+ try {
+ const notifications = await Notification.find({ teamId });
+ return notifications;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "getNotificationsByTeamId";
+ throw error;
+ }
+};
+
+const getNotificationsByIds = async (notificationIds) => {
+ try {
+ const notifications = await Notification.find({ _id: { $in: notificationIds } });
+ return notifications;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "getNotificationsByIds";
+ throw error;
+ }
+};
+
/**
* Retrieves notifications by monitor ID.
* @param {mongoose.Types.ObjectId} monitorId - The ID of the monitor.
@@ -49,8 +72,49 @@ const deleteNotificationsByMonitorId = async (monitorId) => {
}
};
+const deleteNotificationById = async (id) => {
+ try {
+ const result = await Notification.findByIdAndDelete(id);
+ await Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } });
+ return result;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "deleteNotificationById";
+ throw error;
+ }
+};
+
+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,
+ getNotificationsByIds,
getNotificationsByMonitorId,
deleteNotificationsByMonitorId,
+ deleteNotificationById,
+ getNotificationById,
+ editNotification,
};
diff --git a/server/db/mongo/modules/settingsModule.js b/server/db/mongo/modules/settingsModule.js
index d3568ad32..4fd0d1634 100755
--- a/server/db/mongo/modules/settingsModule.js
+++ b/server/db/mongo/modules/settingsModule.js
@@ -26,10 +26,12 @@ const updateAppSettings = async (newSettings) => {
delete update.$set.systemEmailPassword;
}
- const settings = await AppSettings.findOneAndUpdate({}, update, {
- new: true,
+ await AppSettings.findOneAndUpdate({}, update, {
upsert: true,
});
+ const settings = await AppSettings.findOne()
+ .select("-__v -_id -createdAt -updatedAt -singleton")
+ .lean();
return settings;
} catch (error) {
error.service = SERVICE_NAME;
diff --git a/server/db/mongo/modules/statusPageModule.js b/server/db/mongo/modules/statusPageModule.js
index e5c88eecb..f0f2fa762 100755
--- a/server/db/mongo/modules/statusPageModule.js
+++ b/server/db/mongo/modules/statusPageModule.js
@@ -1,16 +1,19 @@
import StatusPage from "../../models/StatusPage.js";
-import Monitor from "../../models/Monitor.js";
import { NormalizeData } from "../../../utils/dataUtils.js";
import ServiceRegistry from "../../../service/serviceRegistry.js";
import StringService from "../../../service/stringService.js";
const SERVICE_NAME = "statusPageModule";
-const createStatusPage = async (statusPageData, image) => {
+const createStatusPage = async ({ statusPageData, image, userId, teamId }) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
try {
- const statusPage = new StatusPage({ ...statusPageData });
+ const statusPage = new StatusPage({
+ ...statusPageData,
+ userId,
+ teamId,
+ });
if (image) {
statusPage.logo = {
data: image.buffer,
@@ -38,7 +41,7 @@ const updateStatusPage = async (statusPageData, image) => {
data: image.buffer,
contentType: image.mimetype,
};
- }else{
+ } else {
statusPageData.logo = null;
}
@@ -61,105 +64,6 @@ const updateStatusPage = async (statusPageData, image) => {
}
};
-const getDistributedStatusPageByUrl = async ({ url, daysToShow = 30 }) => {
- const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
- try {
- const statusPage = await StatusPage.findOne({ url }).lean();
-
- if (!statusPage) {
- const error = new Error(stringService.statusPageNotFound);
- error.status = 404;
- throw error;
- }
-
- // No sub monitors, return status page
- if (statusPage.subMonitors.length === 0) {
- return statusPage;
- }
- // Sub monitors, return status page with sub monitors
- const daysAgo = new Date();
- daysAgo.setDate(daysAgo.getDate() - daysToShow);
-
- const subMonitors = await Monitor.aggregate([
- { $match: { _id: { $in: statusPage.subMonitors } } },
- {
- $addFields: {
- orderIndex: { $indexOfArray: [statusPage.subMonitors, "$_id"] },
- },
- },
-
- // Return 30 days of checks by default
- {
- $lookup: {
- from: "checks",
- let: { monitorId: "$_id" },
- pipeline: [
- {
- $match: {
- $expr: {
- $and: [
- { $eq: ["$monitorId", "$$monitorId"] },
- { $gte: ["$updatedAt", daysAgo] },
- ],
- },
- },
- },
- {
- $group: {
- _id: {
- $dateToString: { format: "%Y-%m-%d", date: "$updatedAt" },
- },
- responseTime: {
- $avg: "$responseTime",
- },
- trueCount: {
- $sum: {
- $cond: [{ $eq: ["$status", true] }, 1, 0],
- },
- },
- totalCount: {
- $sum: 1,
- },
- },
- },
- {
- $project: {
- _id: 1,
- responseTime: 1,
- upPercentage: {
- $cond: [
- { $eq: ["$totalCount", 0] },
- 0,
- { $multiply: [{ $divide: ["$trueCount", "$totalCount"] }, 100] },
- ],
- },
- },
- },
- {
- $sort: { _id: -1 },
- },
- ],
- as: "checks",
- },
- },
- { $sort: { orderIndex: 1 } },
- { $project: { orderIndex: 0 } },
- ]);
-
- const normalizedSubMonitors = subMonitors.map((monitor) => {
- return {
- ...monitor,
- checks: NormalizeData(monitor.checks, 10, 100),
- };
- });
- return { ...statusPage, subMonitors: normalizedSubMonitors };
- } catch (error) {
- error.service = SERVICE_NAME;
- error.method = "getDistributedStatusPageByUrl";
- throw error;
- }
-};
-
const getStatusPageByUrl = async (url, type) => {
// TODO This is deprecated, can remove and have controller call getStatusPage
try {
@@ -202,7 +106,10 @@ const getStatusPage = async (url) => {
},
},
{
- $unwind: "$monitors",
+ $unwind: {
+ path: "$monitors",
+ preserveNullAndEmptyArrays: true,
+ },
},
{
$lookup: {
@@ -227,6 +134,9 @@ const getStatusPage = async (url) => {
},
},
},
+ { $match: { "monitors.orderIndex": { $ne: -1 } } },
+ { $sort: { "monitors.orderIndex": 1 } },
+
{
$group: {
_id: "$_id",
@@ -246,14 +156,10 @@ const getStatusPage = async (url) => {
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
+ showAdminLoginLink: 1,
url: 1,
},
- monitors: {
- $sortArray: {
- input: "$monitors",
- sortBy: { orderIndex: 1 },
- },
- },
+ monitors: 1,
},
},
]);
@@ -306,7 +212,6 @@ export {
getStatusPagesByTeamId,
getStatusPage,
getStatusPageByUrl,
- getDistributedStatusPageByUrl,
deleteStatusPage,
deleteStatusPagesByMonitorId,
};
diff --git a/server/db/mongo/utils/seedDb.js b/server/db/mongo/utils/seedDb.js
index d8317c89b..64f23178c 100755
--- a/server/db/mongo/utils/seedDb.js
+++ b/server/db/mongo/utils/seedDb.js
@@ -1,6 +1,5 @@
import Monitor from "../../models/Monitor.js";
import Check from "../../models/Check.js";
-import DistributedUptimeCheck from "../../models/DistributedUptimeCheck.js";
import logger from "../../../utils/logger.js";
const generateRandomUrl = () => {
@@ -73,105 +72,4 @@ const seedDb = async (userId, teamId) => {
}
};
-const generateDistributedChecks = (monitorId, teamId, count = 2880) => {
- const checks = [];
- const endTime = new Date();
- const startTime = new Date(endTime - 48 * 60 * 60 * 1000);
-
- // Sample locations for variety
- const locations = [
- {
- city: "New York",
- countryCode: "US",
- continent: "NA",
- location: { lat: 40.7128, lng: -74.006 },
- },
- {
- city: "London",
- countryCode: "GB",
- continent: "EU",
- location: { lat: 51.5074, lng: -0.1278 },
- },
- {
- city: "Singapore",
- countryCode: "SG",
- continent: "AS",
- location: { lat: 1.3521, lng: 103.8198 },
- },
- ];
-
- for (let i = 0; i < count; i++) {
- const timestamp = new Date(startTime.getTime() + i * 60 * 1000);
- const location = locations[Math.floor(Math.random() * locations.length)];
- const status = Math.random() > 0.05; // 95% success rate
-
- checks.push({
- monitorId,
- teamId,
- status,
- responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms
- first_byte_took: Math.floor(Math.random() * 300000), // 0-300ms
- body_read_took: Math.floor(Math.random() * 100000), // 0-100ms
- dns_took: Math.floor(Math.random() * 100000), // 0-100ms
- conn_took: Math.floor(Math.random() * 200000), // 0-200ms
- connect_took: Math.floor(Math.random() * 150000), // 0-150ms
- tls_took: Math.floor(Math.random() * 200000), // 0-200ms
- location: location.location,
- continent: location.continent,
- countryCode: location.countryCode,
- city: location.city,
- uptBurnt: "0.01", // Will be converted to Decimal128 by the schema
- createdAt: timestamp,
- updatedAt: timestamp,
- });
- }
-
- return checks;
-};
-
-export const seedDistributedTest = async (userId, teamId) => {
- try {
- logger.info({
- message: "Deleting all test monitors and checks",
- service: "DB",
- method: "seedDistributedTest",
- });
-
- const testMonitors = await Monitor.find({
- type: "distributed_test",
- });
-
- testMonitors.forEach(async (monitor) => {
- await DistributedUptimeCheck.deleteMany({ monitorId: monitor._id });
- await Monitor.deleteOne({ _id: monitor._id });
- });
-
- logger.info({
- message: "Adding test monitors and checks",
- service: "DB",
- method: "seedDistributedTest",
- });
- const monitor = await Monitor.create({
- name: "Distributed Test",
- url: "https://distributed-test.com",
- type: "distributed_test",
- userId,
- teamId,
- interval: 60000,
- active: false,
- });
- const checks = generateDistributedChecks(monitor._id, teamId, 2800);
- await DistributedUptimeCheck.insertMany(checks);
- return monitor;
- } catch (error) {
- logger.error({
- message: "Error seeding distributed test",
- service: "DB",
- method: "seedDistributedTest",
- stack: error.stack,
- });
- throw error;
- }
-};
-
export default seedDb;
diff --git a/server/index.js b/server/index.js
index eea5b015a..4b74ef46c 100755
--- a/server/index.js
+++ b/server/index.js
@@ -36,8 +36,8 @@ import StatusPageController from "./controllers/statusPageController.js";
import QueueRoutes from "./routes/queueRoute.js";
import QueueController from "./controllers/queueController.js";
-import DistributedUptimeRoutes from "./routes/distributedUptimeRoute.js";
-import DistributedUptimeController from "./controllers/distributedUptimeController.js";
+import LogRoutes from "./routes/logRoutes.js";
+import LogController from "./controllers/logController.js";
import NotificationRoutes from "./routes/notificationRoute.js";
import NotificationController from "./controllers/notificationController.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";
@@ -73,6 +76,7 @@ import StatusService from "./service/statusService.js";
// Notification Service and dependencies
import NotificationService from "./service/notificationService.js";
+import NotificationUtils from "./service/notificationUtils.js";
// Buffer Service and dependencies
import BufferService from "./service/bufferService.js";
@@ -173,35 +177,56 @@ const startApp = async () => {
);
const bufferService = new BufferService({ db, logger });
const statusService = new StatusService({ db, logger, buffer: bufferService });
- const notificationService = new NotificationService(
+ const notificationUtils = new NotificationUtils({
+ stringService,
+ emailService,
+ });
+
+ const notificationService = new NotificationService({
emailService,
db,
logger,
networkService,
- stringService
- );
+ stringService,
+ notificationUtils,
+ });
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({
+ const pulseQueue = await PulseQueue.create({
+ appSettings,
db,
- jobQueueHelper,
+ pulseQueueHelper,
logger,
- stringService,
});
// 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);
@@ -224,13 +249,14 @@ const startApp = async () => {
process.on("SIGTERM", shutdown);
//Create controllers
- const authController = new AuthController(
- ServiceRegistry.get(MongoDB.SERVICE_NAME),
- ServiceRegistry.get(SettingsService.SERVICE_NAME),
- ServiceRegistry.get(EmailService.SERVICE_NAME),
- ServiceRegistry.get(JobQueue.SERVICE_NAME),
- ServiceRegistry.get(StringService.SERVICE_NAME)
- );
+ const authController = new AuthController({
+ db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
+ settingsService: ServiceRegistry.get(SettingsService.SERVICE_NAME),
+ emailService: ServiceRegistry.get(EmailService.SERVICE_NAME),
+ jobQueue: ServiceRegistry.get(JobQueue.SERVICE_NAME),
+ stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
+ logger: logger,
+ });
const monitorController = new MonitorController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
@@ -271,22 +297,18 @@ const startApp = async () => {
ServiceRegistry.get(StringService.SERVICE_NAME)
);
+ const logController = new LogController(logger);
+
const statusPageController = new StatusPageController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(StringService.SERVICE_NAME)
);
- const notificationController = new NotificationController(
- ServiceRegistry.get(NotificationService.SERVICE_NAME),
- ServiceRegistry.get(StringService.SERVICE_NAME),
- ServiceRegistry.get(StatusService.SERVICE_NAME)
- );
-
- const distributedUptimeController = new DistributedUptimeController({
- db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
- http,
+ const notificationController = new NotificationController({
+ notificationService: ServiceRegistry.get(NotificationService.SERVICE_NAME),
+ stringService: ServiceRegistry.get(StringService.SERVICE_NAME),
statusService: ServiceRegistry.get(StatusService.SERVICE_NAME),
- logger,
+ db: ServiceRegistry.get(MongoDB.SERVICE_NAME),
});
const diagnosticController = new DiagnosticController(
@@ -303,10 +325,9 @@ const startApp = async () => {
maintenanceWindowController
);
const queueRoutes = new QueueRoutes(queueController);
+ const logRoutes = new LogRoutes(logController);
const statusPageRoutes = new StatusPageRoutes(statusPageController);
- const distributedUptimeRoutes = new DistributedUptimeRoutes(
- distributedUptimeController
- );
+
const notificationRoutes = new NotificationRoutes(notificationController);
const diagnosticRoutes = new DiagnosticRoutes(diagnosticController);
// Middleware
@@ -357,7 +378,7 @@ const startApp = async () => {
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
- app.use("/api/v1/distributed-uptime", distributedUptimeRoutes.getRouter());
+ app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
diff --git a/server/locales/en.json b/server/locales/en.json
index a693c6eef..74296d0a5 100755
--- a/server/locales/en.json
+++ b/server/locales/en.json
@@ -123,6 +123,7 @@
"monitorCertificate": "Got monitor certificate successfully",
"monitorDemoAdded": "Successfully added demo monitors",
"queueGetMetrics": "Got metrics successfully",
+ "queueGetJobs": "Got jobs successfully",
"queueAddJob": "Job added successfully",
"queueObliterate": "Queue obliterated",
"jobQueueDeleteJobSuccess": "Job removed successfully",
diff --git a/server/middleware/handleErrors.js b/server/middleware/handleErrors.js
index b64cda897..87895fcf2 100755
--- a/server/middleware/handleErrors.js
+++ b/server/middleware/handleErrors.js
@@ -5,7 +5,7 @@ import StringService from "../service/stringService.js";
const handleErrors = (error, req, res, next) => {
const status = error.status || 500;
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
- const message = error.message || stringService.friendlyError;
+ const message = error.message || stringService.authIncorrectPassword;
const service = error.service || stringService.unknownService;
logger.error({
message: message,
diff --git a/server/middleware/responseHandler.js b/server/middleware/responseHandler.js
index a58e2e845..5169e7160 100755
--- a/server/middleware/responseHandler.js
+++ b/server/middleware/responseHandler.js
@@ -16,7 +16,12 @@ const responseHandler = (req, res, next) => {
* @param {*} [options.data=null] - Response data payload
* @returns {Object} Express response object
*/
- res.success = ({ status = 200, msg = "OK", data = null }) => {
+ res.success = ({ status = 200, msg = "OK", data = null, headers = {} }) => {
+ // Set custom headers if provided
+ Object.entries(headers).forEach(([key, value]) => {
+ res.set(key, value);
+ });
+
return res.status(status).json({
success: true,
msg: msg,
@@ -40,6 +45,21 @@ const responseHandler = (req, res, next) => {
data,
});
};
+
+ /**
+ * Sends a raw file response (for CSV, PDF, etc.)
+ * @param {Object} options
+ * @param {Buffer|string} options.data - The file content
+ * @param {Object} options.headers - Headers to set (e.g. Content-Type, Content-Disposition)
+ * @param {number} [options.status=200] - HTTP status code
+ */
+ res.file = ({ data, headers = {}, status = 200 }) => {
+ Object.entries(headers).forEach(([key, value]) => {
+ res.setHeader(key, value);
+ });
+ return res.status(status).send(data);
+ };
+
next();
};
diff --git a/server/middleware/verifyOwnership.js b/server/middleware/verifyOwnership.js
index ca2476f54..2040d54ee 100755
--- a/server/middleware/verifyOwnership.js
+++ b/server/middleware/verifyOwnership.js
@@ -1,14 +1,20 @@
import logger from "../utils/logger.js";
import ServiceRegistry from "../service/serviceRegistry.js";
import StringService from "../service/stringService.js";
+import { ObjectId } from "mongodb";
+
const SERVICE_NAME = "verifyOwnership";
const verifyOwnership = (Model, paramName) => {
const stringService = ServiceRegistry.get(StringService.SERVICE_NAME);
return async (req, res, next) => {
const userId = req.user._id;
- const documentId = req.params[paramName];
+ let documentId = req.params[paramName];
+
try {
+ if (typeof documentId === "string") {
+ documentId = ObjectId.createFromHexString(documentId);
+ }
const doc = await Model.findById(documentId);
//If the document is not found, return a 404 error
if (!doc) {
@@ -35,7 +41,7 @@ const verifyOwnership = (Model, paramName) => {
// If the userID does not match the document's userID, return a 403 error
if (userId.toString() !== doc.userId.toString()) {
- const error = new Error(stringService.verifyOwnerUnauthorized);
+ const error = new Error("Unauthorized");
error.status = 403;
throw error;
}
diff --git a/server/middleware/verifyTeamAccess.js b/server/middleware/verifyTeamAccess.js
new file mode 100644
index 000000000..60647bdc2
--- /dev/null
+++ b/server/middleware/verifyTeamAccess.js
@@ -0,0 +1,38 @@
+const SERVICE_NAME = "verifyTeamAccess";
+
+const verifyTeamAccess = (Model, paramName) => {
+ return async (req, res, next) => {
+ try {
+ const documentId = req.params[paramName];
+ const doc = await Model.findById(documentId);
+
+ if (!doc) {
+ const error = new Error("Document not found");
+ error.status = 404;
+ throw error;
+ }
+
+ if (!req?.user?.teamId || !doc.teamId) {
+ const error = new Error("Missing team information");
+ error.status = 400;
+ throw error;
+ }
+
+ if (req.user.teamId.toString() === doc.teamId.toString()) {
+ next();
+ return;
+ }
+
+ const error = new Error("Unauthorized");
+ error.status = 403;
+ throw error;
+ } catch (error) {
+ error.service = SERVICE_NAME;
+ error.method = "verifyTeamAccess";
+ next(error);
+ return;
+ }
+ };
+};
+
+export { verifyTeamAccess };
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/checkRoute.js b/server/routes/checkRoute.js
index bab45e4d4..aedca991a 100755
--- a/server/routes/checkRoute.js
+++ b/server/routes/checkRoute.js
@@ -11,7 +11,15 @@ class CheckRoutes {
}
initRoutes() {
+ this.router.get("/team", this.checkController.getChecksByTeam);
+ this.router.delete(
+ "/team",
+ isAllowed(["admin", "superadmin"]),
+ this.checkController.deleteChecksByTeamId
+ );
+
this.router.get("/:monitorId", this.checkController.getChecksByMonitor);
+
this.router.post(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
@@ -23,14 +31,6 @@ class CheckRoutes {
this.checkController.deleteChecks
);
- this.router.get("/team/:teamId", this.checkController.getChecksByTeam);
-
- this.router.delete(
- "/team/:teamId",
- isAllowed(["admin", "superadmin"]),
- this.checkController.deleteChecksByTeamId
- );
-
this.router.put(
"/team/ttl",
isAllowed(["admin", "superadmin"]),
diff --git a/server/routes/diagnosticRoute.js b/server/routes/diagnosticRoute.js
index c5ee7cdeb..616d01ccf 100755
--- a/server/routes/diagnosticRoute.js
+++ b/server/routes/diagnosticRoute.js
@@ -7,11 +7,6 @@ class DiagnosticRoutes {
this.initRoutes();
}
initRoutes() {
- this.router.get(
- "/db/execution-stats/:monitorId",
- this.diagnosticController.getDistributedUptimeDbExecutionStats
- );
-
this.router.get(
"/db/get-monitors-by-team-id/:teamId",
this.diagnosticController.getMonitorsByTeamIdExecutionStats
diff --git a/server/routes/distributedUptimeRoute.js b/server/routes/distributedUptimeRoute.js
deleted file mode 100755
index 51e8086cb..000000000
--- a/server/routes/distributedUptimeRoute.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { Router } from "express";
-import { verifyJWT } from "../middleware/verifyJWT.js";
-class DistributedUptimeRoutes {
- constructor(distributedUptimeController) {
- this.router = Router();
- this.distributedUptimeController = distributedUptimeController;
- this.initRoutes();
- }
- initRoutes() {
- this.router.post("/callback", this.distributedUptimeController.resultsCallback);
-
- this.router.get(
- "/monitors/:teamId/initial",
- verifyJWT,
- this.distributedUptimeController.getDistributedUptimeMonitors
- );
-
- this.router.get(
- "/monitors/:teamId",
- this.distributedUptimeController.subscribeToDistributedUptimeMonitors
- );
-
- this.router.get(
- "/monitors/details/:monitorId/initial",
- verifyJWT,
- this.distributedUptimeController.getDistributedUptimeMonitorDetails
- );
- this.router.get(
- "/monitors/details/public/:monitorId/initial",
- this.distributedUptimeController.getDistributedUptimeMonitorDetails
- );
-
- this.router.get(
- "/monitors/details/:monitorId",
- this.distributedUptimeController.subscribeToDistributedUptimeMonitorDetails
- );
- }
-
- getRouter() {
- return this.router;
- }
-}
-
-export default DistributedUptimeRoutes;
diff --git a/server/routes/logRoutes.js b/server/routes/logRoutes.js
new file mode 100755
index 000000000..867826dde
--- /dev/null
+++ b/server/routes/logRoutes.js
@@ -0,0 +1,18 @@
+import { Router } from "express";
+import { isAllowed } from "../middleware/isAllowed.js";
+class LogRoutes {
+ constructor(logController) {
+ this.router = Router();
+ this.logController = logController;
+ this.initRoutes();
+ }
+ initRoutes() {
+ this.router.get("/", isAllowed(["admin", "superadmin"]), this.logController.getLogs);
+ }
+
+ getRouter() {
+ return this.router;
+ }
+}
+
+export default LogRoutes;
diff --git a/server/routes/maintenanceWindowRoute.js b/server/routes/maintenanceWindowRoute.js
index 2665c2bb8..36f982e25 100755
--- a/server/routes/maintenanceWindowRoute.js
+++ b/server/routes/maintenanceWindowRoute.js
@@ -1,7 +1,8 @@
import { Router } from "express";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
+import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
import Monitor from "../db/models/Monitor.js";
-
+import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
this.router = Router();
@@ -24,9 +25,17 @@ class MaintenanceWindowRoutes {
this.router.get("/:id", this.maintenanceWindowController.getMaintenanceWindowById);
- this.router.put("/:id", this.maintenanceWindowController.editMaintenanceWindow);
+ this.router.put(
+ "/:id",
+ verifyTeamAccess(MaintenanceWindow, "id"),
+ this.maintenanceWindowController.editMaintenanceWindow
+ );
- this.router.delete("/:id", this.maintenanceWindowController.deleteMaintenanceWindow);
+ this.router.delete(
+ "/:id",
+ verifyTeamAccess(MaintenanceWindow, "id"),
+ this.maintenanceWindowController.deleteMaintenanceWindow
+ );
}
getRouter() {
diff --git a/server/routes/monitorRoute.js b/server/routes/monitorRoute.js
index 6d1db90ad..168f8b88d 100755
--- a/server/routes/monitorRoute.js
+++ b/server/routes/monitorRoute.js
@@ -2,7 +2,9 @@ import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
-
+import Monitor from "../db/models/Monitor.js";
+import { verifyOwnership } from "../middleware/verifyOwnership.js";
+import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
@@ -17,6 +19,11 @@ class MonitorRoutes {
initRoutes() {
this.router.get("/", this.monitorController.getAllMonitors);
this.router.get("/uptime", this.monitorController.getAllMonitorsWithUptimeStats);
+ this.router.get(
+ "/export",
+ isAllowed(["admin", "superadmin"]),
+ this.monitorController.exportMonitorsToCSV
+ );
this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById);
this.router.get(
"/hardware/details/:monitorId",
@@ -35,17 +42,17 @@ class MonitorRoutes {
fetchMonitorCertificate
);
});
+ this.router.get("/team", this.monitorController.getMonitorsByTeamId);
+
this.router.get("/:monitorId", this.monitorController.getMonitorById);
- this.router.get("/team/:teamId", this.monitorController.getMonitorsByTeamId);
-
this.router.get(
- "/summary/team/:teamId",
+ "/summary/team",
this.monitorController.getMonitorsAndSummaryByTeamId
);
this.router.get(
- "/team/:teamId/with-checks",
+ "/team/with-checks",
this.monitorController.getMonitorsWithChecksByTeamId
);
@@ -57,6 +64,7 @@ class MonitorRoutes {
this.router.delete(
"/:monitorId",
+ verifyOwnership(Monitor, "monitorId"),
isAllowed(["admin", "superadmin"]),
this.monitorController.deleteMonitor
);
@@ -69,6 +77,7 @@ class MonitorRoutes {
this.router.put(
"/:monitorId",
+ verifyTeamAccess(Monitor, "monitorId"),
isAllowed(["admin", "superadmin"]),
this.monitorController.editMonitor
);
diff --git a/server/routes/notificationRoute.js b/server/routes/notificationRoute.js
index 7ea94843e..09becd16c 100755
--- a/server/routes/notificationRoute.js
+++ b/server/routes/notificationRoute.js
@@ -1,30 +1,42 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
-
+import { verifyOwnership } from "../middleware/verifyOwnership.js";
+import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
+import Notification from "../db/models/Notification.js";
class NotificationRoutes {
- constructor(notificationController) {
- this.router = Router();
- this.notificationController = notificationController;
- this.initializeRoutes();
- }
+ constructor(notificationController) {
+ this.router = Router();
+ this.notificationController = notificationController;
+ this.initializeRoutes();
+ }
- initializeRoutes() {
- this.router.use(verifyJWT);
-
- this.router.post(
- "/trigger",
- this.notificationController.triggerNotification
- );
-
- this.router.post(
- "/test-webhook",
- this.notificationController.testWebhook
- );
- }
+ initializeRoutes() {
+ this.router.use(verifyJWT);
- getRouter() {
- return this.router;
- }
+ this.router.post("/test", this.notificationController.testNotification);
+ this.router.post("/test/all", this.notificationController.testAllNotifications);
+
+ this.router.post("/", this.notificationController.createNotification);
+
+ this.router.get("/team", this.notificationController.getNotificationsByTeamId);
+
+ this.router.delete(
+ "/:id",
+ verifyOwnership(Notification, "id"),
+ this.notificationController.deleteNotification
+ );
+
+ this.router.get("/:id", this.notificationController.getNotificationById);
+ this.router.put(
+ "/:id",
+ verifyTeamAccess(Notification, "id"),
+ this.notificationController.editNotification
+ );
+ }
+
+ getRouter() {
+ return this.router;
+ }
}
-export default NotificationRoutes;
\ No newline at end of file
+export default NotificationRoutes;
diff --git a/server/routes/queueRoute.js b/server/routes/queueRoute.js
index 38ac32a26..c40d21528 100755
--- a/server/routes/queueRoute.js
+++ b/server/routes/queueRoute.js
@@ -19,18 +19,18 @@ class QueueRoutes {
this.queueController.getJobs
);
+ this.router.get(
+ "/all-metrics",
+ isAllowed(["admin", "superadmin"]),
+ this.queueController.getAllMetrics
+ );
+
this.router.post(
"/jobs",
isAllowed(["admin", "superadmin"]),
this.queueController.addJob
);
- this.router.post(
- "/obliterate",
- isAllowed(["admin", "superadmin"]),
- this.queueController.obliterateQueue
- );
-
this.router.post(
"/flush",
isAllowed(["admin", "superadmin"]),
diff --git a/server/routes/statusPageRoute.js b/server/routes/statusPageRoute.js
index bac960b08..98b3a3433 100755
--- a/server/routes/statusPageRoute.js
+++ b/server/routes/statusPageRoute.js
@@ -12,16 +12,8 @@ class StatusPageRoutes {
initRoutes() {
this.router.get("/", this.statusPageController.getStatusPage);
- this.router.get(
- "/team/:teamId",
- verifyJWT,
- this.statusPageController.getStatusPagesByTeamId
- );
+ this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId);
this.router.get("/:url", this.statusPageController.getStatusPageByUrl);
- this.router.get(
- "/distributed/:url",
- this.statusPageController.getDistributedStatusPageByUrl
- );
this.router.post(
"/",
diff --git a/server/service/JobQueue/JobQueue.js b/server/service/JobQueue/JobQueue.js
index 32d531809..38cca7d4d 100644
--- a/server/service/JobQueue/JobQueue.js
+++ b/server/service/JobQueue/JobQueue.js
@@ -1,4 +1,4 @@
-const QUEUE_NAMES = ["uptime", "pagespeed", "hardware", "distributed"];
+const QUEUE_NAMES = ["uptime", "pagespeed", "hardware"];
const SERVICE_NAME = "JobQueue";
const HEALTH_CHECK_INTERVAL = 10 * 60 * 1000; // 10 minutes
const QUEUE_LOOKUP = {
@@ -8,7 +8,6 @@ const QUEUE_LOOKUP = {
port: "uptime",
docker: "uptime",
pagespeed: "pagespeed",
- distributed_http: "distributed",
};
const getSchedulerId = (monitor) => `scheduler:${monitor.type}:${monitor._id}`;
@@ -51,7 +50,6 @@ class JobQueue {
}
})
);
-
this.healthCheckInterval = setInterval(async () => {
try {
const queueHealthChecks = await this.checkQueueHealth();
@@ -116,6 +114,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"}`,
@@ -181,6 +187,11 @@ class JobQueue {
}
}
+ async updateJob(monitor) {
+ await this.deleteJob(monitor);
+ await this.addJob(monitor._id, monitor);
+ }
+
async getJobs() {
try {
let stats = {};
diff --git a/server/service/JobQueue/JobQueueHelper.js b/server/service/JobQueue/JobQueueHelper.js
index de1e00fb8..d286b7b14 100644
--- a/server/service/JobQueue/JobQueueHelper.js
+++ b/server/service/JobQueue/JobQueueHelper.js
@@ -254,14 +254,8 @@ class JobQueueHelper {
// Get the current status
await job.updateProgress(30);
- const networkResponse = await this.networkService.getStatus(job);
- if (
- job.data.type === "distributed_http" ||
- job.data.type === "distributed_test"
- ) {
- await job.updateProgress(100);
- return true;
- }
+ const monitor = job.data;
+ const networkResponse = await this.networkService.getStatus(monitor);
// If the network response is not found, we're done
if (!networkResponse) {
@@ -271,14 +265,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..50a959559
--- /dev/null
+++ b/server/service/PulseQueue/PulseQueue.js
@@ -0,0 +1,212 @@
+import { Pulse } from "@pulsecron/pulse";
+
+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;
+ }
+
+ static async create({ appSettings, db, pulseQueueHelper, logger }) {
+ const instance = new PulseQueue({ appSettings, db, pulseQueueHelper, logger });
+ await instance.init();
+ return instance;
+ }
+
+ // ****************************************
+ // 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`);
+ if (monitor.isActive === false) {
+ job.disable();
+ }
+ await job.save();
+ };
+
+ deleteJob = async (monitor) => {
+ this.logger.debug({
+ message: `Deleting job ${monitor?.url ?? "No URL"}`,
+ service: SERVICE_NAME,
+ method: "deleteJob",
+ });
+ await this.pulse.cancel({
+ "data.monitor._id": monitor._id,
+ });
+ };
+
+ 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",
+ });
+ };
+
+ updateJob = async (monitor) => {
+ const jobs = await this.pulse.jobs({
+ "data.monitor._id": monitor._id,
+ });
+
+ const job = jobs[0];
+ if (!job) {
+ throw new Error("Job not found");
+ }
+
+ const intervalInSeconds = monitor.interval / 1000;
+ job.repeatEvery(`${intervalInSeconds} seconds`);
+ job.attrs.data.monitor = monitor;
+ await job.save();
+ };
+
+ 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.totalRuns += job.attrs.runCount || 0;
+ acc.totalFailures += job.attrs.failCount || 0;
+ 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,
+ monitorType: job.attrs.data.monitor.type,
+ failedAt: job.attrs.failedAt,
+ failCount: job.attrs.failCount,
+ failReason: job.attrs.failReason,
+ });
+ }
+ return acc;
+ },
+ {
+ jobs: 0,
+ activeJobs: 0,
+ failingJobs: 0,
+ jobsWithFailures: [],
+ totalRuns: 0,
+ totalFailures: 0,
+ }
+ );
+ 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,
+ monitorType: job.attrs.data.monitor.type,
+ active: !job.attrs.disabled,
+ lockedAt: job.attrs.lockedAt,
+ runCount: job.attrs.runCount || 0,
+ failCount: job.attrs.failCount || 0,
+ failReason: job.attrs.failReason,
+ lastRunAt: job.attrs.lastRunAt,
+ lastFinishedAt: job.attrs.lastFinishedAt,
+ lastRunTook: job.attrs.lockedAt
+ ? null
+ : job.attrs.lastFinishedAt - job.attrs.lastRunAt,
+ lastFailedAt: job.attrs.failedAt,
+ };
+ });
+ };
+
+ 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..959c12e3b
--- /dev/null
+++ b/server/service/PulseQueue/PulseQueueHelper.js
@@ -0,0 +1,102 @@
+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 (!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: "getMonitorJob",
+ 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/bufferService.js b/server/service/bufferService.js
index 255c3db9f..84f3efd30 100755
--- a/server/service/bufferService.js
+++ b/server/service/bufferService.js
@@ -1,5 +1,5 @@
const SERVICE_NAME = "BufferService";
-const BUFFER_TIMEOUT = 1000 * 60 * 1; // 1 minute
+const BUFFER_TIMEOUT = process.env.NODE_ENV === "development" ? 5000 : 1000 * 60 * 1; // 1 minute
const TYPE_MAP = {
http: "checks",
ping: "checks",
@@ -7,7 +7,6 @@ const TYPE_MAP = {
docker: "checks",
pagespeed: "pagespeedChecks",
hardware: "hardwareChecks",
- distributed_http: "distributedChecks",
};
class BufferService {
@@ -19,13 +18,11 @@ class BufferService {
checks: [],
pagespeedChecks: [],
hardwareChecks: [],
- distributedChecks: [],
};
this.OPERATION_MAP = {
checks: this.db.createChecks,
pagespeedChecks: this.db.createPageSpeedChecks,
hardwareChecks: this.db.createHardwareChecks,
- distributedChecks: this.db.createDistributedChecks,
};
this.scheduleNextFlush();
@@ -92,7 +89,7 @@ class BufferService {
}
this.buffers[bufferName] = [];
}
- this.logger.info({
+ this.logger.debug({
message: `Flushed ${items} items`,
service: this.SERVICE_NAME,
method: "flushBuffers",
diff --git a/server/service/emailService.js b/server/service/emailService.js
index 035cc6505..433fd0871 100755
--- a/server/service/emailService.js
+++ b/server/service/emailService.js
@@ -80,98 +80,90 @@ class EmailService {
*/
};
- /**
- * Asynchronously builds and sends an email using a specified template and context.
- *
- * @param {string} template - The name of the template to use for the email body.
- * @param {Object} context - The data context to render the template with.
- * @param {string} to - The recipient's email address.
- * @param {string} subject - The subject of the email.
- * @returns {Promise} A promise that resolves to the messageId of the sent email.
- */
- buildAndSendEmail = async (template, context, to, subject, transportConfig) => {
- // TODO - Consider an update transporter method so this only needs to be recreated when smtp settings change
+ buildEmail = async (template, context) => {
+ try {
+ const mjml = this.templateLookup[template](context);
+ const html = await this.mjml2html(mjml);
+ return html.html;
+ } catch (error) {
+ this.logger.error({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "buildEmail",
+ stack: error.stack,
+ });
+ }
+ };
+
+ sendEmail = async (to, subject, html, transportConfig) => {
let config;
if (typeof transportConfig !== "undefined") {
config = transportConfig;
} else {
config = await this.settingsService.getDBSettings();
}
-
const {
systemEmailHost,
systemEmailPort,
+ systemEmailSecure,
+ systemEmailPool,
systemEmailUser,
systemEmailAddress,
systemEmailPassword,
systemEmailConnectionHost,
+ systemEmailTLSServername,
+ systemEmailIgnoreTLS,
+ systemEmailRequireTLS,
+ systemEmailRejectUnauthorized,
} = config;
- const baseEmailConfig = {
+ const emailConfig = {
host: systemEmailHost,
- port: systemEmailPort,
- secure: true,
+ port: Number(systemEmailPort),
+ secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: systemEmailPassword,
},
+ name: systemEmailConnectionHost || "localhost",
connectionTimeout: 5000,
+ pool: systemEmailPool,
+ tls: {
+ rejectUnauthorized: systemEmailRejectUnauthorized,
+ ignoreTLS: systemEmailIgnoreTLS,
+ requireTLS: systemEmailRequireTLS,
+ servername: systemEmailTLSServername,
+ },
};
-
- const isSmtps = Number(systemEmailPort) === 465;
-
- const emailConfig = !isSmtps
- ? {
- ...baseEmailConfig,
- name: systemEmailConnectionHost || "localhost",
- secure: false,
- pool: true,
- tls: { rejectUnauthorized: false },
- }
- : baseEmailConfig;
-
- if (!isSmtps) {
- delete emailConfig.auth;
- }
-
this.transporter = this.nodemailer.createTransport(emailConfig);
- const buildHtml = async (template, context) => {
- try {
- const mjml = this.templateLookup[template](context);
- const html = await this.mjml2html(mjml);
- return html.html;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: SERVICE_NAME,
- method: "buildAndSendEmail",
- stack: error.stack,
- });
- }
- };
+ try {
+ await this.transporter.verify();
+ } catch (error) {
+ this.logger.warn({
+ message: "Email transporter verification failed",
+ service: SERVICE_NAME,
+ method: "verifyTransporter",
+ });
+ return false;
+ }
- const sendEmail = async (to, subject, html) => {
- try {
- const info = await this.transporter.sendMail({
- to: to,
- from: systemEmailAddress,
- subject: subject,
- html: html,
- });
- return info;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: SERVICE_NAME,
- method: "sendEmail",
- stack: error.stack,
- });
- }
- };
- const html = await buildHtml(template, context);
- const info = await sendEmail(to, subject, html);
- return info?.messageId;
+ try {
+ const info = await this.transporter.sendMail({
+ to: to,
+ from: systemEmailAddress,
+ subject: subject,
+ html: html,
+ });
+ return info?.messageId;
+ } catch (error) {
+ this.logger.error({
+ message: error.message,
+ service: SERVICE_NAME,
+ method: "sendEmail",
+ stack: error.stack,
+ });
+ }
};
}
diff --git a/server/service/networkService.js b/server/service/networkService.js
index a023ffadd..08b2f065e 100755
--- a/server/service/networkService.js
+++ b/server/service/networkService.js
@@ -23,8 +23,6 @@ class NetworkService {
this.TYPE_HARDWARE = "hardware";
this.TYPE_DOCKER = "docker";
this.TYPE_PORT = "port";
- this.TYPE_DISTRIBUTED_HTTP = "distributed_http";
- this.TYPE_DISTRIBUTED_TEST = "distributed_test";
this.SERVICE_NAME = SERVICE_NAME;
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
@@ -62,30 +60,30 @@ class NetworkService {
}
/**
- * Sends a ping request to the specified URL and returns the response.
- *
- * @param {Object} job - The job object containing the data for the ping request.
- * @param {Object} job.data - The data object within the job.
- * @param {string} job.data.url - The URL to ping.
- * @param {string} job.data._id - The monitor ID for the ping request.
- * @returns {Promise} An object containing the ping response details.
- * @property {string} monitorId - The monitor ID for the ping request.
- * @property {string} type - The type of request, which is "ping".
- * @property {number} responseTime - The time taken for the ping request to complete, in milliseconds.
- * @property {Object} payload - The response payload from the ping request.
- * @property {boolean} status - The status of the ping request (true if successful, false otherwise).
- * @property {number} code - The response code (200 if successful, error code otherwise).
- * @property {string} message - The message indicating the result of the ping request.
+ * Performs a ping check to a specified host to verify its availability.
+ * @async
+ * @param {Object} monitor - The monitor configuration object
+ * @param {string} monitor.url - The host URL to ping
+ * @param {string} monitor._id - The unique identifier of the monitor
+ * @returns {Promise} A promise that resolves to a ping response object
+ * @returns {string} pingResponse.monitorId - The ID of the monitor
+ * @returns {string} pingResponse.type - The type of monitor (always "ping")
+ * @returns {number} pingResponse.responseTime - The time taken for the ping
+ * @returns {Object} pingResponse.payload - The raw ping response data
+ * @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false)
+ * @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure)
+ * @returns {string} pingResponse.message - Success or failure message
+ * @throws {Error} If there's an error during the ping operation
*/
- async requestPing(job) {
+ async requestPing(monitor) {
try {
- const url = job.data.url;
+ const url = monitor.url;
const { response, responseTime, error } = await this.timeRequest(() =>
this.ping.promise.probe(url)
);
const pingResponse = {
- monitorId: job.data._id,
+ monitorId: monitor._id,
type: "ping",
responseTime,
payload: response,
@@ -109,23 +107,31 @@ class NetworkService {
}
/**
- * Sends an HTTP GET request to the specified URL and returns the response.
- *
- * @param {Object} job - The job object containing the data for the HTTP request.
- * @param {Object} job.data - The data object within the job.
- * @param {string} job.data.url - The URL to send the HTTP GET request to.
- * @param {string} job.data._id - The monitor ID for the HTTP request.
- * @param {string} [job.data.secret] - Secret for authorization if provided.
- * @returns {Promise} An object containing the HTTP response details.
- * @property {string} monitorId - The monitor ID for the HTTP request.
- * @property {string} type - The type of request, which is "http".
- * @property {number} responseTime - The time taken for the HTTP request to complete, in milliseconds.
- * @property {Object} payload - The response payload from the HTTP request.
- * @property {boolean} status - The status of the HTTP request (true if successful, false otherwise).
- * @property {number} code - The response code (200 if successful, error code otherwise).
- * @property {string} message - The message indicating the result of the HTTP request.
+ * Performs an HTTP GET request to a specified URL with optional validation of response data.
+ * @async
+ * @param {Object} monitor - The monitor configuration object
+ * @param {string} monitor.url - The URL to make the HTTP request to
+ * @param {string} [monitor.secret] - Optional Bearer token for authentication
+ * @param {string} monitor._id - The unique identifier of the monitor
+ * @param {string} monitor.name - The name of the monitor
+ * @param {string} monitor.teamId - The team ID associated with the monitor
+ * @param {string} monitor.type - The type of monitor
+ * @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors
+ * @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response
+ * @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match)
+ * @param {string} [monitor.expectedValue] - Expected value to match against response data
+ * @returns {Promise} A promise that resolves to an HTTP response object
+ * @returns {string} httpResponse.monitorId - The ID of the monitor
+ * @returns {string} httpResponse.teamId - The team ID
+ * @returns {string} httpResponse.type - The type of monitor
+ * @returns {number} httpResponse.responseTime - The time taken for the request
+ * @returns {Object} httpResponse.payload - The response data
+ * @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified)
+ * @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR
+ * @returns {string} httpResponse.message - Success or failure message
+ * @throws {Error} If there's an error during the HTTP request or data validation
*/
- async requestHttp(job) {
+ async requestHttp(monitor) {
try {
const {
url,
@@ -138,7 +144,7 @@ class NetworkService {
jsonPath,
matchMethod,
expectedValue,
- } = job.data;
+ } = monitor;
const config = {};
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
@@ -233,25 +239,24 @@ class NetworkService {
}
/**
- * Sends a request to the Google PageSpeed Insights API for the specified URL and returns the response.
- *
- * @param {Object} job - The job object containing the data for the PageSpeed request.
- * @param {Object} job.data - The data object within the job.
- * @param {string} job.data.url - The URL to analyze with PageSpeed Insights.
- * @param {string} job.data._id - The monitor ID for the PageSpeed request.
- * @returns {Promise} An object containing the PageSpeed response details.
- * @property {string} monitorId - The monitor ID for the PageSpeed request.
- * @property {string} type - The type of request, which is "pagespeed".
- * @property {number} responseTime - The time taken for the PageSpeed request to complete, in milliseconds.
- * @property {Object} payload - The response payload from the PageSpeed request.
- * @property {boolean} status - The status of the PageSpeed request (true if successful, false otherwise).
- * @property {number} code - The response code (200 if successful, error code otherwise).
- * @property {string} message - The message indicating the result of the PageSpeed request.
+ * Checks the performance of a webpage using Google's PageSpeed Insights API.
+ * @async
+ * @param {Object} monitor - The monitor configuration object
+ * @param {string} monitor.url - The URL of the webpage to analyze
+ * @returns {Promise} A promise that resolves to a pagespeed response object or undefined if API key is missing
+ * @returns {string} response.monitorId - The ID of the monitor
+ * @returns {string} response.type - The type of monitor
+ * @returns {number} response.responseTime - The time taken for the analysis
+ * @returns {boolean} response.status - Whether the analysis was successful
+ * @returns {number} response.code - HTTP status code from the PageSpeed API
+ * @returns {string} response.message - Success or failure message
+ * @returns {Object} response.payload - The PageSpeed analysis results
+ * @throws {Error} If there's an error during the PageSpeed analysis
*/
- 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 +271,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";
@@ -275,26 +280,9 @@ class NetworkService {
}
}
- /**
- * Sends an HTTP request to check hardware status and returns the response.
- *
- * @param {Object} job - The job object containing the data for the hardware request.
- * @param {Object} job.data - The data object within the job.
- * @param {string} job.data.url - The URL to send the hardware status request to.
- * @param {string} job.data._id - The monitor ID for the hardware request.
- * @param {string} job.data.type - The type of request, which is "hardware".
- * @returns {Promise} An object containing the hardware status response details.
- * @property {string} monitorId - The monitor ID for the hardware request.
- * @property {string} type - The type of request ("hardware").
- * @property {number} responseTime - The time taken for the request to complete, in milliseconds.
- * @property {Object} payload - The response payload from the hardware status request.
- * @property {boolean} status - The status of the request (true if successful, false otherwise).
- * @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";
@@ -303,22 +291,22 @@ class NetworkService {
}
/**
- * Sends a request to inspect a Docker container and returns its status.
- *
- * @param {Object} job - The job object containing the data for the Docker request.
- * @param {Object} job.data - The data object within the job.
- * @param {string} job.data.url - The container ID or name to inspect.
- * @param {string} job.data._id - The monitor ID for the Docker request.
- * @param {string} job.data.type - The type of request, which is "docker".
- * @returns {Promise} An object containing the Docker container status details.
- * @property {string} monitorId - The monitor ID for the Docker request.
- * @property {string} type - The type of request ("docker").
- * @property {number} responseTime - The time taken for the Docker inspection to complete, in milliseconds.
- * @property {boolean} status - The status of the container (true if running, false otherwise).
- * @property {number} code - The response code (200 if successful, error code otherwise).
- * @property {string} message - The message indicating the result of the Docker inspection.
+ * Checks the status of a Docker container by its ID.
+ * @async
+ * @param {Object} monitor - The monitor configuration object
+ * @param {string} monitor.url - The Docker container ID to check
+ * @param {string} monitor._id - The unique identifier of the monitor
+ * @param {string} monitor.type - The type of monitor
+ * @returns {Promise} A promise that resolves to a docker response object
+ * @returns {string} dockerResponse.monitorId - The ID of the monitor
+ * @returns {string} dockerResponse.type - The type of monitor
+ * @returns {number} dockerResponse.responseTime - The time taken for the container inspection
+ * @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false)
+ * @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
+ * @returns {string} dockerResponse.message - Success or failure message
+ * @throws {Error} If the container is not found or if there's an error inspecting the container
*/
- async requestDocker(job) {
+ async requestDocker(monitor) {
try {
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
@@ -326,19 +314,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 +348,26 @@ class NetworkService {
}
}
- async requestPort(job) {
+ /**
+ * Attempts to establish a TCP connection to a specified host and port.
+ * @async
+ * @param {Object} monitor - The monitor configuration object
+ * @param {string} monitor.url - The host URL to connect to
+ * @param {number} monitor.port - The port number to connect to
+ * @param {string} monitor._id - The unique identifier of the monitor
+ * @param {string} monitor.type - The type of monitor
+ * @returns {Promise} A promise that resolves to a port response object
+ * @returns {string} portResponse.monitorId - The ID of the monitor
+ * @returns {string} portResponse.type - The type of monitor
+ * @returns {number} portResponse.responseTime - The time taken for the connection attempt
+ * @returns {boolean} portResponse.status - Whether the connection was successful
+ * @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure)
+ * @returns {string} portResponse.message - Success or failure message
+ * @throws {Error} If the connection times out or encounters an error
+ */
+ 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 +396,8 @@ class NetworkService {
});
const portResponse = {
- monitorId: job.data._id,
- type: job.data.type,
+ monitorId: monitor._id,
+ type: monitor.type,
responseTime,
};
@@ -414,35 +419,6 @@ class NetworkService {
}
}
- async requestDistributedHttp(job) {
- try {
- const monitor = job.data;
- const CALLBACK_URL = process.env.CALLBACK_URL;
-
- const response = await this.axios.post(
- UPROCK_ENDPOINT,
- {
- id: monitor._id,
- url: monitor.url,
- callback: `${CALLBACK_URL}/api/v1/distributed-uptime/callback`,
- },
- {
- headers: {
- "Content-Type": "application/json",
- "x-checkmate-key": process.env.UPROCK_API_KEY,
- },
- }
- );
- if (response.data.success === false) {
- throw new Error(response.data.message);
- }
- } catch (error) {
- error.service = this.SERVICE_NAME;
- error.method = "requestDistributedHttp";
- throw error;
- }
- }
-
/**
* Handles unsupported job types by throwing an error with details.
*
@@ -456,9 +432,9 @@ class NetworkService {
throw err;
}
- async requestWebhook(platform, url, message) {
+ async requestWebhook(type, url, body) {
try {
- const response = await this.axios.post(url, message, {
+ const response = await this.axios.post(url, body, {
headers: {
"Content-Type": "application/json",
},
@@ -468,7 +444,7 @@ class NetworkService {
type: "webhook",
status: true,
code: response.status,
- message: `Successfully sent ${platform} notification`,
+ message: `Successfully sent ${type} notification`,
payload: response.data,
};
} catch (error) {
@@ -476,24 +452,41 @@ class NetworkService {
message: error.message,
service: this.SERVICE_NAME,
method: "requestWebhook",
- url,
- platform,
- error: error.message,
- statusCode: error.response?.status,
- responseData: error.response?.data,
- requestPayload: message,
});
return {
type: "webhook",
status: false,
code: error.response?.status || this.NETWORK_ERROR,
- message: `Failed to send ${platform} notification`,
+ message: `Failed to send ${type} notification`,
payload: error.response?.data,
};
}
}
+ 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.
*
@@ -503,25 +496,21 @@ class NetworkService {
* @returns {Promise} The response object from the appropriate request method.
* @throws {Error} Throws an error if the job type is unsupported.
*/
- async getStatus(job) {
- const type = job?.data?.type ?? "unknown";
+ async getStatus(monitor) {
+ const type = monitor.type ?? "unknown";
switch (type) {
case this.TYPE_PING:
- return await this.requestPing(job);
+ return await this.requestPing(monitor);
case this.TYPE_HTTP:
- return await this.requestHttp(job);
+ return await this.requestHttp(monitor);
case this.TYPE_PAGESPEED:
- return await this.requestPagespeed(job);
+ return await this.requestPagespeed(monitor);
case this.TYPE_HARDWARE:
- return await this.requestHardware(job);
+ return await this.requestHardware(monitor);
case this.TYPE_DOCKER:
- return await this.requestDocker(job);
+ return await this.requestDocker(monitor);
case this.TYPE_PORT:
- return await this.requestPort(job);
- case this.TYPE_DISTRIBUTED_HTTP:
- return await this.requestDistributedHttp(job);
- case this.TYPE_DISTRIBUTED_TEST:
- return;
+ return await this.requestPort(monitor);
default:
return this.handleUnsupportedType(type);
}
diff --git a/server/service/notificationService.js b/server/service/notificationService.js
old mode 100755
new mode 100644
index ed74ff539..a2453faa9
--- a/server/service/notificationService.js
+++ b/server/service/notificationService.js
@@ -1,347 +1,122 @@
-const SERVICE_NAME = "NotificationService";
-const TELEGRAM_API_BASE_URL = "https://api.telegram.org/bot";
-const PLATFORM_TYPES = ["telegram", "slack", "discord", "webhook"];
-
-const MESSAGE_FORMATTERS = {
- telegram: (messageText, chatId) => ({ chat_id: chatId, text: messageText }),
- slack: (messageText) => ({ text: messageText }),
- discord: (messageText) => ({ content: messageText }),
- webhook: (messageText) => ({ text: messageText }),
-};
-
class NotificationService {
- static SERVICE_NAME = SERVICE_NAME;
- /**
- * Creates an instance of NotificationService.
- *
- * @param {Object} emailService - The email service used for sending notifications.
- * @param {Object} db - The database instance for storing notification data.
- * @param {Object} logger - The logger instance for logging activities.
- * @param {Object} networkService - The network service for sending webhook notifications.
- */
- constructor(emailService, db, logger, networkService, stringService) {
- this.SERVICE_NAME = SERVICE_NAME;
+ constructor({
+ emailService,
+ db,
+ logger,
+ networkService,
+ stringService,
+ notificationUtils,
+ }) {
this.emailService = emailService;
this.db = db;
this.logger = logger;
this.networkService = networkService;
this.stringService = stringService;
+ this.notificationUtils = notificationUtils;
}
- /**
- * Formats a notification message based on the monitor status and platform.
- *
- * @param {Object} monitor - The monitor object.
- * @param {string} monitor.name - The name of the monitor.
- * @param {string} monitor.url - The URL of the monitor.
- * @param {boolean} status - The current status of the monitor (true for up, false for down).
- * @param {string} platform - The notification platform (e.g., "telegram", "slack", "discord").
- * @param {string} [chatId] - The chat ID for platforms that require it (e.g., Telegram).
- * @returns {Object|null} The formatted message object for the specified platform, or null if the platform is unsupported.
- */
+ sendNotification = async ({ notification, subject, content, html }) => {
+ const { type, address } = notification;
- formatNotificationMessage(monitor, status, platform, chatId, code, timestamp) {
- // Format timestamp using the local system timezone
- const formatTime = (timestamp) => {
- const date = new Date(timestamp);
-
- // Get timezone abbreviation and format the date
- const timeZoneAbbr = date
- .toLocaleTimeString("en-US", { timeZoneName: "short" })
- .split(" ")
- .pop();
-
- // Format the date with readable format
- return (
- date
- .toLocaleString("en-US", {
- year: "numeric",
- month: "2-digit",
- day: "2-digit",
- hour: "2-digit",
- minute: "2-digit",
- second: "2-digit",
- hour12: false,
- })
- .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
- " " +
- timeZoneAbbr
- );
- };
-
- // Get formatted time
- const formattedTime = timestamp
- ? formatTime(timestamp)
- : formatTime(new Date().getTime());
-
- // Create different messages based on status with extra spacing
- let messageText;
- if (status === true) {
- messageText = this.stringService.monitorUpAlert
- .replace("{monitorName}", monitor.name)
- .replace("{time}", formattedTime)
- .replace("{code}", code || "Unknown");
- } else {
- messageText = this.stringService.monitorDownAlert
- .replace("{monitorName}", monitor.name)
- .replace("{time}", formattedTime)
- .replace("{code}", code || "Unknown");
+ if (type === "email") {
+ const messageId = await this.emailService.sendEmail(address, subject, html);
+ if (!messageId) return false;
+ return true;
}
- if (!PLATFORM_TYPES.includes(platform)) {
- return undefined;
+ // Create a body for webhooks
+ let body = { text: content };
+ if (type === "discord") {
+ body = { content };
}
- return MESSAGE_FORMATTERS[platform](messageText, chatId);
- }
-
- /**
- * Sends a webhook notification to a specified platform.
- *
- * @param {Object} networkResponse - The response object from the network.
- * @param {Object} networkResponse.monitor - The monitor object.
- * @param {boolean} networkResponse.status - The monitor's status (true for up, false for down).
- * @param {Object} notification - The notification settings.
- * @param {string} notification.platform - The target platform ("telegram", "slack", "discord").
- * @param {Object} notification.config - The configuration object for the webhook.
- * @param {string} notification.config.webhookUrl - The webhook URL for the platform.
- * @param {string} [notification.config.botToken] - The bot token for Telegram notifications.
- * @param {string} [notification.config.chatId] - The chat ID for Telegram notifications.
- * @returns {Promise} A promise that resolves to true if the notification was sent successfully, otherwise false.
- */
-
- async sendWebhookNotification(networkResponse, notification) {
- const { monitor, status, code } = networkResponse;
- const { platform } = notification;
- const { webhookUrl, botToken, chatId } = notification.config;
-
- // Early return if platform is not supported
- if (!PLATFORM_TYPES.includes(platform)) {
- this.logger.warn({
- message: this.stringService.getWebhookUnsupportedPlatform(platform),
- service: this.SERVICE_NAME,
- method: "sendWebhookNotification",
- details: { platform },
- });
- return false;
- }
-
- // Early return for telegram if required fields are missing
- if (platform === "telegram" && (!botToken || !chatId)) {
- this.logger.warn({
- message: "Missing required fields for Telegram notification",
- service: this.SERVICE_NAME,
- method: "sendWebhookNotification",
- details: { platform },
- });
- return false;
- }
-
- let url = webhookUrl;
- if (platform === "telegram") {
- url = `${TELEGRAM_API_BASE_URL}${botToken}/sendMessage`;
- }
-
- const message = this.formatNotificationMessage(
- monitor,
- status,
- platform,
- chatId,
- code, // Pass the code field directly
- networkResponse.timestamp
- );
-
- try {
- const response = await this.networkService.requestWebhook(platform, url, message);
+ if (type === "slack" || type === "discord" || type === "webhook") {
+ const response = await this.networkService.requestWebhook(type, address, body);
return response.status;
- } catch (error) {
- this.logger.error({
- message: this.stringService.getWebhookSendError(platform),
- service: this.SERVICE_NAME,
- method: "sendWebhookNotification",
- stack: error.stack,
+ }
+ if (type === "pager_duty") {
+ const response = await this.networkService.requestPagerDuty({
+ message: content,
+ monitorUrl: subject,
+ routingKey: address,
});
- return false;
+
+ return response;
}
- }
+ };
- /**
- * Sends an email notification for hardware infrastructure alerts
- *
- * @async
- * @function sendHardwareEmail
- * @param {Object} networkResponse - Response object containing monitor information
- * @param {string} address - Email address to send the notification to
- * @param {Array} [alerts=[]] - List of hardware alerts to include in the email
- * @returns {Promise} - Indicates whether email was sent successfully
- * @throws {Error}
- */
- async sendHardwareEmail(networkResponse, address, alerts = []) {
- if (alerts.length === 0) return false;
- const { monitor, status, prevStatus } = networkResponse;
- const template = "hardwareIncidentTemplate";
- const context = { monitor: monitor.name, url: monitor.url, alerts };
- const subject = `Monitor ${monitor.name} infrastructure alerts`;
- this.emailService.buildAndSendEmail(template, context, address, subject);
- return true;
- }
-
- /**
- * Sends an email notification about monitor status change
- *
- * @async
- * @function sendEmail
- * @param {Object} networkResponse - Response object containing monitor status information
- * @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";
- const context = { monitor: monitor.name, url: monitor.url };
- const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
- this.emailService.buildAndSendEmail(template, context, address, subject);
- return true;
- }
-
- async handleStatusNotifications(networkResponse) {
- try {
- // If status hasn't changed, we're done
- if (networkResponse.statusChanged === false) return false;
- // if prevStatus is undefined, monitor is resuming, we're done
- if (networkResponse.prevStatus === undefined) return false;
-
- const notifications = await this.db.getNotificationsByMonitorId(
- networkResponse.monitorId
- );
-
- for (const notification of notifications) {
- if (notification.type === "email") {
- await this.sendEmail(networkResponse, notification.address);
- } else if (notification.type === "webhook") {
- await this.sendWebhookNotification(networkResponse, notification);
- }
- // Handle other types of notifications here
- }
- return true;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "handleNotifications",
- stack: error.stack,
- });
- }
- }
- /**
- * Handles status change notifications for a monitor
- *
- * @async
- * @function handleStatusNotifications
- * @param {Object} networkResponse - Response object containing monitor status information
- * @returns {Promise} - Indicates whether notifications were processed
- * @throws {Error}
- */
- async handleHardwareNotifications(networkResponse) {
- const thresholds = networkResponse?.monitor?.thresholds;
- if (thresholds === undefined) return false; // No thresholds set, we're done
-
- // Get thresholds from monitor
- const {
- usage_cpu: cpuThreshold = -1,
- usage_memory: memoryThreshold = -1,
- usage_disk: diskThreshold = -1,
- } = thresholds;
-
- // Get metrics from response
- const metrics = networkResponse?.payload?.data ?? null;
- if (metrics === null) return false;
-
- const {
- cpu: { usage_percent: cpuUsage = -1 } = {},
- memory: { usage_percent: memoryUsage = -1 } = {},
- disk = [],
- } = metrics;
-
- const alerts = {
- cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
- memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
- disk:
- disk?.some(
- (d) =>
- diskThreshold !== -1 &&
- typeof d?.usage_percent === "number" &&
- d?.usage_percent > diskThreshold
- ) ?? false,
- };
-
- const notifications = await this.db.getNotificationsByMonitorId(
- networkResponse.monitorId
- );
- for (const notification of notifications) {
- const alertsToSend = [];
- const alertTypes = ["cpu", "memory", "disk"];
-
- for (const type of alertTypes) {
- // Iterate over each alert type to see if any need to be decremented
- if (alerts[type] === true) {
- notification[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
-
- if (notification[`${type}AlertThreshold`] <= 0) {
- // If threshold drops below 0, reset and send notification
- notification[`${type}AlertThreshold`] = notification.alertThreshold;
-
- const formatAlert = {
- cpu: () =>
- `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
- memory: () =>
- `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
- disk: () =>
- `Your current disk usage: ${disk
- .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
- .join(
- ", "
- )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
- };
- alertsToSend.push(formatAlert[type]());
- }
- }
- }
-
- await notification.save();
-
- if (alertsToSend.length === 0) continue; // No alerts to send, we're done
-
- if (notification.type === "email") {
- this.sendHardwareEmail(networkResponse, notification.address, alertsToSend);
- }
- }
- return true;
- }
-
- /**
- * Handles notifications for different monitor types
- *
- * @async
- * @function handleNotifications
- * @param {Object} networkResponse - Response object containing monitor information
- * @returns {Promise} - Indicates whether notifications were processed successfully
- */
async handleNotifications(networkResponse) {
- try {
- if (networkResponse.monitor.type === "hardware") {
- this.handleHardwareNotifications(networkResponse);
- }
- this.handleStatusNotifications(networkResponse);
- return true;
- } catch (error) {
- this.logger.error({
- message: error.message,
- service: this.SERVICE_NAME,
- method: "handleNotifications",
- stack: error.stack,
- });
+ const { monitor, statusChanged, prevStatus } = networkResponse;
+ const { type } = monitor;
+ if (type !== "hardware" && statusChanged === false) return false;
+ // if prevStatus is undefined, monitor is resuming, we're done
+ if (type !== "hardware" && prevStatus === undefined) return false;
+
+ const notificationIDs = networkResponse.monitor?.notifications ?? [];
+ if (notificationIDs.length === 0) return false;
+
+ if (networkResponse.monitor.type === "hardware") {
+ const thresholds = networkResponse?.monitor?.thresholds;
+
+ if (thresholds === undefined) return false; // No thresholds set, we're done
+ const metrics = networkResponse?.payload?.data ?? null;
+ if (metrics === null) return false; // No metrics, we're done
+
+ const alerts = await this.notificationUtils.buildHardwareAlerts(networkResponse);
+ if (alerts.length === 0) return false;
+
+ const { subject, html } = await this.notificationUtils.buildHardwareEmail(
+ networkResponse,
+ alerts
+ );
+ const content =
+ await this.notificationUtils.buildHardwareNotificationMessage(alerts);
+
+ const success = await this.notifyAll({ notificationIDs, subject, html, content });
+ return success;
}
+
+ // Status monitors
+ const { subject, html } =
+ await this.notificationUtils.buildStatusEmail(networkResponse);
+ const content = await this.notificationUtils.buildWebhookMessage(networkResponse);
+ const success = this.notifyAll({ notificationIDs, subject, html, content });
+ return success;
+ }
+
+ async notifyAll({ notificationIDs, subject, html, content }) {
+ const notifications = await this.db.getNotificationsByIds(notificationIDs);
+
+ // Map each notification to a test promise
+ const promises = notifications.map(async (notification) => {
+ try {
+ await this.sendNotification({ notification, subject, content, html });
+ return true;
+ } catch (err) {
+ return false;
+ }
+ });
+
+ const results = await Promise.all(promises);
+ return results.every((r) => r === true);
+ }
+
+ async getTestNotification() {
+ const html = await this.notificationUtils.buildTestEmail();
+ const content = "This is a test notification";
+ const subject = "Test Notification";
+ return { subject, html, content };
+ }
+
+ async testAllNotifications(notificationIDs) {
+ const { subject, html, content } = await this.getTestNotification();
+ return this.notifyAll({ notificationIDs, subject, html, content });
+ }
+
+ async sendTestNotification(notification) {
+ const { subject, html, content } = await this.getTestNotification();
+ const success = await this.sendNotification({ notification, subject, content, html });
+ return success;
}
}
diff --git a/server/service/notificationUtils.js b/server/service/notificationUtils.js
new file mode 100644
index 000000000..61c3ec36a
--- /dev/null
+++ b/server/service/notificationUtils.js
@@ -0,0 +1,146 @@
+class NotificationUtils {
+ constructor({ stringService, emailService }) {
+ this.stringService = stringService;
+ this.emailService = emailService;
+ }
+
+ buildTestEmail = async () => {
+ const context = { testName: "Monitoring System" };
+ const html = await this.emailService.buildEmail("testEmailTemplate", context);
+ return html;
+ };
+
+ buildStatusEmail = async (networkResponse) => {
+ const { monitor, status, prevStatus } = networkResponse;
+ const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate";
+ const context = { monitor: monitor.name, url: monitor.url };
+ const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`;
+ const html = await this.emailService.buildEmail(template, context);
+ return { subject, html };
+ };
+
+ buildWebhookMessage = (networkResponse) => {
+ const { monitor, status, code, timestamp } = networkResponse;
+ // Format timestamp using the local system timezone
+ const formatTime = (timestamp) => {
+ const date = new Date(timestamp);
+
+ // Get timezone abbreviation and format the date
+ const timeZoneAbbr = date
+ .toLocaleTimeString("en-US", { timeZoneName: "short" })
+ .split(" ")
+ .pop();
+
+ // Format the date with readable format
+ return (
+ date
+ .toLocaleString("en-US", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ hour: "2-digit",
+ minute: "2-digit",
+ second: "2-digit",
+ hour12: false,
+ })
+ .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") +
+ " " +
+ timeZoneAbbr
+ );
+ };
+
+ // Get formatted time
+ const formattedTime = timestamp
+ ? formatTime(timestamp)
+ : formatTime(new Date().getTime());
+
+ // Create different messages based on status with extra spacing
+ let messageText;
+ if (status === true) {
+ messageText = this.stringService.monitorUpAlert
+ .replace("{monitorName}", monitor.name)
+ .replace("{time}", formattedTime)
+ .replace("{code}", code || "Unknown");
+ } else {
+ messageText = this.stringService.monitorDownAlert
+ .replace("{monitorName}", monitor.name)
+ .replace("{time}", formattedTime)
+ .replace("{code}", code || "Unknown");
+ }
+ return messageText;
+ };
+
+ buildHardwareAlerts = async (networkResponse) => {
+ const monitor = networkResponse?.monitor;
+ const thresholds = networkResponse?.monitor?.thresholds;
+ const {
+ usage_cpu: cpuThreshold = -1,
+ usage_memory: memoryThreshold = -1,
+ usage_disk: diskThreshold = -1,
+ } = thresholds;
+
+ const metrics = networkResponse?.payload?.data;
+ const {
+ cpu: { usage_percent: cpuUsage = -1 } = {},
+ memory: { usage_percent: memoryUsage = -1 } = {},
+ disk = [],
+ } = metrics;
+
+ const alerts = {
+ cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false,
+ memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false,
+ disk:
+ disk?.some(
+ (d) =>
+ diskThreshold !== -1 &&
+ typeof d?.usage_percent === "number" &&
+ d?.usage_percent > diskThreshold
+ ) ?? false,
+ };
+
+ const alertsToSend = [];
+ const alertTypes = ["cpu", "memory", "disk"];
+ for (const type of alertTypes) {
+ // Iterate over each alert type to see if any need to be decremented
+ if (alerts[type] === true) {
+ monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered
+
+ if (monitor[`${type}AlertThreshold`] <= 0) {
+ // If threshold drops below 0, reset and send notification
+ monitor[`${type}AlertThreshold`] = monitor.alertThreshold;
+
+ const formatAlert = {
+ cpu: () =>
+ `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`,
+ memory: () =>
+ `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`,
+ disk: () =>
+ `Your current disk usage: ${disk
+ .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`)
+ .join(
+ ", "
+ )} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`,
+ };
+ alertsToSend.push(formatAlert[type]());
+ }
+ }
+ }
+ await monitor.save();
+ return alertsToSend;
+ };
+
+ buildHardwareEmail = async (networkResponse, alerts) => {
+ const { monitor } = networkResponse;
+ const template = "hardwareIncidentTemplate";
+ const context = { monitor: monitor.name, url: monitor.url, alerts };
+ const subject = `Monitor ${monitor.name} infrastructure alerts`;
+ const html = await this.emailService.buildEmail(template, context);
+ return { subject, html };
+ };
+
+ buildHardwareNotificationMessage = (alerts) => {
+ return alerts.map((alert) => alert).join("\n");
+ };
+}
+
+export default NotificationUtils;
diff --git a/server/service/settingsService.js b/server/service/settingsService.js
index bc314abff..de035922c 100755
--- a/server/service/settingsService.js
+++ b/server/service/settingsService.js
@@ -28,8 +28,8 @@ class SettingsService {
* Constructs a new SettingsService
* @constructor
* @throws {Error}
- */ constructor(appSettings) {
- this.appSettings = appSettings;
+ */ constructor(AppSettings) {
+ this.AppSettings = AppSettings;
this.settings = { ...envConfig };
}
/**
@@ -60,16 +60,14 @@ class SettingsService {
async getDBSettings() {
// Remove any old settings
- await this.appSettings.deleteMany({ version: { $exists: false } });
+ await this.AppSettings.deleteMany({ version: { $exists: false } });
- let settings = await this.appSettings
- .findOne({ singleton: true })
+ let settings = await this.AppSettings.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
if (settings === null) {
- await this.appSettings.create({});
- settings = await this.appSettings
- .findOne({ singleton: true })
+ await this.AppSettings.create({});
+ settings = await this.AppSettings.findOne({ singleton: true })
.select("-__v -_id -createdAt -updatedAt -singleton")
.lean();
}
diff --git a/server/service/statusService.js b/server/service/statusService.js
index a53552717..3d7bb4543 100755
--- a/server/service/statusService.js
+++ b/server/service/statusService.js
@@ -209,25 +209,14 @@ class StatusService {
tls_took,
};
- if (type === "distributed_http") {
- if (typeof payload === "undefined") {
- return undefined;
- }
- check.continent = payload.continent;
- check.countryCode = payload.country_code;
- check.city = payload.city;
- check.location = payload.location;
- check.uptBurnt = payload.upt_burnt;
- check.first_byte_took = payload.first_byte_took;
- check.body_read_took = payload.body_read_took;
- check.dns_took = payload.dns_took;
- check.conn_took = payload.conn_took;
- check.connect_took = payload.connect_took;
- check.tls_took = payload.tls_took;
- }
-
if (type === "pagespeed") {
if (typeof payload === "undefined") {
+ this.logger.warn({
+ message: "Failed to build check",
+ service: this.SERVICE_NAME,
+ method: "buildCheck",
+ details: "empty payload",
+ });
return undefined;
}
const categories = payload?.lighthouseResult?.categories ?? {};
@@ -254,6 +243,7 @@ class StatusService {
check.disk = disk ?? {};
check.host = host ?? {};
check.errors = errors ?? [];
+ check.capture = payload?.capture ?? {};
}
return check;
};
@@ -287,9 +277,11 @@ class StatusService {
} catch (error) {
this.logger.error({
message: error.message,
- service: this.SERVICE_NAME,
- method: "insertCheck",
- details: `Error inserting check for monitor: ${networkResponse?.monitorId}`,
+ service: error.service || this.SERVICE_NAME,
+ method: error.method || "insertCheck",
+ details:
+ error.details ||
+ `Error inserting check for monitor: ${networkResponse?.monitorId}`,
stack: error.stack,
});
}
diff --git a/server/service/stringService.js b/server/service/stringService.js
index b02254255..6f9ad8cd8 100755
--- a/server/service/stringService.js
+++ b/server/service/stringService.js
@@ -276,6 +276,10 @@ class StringService {
return this.translationService.getTranslation("queueGetMetrics");
}
+ get queueGetJobs() {
+ return this.translationService.getTranslation("queueGetJobs");
+ }
+
get queueAddJob() {
return this.translationService.getTranslation("queueAddJob");
}
@@ -432,6 +436,18 @@ class StringService {
get testEmailSubject() {
return this.translationService.getTranslation("testEmailSubject");
}
+
+ get verifyOwnerNotFound() {
+ return this.translationService.getTranslation("verifyOwnerNotFound");
+ }
+
+ get verifyOwnerUnauthorized() {
+ return this.translationService.getTranslation("verifyOwnerUnauthorized");
+ }
+
+ get dbUserNotFound() {
+ return this.translationService.getTranslation("dbUserNotFound");
+ }
}
export default StringService;
diff --git a/server/templates/testEmailTemplate.mjml b/server/templates/testEmailTemplate.mjml
index e9fd3221e..4f8fd7644 100644
--- a/server/templates/testEmailTemplate.mjml
+++ b/server/templates/testEmailTemplate.mjml
@@ -1,17 +1,26 @@
-
-
-
-
- Hello!
-
-
- This is a test email from the Monitoring System.
-
-
- If you're receiving this, your email configuration is working correctly.
-
-
-
-
+
+
+
+
+ Hello!
+
+
+ This is a test email from the Monitoring System.
+
+
+ If you're receiving this, your email configuration is working correctly.
+
+
+
+
diff --git a/server/utils/logger.js b/server/utils/logger.js
index 155adc957..49b875d96 100755
--- a/server/utils/logger.js
+++ b/server/utils/logger.js
@@ -4,6 +4,8 @@ dotenv.config();
class Logger {
constructor() {
+ this.logCache = [];
+ this.maxCacheSize = 1000;
const consoleFormat = format.printf(
({ level, message, service, method, details, timestamp, stack }) => {
if (message instanceof Object) {
@@ -72,11 +74,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
info(config) {
- this.logger.info(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- });
+ const logEntry = this.buildLogEntry("info", config);
+ this.cacheLog(logEntry);
+ this.logger.info(logEntry);
}
/**
@@ -88,11 +88,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
warn(config) {
- this.logger.warn(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- });
+ const logEntry = this.buildLogEntry("warn", config);
+ this.cacheLog(logEntry);
+ this.logger.warn(logEntry);
}
/**
@@ -104,12 +102,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
error(config) {
- this.logger.error(config.message, {
- service: config.service,
- method: config.method,
- details: config.details,
- stack: config.stack,
- });
+ const logEntry = this.buildLogEntry("error", config);
+ this.cacheLog(logEntry);
+ this.logger.error(logEntry);
}
/**
* Logs a debug message.
@@ -120,12 +115,32 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
debug(config) {
- this.logger.debug(config.message, {
+ const logEntry = this.buildLogEntry("debug", config);
+ this.cacheLog(logEntry);
+ this.logger.debug(logEntry);
+ }
+
+ cacheLog(entry) {
+ this.logCache.push(entry);
+ if (this.logCache.length > this.maxCacheSize) {
+ this.logCache.shift();
+ }
+ }
+
+ getLogs() {
+ return this.logCache;
+ }
+
+ buildLogEntry(level, config) {
+ return {
+ level,
+ message: config.message,
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
- });
+ timestamp: new Date().toISOString(),
+ };
}
}
diff --git a/server/validation/joi.js b/server/validation/joi.js
index e0e563e84..e5c1d6e43 100755
--- a/server/validation/joi.js
+++ b/server/validation/joi.js
@@ -22,29 +22,19 @@ const passwordPattern =
/^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/])[A-Za-z0-9!?@#$%^&*()\-_=+[\]{};:'",.<>~`|\\/]+$/;
const loginValidation = joi.object({
- email: joi
- .string()
- .email()
- .required()
- .custom((value, helpers) => {
- const lowercasedValue = value.toLowerCase();
- if (value !== lowercasedValue) {
- return helpers.message("Email must be in lowercase");
- }
- return lowercasedValue;
- }),
- password: joi.string().min(8).required().pattern(passwordPattern),
+ email: joi.string().email().required().lowercase(),
+ password: joi.string().required(),
});
const nameValidation = joi
.string()
.trim()
.max(50)
- .pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s']+$/u)
+ .pattern(/^(?=.*[\p{L}\p{Sc}])[\p{L}\p{Sc}\s'\-().]+$/u)
.messages({
"string.empty": "Name is required",
"string.max": "Name must be less than 50 characters",
"string.pattern.base":
- "Name must contain at least 1 letter or currency symbol and only allow letters, spaces, apostrophes, and currency symbols",
+ "Names must contain at least 1 letter and may only include letters, currency symbols, spaces, apostrophes, hyphens (-), periods (.), and parentheses ().",
});
const registrationBodyValidation = joi.object({
@@ -136,9 +126,7 @@ const getMonitorByIdQueryValidation = joi.object({
normalize: joi.boolean(),
});
-const getMonitorsByTeamIdParamValidation = joi.object({
- teamId: joi.string().required(),
-});
+const getMonitorsByTeamIdParamValidation = joi.object({});
const getMonitorsByTeamIdQueryValidation = joi.object({
limit: joi.number(),
@@ -177,8 +165,6 @@ const getCertificateParamValidation = joi.object({
const createMonitorBodyValidation = joi.object({
_id: joi.string(),
- userId: joi.string().required(),
- teamId: joi.string().required(),
name: joi.string().required(),
description: joi.string().required(),
type: joi.string().required(),
@@ -193,25 +179,30 @@ const createMonitorBodyValidation = joi.object({
usage_disk: joi.number(),
usage_temperature: joi.number(),
}),
- notifications: joi.array().items(joi.object()),
+ notifications: joi.array().items(joi.string()),
secret: joi.string(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
matchMethod: joi.string(),
});
-const createMonitorsBodyValidation = joi.array().items(createMonitorBodyValidation);
+const createMonitorsBodyValidation = joi.array().items(
+ createMonitorBodyValidation.keys({
+ userId: joi.string().required(),
+ teamId: joi.string().required(),
+ })
+);
const editMonitorBodyValidation = joi.object({
name: joi.string(),
description: joi.string(),
interval: joi.number(),
- notifications: joi.array().items(joi.object()),
+ notifications: joi.array().items(joi.string()),
secret: joi.string(),
ignoreTlsErrors: joi.boolean(),
jsonPath: joi.string().allow(""),
expectedValue: joi.string().allow(""),
- matchMethod: joi.string(),
+ matchMethod: joi.string().allow(null, ""),
port: joi.number().min(1).max(65535),
thresholds: joi.object().keys({
usage_cpu: joi.number(),
@@ -303,18 +294,7 @@ const getChecksParamValidation = joi.object({
});
const getChecksQueryValidation = joi.object({
- type: joi
- .string()
- .valid(
- "http",
- "ping",
- "pagespeed",
- "hardware",
- "docker",
- "port",
- "distributed_http",
- "distributed_test"
- ),
+ type: joi.string().valid("http", "ping", "pagespeed", "hardware", "docker", "port"),
sortOrder: joi.string().valid("asc", "desc"),
limit: joi.number(),
dateRange: joi.string().valid("recent", "hour", "day", "week", "month", "all"),
@@ -324,9 +304,7 @@ const getChecksQueryValidation = joi.object({
status: joi.boolean(),
});
-const getTeamChecksParamValidation = joi.object({
- teamId: joi.string().required(),
-});
+const getTeamChecksParamValidation = joi.object({});
const getTeamChecksQueryValidation = joi.object({
sortOrder: joi.string().valid("asc", "desc"),
@@ -342,9 +320,7 @@ const deleteChecksParamValidation = joi.object({
monitorId: joi.string().required(),
});
-const deleteChecksByTeamIdParamValidation = joi.object({
- teamId: joi.string().required(),
-});
+const deleteChecksByTeamIdParamValidation = joi.object({});
const updateChecksTTLBodyValidation = joi.object({
ttl: joi.number().required(),
@@ -434,7 +410,13 @@ const updateAppSettingsBodyValidation = joi.object({
systemEmailAddress: joi.string().allow(""),
systemEmailPassword: joi.string().allow(""),
systemEmailUser: joi.string().allow(""),
- systemEmailConnectionHost: joi.string().allow(""),
+ systemEmailConnectionHost: joi.string().allow("").optional(),
+ systemEmailTLSServername: joi.string().allow("").optional(),
+ systemEmailSecure: joi.boolean(),
+ systemEmailPool: joi.boolean(),
+ systemEmailIgnoreTLS: joi.boolean(),
+ systemEmailRequireTLS: joi.boolean(),
+ systemEmailRejectUnauthorized: joi.boolean(),
});
//****************************************
@@ -446,14 +428,12 @@ const getStatusPageParamValidation = joi.object({
});
const getStatusPageQueryValidation = joi.object({
- type: joi.string().valid("uptime", "distributed").required(),
+ type: joi.string().valid("uptime").required(),
timeFrame: joi.number().optional(),
});
const createStatusPageBodyValidation = joi.object({
- userId: joi.string().required(),
- teamId: joi.string().required(),
- type: joi.string().valid("uptime", "distributed").required(),
+ type: joi.string().valid("uptime").required(),
companyName: joi.string().required(),
url: joi
.string()
@@ -483,6 +463,7 @@ const createStatusPageBodyValidation = joi.object({
isPublished: joi.boolean(),
showCharts: joi.boolean().optional(),
showUptimePercentage: joi.boolean(),
+ showAdminLoginLink: joi.boolean().optional(),
});
const imageValidation = joi
@@ -577,6 +558,30 @@ const triggerNotificationBodyValidation = joi.object({
}),
});
+const createNotificationBodyValidation = joi.object({
+ notificationName: joi.string().required().messages({
+ "string.empty": "Notification name is required",
+ "any.required": "Notification name is required",
+ }),
+ address: joi.when("type", {
+ is: "email",
+ then: joi.string().email().required().messages({
+ "string.empty": "E-mail address cannot be empty",
+ "any.required": "E-mail address is required",
+ "string.email": "Please enter a valid e-mail address",
+ }),
+ }),
+ type: joi
+ .string()
+ .valid("email", "webhook", "slack", "discord", "pager_duty")
+ .required()
+ .messages({
+ "string.empty": "Notification type is required",
+ "any.required": "Notification type is required",
+ "any.only": "Notification type must be email, webhook, or pager_duty",
+ }),
+});
+
//****************************************
// Announcetment Page Validation
//****************************************
@@ -597,10 +602,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(),
+ systemEmailConnectionHost: joi.string().allow("").optional(),
+ systemEmailIgnoreTLS: joi.boolean(),
+ systemEmailRequireTLS: joi.boolean(),
+ systemEmailRejectUnauthorized: joi.boolean(),
+ systemEmailTLSServername: joi.string().allow("").optional(),
});
export {
@@ -664,6 +675,7 @@ export {
getStatusPageQueryValidation,
imageValidation,
triggerNotificationBodyValidation,
+ createNotificationBodyValidation,
webhookConfigValidation,
createAnnouncementValidation,
sendTestEmailBodyValidation,