Merge remote-tracking branch 'upstream/develop' into develop

This commit is contained in:
shanika Jayawardane
2025-06-24 23:37:16 -06:00
284 changed files with 12822 additions and 13977 deletions
+2 -1
View File
@@ -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.
+62
View File
@@ -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."
@@ -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 }}
+154
View File
@@ -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
-80
View File
@@ -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
@@ -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
@@ -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
+6
View File
@@ -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
+6
View File
@@ -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
+5 -1
View File
@@ -27,6 +27,10 @@ Process. If more revisions are required after the second review were 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 weve reached our cognitive limit were 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 dont! 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.
+30 -7
View File
@@ -14,7 +14,7 @@
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8)
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
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
<p>
<img width="2714" alt="server" src="https://github.com/user-attachments/assets/f7cb272a-69a6-48c5-93b0-249ecf20ecc6" />
<img width="1628" alt="image" src="https://github.com/user-attachments/assets/2eff6464-0738-4a32-9312-26e1e8e86275" />
</p>
<p>
<img width="2714" alt="uptime" src="https://github.com/user-attachments/assets/98ddc6c0-3384-47fd-96ce-7e53e6b688ac" />
<img width="1656" alt="image" src="https://github.com/user-attachments/assets/616c3563-c2a7-4ee4-af6c-7e6068955d1a" />
</p>
<p>
<img width="2714" alt="page speed" src="https://github.com/user-attachments/assets/b5589f79-da30-4239-9846-1f8bb2637ff9" />
</p><img width="1652" alt="image" src="https://github.com/user-attachments/assets/7912d7cf-0d0e-4f26-aa5c-2ad7170b5c99" />
</p>
<p>
<img width="1652" alt="image" src="https://github.com/user-attachments/assets/08c2c6ac-3a2f-44d1-a229-d1746a3f9d16" />
</p>
## 🏗️ 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 dont hold back — jump in, contribute and learn with us!
@@ -127,11 +148,13 @@ Here's how you can contribute:
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
</a>
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](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
[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](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:
+4
View File
@@ -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",
},
};
+246 -668
View File
File diff suppressed because it is too large Load Diff
+1 -8
View File
@@ -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"
+5 -11
View File
@@ -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() {
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<WalletProvider>
<CssBaseline />
<GlobalStyles
styles={({ palette }) => {
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,
},
};
}}
/>
<Routes />
<AppLayout>
<Routes />
</AppLayout>
<ToastContainer />
</WalletProvider>
</ThemeProvider>
+19 -29
View File
@@ -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"
/>
+1 -1
View File
@@ -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";
@@ -43,7 +43,6 @@ const ChartBox = ({
>
<Stack
flex={1}
alignItems="center"
sx={{
padding: theme.spacing(8),
justifyContent,
-3
View File
@@ -47,9 +47,6 @@ const AppAppBar = () => {
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";
+1 -1
View File
@@ -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;
+6 -2
View File
@@ -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 (
@@ -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 (
<>
<Typography
@@ -10,9 +13,9 @@ const NetworkError = () => {
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
Network error
{t("common.toasts.networkError")}
</Typography>
<Typography>Please check your connection</Typography>
<Typography>{t("common.toasts.checkConnection")}</Typography>
</>
);
};
+15 -3
View File
@@ -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 (
<WrappedComponent
{...props}
isSuperAdmin={true}
superAdminExists={superAdminExists}
/>
);
};
@@ -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"]),
+20 -14
View File
@@ -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 (
<FormControlLabel
className="custom-radio-button"
checked={props.checked}
value={props.value}
name={name}
checked={checked}
value={value}
control={
<MUIRadio
id={props.id}
size={props.size}
id={id}
size={size}
checkedIcon={<RadioChecked />}
sx={{
color: "transparent",
@@ -49,16 +50,16 @@ const Radio = (props) => {
}}
/>
}
onChange={props.onChange}
onChange={onChange}
label={
<>
<Typography component="p">{props.title}</Typography>
<Typography component="p">{title}</Typography>
<Typography
component="h6"
mt={theme.spacing(1)}
color={theme.palette.primary.contrastTextSecondary}
>
{props.desc}
{desc}
</Typography>
</>
}
@@ -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;
+87 -25
View File
@@ -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 (
<Autocomplete
onBlur={onBlur}
multiple={multiple}
id={id}
value={value}
open={open}
onOpen={() => 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 && (
<Typography
@@ -140,6 +178,9 @@ const Search = ({
</Stack>
)}
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 (
<ListItem
key={key}
@@ -166,9 +208,29 @@ const Search = ({
pointerEvents: "none",
backgroundColor: theme.palette.primary.main,
}
: {}
: option.isSelectAll
? {
fontWeight: "bold",
backgroundColor: theme.palette.primary.light,
"&:hover": {
backgroundColor: theme.palette.primary.light,
},
}
: {}
}
>
{multiple && isAdorned && !option.noOptions && (
<Checkbox
checked={option.isSelectAll ? selectAll : selected}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"&.Mui-checked": {
color: theme.palette.secondary.main,
},
padding: 0,
}}
/>
)}
{option[filteredBy] +
(hasSecondaryLabel
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
@@ -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,
@@ -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 (
<Box
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
color: theme.palette.primary.contrastText,
}}
>
{children}
</Box>
);
};
AppLayout.propTypes = {
children: PropTypes.node,
};
export default AppLayout;
+8 -4
View File
@@ -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 (
<MuiLink
href={url}
component={external ? "a" : RouterLink}
to={external ? undefined : url}
href={external ? url : undefined}
sx={{ width: "fit-content", ...sx }}
color={color}
target="_blank"
rel="noreferrer"
{...(external && { target: "_blank", rel: "noreferrer" })}
>
{label}
</MuiLink>
@@ -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;
@@ -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 (
<React.Fragment>
<ButtonGroup
variant="contained"
color="accent"
ref={anchorRef}
aria-label="Monitor actions"
disabled={isLoading || isExporting}
>
<Button onClick={handleClick}>{options[selectedIndex]}</Button>
<Button
size="small"
aria-controls={open ? "split-button-menu" : undefined}
aria-expanded={open ? "true" : undefined}
aria-label="select monitor action"
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
</Button>
</ButtonGroup>
<Popper
sx={{ zIndex: 1 }}
open={open}
anchorEl={anchorRef.current}
role={undefined}
transition
disablePortal
>
{({ TransitionProps, placement }) => (
<Grow
{...TransitionProps}
style={{
transformOrigin: placement === "bottom" ? "center top" : "center bottom",
}}
>
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList
id="split-button-menu"
autoFocusItem
>
{options.map((option, index) => (
<MenuItem
key={option}
selected={index === selectedIndex}
onClick={(event) => handleMenuItemClick(event, index)}
>
{option}
</MenuItem>
))}
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
</React.Fragment>
);
};
export default MonitorActions;
@@ -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 <SkeletonLayout />;
if (isLoading) return <SkeletonLayout />;
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,
@@ -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")}
</Button>
{bulkPath && (
<Button
loading={isLoading}
variant="contained"
color="accent"
onClick={() => {
navigate(`${bulkPath}`);
}}
>
{t("bulkImport.title")}
</Button>
)}
{/* {bulkPath && <MonitorActions isLoading={isLoading} />} */}
</Stack>
);
};
@@ -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 <Skeleton />;
@@ -66,10 +66,13 @@ const MonitorDetailsControlHeader = ({
loading={isSending}
startIcon={<EmailIcon />}
onClick={() => {
sendTestEmail();
testAllNotifications({ monitorId: monitor?._id });
}}
sx={{
whiteSpace: "nowrap",
}}
>
{t("sendTestEmail")}
{t("sendTestNotifications")}
</Button>
<Button
variant="contained"
@@ -88,7 +91,10 @@ const MonitorDetailsControlHeader = ({
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
}
onClick={() => {
pauseMonitor();
pauseMonitor({
monitorId: monitor?._id,
triggerUpdate,
});
}}
>
{monitor?.isActive ? "Pause" : "Resume"}
@@ -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 (
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Typography variant="monitorName">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography variant="monitorUrl">{formatMonitorUrl(monitor?.url)}</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
@@ -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 (
<Box alignSelf="flex-end">
<Button
variant="contained"
color="secondary"
onClick={() => 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,
},
},
}}
>
<SettingsIcon /> Configure
</Button>
</Box>
);
};
ConfigButton.propTypes = {
shouldRender: PropTypes.bool,
monitorId: PropTypes.string.isRequired,
path: PropTypes.string.isRequired,
};
export default ConfigButton;
@@ -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 <SkeletonLayout />;
}
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Stack>
<Typography variant="h1">{monitor?.name}</Typography>
<Stack
direction="row"
alignItems={"center"}
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[determineState(monitor)]} />
<Typography
variant="h2"
style={{ fontFamily: "monospace", fontWeight: "bolder" }}
>
{monitor?.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Dot />
<Typography>
Checking every {formatDurationRounded(monitor?.interval)}.
</Typography>
</Stack>
</Stack>
<ConfigButton
path={path}
shouldRender={isAdmin}
monitorId={monitor?._id}
/>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
path: PropTypes.string.isRequired,
isLoading: PropTypes.bool,
isAdmin: PropTypes.bool,
monitor: PropTypes.object,
};
export default MonitorStatusHeader;
@@ -1,23 +0,0 @@
import { Stack, Skeleton } from "@mui/material";
const SkeletonLayout = () => {
return (
<Stack
direction="row"
justifyContent="space-between"
>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
<Skeleton
height={40}
variant="rounded"
width="15%"
/>
</Stack>
);
};
export default SkeletonLayout;
@@ -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 (
<Stack gap={theme.spacing(6)}>
<Search
type="notifications"
label="Notifications"
options={notifications}
filteredBy="notificationName"
multiple={true}
value={selectedNotifications}
inputValue={notificationsSearch}
handleInputChange={setNotificationsSearch}
handleChange={(value) => {
handleSearch(value);
}}
/>
<Stack
flex={1}
width="100%"
>
{selectedNotifications.map((notification, index) => (
<Stack
direction="row"
alignItems="center"
key={notification._id}
width="100%"
>
<Typography
flexGrow={1} // <-- This will take up all available horizontal space
>
{notification.notificationName}
</Typography>
<DeleteOutlineRoundedIcon
onClick={() => {
handleDelete(notification._id);
}}
sx={{ cursor: "pointer" }}
/>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
))}
</Stack>
</Stack>
);
};
NotificationConfig.propTypes = {
notifications: PropTypes.array,
setMonitor: PropTypes.func,
setNotifications: PropTypes.array,
};
export default NotificationConfig;
@@ -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
) : (
<Navigate
to="/uptime"
replace
/>
);
};
ProtectedDistributedUptimeRoute.propTypes = {
children: PropTypes.node.isRequired,
};
export default ProtectedDistributedUptimeRoute;
@@ -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
) : (
<Navigate
to="/uptime"
replace
/>
);
};
RoleProtectedRoute.propTypes = {
children: PropTypes.element.isRequired,
roles: PropTypes.array,
};
export default RoleProtectedRoute;
+21 -24
View File
@@ -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: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{
name: t("menu.distributedUptime"),
path: "distributed-uptime",
icon: <DistributedUptimeIcon />,
name: t("menu.notifications"),
path: "notifications",
icon: <Notifications />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
// { name: t("menu.integrations"), path: "integrations", icon: <Integrations /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
icon: <Settings />,
@@ -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")}
</Typography>
</Stack>
</Stack>
@@ -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 */
<Tooltip
@@ -33,7 +33,6 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
borderBottom: `1px solid ${theme.palette.primary.lowContrast}`,
borderRadius: 0,
gap: theme.spacing(1.5),
backgroundColor: theme.palette.primary.main,
}}
>
<Stack
+4 -4
View File
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
import Image from "../Image";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils";
import { useMonitorUtils } from "../../Hooks/useMonitorUtils";
/**
* StatBox Component
@@ -41,7 +41,7 @@ const StatBox = ({
sx,
}) => {
const theme = useTheme();
const { statusToTheme } = useUtils();
const { statusToTheme } = useMonitorUtils();
const themeColor = statusToTheme[status];
const statusBoxStyles = gradient
@@ -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,
+1 -1
View File
@@ -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 (
@@ -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 },
});
@@ -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}
>
<Stack flex={0.9}>
<Typography component="h1">{t("email")}</Typography>
<Typography component="h1">{t("auth.common.inputs.email.label")}</Typography>
<Typography
component="p"
sx={{ opacity: 0.6 }}
@@ -4,7 +4,7 @@ import { Button, ButtonGroup, Stack, Typography } from "@mui/material";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import TextInput from "../../Inputs/TextInput";
import { credentials } from "../../../Validation/validation";
import { newOrChangedCredentials } from "../../../Validation/validation";
import { networkService } from "../../../main";
import { createToast } from "../../../Utils/toastUtils";
import Select from "../../Inputs/Select";
@@ -115,7 +115,10 @@ const TeamPanel = () => {
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)}
/>
<Select
id="team-member-role"
+11
View File
@@ -0,0 +1,11 @@
export const createHeaderFactory = (getCellSx = () => {}) => {
return ({ id, content, onClick = () => {}, render = () => {} }) => {
return {
id,
content,
onClick,
getCellSx,
render,
};
};
};
+36
View File
@@ -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 (
<Stack
direction="row"
gap={theme.spacing(4)}
>
<Typography>{text}</Typography>
<Link
color="accent"
to={href}
component={RouterLink}
target={target}
>
{linkText}
</Link>
</Stack>
);
};
TextLink.propTypes = {
text: PropTypes.string,
linkText: PropTypes.string,
href: PropTypes.string,
target: PropTypes.string,
};
export default TextLink;
+3 -1
View File
@@ -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 (
<IconButton
id="theme-toggle"
title="Toggles light & dark"
title={t("common.buttons.toggleTheme")}
className={`theme-${mode}`}
aria-label="auto"
aria-live="polite"
@@ -1,389 +0,0 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { networkService } from "../../main";
const initialState = {
isLoading: false,
monitorsSummary: [],
success: null,
msg: null,
};
export const createInfrastructureMonitor = createAsyncThunk(
"infrastructureMonitors/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 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;
@@ -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;
@@ -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;
+3
View File
@@ -16,6 +16,9 @@ const initialState = {
maintenance: {
rowsPerPage: 5,
},
infrastructure: {
rowsPerPage: 5,
},
sidebar: {
collapsed: false,
},
@@ -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;
+120
View File
@@ -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 };
+35
View File
@@ -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 };
+506
View File
@@ -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,
};
+57
View File
@@ -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 };
@@ -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") });
-27
View File
@@ -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];
};
-24
View File
@@ -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 };
@@ -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 };
@@ -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;
@@ -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;
@@ -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;
-28
View File
@@ -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 };
+21 -1
View File
@@ -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 };
+206
View File
@@ -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,
};
@@ -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 };
@@ -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 };
+1 -1
View File
@@ -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";
+58 -42
View File
@@ -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("<seconds/>", "3"),
},
{
body: "Redirecting in 2...",
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "2"),
},
{
body: "Redirecting in 1...",
body: t("auth.forgotPassword.toasts.redirect").replace("<seconds/>", "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("<email/>", 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)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -160,18 +160,24 @@ const CheckEmail = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<EmailIcon alt="email icon" />
<EmailIcon alt={t("auth.forgotPassword.imageAlts.email")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authCheckEmailTitle")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>
{t("authCheckEmailDescription")}{" "}
<Typography
className="email-sent-to"
component="span"
>
{email || "username@email.com"}
</Typography>
<Trans
i18nKey="auth.forgotPassword.subheadings.stepTwo"
components={{
email: (
<Typography
className="email-sent-to"
component="span"
>
{email || "username@email.com"}
</Typography>
),
}}
/>
</Typography>
</Box>
<Button
@@ -183,23 +189,27 @@ const CheckEmail = () => {
maxWidth: 400,
}}
>
{t("authCheckEmailOpenEmailButton")}
{t("auth.forgotPassword.buttons.openEmail")}
</Button>
<Typography sx={{ alignSelf: "center", mt: theme.spacing(6) }}>
{t("authCheckEmailDidntReceiveEmail")}{" "}
<Typography
component="span"
onClick={resendToken}
sx={{
color: theme.palette.accent.main,
userSelect: "none",
pointerEvents: disabled ? "none" : "auto",
cursor: disabled ? "default" : "pointer",
opacity: disabled ? 0.5 : 1,
<Trans
i18nKey="auth.forgotPassword.links.resend"
components={{
a: (
<Typography
component="span"
onClick={resendToken}
sx={{
color: theme.palette.accent.main,
userSelect: "none",
pointerEvents: disabled ? "none" : "auto",
cursor: disabled ? "default" : "pointer",
opacity: disabled ? 0.5 : 1,
}}
/>
),
}}
>
{t("authCheckEmailClickToResend")}
</Typography>
/>
</Typography>
</Stack>
</Stack>
@@ -207,15 +217,21 @@ const CheckEmail = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
ml={theme.spacing(2)}
color={theme.palette.accent.main}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
+35 -26
View File
@@ -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("<email/>", 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)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -162,11 +165,11 @@ const ForgotPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<Key alt="password key icon" />
<Key alt={t("auth.forgotPassword.imageAlts.passwordKey")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authForgotPasswordTitle")}</Typography>
<Typography>{t("authForgotPasswordInstructions")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>{t("auth.forgotPassword.subheadings.stepOne")}</Typography>
</Box>
<Box
component="form"
@@ -179,13 +182,13 @@ const ForgotPassword = () => {
<TextInput
type="email"
id="forgot-password-email-input"
label={t("email")}
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("enterEmail")}
placeholder={t("auth.common.inputs.email.placeholder")}
value={form.email}
onChange={handleChange}
error={errors.email ? true : false}
helperText={t(errors.email)}
helperText={t(errors.email)} // Localization keys are in validation.js
/>
<Button
variant="contained"
@@ -198,7 +201,7 @@ const ForgotPassword = () => {
mt: theme.spacing(15),
}}
>
{t("continue")}
{t("auth.common.navigation.continue")}
</Button>
</Box>
</Stack>
@@ -207,15 +210,21 @@ const ForgotPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
@@ -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 (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
position="relative"
>
<Box>
<Typography component="h1">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterEmail")}</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="login-email-input"
label={t("email")}
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (e.target.value = e.target.value.toLowerCase())}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""}
ref={inputRef}
/>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
type="submit"
disabled={errors.email && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(6),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
};
EmailStep.propTypes = {
form: PropTypes.object.isRequired,
errors: PropTypes.object.isRequired,
onSubmit: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
export default EmailStep;
@@ -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 (
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
{t("authForgotPasswordTitle")}
</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{ userSelect: "none" }}
onClick={handleNavigate}
>
{t("authForgotPasswordResetPassword")}
</Typography>
</Box>
);
};
ForgotPasswordLabel.propTypes = {
email: PropTypes.string.isRequired,
errorEmail: PropTypes.string.isRequired,
};
export default ForgotPasswordLabel;
@@ -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 (
<>
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
position="relative"
textAlign="center"
>
<Box>
<Typography component="h1">{t("authLoginTitle")}</Typography>
<Typography>{t("authLoginEnterPassword")}</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
mb={theme.spacing(5)}
sx={{
display: "grid",
gap: { xs: theme.spacing(12), sm: theme.spacing(16) },
}}
>
<TextInput
type="password"
id="login-password-input"
label={t("password")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password}
ref={inputRef}
endAdornment={<PasswordEndAdornment />}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
className="dashboard-style-button"
sx={{
px: theme.spacing(5),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}{" "}
</Button>
<Button
variant="contained"
color="accent"
type="submit"
loading={authState.isLoading}
disabled={errors.password && true}
className="dashboard-style-button"
sx={{
width: "30%",
px: theme.spacing(4),
borderRadius: `${theme.shape.borderRadius}px !important`,
"&.MuiButtonBase-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.MuiButton-root": {
borderRadius: `${theme.shape.borderRadius}px !important`,
},
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
boxShadow: `none`,
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
};
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;
-262
View File
@@ -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 (
<Stack
className="login-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.contrastText,
fontWeight: 600,
fontSize: 28,
},
/* TODO set font size from theme */
"& p": { fontSize: 14, color: theme.palette.primary.contrastTextSecondary },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
</Stack>
<Stack
direction="row"
spacing={2}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
rowGap={theme.spacing(8)}
sx={{
"& > .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 ? (
<EmailStep
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
/>
) : (
step === 1 && (
<PasswordStep
form={form}
errors={errors}
onSubmit={handleSubmit}
onChange={handleChange}
onBack={() => setStep(0)}
/>
)
)}
<ForgotPasswordLabel
email={form.email}
errorEmail={errors.email}
/>
{/* Registration link */}
<Box textAlign="center">
<Typography
className="forgot-p"
display="inline-block"
color={theme.palette.primary.main}
>
{t("doNotHaveAccount")}
</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.accent.darker,
},
}}
onClick={() => navigate("/register")}
>
{t("registerHere")}
</Typography>
</Box>
</Stack>
</Stack>
);
};
export default Login;
+167
View File
@@ -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 (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
>
<AuthHeader />
<Stack
margin="auto"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
>
<Typography variant="h1">{t("auth.login.heading")}</Typography>
<Stack
component="form"
width="100%"
maxWidth={600}
alignSelf="center"
justifyContent="center"
borderRadius={theme.spacing(5)}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.primary.main}
padding={theme.spacing(12)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onChange={onChange}
error={errors.email ? true : false}
helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onChange}
error={errors.password ? true : false}
helperText={errors.password ? t(errors.password) : ""} // Localization keys are in validation.js
endAdornment={<PasswordEndAdornment />}
/>
<Button
variant="contained"
color="accent"
type="submit"
sx={{ width: "30%", alignSelf: "flex-end" }}
>
Login
</Button>
</Stack>
<TextLink
text={t("auth.login.links.forgotPassword")}
linkText={t("auth.login.links.forgotPasswordLink")}
href="/forgot-password"
/>
<TextLink
text={t("auth.login.links.register")}
linkText={t("auth.login.links.registerLink")}
href="/register"
/>
</Stack>
</Stack>
);
};
export default Login;
+24 -18
View File
@@ -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)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>Checkmate</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -96,11 +94,13 @@ const NewPasswordConfirmed = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<ConfirmIcon alt="password confirm icon" />
<ConfirmIcon alt={t("auth.forgotPassword.imageAlts.passwordConfirm")} />
</IconBox>
</Stack>
<Typography component="h1">{t("passwordreset")}</Typography>
<Typography mb={theme.spacing(2)}>{t("authNewPasswordConfirmed")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography mb={theme.spacing(2)}>
{t("auth.forgotPassword.subheadings.stepFour")}
</Typography>
</Box>
<Button
variant="contained"
@@ -110,7 +110,7 @@ const NewPasswordConfirmed = () => {
maxWidth: 400,
}}
>
{t("continue")}
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
@@ -118,15 +118,21 @@ const NewPasswordConfirmed = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")}</Typography>
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.accent.main}
ml={theme.spacing(2)}
onClick={handleNavigate}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
-399
View File
@@ -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 (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
alignItems="center"
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>
{isSuperAdmin
? t("authRegisterCreateSuperAdminAccount")
: t("authRegisterCreateAccount")}
</Typography>
</Box>
<Box width="100%">
<Button
variant="outlined"
color="info"
onClick={onSignup}
sx={{
width: "100%",
"& svg": {
mr: theme.spacing(4),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<Mail />
{t("authRegisterSignUpWithEmail")}
</Button>
</Box>
<Box maxWidth={400}>
<Typography className="tos-p">
<Trans
i18nKey="authRegisterBySigningUp"
components={{
a1: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/terms-of-service-open-source",
"_blank",
"noreferrer"
);
}}
/>
),
a2: (
<Typography
component="span"
sx={{
cursor: "pointer",
"&:hover": {
color: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={() => {
window.open(
"https://bluewavelabs.ca/privacy-policy-open-source",
"_blank",
"noreferrer"
);
}}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
</>
);
};
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 (
<Stack
className="register-page auth"
overflow="hidden"
sx={{
"& h1": {
color: theme.palette.primary.contrastText,
fontWeight: 600,
fontSize: 28,
},
/* TODO set fontsize from theme */
"& p": { fontSize: 14, color: theme.palette.primary.contrastTextSecondary },
"& span": { fontSize: "inherit" },
}}
>
<Box
className="background-pattern-svg"
sx={{
"& svg g g:last-of-type path": {
stroke: theme.palette.primary.lowContrast,
},
}}
>
<Background style={{ width: "100%" }} />
</Box>
<Stack
direction="row"
alignItems="center"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("commonAppName")}</Typography>
</Stack>
<Stack
width="100%"
maxWidth={600}
flex={1}
justifyContent="center"
px={{ xs: theme.spacing(12), lg: theme.spacing(20) }}
pb={theme.spacing(20)}
mx="auto"
sx={{
"& > .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 ? (
<LandingPage
isSuperAdmin={isSuperAdmin}
onSignup={() => setStep(1)}
/>
) : step === 1 ? (
<StepOne
form={form}
errors={errors}
onSubmit={handleStepOne}
onChange={handleChange}
onBack={() => setStep(0)}
/>
) : step === 2 ? (
<StepTwo
form={form}
errors={errors}
onSubmit={handleStepTwo}
onChange={handleChange}
onBack={() => setStep(1)}
/>
) : step === 3 ? (
<StepThree
/* form={form}
errors={errors} */
onSubmit={handleStepThree}
/* onChange={handleChange} */
onBack={() => setStep(2)}
/>
) : (
""
)}
</Stack>
<Box
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">
{t("authRegisterAlreadyHaveAccount")}
</Typography>
<Typography
component="span"
ml={theme.spacing(2)}
onClick={() => {
navigate("/login");
}}
sx={{ userSelect: "none", color: theme.palette.accent.main }}
>
{t("authRegisterLoginLink")}
</Typography>
</Box>
</Stack>
);
};
Register.propTypes = {
isSuperAdmin: PropTypes.bool,
};
export default Register;
@@ -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 */}
<Stack
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("authRegisterStepOnePersonalDetails")}</Typography>
</Box>
<Box
textAlign="left"
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<Box
display="grid"
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
>
<TextInput
id="register-firstname-input"
label={t("authRegisterFirstName")}
isRequired={true}
placeholder="Jordan"
autoComplete="given-name"
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName}
ref={inputRef}
/>
<TextInput
id="register-lastname-input"
label={t("authRegisterLastName")}
isRequired={true}
placeholder="Ellis"
autoComplete="family-name"
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName}
/>
</Box>
<Stack
direction="row"
justifyContent="space-between"
>
{/* TODO buttons should be a component should be a component */}
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}
</Button>
<Button
variant="contained"
color="accent"
type="submit"
disabled={(errors.firstName || errors.lastName) && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepOne };
@@ -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 (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("createPassword")}</Typography>
</Box>
<Box
component="form"
noValidate
spellCheck={false}
onSubmit={onSubmit}
textAlign="left"
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
sx={{
"& .input-error": {
display: "none",
},
}}
>
<Box
display="grid"
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("authLoginEnterPassword")}
isRequired={true}
placeholder={t("createAPassword")}
autoComplete="current-password"
value={form.password}
onChange={handleChange}
error={errors.password && errors.password[0] ? true : false}
ref={inputRef}
/>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("authSetNewPasswordConfirmPassword")}
isRequired={true}
placeholder={t("confirmPassword")}
autoComplete="current-password"
value={form.confirm}
onChange={handleChange}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
</Box>
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
variant={feedbacks.length}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
variant={feedbacks.special}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
variant={feedbacks.number}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
variant={feedbacks.confirm}
/>
</Stack>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
":focus-visible": {
outline: `2px solid ${theme.palette.primary.lowContrast}`,
outlineOffset: "4px",
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}
</Button>
<Button
type="submit"
variant="contained"
color="accent"
disabled={
form.password.length === 0 ||
form.confirm.length === 0 ||
Object.keys(errors).length !== 0
}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepThree };
@@ -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 (
<>
<Stack
gap={{ xs: theme.spacing(8), sm: theme.spacing(12) }}
textAlign="center"
>
<Box>
<Typography component="h1">{t("signUp")}</Typography>
<Typography>{t("enterEmail")}</Typography>
</Box>
<Box
component="form"
textAlign="left"
noValidate
spellCheck={false}
onSubmit={onSubmit}
mb={theme.spacing(5)}
display="grid"
gap={{ xs: theme.spacing(12), sm: theme.spacing(16) }}
>
<TextInput
type="email"
id="register-email-input"
label={t("authRegisterEmail")}
isRequired={true}
placeholder="jordan.ellis@domain.com"
autoComplete="email"
value={form.email}
onInput={(e) => (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}
/>
<Stack
direction="row"
justifyContent="space-between"
>
<Button
variant="outlined"
color="info"
onClick={onBack}
sx={{
px: theme.spacing(5),
"& svg.MuiSvgIcon-root": {
mr: theme.spacing(3),
},
"&:focus-visible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
},
}}
>
<ArrowBackRoundedIcon />
{t("commonBack")}
</Button>
<Button
variant="contained"
color="accent"
onClick={onSubmit}
disabled={errors.email && true}
sx={{
width: "30%",
"&.Mui-focusVisible": {
outline: `2px solid ${theme.palette.primary.main}`,
outlineOffset: `2px`,
boxShadow: `none`,
},
}}
>
{t("continue")}
</Button>
</Stack>
</Box>
</Stack>
</>
);
}
export { StepTwo };
+353
View File
@@ -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 (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
>
<AuthHeader />
<Stack
margin="auto"
width="100%"
alignItems="center"
gap={theme.spacing(10)}
>
<Typography variant="h1">{t("auth.registration.heading.user")}</Typography>
<Stack
component="form"
width="100%"
maxWidth={600}
alignSelf="center"
justifyContent="center"
border={1}
borderRadius={theme.spacing(5)}
borderColor={theme.palette.primary.lowContrast}
backgroundColor={theme.palette.primary.main}
padding={theme.spacing(12)}
gap={theme.spacing(12)}
onSubmit={onSubmit}
>
<Typography variant="h1">
{superAdminExists
? t("auth.registration.heading.user")
: t("auth.registration.heading.superAdmin")}
</Typography>
<Typography>
{superAdminExists
? t("auth.registration.description.user")
: t("auth.registration.description.superAdmin")}
</Typography>
<TextInput
type="email"
name="email"
label={t("auth.common.inputs.email.label")}
isRequired={true}
placeholder={t("auth.common.inputs.email.placeholder")}
autoComplete="email"
value={form.email}
onInput={(e) => (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
/>
<TextInput
name="firstName"
label={t("auth.common.inputs.firstName.label")}
isRequired={true}
placeholder={t("auth.common.inputs.firstName.placeholder")}
autoComplete="given-name"
value={form.firstName}
onChange={onChange}
error={errors.firstName ? true : false}
helperText={errors.firstName ? t(errors.firstName) : ""}
/>
<TextInput
name="lastName"
label={t("auth.common.inputs.lastName.label")}
isRequired={true}
placeholder={t("auth.common.inputs.lastName.placeholder")}
autoComplete="family-name"
value={form.lastName}
onChange={onChange}
error={errors.lastName ? true : false}
helperText={errors.lastName ? t(errors.lastName) : ""} // Localization keys are in validation.js
/>
<TextInput
type="password"
id="register-password-input"
name="password"
label={t("auth.common.inputs.password.label")}
isRequired={true}
placeholder="••••••••••"
autoComplete="current-password"
value={form.password}
onChange={onPasswordChange}
error={errors.password && errors.password[0] ? true : false}
helperText={
errors.password === "auth.common.inputs.password.errors.empty"
? t(errors.password)
: ""
} // Other errors are related to required password conditions and are visualized below the input
/>
<TextInput
type="password"
id="register-confirm-input"
name="confirm"
label={t("auth.common.inputs.passwordConfirm.label")}
isRequired={true}
placeholder={t("auth.common.inputs.passwordConfirm.placeholder")}
autoComplete="current-password"
value={form.confirm}
onChange={onPasswordChange}
error={errors.confirm && errors.confirm[0] ? true : false}
/>
<Stack
gap={theme.spacing(4)}
mb={{ xs: theme.spacing(6), sm: theme.spacing(8) }}
>
<Check
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
text={t("auth.common.inputs.password.rules.length.highlighted")}
variant={feedback.length}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
text={t("auth.common.inputs.password.rules.special.highlighted")}
variant={feedback.special}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
text={t("auth.common.inputs.password.rules.number.highlighted")}
variant={feedback.number}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.uppercase.beginning")}
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
variant={feedback.uppercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.lowercase.beginning")}
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
variant={feedback.lowercase}
/>
<Check
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
text={t("auth.common.inputs.password.rules.match.highlighted")}
variant={feedback.confirm}
/>
</Stack>
<Button
disabled={isLoading}
variant="contained"
color="accent"
type="submit"
sx={{ width: "30%", alignSelf: "flex-end" }}
>
{t("auth.common.navigation.continue")}
</Button>
</Stack>
</Stack>
</Stack>
);
};
Register.propTypes = {
superAdminExists: PropTypes.bool,
};
export default Register;
+53 -39
View File
@@ -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)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("commonAppName")}</Typography>
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
width="100%"
@@ -140,11 +140,11 @@ const SetNewPassword = () => {
svgHeight={24}
mb={theme.spacing(4)}
>
<LockIcon alt="lock icon" />
<LockIcon alt={t("auth.forgotPassword.imageAlts.lock")} />
</IconBox>
</Stack>
<Typography component="h1">{t("authSetNewPasswordTitle")}</Typography>
<Typography>{t("authSetNewPasswordDescription")}</Typography>
<Typography component="h1">{t("auth.forgotPassword.heading")}</Typography>
<Typography>{t("auth.forgotPassword.subheadings.stepThree")}</Typography>
</Box>
<Box
width="100%"
@@ -165,13 +165,17 @@ const SetNewPassword = () => {
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={<PasswordEndAdornment />}
/>
</Box>
@@ -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={<PasswordEndAdornment />}
/>
</Box>
@@ -200,33 +204,37 @@ const SetNewPassword = () => {
mb={theme.spacing(12)}
>
<Check
noHighlightText={t("authPasswordMustBeAtLeast")}
text={t("authPasswordCharactersLong")}
noHighlightText={t("auth.common.inputs.password.rules.length.beginning")}
text={t("auth.common.inputs.password.rules.length.highlighted")}
variant={feedbacks.length}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordSpecialCharacter")}
noHighlightText={t("auth.common.inputs.password.rules.special.beginning")}
text={t("auth.common.inputs.password.rules.special.highlighted")}
variant={feedbacks.special}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordOneNumber")}
noHighlightText={t("auth.common.inputs.password.rules.number.beginning")}
text={t("auth.common.inputs.password.rules.number.highlighted")}
variant={feedbacks.number}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordUpperCharacter")}
noHighlightText={t(
"auth.common.inputs.password.rules.uppercase.beginning"
)}
text={t("auth.common.inputs.password.rules.uppercase.highlighted")}
variant={feedbacks.uppercase}
/>
<Check
noHighlightText={t("authPasswordMustContainAtLeast")}
text={t("authPasswordLowerCharacter")}
noHighlightText={t(
"auth.common.inputs.password.rules.lowercase.beginning"
)}
text={t("auth.common.inputs.password.rules.lowercase.highlighted")}
variant={feedbacks.lowercase}
/>
<Check
noHighlightText={t("authPasswordConfirmAndPassword")}
text={t("authPasswordMustMatch")}
noHighlightText={t("auth.common.inputs.password.rules.match.beginning")}
text={t("auth.common.inputs.password.rules.match.highlighted")}
variant={feedbacks.confirm}
/>
</Stack>
@@ -243,7 +251,7 @@ const SetNewPassword = () => {
}
sx={{ width: "100%", maxWidth: 400 }}
>
{t("authSetNewPasswordResetPassword")}
{t("auth.forgotPassword.buttons.resetPassword")}
</Button>
</Stack>
</Stack>
@@ -251,15 +259,21 @@ const SetNewPassword = () => {
textAlign="center"
p={theme.spacing(12)}
>
<Typography display="inline-block">{t("goBackTo")} </Typography>
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
>
{t("authLoginTitle")}
<Typography display="inline-block">
<Trans
i18nKey="auth.forgotPassword.links.login"
components={{
a: (
<Typography
component="span"
color={theme.palette.primary.main}
ml={theme.spacing(2)}
onClick={() => navigate("/login")}
sx={{ userSelect: "none" }}
/>
),
}}
/>
</Typography>
</Box>
</Stack>
@@ -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 (
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Logo style={{ borderRadius: theme.shape.borderRadius }} />
<Typography sx={{ userSelect: "none" }}>{t("common.appName")}</Typography>
</Stack>
<Stack
direction="row"
spacing={theme.spacing(2)}
alignItems="center"
>
<LanguageSelector />
<ThemeSwitch />
</Stack>
</Stack>
);
};
export default AuthHeader;
@@ -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,
@@ -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 };
@@ -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;
@@ -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 (
<Box>
<Breadcrumbs list={BREADCRUMBS} />
<Stack
component="form"
gap={theme.spacing(12)}
mt={theme.spacing(6)}
onSubmit={() => console.log("submit")}
>
<Typography
component="h1"
variant="h1"
>
<Typography
component="span"
fontSize="inherit"
>
{isCreate ? "Create your" : "Edit your"}{" "}
</Typography>
<Typography
component="span"
variant="h2"
fontSize="inherit"
fontWeight="inherit"
>
{t("monitor")}
</Typography>
</Typography>
<ConfigBox>
<Box>
<Typography component="h2">{t("settingsGeneralSettings")}</Typography>
<Typography component="p">{t("distributedUptimeCreateSelectURL")}</Typography>
</Box>
<Stack gap={theme.spacing(15)}>
<TextInput
type={"url"}
id="monitor-url"
startAdornment={<HttpAdornment https={https} />}
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"]}
/>
<TextInput
type="text"
id="monitor-name"
label="Display name"
isOptional={true}
placeholder={"Google"}
value={form.name}
name="name"
onChange={handleChange}
error={errors["name"] ? true : false}
helperText={errors["name"]}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">{t("distributedUptimeCreateChecks")}</Typography>
<Typography component="p">
{t("distributedUptimeCreateChecksDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Stack gap={theme.spacing(6)}>
<Radio
id="monitor-checks-http"
title="Website monitoring"
desc="Use HTTP(s) to monitor your website or API endpoint."
size="small"
value="http"
name="type"
checked={true}
onChange={handleChange}
/>
{form.type === "http" || form.type === "distributed_http" ? (
<ButtonGroup
disabled={!isCreate}
sx={{ ml: theme.spacing(16) }}
>
<Button
variant="group"
filled={https.toString()}
onClick={() => setHttps(true)}
>
{t("https")}
</Button>
<Button
variant="group"
filled={(!https).toString()}
onClick={() => setHttps(false)}
>
{t("http")}
</Button>
</ButtonGroup>
) : (
""
)}
</Stack>
{errors["type"] ? (
<Box className="error-container">
<Typography
component="p"
className="input-error"
color={theme.palette.error.contrastText}
>
{errors["type"]}
</Typography>
</Box>
) : (
""
)}
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateIncidentNotification")}
</Typography>
<Typography component="p">
{t("distributedUptimeCreateIncidentDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
<Checkbox
id="notify-email-default"
label={`Notify via email (to ${user.email})`}
isChecked={notifications.some(
(notification) => notification.type === "email"
)}
value={user?.email}
onChange={(event) => handleNotifications(event, "email")}
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h2">
{t("distributedUptimeCreateAdvancedSettings")}
</Typography>
</Box>
<Stack gap={theme.spacing(12)}>
<Select
id="monitor-interval"
label="Check frequency"
name="interval"
value={form.interval}
onChange={handleChange}
items={SELECT_VALUES}
/>
</Stack>
</ConfigBox>
<Stack
direction="row"
justifyContent="flex-end"
>
<Button
variant="contained"
color="accent"
onClick={() => handleCreateMonitor()}
disabled={!Object.values(errors).every((value) => value === undefined)}
loading={isLoading}
>
{isCreate ? "Create monitor" : "Configure monitor"}
</Button>
</Stack>
</Stack>
</Box>
);
};
export default CreateDistributedUptime;
@@ -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 (
<ColContainer
backgroundColor={theme.palette.chatbot.background}
sx={{ ...sx }}
>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<SmartToyIcon sx={{ color: theme.palette.chatbot.textAccent }} />
<Typography color={theme.palette.chatbot.textAccent}>Status Bot</Typography>
<Dot
color={theme.palette.chatbot.textAccent}
style={{ opacity: 0.4 }}
/>
<Typography
variant="body2"
color={theme.palette.chatbot.textAccent}
sx={{ opacity: 0.4 }}
>
{t("now")}
</Typography>
</Stack>
<Typography>{MESSAGES[Math.floor(Math.random() * MESSAGES.length)]}</Typography>
</ColContainer>
);
};
export default ChatBot;
@@ -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 (
<Stack
direction="row"
gap={theme.spacing(2)}
>
<Box>
<Button
variant="contained"
color="error"
onClick={() => setIsDeleteOpen(!isDeleteOpen)}
loading={isDeleting}
>
{t("delete")}
</Button>
</Box>
<Box>
<Button
variant="contained"
color="secondary"
onClick={() => {
navigate(`/distributed-uptime/configure/${monitorId}`);
}}
sx={{
px: theme.spacing(5),
"& svg": {
mr: theme.spacing(3),
"& path": {
stroke: theme.palette.secondary.contrastText,
},
},
}}
>
<SettingsIcon /> {t("configure")}
</Button>
</Box>
</Stack>
);
};
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 (
<Stack
alignSelf="flex-start"
direction="row"
width="100%"
gap={theme.spacing(2)}
justifyContent="flex-end"
alignItems="flex-end"
>
<Controls
isDeleting={isDeleting}
isDeleteOpen={isDeleteOpen}
setIsDeleteOpen={setIsDeleteOpen}
monitorId={monitorId}
/>
</Stack>
);
};
ControlsHeader.propTypes = {
monitorId: PropTypes.string,
isDeleting: PropTypes.bool,
isDeleteOpen: PropTypes.bool.isRequired,
setIsDeleteOpen: PropTypes.func.isRequired,
};
export default ControlsHeader;
@@ -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 (
<ColContainer sx={{ height: "100%" }}>
<Stack
direction="row"
gap={theme.spacing(4)}
>
<PulseDot color={statusColor[connectionStatus]} />
<Typography
variant="h1"
mb={theme.spacing(8)}
sx={{ alignSelf: "center" }}
>
{connectionStatus === "up" ? "Connected" : "Connecting..."}
</Typography>
</Stack>
<div
style={{
overflowX: "auto",
maxWidth: "100%",
// Optional: add a max height if you want vertical scrolling too
// maxHeight: '400px',
// overflowY: 'auto'
}}
>
<table style={{ width: "100%" }}>
<thead>
<tr>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("country")}</Typography>
</th>
<th style={{ textAlign: "left", paddingLeft: theme.spacing(4) }}>
<Typography>{t("city")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>{t("response")}</Typography>
</th>
<th style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography sx={{ whiteSpace: "nowrap" }}>{"UPT BURNED"}</Typography>
</th>
</tr>
</thead>
<tbody>
{data.map((dataPoint) => {
const countryCode = dataPoint?.countryCode?.toLowerCase() ?? null;
const flag = countryCode ? `fi fi-${countryCode}` : null;
const city = dataPoint?.city !== "" ? dataPoint?.city : "Unknown";
return (
<tr key={Math.random()}>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>
{flag ? <span className={flag} /> : null}{" "}
{countryCode?.toUpperCase() ?? "N/A"}
</Typography>
</td>
<td style={{ paddingLeft: theme.spacing(4) }}>
<Typography>{city}</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography>
{Math.floor(dataPoint.responseTime)} {t("ms")}
</Typography>
</td>
<td style={{ textAlign: "right", paddingLeft: theme.spacing(4) }}>
<Typography color={theme.palette.warning.main}>
+{dataPoint.uptBurnt}
</Typography>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</ColContainer>
);
};
export default DeviceTicker;
@@ -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"
}
}
@@ -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;
@@ -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 (
<div
ref={mapContainer}
style={{
width: width,
height: height,
minHeight: minHeight,
borderRadius: theme.spacing(4),
borderColor: theme.palette.primary.lowContrast,
borderStyle: "solid",
borderWidth: 1,
}}
/>
);
};
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;
@@ -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 (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<AreaChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
onMouseMove={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id.date"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Tooltip
cursor={{ stroke: theme.palette.primary.lowContrast }}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<Area
type="monotone"
dataKey="avgResponseTime"
stroke={theme.palette.primary.accent}
fill="url(#colorUv)"
strokeWidth={isHovered ? 2.5 : 1.5}
activeDot={{ stroke: theme.palette.background.main, r: 5 }}
/>
</AreaChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseAreaChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseAreaChart;
@@ -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 (
<ResponsiveContainer
width="100%"
minWidth={25}
height={220}
>
<BarChart
width="100%"
height="100%"
data={checks}
margin={{
top: 10,
right: 0,
left: 0,
bottom: 0,
}}
>
<defs>
<linearGradient
id="colorUv"
x1="0"
y1="0"
x2="0"
y2="1"
>
<stop
offset="0%"
stopColor={theme.palette.accent.darker}
stopOpacity={0.8}
/>
<stop
offset="100%"
stopColor={theme.palette.accent.main}
stopOpacity={0}
/>
</linearGradient>
</defs>
<CartesianGrid
stroke={theme.palette.primary.lowContrast}
strokeWidth={1}
strokeOpacity={1}
fill="transparent"
vertical={false}
/>
<Tooltip
cursor={{
stroke: theme.palette.primary.lowContrast,
fill: "transparent",
}}
content={<CustomToolTip />}
wrapperStyle={{ pointerEvents: "none" }}
/>
<XAxis
stroke={theme.palette.primary.lowContrast}
dataKey="_id.date"
tick={<CustomTick />}
minTickGap={0}
axisLine={false}
tickLine={false}
height={20}
/>
<Bar
maxBarSize={25}
dataKey="avgResponseTime"
fill="url(#colorUv)"
activeBar={{
stroke: theme.palette.accent.main,
r: 5,
}}
/>
</BarChart>
</ResponsiveContainer>
);
};
DistributedUptimeResponseBarChart.propTypes = {
checks: PropTypes.array,
};
export default DistributedUptimeResponseBarChart;
@@ -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 (
<Text
x={x}
y={y + 10}
textAnchor="middle"
fill={theme.palette.text.tertiary}
fontSize={11}
fontWeight={400}
>
{formatDateWithTz(payload?.value, "h:mm a", uiTimezone)}
</Text>
);
};
CustomTick.propTypes = {
x: PropTypes.number,
y: PropTypes.number,
payload: PropTypes.object,
index: PropTypes.number,
};
export default CustomTick;

Some files were not shown because too many files have changed in this diff Show More