diff --git a/.coderabbit.yml b/.coderabbit.yml new file mode 100644 index 000000000..61b2e6361 --- /dev/null +++ b/.coderabbit.yml @@ -0,0 +1 @@ +release_notes: false diff --git a/.github/scripts/download-translations.js b/.github/scripts/download-translations.js index 7a8125ab8..185992fd5 100644 --- a/.github/scripts/download-translations.js +++ b/.github/scripts/download-translations.js @@ -7,7 +7,7 @@ import { URLSearchParams } from "url"; const API_TOKEN = process.env.POEDITOR_API; const PROJECT_ID = process.env.POEDITOR_PROJECT_ID; const LANGUAGES = ( - process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja" + process.env.LANGUAGES || "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" ).split(","); const EXPORT_FORMAT = process.env.EXPORT_FORMAT || "key_value_json"; diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index d3f34635c..056e2c586 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -16,7 +16,7 @@ jobs: - name: Install client dependencies working-directory: client - run: npm ci + run: npm install - name: Check client formatting working-directory: client @@ -34,7 +34,7 @@ jobs: - name: Install server dependencies working-directory: server - run: npm ci + run: npm install - name: Check server formatting working-directory: server diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml index cf06f7840..a9b2dfdb8 100644 --- a/.github/workflows/deploy-images-on-release.yml +++ b/.github/workflows/deploy-images-on-release.yml @@ -79,18 +79,6 @@ jobs: 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: diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml index fa39fed85..53d7ab368 100644 --- a/.github/workflows/deploy-images.yml +++ b/.github/workflows/deploy-images.yml @@ -74,18 +74,6 @@ jobs: 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: diff --git a/.github/workflows/poeditor-sync.yml b/.github/workflows/poeditor-sync.yml index a8b330290..87396d3d0 100644 --- a/.github/workflows/poeditor-sync.yml +++ b/.github/workflows/poeditor-sync.yml @@ -7,7 +7,7 @@ on: languages: description: "Languages to synchronize (comma separated, e.g.: tr,en,es)" required: false - default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja" + default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn" format: description: "Export format (key_value_json or json)" required: false diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml index 7764c488b..532197710 100644 --- a/.github/workflows/production-deploy.yml +++ b/.github/workflows/production-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-demo \ - -f ./docker/prod/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-demo - deploy-to-demo: needs: docker-build-and-push-server runs-on: ubuntu-latest diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index ea5477815..27b7e8690 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -71,17 +71,6 @@ jobs: - name: Push MongoDB Docker image run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:redis-staging \ - -f ./docker/staging/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-staging - deploy-to-staging: needs: docker-build-and-push-server runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..600d2d33b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.vscode \ No newline at end of file diff --git a/README.md b/README.md index cc1b10e0e..048cf0706 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 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. -Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. +Checkmate also has an agent, called [Capture](https://github.com/bluewave-labs/capture), to retrieve data from remote servers. While Capture is not required to run Checkmate, it provides additional insights about your servers' CPU, RAM, disk, and temperature status. Capture can run on Linux, Windows, Mac, Raspberry Pi, or any device that can run Go. Checkmate has been stress-tested with 1000+ active monitors without any particular issues or performance bottlenecks. @@ -54,6 +54,12 @@ See installation instructions in [Checkmate documentation portal](https://docs.c Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions. +### Using a Custom CA + +If you need to monitor internal HTTPS endpoints with certificates from private Certificate Authorities (like Smallstep), see our [Custom CA Trust Guide](./docs/custom-ca-trust.md) for Docker configuration options. + +For more documentation, see the [docs directory](./docs/). + ## Translations @@ -84,20 +90,22 @@ Feel free to ask questions or share your ideas - we'd love to hear from you! - Completely open source, deployable on your servers or home devices (e.g Raspberry Pi 4 or 5) - Website monitoring - Page speed monitoring -- Infrastructure monitoring (memory, disk usage, CPU performance etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent +- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent - Docker monitoring - Ping monitoring - SSL monitoring - Port monitoring +- Game server monitoring (3.0) - Incidents at a glance - Status pages -- E-mail, Webhooks, Discord, Telegram, Slack notifications +- E-mail, Webhooks, Discord and Slack notifications - Scheduled maintenance - JSON query monitoring - Multi-language support for English, German, Japanese, Portuguese (Brazil), Russian, Turkish, Ukrainian, Vietnamese, Chinese (Traditional, Taiwan) **Short term roadmap:** +- Plugins that will help Checkmate get any information from a remote service (e.g database, etc) - Better notifications - Network monitoring - ..and a few more features @@ -139,7 +147,7 @@ If you would like to sponsor an additional feature, [see this page](https://chec ## Contributing -We are [Alex](http://github.com/ajhollid) (team lead), [Mohadeseh](http://github.com/mohicody), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) helping individuals and businesses monitor their infra and servers. +We are [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Owaise](http://github.com/Owaiseimdad), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) and [Karen](https://github.com/karenvicent) 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 7000+ stars and attracted 90+ contributors from around the globe. diff --git a/client/package-lock.json b/client/package-lock.json index f58f56f44..32446c5d0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -21,7 +21,7 @@ "dayjs": "1.11.13", "flag-icons": "7.3.2", "html2canvas": "^1.4.1", - "i18next": "^24.2.2", + "i18next": "25.4.2", "joi": "17.13.3", "mui-color-input": "^6.0.0", "react": "18.3.1", @@ -46,9 +46,6 @@ "eslint-plugin-react-refresh": "^0.4.6", "prettier": "^3.3.3", "vite": "6.3.5" - }, - "optionalDependencies": { - "@rollup/rollup-linux-arm64-musl": "4.41.0" } }, "node_modules/@ampproject/remapping": { @@ -283,9 +280,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz", - "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", + "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -491,262 +488,6 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", @@ -763,134 +504,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", - "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", - "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -2063,123 +1676,6 @@ } } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", - "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", - "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", - "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", - "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", - "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", - "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", - "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", - "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", - "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", @@ -2193,71 +1689,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", - "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", - "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", - "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", - "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", - "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", @@ -2284,45 +1715,6 @@ "linux" ] }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", - "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", - "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", - "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -4342,14 +3734,15 @@ } }, "node_modules/form-data": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", - "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" }, "engines": { @@ -4363,20 +3756,6 @@ "dev": true, "license": "ISC" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4690,9 +4069,9 @@ } }, "node_modules/i18next": { - "version": "24.2.3", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz", - "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==", + "version": "25.4.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.4.2.tgz", + "integrity": "sha512-gD4T25a6ovNXsfXY1TwHXXXLnD/K2t99jyYMCSimSCBnBRJVQr5j+VAaU83RJCPzrTGhVQ6dqIga66xO2rtd5g==", "funding": [ { "type": "individual", @@ -4709,7 +4088,7 @@ ], "license": "MIT", "dependencies": { - "@babel/runtime": "^7.26.10" + "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" diff --git a/client/package.json b/client/package.json index 611ba5093..92da6fd13 100644 --- a/client/package.json +++ b/client/package.json @@ -26,7 +26,7 @@ "dayjs": "1.11.13", "flag-icons": "7.3.2", "html2canvas": "^1.4.1", - "i18next": "^24.2.2", + "i18next": "25.4.2", "joi": "17.13.3", "mui-color-input": "^6.0.0", "react": "18.3.1", @@ -59,8 +59,5 @@ "eslint-plugin-react-refresh": "^0.4.6", "prettier": "^3.3.3", "vite": "6.3.5" - }, - "optionalDependencies": { - "@rollup/rollup-linux-arm64-musl": "4.41.0" } } diff --git a/client/src/Components/Charts/StatusPageBarChart/index.jsx b/client/src/Components/Charts/StatusPageBarChart/index.jsx index e912934a0..eab0c7d1d 100644 --- a/client/src/Components/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/Charts/StatusPageBarChart/index.jsx @@ -1,7 +1,7 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; import { formatDateWithTz } from "../../../Utils/timeUtils"; -import { useEffect, useState } from "react"; +import { useEffect, useState, forwardRef } from "react"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; @@ -17,26 +17,32 @@ import PropTypes from "prop-types"; * @returns {JSX.Element} The Bar component. */ -const Bar = ({ width, height, backgroundColor, borderRadius, children }) => { - const theme = useTheme(); +const Bar = forwardRef( + ({ width, height, backgroundColor, borderRadius, children, ...otherProps }, ref) => { + const theme = useTheme(); - return ( - - {children} - - ); -}; + return ( + + {children} + + ); + } +); + +Bar.displayName = "Bar"; Bar.propTypes = { - width: PropTypes.string.isRequired, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired, height: PropTypes.string.isRequired, backgroundColor: PropTypes.string.isRequired, borderRadius: PropTypes.string, diff --git a/client/src/Components/Charts/Utils/chartUtilFunctions.js b/client/src/Components/Charts/Utils/chartUtilFunctions.js index d8ec6cdb6..2851af93e 100644 --- a/client/src/Components/Charts/Utils/chartUtilFunctions.js +++ b/client/src/Components/Charts/Utils/chartUtilFunctions.js @@ -1,5 +1,6 @@ export const tooltipDateFormatLookup = (dateRange) => { const dateFormatLookup = { + recent: "ddd. MMMM D, YYYY, hh:mm A", day: "ddd. MMMM D, YYYY, hh:mm A", week: "ddd. MMMM D, YYYY, hh:mm A", month: "ddd. MMMM D, YYYY", diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/Charts/Utils/chartUtils.jsx index 414e1ff9c..e24881c1c 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/Charts/Utils/chartUtils.jsx @@ -88,7 +88,51 @@ PercentTick.propTypes = { */ const getFormattedPercentage = (value) => { if (typeof value !== "number") return value; - return `${(value * 100).toFixed(2)}.%`; + return `${(value * 100).toFixed(2)}%`; +}; + +/** + * Custom tick component for rendering network bytes per second. + * + * @param {Object} props - The properties object. + * @param {number} props.x - The x-coordinate for the tick. + * @param {number} props.y - The y-coordinate for the tick. + * @param {Object} props.payload - The payload object containing tick data. + * @param {number} props.index - The index of the tick. + * @returns {JSX.Element|null} The rendered tick component or null for the first tick. + */ +export const NetworkTick = ({ x, y, payload, index, formatter }) => { + const theme = useTheme(); + if (index === 0) return null; + + if (formatter === undefined) { + formatter = (value, space = false) => { + if (typeof value !== "number") return value; + // need to add space between value and unit + return `${(value / 1024).toFixed(1)}${space ? " " : ""}Kbps`; + }; + } + + return ( + + {formatter(payload?.value, true)} + + ); +}; + +NetworkTick.propTypes = { + x: PropTypes.number, + y: PropTypes.number, + payload: PropTypes.object, + index: PropTypes.number, + formatter: PropTypes.func, }; /** @@ -112,6 +156,7 @@ export const InfrastructureTooltip = ({ yLabel, dotColor, dateRange, + formatter = getFormattedPercentage, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); const theme = useTheme(); @@ -166,8 +211,8 @@ export const InfrastructureTooltip = ({ sx={{ opacity: 0.8 }} > {yIdx >= 0 - ? `${yLabel} ${getFormattedPercentage(payload[0].payload[hardwareType][yIdx][metric])}` - : `${yLabel} ${getFormattedPercentage(payload[0].payload[yKey])}`} + ? `${yLabel} ${formatter(payload[0].payload[hardwareType][yIdx][metric])}` + : `${yLabel} ${formatter(payload[0].payload[yKey])}`} diff --git a/client/src/Components/Check/Check.jsx b/client/src/Components/Check/Check.jsx index 7f37a4dc1..02b9fd78a 100644 --- a/client/src/Components/Check/Check.jsx +++ b/client/src/Components/Check/Check.jsx @@ -65,7 +65,7 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => }; Check.propTypes = { - text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]).isRequired, + text: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), noHighlightText: PropTypes.string, variant: PropTypes.oneOf(["info", "error", "success"]), outlined: PropTypes.bool, diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/Dialog/genericDialog.jsx index b2cbe1299..a13eba849 100644 --- a/client/src/Components/Dialog/genericDialog.jsx +++ b/client/src/Components/Dialog/genericDialog.jsx @@ -2,7 +2,7 @@ import { useId } from "react"; import PropTypes from "prop-types"; import { Modal, Stack, Typography } from "@mui/material"; -const GenericDialog = ({ title, description, open, onClose, theme, children }) => { +const GenericDialog = ({ title, description, open, onClose, theme, children, width }) => { const titleId = useId(); const descriptionId = useId(); const ariaDescribedBy = description?.length > 0 ? descriptionId : ""; @@ -16,6 +16,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = > {title} @@ -46,6 +48,7 @@ const GenericDialog = ({ title, description, open, onClose, theme, children }) = {description} @@ -64,6 +67,7 @@ GenericDialog.propTypes = { theme: PropTypes.object.isRequired, children: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.node), PropTypes.node]) .isRequired, + width: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.object]), }; export { GenericDialog }; diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/Fallback/FallbackBackground.jsx index 00ecafb4e..fe46f4064 100644 --- a/client/src/Components/Fallback/FallbackBackground.jsx +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -1,7 +1,6 @@ import { useTheme } from "@emotion/react"; import Box from "@mui/material/Box"; import Background from "../../assets/Images/background-grid.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; import OutputAnimation from "../../assets/Animations/output.gif"; import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; import { useSelector } from "react-redux"; @@ -13,7 +12,7 @@ const FallbackBackground = () => { { + const theme = useTheme(); + const { t } = useTranslation(); + const isAdmin = useIsAdmin(); + const isEmpty = items === null || (Array.isArray(items) && items.length === 0); + const hasInitialized = items !== undefined && !(isLoading && isEmpty); + const showEmpty = !isLoading && isEmpty; + if (networkError) { + return ( + + + {t("common.toasts.networkError")} + + {t("common.toasts.checkConnection")} + + ); + } + if (!hasInitialized) { + return null; + } + if (showEmpty) { + return ( + + {fallbackChildren} + + ); + } + return <>{children}; +}; + +PageStateWrapper.propTypes = { + networkError: PropTypes.any, + isLoading: PropTypes.bool, + items: PropTypes.oneOfType([PropTypes.array, PropTypes.oneOf([null])]), + type: PropTypes.string, + fallbackLink: PropTypes.string, + fallbackChildren: PropTypes.node, + children: PropTypes.node, +}; + +export default PageStateWrapper; diff --git a/client/src/Components/StarPrompt/index.jsx b/client/src/Components/StarPrompt/index.jsx index 64fce1d3f..f5f130387 100644 --- a/client/src/Components/StarPrompt/index.jsx +++ b/client/src/Components/StarPrompt/index.jsx @@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) {}, }, }) => { @@ -101,24 +103,54 @@ const DataTable = ({ data.map((row) => { const key = row.id || row._id || Math.random(); return ( - config.onRowClick(row) : null} + followCursor + enterDelay={500} + enterNextDelay={500} + title={ + typeof config.tooltipContent === "function" + ? config.tooltipContent(row) + : config.tooltipContent + } + slotProps={{ + tooltip: { + sx: { + background: "unset", + }, + }, + popper: { + modifiers: [ + { + name: "offset", + options: { + offset: ({ popper }) => { + return [popper.width / 2 + 20, -popper.height / 8]; + }, + }, + }, + ], + }, + }} > - {headers.map((header, index) => { - return ( - header.onClick(e, row) : null} - sx={header.getCellSx ? header.getCellSx(row) : {}} - > - {header.render(row)} - - ); - })} - + config.onRowClick(row) : null} + > + {headers.map((header, index) => { + return ( + header.onClick(e, row) : null} + sx={header.getCellSx ? header.getCellSx(row) : {}} + > + {header.render(row)} + + ); + })} + + ); }) )} diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/inviteHooks.js index 705906efa..fb577272a 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -14,25 +14,25 @@ const useGetInviteToken = () => { const clearToken = () => { setToken(undefined); }; - + const fetchToken = async (email, role) => { + const response = await networkService.requestInvitationToken({ email, role }); + const token = response?.data?.data?.token; + if (typeof token === "undefined") { + throw new Error(t("inviteNoTokenFound")); + } + return token; + }; const getInviteToken = async ({ email, role }) => { try { - const response = await networkService.requestInvitationToken({ - email, - role, - }); - const token = response?.data?.data?.token; - if (typeof token === "undefined") { - throw new Error(t("inviteNoTokenFound")); - } - + setIsLoading(true); + const token = await fetchToken(email, role); let inviteLink = token; if (typeof CLIENT_HOST !== "undefined") { inviteLink = `${CLIENT_HOST}/register/${token}`; } - setToken(inviteLink); + return token; } catch (error) { setError(error); } finally { @@ -40,7 +40,26 @@ const useGetInviteToken = () => { } }; - return [getInviteToken, clearToken, isLoading, error, token]; + const addTeamMember = async (formData, role) => { + try { + setIsLoading(true); + const token = await fetchToken(formData.email, role); + const toSubmit = { + ...formData, + inviteToken: token, + }; + delete toSubmit.confirm; + const responseRegister = await networkService.registerUser(toSubmit); + return responseRegister; + } catch (error) { + setError(error); + throw error; + } finally { + setIsLoading(false); + } + }; + + return [getInviteToken, clearToken, isLoading, error, token, addTeamMember]; }; export { useGetInviteToken }; diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/logHooks.js index b954efb4b..989870eea 100644 --- a/client/src/Hooks/logHooks.js +++ b/client/src/Hooks/logHooks.js @@ -15,9 +15,6 @@ const useFetchLogs = () => { setIsLoading(true); const response = await networkService.getLogs(); setLogs(response.data.data); - createToast({ - body: t("logsPage.toast.fetchLogsSuccess"), - }); } catch (error) { setError(error); createToast({ diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 970b5d97f..eb1e7acdc 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -369,6 +369,8 @@ const useUpdateMonitor = () => { setIsLoading(true); const updatedFields = { name: monitor.name, + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, description: monitor.description, interval: monitor.interval, notifications: monitor.notifications, diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 7feebd44c..539725958 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -34,7 +34,7 @@ const useCreateNotification = () => { const useGetNotificationsByTeamId = (updateTrigger) => { const [notifications, setNotifications] = useState([]); - const [isLoading, setIsLoading] = useState(false); + const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const { t } = useTranslation(); diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx new file mode 100644 index 000000000..ab1ba599d --- /dev/null +++ b/client/src/Pages/Account/components/AddMemberMenu/index.jsx @@ -0,0 +1,68 @@ +import { useState } from "react"; +import Button from "@mui/material/Button"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { useTheme } from "@emotion/react"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import { useTranslation } from "react-i18next"; +import Proptypes from "prop-types"; + +const AddMemberMenu = ({ handleInviteOpen, handleIsRegisterOpen }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + const { t } = useTranslation(); + const theme = useTheme(); + const handleClick = (event) => { + setAnchorEl(event.currentTarget); + }; + const handleClose = () => { + setAnchorEl(null); + }; + + return ( + <> + + + { + handleClose(); + handleInviteOpen(); + }} + > + {t("teamPanel.inviteTeamMember")} + + { + handleClose(); + handleIsRegisterOpen(true); + }} + > + {t("teamPanel.register")} + + + + ); +}; + +AddMemberMenu.propTypes = { + handleInviteOpen: Proptypes.func.isRequired, + handleIsRegisterOpen: Proptypes.func.isRequired, +}; + +export default AddMemberMenu; diff --git a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx new file mode 100644 index 000000000..e42c504b2 --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx @@ -0,0 +1,48 @@ +import { useState } from "react"; +import { newOrChangedCredentials } from "../../../../../Validation/validation"; +import { useTranslation } from "react-i18next"; +const useAddTeamMember = () => { + const { t } = useTranslation(); + const [errors, setErrors] = useState({}); + + const clearErrors = () => setErrors({}); + + const validateFields = (name, value, formData) => { + const { error } = newOrChangedCredentials.validate( + { [name]: value }, + { abortEarly: false, context: { password: formData.password } } + ); + + setErrors((prev) => ({ + ...prev, + [name]: error?.details?.[0]?.message || "", + })); + }; + + const validateForm = (formData, role) => { + const { error } = newOrChangedCredentials.validate(formData, { + abortEarly: false, + context: { password: formData.password }, + }); + const formErrors = {}; + if (error) { + for (const err of error.details) { + formErrors[err.path[0]] = err.message; + } + } + if (!role[0] || role.length === 0) { + formErrors.role = t( + "teamPanel.registerTeamMember.auth.common.inputs.role.errors.empty" + ); + } + if (Object.keys(formErrors).length > 0) { + setErrors(formErrors); + return false; + } + setErrors({}); + return true; + }; + + return { errors, setErrors, clearErrors, validateFields, validateForm }; +}; +export default useAddTeamMember; diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx new file mode 100644 index 000000000..2df8fb226 --- /dev/null +++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx @@ -0,0 +1,213 @@ +import { Button, Stack } from "@mui/material"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import Select from "../../../../Components/Inputs/Select"; +import { useGetInviteToken } from "../../../../Hooks/inviteHooks"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../../Utils/toastUtils"; +import { useState } from "react"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import useAddTeamMember from "./hooks/useAddTeamMember"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import PropTypes from "prop-types"; + +const INITIAL_FORM_STATE = { + firstName: "", + lastName: "", + email: "", + password: "", + confirm: "", + teamId: "", +}; + +const INITIAL_ROLE_STATE = ["user"]; +const AddTeamMember = ({ handleIsRegisterOpen, isRegisterOpen, onMemberAdded }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { errors, setErrors, clearErrors, validateFields, validateForm } = + useAddTeamMember(); + const { feedback, handlePasswordFeedback } = usePasswordFeedback(); + const [getInviteToken, clearToken, isLoading, error, token, addTeamMember] = + useGetInviteToken(); + const [form, setForm] = useState(INITIAL_FORM_STATE); + const [role, setRole] = useState(INITIAL_ROLE_STATE); + const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + const closeAddMemberModal = () => { + handleIsRegisterOpen(false); + setForm(INITIAL_FORM_STATE); + setRole(INITIAL_ROLE_STATE); + clearErrors(); + clearToken(); + }; + + const onChange = (e) => { + let { name, value } = e.target; + if (name === "email") value = value.toLowerCase(); + const updatedForm = { ...form, [name]: value }; + validateFields(name, value, updatedForm); + setForm(updatedForm); + + if (name === "password" || name === "confirm") { + handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); + } + }; + + const onsubmitAddMember = async (event) => { + event.preventDefault(); + if (!validateForm(form, role)) return; + try { + setIsLoadingSubmit(true); + await addTeamMember(form, role); + createToast({ + body: t("teamPanel.registerToast.success"), + }); + onMemberAdded(); + closeAddMemberModal(); + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || "unknownError"; + createToast({ + type: "error", + body: t(errorMsg), + }); + } finally { + setIsLoadingSubmit(false); + } + }; + const tErr = (key) => (key ? t([`teamPanel.registerTeamMember.${key}`, key]) : ""); + return ( + <> + + + + + + + + setSelectedInterface(e.target.value)} + items={availableInterfaces.map((interfaceName) => ({ + _id: interfaceName, + name: interfaceName, + }))} + sx={{ minWidth: 200 }} + /> + )} + + + + + ); +}; + +Network.propTypes = { + net: PropTypes.array, + checks: PropTypes.array, + isLoading: PropTypes.bool.isRequired, + dateRange: PropTypes.string.isRequired, + setDateRange: PropTypes.func.isRequired, +}; + +export default Network; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx new file mode 100644 index 000000000..52fcfad87 --- /dev/null +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx @@ -0,0 +1,56 @@ +import { + Card, + CardContent, + Skeleton, + Table, + TableHead, + TableRow, + TableCell, + TableBody, +} from "@mui/material"; + +const SkeletonLayout = () => { + return ( + + + + + + + Name + Bytes Sent + Bytes Received + Packets Sent + Packets Received + Errors In + Errors Out + Drops In + Drops Out + + + + {Array.from({ length: 5 }).map((_, idx) => ( + + {Array.from({ length: 9 }).map((__, colIdx) => ( + + + + ))} + + ))} + +
+
+
+ ); +}; + +export default SkeletonLayout; diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx index a2e4c5153..8edb6fb87 100644 --- a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx +++ b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx @@ -57,7 +57,7 @@ const useHardwareUtils = () => { if (GB >= 1) { return ( <> - {Number(GB.toFixed(0))} + {Number(GB.toFixed(2))} {space ? " " : ""} {t("gb")} @@ -65,7 +65,7 @@ const useHardwareUtils = () => { } else { return ( <> - {Number(MB.toFixed(0))} + {Number(MB.toFixed(2))} {space ? " " : ""} {t("mb")} @@ -73,6 +73,53 @@ const useHardwareUtils = () => { } }; + const formatBytesPerSecondString = (bytesPerSec, space = false) => { + if ( + bytesPerSec === undefined || + bytesPerSec === null || + typeof bytesPerSec !== "number" || + bytesPerSec === 0 + ) { + return `0${space ? " " : ""}B/s`; + } + + const GB = bytesPerSec / (1024 * 1024 * 1024); + const MB = bytesPerSec / (1024 * 1024); + const KB = bytesPerSec / 1024; + + if (GB >= 1) { + return `${Number(GB.toFixed(1))}${space ? " " : ""}GB/s`; + } else if (MB >= 1) { + return `${Number(MB.toFixed(1))}${space ? " " : ""}MB/s`; + } else if (KB >= 1) { + return `${Number(KB.toFixed(1))}${space ? " " : ""}KB/s`; + } else { + return `${Number(bytesPerSec.toFixed(1))}${space ? " " : ""}B/s`; + } + }; + + const formatPacketsPerSecondString = (packetsPerSec, space = false) => { + if ( + packetsPerSec === undefined || + packetsPerSec === null || + typeof packetsPerSec !== "number" || + packetsPerSec === 0 + ) { + return `0${space ? " " : ""}pps`; + } + + const M = packetsPerSec / (1000 * 1000); + const K = packetsPerSec / 1000; + + if (M >= 1) { + return `${Number(M.toFixed(1))}${space ? " " : ""}Mpps`; + } else if (K >= 1) { + return `${Number(K.toFixed(1))}${space ? " " : ""}Kpps`; + } else { + return `${Math.round(packetsPerSec)}${space ? " " : ""}pps`; + } + }; + /** * Converts a decimal value to a percentage * @@ -134,6 +181,8 @@ const useHardwareUtils = () => { decimalToPercentage, buildTemps, getDimensions, + formatBytesPerSecondString, + formatPacketsPerSecondString, }; }; diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx index 30840851f..a16f3906b 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -1,5 +1,5 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack, Typography, Tab } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; @@ -7,6 +7,9 @@ import StatusBoxes from "./Components/StatusBoxes"; import GaugeBoxes from "./Components/GaugeBoxes"; import AreaChartBoxes from "./Components/AreaChartBoxes"; import GenericFallback from "../../../Components/GenericFallback"; +import NetworkStats from "./Components/NetworkStats"; +import CustomTabList from "../../../Components/Tab"; +import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; @@ -22,11 +25,10 @@ const BREADCRUMBS = [ { name: "details", path: "" }, ]; const InfrastructureDetails = () => { - // Redux state - // Local state const [dateRange, setDateRange] = useState("recent"); const [trigger, setTrigger] = useState(false); + const [tab, setTab] = useState("details"); // Utils const theme = useTheme(); @@ -87,23 +89,52 @@ const InfrastructureDetails = () => { monitor={monitor} triggerUpdate={triggerUpdate} /> - - - - + + setTab(v)} + > + + + + {tab === "details" && ( + <> + + + + + + )} + {tab === "network" && ( + + )} +
); }; diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index f7ca892c2..bfd2270fe 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -1,12 +1,11 @@ // Components -import { Stack, Typography } from "@mui/material"; +import { Stack } from "@mui/material"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; import MonitorsTable from "./Components/MonitorsTable"; import Pagination from "../../..//Components/Table/TablePagination"; -import GenericFallback from "../../../Components/GenericFallback"; -import Fallback from "../../../Components/Fallback"; +import PageStateWrapper from "../../../Components/PageStateWrapper"; import Filter from "./Components/Filters"; import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent"; // Utils @@ -81,75 +80,58 @@ const InfrastructureMonitors = () => { updateTrigger, }); - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (!isLoading && typeof summary?.totalMonitors === "undefined") { - return ( - - ); - } - return ( - - - + - - - - - + items={monitors} + type="infrastructureMonitor" + fallbackLink="/infrastructure/create" + > + + + + + + + + - - - + + + + + ); }; diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx index b65f9c0fd..b44b3fd1a 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -51,16 +51,18 @@ const Logs = () => { ]; return ( - - {t("logsPage.description")} - - {t("logsPage.logLevelSelect.title")} { + const newValue = getValueById(configSelection, event.target.value); + onChange(newValue); + }} + items={configSelection} + {...props} + /> + ); +}; + +ConfigSelect.propTypes = { + configSelection: PropTypes.array.isRequired, + valueSelect: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, +}; +export default ConfigSelect; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx new file mode 100644 index 000000000..84333a14f --- /dev/null +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx @@ -0,0 +1,64 @@ +import { networkService } from "../../../../main"; +import dayjs from "dayjs"; +import { + MS_PER_SECOND, + MS_PER_MINUTE, + MS_PER_HOUR, + MS_PER_DAY, + MS_PER_WEEK, +} from "../../../../Utils/timeUtils"; + +const useMaintenanceActions = () => { + const MS_LOOKUP = { + seconds: MS_PER_SECOND, + minutes: MS_PER_MINUTE, + hours: MS_PER_HOUR, + days: MS_PER_DAY, + weeks: MS_PER_WEEK, + }; + const REPEAT_LOOKUP = { + none: 0, + daily: MS_PER_DAY, + weekly: MS_PER_DAY * 7, + }; + const handleSubmitForm = async (maintenanceWindowId, form) => { + const start = dayjs(form.startDate) + .set("hour", form.startTime.hour()) + .set("minute", form.startTime.minute()); + + const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; + const durationInMs = form.duration * MS_MULTIPLIER; + const end = start.add(durationInMs); + + const repeat = REPEAT_LOOKUP[form.repeat]; + + const submit = { + monitors: form.monitors.map((monitor) => monitor._id), + name: form.name, + start: start.toISOString(), + end: end.toISOString(), + repeat, + }; + + if (repeat === 0) { + submit.expiry = end; + } + + const requestConfig = { maintenanceWindow: submit }; + + if (maintenanceWindowId !== undefined) { + requestConfig.maintenanceWindowId = maintenanceWindowId; + } + const request = + maintenanceWindowId === undefined + ? networkService.createMaintenanceWindow(requestConfig) + : networkService.editMaintenanceWindow(requestConfig); + return request; + }; + + return { + handleSubmitForm, + }; +}; + +export default useMaintenanceActions; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx new file mode 100644 index 000000000..8ba9b5e80 --- /dev/null +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -0,0 +1,74 @@ +import { networkService } from "../../../../main"; +import dayjs from "dayjs"; +import { + MS_PER_SECOND, + MS_PER_MINUTE, + MS_PER_HOUR, + MS_PER_DAY, + MS_PER_WEEK, +} from "../../../../Utils/timeUtils"; +const useMaintenanceData = () => { + const REVERSE_REPEAT_LOOKUP = { + 0: "none", + [MS_PER_DAY]: "daily", + [MS_PER_WEEK]: "weekly", + }; + const getDurationAndUnit = (durationInMs) => { + if (durationInMs % MS_PER_DAY === 0) { + return { + duration: (durationInMs / MS_PER_DAY).toString(), + durationUnit: "days", + }; + } else if (durationInMs % MS_PER_HOUR === 0) { + return { + duration: (durationInMs / MS_PER_HOUR).toString(), + durationUnit: "hours", + }; + } else if (durationInMs % MS_PER_MINUTE === 0) { + return { + duration: (durationInMs / MS_PER_MINUTE).toString(), + durationUnit: "minutes", + }; + } else { + return { + duration: (durationInMs / MS_PER_SECOND).toString(), + durationUnit: "seconds", + }; + } + }; + const fetchMonitorsMaintenance = async () => { + const response = await networkService.getMonitorsByTeamId({ + limit: null, + types: ["http", "ping", "pagespeed", "port"], + }); + const fetchedMonitors = response.data.data.monitors; + return fetchedMonitors; + }; + + const initializeMaintenanceForEdit = async (maintenanceWindowId, monitorList) => { + const res = await networkService.getMaintenanceWindowById({ + maintenanceWindowId: maintenanceWindowId, + }); + const maintenanceWindow = res.data.data; + const { name, start, end, repeat, monitorId } = maintenanceWindow; + const startTime = dayjs(start); + const endTime = dayjs(end); + const durationInMs = endTime.diff(startTime, "milliseconds").toString(); + const { duration, durationUnit } = getDurationAndUnit(durationInMs); + const monitor = monitorList.find((monitor) => monitor._id === monitorId); + const maintenanceWindowInformation = { + name, + repeat: REVERSE_REPEAT_LOOKUP[repeat], + startDate: startTime, + startTime, + duration, + durationUnit, + monitors: monitor ? [monitor] : [], + }; + return maintenanceWindowInformation; + }; + + return { fetchMonitorsMaintenance, initializeMaintenanceForEdit }; +}; + +export default useMaintenanceData; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 9d1b060c2..9dbf9d4aa 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -10,71 +10,21 @@ import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; import { maintenanceWindowValidation } from "../../../Validation/validation"; import { createToast } from "../../../Utils/toastUtils"; import MonitorList from "./Components/MonitorList"; -import Checkbox from "../../../Components/Inputs/Checkbox"; +import ConfigSelect from "./Components/ConfigSelect"; +import useMaintenanceData from "./hooks/useMaintenanceData"; +import useMaintenanceActions from "./hooks/useMaintenanceActions"; -import dayjs from "dayjs"; -import Select from "../../../Components/Inputs/Select"; import TextInput from "../../../Components/Inputs/TextInput"; import Breadcrumbs from "../../../Components/Breadcrumbs"; import CalendarIcon from "../../../assets/icons/calendar.svg?react"; import "./index.css"; import Search from "../../../Components/Inputs/Search"; -import { networkService } from "../../../main"; import { logger } from "../../../Utils/Logger"; -import { - MS_PER_SECOND, - MS_PER_MINUTE, - MS_PER_HOUR, - MS_PER_DAY, - MS_PER_WEEK, -} from "../../../Utils/timeUtils"; + import { useNavigate, useParams } from "react-router-dom"; import { buildErrors, hasValidationErrors } from "../../../Validation/error"; import { useTranslation } from "react-i18next"; - -const getDurationAndUnit = (durationInMs) => { - if (durationInMs % MS_PER_DAY === 0) { - return { - duration: (durationInMs / MS_PER_DAY).toString(), - durationUnit: "days", - }; - } else if (durationInMs % MS_PER_HOUR === 0) { - return { - duration: (durationInMs / MS_PER_HOUR).toString(), - durationUnit: "hours", - }; - } else if (durationInMs % MS_PER_MINUTE === 0) { - return { - duration: (durationInMs / MS_PER_MINUTE).toString(), - durationUnit: "minutes", - }; - } else { - return { - duration: (durationInMs / MS_PER_SECOND).toString(), - durationUnit: "seconds", - }; - } -}; - -const MS_LOOKUP = { - seconds: MS_PER_SECOND, - minutes: MS_PER_MINUTE, - hours: MS_PER_HOUR, - days: MS_PER_DAY, - weeks: MS_PER_WEEK, -}; - -const REPEAT_LOOKUP = { - none: 0, - daily: MS_PER_DAY, - weekly: MS_PER_DAY * 7, -}; - -const REVERSE_REPEAT_LOOKUP = { - 0: "none", - [MS_PER_DAY]: "daily", - [MS_PER_WEEK]: "weekly", -}; +import dayjs from "dayjs"; const repeatConfig = [ { _id: 0, name: "Don't repeat", value: "none" }, @@ -96,24 +46,10 @@ const durationConfig = [ }, ]; -const getValueById = (config, id) => { - const item = config.find((config) => config._id === id); - return item ? (item.value ? item.value : item.name) : null; -}; - -const getIdByValue = (config, name) => { - const item = config.find((config) => { - if (config.value) { - return config.value === name; - } else { - return config.name === name; - } - }); - return item ? item._id : null; -}; - const CreateMaintenance = () => { const { maintenanceWindowId } = useParams(); + const { handleSubmitForm } = useMaintenanceActions(); + const { fetchMonitorsMaintenance, initializeMaintenanceForEdit } = useMaintenanceData(); const navigate = useNavigate(); const theme = useTheme(); const { t } = useTranslation(); @@ -121,6 +57,7 @@ const CreateMaintenance = () => { const [monitors, setMonitors] = useState([]); const [search, setSearch] = useState(""); const [isLoading, setIsLoading] = useState(false); + const [errors, setErrors] = useState({}); const [form, setForm] = useState({ repeat: "none", startDate: dayjs(), @@ -130,43 +67,35 @@ const CreateMaintenance = () => { name: "", monitors: [], }); - const [errors, setErrors] = useState({}); + const handleFormChange = (key, value) => { + setForm((prev) => ({ ...prev, [key]: value })); + const { error } = maintenanceWindowValidation.validate( + { [key]: value }, + { abortEarly: false } + ); + setErrors((prev) => { + return buildErrors(prev, key, error); + }); + }; useEffect(() => { const fetchMonitors = async () => { setIsLoading(true); try { - const response = await networkService.getMonitorsByTeamId({ - limit: null, - types: ["http", "ping", "pagespeed", "port"], - }); - const monitors = response.data.data.monitors; - setMonitors(monitors); + const fetchedMonitors = await fetchMonitorsMaintenance(); + setMonitors(fetchedMonitors); if (maintenanceWindowId === undefined) { return; } - - const res = await networkService.getMaintenanceWindowById({ - maintenanceWindowId: maintenanceWindowId, - }); - const maintenanceWindow = res.data.data; - const { name, start, end, repeat, monitorId } = maintenanceWindow; - const startTime = dayjs(start); - const endTime = dayjs(end); - const durationInMs = endTime.diff(startTime, "milliseconds").toString(); - const { duration, durationUnit } = getDurationAndUnit(durationInMs); - const monitor = monitors.find((monitor) => monitor._id === monitorId); - setForm({ - ...form, - name, - repeat: REVERSE_REPEAT_LOOKUP[repeat], - startDate: startTime, - startTime, - duration, - durationUnit, - monitors: monitor ? [monitor] : [], - }); + const maintenanceWindowInformation = await initializeMaintenanceForEdit( + maintenanceWindowId, + fetchedMonitors + ); + setForm((prev) => ({ + ...prev, + ...maintenanceWindowInformation, + })); } catch (error) { createToast({ body: "Failed to fetch data" }); logger.error("Failed to fetch monitors", error); @@ -177,90 +106,9 @@ const CreateMaintenance = () => { fetchMonitors(); }, [user]); - const handleSearch = (value) => { - setSearch(value); - }; - - const handleSelectMonitors = (monitors) => { - setForm({ ...form, monitors }); - const { error } = maintenanceWindowValidation.validate( - { monitors }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, "monitors", error); - }); - }; - - const handleFormChange = (key, value) => { - setForm({ ...form, [key]: value }); - const { error } = maintenanceWindowValidation.validate( - { [key]: value }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, key, error); - }); - }; - - const handleTimeChange = (key, newTime) => { - setForm({ ...form, [key]: newTime }); - const { error } = maintenanceWindowValidation.validate( - { [key]: newTime }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, key, error); - }); - }; - - const handleMonitorsChange = (selected) => { - setForm((prev) => ({ ...prev, monitors: selected })); - const { error } = maintenanceWindowValidation.validate( - { monitors: selected }, - { abortEarly: false } - ); - setErrors((prev) => { - return buildErrors(prev, "monitors", error); - }); - }; - const handleSubmit = async () => { if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; - // Build timestamp for maintenance window from startDate and startTime - const start = dayjs(form.startDate) - .set("hour", form.startTime.hour()) - .set("minute", form.startTime.minute()); - // Build end timestamp for maintenance window - const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit]; - const durationInMs = form.duration * MS_MULTIPLIER; - const end = start.add(durationInMs); - - // Get repeat value in milliseconds - const repeat = REPEAT_LOOKUP[form.repeat]; - - const submit = { - monitors: form.monitors.map((monitor) => monitor._id), - name: form.name, - start: start.toISOString(), - end: end.toISOString(), - repeat, - }; - - if (repeat === 0) { - submit.expiry = end; - } - - const requestConfig = { maintenanceWindow: submit }; - - if (maintenanceWindowId !== undefined) { - requestConfig.maintenanceWindowId = maintenanceWindowId; - } - const request = - maintenanceWindowId === undefined - ? networkService.createMaintenanceWindow(requestConfig) - : networkService.editMaintenanceWindow(requestConfig); - + const request = handleSubmitForm(maintenanceWindowId, form); try { setIsLoading(true); await request; @@ -353,18 +201,13 @@ const CreateMaintenance = () => { error={errors["name"] ? true : false} helperText={errors["name"]} /> - { - handleFormChange( - "durationUnit", - getValueById(durationConfig, event.target.value) - ); - }} + valueSelect={form.durationUnit} + configSelection={durationConfig} + onChange={(value) => handleFormChange("durationUnit", value)} error={errors["durationUnit"]} /> @@ -545,7 +383,9 @@ const CreateMaintenance = () => { inputValue={search} value={form.monitors} handleInputChange={setSearch} - handleChange={handleMonitorsChange} + handleChange={(selected) => { + handleFormChange("monitors", selected); + }} error={errors["monitors"]} /> { const theme = useTheme(); const { t } = useTranslation(); const navigate = useNavigate(); const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5); - const isAdmin = useIsAdmin(); - const [maintenanceWindows, setMaintenanceWindows] = useState([]); + const [maintenanceWindows, setMaintenanceWindows] = useState(undefined); const [maintenanceWindowCount, setMaintenanceWindowCount] = useState(0); const [page, setPage] = useState(0); const [sort, setSort] = useState({}); const [updateTrigger, setUpdateTrigger] = useState(false); const [networkError, setNetworkError] = useState(false); - const [isDataFetched, setIsDataFetched] = useState(false); + const [isLoading, setIsLoading] = useState(true); const handleActionMenuDelete = () => { setUpdateTrigger((prev) => !prev); @@ -34,80 +30,65 @@ const Maintenance = () => { useEffect(() => { const fetchMaintenanceWindows = async () => { try { + setNetworkError(false); + setIsLoading(true); const response = await networkService.getMaintenanceWindowsByTeamId({ page: page, rowsPerPage: rowsPerPage, }); const { maintenanceWindows, maintenanceWindowCount } = response.data.data; - setMaintenanceWindows(maintenanceWindows); - setMaintenanceWindowCount(maintenanceWindowCount); + setMaintenanceWindows(maintenanceWindows ?? []); + setMaintenanceWindowCount(maintenanceWindowCount ?? 0); } catch (error) { setNetworkError(true); } finally { - setIsDataFetched(true); + setIsLoading(false); } }; fetchMaintenanceWindows(); }, [page, rowsPerPage, updateTrigger]); - if (networkError) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - // Only show the fallback if we've fetched data and there are no maintenance windows - if (isDataFetched && maintenanceWindows.length === 0) { - return ( - - ); - } - return ( - - + - - - - - + + + + + + + + + ); }; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 10c280efb..6d8afe665 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -89,8 +89,6 @@ const CreateNotifications = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - console.log(JSON.stringify(newErrors)); - console.log(JSON.stringify(form, null, 2)); createToast({ body: Object.values(newErrors)[0] }); setErrors(newErrors); return; diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx index f9fc2e674..910fbf1fc 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -4,11 +4,10 @@ import Typography from "@mui/material/Typography"; import Breadcrumbs from "../../Components/Breadcrumbs"; import Button from "@mui/material/Button"; import DataTable from "../../Components/Table"; -import Fallback from "../../Components/Fallback"; import ActionMenu from "./components/ActionMenu"; +import PageStateWrapper from "../../Components/PageStateWrapper"; // Utils -import { useIsAdmin } from "../../Hooks/useIsAdmin"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; @@ -23,7 +22,6 @@ const Notifications = () => { const theme = useTheme(); const BREADCRUMBS = [{ name: "notifications", path: "/notifications" }]; const [updateTrigger, setUpdateTrigger] = useState(false); - const isAdmin = useIsAdmin(); const [notifications, isLoading, error] = useGetNotificationsByTeamId(updateTrigger); const [deleteNotification, isDeleting, deleteError] = useDeleteNotification(); const { t } = useTranslation(); @@ -76,49 +74,47 @@ const Notifications = () => { }, ]; - if (notifications?.length === 0) { - return ( - - ); - } - return ( - - - + - - - {t("notifications.createTitle")} - navigate(`/notifications/${row._id}`), - rowSX: { - cursor: "pointer", - "&:hover td": { - backgroundColor: theme.palette.tertiary.main, - transition: "background-color .3s ease", - }, - }, - }} - headers={headers} - data={notifications} - /> - + + + + + + {t("notifications.createTitle")} + navigate(`/notifications/${row._id}`), + rowSX: { + cursor: "pointer", + "&:hover td": { + backgroundColor: theme.palette.tertiary.main, + transition: "background-color .3s ease", + }, + }, + }} + headers={headers} + data={notifications} + /> + + + ); }; diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 7ef6e5a60..0813f6868 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [ export const TITLE_MAP = { email: "createNotifications.emailSettings.title", slack: "createNotifications.slackSettings.title", - pager_duty: "createNotifications.pagerDutySettings.title", + pager_duty: "createNotifications.pagerdutySettings.title", webhook: "createNotifications.webhookSettings.title", discord: "createNotifications.discordSettings.title", }; @@ -17,7 +17,7 @@ export const TITLE_MAP = { export const DESCRIPTION_MAP = { email: "createNotifications.emailSettings.description", slack: "createNotifications.slackSettings.description", - pager_duty: "createNotifications.pagerDutySettings.description", + pager_duty: "createNotifications.pagerdutySettings.description", webhook: "createNotifications.webhookSettings.description", discord: "createNotifications.discordSettings.description", }; @@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = { export const LABEL_MAP = { email: "createNotifications.emailSettings.emailLabel", slack: "createNotifications.slackSettings.webhookLabel", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel", webhook: "createNotifications.webhookSettings.webhookLabel", discord: "createNotifications.discordSettings.webhookLabel", }; @@ -33,7 +33,7 @@ export const LABEL_MAP = { export const PLACEHOLDER_MAP = { email: "createNotifications.emailSettings.emailPlaceholder", slack: "createNotifications.slackSettings.webhookPlaceholder", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder", webhook: "createNotifications.webhookSettings.webhookPlaceholder", discord: "createNotifications.discordSettings.webhookPlaceholder", }; diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 78709093a..46bc7f977 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -152,7 +152,7 @@ const PageSpeedSetup = () => { }); const { error } = monitorValidation.validate( - { [name]: value }, + { [name]: value, type: monitor.type }, { abortEarly: false } ); setErrors((prev) => ({ diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx index 010716b88..18ca0733c 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx @@ -29,7 +29,7 @@ const PieChartLegend = ({ audits }) => { : theme.palette.tertiary.main; // Find the position where the number ends and the unit begins - const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); + const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); let value; let unit; if (match) { diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx index dff28e9cb..b5b13ba6e 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/index.jsx @@ -1,18 +1,16 @@ // Components import { useState } from "react"; import Breadcrumbs from "../../../Components/Breadcrumbs"; -import { Stack, Typography } from "@mui/material"; +import { Stack } from "@mui/material"; import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; import MonitorCountHeader from "../../../Components/MonitorCountHeader"; import MonitorGrid from "./Components/MonitorGrid"; -import Fallback from "../../../Components/Fallback"; -import GenericFallback from "../../../Components/GenericFallback"; +import PageStateWrapper from "../../../Components/PageStateWrapper"; import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning"; // Utils import { useTheme } from "@emotion/react"; import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useTranslation } from "react-i18next"; import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks"; import { useFetchSettings } from "../../../Hooks/settingsHooks"; // Constants @@ -20,7 +18,6 @@ const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; const TYPES = ["pagespeed"]; const PageSpeed = () => { const theme = useTheme(); - const { t } = useTranslation(); const isAdmin = useIsAdmin(); const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({ @@ -40,56 +37,42 @@ const PageSpeed = () => { setIsEmailPasswordSet: () => {}, }); - if (networkError === true) { - return ( - - - {t("common.toasts.networkError")} - - {t("common.toasts.checkConnection")} - - ); - } - - if (!isLoading && monitors?.length === 0) { - return ( - - {isAdmin && settingsData && !settingsData.pagespeedApiKey && ( - - )} - - ); - } - return ( - - - + - - - + items={monitors} + type="pageSpeed" + fallbackLink="/pagespeed/create" + fallbackChildren={ + isAdmin && + settingsData && + !settingsData.pagespeedApiKey && ( + + ) + } + > + + + + + + + + ); }; diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx index 1ce2e8c73..24c49cad1 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/Settings/SettingsURL.jsx @@ -29,7 +29,7 @@ const SettingsURL = ({ HEADING_SX, handleChange, showURL = false }) => {