diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 26956309b..16273e9d5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -20,7 +20,8 @@ const { t } = useTranslation(); ``` - [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed - [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). -- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I have no hardcoded dimensions. +- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions. - [ ] My PR is granular and targeted to one specific feature. +- [ ] I ran `npm run format` in server and client directories, which automatically formats your code. - [ ] I took a screenshot or a video and attached to this PR if there is a UI change. diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml new file mode 100644 index 000000000..d3d4c0d6b --- /dev/null +++ b/.github/workflows/check-format.yml @@ -0,0 +1,62 @@ +name: Format Check (Client & Server) +on: + pull_request: + workflow_dispatch: + +jobs: + format-client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install client dependencies + working-directory: client + run: npm ci + + - name: Check client formatting + working-directory: client + run: npm run format-check + + format-server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install server dependencies + working-directory: server + run: npm ci + + - name: Check server formatting + working-directory: server + run: npm run format-check + close-pr-if-needed: + if: always() + runs-on: ubuntu-latest + needs: [format-client, format-server] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Get PR number + id: pr + run: echo "PR_NUMBER=$(jq -r .pull_request.number "$GITHUB_EVENT_PATH")" >> $GITHUB_ENV + + - name: Close PR using GitHub CLI + if: | + needs.format-client.result == 'failure' || + needs.format-server.result == 'failure' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh pr close "$PR_NUMBER" --delete-branch --comment "❌ Formatting check failed — PR auto-closed. + Please run \`npm run format\` and push again." diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml new file mode 100644 index 000000000..f1d0dd7ce --- /dev/null +++ b/.github/workflows/deploy-images-on-release.yml @@ -0,0 +1,149 @@ +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 + - name: Extract version from tag + id: extract_tag + run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Client Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \ + -f ./docker/dist/client.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Client Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} + + docker-build-and-push-server: + needs: docker-build-and-push-client + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version + id: extract_tag + run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Server Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \ + -f ./docker/dist/server.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Server Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} + + - name: Build Mongo Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \ + -f ./docker/dist/mongoDB.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push MongoDB Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} + + - name: Build Redis Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} \ + -f ./docker/dist/redis.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Redis Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-redis:${{ steps.extract_tag.outputs.version }} + + docker-build-and-push-server-mono-multiarch: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract version + id: extract_tag + run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/dist-arm/server.Dockerfile + push: true + tags: | + ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }} + platforms: linux/amd64,linux/arm64 + labels: | + org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate + + docker-build-and-push-server-mono: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Extract version + id: extract_tag + run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Server Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \ + -f ./docker/dist-mono/server.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Server Docker image + run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml new file mode 100644 index 000000000..d3d70b90b --- /dev/null +++ b/.github/workflows/deploy-images.yml @@ -0,0 +1,133 @@ +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 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Client Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-client:latest \ + -f ./docker/dist/client.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Client Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-client:latest + + docker-build-and-push-server: + needs: docker-build-and-push-client + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Server Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-backend:latest \ + -f ./docker/dist/server.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Server Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-backend:latest + + - name: Build Mongo Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-mongo:latest \ + -f ./docker/dist/mongoDB.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push MongoDB Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-mongo:latest + + - name: Build Redis Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-redis:latest \ + -f ./docker/dist/redis.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Redis Docker image + run: | + docker push ghcr.io/bluewave-labs/checkmate-redis:latest + + docker-build-and-push-server-mono-multiarch: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push multi-arch Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./docker/dist-arm/server.Dockerfile + push: true + tags: | + ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest + platforms: linux/amd64,linux/arm64 + labels: | + org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate + + docker-build-and-push-server-mono: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build Server Docker image + run: | + docker build \ + -t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \ + -f ./docker/dist-mono/server.Dockerfile \ + --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ + . + + - name: Push Server Docker image + run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest diff --git a/.github/workflows/distribution-deploy.yml b/.github/workflows/distribution-deploy.yml deleted file mode 100644 index d4bcfc8b7..000000000 --- a/.github/workflows/distribution-deploy.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: Distribution deploy - -on: - push: - branches: ["master"] - workflow_dispatch: -jobs: - docker-build-and-push-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Client Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:frontend-dist \ - -f ./docker/dist/client.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Client Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:frontend-dist - - docker-build-and-push-server: - needs: docker-build-and-push-client - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:backend-dist \ - -f ./docker/dist/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:mongo-dist \ - -f ./docker/dist/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push MongoDB Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:mongo-dist - - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-dist \ - -f ./docker/dist/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:redis-dist diff --git a/.github/workflows/distribution-mono-deploy-arm.yml b/.github/workflows/distribution-mono-deploy-arm.yml deleted file mode 100644 index 345cab560..000000000 --- a/.github/workflows/distribution-mono-deploy-arm.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Distribution deploy - Monolithic Multiarch - -on: - push: - branches: ["master"] - workflow_dispatch: -jobs: - docker-build-and-push-server: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push multi-arch Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/dist-arm/server.Dockerfile - push: true - tags: ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch - platforms: linux/amd64,linux/arm64 - labels: | - org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate - - # - name: Build Server Docker image - # run: | - # docker build \ - # -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch \ - # -f ./docker/dist-arm/server.Dockerfile \ - # --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - # . - - # - name: Push Server Docker image - # run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono-multiarch diff --git a/.github/workflows/distribution-mono-deploy.yml b/.github/workflows/distribution-mono-deploy.yml deleted file mode 100644 index 7dafbfc21..000000000 --- a/.github/workflows/distribution-mono-deploy.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Distribution deploy - Monolithic - -on: - push: - branches: ["master"] - workflow_dispatch: -jobs: - docker-build-and-push-server: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:backend-dist-mono \ - -f ./docker/dist-mono/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:backend-dist-mono diff --git a/PULLREQUESTS.md b/PULLREQUESTS.md index 725a175b7..d1ee8eea9 100644 --- a/PULLREQUESTS.md +++ b/PULLREQUESTS.md @@ -27,6 +27,10 @@ Process. If more revisions are required after the second review we’re looking If PRs are small and manageable it is far more likely that a dev will catch bugs during the review process. If our eyes glaze over at line 400 of a 700 line PR since we’ve reached our cognitive limit we’re not going to likely miss bugs in the last 300 lines of code. -### Bonus Topic: Keep PRs focused +### Format your PRs for better readability + +Ensure you execute `npm run format` before submitting your pull requests in both the client and server directories. This command automatically applies our formatting structure, making the code easier to follow and review. + +### Keep PRs focused It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature. diff --git a/README.md b/README.md index d276af81f..c9c9073ad 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@

An open source uptime and infrastructure monitoring application

-![dashboard](https://github.com/user-attachments/assets/252d6047-522b-4576-8f14-233510e464b8) +image 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

-server +image

-uptime + image

-page speed +

image

+

+image +

+ + ## 🏗️ Tech stack @@ -109,7 +130,7 @@ If you have any questions, suggestions or comments, please use our [Discord chan We are [Alex](http://github.com/ajhollid) (team lead), [Vishnu](http://github.com/vishnusn77), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer) and [Mert](https://github.com/mertssmnoglu) helping individuals and businesses monitor their infra and servers. -We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 5800+ stars and attracted 70+ contributors from around the globe. +We pride ourselves on building strong connections with contributors at every level. Despite being a young project, Checkmate has already earned 6000+ stars and attracted 80+ contributors from around the globe. Our repo is starred by employees from **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systèmes and NEC**, so don’t hold back — jump in, contribute and learn with us! @@ -127,11 +148,13 @@ Here's how you can contribute: +[![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: diff --git a/client/package-lock.json b/client/package-lock.json index e12e76f97..4050adfbb 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,13 +10,11 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@fontsource/roboto": "^5.0.13", "@hello-pangea/dnd": "^18.0.0", "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.29.0", "@mui/x-date-pickers": "7.29.0", "@reduxjs/toolkit": "2.7.0", "axios": "^1.7.4", @@ -24,14 +22,9 @@ "flag-icons": "7.3.2", "html2canvas": "^1.4.1", "i18next": "^24.2.2", - "immutability-helper": "^3.1.1", "joi": "17.13.3", - "jwt-decode": "^4.0.0", - "maplibre-gl": "5.3.1", "mui-color-input": "^6.0.0", "react": "18.3.1", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", @@ -983,15 +976,6 @@ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", "license": "MIT" }, - "node_modules/@fontsource/roboto": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.2.5.tgz", - "integrity": "sha512-70r2UZ0raqLn5W+sPeKhqlf8wGvUXFWlofaDlcbt/S3d06+17gXKr3VNqDODB0I1ASme3dGT5OJj9NABt7OTZQ==", - "license": "OFL-1.1", - "funding": { - "url": "https://github.com/sponsors/ayuhito" - } - }, "node_modules/@hapi/hoek": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", @@ -1110,83 +1094,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mapbox/geojson-rewind": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz", - "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==", - "license": "ISC", - "dependencies": { - "get-stream": "^6.0.1", - "minimist": "^1.2.6" - }, - "bin": { - "geojson-rewind": "geojson-rewind" - } - }, - "node_modules/@mapbox/jsonlint-lines-primitives": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", - "integrity": "sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@mapbox/point-geometry": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", - "integrity": "sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==", - "license": "ISC" - }, - "node_modules/@mapbox/tiny-sdf": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-2.0.6.tgz", - "integrity": "sha512-qMqa27TLw+ZQz5Jk+RcwZGH7BQf5G/TrutJhspsca/3SHwmgKQ1iq+d3Jxz5oysPVYTGP6aXxCo5Lk9Er6YBAA==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/unitbezier": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.1.tgz", - "integrity": "sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==", - "license": "BSD-2-Clause" - }, - "node_modules/@mapbox/vector-tile": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@mapbox/vector-tile/-/vector-tile-1.3.1.tgz", - "integrity": "sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/point-geometry": "~0.1.0" - } - }, - "node_modules/@mapbox/whoots-js": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@mapbox/whoots-js/-/whoots-js-3.1.0.tgz", - "integrity": "sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==", - "license": "ISC", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@maplibre/maplibre-gl-style-spec": { - "version": "23.3.0", - "resolved": "https://registry.npmjs.org/@maplibre/maplibre-gl-style-spec/-/maplibre-gl-style-spec-23.3.0.tgz", - "integrity": "sha512-IGJtuBbaGzOUgODdBRg66p8stnwj9iDXkgbYKoYcNiiQmaez5WVRfXm4b03MCDwmZyX93csbfHFWEJJYHnn5oA==", - "license": "ISC", - "dependencies": { - "@mapbox/jsonlint-lines-primitives": "~2.0.2", - "@mapbox/unitbezier": "^0.0.1", - "json-stringify-pretty-compact": "^4.0.0", - "minimist": "^1.2.8", - "quickselect": "^3.0.0", - "rw": "^1.3.3", - "tinyqueue": "^3.0.0" - }, - "bin": { - "gl-style-format": "dist/gl-style-format.mjs", - "gl-style-migrate": "dist/gl-style-migrate.mjs", - "gl-style-validate": "dist/gl-style-validate.mjs" - } - }, "node_modules/@mui/base": { "version": "5.0.0-beta.42", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz", @@ -1861,44 +1768,6 @@ "robust-predicates": "^3.0.2" } }, - "node_modules/@mui/x-data-grid": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.29.0.tgz", - "integrity": "sha512-v3HiwcT/oqgv6xkynOd3oxH26IS0OvspgmfHtfPF9WeRsnUHcTTZ7E1EoAKjT9/Ms2f3oKKJAR6rZBZ5ymik1g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.25.7", - "@mui/utils": "^5.16.6 || ^6.0.0 || ^7.0.0", - "@mui/x-internals": "7.29.0", - "clsx": "^2.1.1", - "prop-types": "^15.8.1", - "reselect": "^5.1.1", - "use-sync-external-store": "^1.0.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/mui-org" - }, - "peerDependencies": { - "@emotion/react": "^11.9.0", - "@emotion/styled": "^11.8.1", - "@mui/material": "^5.15.14 || ^6.0.0 || ^7.0.0", - "@mui/system": "^5.15.14 || ^6.0.0 || ^7.0.0", - "react": "^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/react": { - "optional": true - }, - "@emotion/styled": { - "optional": true - } - } - }, "node_modules/@mui/x-date-pickers": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-7.29.0.tgz", @@ -2033,24 +1902,6 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@react-dnd/asap": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/asap/-/asap-5.0.2.tgz", - "integrity": "sha512-WLyfoHvxhs0V9U+GTsGilGgf2QsPl6ZZ44fnv0/b8T3nQyvzxidxsg/ZltbWssbsRDlYW8UKSQMTGotuTotZ6A==", - "license": "MIT" - }, - "node_modules/@react-dnd/invariant": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/invariant/-/invariant-4.0.2.tgz", - "integrity": "sha512-xKCTqAK/FFauOM9Ta2pswIyT3D8AQlfrYdOi/toTPEhqCuAs1v5tcJ3Y08Izh1cJ5Jchwy9SeAXmMg6zrKs2iw==", - "license": "MIT" - }, - "node_modules/@react-dnd/shallowequal": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@react-dnd/shallowequal/-/shallowequal-4.0.2.tgz", - "integrity": "sha512-/RVXdLvJxLg4QKvMoM5WlwNR9ViO9z8B/qPcc+C0Sa/teJY7QG7kJ441DwzOjMYEY7GmU4dj5EcGHIkKZiQZCA==", - "license": "MIT" - }, "node_modules/@react-spring/animated": { "version": "9.7.5", "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", @@ -2832,50 +2683,12 @@ "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", "license": "MIT" }, - "node_modules/@types/geojson": { - "version": "7946.0.16", - "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", - "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", - "license": "MIT" - }, - "node_modules/@types/geojson-vt": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/@types/geojson-vt/-/geojson-vt-3.2.5.tgz", - "integrity": "sha512-qDO7wqtprzlpe8FfQ//ClPV9xiuoh2nkIgiouIptON9w5jvD/fA4szvP9GBlDVdJ5dldAl0kX/sy3URbWwLx0g==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, - "node_modules/@types/mapbox__point-geometry": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__point-geometry/-/mapbox__point-geometry-0.1.4.tgz", - "integrity": "sha512-mUWlSxAmYLfwnRBmgYV86tgYmMIICX4kza8YnE/eIlywGe2XoOxlpVnXWwir92xRLjwyarqwpu2EJKD2pk0IUA==", - "license": "MIT" - }, - "node_modules/@types/mapbox__vector-tile": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/mapbox__vector-tile/-/mapbox__vector-tile-1.3.4.tgz", - "integrity": "sha512-bpd8dRn9pr6xKvuEBQup8pwQfD4VUyqO/2deGjfpe6AwC8YRlyEipvefyRJUSiCJTZuCb8Pl1ciVV5ekqJ96Bg==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*", - "@types/mapbox__point-geometry": "*", - "@types/pbf": "*" - } - }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", "license": "MIT" }, - "node_modules/@types/pbf": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@types/pbf/-/pbf-3.0.5.tgz", - "integrity": "sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==", - "license": "MIT" - }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -2911,15 +2724,6 @@ "@types/react": "*" } }, - "node_modules/@types/supercluster": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/supercluster/-/supercluster-7.1.3.tgz", - "integrity": "sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==", - "license": "MIT", - "dependencies": { - "@types/geojson": "*" - } - }, "node_modules/@types/use-sync-external-store": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", @@ -3771,26 +3575,6 @@ "node": ">=0.4.0" } }, - "node_modules/dnd-core": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dnd-core/-/dnd-core-16.0.1.tgz", - "integrity": "sha512-HK294sl7tbw6F6IeuK16YSBUoorvHpY8RHO+9yFfaJyCDVb6n7PRcezrOEOa2SBCqiYpemh5Jx20ZcjKdFAVng==", - "license": "MIT", - "dependencies": { - "@react-dnd/asap": "^5.0.1", - "@react-dnd/invariant": "^4.0.1", - "redux": "^4.2.0" - } - }, - "node_modules/dnd-core/node_modules/redux": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz", - "integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.9.2" - } - }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -3838,12 +3622,6 @@ "node": ">= 0.4" } }, - "node_modules/earcut": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/earcut/-/earcut-3.0.1.tgz", - "integrity": "sha512-0l1/0gOjESMeQyYaK5IDiPNvFeu93Z/cO0TjZh9eZ1vyCtZnA7KMZ8rQggpsJHIbGSdrqYq9OhuveadOVHCshw==", - "license": "ISC" - }, "node_modules/electron-to-chromium": { "version": "1.5.155", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", @@ -4370,6 +4148,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, "license": "MIT" }, "node_modules/fast-equals": { @@ -4590,12 +4369,6 @@ "node": ">=6.9.0" } }, - "node_modules/geojson-vt": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/geojson-vt/-/geojson-vt-4.0.2.tgz", - "integrity": "sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==", - "license": "ISC" - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -4633,18 +4406,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -4663,12 +4424,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/gl-matrix": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/gl-matrix/-/gl-matrix-3.4.3.tgz", - "integrity": "sha512-wcCp8vu8FT22BnvKVPjXa/ICBWRq/zjFfdofZy1WSpQZpphblv12/bOQLBC1rMM7SGOFS9ltVmKOHil5+Ml7gA==", - "license": "MIT" - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -4704,44 +4459,6 @@ "node": ">=10.13.0" } }, - "node_modules/global-prefix": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-4.0.0.tgz", - "integrity": "sha512-w0Uf9Y9/nyHinEk5vMJKRie+wa4kR5hmDbEhGGds/kG1PwGLLHKRoNMeJOyCQjjBkANlnScqgzcFwGHgmgLkVA==", - "license": "MIT", - "dependencies": { - "ini": "^4.1.3", - "kind-of": "^6.0.3", - "which": "^4.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/global-prefix/node_modules/isexe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", - "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "license": "ISC", - "engines": { - "node": ">=16" - } - }, - "node_modules/global-prefix/node_modules/which": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", - "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, - "engines": { - "node": "^16.13.0 || >=18.0.0" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -4946,26 +4663,6 @@ } } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -4986,12 +4683,6 @@ "url": "https://opencollective.com/immer" } }, - "node_modules/immutability-helper": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/immutability-helper/-/immutability-helper-3.1.1.tgz", - "integrity": "sha512-Q0QaXjPjwIju/28TsugCHNEASwoCcJSyJV3uO1sOIQGI0jKgm9f41Lvz0DZj3n46cNCyAZTsEYoY4C2bVRUzyQ==", - "license": "MIT" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -5037,15 +4728,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ini": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", - "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -5555,12 +5237,6 @@ "dev": true, "license": "MIT" }, - "node_modules/json-stringify-pretty-compact": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/json-stringify-pretty-compact/-/json-stringify-pretty-compact-4.0.0.tgz", - "integrity": "sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==", - "license": "MIT" - }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", @@ -5589,21 +5265,6 @@ "node": ">=4.0" } }, - "node_modules/jwt-decode": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz", - "integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/kdbush": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", - "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==", - "license": "ISC" - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -5614,15 +5275,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -5702,47 +5354,6 @@ "yallist": "^3.0.2" } }, - "node_modules/maplibre-gl": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.3.1.tgz", - "integrity": "sha512-Ihx+oUUSsZkjMou1Cw5J6silE+5OtFFQSPslWF9+7v4yFC/XDHrpsORYO9lWE4KZI0djCEUpZQJpkpnMArAbeA==", - "license": "BSD-3-Clause", - "dependencies": { - "@mapbox/geojson-rewind": "^0.5.2", - "@mapbox/jsonlint-lines-primitives": "^2.0.2", - "@mapbox/point-geometry": "^0.1.0", - "@mapbox/tiny-sdf": "^2.0.6", - "@mapbox/unitbezier": "^0.0.1", - "@mapbox/vector-tile": "^1.3.1", - "@mapbox/whoots-js": "^3.1.0", - "@maplibre/maplibre-gl-style-spec": "^23.1.0", - "@types/geojson": "^7946.0.16", - "@types/geojson-vt": "3.2.5", - "@types/mapbox__point-geometry": "^0.1.4", - "@types/mapbox__vector-tile": "^1.3.4", - "@types/pbf": "^3.0.5", - "@types/supercluster": "^7.1.3", - "earcut": "^3.0.1", - "geojson-vt": "^4.0.2", - "gl-matrix": "^3.4.3", - "global-prefix": "^4.0.0", - "kdbush": "^4.0.2", - "murmurhash-js": "^1.0.0", - "pbf": "^3.3.0", - "potpack": "^2.0.0", - "quickselect": "^3.0.0", - "supercluster": "^8.0.1", - "tinyqueue": "^3.0.0", - "vt-pbf": "^3.1.3" - }, - "engines": { - "node": ">=16.14.0", - "npm": ">=8.1.0" - }, - "funding": { - "url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1" - } - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5786,15 +5397,6 @@ "node": "*" } }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5823,12 +5425,6 @@ } } }, - "node_modules/murmurhash-js": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/murmurhash-js/-/murmurhash-js-1.0.0.tgz", - "integrity": "sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==", - "license": "MIT" - }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -6130,19 +5726,6 @@ "node": ">=8" } }, - "node_modules/pbf": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pbf/-/pbf-3.3.0.tgz", - "integrity": "sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "ieee754": "^1.1.12", - "resolve-protobuf-schema": "^2.1.0" - }, - "bin": { - "pbf": "bin/pbf" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -6199,12 +5782,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/potpack": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/potpack/-/potpack-2.0.0.tgz", - "integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw==", - "license": "ISC" - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -6248,12 +5825,6 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, - "node_modules/protocol-buffers-schema": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz", - "integrity": "sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==", - "license": "MIT" - }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -6291,12 +5862,6 @@ ], "license": "MIT" }, - "node_modules/quickselect": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/quickselect/-/quickselect-3.0.0.tgz", - "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", - "license": "ISC" - }, "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", @@ -6315,45 +5880,6 @@ "node": ">=0.10.0" } }, - "node_modules/react-dnd": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd/-/react-dnd-16.0.1.tgz", - "integrity": "sha512-QeoM/i73HHu2XF9aKksIUuamHPDvRglEwdHL4jsp784BgUuWcg6mzfxT0QDdQz8Wj0qyRKx2eMg8iZtWvU4E2Q==", - "license": "MIT", - "dependencies": { - "@react-dnd/invariant": "^4.0.1", - "@react-dnd/shallowequal": "^4.0.1", - "dnd-core": "^16.0.1", - "fast-deep-equal": "^3.1.3", - "hoist-non-react-statics": "^3.3.2" - }, - "peerDependencies": { - "@types/hoist-non-react-statics": ">= 3.3.1", - "@types/node": ">= 12", - "@types/react": ">= 16", - "react": ">= 16.14" - }, - "peerDependenciesMeta": { - "@types/hoist-non-react-statics": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-dnd-html5-backend": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/react-dnd-html5-backend/-/react-dnd-html5-backend-16.0.1.tgz", - "integrity": "sha512-Wu3dw5aDJmOGw8WjH1I1/yTH+vlXEL4vmjk5p+MHxP8HuHJS1lAGeIdG/hze1AvNeXWo/JgULV87LyQOr+r5jw==", - "license": "MIT", - "dependencies": { - "dnd-core": "^16.0.1" - } - }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -6658,15 +6184,6 @@ "node": ">=4" } }, - "node_modules/resolve-protobuf-schema": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/resolve-protobuf-schema/-/resolve-protobuf-schema-2.1.0.tgz", - "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", - "license": "MIT", - "dependencies": { - "protocol-buffers-schema": "^3.3.1" - } - }, "node_modules/reusify": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", @@ -6764,12 +6281,6 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/rw": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", - "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", - "license": "BSD-3-Clause" - }, "node_modules/safe-array-concat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", @@ -7149,15 +6660,6 @@ "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", "license": "MIT" }, - "node_modules/supercluster": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", - "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", - "license": "ISC", - "dependencies": { - "kdbush": "^4.0.2" - } - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -7211,12 +6713,6 @@ "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", "license": "MIT" }, - "node_modules/tinyqueue": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", - "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==", - "license": "ISC" - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -7508,17 +7004,6 @@ "node": ">=0.10.0" } }, - "node_modules/vt-pbf": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/vt-pbf/-/vt-pbf-3.1.3.tgz", - "integrity": "sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==", - "license": "MIT", - "dependencies": { - "@mapbox/point-geometry": "0.1.0", - "@mapbox/vector-tile": "^1.3.1", - "pbf": "^3.2.1" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/client/package.json b/client/package.json index 238ced4ac..83dfe8407 100644 --- a/client/package.json +++ b/client/package.json @@ -15,13 +15,11 @@ "dependencies": { "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", - "@fontsource/roboto": "^5.0.13", "@hello-pangea/dnd": "^18.0.0", "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", "@mui/x-charts": "^7.5.1", - "@mui/x-data-grid": "7.29.0", "@mui/x-date-pickers": "7.29.0", "@reduxjs/toolkit": "2.7.0", "axios": "^1.7.4", @@ -29,14 +27,9 @@ "flag-icons": "7.3.2", "html2canvas": "^1.4.1", "i18next": "^24.2.2", - "immutability-helper": "^3.1.1", "joi": "17.13.3", - "jwt-decode": "^4.0.0", - "maplibre-gl": "5.3.1", "mui-color-input": "^6.0.0", "react": "18.3.1", - "react-dnd": "^16.0.1", - "react-dnd-html5-backend": "^16.0.1", "react-dom": "^18.2.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", diff --git a/client/src/App.jsx b/client/src/App.jsx index 7fb63ee61..b599890f1 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Routes } from "./Routes"; import WalletProvider from "./Components/WalletProvider"; +import AppLayout from "./Components/Layouts/AppLayout"; function App() { const mode = useSelector((state) => state.ui.mode); @@ -27,17 +28,10 @@ function App() { - { - return { - body: { - backgroundImage: `radial-gradient(circle, ${palette.gradient.color1}, ${palette.gradient.color2}, ${palette.gradient.color3}, ${palette.gradient.color4}, ${palette.gradient.color5})`, - color: palette.primary.contrastText, - }, - }; - }} - /> - + + + + diff --git a/client/src/Components/ActionsMenu/index.jsx b/client/src/Components/ActionsMenu/index.jsx index 4d5ab7c01..1b590a471 100644 --- a/client/src/Components/ActionsMenu/index.jsx +++ b/client/src/Components/ActionsMenu/index.jsx @@ -1,17 +1,18 @@ +// Components +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Settings from "../../assets/icons/settings-bold.svg?react"; +import Dialog from "../../Components/Dialog"; + +// Utils import { useState } from "react"; -import { useSelector, useDispatch } from "react-redux"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { createToast } from "../../Utils/toastUtils"; -import { logger } from "../../Utils/Logger"; -import { IconButton, Menu, MenuItem } from "@mui/material"; -import { - deleteUptimeMonitor, - pauseUptimeMonitor, -} from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; -import Settings from "../../assets/icons/settings-bold.svg?react"; + import PropTypes from "prop-types"; -import Dialog from "../../Components/Dialog"; +import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks"; const ActionsMenu = ({ monitor, @@ -23,38 +24,27 @@ const ActionsMenu = ({ const [anchorEl, setAnchorEl] = useState(null); const [actions, setActions] = useState({}); const [isOpen, setIsOpen] = useState(false); - const dispatch = useDispatch(); const theme = useTheme(); - const { isLoading } = useSelector((state) => state.uptimeMonitors); + const [pauseMonitor, isPausing, error] = usePauseMonitor(); + const [deleteMonitor, isDeleting] = useDeleteMonitor(); const handleRemove = async (event) => { event.preventDefault(); event.stopPropagation(); let monitor = { _id: actions.id }; - const action = await dispatch(deleteUptimeMonitor({ monitor })); - if (action.meta.requestStatus === "fulfilled") { - setIsOpen(false); // close modal - updateRowCallback(); - createToast({ body: "Monitor deleted successfully." }); - } else { - createToast({ body: "Failed to delete monitor." }); - } + await deleteMonitor({ monitor }); + updateRowCallback(); }; const handlePause = async () => { try { setIsLoading(true); - const action = await dispatch(pauseUptimeMonitor({ monitorId: monitor._id })); - if (pauseUptimeMonitor.fulfilled.match(action)) { - const state = action?.payload?.data.isActive === false ? "resumed" : "paused"; - createToast({ body: `Monitor ${state} successfully.` }); - pauseCallback(); - } else { - throw new Error(action?.error?.message ?? "Failed to pause monitor."); - } + await pauseMonitor({ monitorId: monitor._id }); + pauseCallback(); } catch (error) { - logger.error("Error pausing monitor:", monitor._id, error); createToast({ body: "Failed to pause monitor." }); + } finally { + setIsLoading(false); } }; @@ -210,7 +200,7 @@ const ActionsMenu = ({ e.stopPropagation(); handleRemove(e); }} - isLoading={isLoading} + isLoading={isDeleting} modelTitle="modal-delete-monitor" modelDescription="delete-monitor-confirmation" /> diff --git a/client/src/Components/Breadcrumbs/index.jsx b/client/src/Components/Breadcrumbs/index.jsx index 0ebd8dab1..bf04afa2e 100644 --- a/client/src/Components/Breadcrumbs/index.jsx +++ b/client/src/Components/Breadcrumbs/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router"; +import { useNavigate } from "react-router-dom"; import ArrowRight from "../../assets/icons/right-arrow.svg?react"; import "./index.css"; diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/Charts/ChartBox/index.jsx index 6b74ace68..2c13ece87 100644 --- a/client/src/Components/Charts/ChartBox/index.jsx +++ b/client/src/Components/Charts/ChartBox/index.jsx @@ -43,7 +43,6 @@ const ChartBox = ({ > { const location = useLocation(); const navigate = useNavigate(); - // Debugging: Log the current theme mode - console.log("Current theme mode:", mode); - const logoSrc = mode === "light" ? "/images/prism-black.png" : "/images/prism-white.png"; diff --git a/client/src/Components/Dialog/index.jsx b/client/src/Components/Dialog/index.jsx index e07597df4..678eb8721 100644 --- a/client/src/Components/Dialog/index.jsx +++ b/client/src/Components/Dialog/index.jsx @@ -57,7 +57,7 @@ Dialog.propTypes = { onCancel: PropTypes.func.isRequired, confirmationButtonLabel: PropTypes.string.isRequired, onConfirm: PropTypes.func.isRequired, - isLoading: PropTypes.bool.isRequired, + isLoading: PropTypes.bool, }; export default Dialog; diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/Fallback/index.jsx index b00ddbecc..b77432d58 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/Fallback/index.jsx @@ -11,7 +11,7 @@ import { useSelector } from "react-redux"; import Alert from "../Alert"; import { useTranslation } from "react-i18next"; import "./index.css"; -import { useFetchSettings } from "../../Hooks/useFetchSettings"; +import { useFetchSettings } from "../../Hooks/settingsHooks"; import { useState } from "react"; /** * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. @@ -39,7 +39,11 @@ const Fallback = ({ const { t } = useTranslation(); const [settingsData, setSettingsData] = useState(undefined); - const [isLoading, error] = useFetchSettings({ setSettingsData }); + const [isLoading, error] = useFetchSettings({ + setSettingsData, + setIsApiKeySet: () => {}, + setIsEmailPasswordSet: () => {}, + }); // Custom warning message with clickable link const renderWarningMessage = () => { return ( diff --git a/client/src/Components/GenericFallback/NetworkError.jsx b/client/src/Components/GenericFallback/NetworkError.jsx index b26037a57..ddffa1c31 100644 --- a/client/src/Components/GenericFallback/NetworkError.jsx +++ b/client/src/Components/GenericFallback/NetworkError.jsx @@ -1,8 +1,11 @@ import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; const NetworkError = () => { const theme = useTheme(); + const { t } = useTranslation(); + return ( <> { marginY={theme.spacing(4)} color={theme.palette.primary.contrastTextTertiary} > - Network error + {t("common.toasts.networkError")} - Please check your connection + {t("common.toasts.checkConnection")} ); }; diff --git a/client/src/Components/HOC/withAdminCheck.jsx b/client/src/Components/HOC/withAdminCheck.jsx index 8edf7a02b..458330a1f 100644 --- a/client/src/Components/HOC/withAdminCheck.jsx +++ b/client/src/Components/HOC/withAdminCheck.jsx @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { logger } from "../../Utils/Logger"; @@ -7,23 +7,35 @@ import { networkService } from "../../main"; const withAdminCheck = (WrappedComponent) => { const WithAdminCheck = (props) => { const navigate = useNavigate(); + const [isChecking, setIsChecking] = useState(true); + const [superAdminExists, setSuperAdminExists] = useState(false); useEffect(() => { networkService .doesSuperAdminExist() .then((response) => { - if (response.data.data === true) { + if (response?.data?.data === true) { navigate("/login"); + } else { + setSuperAdminExists(false); } }) .catch((error) => { logger.error(error); + }) + .finally(() => { + setIsChecking(false); }); }, [navigate]); + + if (isChecking) { + return null; + } + return ( ); }; diff --git a/client/src/Components/Inputs/Checkbox/index.jsx b/client/src/Components/Inputs/Checkbox/index.jsx index 58b83c7c7..1497c44fb 100644 --- a/client/src/Components/Inputs/Checkbox/index.jsx +++ b/client/src/Components/Inputs/Checkbox/index.jsx @@ -105,7 +105,7 @@ const Checkbox = ({ }; Checkbox.propTypes = { - id: PropTypes.string.isRequired, + id: PropTypes.string, name: PropTypes.string, label: PropTypes.oneOfType([PropTypes.string, PropTypes.node]).isRequired, size: PropTypes.oneOf(["small", "medium", "large"]), diff --git a/client/src/Components/Inputs/Radio/index.jsx b/client/src/Components/Inputs/Radio/index.jsx index 8c1c06b38..646b38e93 100644 --- a/client/src/Components/Inputs/Radio/index.jsx +++ b/client/src/Components/Inputs/Radio/index.jsx @@ -16,26 +16,27 @@ import "./index.css"; * size="small" * /> * - * @param {Object} props - The component props. - * @param {string} props.id - The id of the radio button. - * @param {string} props.title - The title of the radio button. - * @param {string} [props.desc] - The description of the radio button. - * @param {string} [props.size="small"] - The size of the radio button. + * @param {Object} props - The component + * @param {string} id - The id of the radio button. + * @param {string} title - The title of the radio button. + * @param {string} [desc] - The description of the radio button. + * @param {string} [size="small"] - The size of the radio button. * @returns {JSX.Element} - The rendered Radio component. */ -const Radio = (props) => { +const Radio = ({ name, checked, value, id, size, title, desc, onChange }) => { const theme = useTheme(); return ( } sx={{ color: "transparent", @@ -49,16 +50,16 @@ const Radio = (props) => { }} /> } - onChange={props.onChange} + onChange={onChange} label={ <> - {props.title} + {title} - {props.desc} + {desc} } @@ -81,9 +82,14 @@ const Radio = (props) => { }; Radio.propTypes = { - title: PropTypes.string.isRequired, + title: PropTypes.string, desc: PropTypes.string, size: PropTypes.string, + name: PropTypes.string, + checked: PropTypes.bool, + value: PropTypes.string, + id: PropTypes.string, + onChange: PropTypes.func, }; export default Radio; diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx index 86ce57779..4624b2cba 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/Inputs/Search/index.jsx @@ -1,7 +1,17 @@ import PropTypes from "prop-types"; -import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material"; +import { + Box, + ListItem, + Autocomplete, + TextField, + Stack, + Typography, + Checkbox, +} from "@mui/material"; import { useTheme } from "@emotion/react"; import SearchIcon from "../../../assets/icons/search.svg?react"; +import React, { useEffect } from "react"; +import { useTranslation } from "react-i18next"; /** * Search component using Material UI's Autocomplete. @@ -60,24 +70,72 @@ const Search = ({ onBlur, }) => { const theme = useTheme(); + const { t } = useTranslation(); + const [selectAll, setSelectAll] = React.useState(false); + + const [open, setOpen] = React.useState(false); + const enhancedOptions = React.useMemo(() => { + return multiple && isAdorned + ? [ + { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" }, + ...options, + ] + : options; + }, [multiple, isAdorned, options, filteredBy]); + const isOptionSelected = (option) => { + if (!multiple && !isAdorned) return false; + if (Array.isArray(value)) { + return value.some((item) => item._id === option._id); + } + return false; + }; + const handleSelectAll = (isSelectAll) => { + const newValue = isSelectAll ? [...options] : []; + handleChange(newValue); + setSelectAll(isSelectAll); + }; + useEffect(() => { + const allSelected = + Array.isArray(value) && Array.isArray(options) && value.length === options.length; + if (selectAll !== allSelected) setSelectAll(allSelected); + }, [value, options]); return ( setOpen(true)} + onClose={(event, reason) => { + if (reason === "blur" || reason === "escape") { + setOpen(false); + } + }} inputValue={inputValue} onInputChange={(_, newValue) => { handleInputChange(newValue); }} onChange={(_, newValue) => { - handleChange(newValue); + if (multiple && isAdorned) { + const hasSelectAllSelected = + Array.isArray(newValue) && newValue.some((item) => item.isSelectAll); + if (hasSelectAllSelected) { + handleSelectAll(!selectAll); + } else { + handleChange(newValue); + setSelectAll(Array.isArray(newValue) && newValue.length === options.length); + } + } else { + handleChange(newValue); + setOpen(false); + } }} fullWidth freeSolo disabled={disabled} disableClearable - options={options} + options={enhancedOptions} getOptionLabel={(option) => option[filteredBy]} isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier renderInput={(params) => ( @@ -102,27 +160,7 @@ const Search = ({ ...(endAdornment && { endAdornment: endAdornment }), }, }} - sx={{ - "& fieldset": { - borderColor: theme.palette.primary.lowContrast, - borderRadius: theme.shape.borderRadius, - }, - "& .MuiOutlinedInput-root:hover:not(:has(input:focus)):not(:has(textarea:focus)) fieldset": - { - borderColor: theme.palette.primary.lowContrast, - }, - "& .MuiOutlinedInput-root": { - paddingY: 0, - }, - "& .MuiAutocomplete-tag": { - // CAIO_REVIEW - color: theme.palette.primary.contrastText, - backgroundColor: theme.palette.primary.lowContrast, - }, - "& .MuiChip-deleteIcon": { - color: theme.palette.primary.contrastText, // CAIO_REVIEW - }, - }} + sx={{}} /> {error && ( )} filterOptions={(options, { inputValue }) => { + if (inputValue.trim() === "" && multiple && isAdorned) { + return enhancedOptions; + } const filtered = options.filter((option) => option[filteredBy].toLowerCase().includes(inputValue.toLowerCase()) ); @@ -156,6 +197,7 @@ const Search = ({ const { key, ...optionProps } = props; const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined; const port = option["port"]; + const selected = isOptionSelected(option); return ( + {multiple && isAdorned && !option.noOptions && ( + + )} {option[filteredBy] + (hasSecondaryLabel ? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})` diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/Inputs/Select/index.jsx index a31fedf66..679a5b3b5 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/Inputs/Select/index.jsx @@ -12,9 +12,9 @@ import "./index.css"; * @param {string} props.placeholder - The label of the select element. * @param {string} props.placeholder - The placeholder text when no option is selected. * @param {boolean} props.isHidden - Whether the placeholder should be hidden. - * @param {string} props.value - The currently selected value. + * @param {(string | number | boolean)} props.value - The currently selected value. * @param {object[]} props.items - The array of items to populate in the select dropdown. - * @param {(string | number)} props.items._id - The unique identifier of each item. + * @param {(string | number | boolean)} props.items._id - The unique identifier of each item. * @param {string} props.items.name - The display name of each item. * @param {function} props.onChange - The function to handle onChange event. * @param {object} props.sx - The custom styles object for MUI Select component. @@ -161,11 +161,13 @@ Select.propTypes = { label: PropTypes.string, placeholder: PropTypes.string, isHidden: PropTypes.bool, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) + .isRequired, items: PropTypes.arrayOf( PropTypes.shape({ _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) .isRequired, + name: PropTypes.string.isRequired, }) ).isRequired, diff --git a/client/src/Components/Layouts/AppLayout/index.jsx b/client/src/Components/Layouts/AppLayout/index.jsx new file mode 100644 index 000000000..564012d72 --- /dev/null +++ b/client/src/Components/Layouts/AppLayout/index.jsx @@ -0,0 +1,30 @@ +import Box from "@mui/material/Box"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import BackgroundSVG from "../../../assets/Images/background.svg"; + +const AppLayout = ({ children }) => { + const theme = useTheme(); + + return ( + + {children} + + ); +}; + +AppLayout.propTypes = { + children: PropTypes.node, +}; + +export default AppLayout; diff --git a/client/src/Components/Link/index.jsx b/client/src/Components/Link/index.jsx index 7a623a3b0..987c56f37 100644 --- a/client/src/Components/Link/index.jsx +++ b/client/src/Components/Link/index.jsx @@ -1,4 +1,6 @@ import { Link as MuiLink, useTheme } from "@mui/material"; +import { Link as RouterLink } from "react-router-dom"; + import PropTypes from "prop-types"; /** @@ -10,7 +12,7 @@ import PropTypes from "prop-types"; * @returns {JSX.Element} */ -const Link = ({ level, label, url }) => { +const Link = ({ level, label, url, external = true }) => { const theme = useTheme(); const levelConfig = { @@ -49,11 +51,12 @@ const Link = ({ level, label, url }) => { const { sx, color } = levelConfig[level]; return ( {label} @@ -64,6 +67,7 @@ Link.propTypes = { url: PropTypes.string.isRequired, level: PropTypes.oneOf(["primary", "secondary", "tertiary", "error"]), label: PropTypes.string.isRequired, + external: PropTypes.bool, }; export default Link; diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx new file mode 100644 index 000000000..ac5d89a47 --- /dev/null +++ b/client/src/Components/MonitorActions/index.jsx @@ -0,0 +1,116 @@ +import * as React from "react"; +import Button from "@mui/material/Button"; +import ButtonGroup from "@mui/material/ButtonGroup"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import ClickAwayListener from "@mui/material/ClickAwayListener"; +import Grow from "@mui/material/Grow"; +import Paper from "@mui/material/Paper"; +import Popper from "@mui/material/Popper"; +import MenuItem from "@mui/material/MenuItem"; +import MenuList from "@mui/material/MenuList"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../Utils/toastUtils"; +import { useExportMonitors } from "../../Hooks/monitorHooks"; + +const MonitorActions = ({ isLoading }) => { + const [open, setOpen] = React.useState(false); + const anchorRef = React.useRef(null); + const [selectedIndex, setSelectedIndex] = React.useState(0); + const navigate = useNavigate(); + const { t } = useTranslation(); + const [exportMonitors, isExporting] = useExportMonitors(); + + const options = [t("monitorActions.import"), t("monitorActions.export")]; + + const handleClick = async () => { + if (selectedIndex === 0) { + // Import + navigate("/uptime/bulk-import"); + } else { + // Export + const [success, error] = await exportMonitors(); + if (!success) { + createToast({ body: error || t("export.failed") }); + } + } + }; + + const handleMenuItemClick = (event, index) => { + setSelectedIndex(index); + setOpen(false); + }; + + const handleToggle = () => { + setOpen((prevOpen) => !prevOpen); + }; + + const handleClose = (event) => { + if (anchorRef.current && anchorRef.current.contains(event.target)) { + return; + } + setOpen(false); + }; + + return ( + + + + + + + {({ TransitionProps, placement }) => ( + + + + + {options.map((option, index) => ( + handleMenuItemClick(event, index)} + > + {option} + + ))} + + + + + )} + + + ); +}; + +export default MonitorActions; diff --git a/client/src/Components/MonitorCountHeader/index.jsx b/client/src/Components/MonitorCountHeader/index.jsx index 0b3478df5..7610a9d5c 100644 --- a/client/src/Components/MonitorCountHeader/index.jsx +++ b/client/src/Components/MonitorCountHeader/index.jsx @@ -4,14 +4,14 @@ import { useTheme } from "@emotion/react"; import SkeletonLayout from "./skeleton"; const MonitorCountHeader = ({ - shouldRender = true, + isLoading = false, monitorCount, heading = "monitors", sx, children, }) => { const theme = useTheme(); - if (!shouldRender) return ; + if (isLoading) return ; if (monitorCount === 1) { heading = "monitor"; @@ -42,7 +42,7 @@ const MonitorCountHeader = ({ }; MonitorCountHeader.propTypes = { - shouldRender: PropTypes.bool, + isLoading: PropTypes.bool, monitorCount: PropTypes.number, heading: PropTypes.string, children: PropTypes.node, diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/MonitorCreateHeader/index.jsx index b630cb259..f2b6bbce4 100644 --- a/client/src/Components/MonitorCreateHeader/index.jsx +++ b/client/src/Components/MonitorCreateHeader/index.jsx @@ -3,6 +3,7 @@ import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; +import MonitorActions from "../MonitorActions"; const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => { const navigate = useNavigate(); @@ -28,18 +29,7 @@ const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath > {label || t("createNew")} - {bulkPath && ( - - )} + {bulkPath && } ); }; diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx index aa9ef91b9..5ad665069 100644 --- a/client/src/Components/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx @@ -11,10 +11,10 @@ import EmailIcon from "@mui/icons-material/Email"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { usePauseMonitor } from "../../Hooks/useMonitorControls"; +import { usePauseMonitor } from "../../Hooks/monitorHooks"; import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; import { useTranslation } from "react-i18next"; - +import { useTestAllNotifications } from "../../Hooks/useNotifications"; /** * MonitorDetailsControlHeader component displays the control header for monitor details. * It includes status display, pause/resume button, and a configure button for admins. @@ -38,12 +38,12 @@ const MonitorDetailsControlHeader = ({ const navigate = useNavigate(); const theme = useTheme(); const { t } = useTranslation(); - const [pauseMonitor, isPausing, error] = usePauseMonitor({ - monitorId: monitor?._id, - triggerUpdate, - }); + const [pauseMonitor, isPausing, error] = usePauseMonitor(); - const [isSending, emailError, sendTestEmail] = useSendTestEmail(); + // const [isSending, emailError, sendTestEmail] = useSendTestEmail(); + + const [testAllNotifications, isSending, errorAllNotifications] = + useTestAllNotifications(); if (isLoading) { return ; @@ -66,10 +66,13 @@ const MonitorDetailsControlHeader = ({ loading={isSending} startIcon={} onClick={() => { - sendTestEmail(); + testAllNotifications({ monitorId: monitor?._id }); + }} + sx={{ + whiteSpace: "nowrap", }} > - {t("sendTestEmail")} + {t("sendTestNotifications")} - - ); -}; - -ConfigButton.propTypes = { - shouldRender: PropTypes.bool, - monitorId: PropTypes.string.isRequired, - path: PropTypes.string.isRequired, -}; - -export default ConfigButton; diff --git a/client/src/Components/MonitorStatusHeader/index.jsx b/client/src/Components/MonitorStatusHeader/index.jsx deleted file mode 100644 index edadfb8df..000000000 --- a/client/src/Components/MonitorStatusHeader/index.jsx +++ /dev/null @@ -1,59 +0,0 @@ -import { Stack, Typography } from "@mui/material"; -import PulseDot from "../Animated/PulseDot"; -import Dot from "../Dot"; -import { useTheme } from "@emotion/react"; -import useUtils from "../../Pages/Uptime/Monitors/Hooks/useUtils"; -import { formatDurationRounded } from "../../Utils/timeUtils"; -import ConfigButton from "./ConfigButton"; -import SkeletonLayout from "./skeleton"; -import PropTypes from "prop-types"; - -const MonitorStatusHeader = ({ path, isLoading = false, isAdmin, monitor }) => { - const theme = useTheme(); - const { statusColor, determineState } = useUtils(); - if (isLoading) { - return ; - } - - return ( - - - {monitor?.name} - - - - {monitor?.url?.replace(/^https?:\/\//, "") || "..."} - - - - Checking every {formatDurationRounded(monitor?.interval)}. - - - - - - ); -}; - -MonitorStatusHeader.propTypes = { - path: PropTypes.string.isRequired, - isLoading: PropTypes.bool, - isAdmin: PropTypes.bool, - monitor: PropTypes.object, -}; - -export default MonitorStatusHeader; diff --git a/client/src/Components/MonitorStatusHeader/skeleton.jsx b/client/src/Components/MonitorStatusHeader/skeleton.jsx deleted file mode 100644 index 64dc0547f..000000000 --- a/client/src/Components/MonitorStatusHeader/skeleton.jsx +++ /dev/null @@ -1,23 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/NotificationConfig/index.jsx new file mode 100644 index 000000000..d3707eae4 --- /dev/null +++ b/client/src/Components/NotificationConfig/index.jsx @@ -0,0 +1,106 @@ +// Components +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Divider from "@mui/material/Divider"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import Search from "../Inputs/Search"; + +// Utils +import { useState, useEffect } from "react"; +import { useTheme } from "@mui/material/styles"; +import PropTypes from "prop-types"; + +const NotificationConfig = ({ notifications, setMonitor, setNotifications }) => { + // Local state + const [notificationsSearch, setNotificationsSearch] = useState(""); + const [selectedNotifications, setSelectedNotifications] = useState([]); + + const handleSearch = (value) => { + setSelectedNotifications(value); + setMonitor((prev) => { + return { + ...prev, + notifications: value.map((notification) => notification._id), + }; + }); + }; + + // Handlers + const handleDelete = (id) => { + const updatedNotifications = selectedNotifications.filter( + (notification) => notification._id !== id + ); + + setSelectedNotifications(updatedNotifications); + setMonitor((prev) => { + return { + ...prev, + notifications: updatedNotifications.map((notification) => notification._id), + }; + }); + }; + + // Setup + const theme = useTheme(); + + useEffect(() => { + if (setNotifications) { + const toSet = setNotifications.map((notification) => { + return notifications.find((n) => n._id === notification); + }); + setSelectedNotifications(toSet); + } + }, [setNotifications, notifications]); + + return ( + + { + handleSearch(value); + }} + /> + + {selectedNotifications.map((notification, index) => ( + + + {notification.notificationName} + + { + handleDelete(notification._id); + }} + sx={{ cursor: "pointer" }} + /> + {index < selectedNotifications.length - 1 && } + + ))} + + + ); +}; + +NotificationConfig.propTypes = { + notifications: PropTypes.array, + setMonitor: PropTypes.func, + setNotifications: PropTypes.array, +}; + +export default NotificationConfig; diff --git a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx b/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx deleted file mode 100644 index 4a59859fe..000000000 --- a/client/src/Components/ProtectedDistributedUptimeRoute/index.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Navigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -/** - * @param {Object} props - The props passed to the ProtectedDistributedUptimeRoute component. - * @param {React.ReactNode} props.children - The children to render if the user is authenticated. - * @returns {React.ReactElement} The children wrapped in a protected route or a redirect to the login page. - */ - -const ProtectedDistributedUptimeRoute = ({ children }) => { - const distributedUptimeEnabled = useSelector( - (state) => state.ui.distributedUptimeEnabled - ); - - return distributedUptimeEnabled === true ? ( - children - ) : ( - - ); -}; - -ProtectedDistributedUptimeRoute.propTypes = { - children: PropTypes.node.isRequired, -}; - -export default ProtectedDistributedUptimeRoute; diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/Sidebar/index.jsx index f78ac62f0..06a140c54 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/Sidebar/index.jsx @@ -23,7 +23,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 +36,10 @@ 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 "./index.css"; // Utils @@ -50,22 +49,22 @@ 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"; const getMenu = (t) => [ { name: t("menu.uptime"), path: "uptime", icon: }, { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + { name: t("menu.infrastructure"), path: "infrastructure", icon: }, { - name: t("menu.distributedUptime"), - path: "distributed-uptime", - icon: , + name: t("menu.notifications"), + path: "notifications", + icon: , }, { name: t("menu.incidents"), path: "incidents", icon: }, { name: t("menu.statusPages"), path: "status", icon: }, { name: t("menu.maintenance"), path: "maintenance", icon: }, - // { name: t("menu.integrations"), path: "integrations", icon: }, + { name: t("menu.settings"), icon: , @@ -95,14 +94,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", }; @@ -130,9 +128,7 @@ function Sidebar() { const [anchorEl, setAnchorEl] = useState(null); const [popup, setPopup] = useState(); const { user } = useSelector((state) => state.auth); - const distributedUptimeEnabled = useSelector( - (state) => state.ui.distributedUptimeEnabled - ); + const sidebarRef = useRef(null); const [sidebarReady, setSidebarReady] = useState(false); const TRANSITION_DURATION = 200; @@ -200,7 +196,6 @@ function Sidebar() { const logout = async () => { // Clear auth state dispatch(clearAuthState()); - dispatch(clearUptimeMonitorState()); navigate("/login"); }; @@ -234,7 +229,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 +340,7 @@ function Sidebar() { mt={theme.spacing(2)} sx={{ opacity: 0.8, fontWeight: 500 }} > - Checkmate + {t("common.appName")} @@ -375,12 +369,6 @@ function Sidebar() { }} > {menu.map((item) => { - if ( - item.path === "distributed-uptime" && - distributedUptimeEnabled === false - ) { - return null; - } return item.path ? ( /* If item has a path */ { const theme = useTheme(); - const { statusToTheme } = useUtils(); + const { statusToTheme } = useMonitorUtils(); const themeColor = statusToTheme[status]; const statusBoxStyles = gradient @@ -136,8 +136,8 @@ const StatBox = ({ }; StatBox.propTypes = { - heading: PropTypes.string.isRequired, - subHeading: PropTypes.node.isRequired, + heading: PropTypes.string, + subHeading: PropTypes.node, gradient: PropTypes.bool, status: PropTypes.string, sx: PropTypes.object, diff --git a/client/src/Components/TabPanels/Account/PasswordPanel.jsx b/client/src/Components/TabPanels/Account/PasswordPanel.jsx index 88604c72b..84d16e8b0 100644 --- a/client/src/Components/TabPanels/Account/PasswordPanel.jsx +++ b/client/src/Components/TabPanels/Account/PasswordPanel.jsx @@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; import { PasswordEndAdornment } from "../../Inputs/TextInput/Adornments"; import TextInput from "../../Inputs/TextInput"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import Alert from "../../Alert"; import { update } from "../../../Features/Auth/authSlice"; import { useDispatch, useSelector } from "react-redux"; @@ -61,7 +61,7 @@ const PasswordPanel = () => { [name]: true, }; - const validation = credentials.validate( + const validation = newOrChangedCredentials.validate( { ...updatedData }, { abortEarly: false, context: { password: updatedData.newPassword } } ); @@ -79,7 +79,7 @@ const PasswordPanel = () => { const handleSubmit = async (event) => { event.preventDefault(); - const { error } = credentials.validate(localData, { + const { error } = newOrChangedCredentials.validate(localData, { abortEarly: false, context: { password: localData.newPassword }, }); diff --git a/client/src/Components/TabPanels/Account/ProfilePanel.jsx b/client/src/Components/TabPanels/Account/ProfilePanel.jsx index 53a8a8d77..9b209bf3e 100644 --- a/client/src/Components/TabPanels/Account/ProfilePanel.jsx +++ b/client/src/Components/TabPanels/Account/ProfilePanel.jsx @@ -5,10 +5,9 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material"; import Avatar from "../../Avatar"; import TextInput from "../../Inputs/TextInput"; import ImageUpload from "../../Inputs/ImageUpload"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; import { useDispatch, useSelector } from "react-redux"; import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice"; -import { clearUptimeMonitorState } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"; import { createToast } from "../../../Utils/toastUtils"; import { logger } from "../../../Utils/Logger"; import { GenericDialog } from "../../Dialog/genericDialog"; @@ -58,7 +57,7 @@ const ProfilePanel = () => { [name]: value, })); - validateField({ [name]: value }, credentials, name); + validateField({ [name]: value }, newOrChangedCredentials, name); }; // Validates input against provided schema and updates error state @@ -162,7 +161,6 @@ const ProfilePanel = () => { const action = await dispatch(deleteUser()); if (action.payload.success) { dispatch(clearAuthState()); - dispatch(clearUptimeMonitorState()); } else { if (action.payload) { // dispatch errors @@ -240,7 +238,7 @@ const ProfilePanel = () => { gap={SPACING_GAP} > - {t("email")} + {t("auth.common.inputs.email.label")} { email: newEmail, })); - const validation = credentials.validate({ email: newEmail }, { abortEarly: false }); + const validation = newOrChangedCredentials.validate( + { email: newEmail }, + { abortEarly: false } + ); setErrors((prev) => { const updatedErrors = { ...prev }; @@ -142,7 +145,7 @@ const TeamPanel = () => { if (!toInvite.role.includes("user") || !toInvite.role.includes("admin")) setToInvite((prev) => ({ ...prev, role: ["user"] })); - const { error } = credentials.validate( + const { error } = newOrChangedCredentials.validate( { email: toInvite.email }, { abortEarly: false, diff --git a/client/src/Components/Table/TableUtils.js b/client/src/Components/Table/TableUtils.js new file mode 100644 index 000000000..a9f95a5b7 --- /dev/null +++ b/client/src/Components/Table/TableUtils.js @@ -0,0 +1,11 @@ +export const createHeaderFactory = (getCellSx = () => {}) => { + return ({ id, content, onClick = () => {}, render = () => {} }) => { + return { + id, + content, + onClick, + getCellSx, + render, + }; + }; +}; diff --git a/client/src/Components/TextLink/index.jsx b/client/src/Components/TextLink/index.jsx new file mode 100644 index 000000000..a10839e84 --- /dev/null +++ b/client/src/Components/TextLink/index.jsx @@ -0,0 +1,34 @@ +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import Link from "@mui/material/Link"; +import PropTypes from "prop-types"; +import { useTheme } from "@emotion/react"; +import { Link as RouterLink } from "react-router-dom"; + +const TextLink = ({ text, linkText, href }) => { + const theme = useTheme(); + + return ( + + {text} + + {linkText} + + + ); +}; + +TextLink.propTypes = { + text: PropTypes.string, + linkText: PropTypes.string, + href: PropTypes.string, +}; + +export default TextLink; diff --git a/client/src/Components/ThemeSwitch/index.jsx b/client/src/Components/ThemeSwitch/index.jsx index 6c879b289..5efb17419 100644 --- a/client/src/Components/ThemeSwitch/index.jsx +++ b/client/src/Components/ThemeSwitch/index.jsx @@ -14,10 +14,12 @@ import SunAndMoonIcon from "./SunAndMoonIcon"; import { useDispatch, useSelector } from "react-redux"; import { setMode } from "../../Features/UI/uiSlice"; import "./index.css"; +import { useTranslation } from "react-i18next"; const ThemeSwitch = ({ width = 48, height = 48, color }) => { const mode = useSelector((state) => state.ui.mode); const dispatch = useDispatch(); + const { t } = useTranslation(); const toggleTheme = () => { dispatch(setMode(mode === "light" ? "dark" : "light")); @@ -26,7 +28,7 @@ const ThemeSwitch = ({ width = 48, height = 48, color }) => { return ( { - try { - const { monitor } = data; - const res = await networkService.createMonitor({ monitor: monitor }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const checkInfrastructureEndpointResolution = createAsyncThunk( - "infrastructureMonitors/CheckEndpoint", - async (data, thunkApi) => { - try { - const { monitorURL } = data; - const res = await networkService.checkEndpointResolution({ - monitorURL: monitorURL, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const getInfrastructureMonitorById = createAsyncThunk( - "infrastructureMonitors/getMonitorById", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.getMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const getInfrastructureMonitorsByTeamId = createAsyncThunk( - "infrastructureMonitors/getMonitorsByTeamId", - async (_, thunkApi) => { - const user = thunkApi.getState().auth.user; - try { - const res = await networkService.getMonitorsAndSummaryByTeamId({ - teamId: user.teamId, - types: ["hardware"], - limit: 1, - rowsPerPage: 0, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const updateInfrastructureMonitor = createAsyncThunk( - "infrastructureMonitors/updateMonitor", - async ({ monitorId, monitor }, thunkApi) => { - try { - const updatedFields = { - name: monitor.name, - description: monitor.description, - interval: monitor.interval, - notifications: monitor.notifications, - thresholds: monitor.thresholds, - secret: monitor.secret, - }; - const res = await networkService.updateMonitor({ - monitorId, - monitor, - updatedFields, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteInfrastructureMonitor = createAsyncThunk( - "infrastructureMonitors/deleteMonitor", - async (data, thunkApi) => { - try { - const { monitor } = data; - const res = await networkService.deleteMonitorById({ monitorId: monitor._id }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const pauseInfrastructureMonitor = createAsyncThunk( - "infrastructureMonitors/pauseMonitor", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.pauseMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteInfrastructureMonitorChecksByTeamId = createAsyncThunk( - "infrastructureMonitors/deleteChecksByTeamId", - async (data, thunkApi) => { - try { - const { teamId } = data; - const res = await networkService.deleteChecksByTeamId({ teamId: teamId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteAllInfrastructureMonitors = createAsyncThunk( - "infrastructureMonitors/deleteAllMonitors", - async (data, thunkApi) => { - try { - const res = await networkService.deleteAllMonitors(); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -const infrastructureMonitorsSlice = createSlice({ - name: "infrastructureMonitors", - initialState, - reducers: { - clearInfrastructureMonitorState: (state) => { - state.isLoading = false; - state.monitorsSummary = []; - state.success = null; - state.msg = null; - }, - }, - extraReducers: (builder) => { - builder - // ***************************************************** - // Monitors by teamId - // ***************************************************** - - .addCase(getInfrastructureMonitorsByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(getInfrastructureMonitorsByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.msg; - state.monitorsSummary = action.payload.data; - }) - .addCase(getInfrastructureMonitorsByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Getting infrastructure monitors failed"; - }) - - // ***************************************************** - // Create Monitor - // ***************************************************** - .addCase(createInfrastructureMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(createInfrastructureMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(createInfrastructureMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to create infrastructure monitor"; - }) - // ***************************************************** - // Resolve Endpoint - // ***************************************************** - .addCase(checkInfrastructureEndpointResolution.pending, (state) => { - state.isLoading = true; - }) - .addCase(checkInfrastructureEndpointResolution.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(checkInfrastructureEndpointResolution.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to check endpoint resolution"; - }) - // ***************************************************** - // Get Monitor By Id - // ***************************************************** - .addCase(getInfrastructureMonitorById.pending, (state) => { - state.isLoading = true; - }) - .addCase(getInfrastructureMonitorById.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(getInfrastructureMonitorById.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to get infrastructure monitor"; - }) - // ***************************************************** - // update Monitor - // ***************************************************** - .addCase(updateInfrastructureMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(updateInfrastructureMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(updateInfrastructureMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to update infrastructure monitor"; - }) - - // ***************************************************** - // Delete Monitor - // ***************************************************** - .addCase(deleteInfrastructureMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteInfrastructureMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteInfrastructureMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to delete infrastructure monitor"; - }) - // ***************************************************** - // Delete Monitor checks by Team ID - // ***************************************************** - .addCase(deleteInfrastructureMonitorChecksByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteInfrastructureMonitorChecksByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteInfrastructureMonitorChecksByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to delete monitor checks"; - }) - // ***************************************************** - // Pause Monitor - // ***************************************************** - .addCase(pauseInfrastructureMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(pauseInfrastructureMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(pauseInfrastructureMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to pause infrastructure monitor"; - }) - // ***************************************************** - // Delete all Monitors - // ***************************************************** - .addCase(deleteAllInfrastructureMonitors.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteAllInfrastructureMonitors.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteAllInfrastructureMonitors.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors"; - }); - }, -}); - -export const { setInfrastructureMonitors, clearInfrastructureMonitorState } = - infrastructureMonitorsSlice.actions; - -export default infrastructureMonitorsSlice.reducer; diff --git a/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js b/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js deleted file mode 100644 index 3ed2da13f..000000000 --- a/client/src/Features/PageSpeedMonitor/pageSpeedMonitorSlice.js +++ /dev/null @@ -1,309 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { networkService } from "../../main"; -const initialState = { - isLoading: false, - monitorsSummary: [], - success: null, - msg: null, -}; - -export const createPageSpeed = createAsyncThunk( - "pageSpeedMonitors/createPageSpeed", - async (data, thunkApi) => { - try { - const { monitor } = data; - const res = await networkService.createMonitor({ monitor: monitor }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const checkEndpointResolution = createAsyncThunk( - "monitors/checkEndpoint", - async (data, thunkApi) => { - try { - const { monitorURL } = data; - const res = await networkService.checkEndpointResolution({ - monitorURL: monitorURL, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const getPagespeedMonitorById = createAsyncThunk( - "monitors/getMonitorById", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.getMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const getPageSpeedByTeamId = createAsyncThunk( - "pageSpeedMonitors/getPageSpeedByTeamId", - async (_, thunkApi) => { - const user = thunkApi.getState().auth.user; - try { - const res = await networkService.getMonitorsAndSummaryByTeamId({ - teamId: user.teamId, - types: ["pagespeed"], - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const updatePageSpeed = createAsyncThunk( - "pageSpeedMonitors/updatePageSpeed", - async (data, thunkApi) => { - try { - const { monitor } = data; - const updatedFields = { - name: monitor.name, - description: monitor.description, - interval: monitor.interval, - notifications: monitor.notifications, - }; - const res = await networkService.updateMonitor({ - monitorId: monitor._id, - updatedFields: updatedFields, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deletePageSpeed = createAsyncThunk( - "pageSpeedMonitors/deletePageSpeed", - async (data, thunkApi) => { - try { - const { monitor } = data; - const res = await networkService.deleteMonitorById({ monitorId: monitor._id }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); -export const pausePageSpeed = createAsyncThunk( - "pageSpeedMonitors/pausePageSpeed", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.pauseMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -const pageSpeedMonitorSlice = createSlice({ - name: "pageSpeedMonitor", - initialState, - reducers: { - clearMonitorState: (state) => { - state.isLoading = false; - state.monitorsSummary = []; - state.success = null; - state.msg = null; - }, - }, - extraReducers: (builder) => { - builder - // ***************************************************** - // Monitors by teamId - // ***************************************************** - - .addCase(getPageSpeedByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(getPageSpeedByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.msg; - state.monitorsSummary = action.payload.data; - }) - .addCase(getPageSpeedByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Getting page speed monitors failed"; - }) - - // ***************************************************** - .addCase(getPagespeedMonitorById.pending, (state) => { - state.isLoading = true; - }) - .addCase(getPagespeedMonitorById.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(getPagespeedMonitorById.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to get pagespeed monitor"; - }) - - // ***************************************************** - // Create Monitor - // ***************************************************** - .addCase(createPageSpeed.pending, (state) => { - state.isLoading = true; - }) - .addCase(createPageSpeed.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(createPageSpeed.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to create page speed monitor"; - }) - // ***************************************************** - // Resolve Endpoint - // ***************************************************** - .addCase(checkEndpointResolution.pending, (state) => { - state.isLoading = true; - }) - .addCase(checkEndpointResolution.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(checkEndpointResolution.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to check endpoint resolution"; - }) - // ***************************************************** - // Update Monitor - // ***************************************************** - .addCase(updatePageSpeed.pending, (state) => { - state.isLoading = true; - }) - .addCase(updatePageSpeed.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(updatePageSpeed.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to update page speed monitor"; - }) - - // ***************************************************** - // Delete Monitor - // ***************************************************** - .addCase(deletePageSpeed.pending, (state) => { - state.isLoading = true; - }) - .addCase(deletePageSpeed.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deletePageSpeed.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to delete page speed monitor"; - }) - // ***************************************************** - // Pause Monitor - // ***************************************************** - .addCase(pausePageSpeed.pending, (state) => { - state.isLoading = true; - }) - .addCase(pausePageSpeed.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(pausePageSpeed.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to pause page speed monitor"; - }); - }, -}); - -export const { setMonitors, clearMonitorState } = pageSpeedMonitorSlice.actions; - -export default pageSpeedMonitorSlice.reducer; diff --git a/client/src/Features/Settings/settingsSlice.js b/client/src/Features/Settings/settingsSlice.js deleted file mode 100644 index d0725627a..000000000 --- a/client/src/Features/Settings/settingsSlice.js +++ /dev/null @@ -1,100 +0,0 @@ -import { networkService } from "../../main"; -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; - -const initialState = { - isLoading: false, - apiBaseUrl: "", - logLevel: "debug", - pagespeedApiKey: "", -}; - -export const getAppSettings = createAsyncThunk( - "settings/getSettings", - async (data, thunkApi) => { - try { - const res = await networkService.getAppSettings(); - return res.data; - } catch (error) { - if (error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const updateAppSettings = createAsyncThunk( - "settings/updateSettings", - async ({ settings }, thunkApi) => { - try { - const parsedSettings = { - language: settings.language, - pagespeedApiKey: settings.pagespeedApiKey, - }; - const res = await networkService.updateAppSettings({ settings: parsedSettings }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -const handleGetSettingsFulfilled = (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - state.apiBaseUrl = action.payload.data.apiBaseUrl; - state.logLevel = action.payload.data.logLevel; - state.language = action.payload.data.language; - state.pagespeedApiKey = action.payload.data.pagespeedApiKey; -}; -const handleGetSettingsRejected = (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to get settings."; -}; -const handleUpdateSettingsFulfilled = (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - state.apiBaseUrl = action.payload.data.apiBaseUrl; - state.logLevel = action.payload.data.logLevel; -}; -const handleUpdateSettingsRejected = (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to update settings."; -}; - -const settingsSlice = createSlice({ - name: "settings", - initialState, - extraReducers: (builder) => { - builder - .addCase(getAppSettings.pending, (state) => { - state.isLoading = true; - }) - .addCase(getAppSettings.fulfilled, handleGetSettingsFulfilled) - .addCase(getAppSettings.rejected, handleGetSettingsRejected); - - builder - .addCase(updateAppSettings.pending, (state) => { - state.isLoading = true; - }) - .addCase(updateAppSettings.fulfilled, handleUpdateSettingsFulfilled) - .addCase(updateAppSettings.rejected, handleUpdateSettingsRejected); - }, -}); - -export default settingsSlice.reducer; diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 3addfbb7a..45ba267b0 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -16,6 +16,9 @@ const initialState = { maintenance: { rowsPerPage: 5, }, + infrastructure: { + rowsPerPage: 5, + }, sidebar: { collapsed: false, }, diff --git a/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js b/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js deleted file mode 100644 index 74ad803d3..000000000 --- a/client/src/Features/UptimeMonitors/uptimeMonitorsSlice.js +++ /dev/null @@ -1,380 +0,0 @@ -import { createAsyncThunk, createSlice } from "@reduxjs/toolkit"; -import { networkService } from "../../main"; -const initialState = { - isLoading: false, - monitorsSummary: [], - success: null, - msg: null, -}; - -export const createUptimeMonitor = createAsyncThunk( - "monitors/createMonitor", - async (data, thunkApi) => { - try { - const { monitor } = data; - const res = await networkService.createMonitor({ monitor: monitor }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const checkEndpointResolution = createAsyncThunk( - "monitors/checkEndpoint", - async (data, thunkApi) => { - try { - const { monitorURL } = data; - const res = await networkService.checkEndpointResolution({ - monitorURL: monitorURL, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const getUptimeMonitorById = createAsyncThunk( - "monitors/getMonitorById", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.getMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const updateUptimeMonitor = createAsyncThunk( - "monitors/updateMonitor", - async (data, thunkApi) => { - try { - const { monitor } = data; - const updatedFields = { - name: monitor.name, - description: monitor.description, - interval: monitor.interval, - notifications: monitor.notifications, - matchMethod: monitor.matchMethod, - expectedValue: monitor.expectedValue, - ignoreTlsErrors: monitor.ignoreTlsErrors, - jsonPath: monitor.jsonPath, - ...(monitor.type === "port" && { port: monitor.port }), - }; - const res = await networkService.updateMonitor({ - monitorId: monitor._id, - updatedFields, - }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteUptimeMonitor = createAsyncThunk( - "monitors/deleteMonitor", - async (data, thunkApi) => { - try { - const { monitor } = data; - const res = await networkService.deleteMonitorById({ monitorId: monitor._id }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const pauseUptimeMonitor = createAsyncThunk( - "monitors/pauseMonitor", - async (data, thunkApi) => { - try { - const { monitorId } = data; - const res = await networkService.pauseMonitorById({ monitorId: monitorId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteMonitorChecksByTeamId = createAsyncThunk( - "monitors/deleteChecksByTeamId", - async (data, thunkApi) => { - try { - const { teamId } = data; - const res = await networkService.deleteChecksByTeamId({ teamId: teamId }); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); -export const addDemoMonitors = createAsyncThunk( - "monitors/addDemoMonitors", - async (data, thunkApi) => { - try { - const res = await networkService.addDemoMonitors(); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -export const deleteAllMonitors = createAsyncThunk( - "monitors/deleteAllMonitors", - async (data, thunkApi) => { - try { - const res = await networkService.deleteAllMonitors(); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - return thunkApi.rejectWithValue(error.response.data); - } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); - } - } -); - -const uptimeMonitorsSlice = createSlice({ - name: "uptimeMonitors", - initialState, - reducers: { - clearUptimeMonitorState: (state) => { - state.isLoading = false; - state.monitorsSummary = []; - state.success = null; - state.msg = null; - }, - }, - extraReducers: (builder) => { - builder - // ***************************************************** - // Create Monitor - // ***************************************************** - .addCase(createUptimeMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(createUptimeMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(createUptimeMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to create uptime monitor"; - }) - // ***************************************************** - // Resolve Endpoint - // ***************************************************** - .addCase(checkEndpointResolution.pending, (state) => { - state.isLoading = true; - }) - .addCase(checkEndpointResolution.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(checkEndpointResolution.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to check endpoint resolution"; - }) - // ***************************************************** - // Get Monitor By Id - // ***************************************************** - .addCase(getUptimeMonitorById.pending, (state) => { - state.isLoading = true; - }) - .addCase(getUptimeMonitorById.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(getUptimeMonitorById.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to get uptime monitor"; - }) - // ***************************************************** - // update Monitor - // ***************************************************** - .addCase(updateUptimeMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(updateUptimeMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(updateUptimeMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to update uptime monitor"; - }) - - // ***************************************************** - // Delete Monitor - // ***************************************************** - .addCase(deleteUptimeMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteUptimeMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteUptimeMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to delete uptime monitor"; - }) - // ***************************************************** - // Delete Monitor checks by Team ID - // ***************************************************** - .addCase(deleteMonitorChecksByTeamId.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteMonitorChecksByTeamId.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteMonitorChecksByTeamId.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to delete monitor checks"; - }) - // ***************************************************** - // Pause Monitor - // ***************************************************** - .addCase(pauseUptimeMonitor.pending, (state) => { - state.isLoading = true; - }) - .addCase(pauseUptimeMonitor.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(pauseUptimeMonitor.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to pause uptime monitor"; - }) - // ***************************************************** - // Add Demo Monitors - // ***************************************************** - .addCase(addDemoMonitors.pending, (state) => { - state.isLoading = true; - }) - .addCase(addDemoMonitors.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(addDemoMonitors.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload - ? action.payload.msg - : "Failed to add demo uptime monitors"; - }) - // ***************************************************** - // Delete all Monitors - // ***************************************************** - .addCase(deleteAllMonitors.pending, (state) => { - state.isLoading = true; - }) - .addCase(deleteAllMonitors.fulfilled, (state, action) => { - state.isLoading = false; - state.success = action.payload.success; - state.msg = action.payload.msg; - }) - .addCase(deleteAllMonitors.rejected, (state, action) => { - state.isLoading = false; - state.success = false; - state.msg = action.payload ? action.payload.msg : "Failed to delete all monitors"; - }); - }, -}); - -export const { setUptimeMonitors, clearUptimeMonitorState } = uptimeMonitorsSlice.actions; - -export default uptimeMonitorsSlice.reducer; diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js new file mode 100644 index 000000000..683b89b20 --- /dev/null +++ b/client/src/Hooks/checkHooks.js @@ -0,0 +1,120 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; + +const useFetchChecksTeam = ({ + status, + sortOrder, + limit, + dateRange, + filter, + page, + rowsPerPage, + enabled = true, +}) => { + const [checks, setChecks] = useState(undefined); + const [checksCount, setChecksCount] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchChecks = async () => { + if (!enabled) { + return; + } + + const config = { + status, + sortOrder, + limit, + dateRange, + filter, + page, + rowsPerPage, + }; + + try { + setIsLoading(true); + const res = await networkService.getChecksByTeam(config); + setChecks(res.data.data.checks); + setChecksCount(res.data.data.checksCount); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + + fetchChecks(); + }, [status, sortOrder, limit, dateRange, filter, page, rowsPerPage, enabled]); + + return [checks, checksCount, isLoading, networkError]; +}; + +const useFetchChecksByMonitor = ({ + monitorId, + type, + status, + sortOrder, + limit, + dateRange, + filter, + page, + rowsPerPage, + enabled = true, +}) => { + const [checks, setChecks] = useState(undefined); + const [checksCount, setChecksCount] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchChecks = async () => { + if (!enabled || !type) { + return; + } + + const config = { + monitorId, + type, + status, + sortOrder, + limit, + dateRange, + filter, + page, + rowsPerPage, + }; + + try { + setIsLoading(true); + const res = await networkService.getChecksByMonitor(config); + setChecks(res.data.data.checks); + setChecksCount(res.data.data.checksCount); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + + fetchChecks(); + }, [ + monitorId, + type, + status, + sortOrder, + limit, + dateRange, + filter, + page, + rowsPerPage, + enabled, + ]); + + return [checks, checksCount, isLoading, networkError]; +}; + +export { useFetchChecksByMonitor, useFetchChecksTeam }; diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js new file mode 100644 index 000000000..013e7fc25 --- /dev/null +++ b/client/src/Hooks/monitorHooks.js @@ -0,0 +1,506 @@ +import { useEffect, useState } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; +import { useTheme } from "@emotion/react"; +import { useMonitorUtils } from "./useMonitorUtils"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; + +const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { + const [isLoading, setIsLoading] = useState(false); + const [monitors, setMonitors] = useState(undefined); + const [monitorsSummary, setMonitorsSummary] = useState(undefined); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsWithSummaryByTeamId({ + types, + }); + const { monitors, summary } = res?.data?.data ?? {}; + setMonitors(monitors); + setMonitorsSummary(summary); + } catch (error) { + console.error(error); + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [types, monitorUpdateTrigger]); + return [monitors, monitorsSummary, isLoading, networkError]; +}; + +const useFetchMonitorsWithChecks = ({ + types, + limit, + page, + rowsPerPage, + filter, + field, + order, + monitorUpdateTrigger, +}) => { + const [isLoading, setIsLoading] = useState(false); + const [count, setCount] = useState(undefined); + const [monitors, setMonitors] = useState(undefined); + const [networkError, setNetworkError] = useState(false); + + const theme = useTheme(); + const { getMonitorWithPercentage } = useMonitorUtils(); + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsWithChecksByTeamId({ + limit, + types, + page, + rowsPerPage, + filter, + field, + order, + }); + const { count, monitors } = res?.data?.data ?? {}; + const mappedMonitors = monitors.map((monitor) => + getMonitorWithPercentage(monitor, theme) + ); + setMonitors(mappedMonitors); + setCount(count?.monitorsCount ?? 0); + } catch (error) { + console.error(error); + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [ + field, + filter, + getMonitorWithPercentage, + limit, + order, + page, + rowsPerPage, + theme, + types, + monitorUpdateTrigger, + ]); + return [monitors, count, isLoading, networkError]; +}; + +const useFetchMonitorsByTeamId = ({ + types, + limit, + page, + rowsPerPage, + filter, + field, + order, + checkOrder, + normalize, + status, + updateTrigger, +}) => { + const [isLoading, setIsLoading] = useState(false); + const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); + const [networkError, setNetworkError] = useState(false); + + useEffect(() => { + const fetchMonitors = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorsByTeamId({ + limit, + types, + page, + rowsPerPage, + filter, + field, + order, + checkOrder, + status, + normalize, + }); + if (res?.data?.data?.filteredMonitors) { + setMonitors(res.data.data.filteredMonitors); + setSummary(res.data.data.summary); + } + } catch (error) { + setNetworkError(true); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [ + types, + limit, + page, + rowsPerPage, + filter, + field, + order, + updateTrigger, + checkOrder, + normalize, + status, + ]); + return [monitors, summary, isLoading, networkError]; +}; + +const useFetchStatsByMonitorId = ({ + monitorId, + sortOrder, + limit, + dateRange, + numToDisplay, + normalize, + updateTrigger, +}) => { + const [monitor, setMonitor] = useState(undefined); + const [audits, setAudits] = useState(undefined); + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + useEffect(() => { + const fetchMonitor = async () => { + try { + setIsLoading(true); + const res = await networkService.getStatsByMonitorId({ + monitorId: monitorId, + sortOrder, + limit, + dateRange, + numToDisplay, + normalize, + }); + setMonitor(res?.data?.data ?? undefined); + setAudits(res?.data?.data?.checks?.[0]?.audits ?? undefined); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, dateRange, numToDisplay, normalize, sortOrder, limit, updateTrigger]); + return [monitor, audits, isLoading, networkError]; +}; + +const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + useEffect(() => { + const fetchMonitor = async () => { + try { + setIsLoading(true); + const res = await networkService.getMonitorById({ monitorId: monitorId }); + setMonitor(res.data.data); + } catch (error) { + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, setMonitor, updateTrigger]); + return [isLoading]; +}; + +const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitor, setMonitor] = useState(undefined); + + useEffect(() => { + const fetchMonitor = async () => { + try { + if (!monitorId) { + return { monitor: undefined, isLoading: false, networkError: undefined }; + } + const response = await networkService.getHardwareDetailsByMonitorId({ + monitorId: monitorId, + dateRange: dateRange, + }); + setMonitor(response.data.data); + } catch (error) { + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, dateRange, updateTrigger]); + return [monitor, isLoading, networkError]; +}; + +const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { + const [networkError, setNetworkError] = useState(false); + const [isLoading, setIsLoading] = useState(true); + const [monitor, setMonitor] = useState(undefined); + const [monitorStats, setMonitorStats] = useState(undefined); + useEffect(() => { + const fetchMonitors = async () => { + try { + const res = await networkService.getUptimeDetailsById({ + monitorId: monitorId, + dateRange: dateRange, + normalize: true, + }); + const { monitorData, monitorStats } = res?.data?.data ?? {}; + setMonitor(monitorData); + setMonitorStats(monitorStats); + } catch (error) { + setNetworkError(true); + createToast({ body: error.message }); + } finally { + setIsLoading(false); + } + }; + fetchMonitors(); + }, [dateRange, monitorId, trigger]); + return [monitor, monitorStats, isLoading, networkError]; +}; + +const useCreateMonitor = () => { + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const createMonitor = async ({ monitor, redirect }) => { + try { + setIsLoading(true); + await networkService.createMonitor({ monitor }); + createToast({ body: "Monitor created successfully!" }); + if (redirect) { + navigate(redirect); + } + } catch (error) { + createToast({ body: "Failed to create monitor." }); + } finally { + setIsLoading(false); + } + }; + return [createMonitor, isLoading]; +}; + +const useDeleteMonitor = () => { + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const deleteMonitor = async ({ monitor, redirect }) => { + try { + setIsLoading(true); + await networkService.deleteMonitorById({ monitorId: monitor._id }); + createToast({ body: "Monitor deleted successfully!" }); + if (redirect) { + navigate(redirect); + } + } catch (error) { + createToast({ body: "Failed to delete monitor." }); + } finally { + setIsLoading(false); + } + }; + return [deleteMonitor, isLoading]; +}; + +const useUpdateMonitor = () => { + const [isLoading, setIsLoading] = useState(false); + const navigate = useNavigate(); + const updateMonitor = async ({ monitor, redirect }) => { + try { + setIsLoading(true); + const updatedFields = { + name: monitor.name, + description: monitor.description, + interval: monitor.interval, + notifications: monitor.notifications, + matchMethod: monitor.matchMethod, + expectedValue: monitor.expectedValue, + ignoreTlsErrors: monitor.ignoreTlsErrors, + jsonPath: monitor.jsonPath, + ...(monitor.type === "port" && { port: monitor.port }), + ...(monitor.type === "hardware" && { + thresholds: monitor.thresholds, + secret: monitor.secret, + }), + }; + await networkService.updateMonitor({ + monitorId: monitor._id, + updatedFields, + }); + + createToast({ body: "Monitor updated successfully!" }); + if (redirect) { + navigate(redirect); + } + } catch (error) { + createToast({ body: "Failed to update monitor." }); + } finally { + setIsLoading(false); + } + }; + return [updateMonitor, isLoading]; +}; + +const usePauseMonitor = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const pauseMonitor = async ({ monitorId, triggerUpdate }) => { + try { + setIsLoading(true); + const res = await networkService.pauseMonitorById({ monitorId }); + createToast({ + body: res.data.data.isActive + ? "Monitor resumed successfully" + : "Monitor paused successfully", + }); + triggerUpdate(); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + return [pauseMonitor, isLoading, error]; +}; + +const useAddDemoMonitors = () => { + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const addDemoMonitors = async () => { + try { + setIsLoading(true); + await networkService.addDemoMonitors(); + createToast({ body: t("settingsDemoMonitorsAdded") }); + } catch (error) { + createToast({ body: t("settingsFailedToAddDemoMonitors") }); + } finally { + setIsLoading(false); + } + }; + return [addDemoMonitors, isLoading]; +}; + +const useDeleteAllMonitors = () => { + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + const deleteAllMonitors = async () => { + try { + setIsLoading(true); + await networkService.deleteAllMonitors(); + createToast({ body: t("settingsMonitorsDeleted") }); + } catch (error) { + createToast({ body: t("settingsFailedToDeleteMonitors") }); + } finally { + setIsLoading(false); + } + }; + return [deleteAllMonitors, isLoading]; +}; + +const useDeleteMonitorStats = () => { + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const deleteMonitorStats = async () => { + setIsLoading(true); + try { + await networkService.deleteChecksByTeamId(); + createToast({ body: t("settingsStatsCleared") }); + } catch (error) { + createToast({ body: t("settingsFailedToClearStats") }); + } finally { + setIsLoading(false); + } + }; + + return [deleteMonitorStats, isLoading]; +}; + +const useCreateBulkMonitors = () => { + const [isLoading, setIsLoading] = useState(false); + + const createBulkMonitors = async (file, user) => { + setIsLoading(true); + + const formData = new FormData(); + formData.append("csvFile", file); + + try { + const response = await networkService.createBulkMonitors(formData); + return [true, response.data, null]; // [success, data, error] + } catch (err) { + const errorMessage = err?.response?.data?.msg || err.message; + return [false, null, errorMessage]; + } finally { + setIsLoading(false); + } + }; + + return [createBulkMonitors, isLoading]; +}; + +const useExportMonitors = () => { + const [isLoading, setIsLoading] = useState(false); + const { t } = useTranslation(); + + const exportMonitors = async () => { + setIsLoading(true); + try { + const response = await networkService.exportMonitors(); + + // Create a download link + const url = window.URL.createObjectURL(response.data); + const link = document.createElement("a"); + link.href = url; + link.setAttribute("download", "monitors.csv"); + document.body.appendChild(link); + link.click(); + link.remove(); + window.URL.revokeObjectURL(url); + + createToast({ body: t("export.success") }); + return [true, null]; + } catch (err) { + const errorMessage = err?.response?.data?.msg || err.message; + createToast({ body: errorMessage || t("export.failed") }); + return [false, errorMessage]; + } finally { + setIsLoading(false); + } + }; + + return [exportMonitors, isLoading]; +}; + +export { + useFetchMonitorsWithSummary, + useFetchMonitorsWithChecks, + useFetchMonitorsByTeamId, + useFetchStatsByMonitorId, + useFetchMonitorById, + useFetchUptimeMonitorById, + useFetchHardwareMonitorById, + useCreateMonitor, + useDeleteMonitor, + useUpdateMonitor, + usePauseMonitor, + useAddDemoMonitors, + useDeleteAllMonitors, + useDeleteMonitorStats, + useCreateBulkMonitors, + useExportMonitors, +}; diff --git a/client/src/Hooks/queueHooks.js b/client/src/Hooks/queueHooks.js new file mode 100644 index 000000000..f7a25b3d3 --- /dev/null +++ b/client/src/Hooks/queueHooks.js @@ -0,0 +1,57 @@ +import { useState, useEffect } from "react"; +import { networkService } from "../main"; +import { createToast } from "../Utils/toastUtils"; + +const useFetchQueueData = (trigger) => { + const [jobs, setJobs] = useState(undefined); + const [metrics, setMetrics] = useState(undefined); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + useEffect(() => { + const fetchJobs = async () => { + try { + setIsLoading(true); + const response = await networkService.getQueueData(); + if (response.status === 200) { + setJobs(response.data.data.jobs); + setMetrics(response.data.data.metrics); + } + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }; + + fetchJobs(); + }, [trigger]); + + return [jobs, metrics, isLoading, error]; +}; + +const useFlushQueue = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + + const flushQueue = async (trigger, setTrigger) => { + try { + setIsLoading(true); + await networkService.flushQueue(); + createToast({ + body: "Queue flushed", + }); + } catch (error) { + setError(error); + createToast({ + body: error.message, + }); + } finally { + setIsLoading(false); + setTrigger(!trigger); + } + }; + return [flushQueue, isLoading, error]; +}; + +export { useFetchQueueData, useFlushQueue }; diff --git a/client/src/Hooks/useFetchSettings.js b/client/src/Hooks/settingsHooks.js similarity index 59% rename from client/src/Hooks/useFetchSettings.js rename to client/src/Hooks/settingsHooks.js index f7651e789..98a21c1fa 100644 --- a/client/src/Hooks/useFetchSettings.js +++ b/client/src/Hooks/settingsHooks.js @@ -3,7 +3,7 @@ import { networkService } from "../main"; import { createToast } from "../Utils/toastUtils"; import { useTranslation } from "react-i18next"; -const useFetchSettings = ({ setSettingsData }) => { +const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); useEffect(() => { @@ -12,6 +12,8 @@ const useFetchSettings = ({ setSettingsData }) => { try { const response = await networkService.getAppSettings(); setSettingsData(response?.data?.data); + setIsApiKeySet(response?.data?.data?.pagespeedKeySet); + setIsEmailPasswordSet(response?.data?.data?.emailPasswordSet); } catch (error) { createToast({ body: "Failed to fetch settings" }); setError(error); @@ -20,12 +22,18 @@ const useFetchSettings = ({ setSettingsData }) => { } }; fetchSettings(); - }, []); + }, [setSettingsData]); return [isLoading, error]; }; -const useSaveSettings = () => { +const useSaveSettings = ({ + setSettingsData, + setIsApiKeySet, + setApiKeyHasBeenReset, + setIsEmailPasswordSet, + setEmailPasswordHasBeenReset, +}) => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const { t } = useTranslation(); @@ -33,12 +41,21 @@ const useSaveSettings = () => { const saveSettings = async (settings) => { setIsLoading(true); try { - await networkService.updateAppSettings({ settings }); + const settingsResponse = await networkService.updateAppSettings({ settings }); if (settings.checkTTL) { await networkService.updateChecksTTL({ ttl: settings.checkTTL, }); } + setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet); + setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet); + if (settingsResponse.data.data.pagespeedKeySet === true) { + setApiKeyHasBeenReset(false); + } + if (settingsResponse.data.data.emailPasswordSet === true) { + setEmailPasswordHasBeenReset(false); + } + setSettingsData(settingsResponse.data.data); createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Hooks/useBulkMonitors.js b/client/src/Hooks/useBulkMonitors.js deleted file mode 100644 index a834f0140..000000000 --- a/client/src/Hooks/useBulkMonitors.js +++ /dev/null @@ -1,27 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../main"; - -export const useBulkMonitors = () => { - const [isLoading, setIsLoading] = useState(false); - - const createBulkMonitors = async (file, user) => { - setIsLoading(true); - - const formData = new FormData(); - formData.append("csvFile", file); - formData.append("userId", user._id); - formData.append("teamId", user.teamId); - - try { - const response = await networkService.createBulkMonitors(formData); - return [true, response.data, null]; // [success, data, error] - } catch (err) { - const errorMessage = err?.response?.data?.msg || err.message; - return [false, null, errorMessage]; - } finally { - setIsLoading(false); - } - }; - - return [createBulkMonitors, isLoading]; -}; diff --git a/client/src/Hooks/useDeleteMonitorStats.js b/client/src/Hooks/useDeleteMonitorStats.js deleted file mode 100644 index 64138929f..000000000 --- a/client/src/Hooks/useDeleteMonitorStats.js +++ /dev/null @@ -1,24 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; -import { useTranslation } from "react-i18next"; - -const UseDeleteMonitorStats = () => { - const { t } = useTranslation(); - const [isLoading, setIsLoading] = useState(false); - const deleteMonitorStats = async ({ teamId }) => { - setIsLoading(true); - try { - const res = await networkService.deleteChecksByTeamId({ teamId }); - createToast({ body: t("settingsStatsCleared") }); - } catch (error) { - createToast({ body: t("settingsFailedToClearStats") }); - } finally { - setIsLoading(false); - } - }; - - return [deleteMonitorStats, isLoading]; -}; - -export { UseDeleteMonitorStats }; diff --git a/client/src/Hooks/useFetchDepinStatusPage.js b/client/src/Hooks/useFetchDepinStatusPage.js deleted file mode 100644 index cb436d6d6..000000000 --- a/client/src/Hooks/useFetchDepinStatusPage.js +++ /dev/null @@ -1,58 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; -import { useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils"; - -const useFetchDepinStatusPage = ({ url, timeFrame, isCreate = false }) => { - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [statusPage, setStatusPage] = useState(undefined); - const [monitorId, setMonitorId] = useState(undefined); - const [isPublished, setIsPublished] = useState(false); - const { authToken } = useSelector((state) => state.auth); - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - - useEffect(() => { - if (isCreate) return; - const fetchStatusPageByUrl = async () => { - try { - const response = await networkService.getDistributedStatusPageByUrl({ - authToken, - url, - type: "distributed", - timeFrame, - }); - if (!response?.data?.data) return; - const statusPage = response.data.data; - - const monitorsWithPercentage = statusPage?.subMonitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - - const statusPageWithSubmonitorPercentages = { - ...statusPage, - subMonitors: monitorsWithPercentage, - }; - setStatusPage(statusPageWithSubmonitorPercentages); - - setMonitorId(statusPage?.monitors[0]); - setIsPublished(statusPage?.isPublished); - } catch (error) { - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchStatusPageByUrl(); - }, [authToken, url, getMonitorWithPercentage, theme, timeFrame, isCreate]); - - return [isLoading, networkError, statusPage, monitorId, isPublished]; -}; - -export { useFetchDepinStatusPage }; diff --git a/client/src/Hooks/useFetchMonitorsWithChecks.js b/client/src/Hooks/useFetchMonitorsWithChecks.js deleted file mode 100644 index a6f0437b9..000000000 --- a/client/src/Hooks/useFetchMonitorsWithChecks.js +++ /dev/null @@ -1,72 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils"; - -export const useFetchMonitorsWithChecks = ({ - teamId, - types, - limit, - page, - rowsPerPage, - filter, - field, - order, - monitorUpdateTrigger, -}) => { - const [isLoading, setIsLoading] = useState(false); - const [count, setCount] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - const [networkError, setNetworkError] = useState(false); - - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsWithChecksByTeamId({ - teamId, - limit, - types, - page, - rowsPerPage, - filter, - field, - order, - }); - const { count, monitors } = res?.data?.data ?? {}; - const mappedMonitors = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(mappedMonitors); - setCount(count?.monitorsCount ?? 0); - } catch (error) { - console.error(error); - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [ - field, - filter, - getMonitorWithPercentage, - limit, - order, - page, - rowsPerPage, - teamId, - theme, - types, - monitorUpdateTrigger, - ]); - return [monitors, count, isLoading, networkError]; -}; - -export default useFetchMonitorsWithChecks; diff --git a/client/src/Hooks/useFetchMonitorsWithSummary.js b/client/src/Hooks/useFetchMonitorsWithSummary.js deleted file mode 100644 index cd21271d4..000000000 --- a/client/src/Hooks/useFetchMonitorsWithSummary.js +++ /dev/null @@ -1,37 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -export const useFetchMonitorsWithSummary = ({ teamId, types, monitorUpdateTrigger }) => { - const [isLoading, setIsLoading] = useState(false); - const [monitors, setMonitors] = useState(undefined); - const [monitorsSummary, setMonitorsSummary] = useState(undefined); - const [networkError, setNetworkError] = useState(false); - - useEffect(() => { - const fetchMonitors = async () => { - try { - setIsLoading(true); - const res = await networkService.getMonitorsWithSummaryByTeamId({ - teamId, - types, - }); - const { monitors, summary } = res?.data?.data ?? {}; - setMonitors(monitors); - setMonitorsSummary(summary); - } catch (error) { - console.error(error); - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [teamId, types, monitorUpdateTrigger]); - return [monitors, monitorsSummary, isLoading, networkError]; -}; - -export default useFetchMonitorsWithSummary; diff --git a/client/src/Hooks/useFetchUptimeMonitorDetails.js b/client/src/Hooks/useFetchUptimeMonitorDetails.js deleted file mode 100644 index 3c0ac7ee6..000000000 --- a/client/src/Hooks/useFetchUptimeMonitorDetails.js +++ /dev/null @@ -1,35 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { useNavigate } from "react-router-dom"; -import { createToast } from "../Utils/toastUtils"; - -export const useFetchUptimeMonitorDetails = ({ monitorId, dateRange, trigger }) => { - const [networkError, setNetworkError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [monitor, setMonitor] = useState(undefined); - const [monitorStats, setMonitorStats] = useState(undefined); - const navigate = useNavigate(); - useEffect(() => { - const fetchMonitors = async () => { - try { - const res = await networkService.getUptimeDetailsById({ - monitorId: monitorId, - dateRange: dateRange, - normalize: true, - }); - const { monitorData, monitorStats } = res?.data?.data ?? {}; - setMonitor(monitorData); - setMonitorStats(monitorStats); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [dateRange, monitorId, navigate, trigger]); - return [monitor, monitorStats, isLoading, networkError]; -}; - -export default useFetchUptimeMonitorDetails; diff --git a/client/src/Hooks/useMonitorControls.js b/client/src/Hooks/useMonitorControls.js deleted file mode 100644 index b47603b58..000000000 --- a/client/src/Hooks/useMonitorControls.js +++ /dev/null @@ -1,28 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const usePauseMonitor = ({ monitorId, triggerUpdate }) => { - const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(undefined); - const pauseMonitor = async () => { - try { - setIsLoading(true); - const res = await networkService.pauseMonitorById({ monitorId }); - createToast({ - body: res.data.data.isActive - ? "Monitor resumed successfully" - : "Monitor paused successfully", - }); - triggerUpdate(); - } catch (error) { - setError(error); - } finally { - setIsLoading(false); - } - }; - - return [pauseMonitor, isLoading, error]; -}; - -export { usePauseMonitor }; diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/useMonitorUtils.js index 424112a81..417c6eadd 100644 --- a/client/src/Hooks/useMonitorUtils.js +++ b/client/src/Hooks/useMonitorUtils.js @@ -46,7 +46,27 @@ const useMonitorUtils = () => { pending: theme.palette.warning.lowContrast, }; - return { getMonitorWithPercentage, determineState, statusColor }; + const statusToTheme = { + up: "success", + down: "error", + paused: "warning", + pending: "secondary", + "cannot resolve": "tertiary", + }; + + const pagespeedStatusMsg = { + up: "Live (collecting data)", + down: "Inactive", + paused: "Paused", + }; + + return { + getMonitorWithPercentage, + determineState, + statusColor, + statusToTheme, + pagespeedStatusMsg, + }; }; export { useMonitorUtils }; diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js new file mode 100644 index 000000000..cc6c3e875 --- /dev/null +++ b/client/src/Hooks/useNotifications.js @@ -0,0 +1,206 @@ +import { useState, useEffect, useCallback } from "react"; +import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../main"; +import { useNavigate } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils"; + +const useCreateNotification = () => { + const navigate = useNavigate(); + const { t } = useTranslation(); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const createNotification = async (notification) => { + try { + setIsLoading(true); + await networkService.createNotification({ notification }); + createToast({ + body: t("notifications.create.success"), + }); + navigate("/notifications"); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.create.failed"), + }); + } finally { + setIsLoading(false); + } + }; + + return [createNotification, isLoading, error]; +}; + +const useGetNotificationsByTeamId = (updateTrigger) => { + const [notifications, setNotifications] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const getNotifications = useCallback(async () => { + try { + setIsLoading(true); + const response = await networkService.getNotificationsByTeamId(); + setNotifications(response?.data?.data ?? []); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.fetch.failed"), + }); + } finally { + setIsLoading(false); + } + }, [t]); + + useEffect(() => { + getNotifications(); + }, [getNotifications, updateTrigger]); + + return [notifications, isLoading, error]; +}; + +const useDeleteNotification = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const deleteNotification = async (id, triggerUpdate) => { + try { + setIsLoading(true); + await networkService.deleteNotificationById({ id }); + createToast({ + body: t("notifications.delete.success"), + }); + triggerUpdate(); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.delete.failed"), + }); + } finally { + setIsLoading(false); + } + }; + + return [deleteNotification, isLoading, error]; +}; + +const useGetNotificationById = (id, setNotification) => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const getNotificationById = useCallback(async () => { + try { + setIsLoading(true); + const response = await networkService.getNotificationById({ id }); + + const notification = response?.data?.data ?? null; + + const notificationData = { + address: notification?.address, + notificationName: notification?.notificationName, + type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, + }; + + setNotification(notificationData); + } catch (error) { + setError(error); + } finally { + setIsLoading(false); + } + }, [id, setNotification]); + + useEffect(() => { + if (id) { + getNotificationById(); + } + }, [getNotificationById, id]); + + return [isLoading, error]; +}; + +const useEditNotification = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + const navigate = useNavigate(); + const editNotification = async (id, notification) => { + try { + setIsLoading(true); + await networkService.editNotification({ id, notification }); + createToast({ + body: t("notifications.edit.success"), + }); + navigate(`/notifications`); + } catch (error) { + setError(error); + createToast({ + body: t("notifications.edit.failed"), + }); + } finally { + setIsLoading(false); + } + }; + + return [editNotification, isLoading, error]; +}; + +const useTestNotification = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + const { t } = useTranslation(); + + const testNotification = async (notification) => { + try { + setIsLoading(true); + await networkService.testNotification({ notification }); + createToast({ + body: t("notifications.test.success"), + }); + } catch (error) { + setError(error); + createToast({ + body: error?.response?.data?.msg || t("notifications.test.failed"), + }); + } finally { + setIsLoading(false); + } + }; + + return [testNotification, isLoading, error]; +}; + +const useTestAllNotifications = () => { + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(undefined); + const { t } = useTranslation(); + const testAllNotifications = async ({ monitorId }) => { + try { + setIsLoading(true); + await networkService.testAllNotifications({ monitorId }); + createToast({ + body: t("notifications.test.success"), + }); + } catch (error) { + createToast({ + body: error.response?.data?.msg || error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }; + + return [testAllNotifications, isLoading, error]; +}; + +export { + useCreateNotification, + useGetNotificationsByTeamId, + useDeleteNotification, + useGetNotificationById, + useEditNotification, + useTestNotification, + useTestAllNotifications, +}; diff --git a/client/src/Hooks/useSubscribeToDepinDetails.js b/client/src/Hooks/useSubscribeToDepinDetails.js deleted file mode 100644 index 0e8c2744f..000000000 --- a/client/src/Hooks/useSubscribeToDepinDetails.js +++ /dev/null @@ -1,96 +0,0 @@ -import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; - -const useSubscribeToDepinDetails = ({ monitorId, isPublic, isPublished, dateRange }) => { - const [isLoading, setIsLoading] = useState(true); - const [connectionStatus, setConnectionStatus] = useState(undefined); - const [retryCount, setRetryCount] = useState(0); - const [networkError, setNetworkError] = useState(false); - const [monitor, setMonitor] = useState(undefined); - const [lastUpdateTrigger, setLastUpdateTrigger] = useState(0); - - useEffect(() => { - if (typeof monitorId === "undefined") { - return; - } - // If this page is public and not published, don't subscribe to details - if (isPublic && isPublished === false) { - return; - } - - // Get initial data - - const fetchInitialData = async () => { - try { - const res = await networkService.getDistributedUptimeDetails({ - monitorId, - dateRange: dateRange, - normalize: true, - isPublic, - }); - const responseData = res?.data?.data; - - if (typeof responseData === "undefined") { - throw new Error("No data"); - } - setConnectionStatus("up"); - setLastUpdateTrigger(Date.now()); - setMonitor(responseData); - } catch (error) { - setNetworkError(true); - setConnectionStatus("down"); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - - fetchInitialData(); - - try { - const cleanup = networkService.subscribeToDistributedUptimeDetails({ - monitorId, - dateRange: dateRange, - normalize: true, - onUpdate: (data) => { - if (isLoading === true) { - setIsLoading(false); - } - if (networkError === true) { - setNetworkError(false); - } - setLastUpdateTrigger(Date.now()); - setMonitor(data.monitor); - }, - onOpen: () => { - setConnectionStatus("up"); - setRetryCount(0); // Reset retry count on successful connection - }, - onError: () => { - setIsLoading(false); - setNetworkError(true); - setConnectionStatus("down"); - }, - }); - return cleanup; - } catch (error) { - setNetworkError(true); - } - }, [ - dateRange, - monitorId, - retryCount, - setConnectionStatus, - networkError, - isLoading, - isPublic, - isPublished, - ]); - - return [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger]; -}; - -export { useSubscribeToDepinDetails }; diff --git a/client/src/Hooks/useSubscribeToDepinMonitors.js b/client/src/Hooks/useSubscribeToDepinMonitors.js deleted file mode 100644 index 918ead6bc..000000000 --- a/client/src/Hooks/useSubscribeToDepinMonitors.js +++ /dev/null @@ -1,95 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { useSelector } from "react-redux"; -import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils"; -import { createToast } from "../Utils/toastUtils"; - -const useSubscribeToDepinMonitors = (page, rowsPerPage) => { - // Redux - const { user } = useSelector((state) => state.auth); - - // Local state - const [isLoading, setIsLoading] = useState(true); - const [networkError, setNetworkError] = useState(false); - const [count, setCount] = useState(undefined); - const [monitors, setMonitors] = useState(undefined); - - const theme = useTheme(); - const { getMonitorWithPercentage } = useMonitorUtils(); - - useEffect(() => { - const fetchInitialData = async () => { - try { - const initialDataRes = await networkService.getDistributedUptimeMonitors({ - teamId: user.teamId, - limit: 25, - types: ["distributed_http"], - page, - rowsPerPage, - }); - - const { count, monitors } = initialDataRes?.data?.data ?? {}; - const responseData = initialDataRes?.data?.data; - if (typeof responseData === "undefined") throw new Error("No data"); - - const mappedMonitors = monitors?.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - - setMonitors(mappedMonitors); - setCount(count?.monitorsCount ?? 0); - } catch (error) { - setNetworkError(true); - createToast({ - body: error.message, - }); - } finally { - setIsLoading(false); - } - }; - - fetchInitialData(); - - try { - const cleanup = networkService.subscribeToDistributedUptimeMonitors({ - teamId: user.teamId, - limit: 25, - types: [ - typeof import.meta.env.VITE_DEPIN_TESTING === "undefined" - ? "distributed_http" - : "distributed_test", - ], - page, - rowsPerPage, - filter: null, - field: null, - order: null, - onUpdate: (data) => { - if (isLoading === true) { - setIsLoading(false); - } - const { count, monitors } = data; - const mappedMonitors = monitors.map((monitor) => - getMonitorWithPercentage(monitor, theme) - ); - setMonitors(mappedMonitors); - setCount(count?.monitorsCount ?? 0); - }, - onError: () => { - setIsLoading(false); - setNetworkError(true); - }, - }); - - return cleanup; - } catch (error) { - createToast({ - body: error.message, - }); - setNetworkError(true); - } - }, [user, getMonitorWithPercentage, theme, isLoading, page, rowsPerPage]); - return [monitors, count, isLoading, networkError]; -}; -export { useSubscribeToDepinMonitors }; diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/Account/index.jsx index 9ba57eb06..d8672f51c 100644 --- a/client/src/Pages/Account/index.jsx +++ b/client/src/Pages/Account/index.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; import PropTypes from "prop-types"; -import { useNavigate } from "react-router"; +import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { Box, Tab, useTheme } from "@mui/material"; import CustomTabList from "../../Components/Tab"; diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/Auth/CheckEmail.jsx index 695be62b0..e1f5e43ed 100644 --- a/client/src/Pages/Auth/CheckEmail.jsx +++ b/client/src/Pages/Auth/CheckEmail.jsx @@ -2,10 +2,10 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; -import { useNavigate } from "react-router"; +import { useNavigate } from "react-router-dom"; import { createToast } from "../../Utils/toastUtils"; import { forgotPassword } from "../../Features/Auth/authSlice"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import Background from "../../assets/Images/background-grid.svg?react"; import EmailIcon from "../../assets/icons/email.svg?react"; import Logo from "../../assets/icons/checkmate-icon.svg?react"; @@ -31,16 +31,16 @@ const CheckEmail = () => { const toastFail = [ { - body: "Email not found.", + body: t("auth.forgotPassword.toasts.emailNotFound"), }, { - body: "Redirecting in 3...", + body: t("auth.forgotPassword.toasts.redirect").replace("", "3"), }, { - body: "Redirecting in 2...", + body: t("auth.forgotPassword.toasts.redirect").replace("", "2"), }, { - body: "Redirecting in 1...", + body: t("auth.forgotPassword.toasts.redirect").replace("", "1"), }, ]; @@ -62,19 +62,19 @@ const CheckEmail = () => { const action = await dispatch(forgotPassword(form)); if (action.payload.success) { createToast({ - body: `Instructions sent to ${form.email}.`, + body: t("auth.forgotPassword.toasts.sent").replace("", form.email), }); setDisabled(false); } else { if (action.payload) { // dispatch errors createToast({ - body: action.payload.msg, + body: action.payload.msg, // FIXME: Potential untranslated string }); } else { // unknown errors createToast({ - body: "Unknown error.", + body: t("common.toasts.unknownError"), }); } } @@ -118,7 +118,7 @@ const CheckEmail = () => { gap={theme.spacing(4)} > - Checkmate + {t("common.appName")} { svgHeight={24} mb={theme.spacing(4)} > - + - {t("authCheckEmailTitle")} + {t("auth.forgotPassword.heading")} - {t("authCheckEmailDescription")}{" "} - - {email || "username@email.com"} - + + {email || "username@email.com"} + + ), + }} + /> - {t("authCheckEmailDidntReceiveEmail")}{" "} - + ), }} - > - {t("authCheckEmailClickToResend")} - + /> @@ -207,15 +217,21 @@ const CheckEmail = () => { textAlign="center" p={theme.spacing(12)} > - {t("goBackTo")} - - {t("authLoginTitle")} + + + ), + }} + /> diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx index 6a31092c9..2a887756e 100644 --- a/client/src/Pages/Auth/ForgotPassword.jsx +++ b/client/src/Pages/Auth/ForgotPassword.jsx @@ -4,14 +4,14 @@ import { createToast } from "../../Utils/toastUtils"; import { useDispatch, useSelector } from "react-redux"; import { forgotPassword } from "../../Features/Auth/authSlice"; import { useEffect, useState } from "react"; -import { credentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../Validation/validation"; import { useNavigate } from "react-router-dom"; import TextInput from "../../Components/Inputs/TextInput"; import Logo from "../../assets/icons/checkmate-icon.svg?react"; import Key from "../../assets/icons/key.svg?react"; import Background from "../../assets/Images/background-grid.svg?react"; import IconBox from "../../Components/IconBox"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import "./index.css"; const ForgotPassword = () => { @@ -35,14 +35,14 @@ const ForgotPassword = () => { const handleSubmit = async (event) => { event.preventDefault(); - const { error } = credentials.validate(form, { abortEarly: false }); + const { error } = newOrChangedCredentials.validate(form, { abortEarly: false }); if (error) { // validation errors const err = error.details && error.details.length > 0 - ? error.details[0].message - : "Error validating data."; + ? error.details[0].message // FIXME: Possibly untranslated string + : t("auth.common.errors.validation"); setErrors({ email: err }); createToast({ body: err, @@ -53,18 +53,18 @@ const ForgotPassword = () => { sessionStorage.setItem("email", form.email); navigate("/check-email"); createToast({ - body: `Instructions sent to ${form.email}.`, + body: t("auth.forgotPassword.toasts.sent").replace("", form.email), }); } else { if (action.payload) { // dispatch errors createToast({ - body: action.payload.msg, + body: action.payload.msg, // FIXME: Potentially untranslated string }); } else { // unknown errors createToast({ - body: "Unknown error.", + body: t("common.toasts.unknownError"), }); } } @@ -75,7 +75,10 @@ const ForgotPassword = () => { const { value } = event.target; setForm({ email: value }); - const { error } = credentials.validate({ email: value }, { abortEarly: false }); + const { error } = newOrChangedCredentials.validate( + { email: value }, + { abortEarly: false } + ); if (error) setErrors({ email: error.details[0].message }); else delete errors.email; @@ -120,7 +123,7 @@ const ForgotPassword = () => { gap={theme.spacing(4)} > - Checkmate + {t("common.appName")} { svgHeight={24} mb={theme.spacing(4)} > - + - {t("authForgotPasswordTitle")} - {t("authForgotPasswordInstructions")} + {t("auth.forgotPassword.heading")} + {t("auth.forgotPassword.subheadings.stepOne")} { @@ -207,15 +210,21 @@ const ForgotPassword = () => { textAlign="center" p={theme.spacing(12)} > - {t("goBackTo")} - - {t("authLoginTitle")} + + + ), + }} + /> diff --git a/client/src/Pages/Auth/Login/Components/EmailStep.jsx b/client/src/Pages/Auth/Login/Components/EmailStep.jsx deleted file mode 100644 index 45babead1..000000000 --- a/client/src/Pages/Auth/Login/Components/EmailStep.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { useRef, useEffect } from "react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; - -/** - * Renders the email step of the login process which includes an email field. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -const EmailStep = ({ form, errors, onSubmit, onChange }) => { - const theme = useTheme(); - const inputRef = useRef(null); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - {t("authLoginTitle")} - {t("authLoginEnterEmail")} - - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email ? true : false} - helperText={errors.email ? t(errors.email) : ""} - ref={inputRef} - /> - - - - - - - ); -}; - -EmailStep.propTypes = { - form: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, -}; - -export default EmailStep; diff --git a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx b/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx deleted file mode 100644 index a9cb17d7c..000000000 --- a/client/src/Pages/Auth/Login/Components/ForgotPasswordLabel.jsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Box, Typography, useTheme } from "@mui/material"; -import PropTypes from "prop-types"; -import { useNavigate } from "react-router"; -import { useTranslation } from "react-i18next"; - -const ForgotPasswordLabel = ({ email, errorEmail }) => { - const theme = useTheme(); - const navigate = useNavigate(); - const { t } = useTranslation(); - - const handleNavigate = () => { - if (email !== "" && !errorEmail) { - sessionStorage.setItem("email", email); - } - navigate("/forgot-password"); - }; - - return ( - - - {t("authForgotPasswordTitle")} - - - {t("authForgotPasswordResetPassword")} - - - ); -}; - -ForgotPasswordLabel.propTypes = { - email: PropTypes.string.isRequired, - errorEmail: PropTypes.string.isRequired, -}; - -export default ForgotPasswordLabel; diff --git a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx b/client/src/Pages/Auth/Login/Components/PasswordStep.jsx deleted file mode 100644 index 1fbdf93f0..000000000 --- a/client/src/Pages/Auth/Login/Components/PasswordStep.jsx +++ /dev/null @@ -1,141 +0,0 @@ -import { useRef, useEffect } from "react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useSelector } from "react-redux"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -/** - * Renders the password step of the login process, including a password input field. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -const PasswordStep = ({ form, errors, onSubmit, onChange, onBack }) => { - const theme = useTheme(); - const inputRef = useRef(null); - const authState = useSelector((state) => state.auth); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - {t("authLoginTitle")} - {t("authLoginEnterPassword")} - - - } - /> - - - - - - - - ); -}; - -PasswordStep.propTypes = { - form: PropTypes.object.isRequired, - errors: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onBack: PropTypes.func.isRequired, -}; - -export default PasswordStep; diff --git a/client/src/Pages/Auth/Login/Login.jsx b/client/src/Pages/Auth/Login/Login.jsx deleted file mode 100644 index 5df2ed011..000000000 --- a/client/src/Pages/Auth/Login/Login.jsx +++ /dev/null @@ -1,262 +0,0 @@ -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { Box, Stack, Typography } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { credentials } from "../../../Validation/validation"; -import { login } from "../../../Features/Auth/authSlice"; -import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../Utils/toastUtils"; -import { networkService } from "../../../main"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import "../index.css"; -import EmailStep from "./Components/EmailStep"; -import PasswordStep from "./Components/PasswordStep"; -import ThemeSwitch from "../../../Components/ThemeSwitch"; -import ForgotPasswordLabel from "./Components/ForgotPasswordLabel"; -import LanguageSelector from "../../../Components/LanguageSelector"; -import { useTranslation } from "react-i18next"; - -const DEMO = import.meta.env.VITE_APP_DEMO; - -/** - * Displays the login page. - */ - -const Login = () => { - const theme = useTheme(); - const dispatch = useDispatch(); - const { t } = useTranslation(); - const navigate = useNavigate(); - const authState = useSelector((state) => state.auth); - const { authToken } = authState; - - const idMap = { - "login-email-input": "email", - "login-password-input": "password", - }; - - const [form, setForm] = useState({ - email: DEMO !== undefined ? "uptimedemo@demo.com" : "", - password: DEMO !== undefined ? "Demouser1!" : "", - }); - const [errors, setErrors] = useState({}); - const [step, setStep] = useState(0); - - useEffect(() => { - if (authToken) { - navigate("/uptime"); - } - }, [authToken, navigate]); - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idMap[id]; - const lowerCasedValue = - name === idMap["login-email-input"] ? value?.toLowerCase() || value : value; - setForm((prev) => ({ - ...prev, - [name]: lowerCasedValue, - })); - - const { error } = credentials.validate( - { [name]: lowerCasedValue }, - { abortEarly: false } - ); - - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - }; - - const handleSubmit = async (event) => { - event.preventDefault(); - - if (step === 0) { - const { error } = credentials.validate( - { email: form.email }, - { abortEarly: false } - ); - if (error) { - const errorMessage = error.details[0].message; - const translatedMessage = errorMessage.startsWith("auth") - ? t(errorMessage) - : errorMessage; - setErrors((prev) => ({ ...prev, email: translatedMessage })); - createToast({ body: translatedMessage }); - } else { - setStep(1); - } - } else if (step === 1) { - const { error } = credentials.validate(form, { abortEarly: false }); - - if (error) { - // validation errors - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ - body: - error.details && error.details.length > 0 - ? error.details[0].message.startsWith("auth") - ? t(error.details[0].message) - : error.details[0].message - : t("Error validating data."), - }); - } else { - const action = await dispatch(login(form)); - if (action.payload.success) { - navigate("/uptime"); - createToast({ - body: t("welcomeBack"), - }); - } else { - if (action.payload) { - if (action.payload.msg === "Incorrect password") - setErrors({ - password: "The password you provided does not match our records", - }); - // dispatch errors - createToast({ - body: action.payload.msg, - }); - } else { - // unknown errors - createToast({ - body: "Unknown error.", - }); - } - } - } - } - }; - - return ( - - - - - - - - Checkmate - - - - - - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - {step === 0 ? ( - - ) : ( - step === 1 && ( - setStep(0)} - /> - ) - )} - - - {/* Registration link */} - - - {t("doNotHaveAccount")} - - navigate("/register")} - > - {t("registerHere")} - - - - - ); -}; - -export default Login; diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx new file mode 100644 index 000000000..098fbbf08 --- /dev/null +++ b/client/src/Pages/Auth/Login/index.jsx @@ -0,0 +1,167 @@ +// Components +import Stack from "@mui/material/Stack"; +import AuthHeader from "../components/AuthHeader"; +import Button from "@mui/material/Button"; +import TextInput from "../../../Components/Inputs/TextInput"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; +import { loginCredentials } from "../../../Validation/validation"; +import TextLink from "../../../Components/TextLink"; +import Typography from "@mui/material/Typography"; + +// Utils +import { login } from "../../../Features/Auth/authSlice"; +import { useNavigate } from "react-router-dom"; +import { useDispatch } from "react-redux"; +import { useState } from "react"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../Utils/toastUtils"; + +const Login = () => { + // Local state + const [form, setForm] = useState({ + email: "", + password: "", + }); + const [errors, setErrors] = useState({ + email: "", + password: "", + }); + + // Hooks + const { t } = useTranslation(); + const theme = useTheme(); + const dispatch = useDispatch(); + const navigate = useNavigate(); + + // Handlers + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") { + value = value.toLowerCase(); + } + const updatedForm = { ...form, [name]: value }; + const { error } = loginCredentials.validate({ [name]: value }); + setForm(updatedForm); + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + + const onSubmit = async (e) => { + e.preventDefault(); + const toSubmit = { ...form }; + const { error } = loginCredentials.validate(toSubmit, { abortEarly: false }); + + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return; + } + + const action = await dispatch(login(form)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.login.toasts.success"), + }); + } else { + if (action.payload) { + if (action.payload.msg === "Incorrect password") + setErrors({ + password: t("auth.login.errors.password.incorrect"), + }); + // dispatch errors + createToast({ + body: t("auth.login.toasts.incorrectPassword"), + }); + } else { + // unknown errors + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + + return ( + + + + {t("auth.login.heading")} + + + + } + /> + + + + + + + ); +}; + +export default Login; diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx index 57a4aaf25..7ace77abc 100644 --- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -1,14 +1,13 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { useNavigate } from "react-router"; +import { useNavigate } from "react-router-dom"; import { useDispatch } from "react-redux"; import { clearAuthState } from "../../Features/Auth/authSlice"; -import { clearUptimeMonitorState } from "../../Features/UptimeMonitors/uptimeMonitorsSlice"; import Background from "../../assets/Images/background-grid.svg?react"; import ConfirmIcon from "../../assets/icons/check-outlined.svg?react"; import Logo from "../../assets/icons/checkmate-icon.svg?react"; import IconBox from "../../Components/IconBox"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import "./index.css"; const NewPasswordConfirmed = () => { @@ -19,7 +18,6 @@ const NewPasswordConfirmed = () => { const handleNavigate = () => { dispatch(clearAuthState()); - dispatch(clearUptimeMonitorState()); navigate("/login"); }; @@ -54,7 +52,7 @@ const NewPasswordConfirmed = () => { gap={theme.spacing(4)} > - Checkmate + {t("common.appName")} { svgHeight={24} mb={theme.spacing(4)} > - + - {t("passwordreset")} - {t("authNewPasswordConfirmed")} + {t("auth.forgotPassword.heading")} + + {t("auth.forgotPassword.subheadings.stepFour")} + @@ -118,15 +118,21 @@ const NewPasswordConfirmed = () => { textAlign="center" p={theme.spacing(12)} > - {t("goBackTo")} - - {t("authLoginTitle")} + + + ), + }} + /> diff --git a/client/src/Pages/Auth/Register/Register.jsx b/client/src/Pages/Auth/Register/Register.jsx deleted file mode 100644 index ffa1dd0ad..000000000 --- a/client/src/Pages/Auth/Register/Register.jsx +++ /dev/null @@ -1,399 +0,0 @@ -import { useState, useEffect } from "react"; -import { useNavigate, useParams } from "react-router-dom"; -import { useDispatch } from "react-redux"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import { StepOne } from "./StepOne"; -import { StepTwo } from "./StepTwo"; -import { StepThree } from "./StepThree"; -import { networkService } from "../../../main"; -import { credentials } from "../../../Validation/validation"; -import { createToast } from "../../../Utils/toastUtils"; -import { register } from "../../../Features/Auth/authSlice"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import Mail from "../../../assets/icons/mail.svg?react"; -import "../index.css"; -import { useTranslation, Trans } from "react-i18next"; -/** - * Displays the initial landing page. - * - * @param {Object} props - * @param {boolean} props.isSuperAdmin - Whether the user is creating and admin account - * @param {Function} props.onContinue - Callback function to handle "Continue with Email" button click. - * @returns {JSX.Element} - */ -const LandingPage = ({ isSuperAdmin, onSignup }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - <> - - - {t("signUp")} - - {isSuperAdmin - ? t("authRegisterCreateSuperAdminAccount") - : t("authRegisterCreateAccount")} - - - - - - - - { - window.open( - "https://bluewavelabs.ca/terms-of-service-open-source", - "_blank", - "noreferrer" - ); - }} - /> - ), - a2: ( - { - window.open( - "https://bluewavelabs.ca/privacy-policy-open-source", - "_blank", - "noreferrer" - ); - }} - /> - ), - }} - /> - - - - - ); -}; - -LandingPage.propTypes = { - isSuperAdmin: PropTypes.bool, - onSignup: PropTypes.func, -}; - -const Register = ({ isSuperAdmin }) => { - const dispatch = useDispatch(); - const navigate = useNavigate(); - const { token } = useParams(); - const theme = useTheme(); - const { t } = useTranslation(); - // TODO If possible, change the IDs of these fields to match the backend - const idMap = { - "register-firstname-input": "firstName", - "register-lastname-input": "lastName", - "register-email-input": "email", - "register-password-input": "password", - "register-confirm-input": "confirm", - }; - - const [form, setForm] = useState({ - firstName: "", - lastName: "", - email: "", - password: "", - confirm: "", - role: [], - teamId: "", - }); - const [errors, setErrors] = useState({}); - const [step, setStep] = useState(0); - - useEffect(() => { - const fetchInvite = async () => { - if (token !== undefined) { - try { - const res = await networkService.verifyInvitationToken(token); - const invite = res.data.data; - const { email } = invite; - setForm({ ...form, email }); - } catch (error) { - navigate("/register", { replace: true }); - } - } - }; - fetchInvite(); - }, []); - - /** - * Validates the form data against the validation schema. - * - * @param {Object} data - The form data to validate. - * @param {Object} [options] - Optional settings for validation. - * @returns {Object | undefined} - Returns the validation error object if there are validation errors; otherwise, `undefined`. - */ - const validateForm = (data, options = {}) => { - const { error } = credentials.validate(data, { - abortEarly: false, - ...options, - }); - return error; - }; - - /** - * Handles validation errors by setting the state with error messages and displaying a toast notification. - * - * @param {Object} error - The validation error object returned from the validation schema. - */ - const handleError = (error) => { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: error.details[0].message || "Error validating data." }); - }; - - const handleStepOne = async (e) => { - e.preventDefault(); - let error = validateForm({ - firstName: form.firstName, - lastName: form.lastName, - }); - - if (error) { - handleError(error); - return; - } - - setStep(2); - }; - - const handleStepTwo = async (e) => { - e.preventDefault(); - - let error; - error = validateForm({ email: form.email }); - if (error) { - handleError(error); - return; - } - - setStep(3); - }; - - // Final step - // Attempts account registration - const handleStepThree = async (e) => { - e.preventDefault(); - const { password, confirm } = e.target.elements; - let registerForm = { - ...form, - password: password.value, - confirm: confirm.value, - role: isSuperAdmin ? ["superadmin"] : form.role, - inviteToken: token ? token : "", // Add the token to the request for verification - }; - let error = validateForm(registerForm, { - context: { password: registerForm.password }, - }); - if (error) { - handleError(error); - return; - } - - delete registerForm.confirm; - const action = await dispatch(register(registerForm)); - if (action.payload.success) { - const authToken = action.payload.data; - navigate("/uptime"); - createToast({ - body: "Welcome! Your account was created successfully.", - }); - } else { - if (action.payload) { - createToast({ - body: action.payload.msg, - }); - } else { - createToast({ - body: "Unknown error.", - }); - } - } - }; - - const handleChange = (event) => { - const { value, id } = event.target; - const name = idMap[id]; - const lowerCasedValue = - name === idMap["register-email-input"] ? value?.toLowerCase() || value : value; - setForm((prev) => ({ - ...prev, - [name]: lowerCasedValue, - })); - - const { error } = credentials.validate( - { [name]: lowerCasedValue }, - { abortEarly: false, context: { password: form.password } } - ); - - setErrors((prev) => { - const prevErrors = { ...prev }; - if (error) prevErrors[name] = error.details[0].message; - else delete prevErrors[name]; - return prevErrors; - }); - }; - - return ( - - - - - - - {t("commonAppName")} - - .MuiStack-root": { - border: 1, - borderRadius: theme.spacing(5), - borderColor: theme.palette.primary.lowContrast, - backgroundColor: theme.palette.primary.main, - padding: { - xs: theme.spacing(12), - sm: theme.spacing(20), - }, - }, - }} - > - {step === 0 ? ( - setStep(1)} - /> - ) : step === 1 ? ( - setStep(0)} - /> - ) : step === 2 ? ( - setStep(1)} - /> - ) : step === 3 ? ( - setStep(2)} - /> - ) : ( - "" - )} - - - - {t("authRegisterAlreadyHaveAccount")} - - { - navigate("/login"); - }} - sx={{ userSelect: "none", color: theme.palette.accent.main }} - > - {t("authRegisterLoginLink")} - - - - ); -}; -Register.propTypes = { - isSuperAdmin: PropTypes.bool, -}; -export default Register; diff --git a/client/src/Pages/Auth/Register/StepOne/index.jsx b/client/src/Pages/Auth/Register/StepOne/index.jsx deleted file mode 100644 index cb66f6704..000000000 --- a/client/src/Pages/Auth/Register/StepOne/index.jsx +++ /dev/null @@ -1,136 +0,0 @@ -import { useEffect, useRef } from "react"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import { useTranslation } from "react-i18next"; - -StepOne.propTypes = { - form: PropTypes.object, - errors: PropTypes.object, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onBack: PropTypes.func, -}; - -/** - * Renders the first step of the sign up process. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ - -function StepOne({ form, errors, onSubmit, onChange, onBack }) { - const theme = useTheme(); - const inputRef = useRef(null); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - {/* TODO this stack should be a component */} - - - {t("signUp")} - {t("authRegisterStepOnePersonalDetails")} - - - - - - - - - {/* TODO buttons should be a component should be a component */} - - - - - - - ); -} - -export { StepOne }; diff --git a/client/src/Pages/Auth/Register/StepThree/index.jsx b/client/src/Pages/Auth/Register/StepThree/index.jsx deleted file mode 100644 index 1af8a60dc..000000000 --- a/client/src/Pages/Auth/Register/StepThree/index.jsx +++ /dev/null @@ -1,173 +0,0 @@ -import { useEffect, useRef } from "react"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import Check from "../../../../Components/Check/Check"; -import { useValidatePassword } from "../../hooks/useValidatePassword"; -import { useTranslation } from "react-i18next"; -StepThree.propTypes = { - onSubmit: PropTypes.func, - onBack: PropTypes.func, -}; - -/** - * Renders the third step of the sign up process. - * - * @param {Object} props - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -function StepThree({ onSubmit, onBack }) { - const theme = useTheme(); - const inputRef = useRef(null); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - const { handleChange, feedbacks, form, errors } = useValidatePassword(); - return ( - <> - - - {t("signUp")} - {t("createPassword")} - - - - - - - - - - - - - - - - - - - - - - ); -} - -export { StepThree }; diff --git a/client/src/Pages/Auth/Register/StepTwo/index.jsx b/client/src/Pages/Auth/Register/StepTwo/index.jsx deleted file mode 100644 index 4fa0e8ebc..000000000 --- a/client/src/Pages/Auth/Register/StepTwo/index.jsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useEffect, useRef } from "react"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { Box, Button, Stack, Typography } from "@mui/material"; -import ArrowBackRoundedIcon from "@mui/icons-material/ArrowBackRounded"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import { useTranslation } from "react-i18next"; - -StepTwo.propTypes = { - form: PropTypes.object, - errors: PropTypes.object, - onSubmit: PropTypes.func, - onChange: PropTypes.func, - onBack: PropTypes.func, -}; - -/** - * Renders the second step of the sign up process. - * - * @param {Object} props - * @param {Object} props.form - Form state object. - * @param {Object} props.errors - Object containing form validation errors. - * @param {Function} props.onSubmit - Callback function to handle form submission. - * @param {Function} props.onChange - Callback function to handle form input changes. - * @param {Function} props.onBack - Callback function to handle "Back" button click. - * @returns {JSX.Element} - */ -function StepTwo({ form, errors, onSubmit, onChange, onBack }) { - const theme = useTheme(); - const inputRef = useRef(null); - const { t } = useTranslation(); - - useEffect(() => { - if (inputRef.current) { - inputRef.current.focus(); - } - }, []); - - return ( - <> - - - {t("signUp")} - {t("enterEmail")} - - - - (e.target.value = e.target.value.toLowerCase())} - onChange={onChange} - error={errors.email ? true : false} - helperText={ - errors.email && - (errors.email.includes("required") - ? t("authRegisterEmailRequired") - : errors.email.includes("valid email") - ? t("authRegisterEmailInvalid") - : t(errors.email)) - } - ref={inputRef} - /> - - - - - - - - ); -} - -export { StepTwo }; diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx new file mode 100644 index 000000000..97b27e5cf --- /dev/null +++ b/client/src/Pages/Auth/Register/index.jsx @@ -0,0 +1,353 @@ +// Components +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import AuthHeader from "../components/AuthHeader"; +import TextInput from "../../../Components/Inputs/TextInput"; +import Check from "../../../Components/Check/Check"; +import Button from "@mui/material/Button"; + +// Utils +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { useDispatch, useSelector } from "react-redux"; +import { useParams } from "react-router-dom"; +import { networkService } from "../../../main"; +import { newOrChangedCredentials } from "../../../Validation/validation"; +import { register } from "../../../Features/Auth/authSlice"; +import { createToast } from "../../../Utils/toastUtils"; +import PropTypes from "prop-types"; + +const getFeedbackStatus = (form, errors, field, criteria) => { + const fieldErrors = errors?.[field]; + const isFieldEmpty = form?.[field]?.length === 0; + const hasError = fieldErrors?.includes(criteria) || fieldErrors?.includes("empty"); + const isCorrect = !isFieldEmpty && !hasError; + + if (isCorrect) { + return "success"; + } else if (hasError) { + return "error"; + } else { + return "info"; + } +}; + +const Register = ({ superAdminExists }) => { + // Redux + const { isLoading } = useSelector((state) => state.auth); + + // Local state + const [form, setForm] = useState({ + firstName: "", + lastName: "", + email: "", + password: "", + confirm: "", + role: [], + teamId: "", + }); + + const [errors, setErrors] = useState({}); + const [feedback, setFeedback] = useState({}); + + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + const { token } = useParams(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + + // Effects + useEffect(() => { + const fetchInvite = async () => { + if (token !== undefined) { + try { + const res = await networkService.verifyInvitationToken(token); + const invite = res.data.data; + const { email } = invite; + setForm((prevForm) => { + if (!prevForm.email) { + return { ...prevForm, email }; + } + return prevForm; + }); + } catch (error) { + navigate("/register", { replace: true }); + } + } + }; + fetchInvite(); + }, [form, token, navigate]); + + // Handlers + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") value = value.toLowerCase(); + const updatedForm = { ...form, [name]: value }; + + const { error } = newOrChangedCredentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: form.password } } + ); + + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + + setForm(updatedForm); + }; + + const onPasswordChange = (e) => { + const { name, value } = e.target; + const updatedForm = { ...form, [name]: value }; + setForm(updatedForm); + const validateValue = { [name]: value }; + const validateOptions = { abortEarly: false, context: { password: form.password } }; + if (name === "password" && form.confirm.length > 0) { + validateValue.confirm = form.confirm; + validateOptions.context = { password: value }; + } else if (name === "confirm") { + validateValue.password = form.password; + } + const { error } = newOrChangedCredentials.validate(validateValue, validateOptions); + + const pwdErrors = error?.details.map((error) => ({ + path: error.path[0], + type: error.type, + })); + + const errorsByPath = + pwdErrors && + pwdErrors.reduce((acc, { path, type }) => { + if (!acc[path]) { + acc[path] = []; + } + acc[path].push(type); + return acc; + }, {}); + + const oldErrors = { ...errors }; + if (name === "password") { + oldErrors.password = undefined; + } else if (name === "confirm") { + oldErrors.confirm = undefined; + } + const newErrors = { ...oldErrors, ...errorsByPath }; + + setErrors(newErrors); + + const newFeedback = { + length: getFeedbackStatus(updatedForm, errorsByPath, "password", "string.min"), + special: getFeedbackStatus(updatedForm, errorsByPath, "password", "special"), + number: getFeedbackStatus(updatedForm, errorsByPath, "password", "number"), + uppercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "uppercase"), + lowercase: getFeedbackStatus(updatedForm, errorsByPath, "password", "lowercase"), + confirm: getFeedbackStatus(updatedForm, errorsByPath, "confirm", "different"), + }; + + setFeedback(newFeedback); + }; + + const onSubmit = async (e) => { + e.preventDefault(); + const toSubmit = { + ...form, + role: superAdminExists ? form.role : ["superadmin"], + inviteToken: token ? token : "", + }; + + const { error } = newOrChangedCredentials.validate(toSubmit, { + abortEarly: false, + context: { password: form.password }, + }); + + if (error) { + const formErrors = {}; + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + setErrors(formErrors); + return; + } + + delete toSubmit.confirm; + + const action = await dispatch(register(toSubmit)); + if (action.payload.success) { + navigate("/uptime"); + createToast({ + body: t("auth.registration.toasts.success"), + }); + } else { + if (action.payload) { + createToast({ + body: action.payload.msg, + }); + } else { + createToast({ + body: t("common.toasts.unknownError"), + }); + } + } + }; + + return ( + + + + {t("auth.registration.heading.user")} + + + + {superAdminExists + ? t("auth.registration.heading.user") + : t("auth.registration.heading.superAdmin")} + + + {superAdminExists + ? t("auth.registration.description.user") + : t("auth.registration.description.superAdmin")} + + (e.target.value = e.target.value.toLowerCase())} + onChange={onChange} + error={errors.email ? true : false} + helperText={errors.email ? t(errors.email) : ""} // Localization keys are in validation.js + /> + + + + + + + + + + + + + + + + + ); +}; + +Register.propTypes = { + superAdminExists: PropTypes.bool, +}; + +export default Register; diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx index 4467a4adf..b4899f47c 100644 --- a/client/src/Pages/Auth/SetNewPassword.jsx +++ b/client/src/Pages/Auth/SetNewPassword.jsx @@ -6,7 +6,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; import { setNewPassword } from "../../Features/Auth/authSlice"; import { createToast } from "../../Utils/toastUtils"; -import { credentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../Validation/validation"; import Check from "../../Components/Check/Check"; import TextInput from "../../Components/Inputs/TextInput"; import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; @@ -16,7 +16,7 @@ import Logo from "../../assets/icons/checkmate-icon.svg?react"; import Background from "../../assets/Images/background-grid.svg?react"; import "./index.css"; import { useValidatePassword } from "./hooks/useValidatePassword"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; const SetNewPassword = () => { const navigate = useNavigate(); @@ -34,7 +34,7 @@ const SetNewPassword = () => { const handleSubmit = async (e) => { e.preventDefault(); - const { error } = credentials.validate(form, { + const { error } = newOrChangedCredentials.validate(form, { abortEarly: false, context: { password: form.password }, }); @@ -43,20 +43,20 @@ const SetNewPassword = () => { createToast({ body: error.details && error.details.length > 0 - ? error.details[0].message - : "Error validating data.", + ? error.details[0].message // FIXME: Potential untranslated string + : t("auth.common.errors.validation"), }); } else { const action = await dispatch(setNewPassword({ token, form })); if (action.payload.success) { navigate("/new-password-confirmed"); createToast({ - body: "Your password was reset successfully.", + body: t("auth.forgotPassword.toasts.success"), }); } else { const errorMessage = action.payload - ? action.payload.msg - : "Unable to reset password. Please try again later or contact support."; + ? action.payload.msg // FIXME: Potential untranslated string + : t("auth.forgotPassword.toasts.error"); createToast({ body: errorMessage, }); @@ -98,7 +98,7 @@ const SetNewPassword = () => { gap={theme.spacing(4)} > - {t("commonAppName")} + {t("common.appName")} { svgHeight={24} mb={theme.spacing(4)} > - + - {t("authSetNewPasswordTitle")} - {t("authSetNewPasswordDescription")} + {t("auth.forgotPassword.heading")} + {t("auth.forgotPassword.subheadings.stepThree")} { id={passwordId} type="password" name="password" - label={t("commonPassword")} + label={t("auth.common.inputs.password.label")} isRequired={true} placeholder="••••••••" value={form.password} onChange={handleChange} error={errors.password ? true : false} - helperText={errors.password} + helperText={ + errors.password === "auth.common.inputs.password.errors.empty" + ? t(errors.password) + : "" + } // Other errors are related to required password conditions and are visualized below the input endAdornment={} /> @@ -185,13 +189,13 @@ const SetNewPassword = () => { id={confirmPasswordId} type="password" name="confirm" - label={t("authSetNewPasswordConfirmPassword")} + label={t("auth.common.inputs.passwordConfirm.label")} isRequired={true} - placeholder="••••••••" + placeholder={t("auth.common.inputs.passwordConfirm.placeholder")} value={form.confirm} onChange={handleChange} error={errors.confirm ? true : false} - helperText={errors.confirm} + helperText={t(errors.confirm)} // Localization keys are in validation.js endAdornment={} /> @@ -200,33 +204,37 @@ const SetNewPassword = () => { mb={theme.spacing(12)} > @@ -243,7 +251,7 @@ const SetNewPassword = () => { } sx={{ width: "100%", maxWidth: 400 }} > - {t("authSetNewPasswordResetPassword")} + {t("auth.forgotPassword.buttons.resetPassword")} @@ -251,15 +259,21 @@ const SetNewPassword = () => { textAlign="center" p={theme.spacing(12)} > - {t("goBackTo")} — - navigate("/login")} - sx={{ userSelect: "none" }} - > - {t("authLoginTitle")} + + navigate("/login")} + sx={{ userSelect: "none" }} + /> + ), + }} + /> diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx new file mode 100644 index 000000000..c6ad46a47 --- /dev/null +++ b/client/src/Pages/Auth/components/AuthHeader.jsx @@ -0,0 +1,45 @@ +// Components +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import LanguageSelector from "../../../Components/LanguageSelector"; +import ThemeSwitch from "../../../Components/ThemeSwitch"; + +// Utils +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; + +const AuthHeader = () => { + // Hooks + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + + {t("common.appName")} + + + + + + + ); +}; + +export default AuthHeader; diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx index b49d0944b..102d8d73e 100644 --- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx +++ b/client/src/Pages/Auth/hooks/useValidatePassword.jsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { credentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation"; const getFeedbackStatus = (form, errors, field, criteria) => { const fieldErrors = errors[field]; @@ -36,7 +36,7 @@ function useValidatePassword() { validateValue.password = form.password; } - const { error } = credentials.validate(validateValue, validateOptions); + const { error } = newOrChangedCredentials.validate(validateValue, validateOptions); const errors = error?.details.map((error) => ({ path: error.path[0], type: error.type, diff --git a/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx b/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx deleted file mode 100644 index b2634e9a7..000000000 --- a/client/src/Pages/DistributedUptime/Create/Hooks/useCreateDistributedUptimeMonitor.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useState } from "react"; -import { networkService } from "../../../../main"; -import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; - -const useCreateDistributedUptimeMonitor = ({ isCreate, monitorId }) => { - const [isLoading, setIsLoading] = useState(false); - const [networkError, setNetworkError] = useState(false); - const createDistributedUptimeMonitor = async ({ form }) => { - setIsLoading(true); - try { - if (isCreate) { - await networkService.createMonitor({ monitor: form }); - } else { - await networkService.updateMonitor({ monitor: form, monitorId }); - } - - return true; - } catch (error) { - setNetworkError(true); - createToast({ body: error?.response?.data?.msg ?? error.message }); - return false; - } finally { - setIsLoading(false); - } - }; - - return [createDistributedUptimeMonitor, isLoading, networkError]; -}; - -export { useCreateDistributedUptimeMonitor }; diff --git a/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx b/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx deleted file mode 100644 index efffbe437..000000000 --- a/client/src/Pages/DistributedUptime/Create/Hooks/useMonitorFetch.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { useEffect, useState } from "react"; -import { networkService } from "../../../../main"; -import { createToast } from "../../../../Utils/toastUtils"; - -export const useMonitorFetch = ({ monitorId, isCreate }) => { - const [networkError, setNetworkError] = useState(false); - const [isLoading, setIsLoading] = useState(true); - const [monitor, setMonitor] = useState(undefined); - - useEffect(() => { - const fetchMonitors = async () => { - try { - if (isCreate) return; - const res = await networkService.getUptimeDetailsById({ - monitorId: monitorId, - normalize: true, - }); - setMonitor(res?.data?.data ?? {}); - } catch (error) { - setNetworkError(true); - createToast({ body: error.message }); - } finally { - setIsLoading(false); - } - }; - fetchMonitors(); - }, [monitorId, isCreate]); - return [monitor, isLoading, networkError]; -}; - -export default useMonitorFetch; diff --git a/client/src/Pages/DistributedUptime/Create/index.jsx b/client/src/Pages/DistributedUptime/Create/index.jsx deleted file mode 100644 index aff2a7d22..000000000 --- a/client/src/Pages/DistributedUptime/Create/index.jsx +++ /dev/null @@ -1,349 +0,0 @@ -// Components -import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; - -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import ConfigBox from "../../../Components/ConfigBox"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Radio from "../../../Components/Inputs/Radio"; -import Checkbox from "../../../Components/Inputs/Checkbox"; -import Select from "../../../Components/Inputs/Select"; -import { createToast } from "../../../Utils/toastUtils"; - -// Utility -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import { useNavigate } from "react-router-dom"; -import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../Validation/validation"; -import { useParams } from "react-router-dom"; -import { useCreateDistributedUptimeMonitor } from "./Hooks/useCreateDistributedUptimeMonitor"; -import { useMonitorFetch } from "./Hooks/useMonitorFetch"; -import { useTranslation } from "react-i18next"; - -// Constants -const MS_PER_MINUTE = 60000; -const SELECT_VALUES = [ - { _id: 1, name: "1 minute" }, - { _id: 2, name: "2 minutes" }, - { _id: 3, name: "3 minutes" }, - { _id: 4, name: "4 minutes" }, - { _id: 5, name: "5 minutes" }, -]; - -const parseUrl = (url) => { - try { - return new URL(url); - } catch (error) { - return null; - } -}; - -const CreateDistributedUptime = () => { - const { monitorId } = useParams(); - const isCreate = typeof monitorId === "undefined"; - const { t } = useTranslation(); - - let BREADCRUMBS = [ - { name: `distributed uptime`, path: "/distributed-uptime" }, - ...(isCreate ? [] : [{ name: "details", path: `/distributed-uptime/${monitorId}` }]), - { name: isCreate ? "create" : "configure", path: `` }, - ]; - - // Redux state - const { user } = useSelector((state) => state.auth); - - // Local state - const [https, setHttps] = useState(true); - const [notifications, setNotifications] = useState([]); - const [form, setForm] = useState({ - type: "distributed_http", - name: "", - url: "", - interval: 1, - }); - const [errors, setErrors] = useState({}); - - //utils - const theme = useTheme(); - const navigate = useNavigate(); - const [createDistributedUptimeMonitor, isLoading, networkError] = - useCreateDistributedUptimeMonitor({ isCreate, monitorId }); - - const [monitor, monitorIsLoading, monitorNetworkError] = useMonitorFetch({ - monitorId, - isCreate, - }); - - // Effect to set monitor to fetched monitor - useEffect(() => { - if (typeof monitor !== "undefined") { - const parsedUrl = parseUrl(monitor?.url); - const protocol = parsedUrl?.protocol?.replace(":", "") || ""; - setHttps(protocol === "https"); - - const newForm = { - name: monitor.name, - interval: monitor.interval / MS_PER_MINUTE, - url: parsedUrl.host, - type: monitor.type, - }; - - setForm(newForm); - } - }, [monitor]); - - // Handlers - const handleCreateMonitor = async () => { - const monitorToSubmit = { ...form }; - - // Prepend protocol to url - monitorToSubmit.url = `http${https ? "s" : ""}://` + monitorToSubmit.url; - - const { error } = monitorValidation.validate(monitorToSubmit, { - abortEarly: false, - }); - if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); - return; - } - - // Append needed fields - monitorToSubmit.description = form.name; - monitorToSubmit.interval = form.interval * MS_PER_MINUTE; - monitorToSubmit.teamId = user.teamId; - monitorToSubmit.userId = user._id; - monitorToSubmit.notifications = notifications; - - const success = await createDistributedUptimeMonitor({ form: monitorToSubmit }); - if (success) { - createToast({ body: "Monitor created successfully!" }); - navigate("/distributed-uptime"); - } else { - createToast({ body: "Failed to create monitor." }); - } - }; - - const handleChange = (event) => { - let { name, value } = event.target; - - setForm({ - ...form, - [name]: value, - }); - const { error } = monitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const handleNotifications = (event, type) => { - const { value } = event.target; - let currentNotifications = [...notifications]; - const notificationAlreadyExists = notifications.some((notification) => { - if (notification.type === type && notification.address === value) { - return true; - } - return false; - }); - if (notificationAlreadyExists) { - currentNotifications = currentNotifications.filter((notification) => { - if (notification.type === type && notification.address === value) { - return false; - } - return true; - }); - } else { - currentNotifications.push({ type, address: value }); - } - setNotifications(currentNotifications); - }; - - return ( - - - console.log("submit")} - > - - - {isCreate ? "Create your" : "Edit your"}{" "} - - - {t("monitor")} - - - - - {t("settingsGeneralSettings")} - {t("distributedUptimeCreateSelectURL")} - - - } - label="URL to monitor" - https={https} - placeholder={"www.google.com"} - disabled={!isCreate} - value={form.url} - name="url" - onChange={handleChange} - error={errors["url"] ? true : false} - helperText={errors["url"]} - /> - - - - - - {t("distributedUptimeCreateChecks")} - - {t("distributedUptimeCreateChecksDescription")} - - - - - - {form.type === "http" || form.type === "distributed_http" ? ( - - - - - ) : ( - "" - )} - - - {errors["type"] ? ( - - - {errors["type"]} - - - ) : ( - "" - )} - - - - - - {t("distributedUptimeCreateIncidentNotification")} - - - {t("distributedUptimeCreateIncidentDescription")} - - - - notification.type === "email" - )} - value={user?.email} - onChange={(event) => handleNotifications(event, "email")} - /> - - - - - - {t("distributedUptimeCreateAdvancedSettings")} - - - - - ); -}; - -export default VisuallyHiddenInput; diff --git a/client/src/Pages/DistributedUptimeStatus/Create/index.jsx b/client/src/Pages/DistributedUptimeStatus/Create/index.jsx deleted file mode 100644 index 308f9489b..000000000 --- a/client/src/Pages/DistributedUptimeStatus/Create/index.jsx +++ /dev/null @@ -1,322 +0,0 @@ -// Components -import { Stack, Typography, Button, Box } from "@mui/material"; -import ConfigBox from "../../../Components/ConfigBox"; -import Checkbox from "../../../Components/Inputs/Checkbox"; -import TextInput from "../../../Components/Inputs/TextInput"; -import VisuallyHiddenInput from "./Components/VisuallyHiddenInput"; -import Image from "../../../Components/Image"; -import LogoPlaceholder from "../../../assets/Images/logo_placeholder.svg"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import Search from "../../../Components/Inputs/Search"; -import MonitorList from "../../StatusPage/Create/Components/MonitorList"; -// Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import { useParams } from "react-router-dom"; -import { useCreateStatusPage } from "../../StatusPage/Create/Hooks/useCreateStatusPage"; -import { statusPageValidation } from "../../../Validation/validation"; -import { buildErrors } from "../../../Validation/error"; -import { createToast } from "../../../Utils/toastUtils"; -import { useNavigate } from "react-router-dom"; -import { useMonitorsFetch } from "../../StatusPage/Create/Hooks/useMonitorsFetch"; -import { useTranslation } from "react-i18next"; -import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage"; -const CreateStatus = () => { - const theme = useTheme(); - const { monitorId, url } = useParams(); - const navigate = useNavigate(); - const { t } = useTranslation(); - const isCreate = typeof url === "undefined"; - - const [createStatusPage, isLoading, networkError] = useCreateStatusPage(isCreate); - - const [statusPageIsLoading, statusPageNetworkError, statusPage, _, isPublished] = - useFetchDepinStatusPage({ - url, - timeFrame: 30, - isCreate, - }); - - const [monitors, monitorsIsLoading, monitorsNetworkError] = useMonitorsFetch(); - - const BREADCRUMBS = [ - { name: "distributed uptime", path: "/distributed-uptime" }, - { - name: "details", - path: `/distributed-uptime/${isCreate ? monitorId : statusPage?.monitors[0]}`, - }, - { name: isCreate ? "create status page" : "edit status page", path: `` }, - ]; - // Local state - const [form, setForm] = useState({ - type: "distributed", - isPublished: false, - url: url ?? Math.floor(Math.random() * 1000000).toFixed(0), - logo: undefined, - companyName: "", - monitors: [monitorId], - }); - const [errors, setErrors] = useState({}); - const [search, setSearch] = useState(""); - const [selectedMonitors, setSelectedMonitors] = useState([]); - - const handleFormChange = (e) => { - const { name, value, checked, type } = e.target; - - // Check for errors - const { error } = statusPageValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - - setErrors((prev) => buildErrors(prev, name, error)); - - if (type === "checkbox") { - setForm({ ...form, [name]: checked }); - return; - } - setForm({ ...form, [name]: value }); - }; - - const handleMonitorsChange = (selectedMonitors) => { - handleFormChange({ - target: { - name: "subMonitors", - value: selectedMonitors.map((monitor) => monitor._id), - }, - }); - setSelectedMonitors(selectedMonitors); - }; - - const handleImageUpload = (e) => { - const img = e.target?.files?.[0]; - setForm((prev) => ({ - ...prev, - logo: img, - })); - }; - const handleSubmit = async () => { - let logoToSubmit = undefined; - - // Handle image - if (typeof form.logo !== "undefined" && typeof form.logo.src === "undefined") { - logoToSubmit = { - src: URL.createObjectURL(form.logo), - name: form.logo.name, - type: form.logo.type, - size: form.logo.size, - }; - } else if (typeof form.logo !== "undefined") { - logoToSubmit = form.logo; - } - const formToSubmit = { ...form }; - if (typeof logoToSubmit !== "undefined") { - formToSubmit.logo = logoToSubmit; - } - // Validate - const { error } = statusPageValidation.validate(formToSubmit, { abortEarly: false }); - if (typeof error === "undefined") { - const success = await createStatusPage({ form: formToSubmit }); - if (success) { - const verb = isCreate ? "created" : "updated"; - createToast({ body: `Status page ${verb} successfully` }); - navigate(`/status/distributed/${form.url}`); - } - return; - } - const newErrors = {}; - error?.details?.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors((prev) => ({ ...prev, ...newErrors })); - }; - - // If we are configuring, populate fields - useEffect(() => { - if (isCreate) return; - if (typeof statusPage === "undefined") { - return; - } - - let newLogo = undefined; - if (statusPage.logo) { - newLogo = { - src: `data:${statusPage.logo.contentType};base64,${statusPage.logo.data}`, - name: "logo", - type: statusPage.logo.contentType, - size: null, - }; - } - - setForm((prev) => { - return { - ...prev, - companyName: statusPage?.companyName, - isPublished: statusPage?.isPublished, - timezone: statusPage?.timezone, - monitors: statusPage?.monitors, - subMonitors: statusPage?.subMonitors.map((monitor) => monitor._id), - color: statusPage?.color, - logo: newLogo, - }; - }); - setSelectedMonitors(statusPage?.subMonitors); - }, [isCreate, statusPage]); - - const imgSrc = form?.logo?.src - ? form.logo.src - : form.logo - ? URL.createObjectURL(form.logo) - : undefined; - - return ( - - - - - {isCreate - ? t("distributedUptimeStatusCreateYour") - : t("distributedUptimeStatusEditYour")}{" "} - - - {t("distributedUptimeStatusCreateStatusPage")} - - - - - - {t("distributedUptimeStatusCreateStatusPageAccess")} - - - {t("distributedUptimeStatusCreateStatusPageReady")} - - - - - - - - - - {t("distributedUptimeStatusBasicInfoHeader")} - - - {t("distributedUptimeStatusBasicInfoDescription")} - - - - - - - - - - {t("distributedUptimeStatusLogoHeader")} - - {t("distributedUptimeStatusLogoDescription")}{" "} - - - - Logo - - - - - - - - - {t("distributedUptimeStatusStandardMonitorsHeader")} - - - {t("distributedUptimeStatusStandardMonitorsDescription")} - - - - - - - - - - - - ); -}; - -export default CreateStatus; diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx deleted file mode 100644 index d9ed16e63..000000000 --- a/client/src/Pages/DistributedUptimeStatus/Status/Components/MonitorsList/index.jsx +++ /dev/null @@ -1,66 +0,0 @@ -// Components -import { Stack, Box } from "@mui/material"; -import Host from "../../../../../Components/Host"; -import DePINStatusPageBarChart from "../../../../../Components/Charts/DePINStatusPageBarChart"; -import { StatusLabel } from "../../../../../Components/Label"; - -//Utils -import { useTheme } from "@mui/material/styles"; -import useUtils from "../../../../Uptime/Monitors/Hooks/useUtils"; -import PropTypes from "prop-types"; -const MonitorsList = ({ - isLoading = false, - shouldRender = true, - monitors = [], - timeFrame, -}) => { - const theme = useTheme(); - const { determineState } = useUtils(); - return ( - <> - {monitors?.map((monitor) => { - const status = determineState(monitor); - return ( - - - - - - - - - - - - ); - })} - - ); -}; - -MonitorsList.propTypes = { - monitors: PropTypes.array.isRequired, -}; - -export default MonitorsList; diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx deleted file mode 100644 index 233fc3c06..000000000 --- a/client/src/Pages/DistributedUptimeStatus/Status/Components/Skeleton/index.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Stack, Skeleton } from "@mui/material"; - -export const SkeletonLayout = () => { - return ( - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx deleted file mode 100644 index 7ee4ff292..000000000 --- a/client/src/Pages/DistributedUptimeStatus/Status/Components/TimeframeHeader/index.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Stack, Button, ButtonGroup } from "@mui/material"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; - -const TimeFrameHeader = ({ timeFrame, setTimeFrame, sx, ...props }) => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( - - - - - - - - ); -}; - -export default TimeFrameHeader; diff --git a/client/src/Pages/DistributedUptimeStatus/Status/index.jsx b/client/src/Pages/DistributedUptimeStatus/Status/index.jsx deleted file mode 100644 index 117a297e5..000000000 --- a/client/src/Pages/DistributedUptimeStatus/Status/index.jsx +++ /dev/null @@ -1,319 +0,0 @@ -//Components -import DistributedUptimeMap from "../../DistributedUptime/Details/Components/DistributedUptimeMap"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { Stack, Typography } from "@mui/material"; -import DeviceTicker from "../../DistributedUptime/Details/Components/DeviceTicker"; -import DistributedUptimeResponseChart from "../../DistributedUptime/Details/Components/DistributedUptimeResponseChart"; -import NextExpectedCheck from "../../DistributedUptime/Details/Components/NextExpectedCheck"; -import Footer from "../../DistributedUptime/Details/Components/Footer"; -import StatBoxes from "../../DistributedUptime/Details/Components/StatBoxes"; -import ControlsHeader from "../../StatusPage/Status/Components/ControlsHeader"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import GenericFallback from "../../../Components/GenericFallback"; -import Dialog from "../../../Components/Dialog"; -import SkeletonLayout from "./Components/Skeleton"; -import UptLogo from "../../../assets/icons/upt_logo.png"; -import PeopleAltOutlinedIcon from "@mui/icons-material/PeopleAltOutlined"; -import InfoBox from "../../../Components/InfoBox"; -import StatusHeader from "../../DistributedUptime/Details/Components/StatusHeader"; -import MonitorsList from "./Components/MonitorsList"; -import { RowContainer } from "../../../Components/StandardContainer"; - -//Utils -import { useTheme } from "@mui/material/styles"; -import { useState, useEffect, useRef } from "react"; -import { useParams } from "react-router-dom"; -import { useFetchDepinStatusPage } from "../../../Hooks/useFetchDepinStatusPage"; -import { useSubscribeToDepinDetails } from "../../../Hooks/useSubscribeToDepinDetails"; -import { useStatusPageDelete } from "../../StatusPage/Status/Hooks/useStatusPageDelete"; -import TimeFrameHeader from "./Components/TimeframeHeader"; -import SubHeader from "../../../Components/Subheader"; -import { safelyParseFloat } from "../../../Utils/utils"; -import { useNavigate, useLocation } from "react-router-dom"; -import { useTranslation } from "react-i18next"; -import { useSelector, useDispatch } from "react-redux"; -import { setMode } from "../../../Features/UI/uiSlice"; - -// Responsive -import { useMediaQuery } from "@mui/material"; - -const DistributedUptimeStatus = () => { - const { url } = useParams(); - const location = useLocation(); - const { t } = useTranslation(); - const isPublic = location.pathname.startsWith("/status/distributed/public"); - const elementToCapture = useRef(null); - - // Redux state - const mode = useSelector((state) => state.ui.mode); - const originalModeRef = useRef(null); - - // Local State - const [dateRange, setDateRange] = useState("recent"); - const [isDeleteOpen, setIsDeleteOpen] = useState(false); - const [timeFrame, setTimeFrame] = useState(30); - // Utils - const theme = useTheme(); - const isSmallScreen = useMediaQuery(theme.breakpoints.down("sm")); - const navigate = useNavigate(); - const dispatch = useDispatch(); - - const [ - statusPageIsLoading, - statusPageNetworkError, - statusPage, - monitorId, - isPublished, - ] = useFetchDepinStatusPage({ - url, - timeFrame, - }); - - const [isLoading, networkError, connectionStatus, monitor, lastUpdateTrigger] = - useSubscribeToDepinDetails({ monitorId, dateRange, isPublic, isPublished }); - - const [deleteStatusPage, isDeleting] = useStatusPageDelete(() => { - navigate("/distributed-uptime"); - }, url); - // Constants - const BREADCRUMBS = [ - { name: "Distributed Uptime", path: "/distributed-uptime" }, - { name: "details", path: `/distributed-uptime/${monitorId}` }, - { name: "status", path: `` }, - ]; - - let sx = {}; - if (isPublic) { - sx = { - paddingTop: "10vh", - paddingRight: "10vw", - paddingBottom: "10vh", - paddingLeft: "10vw", - }; - } - - // Default to dark mode - useEffect(() => { - const cleanup = () => { - if (originalModeRef.current === null) { - originalModeRef.current = mode; - } - - if (isPublic) { - dispatch(setMode(originalModeRef.current)); - } - }; - - if (isPublic) { - dispatch(setMode("dark")); - } - - window.addEventListener("beforeunload", cleanup); - return () => { - window.removeEventListener("beforeunload", cleanup); - }; - }, [dispatch, isPublic]); - - // Done loading, a status page doesn't exist - if (!statusPageIsLoading && typeof statusPage === "undefined") { - return ( - - - - {t("distributedUptimeStatusPageNotSetUp")} - - {t("distributedUptimeStatusContactAdmin")} - - - ); - } - - // Done loading, a status page exists but is not public - if (!statusPageIsLoading && isPublic && statusPage.isPublished === false) { - return ( - - - {t("distributedUptimeStatusPageNotPublic")} - - - ); - } - - if (isLoading || statusPageIsLoading) { - return ; - } - - if (networkError || statusPageNetworkError) { - return ( - - - {t("networkError")} - - {t("checkConnection")} - - ); - } - - if ( - typeof statusPage === "undefined" || - typeof monitor === "undefined" || - monitor.totalChecks === 0 - ) { - return ( - - - - {t("distributedUptimeDetailsNoMonitorHistory")} - - - ); - } - - return ( - - {!isPublic && } - - - - - - - - - {t("distributedRightCategoryTitle")} - - {statusPage.companyName} - - - - - - - - - - - - - - - - - - - - - 0} - direction={{ s: "column", md: "row" }} - headerText={t("distributedStatusServerMonitors")} - subHeaderText={t("distributedStatusServerMonitorsDescription")} - gap={isSmallScreen ? theme.spacing(10) : 0} - alignItems={{ s: "flex-start", md: "flex-end" }} - > - - - - -